# HG changeset patch # User Jaideep Bhoosreddy # Parent 24530e316b12694eebf9f853d6f7800f7656b579 Bug 670002 - Use source maps in the web console w/ performance issues; r?jsantell diff --git a/devtools/client/framework/location-store.js b/devtools/client/framework/location-store.js new file mode 100644 --- /dev/null +++ b/devtools/client/framework/location-store.js @@ -0,0 +1,103 @@ +/* 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 SOURCE_TOKEN = "<:>"; + +function LocationStore (store) { + this._store = store || new Map(); +} + +/** + * Method to get a promised location from the Store. + * @param location + * @returns Promise + */ +LocationStore.prototype.get = function (location) { + this._safeAccessInit(location.url); + return this._store.get(location.url).get(location); +}; + +/** + * Method to set a promised location to the Store + * @param location + * @param promisedLocation + */ +LocationStore.prototype.set = function (location, promisedLocation = null) { + this._safeAccessInit(location.url); + this._store.get(location.url).set(serialize(location), promisedLocation); +}; + +/** + * Utility method to verify if key exists in Store before accessing it. + * If not, initializing it. + * @param url + * @private + */ +LocationStore.prototype._safeAccessInit = function (url) { + if (!this._store.has(url)) { + this._store.set(url, new Map()); + } +}; + +/** + * Utility proxy method to Map.clear() method + */ +LocationStore.prototype.clear = function () { + this._store.clear(); +}; + +/** + * Retrieves an object containing all locations to be resolved when `source-updated` + * event is triggered. + * @param url + * @returns {Array} + */ +LocationStore.prototype.getByURL = function (url){ + if (this._store.has(url)) { + return [...this._store.get(url).keys()]; + } + return []; +}; + +/** + * Invalidates the stale location promises from the store when `source-updated` + * event is triggered, and when FrameView unsubscribes from a location. + * @param url + */ +LocationStore.prototype.clearByURL = function (url) { + this._safeAccessInit(url); + this._store.set(url, new Map()); +}; + +exports.LocationStore = LocationStore; +exports.serialize = serialize; +exports.deserialize = deserialize; + +/** + * Utility method to serialize the source + * @param source + * @returns {string} + */ +function serialize(source) { + let { url, line, column } = source; + line = line || 0; + column = column || 0; + return `${url}${SOURCE_TOKEN}${line}${SOURCE_TOKEN}${column}`; +}; + +/** + * Utility method to serialize the source + * @param source + * @returns Object + */ +function deserialize(source) { + let [ url, line, column ] = source.split(SOURCE_TOKEN); + line = parseInt(line); + column = parseInt(column); + if (column === 0) { + return { url, line }; + } + return { url, line, column }; +}; diff --git a/devtools/client/framework/moz.build b/devtools/client/framework/moz.build --- a/devtools/client/framework/moz.build +++ b/devtools/client/framework/moz.build @@ -11,21 +11,22 @@ TEST_HARNESS_FILES.xpcshell.devtools.cli DevToolsModules( 'about-devtools-toolbox.js', 'attach-thread.js', 'browser-menus.js', 'devtools-browser.js', 'devtools.js', 'gDevTools.jsm', + 'location-store.js', 'menu-item.js', 'menu.js', 'selection.js', 'sidebar.js', - 'source-location.js', + 'source-map-service.js', 'target-from-url.js', 'target.js', 'toolbox-highlighter-utils.js', 'toolbox-hosts.js', 'toolbox-options.js', 'toolbox.js', 'ToolboxProcess.jsm', ) diff --git a/devtools/client/framework/source-location.js b/devtools/client/framework/source-map-service.js rename from devtools/client/framework/source-location.js rename to devtools/client/framework/source-map-service.js --- a/devtools/client/framework/source-location.js +++ b/devtools/client/framework/source-map-service.js @@ -1,98 +1,171 @@ /* 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 { assert } = require("devtools/shared/DevToolsUtils"); +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 SourceLocationController(target) { - this.target = target; - this.locations = new Set(); + +function SourceMapService(target) { + this._target = target; + this._locationStore = new LocationStore(); + this._isInitialResolve = true; + + 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); target.on("close", this.destroy); } -SourceLocationController.prototype.reset = function () { - this.locations.clear(); +/** + * Clears the store containing the cached resolved locations and promises + */ +SourceMapService.prototype.reset = function () { + this._isInitialResolve = true; + this._locationStore.clear(); }; -SourceLocationController.prototype.destroy = function () { - this.locations.clear(); - 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.locations = null; +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._isInitialResolve = null; + this._target = this._locationStore = null; }; /** - * Add this `location` to be observed and register a callback - * whenever the underlying source is updated. - * - * @param {Object} location - * An object with a {String} url, {Number} line, and optionally - * a {Number} column. - * @param {Function} callback + * Sets up listener for the callback to update the FrameView and tries to resolve location + * @param location + * @param callback */ -SourceLocationController.prototype.bindLocation = function (location, callback) { - assert(location.url, "Location must have a url."); - assert(location.line, "Location must have a line."); - this.locations.add({ location, callback }); +SourceMapService.prototype.subscribe = function (location, callback) { + this.on(serialize(location), callback); + this._locationStore.set(location); + if (this._isInitialResolve) { + this._resolveAndUpdate(location); + this._isInitialResolve = false; + } }; /** - * Called when a new source occurs (a normal source, source maps) or an updated - * source (pretty print) occurs. - * - * @param {String} eventName - * @param {Object} sourceEvent + * Removes the listener for the location and clears cached locations + * @param location + * @param callback */ -SourceLocationController.prototype._onSourceUpdated = function (_, sourceEvent) { +SourceMapService.prototype.unsubscribe = function (location, callback) { + this.off(serialize(location), callback); + this._locationStore.clearByURL(location.url); +}; + +/** + * Tries to resolve the location and if successful, + * emits the resolved location and caches it + * @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) { + if (this._isInitialResolve) { + if (!isSameLocation(location, resolvedLocation)) { + this.emit(serialize(location), location, resolvedLocation); + return; + } + } + this.emit(serialize(location), location, resolvedLocation); + } + }); +}; + +/** + * Validates the location model, + * checks if there is existing promise to resolve location, if so returns cached promise + * if not promised to resolve, + * tries to resolve location and returns a promised location + * @param location + * @return Promise + * @private + */ +SourceMapService.prototype._resolveLocation = Task.async(function* (location) { + // Location must have a url and a line + if (!location.url || !location.line) { + return null; + } + const cachedLocation = this._locationStore.get(location); + if (cachedLocation) { + return cachedLocation; + } else { + const promisedLocation = resolveLocation(this._target, location); + if (promisedLocation) { + this._locationStore.set(location, promisedLocation); + return promisedLocation; + } + } +}); + +/** + * 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. + * @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 ahve no actionable updates as this is just a new normal source. + // we can have no actionable updates as this is just a new normal source. // Also abort if there's no `url`, which means it's unsourcemappable anyway, // like an eval script. if (!source.url || type === "newSource" && !source.isSourceMapped) { return; } - - for (let locationItem of this.locations) { - if (isSourceRelated(locationItem.location, source)) { - this._updateSource(locationItem); + 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)); } } }; -SourceLocationController.prototype._updateSource = Task.async(function* (locationItem) { - let newLocation = yield resolveLocation(this.target, locationItem.location); - if (newLocation) { - let previousLocation = Object.assign({}, locationItem.location); - Object.assign(locationItem.location, newLocation); - locationItem.callback(previousLocation, newLocation); - } -}); +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 @@ -100,38 +173,28 @@ SourceLocationController.prototype._upda */ 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; }); } /** - * Takes a serialized SourceActor form and returns a boolean indicating - * if this source is related to this location, like if a location is a generated source, - * and the source map is loaded subsequently, the new source mapped SourceActor - * will be considered related to this location. Same with pretty printing new sources. - * - * @param {Object} location - * @param {Object} source - * @return {Boolean} + * Returns if the original location and resolved location are the same + * @param location + * @param resolvedLocation + * @returns {boolean} */ -function isSourceRelated(location, source) { - // Mapping location to subsequently loaded source map - return source.generatedUrl === location.url || - // Mapping source map loc to source map - source.url === location.url; -} - -exports.SourceLocationController = SourceLocationController; -exports.resolveLocation = resolveLocation; -exports.isSourceRelated = isSourceRelated; +function isSameLocation(location, resolvedLocation) { + return location.url === resolvedLocation.url && + location.line === resolvedLocation.line && + location.column === resolvedLocation.column; +}; \ No newline at end of file diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js --- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -6,16 +6,17 @@ const MAX_ORDINAL = 99; const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled"; const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight"; const OS_HISTOGRAM = "DEVTOOLS_OS_ENUMERATED_PER_USER"; const OS_IS_64_BITS = "DEVTOOLS_OS_IS_64_BITS_PER_USER"; const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER"; const HTML_NS = "http://www.w3.org/1999/xhtml"; +const { SourceMapService } = require("./source-map-service"); var {Cc, Ci, Cu} = require("chrome"); var promise = require("promise"); var defer = require("devtools/shared/defer"); var Services = require("Services"); var {Task} = require("devtools/shared/task"); var {gDevTools} = require("devtools/client/framework/devtools"); var EventEmitter = require("devtools/shared/event-emitter"); @@ -113,16 +114,19 @@ const ToolboxButtons = exports.ToolboxBu * Type of host that will host the toolbox (e.g. sidebar, window) * @param {object} hostOptions * Options for host specifically */ function Toolbox(target, selectedTool, hostType, hostOptions) { this._target = target; this._toolPanels = new Map(); this._telemetry = new Telemetry(); + if (Services.prefs.getBoolPref("devtools.sourcemap.locations.enabled")) { + this._sourceMapService = new SourceMapService(this._target); + } this._initInspector = null; this._inspector = null; // Map of frames (id => frame-info) and currently selected frame id. this.frameMap = new Map(); this.selectedFrameId = null; @@ -2026,16 +2030,20 @@ Toolbox.prototype = { this.off("ready", this._showDevEditionPromo); gDevTools.off("tool-registered", this._toolRegistered); gDevTools.off("tool-unregistered", this._toolUnregistered); gDevTools.off("pref-changed", this._prefChanged); this._lastFocusedElement = null; + if (this._sourceMapService) { + this._sourceMapService.destroy(); + this._sourceMapService = null; + } if (this.webconsolePanel) { this._saveSplitConsoleHeight(); this.webconsolePanel.removeEventListener("resize", this._saveSplitConsoleHeight); } this.closeButton.removeEventListener("click", this.destroy, true); this.textboxContextMenuPopup.removeEventListener("popupshowing", diff --git a/devtools/client/preferences/devtools.js b/devtools/client/preferences/devtools.js --- a/devtools/client/preferences/devtools.js +++ b/devtools/client/preferences/devtools.js @@ -291,16 +291,19 @@ pref("devtools.webconsole.timestampMessa // Web Console automatic multiline mode: |true| if you want incomplete statements // to automatically trigger multiline editing (equivalent to shift + enter). pref("devtools.webconsole.autoMultiline", true); // Enable the experimental webconsole frontend (work in progress) pref("devtools.webconsole.new-frontend-enabled", false); +// Enable the experimental support for source maps in console (work in progress) +pref("devtools.sourcemap.locations.enabled", false); + // The number of lines that are displayed in the web console. pref("devtools.hud.loglimit", 1000); // The number of lines that are displayed in the web console for the Net, // CSS, JS and Web Developer categories. These defaults should be kept in sync // with DEFAULT_LOG_LIMIT in the webconsole frontend. pref("devtools.hud.loglimit.network", 1000); pref("devtools.hud.loglimit.cssparser", 1000); diff --git a/devtools/client/shared/components/frame.js b/devtools/client/shared/components/frame.js --- a/devtools/client/shared/components/frame.js +++ b/devtools/client/shared/components/frame.js @@ -29,59 +29,136 @@ module.exports = createClass({ // Option to display a function name even if it's anonymous. showAnonymousFunctionName: PropTypes.bool, // Option to display a host name after the source link. showHost: PropTypes.bool, // Option to display a host name if the filename is empty or just '/' showEmptyPathAsHost: PropTypes.bool, // Option to display a full source instead of just the filename. showFullSourceUrl: PropTypes.bool, + // Service to enable the source map feature for console. + sourceMapService: PropTypes.object, }, getDefaultProps() { return { showFunctionName: false, showAnonymousFunctionName: false, showHost: false, showEmptyPathAsHost: false, showFullSourceUrl: false, }; }, + componentWillMount() { + const sourceMapService = this.props.sourceMapService; + if (sourceMapService) { + const source = this.getSource(); + sourceMapService.subscribe(source, this.onSourceUpdated); + } + }, + + componentWillUnmount() { + const sourceMapService = this.props.sourceMapService; + if (sourceMapService) { + const source = this.getSource(); + sourceMapService.unsubscribe(source, this.onSourceUpdated); + } + }, + + /** + * Component method to update the FrameView when a resolved location is available + * @param event + * @param location + */ + onSourceUpdated(event, location, resolvedLocation) { + const frame = this.getFrame(resolvedLocation); + this.setState({ + frame, + isSourceMapped: true, + }); + }, + + /** + * Utility method to convert the Frame object to the + * Source Object model required by SourceMapService + * @param frame + * @returns {{url: *, line: *, column: *}} + */ + getSource(frame) { + frame = frame || this.props.frame; + const { source, line, column } = frame; + return { + url: source, + line, + column, + }; + }, + + /** + * Utility method to convert the Source object model to the + * Frame object model required by FrameView class. + * @param source + * @returns {{source: *, line: *, column: *, functionDisplayName: *}} + */ + getFrame(source) { + const { url, line, column } = source; + return { + source: url, + line, + column, + functionDisplayName: this.props.frame.functionDisplayName, + }; + }, + render() { + let frame, isSourceMapped; let { onClick, - frame, showFunctionName, showAnonymousFunctionName, showHost, showEmptyPathAsHost, showFullSourceUrl } = this.props; + if (this.state && this.state.isSourceMapped) { + frame = this.state.frame; + isSourceMapped = this.state.isSourceMapped; + } else { + frame = this.props.frame; + } + let source = frame.source ? String(frame.source) : ""; let line = frame.line != void 0 ? Number(frame.line) : null; let column = frame.column != void 0 ? Number(frame.column) : null; const { short, long, host } = getSourceNames(source); // Reparse the URL to determine if we should link this; `getSourceNames` // has already cached this indirectly. We don't want to attempt to // link to "self-hosted" and "(unknown)". However, we do want to link // to Scratchpad URIs. - const isLinkable = !!(isScratchpadScheme(source) || parseURL(source)); + // Source mapped sources might not necessary linkable, but they + // are still valid in the debugger. + const isLinkable = !!(isScratchpadScheme(source) || parseURL(source)) + || isSourceMapped; const elements = []; const sourceElements = []; let sourceEl; let tooltip = long; + + // If the source is linkable and line > 0 + const shouldDisplayLine = isLinkable && line; + // Exclude all falsy values, including `0`, as even // a number 0 for line doesn't make sense, and should not be displayed. // If source isn't linkable, don't attempt to append line and column // info, as this probably doesn't make sense. - if (isLinkable && line) { + if (shouldDisplayLine) { tooltip += `:${line}`; // Intentionally exclude 0 if (column) { tooltip += `:${column}`; } } let attributes = { @@ -99,26 +176,35 @@ module.exports = createClass({ elements.push( dom.span({ className: "frame-link-function-display-name" }, functionDisplayName) ); } } let displaySource = showFullSourceUrl ? long : short; - if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) { + // SourceMapped locations might not be parsed properly by parseURL. + // Eg: sourcemapped location could be /folder/file.coffee instead of a url + // and so the url parser would not parse non-url locations properly + // Check for "/" in displaySource. If "/" is in displaySource, + // take everything after last "/". + if (isSourceMapped) { + displaySource = displaySource.lastIndexOf("/") < 0 ? + displaySource : + displaySource.slice(displaySource.lastIndexOf("/") + 1); + } else if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) { displaySource = host; } sourceElements.push(dom.span({ className: "frame-link-filename", }, displaySource)); // If source is linkable, and we have a line number > 0 - if (isLinkable && line) { + if (shouldDisplayLine) { let lineInfo = `:${line}`; // Add `data-line` attribute for testing attributes["data-line"] = line; // Intentionally exclude 0 if (column) { lineInfo += `:${column}`; // Add `data-column` attribute for testing @@ -129,17 +215,17 @@ module.exports = createClass({ } // If source is not a URL (self-hosted, eval, etc.), don't make // it an anchor link, as we can't link to it. if (isLinkable) { sourceEl = dom.a({ onClick: e => { e.preventDefault(); - onClick(frame); + onClick(this.getSource(frame)); }, href: source, className: "frame-link-source", draggable: false, title: l10n.getFormatStr("frame.viewsourceindebugger", tooltip) }, sourceElements); } else { sourceEl = dom.span({ diff --git a/devtools/client/webconsole/webconsole.js b/devtools/client/webconsole/webconsole.js --- a/devtools/client/webconsole/webconsole.js +++ b/devtools/client/webconsole/webconsole.js @@ -2526,55 +2526,62 @@ WebConsoleFrame.prototype = { locationNode.className = "message-location devtools-monospace"; if (!url) { url = ""; } let fullURL = url.split(" -> ").pop(); // Make the location clickable. - let onClick = () => { + let onClick = ({ url, line }) => { let category = locationNode.closest(".message").category; let target = null; if (/^Scratchpad\/\d+$/.test(url)) { target = "scratchpad"; } else if (category === CATEGORY_CSS) { target = "styleeditor"; } else if (category === CATEGORY_JS || category === CATEGORY_WEBDEV) { target = "jsdebugger"; - } else if (/\.js$/.test(fullURL)) { + } else if (/\.js$/.test(url)) { // If it ends in .js, let's attempt to open in debugger // anyway, as this falls back to normal view-source. target = "jsdebugger"; + } else { + // Point everything else to debugger, if source not available, + // it will fall back to view-source. + target = "jsdebugger"; } switch (target) { case "scratchpad": this.owner.viewSourceInScratchpad(url, line); return; case "jsdebugger": - this.owner.viewSourceInDebugger(fullURL, line); + this.owner.viewSourceInDebugger(url, line); return; case "styleeditor": - this.owner.viewSourceInStyleEditor(fullURL, line); + this.owner.viewSourceInStyleEditor(url, line); return; } // No matching tool found; use old school view-source - this.owner.viewSource(fullURL, line); + this.owner.viewSource(url, line); }; + const toolbox = gDevTools.getToolbox(this.owner.target); + this.ReactDOM.render(this.FrameView({ frame: { source: fullURL, line, column }, showEmptyPathAsHost: true, onClick, + sourceMapService: toolbox ? toolbox._sourceMapService : null, }), locationNode); return locationNode; }, /** * Adjusts the category and severity of the given message. * diff --git a/devtools/server/actors/utils/TabSources.js b/devtools/server/actors/utils/TabSources.js --- a/devtools/server/actors/utils/TabSources.js +++ b/devtools/server/actors/utils/TabSources.js @@ -240,17 +240,17 @@ TabSources.prototype = { } } if (url in this._sourceMappedSourceActors) { return this._sourceMappedSourceActors[url]; } } - throw new Error("getSourceByURL: could not find source for " + url); + throw new Error("getSourceActorByURL: could not find source for " + url); return null; }, /** * Returns true if the URL likely points to a minified resource, false * otherwise. * * @param String aURL diff --git a/devtools/server/actors/webbrowser.js b/devtools/server/actors/webbrowser.js --- a/devtools/server/actors/webbrowser.js +++ b/devtools/server/actors/webbrowser.js @@ -2086,51 +2086,45 @@ TabActor.prototype = { * @param {String} request.url * @param {Number} request.line * @param {Number?} request.column * @return {Promise} */ onResolveLocation(request) { let { url, line } = request; let column = request.column || 0; - let actor = this.sources.getSourceActorByURL(url); - - if (actor) { - // Get the generated source actor if this is source mapped - let generatedActor = actor.generatedSource ? - this.sources.createNonSourceMappedActor(actor.generatedSource) : - actor; - let generatedLocation = new GeneratedLocation( - generatedActor, line, column); + const scripts = this.threadActor.dbg.findScripts({ url }); - return this.sources.getOriginalLocation(generatedLocation).then(loc => { - // If no map found, return this packet - if (loc.originalLine == null) { - return { - from: this.actorID, - type: "resolveLocation", - error: "MAP_NOT_FOUND" - }; - } - - loc = loc.toJSON(); - return { - from: this.actorID, - url: loc.source.url, - column: loc.column, - line: loc.line - }; + if (!scripts[0] || !scripts[0].source) { + return promise.resolve({ + from: this.actorID, + type: "resolveLocation", + error: "SOURCE_NOT_FOUND" }); } + const source = scripts[0].source; + const generatedActor = this.sources.createNonSourceMappedActor(source); + let generatedLocation = new GeneratedLocation( + generatedActor, line, column); + return this.sources.getOriginalLocation(generatedLocation).then(loc => { + // If no map found, return this packet + if (loc.originalLine == null) { + return { + type: "resolveLocation", + error: "MAP_NOT_FOUND" + }; + } - // Fall back to this packet when source is not found - return promise.resolve({ - from: this.actorID, - type: "resolveLocation", - error: "SOURCE_NOT_FOUND" + loc = loc.toJSON(); + return { + from: this.actorID, + url: loc.source.url, + column: loc.column, + line: loc.line + }; }); }, }; /** * The request types this actor can handle. */ TabActor.prototype.requestTypes = {