diff options
Diffstat (limited to 'devtools/client/framework/source-map-service.js')
-rw-r--r-- | devtools/client/framework/source-map-service.js | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/devtools/client/framework/source-map-service.js b/devtools/client/framework/source-map-service.js new file mode 100644 index 000000000..838adc392 --- /dev/null +++ b/devtools/client/framework/source-map-service.js @@ -0,0 +1,209 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Task } = require("devtools/shared/task"); +const EventEmitter = require("devtools/shared/event-emitter"); +const { LocationStore, serialize, deserialize } = require("./location-store"); + +/** + * A manager class that wraps a TabTarget and listens to source changes + * from source maps and resolves non-source mapped locations to the source mapped + * versions and back and forth, and creating smart elements with a location that + * auto-update when the source changes (from pretty printing, source maps loading, etc) + * + * @param {TabTarget} target + */ + +function SourceMapService(target) { + this._target = target; + this._locationStore = new LocationStore(); + this._isNotSourceMapped = new Map(); + + EventEmitter.decorate(this); + + this._onSourceUpdated = this._onSourceUpdated.bind(this); + this._resolveLocation = this._resolveLocation.bind(this); + this._resolveAndUpdate = this._resolveAndUpdate.bind(this); + this.subscribe = this.subscribe.bind(this); + this.unsubscribe = this.unsubscribe.bind(this); + this.reset = this.reset.bind(this); + this.destroy = this.destroy.bind(this); + + target.on("source-updated", this._onSourceUpdated); + target.on("navigate", this.reset); + target.on("will-navigate", this.reset); +} + +/** + * Clears the store containing the cached promised locations + */ +SourceMapService.prototype.reset = function () { + // Guard to prevent clearing the store when it is not initialized yet. + if (!this._locationStore) { + return; + } + this._locationStore.clear(); + this._isNotSourceMapped.clear(); +}; + +SourceMapService.prototype.destroy = function () { + this.reset(); + this._target.off("source-updated", this._onSourceUpdated); + this._target.off("navigate", this.reset); + this._target.off("will-navigate", this.reset); + this._target.off("close", this.destroy); + this._target = this._locationStore = this._isNotSourceMapped = null; +}; + +/** + * Sets up listener for the callback to update the FrameView + * and tries to resolve location, if it is source-mappable + * @param location + * @param callback + */ +SourceMapService.prototype.subscribe = function (location, callback) { + // A valid candidate location for source-mapping should have a url and line. + // Abort if there's no `url`, which means it's unsourcemappable anyway, + // like an eval script. + // From previous attempts to source-map locations, we also determine if a location + // is not source-mapped. + if (!location.url || !location.line || this._isNotSourceMapped.get(location.url)) { + return; + } + this.on(serialize(location), callback); + this._locationStore.set(location); + this._resolveAndUpdate(location); +}; + +/** + * Removes the listener for the location and clears cached locations + * @param location + * @param callback + */ +SourceMapService.prototype.unsubscribe = function (location, callback) { + this.off(serialize(location), callback); + // Check to see if the store exists before attempting to clear a location + // Sometimes un-subscribe happens during the destruction cascades and this + // condition is to protect against that. Could be looked into in the future. + if (!this._locationStore) { + return; + } + this._locationStore.clearByURL(location.url); +}; + +/** + * Tries to resolve the location and if successful, + * emits the resolved location + * @param location + * @private + */ +SourceMapService.prototype._resolveAndUpdate = function (location) { + this._resolveLocation(location).then(resolvedLocation => { + // We try to source map the first console log to initiate the source-updated + // event from target. The isSameLocation check is to make sure we don't update + // the frame, if the location is not source-mapped. + if (resolvedLocation && !isSameLocation(location, resolvedLocation)) { + this.emit(serialize(location), location, resolvedLocation); + } + }); +}; + +/** + * Checks if there is existing promise to resolve location, if so returns cached promise + * if not, tries to resolve location and returns a promised location + * @param location + * @return Promise<Object> + * @private + */ +SourceMapService.prototype._resolveLocation = Task.async(function* (location) { + let resolvedLocation; + const cachedLocation = this._locationStore.get(location); + if (cachedLocation) { + resolvedLocation = cachedLocation; + } else { + const promisedLocation = resolveLocation(this._target, location); + if (promisedLocation) { + this._locationStore.set(location, promisedLocation); + resolvedLocation = promisedLocation; + } + } + return resolvedLocation; +}); + +/** + * Checks if the `source-updated` event is fired from the target. + * Checks to see if location store has the source url in its cache, + * if so, tries to update each stale location in the store. + * Determines if the source should be source-mapped or not. + * @param _ + * @param sourceEvent + * @private + */ +SourceMapService.prototype._onSourceUpdated = function (_, sourceEvent) { + let { type, source } = sourceEvent; + + // If we get a new source, and it's not a source map, abort; + // we can have no actionable updates as this is just a new normal source. + // Check Source Actor for sourceMapURL property (after Firefox 48) + // If not present, utilize isSourceMapped and isPrettyPrinted properties + // to estimate if a source is not source-mapped. + const isNotSourceMapped = !(source.sourceMapURL || + source.isSourceMapped || source.isPrettyPrinted); + if (type === "newSource" && isNotSourceMapped) { + this._isNotSourceMapped.set(source.url, true); + return; + } + let sourceUrl = null; + if (source.generatedUrl && source.isSourceMapped) { + sourceUrl = source.generatedUrl; + } else if (source.url && source.isPrettyPrinted) { + sourceUrl = source.url; + } + const locationsToResolve = this._locationStore.getByURL(sourceUrl); + if (locationsToResolve.length) { + this._locationStore.clearByURL(sourceUrl); + for (let location of locationsToResolve) { + this._resolveAndUpdate(deserialize(location)); + } + } +}; + +exports.SourceMapService = SourceMapService; + +/** + * Take a TabTarget and a location, containing a `url`, `line`, and `column`, resolve + * the location to the latest location (so a source mapped location, or if pretty print + * status has been updated) + * + * @param {TabTarget} target + * @param {Object} location + * @return {Promise<Object>} + */ +function resolveLocation(target, location) { + return Task.spawn(function* () { + let newLocation = yield target.resolveLocation({ + url: location.url, + line: location.line, + column: location.column || Infinity + }); + // Source or mapping not found, so don't do anything + if (newLocation.error) { + return null; + } + return newLocation; + }); +} + +/** + * Returns true if the original location and resolved location are the same + * @param location + * @param resolvedLocation + * @returns {boolean} + */ +function isSameLocation(location, resolvedLocation) { + return location.url === resolvedLocation.url && + location.line === resolvedLocation.line && + location.column === resolvedLocation.column; +} |