mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-22 17:55:50 +00:00
210 lines
7.1 KiB
JavaScript
210 lines
7.1 KiB
JavaScript
/* 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;
|
|
}
|