summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/source-map-service.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/framework/source-map-service.js')
-rw-r--r--devtools/client/framework/source-map-service.js209
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;
+}