--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -21,16 +21,17 @@ DevToolsModules(
'devtools.js',
'gDevTools.jsm',
'location-store.js',
'menu-item.js',
'menu.js',
'selection.js',
'sidebar.js',
'source-map-service.js',
+ 'source-map-url-service.js',
'target-from-url.js',
'target.js',
'toolbox-highlighter-utils.js',
'toolbox-host-manager.js',
'toolbox-hosts.js',
'toolbox-options.js',
'toolbox.js',
'ToolboxProcess.jsm',
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/source-map-url-service.js
@@ -0,0 +1,89 @@
+/* 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";
+
+/**
+ * A simple service to track source actors and keep a mapping between
+ * original URLs and objects holding the source actor's ID (which is
+ * used as a cookie by the devtools-source-map service) and the source
+ * map URL.
+ *
+ * @param {object} target
+ * The object the toolbox is debugging.
+ * @param {SourceMapService} sourceMapService
+ * The devtools-source-map functions
+ */
+function SourceMapURLService(target, sourceMapService) {
+ this._target = target;
+ this._sourceMapService = sourceMapService;
+ this._urls = new Map();
+
+ this._onSourceUpdated = this._onSourceUpdated.bind(this);
+ this.reset = this.reset.bind(this);
+
+ target.on("source-updated", this._onSourceUpdated);
+ target.on("will-navigate", this.reset);
+}
+
+/**
+ * Reset the service. This flushes the internal cache.
+ */
+SourceMapURLService.prototype.reset = function () {
+ this._urls.clear();
+};
+
+/**
+ * Shut down the service, unregistering its event listeners and
+ * flushing the cache. After this call the service will no longer
+ * function.
+ */
+SourceMapURLService.prototype.destroy = function () {
+ this.reset();
+ this._target.off("source-updated", this._onSourceUpdated);
+ this._target.off("will-navigate", this.reset);
+ this._target = this._urls = null;
+};
+
+/**
+ * A helper function that is called when a new source is available.
+ */
+SourceMapURLService.prototype._onSourceUpdated = function (_, sourceEvent) {
+ let { source } = sourceEvent;
+ let { generatedUrl, url, actor: id, sourceMapURL } = source;
+
+ // As long as the actor is also handling source maps, we want the
+ // generated URL if it is available. This will be going away in bug 1349354.
+ let seenUrl = generatedUrl || url;
+ this._urls.set(seenUrl, { id, url: seenUrl, sourceMapURL });
+};
+
+/**
+ * Look up the original position for a given location. This returns a
+ * promise resolving to either the original location, or null if the
+ * given location is not source-mapped. If a location is returned, it
+ * is of the same form as devtools-source-map's |getOriginalLocation|.
+ *
+ * @param {String} url
+ * The URL to map.
+ * @param {number} line
+ * The line number to map.
+ * @param {number} column
+ * The column number to map.
+ * @return Promise
+ * A promise resolving either to the original location, or null.
+ */
+SourceMapURLService.prototype.originalPositionFor = async function (url, line, column) {
+ const urlInfo = this._urls.get(url);
+ if (!urlInfo) {
+ return null;
+ }
+ // Call getOriginalURLs to make sure the source map has been
+ // fetched. We don't actually need the result of this though.
+ await this._sourceMapService.getOriginalURLs(urlInfo);
+ const location = { sourceId: urlInfo.id, line, column, sourceUrl: url };
+ let resolvedLocation = await this._sourceMapService.getOriginalLocation(location);
+ return resolvedLocation === location ? null : resolvedLocation;
+};
+
+exports.SourceMapURLService = SourceMapURLService;
--- a/devtools/client/framework/test/browser_source_map-01.js
+++ b/devtools/client/framework/test/browser_source_map-01.js
@@ -19,74 +19,55 @@ registerCleanupFunction(function* () {
Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
});
const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
// Empty page
const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
const JS_URL = `${URL_ROOT}code_binary_search.js`;
const COFFEE_URL = `${URL_ROOT}code_binary_search.coffee`;
-const { SourceMapService } = require("devtools/client/framework/source-map-service");
-const { serialize } = require("devtools/client/framework/location-store");
add_task(function* () {
const toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
- const service = new SourceMapService(toolbox.target);
- let aggregator = new Map();
-
- function onUpdate(e, oldLoc, newLoc) {
- if (oldLoc.line === 6) {
- checkLoc1(oldLoc, newLoc);
- } else if (oldLoc.line === 8) {
- checkLoc2(oldLoc, newLoc);
- } else {
- throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
- }
- aggregator.set(serialize(oldLoc), newLoc);
- }
-
- let loc1 = { url: JS_URL, line: 6 };
- let loc2 = { url: JS_URL, line: 8, column: 3 };
-
- service.subscribe(loc1, onUpdate);
- service.subscribe(loc2, onUpdate);
+ const service = toolbox.sourceMapURLService;
// Inject JS script
let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_binary_search");
yield createScript(JS_URL);
yield sourceShown;
- yield waitUntil(() => aggregator.size === 2);
+ let loc1 = { url: JS_URL, line: 6 };
+ let newLoc1 = yield service.originalPositionFor(loc1.url, loc1.line);
+ checkLoc1(loc1, newLoc1);
- aggregator = Array.from(aggregator.values());
-
- ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 4), "found first updated location");
- ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 6), "found second updated location");
+ let loc2 = { url: JS_URL, line: 8, column: 3 };
+ let newLoc2 = yield service.originalPositionFor(loc2.url, loc2.line, loc2.column);
+ checkLoc2(loc2, newLoc2);
yield toolbox.destroy();
gBrowser.removeCurrentTab();
finish();
});
function checkLoc1(oldLoc, newLoc) {
is(oldLoc.line, 6, "Correct line for JS:6");
is(oldLoc.column, null, "Correct column for JS:6");
is(oldLoc.url, JS_URL, "Correct url for JS:6");
is(newLoc.line, 4, "Correct line for JS:6 -> COFFEE");
is(newLoc.column, 2, "Correct column for JS:6 -> COFFEE -- handles falsy column entries");
- is(newLoc.url, COFFEE_URL, "Correct url for JS:6 -> COFFEE");
+ is(newLoc.sourceUrl, COFFEE_URL, "Correct url for JS:6 -> COFFEE");
}
function checkLoc2(oldLoc, newLoc) {
is(oldLoc.line, 8, "Correct line for JS:8:3");
is(oldLoc.column, 3, "Correct column for JS:8:3");
is(oldLoc.url, JS_URL, "Correct url for JS:8:3");
is(newLoc.line, 6, "Correct line for JS:8:3 -> COFFEE");
is(newLoc.column, 10, "Correct column for JS:8:3 -> COFFEE");
- is(newLoc.url, COFFEE_URL, "Correct url for JS:8:3 -> COFFEE");
+ is(newLoc.sourceUrl, COFFEE_URL, "Correct url for JS:8:3 -> COFFEE");
}
function createScript(url) {
info(`Creating script: ${url}`);
let mm = getFrameScript();
let command = `
let script = document.createElement("script");
script.setAttribute("src", "${url}");
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -57,16 +57,18 @@ loader.lazyRequireGetter(this, "KeyShort
loader.lazyRequireGetter(this, "ZoomKeys",
"devtools/client/shared/zoom-keys");
loader.lazyRequireGetter(this, "settleAll",
"devtools/shared/ThreadSafeDevToolsUtils", true);
loader.lazyRequireGetter(this, "ToolboxButtons",
"devtools/client/definitions", true);
loader.lazyRequireGetter(this, "SourceMapService",
"devtools/client/framework/source-map-service", true);
+loader.lazyRequireGetter(this, "SourceMapURLService",
+ "devtools/client/framework/source-map-url-service", true);
loader.lazyRequireGetter(this, "HUDService",
"devtools/client/webconsole/hudservice");
loader.lazyRequireGetter(this, "viewSource",
"devtools/client/shared/view-source");
loader.lazyGetter(this, "registerHarOverlay", () => {
return require("devtools/client/netmonitor/src/har/toolbox-overlay").register;
});
@@ -531,32 +533,52 @@ Toolbox.prototype = {
},
get ToolboxController() {
return this.browserRequire("devtools/client/framework/components/toolbox-controller");
},
/**
* A common access point for the client-side mapping service for source maps that
- * any panel can use.
+ * any panel can use. This is a "low-level" API that connects to
+ * the source map worker.
*/
get sourceMapService() {
if (!Services.prefs.getBoolPref("devtools.source-map.client-service.enabled")) {
return null;
}
if (this._sourceMapService) {
return this._sourceMapService;
}
// Uses browser loader to access the `Worker` global.
this._sourceMapService =
this.browserRequire("devtools/client/shared/source-map/index");
this._sourceMapService.startSourceMapWorker(SOURCE_MAP_WORKER);
return this._sourceMapService;
},
+ /**
+ * Clients wishing to use source maps but that want the toolbox to
+ * track the source actor mapping can use this source map service.
+ * This is a higher-level service than the one returned by
+ * |sourceMapService|, in that it automatically tracks source actor
+ * IDs.
+ */
+ get sourceMapURLService() {
+ if (this._sourceMapURLService) {
+ return this._sourceMapURLService;
+ }
+ let sourceMaps = this.sourceMapService;
+ if (!sourceMaps) {
+ return null;
+ }
+ this._sourceMapURLService = new SourceMapURLService(this._target, sourceMaps);
+ return this._sourceMapURLService;
+ },
+
// Return HostType id for telemetry
_getTelemetryHostId: function () {
switch (this.hostType) {
case Toolbox.HostType.BOTTOM: return 0;
case Toolbox.HostType.SIDE: return 1;
case Toolbox.HostType.WINDOW: return 2;
case Toolbox.HostType.CUSTOM: return 3;
default: return 9;
@@ -2294,16 +2316,21 @@ Toolbox.prototype = {
this._lastFocusedElement = null;
if (this._deprecatedServerSourceMapService) {
this._deprecatedServerSourceMapService.destroy();
this._deprecatedServerSourceMapService = null;
}
+ if (this._sourceMapURLService) {
+ this._sourceMapURLService.destroy();
+ this._sourceMapURLService = null;
+ }
+
if (this._sourceMapService) {
this._sourceMapService.stopSourceMapWorker();
this._sourceMapService = null;
}
if (this.webconsolePanel) {
this._saveSplitConsoleHeight();
this.webconsolePanel.removeEventListener("resize",
--- a/devtools/client/shared/components/frame.js
+++ b/devtools/client/shared/components/frame.js
@@ -47,70 +47,57 @@ module.exports = createClass({
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);
+ const { source, line, column } = this.props.frame;
+ sourceMapService.originalPositionFor(source, line, column)
+ .then(resolvedLocation => {
+ if (resolvedLocation) {
+ this.onSourceUpdated(resolvedLocation);
+ }
+ });
}
},
/**
* Component method to update the FrameView when a resolved location is available
- * @param event
- * @param location
+ * @param {Location} resolvedLocation
+ * the resolved location as found via a source map
*/
- onSourceUpdated(event, location, resolvedLocation) {
- const frame = this.getFrame(resolvedLocation);
+ onSourceUpdated(resolvedLocation) {
+ const { sourceUrl, line, column } = resolvedLocation;
+ const frame = {
+ source: sourceUrl,
+ line,
+ column,
+ functionDisplayName: this.props.frame.functionDisplayName,
+ };
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: *}}
+ * Utility method to convert the Frame object model to the
+ * object model required by the onClick callback.
+ * @param Frame frame
+ * @returns {{url: *, line: *, column: *, functionDisplayName: *}}
*/
- getSource(frame) {
- frame = frame || this.props.frame;
+ getSourceForClick(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,
@@ -219,17 +206,17 @@ module.exports = createClass({
}, sourceElements);
// 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(this.getSource(frame));
+ onClick(this.getSourceForClick(frame));
},
href: source,
className: "frame-link-source",
draggable: false,
}, sourceInnerEl);
} else {
sourceEl = dom.span({
key: "source",
--- a/devtools/client/shared/components/test/mochitest/test_frame_01.html
+++ b/devtools/client/shared/components/test/mochitest/test_frame_01.html
@@ -282,22 +282,53 @@ window.onload = Task.async(function* ()
showEmptyPathAsHost: true,
}, {
file: "www.cnn.com",
line: "1",
shouldLink: true,
tooltip: "View source in Debugger → http://www.cnn.com/:1",
});
+ const resolvedLocation = {
+ sourceId: "whatever",
+ line: 23,
+ sourceUrl: "https://bugzilla.mozilla.org/original.js",
+ };
+ let mockSourceMapService = {
+ originalPositionFor: function () {
+ // Return a phony promise-like thing that resolves
+ // immediately.
+ return {
+ then: function (consequence) {
+ consequence(resolvedLocation);
+ },
+ };
+ },
+ };
+ yield checkFrameComponent({
+ frame: {
+ line: 97,
+ source: "https://bugzilla.mozilla.org/bundle.js",
+ },
+ sourceMapService: mockSourceMapService,
+ }, {
+ file: "original.js",
+ line: resolvedLocation.line,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:23",
+ source: "https://bugzilla.mozilla.org/original.js",
+ });
+
function* checkFrameComponent(input, expected) {
let props = Object.assign({ onClick: () => {} }, input);
let frame = ReactDOM.render(Frame(props), window.document.body);
let el = ReactDOM.findDOMNode(frame);
let { source } = input.frame;
checkFrameString(Object.assign({ el, source }, expected));
+ ReactDOM.unmountComponentAtNode(window.document.body);
}
} catch (e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
SimpleTest.finish();
}
});
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -87,18 +87,17 @@ NewConsoleOutputWrapper.prototype = {
frame.url,
frame.line
),
openNetworkPanel: (requestId) => {
return this.toolbox.selectTool("netmonitor").then(panel => {
return panel.panelWin.NetMonitorController.inspectRequest(requestId);
});
},
- sourceMapService:
- this.toolbox ? this.toolbox._deprecatedServerSourceMapService : null,
+ sourceMapService: this.toolbox ? this.toolbox.sourceMapURLService : null,
highlightDomElement: (grip, options = {}) => {
return this.toolbox.highlighterUtils
? this.toolbox.highlighterUtils.highlightDomValueGrip(grip, options)
: null;
},
unHighlightDomElement: (forceHide = false) => {
return this.toolbox.highlighterUtils
? this.toolbox.highlighterUtils.unhighlight(forceHide)
--- a/devtools/client/webconsole/webconsole.js
+++ b/devtools/client/webconsole/webconsole.js
@@ -2621,17 +2621,17 @@ WebConsoleFrame.prototype = {
let { url, line, column } = location;
let source = url ? url.split(" -> ").pop() : "";
this.ReactDOM.render(this.FrameView({
frame: { source, line, column },
showEmptyPathAsHost: true,
onClick,
- sourceMapService: toolbox ? toolbox._deprecatedServerSourceMapService : null,
+ sourceMapService: toolbox ? toolbox.sourceMapURLService : null,
}), locationNode);
return locationNode;
},
/**
* Adjusts the category and severity of the given message.
*