summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/source-map-loader
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/shared/source-map-loader/index.js122
-rw-r--r--devtools/client/shared/source-map-loader/moz.build18
-rw-r--r--devtools/client/shared/source-map-loader/source-map.js521
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/browser.ini14
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/browser_getContentType.js32
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/browser_locations.js141
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/browser_source-map.js169
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/browser_wasm-source-map.js126
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js2
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js.map10
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js94
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js.map21
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js2
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js.map10
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/if.js12
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js16
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js.map7
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js62
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js.map8
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/missingmap.js2
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js2
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js.map9
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js2
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js.map10
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/fixtures/wasm.js.map7
-rw-r--r--devtools/client/shared/source-map-loader/test/browser/head.js27
-rw-r--r--devtools/client/shared/source-map-loader/utils/assert.js13
-rw-r--r--devtools/client/shared/source-map-loader/utils/fetchSourceMap.js137
-rw-r--r--devtools/client/shared/source-map-loader/utils/getOriginalStackFrames.js38
-rw-r--r--devtools/client/shared/source-map-loader/utils/index.js103
-rw-r--r--devtools/client/shared/source-map-loader/utils/moz.build15
-rw-r--r--devtools/client/shared/source-map-loader/utils/network-request.js43
-rw-r--r--devtools/client/shared/source-map-loader/utils/sourceMapRequests.js106
-rw-r--r--devtools/client/shared/source-map-loader/utils/wasmRemap.js107
-rw-r--r--devtools/client/shared/source-map-loader/wasm-dwarf/convertToJSON.js66
-rw-r--r--devtools/client/shared/source-map-loader/wasm-dwarf/dwarf_to_json.wasmbin0 -> 246995 bytes
-rw-r--r--devtools/client/shared/source-map-loader/wasm-dwarf/moz.build12
-rw-r--r--devtools/client/shared/source-map-loader/wasm-dwarf/wasmAsset.js17
-rw-r--r--devtools/client/shared/source-map-loader/wasm-dwarf/wasmDwarfExpressions.js260
-rw-r--r--devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes.js215
-rw-r--r--devtools/client/shared/source-map-loader/worker.js52
41 files changed, 2630 insertions, 0 deletions
diff --git a/devtools/client/shared/source-map-loader/index.js b/devtools/client/shared/source-map-loader/index.js
new file mode 100644
index 0000000000..75ce9c800f
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/index.js
@@ -0,0 +1,122 @@
+/* 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 {
+ WorkerDispatcher,
+} = require("resource://devtools/client/shared/worker-utils.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/toolbox.properties"
+);
+
+const SOURCE_MAP_WORKER_URL =
+ "resource://devtools/client/shared/source-map-loader/worker.js";
+
+const {
+ originalToGeneratedId,
+ generatedToOriginalId,
+ isGeneratedId,
+ isOriginalId,
+} = require("resource://devtools/client/shared/source-map-loader/utils/index.js");
+
+class SourceMapLoader extends WorkerDispatcher {
+ #setSourceMapForGeneratedSources = this.task(
+ "setSourceMapForGeneratedSources"
+ );
+ #getOriginalURLs = this.task("getOriginalURLs");
+ #getOriginalSourceText = this.task("getOriginalSourceText");
+
+ constructor() {
+ super(SOURCE_MAP_WORKER_URL);
+ }
+
+ async getOriginalURLs(urlInfo) {
+ try {
+ return await this.#getOriginalURLs(urlInfo);
+ } catch (error) {
+ const message = L10N.getFormatStr(
+ "toolbox.sourceMapFailure",
+ error,
+ urlInfo.url,
+ urlInfo.sourceMapURL
+ );
+ this.emit("source-map-error", message);
+
+ // It's ok to swallow errors here, because a null
+ // result just means that no source map was found.
+ return null;
+ }
+ }
+
+ hasOriginalURL = this.task("hasOriginalURL");
+ getOriginalRanges = this.task("getOriginalRanges");
+
+ getGeneratedRanges = this.task("getGeneratedRanges", {
+ queue: true,
+ });
+ getGeneratedLocation = this.task("getGeneratedLocation", {
+ queue: true,
+ });
+ getOriginalLocation = this.task("getOriginalLocation", {
+ queue: true,
+ });
+
+ getOriginalLocations = this.task("getOriginalLocations");
+ getGeneratedRangesForOriginal = this.task("getGeneratedRangesForOriginal");
+ getFileGeneratedRange = this.task("getFileGeneratedRange");
+ getSourceMapIgnoreList = this.task("getSourceMapIgnoreList");
+
+ async getOriginalSourceText(originalSourceId) {
+ try {
+ return await this.#getOriginalSourceText(originalSourceId);
+ } catch (error) {
+ const message = L10N.getFormatStr(
+ "toolbox.sourceMapSourceFailure",
+ error.message,
+ error.metadata ? error.metadata.url : "<unknown>"
+ );
+ this.emit("source-map-error", message);
+
+ // Also replace the result with the error text.
+ // Note that this result has to have the same form
+ // as whatever the upstream getOriginalSourceText
+ // returns.
+ return {
+ text: message,
+ contentType: "text/plain",
+ };
+ }
+ }
+
+ clearSourceMaps = this.task("clearSourceMaps");
+ getOriginalStackFrames = this.task("getOriginalStackFrames");
+
+ async setSourceMapForGeneratedSources(generatedIds, sourceMap) {
+ const rv = await this.#setSourceMapForGeneratedSources(
+ generatedIds,
+ sourceMap
+ );
+
+ // Notify and ensure waiting for the SourceMapURLService to process the source map before resolving.
+ // Otherwise tests start failing because of pending request made by this component.
+ await this.emitAsync("source-map-created", generatedIds);
+
+ return rv;
+ }
+
+ stopSourceMapWorker = this.stop.bind(this);
+}
+EventEmitter.decorate(SourceMapLoader.prototype);
+
+module.exports = {
+ SourceMapLoader,
+ originalToGeneratedId,
+ generatedToOriginalId,
+ isGeneratedId,
+ isOriginalId,
+};
diff --git a/devtools/client/shared/source-map-loader/moz.build b/devtools/client/shared/source-map-loader/moz.build
new file mode 100644
index 0000000000..c3cf26b358
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "utils",
+ "wasm-dwarf",
+]
+
+DevToolsModules(
+ "index.js",
+ "source-map.js",
+ "worker.js",
+)
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
diff --git a/devtools/client/shared/source-map-loader/source-map.js b/devtools/client/shared/source-map-loader/source-map.js
new file mode 100644
index 0000000000..5fb22cdde4
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/source-map.js
@@ -0,0 +1,521 @@
+/* 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";
+
+/**
+ * Source Map Worker
+ * @module utils/source-map-worker
+ */
+
+const {
+ SourceMapConsumer,
+} = require("resource://devtools/client/shared/vendor/source-map/source-map.js");
+
+// Initialize the source-map library right away so that all other code can use it.
+SourceMapConsumer.initialize({
+ "lib/mappings.wasm":
+ "resource://devtools/client/shared/vendor/source-map/lib/mappings.wasm",
+});
+
+const {
+ networkRequest,
+} = require("resource://devtools/client/shared/source-map-loader/utils/network-request.js");
+const assert = require("resource://devtools/client/shared/source-map-loader/utils/assert.js");
+const {
+ fetchSourceMap,
+ hasOriginalURL,
+ clearOriginalURLs,
+} = require("resource://devtools/client/shared/source-map-loader/utils/fetchSourceMap.js");
+const {
+ getSourceMap,
+ getSourceMapWithMetadata,
+ setSourceMap,
+ clearSourceMaps: clearSourceMapsRequests,
+} = require("resource://devtools/client/shared/source-map-loader/utils/sourceMapRequests.js");
+const {
+ originalToGeneratedId,
+ generatedToOriginalId,
+ isGeneratedId,
+ isOriginalId,
+ getContentType,
+} = require("resource://devtools/client/shared/source-map-loader/utils/index.js");
+const {
+ clearWasmXScopes,
+} = require("resource://devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes.js");
+
+async function getOriginalURLs(generatedSource) {
+ await fetchSourceMap(generatedSource);
+ const data = await getSourceMapWithMetadata(generatedSource.id);
+ return data ? data.sources : null;
+}
+
+async function getSourceMapIgnoreList(generatedSourceId) {
+ const data = await getSourceMapWithMetadata(generatedSourceId);
+ return data ? data.ignoreListUrls : [];
+}
+
+const COMPUTED_SPANS = new WeakSet();
+
+const SOURCE_MAPPINGS = new WeakMap();
+async function getOriginalRanges(sourceId) {
+ if (!isOriginalId(sourceId)) {
+ return [];
+ }
+
+ const generatedSourceId = originalToGeneratedId(sourceId);
+ const data = await getSourceMapWithMetadata(generatedSourceId);
+ if (!data) {
+ return [];
+ }
+ const { map } = data;
+ const url = data.urlsById.get(sourceId);
+
+ let mappings = SOURCE_MAPPINGS.get(map);
+ if (!mappings) {
+ mappings = new Map();
+ SOURCE_MAPPINGS.set(map, mappings);
+ }
+
+ let fileMappings = mappings.get(url);
+ if (!fileMappings) {
+ fileMappings = [];
+ mappings.set(url, fileMappings);
+
+ const originalMappings = fileMappings;
+ map.eachMapping(
+ mapping => {
+ if (mapping.source !== url) {
+ return;
+ }
+
+ const last = originalMappings[originalMappings.length - 1];
+
+ if (last && last.line === mapping.originalLine) {
+ if (last.columnStart < mapping.originalColumn) {
+ last.columnEnd = mapping.originalColumn;
+ } else {
+ // Skip this duplicate original location,
+ return;
+ }
+ }
+
+ originalMappings.push({
+ line: mapping.originalLine,
+ columnStart: mapping.originalColumn,
+ columnEnd: Infinity,
+ });
+ },
+ null,
+ SourceMapConsumer.ORIGINAL_ORDER
+ );
+ }
+
+ return fileMappings;
+}
+
+/**
+ * Given an original location, find the ranges on the generated file that
+ * are mapped from the original range containing the location.
+ */
+async function getGeneratedRanges(location) {
+ if (!isOriginalId(location.sourceId)) {
+ return [];
+ }
+
+ const generatedSourceId = originalToGeneratedId(location.sourceId);
+ const data = await getSourceMapWithMetadata(generatedSourceId);
+ if (!data) {
+ return [];
+ }
+ const { urlsById, map } = data;
+
+ if (!COMPUTED_SPANS.has(map)) {
+ COMPUTED_SPANS.add(map);
+ map.computeColumnSpans();
+ }
+
+ // We want to use 'allGeneratedPositionsFor' to get the _first_ generated
+ // location, but it hard-codes SourceMapConsumer.LEAST_UPPER_BOUND as the
+ // bias, making it search in the wrong direction for this usecase.
+ // To work around this, we use 'generatedPositionFor' and then look up the
+ // exact original location, making any bias value unnecessary, and then
+ // use that location for the call to 'allGeneratedPositionsFor'.
+ const genPos = map.generatedPositionFor({
+ source: urlsById.get(location.sourceId),
+ line: location.line,
+ column: location.column == null ? 0 : location.column,
+ bias: SourceMapConsumer.GREATEST_LOWER_BOUND,
+ });
+ if (genPos.line === null) {
+ return [];
+ }
+
+ const positions = map.allGeneratedPositionsFor(
+ map.originalPositionFor({
+ line: genPos.line,
+ column: genPos.column,
+ })
+ );
+
+ return positions
+ .map(mapping => ({
+ line: mapping.line,
+ columnStart: mapping.column,
+ columnEnd: mapping.lastColumn,
+ }))
+ .sort((a, b) => {
+ const line = a.line - b.line;
+ return line === 0 ? a.column - b.column : line;
+ });
+}
+
+async function getGeneratedLocation(location) {
+ if (!isOriginalId(location.sourceId)) {
+ return null;
+ }
+
+ const generatedSourceId = originalToGeneratedId(location.sourceId);
+ const data = await getSourceMapWithMetadata(generatedSourceId);
+ if (!data) {
+ return null;
+ }
+ const { urlsById, map } = data;
+
+ const positions = map.allGeneratedPositionsFor({
+ source: urlsById.get(location.sourceId),
+ line: location.line,
+ column: location.column == null ? 0 : location.column,
+ });
+
+ // Prior to source-map 0.7, the source-map module returned the earliest
+ // generated location in the file when there were multiple generated
+ // locations. The current comparison fn in 0.7 does not appear to take
+ // generated location into account properly.
+ let match;
+ for (const pos of positions) {
+ if (!match || pos.line < match.line || pos.column < match.column) {
+ match = pos;
+ }
+ }
+
+ if (!match) {
+ match = map.generatedPositionFor({
+ source: urlsById.get(location.sourceId),
+ line: location.line,
+ column: location.column == null ? 0 : location.column,
+ bias: SourceMapConsumer.LEAST_UPPER_BOUND,
+ });
+ }
+
+ return {
+ sourceId: generatedSourceId,
+ line: match.line,
+ column: match.column,
+ };
+}
+
+async function getOriginalLocations(locations, options = {}) {
+ const maps = {};
+
+ const results = [];
+ for (const location of locations) {
+ let map = maps[location.sourceId];
+ if (map === undefined) {
+ map = await getSourceMap(location.sourceId);
+ maps[location.sourceId] = map || null;
+ }
+
+ results.push(map ? getOriginalLocationSync(map, location, options) : null);
+ }
+ return results;
+}
+
+function getOriginalLocationSync(map, location, { search } = {}) {
+ // First check for an exact match
+ let match = map.originalPositionFor({
+ line: location.line,
+ column: location.column == null ? 0 : location.column,
+ });
+
+ // If there is not an exact match, look for a match with a bias at the
+ // current location and then on subsequent lines
+ if (search) {
+ let line = location.line;
+ let column = location.column == null ? 0 : location.column;
+
+ while (match.source === null) {
+ match = map.originalPositionFor({
+ line,
+ column,
+ bias: SourceMapConsumer[search],
+ });
+
+ line += search == "LEAST_UPPER_BOUND" ? 1 : -1;
+ column = search == "LEAST_UPPER_BOUND" ? 0 : Infinity;
+ }
+ }
+
+ const { source: sourceUrl, line, column } = match;
+ if (sourceUrl == null) {
+ // No url means the location didn't map.
+ return null;
+ }
+
+ return {
+ sourceId: generatedToOriginalId(location.sourceId, sourceUrl),
+ sourceUrl,
+ line,
+ column,
+ };
+}
+
+async function getOriginalLocation(location, options = {}) {
+ if (!isGeneratedId(location.sourceId)) {
+ return null;
+ }
+
+ const map = await getSourceMap(location.sourceId);
+ if (!map) {
+ return null;
+ }
+
+ return getOriginalLocationSync(map, location, options);
+}
+
+async function getOriginalSourceText(originalSourceId) {
+ assert(isOriginalId(originalSourceId), "Source is not an original source");
+
+ const generatedSourceId = originalToGeneratedId(originalSourceId);
+ const data = await getSourceMapWithMetadata(generatedSourceId);
+ if (!data) {
+ return null;
+ }
+ const { urlsById, map } = data;
+
+ const url = urlsById.get(originalSourceId);
+ let text = map.sourceContentFor(url, true);
+ if (!text) {
+ try {
+ const response = await networkRequest(url, {
+ sourceMapBaseURL: map.sourceMapBaseURL,
+ loadFromCache: false,
+ allowsRedirects: false,
+ });
+ text = response.content;
+ } catch (err) {
+ // Workers exceptions are processed by worker-utils module and
+ // only metadata attribute is transferred between threads.
+ // Notify the main thread about which url failed loading.
+ err.metadata = {
+ url,
+ };
+ throw err;
+ }
+ }
+
+ return {
+ text,
+ contentType: getContentType(url || ""),
+ };
+}
+
+/**
+ * Find the set of ranges on the generated file that map from the original
+ * file's locations.
+ *
+ * @param sourceId - The original ID of the file we are processing.
+ * @param url - The original URL of the file we are processing.
+ * @param mergeUnmappedRegions - If unmapped regions are encountered between
+ * two mappings for the given original file, allow the two mappings to be
+ * merged anyway. This is useful if you are more interested in the general
+ * contiguous ranges associated with a file, rather than the specifics of
+ * the ranges provided by the sourcemap.
+ */
+const GENERATED_MAPPINGS = new WeakMap();
+async function getGeneratedRangesForOriginal(
+ sourceId,
+ mergeUnmappedRegions = false
+) {
+ assert(isOriginalId(sourceId), "Source is not an original source");
+
+ const data = await getSourceMapWithMetadata(originalToGeneratedId(sourceId));
+ // NOTE: this is only needed for Flow
+ if (!data) {
+ return [];
+ }
+ const { urlsById, map } = data;
+ const url = urlsById.get(sourceId);
+
+ if (!COMPUTED_SPANS.has(map)) {
+ COMPUTED_SPANS.add(map);
+ map.computeColumnSpans();
+ }
+
+ if (!GENERATED_MAPPINGS.has(map)) {
+ GENERATED_MAPPINGS.set(map, new Map());
+ }
+
+ const generatedRangesMap = GENERATED_MAPPINGS.get(map);
+ if (!generatedRangesMap) {
+ return [];
+ }
+
+ if (generatedRangesMap.has(sourceId)) {
+ // NOTE we need to coerce the result to an array for Flow
+ return generatedRangesMap.get(sourceId) || [];
+ }
+
+ // Gather groups of mappings on the generated file, with new groups created
+ // if we cross a mapping for a different file.
+ let currentGroup = [];
+ const originalGroups = [currentGroup];
+ map.eachMapping(
+ mapping => {
+ if (mapping.source === url) {
+ currentGroup.push({
+ start: {
+ line: mapping.generatedLine,
+ column: mapping.generatedColumn,
+ },
+ end: {
+ line: mapping.generatedLine,
+ // The lastGeneratedColumn value is an inclusive value so we add
+ // one to it to get the exclusive end position.
+ column: mapping.lastGeneratedColumn + 1,
+ },
+ });
+ } else if (typeof mapping.source === "string" && currentGroup.length) {
+ // If there is a URL, but it is for a _different_ file, we create a
+ // new group of mappings so that we can tell
+ currentGroup = [];
+ originalGroups.push(currentGroup);
+ }
+ },
+ null,
+ SourceMapConsumer.GENERATED_ORDER
+ );
+
+ const generatedMappingsForOriginal = [];
+ if (mergeUnmappedRegions) {
+ // If we don't care about excluding unmapped regions, then we just need to
+ // create a range that is the fully encompasses each group, ignoring the
+ // empty space between each individual range.
+ for (const group of originalGroups) {
+ if (group.length) {
+ generatedMappingsForOriginal.push({
+ start: group[0].start,
+ end: group[group.length - 1].end,
+ });
+ }
+ }
+ } else {
+ let lastEntry;
+ for (const group of originalGroups) {
+ lastEntry = null;
+ for (const { start, end } of group) {
+ const lastEnd = lastEntry
+ ? wrappedMappingPosition(lastEntry.end)
+ : null;
+
+ // If this entry comes immediately after the previous one, extend the
+ // range of the previous entry instead of adding a new one.
+ if (
+ lastEntry &&
+ lastEnd &&
+ lastEnd.line === start.line &&
+ lastEnd.column === start.column
+ ) {
+ lastEntry.end = end;
+ } else {
+ const newEntry = { start, end };
+ generatedMappingsForOriginal.push(newEntry);
+ lastEntry = newEntry;
+ }
+ }
+ }
+ }
+
+ generatedRangesMap.set(sourceId, generatedMappingsForOriginal);
+ return generatedMappingsForOriginal;
+}
+
+function wrappedMappingPosition(pos) {
+ if (pos.column !== Infinity) {
+ return pos;
+ }
+
+ // If the end of the entry consumes the whole line, treat it as wrapping to
+ // the next line.
+ return {
+ line: pos.line + 1,
+ column: 0,
+ };
+}
+
+async function getFileGeneratedRange(originalSourceId) {
+ assert(isOriginalId(originalSourceId), "Source is not an original source");
+
+ const data = await getSourceMapWithMetadata(
+ originalToGeneratedId(originalSourceId)
+ );
+ if (!data) {
+ return null;
+ }
+ const { urlsById, map } = data;
+
+ const start = map.generatedPositionFor({
+ source: urlsById.get(originalSourceId),
+ line: 1,
+ column: 0,
+ bias: SourceMapConsumer.LEAST_UPPER_BOUND,
+ });
+
+ const end = map.generatedPositionFor({
+ source: urlsById.get(originalSourceId),
+ line: Number.MAX_SAFE_INTEGER,
+ column: Number.MAX_SAFE_INTEGER,
+ bias: SourceMapConsumer.GREATEST_LOWER_BOUND,
+ });
+
+ return {
+ start,
+ end,
+ };
+}
+
+/**
+ * Set the sourceMap for multiple passed source ids.
+ *
+ * @param {Array<string>} generatedSourceIds
+ * @param {Object} map: An actual sourcemap (as generated with SourceMapGenerator#toJSON)
+ */
+function setSourceMapForGeneratedSources(generatedSourceIds, map) {
+ const sourceMapConsumer = new SourceMapConsumer(map);
+ for (const generatedId of generatedSourceIds) {
+ setSourceMap(generatedId, Promise.resolve(sourceMapConsumer));
+ }
+}
+
+function clearSourceMaps() {
+ clearSourceMapsRequests();
+ clearWasmXScopes();
+ clearOriginalURLs();
+}
+
+module.exports = {
+ getOriginalURLs,
+ hasOriginalURL,
+ getOriginalRanges,
+ getGeneratedRanges,
+ getGeneratedLocation,
+ getOriginalLocation,
+ getOriginalLocations,
+ getOriginalSourceText,
+ getGeneratedRangesForOriginal,
+ getFileGeneratedRange,
+ getSourceMapIgnoreList,
+ setSourceMapForGeneratedSources,
+ clearSourceMaps,
+};
diff --git a/devtools/client/shared/source-map-loader/test/browser/browser.ini b/devtools/client/shared/source-map-loader/test/browser/browser.ini
new file mode 100644
index 0000000000..5c8b69c3b2
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/browser.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ fixtures/*
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+
+[browser_getContentType.js]
+[browser_locations.js]
+[browser_source-map.js]
+[browser_wasm-source-map.js]
+skip-if = http3 # Bug 1829298
diff --git a/devtools/client/shared/source-map-loader/test/browser/browser_getContentType.js b/devtools/client/shared/source-map-loader/test/browser/browser_getContentType.js
new file mode 100644
index 0000000000..5ac402951c
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/browser_getContentType.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Cover the automatic mapping of content type based on file extension
+
+const {
+ getContentType,
+ contentMapForTesting,
+} = require("resource://devtools/client/shared/source-map-loader/utils/index.js");
+
+add_task(async () => {
+ for (const ext in contentMapForTesting) {
+ is(
+ getContentType(`whatever.${ext}`),
+ contentMapForTesting[ext],
+ `${ext} file extension is correctly mapping the expected content type`
+ );
+ }
+ is(
+ getContentType(`whateverjs`),
+ "text/plain",
+ `A valid extension in file name doesn't cause a special content type mapping`
+ );
+
+ is(
+ getContentType("whatever.platypus"),
+ "text/plain",
+ "Test unknown extension defaults to text plain"
+ );
+});
diff --git a/devtools/client/shared/source-map-loader/test/browser/browser_locations.js b/devtools/client/shared/source-map-loader/test/browser/browser_locations.js
new file mode 100644
index 0000000000..4d43df5902
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/browser_locations.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Covert getOriginalLocation and getGeneratedLocation functions.
+
+add_task(async function testGetOriginalLocation() {
+ await fetchFixtureSourceMap("bundle");
+
+ const generatedLocation = {
+ sourceId: "bundle.js",
+ line: 49,
+ };
+
+ const originalLocation = await gSourceMapLoader.getOriginalLocation(
+ generatedLocation
+ );
+ Assert.deepEqual(
+ originalLocation,
+ {
+ column: 0,
+ line: 3,
+ sourceId: "bundle.js/originalSource-fe2c41d3535b76c158e39ba4f3ff826a",
+ sourceUrl: "webpack:///entry.js",
+ },
+ "Mapped a generated location"
+ );
+
+ const originalLocation2 = await gSourceMapLoader.getOriginalLocation(
+ originalLocation
+ );
+ Assert.deepEqual(originalLocation2, null, "No mapped location");
+
+ gSourceMapLoader.clearSourceMaps();
+ const originalLocation3 = await gSourceMapLoader.getOriginalLocation(
+ generatedLocation
+ );
+ Assert.deepEqual(
+ originalLocation3,
+ null,
+ "after clearing the source maps, the same generated location no longer maps"
+ );
+});
+
+add_task(async function testGetGeneratedLocation() {
+ await fetchFixtureSourceMap("bundle");
+
+ const originalLocation = {
+ column: 0,
+ line: 3,
+ sourceId: "bundle.js/originalSource-fe2c41d3535b76c158e39ba4f3ff826a",
+ };
+
+ const source = {
+ url: "webpack:///entry.js",
+ id: "bundle.js/originalSource-fe2c41d3535b76c158e39ba4f3ff826a",
+ };
+
+ const generatedLocation = await gSourceMapLoader.getGeneratedLocation(
+ originalLocation,
+ source
+ );
+ Assert.deepEqual(
+ generatedLocation,
+ {
+ sourceId: "bundle.js",
+ line: 49,
+ column: 0,
+ },
+ "Map an original location"
+ );
+
+ {
+ gSourceMapLoader.clearSourceMaps();
+
+ const secondGeneratedLocation = await gSourceMapLoader.getGeneratedLocation(
+ originalLocation,
+ source
+ );
+ Assert.deepEqual(
+ secondGeneratedLocation,
+ null,
+ "after clearing source maps, the same location no longer maps to an original location"
+ );
+ }
+
+ {
+ // we expect symmetric mappings, which means that if
+ // we map a generated location to an original location,
+ // and then map it back, we should get the original generated value.
+ // e.g. G[8, 0] -> O[5, 4] -> G[8, 0]
+ await fetchFixtureSourceMap("if.out");
+
+ const genLoc1 = {
+ sourceId: "if.out.js",
+ column: 0,
+ line: 8,
+ };
+
+ const ifSource = {
+ url: "if.js",
+ id: "if.out.js/originalSource-5ad3141023dae912c5f8833c7e03beeb",
+ };
+
+ const oLoc = await gSourceMapLoader.getOriginalLocation(genLoc1);
+ const genLoc2 = await gSourceMapLoader.getGeneratedLocation(oLoc, ifSource);
+
+ Assert.deepEqual(genLoc2, genLoc1, "location mapping is symmetric");
+ }
+
+ {
+ // we expect that an undefined column will be handled like a
+ // location w/ column 0. e.g. G[8, u] -> O[5, 4] -> G[8, 0]
+ await fetchFixtureSourceMap("if.out");
+
+ const genLoc1 = {
+ sourceId: "if.out.js",
+ column: undefined,
+ line: 8,
+ };
+
+ const ifSource = {
+ url: "if.js",
+ id: "if.out.js/originalSource-5ad3141023dae912c5f8833c7e03beeb",
+ };
+
+ const oLoc = await gSourceMapLoader.getOriginalLocation(genLoc1);
+ const genLoc2 = await gSourceMapLoader.getGeneratedLocation(oLoc, ifSource);
+
+ Assert.deepEqual(
+ genLoc2,
+ {
+ sourceId: "if.out.js",
+ column: 0,
+ line: 8,
+ },
+ "undefined column is handled like 0 column"
+ );
+ }
+});
diff --git a/devtools/client/shared/source-map-loader/test/browser/browser_source-map.js b/devtools/client/shared/source-map-loader/test/browser/browser_source-map.js
new file mode 100644
index 0000000000..3493953d1c
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/browser_source-map.js
@@ -0,0 +1,169 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Cover the high level API of these modules:
+// getOriginalURLs getGeneratedRangesForOriginal functions
+
+async function assertFixtureOriginalURLs(
+ fixtureName,
+ expectedUrls,
+ testMessage
+) {
+ const originalSources = await fetchFixtureSourceMap(fixtureName);
+ const urls = originalSources.map(s => s.url);
+ Assert.deepEqual(urls, expectedUrls, testMessage);
+}
+
+add_task(async function testGetOriginalURLs() {
+ await assertFixtureOriginalURLs(
+ "absolute",
+ ["https://example.com/cheese/heart.js"],
+ "Test absolute URL"
+ );
+
+ await assertFixtureOriginalURLs(
+ "bundle",
+ [
+ "webpack:///webpack/bootstrap%204ef8c7ec7c1df790781e",
+ "webpack:///entry.js",
+ "webpack:///times2.js",
+ "webpack:///output.js",
+ "webpack:///opts.js",
+ ],
+ "Test source with a url"
+ );
+
+ await assertFixtureOriginalURLs(
+ "empty",
+ [`${URL_ROOT_SSL}fixtures/heart.js`],
+ "Test empty sourceRoot resolution"
+ );
+
+ await assertFixtureOriginalURLs(
+ "noroot",
+ [`${URL_ROOT_SSL}fixtures/heart.js`],
+ "Test Non-existing sourceRoot resolution"
+ );
+
+ await assertFixtureOriginalURLs(
+ "noroot2",
+ [`${URL_ROOT_SSL}fixtures/heart.js`],
+ "Test Non-existing sourceRoot resolution with relative URLs"
+ );
+});
+
+add_task(async function testGetGeneratedRangesForOriginal() {
+ const originals = await fetchFixtureSourceMap("intermingled-sources");
+
+ const ranges = await gSourceMapLoader.getGeneratedRangesForOriginal(
+ originals[0].id
+ );
+
+ Assert.deepEqual(
+ ranges,
+ [
+ {
+ start: {
+ line: 4,
+ column: 69,
+ },
+ end: {
+ line: 9,
+ column: Infinity,
+ },
+ },
+ {
+ start: {
+ line: 11,
+ column: 0,
+ },
+ end: {
+ line: 17,
+ column: 3,
+ },
+ },
+ {
+ start: {
+ line: 19,
+ column: 18,
+ },
+ end: {
+ line: 19,
+ column: 22,
+ },
+ },
+ {
+ start: {
+ line: 26,
+ column: 0,
+ },
+ end: {
+ line: 26,
+ column: Infinity,
+ },
+ },
+ {
+ start: {
+ line: 28,
+ column: 0,
+ },
+ end: {
+ line: 28,
+ column: Infinity,
+ },
+ },
+ ],
+ "Test the overall generated ranges on the source"
+ );
+
+ {
+ // Note that we have to clear the source map in order to get the merged ranges,
+ // otherwise we are still fetching the previous unmerged ones!
+ const secondOriginals = await fetchFixtureSourceMap("intermingled-sources");
+ const mergedRanges = await gSourceMapLoader.getGeneratedRangesForOriginal(
+ secondOriginals[0].id,
+ true
+ );
+
+ Assert.deepEqual(
+ mergedRanges,
+ [
+ {
+ start: {
+ line: 4,
+ column: 69,
+ },
+ end: {
+ line: 28,
+ column: Infinity,
+ },
+ },
+ ],
+ "Test the merged generated ranges on the source"
+ );
+ }
+});
+
+add_task(async function testBaseURLErrorHandling() {
+ const source = {
+ id: "missingmap.js",
+ sourceMapURL: "missingmap.js.map",
+ // Notice the duplicated ":" which cause the error here
+ sourceMapBaseURL: "http:://example.com/",
+ };
+
+ const onError = gSourceMapLoader.once("source-map-error");
+ is(
+ await gSourceMapLoader.getOriginalURLs(source),
+ null,
+ "The error is silented..."
+ );
+ info("Wait for source-map-error event");
+ const error = await onError;
+ is(
+ error,
+ `Source map error: Error: URL constructor: http:://example.com/ is not a valid URL.\nResource URL: undefined\nSource Map URL: missingmap.js.map`
+ );
+});
diff --git a/devtools/client/shared/source-map-loader/test/browser/browser_wasm-source-map.js b/devtools/client/shared/source-map-loader/test/browser/browser_wasm-source-map.js
new file mode 100644
index 0000000000..0f69d7b038
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/browser_wasm-source-map.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test source mappings WASM sources.
+// This test is quite general and test various functions.
+
+const {
+ WasmRemap,
+} = require("resource://devtools/client/shared/source-map-loader/utils/wasmRemap.js");
+const {
+ SourceMapConsumer,
+} = require("resource://devtools/client/shared/vendor/source-map/source-map.js");
+
+SourceMapConsumer.initialize({
+ "lib/mappings.wasm":
+ "resource://devtools/client/shared/vendor/source-map/lib/mappings.wasm",
+});
+
+add_task(async function smokeTest() {
+ const testMap1 = {
+ version: 3,
+ file: "min.js",
+ names: [],
+ sources: ["one.js", "two.js"],
+ sourceRoot: "/the/root",
+ mappings: "CAAC,IAAM,SACU,GAAC",
+ };
+ const testMap1Entries = [
+ { offset: 1, line: 1, column: 1 },
+ { offset: 5, line: 1, column: 7 },
+ { offset: 14, line: 2, column: 17 },
+ { offset: 17, line: 2, column: 18 },
+ ];
+
+ const map1 = await new SourceMapConsumer(testMap1);
+
+ const remap1 = new WasmRemap(map1);
+
+ is(remap1.file, "min.js");
+ is(remap1.hasContentsOfAllSources(), false);
+ is(remap1.sources.length, 2);
+ is(remap1.sources[0], "/the/root/one.js");
+ is(remap1.sources[1], "/the/root/two.js");
+
+ const expectedEntries = testMap1Entries.slice(0);
+ remap1.eachMapping(function (entry) {
+ const expected = expectedEntries.shift();
+ is(entry.generatedLine, expected.offset);
+ is(entry.generatedColumn, 0);
+ is(entry.originalLine, expected.line);
+ is(entry.originalColumn, expected.column);
+ is(entry.name, null);
+ });
+
+ const pos1 = remap1.originalPositionFor({ line: 5, column: 0 });
+ is(pos1.line, 1);
+ is(pos1.column, 7);
+ is(pos1.source, "/the/root/one.js");
+
+ const pos2 = remap1.generatedPositionFor({
+ source: "/the/root/one.js",
+ line: 2,
+ column: 18,
+ });
+ is(pos2.line, 17);
+ is(pos2.column, 0);
+ is(pos2.lastColumn, undefined);
+
+ remap1.computeColumnSpans();
+ const pos3 = remap1.allGeneratedPositionsFor({
+ source: "/the/root/one.js",
+ line: 2,
+ column: 17,
+ });
+ is(pos3.length, 1);
+ is(pos3[0].line, 14);
+ is(pos3[0].column, 0);
+ is(pos3[0].lastColumn, 0);
+
+ map1.destroy();
+});
+
+add_task(async function contentPresents() {
+ const testMap2 = {
+ version: 3,
+ file: "none.js",
+ names: [],
+ sources: ["zero.js"],
+ mappings: "",
+ sourcesContent: ["//test"],
+ };
+
+ const map2 = await new SourceMapConsumer(testMap2);
+ const remap2 = new WasmRemap(map2);
+ is(remap2.file, "none.js");
+ ok(remap2.hasContentsOfAllSources());
+ is(remap2.sourceContentFor("zero.js"), "//test");
+
+ map2.destroy();
+});
+
+add_task(async function readAndTransposeWasmMap() {
+ const source = {
+ id: "wasm.js",
+ sourceMapBaseURL: "wasm:http://example.com/whatever/:min.js",
+ sourceMapURL: `${URL_ROOT}fixtures/wasm.js.map`,
+ isWasm: true,
+ };
+
+ const urls = await gSourceMapLoader.getOriginalURLs(source);
+ Assert.deepEqual(urls, [
+ {
+ id: "wasm.js/originalSource-63954a1c231200652c0d99c6a69cd178",
+ url: `${URL_ROOT}fixtures/one.js`,
+ },
+ ]);
+
+ const { line, column } = await gSourceMapLoader.getOriginalLocation({
+ sourceId: source.id,
+ line: 5,
+ });
+ is(line, 1);
+ is(column, 7);
+});
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js
new file mode 100644
index 0000000000..adcc337eca
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js
@@ -0,0 +1,2 @@
+/* Doesn't really matter what is in here. */
+// # sourceMappingURL=absolute.js.map
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js.map
new file mode 100644
index 0000000000..0597515870
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "sources": [
+ "heart.js"
+ ],
+ "names": [],
+ "mappings": ";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA,QAAO,SAAS;AAChB;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;;;;ACfA;AACA;AACA;;;;;;;ACFA;AACA;AACA;;AAEA,mBAAkB;;;;;;;ACJlB;AACA;AACA",
+ "file": "absolute.js",
+ "sourceRoot": "https://example.com/cheese/"
+}
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js
new file mode 100644
index 0000000000..9ee477b856
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js
@@ -0,0 +1,94 @@
+/** ****/ (function(modules) { // webpackBootstrap
+/** ****/ // The module cache
+/** ****/ var installedModules = {};
+/** ****/
+/** ****/ // The require function
+/** ****/ function __webpack_require__(moduleId) {
+/** ****/
+/** ****/ // Check if module is in cache
+/** ****/ if (installedModules[moduleId])
+/** ****/ {return installedModules[moduleId].exports;}
+/** ****/
+/** ****/ // Create a new module (and put it into the cache)
+/** ****/ var module = installedModules[moduleId] = {
+/** ****/ exports: {},
+/** ****/ id: moduleId,
+/** ****/ loaded: false
+/** ****/ };
+/** ****/
+/** ****/ // Execute the module function
+/** ****/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/** ****/
+/** ****/ // Flag the module as loaded
+/** ****/ module.loaded = true;
+/** ****/
+/** ****/ // Return the exports of the module
+/** ****/ return module.exports;
+/** ****/ }
+/** ****/
+/** ****/
+/** ****/ // expose the modules object (__webpack_modules__)
+/** ****/ __webpack_require__.m = modules;
+/** ****/
+/** ****/ // expose the module cache
+/** ****/ __webpack_require__.c = installedModules;
+/** ****/
+/** ****/ // __webpack_public_path__
+/** ****/ __webpack_require__.p = "";
+/** ****/
+/** ****/ // Load entry module and return exports
+/** ****/ return __webpack_require__(0);
+/** ****/ })([
+/* 0 */
+/** */ function(module, exports, __webpack_require__) {
+
+ const times2 = __webpack_require__(1);
+ const { output } = __webpack_require__(2);
+ const opts = __webpack_require__(3);
+
+ output(times2(1));
+ output(times2(2));
+
+ if (opts.extra) {
+ output(times2(3));
+}
+
+ window.keepMeAlive = function() {
+ // This function exists to make sure this script is never garbage
+ // collected. It is also callable from tests.
+ return times2(4);
+};
+
+/** *
+/ },
+/* 1 */
+/** */ function(module, exports) {
+
+ module.exports = function(x) {
+ return x * 2;
+};
+
+/** *
+/ },
+/* 2 */
+/** */ function(module, exports) {
+
+ function output(str) {
+ console.log(str);
+}
+
+ module.exports = { output };
+
+/** *
+/ },
+/* 3 */
+/** */ function(module, exports) {
+
+ module.exports = {
+ extra: true
+};
+
+/** *
+/ }
+/** ****/ ]);
+// # sourceMappingURL=bundle.js.map
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js.map
new file mode 100644
index 0000000000..f53036cb8e
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js.map
@@ -0,0 +1,21 @@
+{
+ "version": 3,
+ "sources": [
+ "webpack:///webpack/bootstrap 4ef8c7ec7c1df790781e",
+ "webpack:///./entry.js",
+ "webpack:///./times2.js",
+ "webpack:///./output.js",
+ "webpack:///./opts.js"
+ ],
+ "names": [],
+ "mappings": ";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA,QAAO,SAAS;AAChB;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;;;;ACfA;AACA;AACA;;;;;;;ACFA;AACA;AACA;;AAEA,mBAAkB;;;;;;;ACJlB;AACA;AACA",
+ "file": "bundle.js",
+ "sourcesContent": [
+ " \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 4ef8c7ec7c1df790781e",
+ "const times2 = require(\"./times2\");\nconst { output } = require(\"./output\");\nconst opts = require(\"./opts\");\n\noutput(times2(1));\noutput(times2(2));\n\nif(opts.extra) {\n output(times2(3));\n}\n\nwindow.keepMeAlive = function() {\n // This function exists to make sure this script is never garbage\n // collected. It is also callable from tests.\n return times2(4);\n}\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./entry.js\n// module id = 0\n// module chunks = 0",
+ "module.exports = function(x) {\n return x * 2;\n}\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./times2.js\n// module id = 1\n// module chunks = 0",
+ "function output(str) {\n console.log(str);\n}\n\nmodule.exports = { output };\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./output.js\n// module id = 2\n// module chunks = 0",
+ "module.exports = {\n extra: true\n};\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./opts.js\n// module id = 3\n// module chunks = 0"
+ ],
+ "sourceRoot": ""
+}
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js
new file mode 100644
index 0000000000..72c472a745
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js
@@ -0,0 +1,2 @@
+/* Doesn't really matter what is in here. */
+// # sourceMappingURL=empty.js.map
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js.map
new file mode 100644
index 0000000000..9ebfec598d
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "sources": [
+ "heart.js"
+ ],
+ "names": [],
+ "mappings": ";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA,QAAO,SAAS;AAChB;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;;;;ACfA;AACA;AACA;;;;;;;ACFA;AACA;AACA;;AAEA,mBAAkB;;;;;;;ACJlB;AACA;AACA",
+ "file": "empty.js",
+ "sourceRoot": ""
+}
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/if.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/if.js
new file mode 100644
index 0000000000..4ee69b19ef
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/if.js
@@ -0,0 +1,12 @@
+function componentWillReceiveProps(nextProps) {
+ console.log("start");
+ const { selectedSource } = nextProps;
+
+ if (
+ nextProps.startPanelSize !== this.props.startPanelSize ||
+ nextProps.endPanelSize !== this.props.endPanelSize
+ ) {
+ this.state.editor.codeMirror.setSize();
+ }
+ console.log("done");
+}
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js
new file mode 100644
index 0000000000..9bb0752a92
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js
@@ -0,0 +1,16 @@
+"use strict";
+
+function componentWillReceiveProps(nextProps) {
+ console.log("start");
+ var selectedSource = nextProps.selectedSource;
+
+ if (
+ nextProps.startPanelSize !== this.props.startPanelSize ||
+ nextProps.endPanelSize !== this.props.endPanelSize
+ ) {
+ this.state.editor.codeMirror.setSize();
+ }
+ console.log("done");
+}
+
+//# sourceMappingURL=if.out.js.map
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js.map
new file mode 100644
index 0000000000..27feb5278f
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js.map
@@ -0,0 +1,7 @@
+{
+ "version":3,
+ "sources":["if.js"],
+ "names":[],
+ "mappings":";;AAAA,SAAS,yBAAT,CAAmC,SAAnC,EAA8C;AAC7C,UAAQ,GAAR,CAAY,OAAZ;AAD6C,MAElC,cAFkC,GAEf,SAFe,CAElC,cAFkC;;;AAI1C,MACE,UAAU,cAAV,KAA6B,KAAK,KAAL,CAAW,cAAxC,IACA,UAAU,YAAV,KAA2B,KAAK,KAAL,CAAW,YAFxC,EAGE;AACA,SAAK,KAAL,CAAW,MAAX,CAAkB,UAAlB,CAA6B,OAA7B;AACD;AACJ,UAAQ,GAAR,CAAY,MAAZ;AACA","file":"if.out.js",
+ "sourcesContent":["function componentWillReceiveProps(nextProps) {\n\tconsole.log('start');\n const { selectedSource } = nextProps;\n\n if (\n nextProps.startPanelSize !== this.props.startPanelSize ||\n nextProps.endPanelSize !== this.props.endPanelSize\n ) {\n this.state.editor.codeMirror.setSize();\n }\n\tconsole.log('done');\n}\n"]
+}
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js
new file mode 100644
index 0000000000..c3d9a150bd
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js
@@ -0,0 +1,62 @@
+"use strict";
+
+var decl = (function() {
+ var _ref = _asyncToGenerator(
+ /*#__PURE__*/ regeneratorRuntime.mark(function _callee() {
+ return regeneratorRuntime.wrap(
+ function _callee$(_context) {
+ while (1) {
+ switch ((_context.prev = _context.next)) {
+ case 0:
+ console.log("2");
+
+ case 1:
+ case "end":
+ return _context.stop();
+ }
+ }
+ },
+ _callee,
+ this
+ );
+ })
+ );
+
+ return function decl() {
+ return _ref.apply(this, arguments);
+ };
+})();
+
+function _asyncToGenerator(fn) {
+ return function() {
+ var gen = fn.apply(this, arguments);
+ return new Promise(function(resolve, reject) {
+ function step(key, arg) {
+ try {
+ var info = gen[key](arg);
+ var value = info.value;
+ } catch (error) {
+ reject(error);
+ return;
+ }
+ if (info.done) {
+ resolve(value);
+ } else {
+ return Promise.resolve(value).then(
+ function(value) {
+ step("next", value);
+ },
+ function(err) {
+ step("throw", err);
+ }
+ );
+ }
+ }
+ return step("next");
+ });
+ };
+}
+
+console.log("1");
+
+console.log("3");
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js.map
new file mode 100644
index 0000000000..2876b9900b
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js.map
@@ -0,0 +1,8 @@
+{
+ "version":3,
+ "file":"output.js",
+ "mappings":";;;qEAEA;AAAA;AAAA;AAAA;AAAA;AACE,oBAAQ,GAAR,CAAY,GAAZ;;AADF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,G;;kBAAe,I;;;;;;;AAFf,QAAQ,GAAR,CAAY,GAAZ;;AAMA,QAAQ,GAAR,CAAY,GAAZ",
+ "sources":["input.js"],
+ "sourcesContent":["console.log(\"1\")\n\nasync function decl(){\n console.log(\"2\");\n}\n\nconsole.log(\"3\");"],
+ "names":[]
+} \ No newline at end of file
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/missingmap.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/missingmap.js
new file mode 100644
index 0000000000..5a162652a7
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/missingmap.js
@@ -0,0 +1,2 @@
+// Source where the map is missing.
+// # sourceMappingURL=missingmap.js.map
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js
new file mode 100644
index 0000000000..33d15cf494
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js
@@ -0,0 +1,2 @@
+/* Doesn't really matter what is in here. */
+// # sourceMappingURL=noroot.js.map
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js.map
new file mode 100644
index 0000000000..dfeee2d765
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js.map
@@ -0,0 +1,9 @@
+{
+ "version": 3,
+ "sources": [
+ "heart.js"
+ ],
+ "names": [],
+ "mappings": ";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA,QAAO,SAAS;AAChB;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;;;;ACfA;AACA;AACA;;;;;;;ACFA;AACA;AACA;;AAEA,mBAAkB;;;;;;;ACJlB;AACA;AACA",
+ "file": "noroot.js"
+}
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js
new file mode 100644
index 0000000000..35aa77b92c
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js
@@ -0,0 +1,2 @@
+/* Doesn't really matter what is in here. */
+// # sourceMappingURL=noroot2.js.map
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js.map
new file mode 100644
index 0000000000..1d22100178
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "sources": [
+ "heart.js"
+ ],
+ "names": [],
+ "mappings": ";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA,QAAO,SAAS;AAChB;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;;;;ACfA;AACA;AACA;;;;;;;ACFA;AACA;AACA;;AAEA,mBAAkB;;;;;;;ACJlB;AACA;AACA",
+ "file": "noroot.js",
+ "sourceContents": ["this is the full text"]
+}
diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/wasm.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/wasm.js.map
new file mode 100644
index 0000000000..828f6a9aa4
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/wasm.js.map
@@ -0,0 +1,7 @@
+{
+ "version": 3,
+ "file": "wasm.js",
+ "names": [],
+ "sources": ["one.js"],
+ "mappings": "CAAC,IAAM"
+}
diff --git a/devtools/client/shared/source-map-loader/test/browser/head.js b/devtools/client/shared/source-map-loader/test/browser/head.js
new file mode 100644
index 0000000000..0f54e5a5f5
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/test/browser/head.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+const {
+ SourceMapLoader,
+} = require("resource://devtools/client/shared/source-map-loader/index.js");
+
+const gSourceMapLoader = new SourceMapLoader();
+
+function fetchFixtureSourceMap(name) {
+ gSourceMapLoader.clearSourceMaps();
+
+ const source = {
+ id: `${name}.js`,
+ sourceMapURL: `${name}.js.map`,
+ sourceMapBaseURL: `${URL_ROOT_SSL}fixtures/${name}.js`,
+ };
+
+ return gSourceMapLoader.getOriginalURLs(source);
+}
diff --git a/devtools/client/shared/source-map-loader/utils/assert.js b/devtools/client/shared/source-map-loader/utils/assert.js
new file mode 100644
index 0000000000..e784100796
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/utils/assert.js
@@ -0,0 +1,13 @@
+/* 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";
+
+function assert(condition, message) {
+ if (!condition) {
+ throw new Error(`Assertion failure: ${message}`);
+ }
+}
+
+module.exports = assert;
diff --git a/devtools/client/shared/source-map-loader/utils/fetchSourceMap.js b/devtools/client/shared/source-map-loader/utils/fetchSourceMap.js
new file mode 100644
index 0000000000..8cf7ab31a9
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/utils/fetchSourceMap.js
@@ -0,0 +1,137 @@
+/* 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 {
+ networkRequest,
+} = require("resource://devtools/client/shared/source-map-loader/utils/network-request");
+
+const {
+ SourceMapConsumer,
+} = require("resource://devtools/client/shared/vendor/source-map/source-map.js");
+const {
+ getSourceMap,
+ setSourceMap,
+} = require("resource://devtools/client/shared/source-map-loader/utils/sourceMapRequests");
+const {
+ WasmRemap,
+} = require("resource://devtools/client/shared/source-map-loader/utils/wasmRemap");
+const {
+ convertToJSON,
+} = require("resource://devtools/client/shared/source-map-loader/wasm-dwarf/convertToJSON");
+
+// URLs which have been seen in a completed source map request.
+const originalURLs = new Set();
+
+function clearOriginalURLs() {
+ originalURLs.clear();
+}
+
+function hasOriginalURL(url) {
+ return originalURLs.has(url);
+}
+
+function _resolveSourceMapURL(source) {
+ let { sourceMapBaseURL, sourceMapURL } = source;
+ sourceMapBaseURL = sourceMapBaseURL || "";
+ sourceMapURL = sourceMapURL || "";
+
+ if (!sourceMapBaseURL) {
+ // If the source doesn't have a URL, don't resolve anything.
+ return { sourceMapURL, baseURL: sourceMapURL };
+ }
+
+ let resolvedString;
+ let baseURL;
+
+ // When the sourceMap is a data: URL, fall back to using the source's URL,
+ // if possible. We don't use `new URL` here because it will be _very_ slow
+ // for large inlined source-maps, and we don't actually need to parse them.
+ if (sourceMapURL.startsWith("data:")) {
+ resolvedString = sourceMapURL;
+ baseURL = sourceMapBaseURL;
+ } else {
+ resolvedString = new URL(
+ sourceMapURL,
+ // If the URL is a data: URL, the sourceMapURL needs to be absolute, so
+ // we might as well pass `undefined` to avoid parsing a potentially
+ // very large data: URL for no reason.
+ sourceMapBaseURL.startsWith("data:") ? undefined : sourceMapBaseURL
+ ).toString();
+ baseURL = resolvedString;
+ }
+
+ return { sourceMapURL: resolvedString, baseURL };
+}
+
+async function _resolveAndFetch(generatedSource) {
+ // Fetch the sourcemap over the network and create it.
+ const { sourceMapURL, baseURL } = _resolveSourceMapURL(generatedSource);
+
+ let fetched = await networkRequest(sourceMapURL, {
+ loadFromCache: false,
+ // Blocking redirects on the sourceMappingUrl as its not easy to verify if the
+ // redirect protocol matches the supported ones.
+ allowRedirects: false,
+ sourceMapBaseURL: generatedSource.sourceMapBaseURL,
+ });
+
+ if (fetched.isDwarf) {
+ fetched = { content: await convertToJSON(fetched.content) };
+ }
+
+ // Create the source map and fix it up.
+ let map = await new SourceMapConsumer(fetched.content, baseURL);
+
+ if (generatedSource.isWasm) {
+ map = new WasmRemap(map);
+ // Check if experimental scope info exists.
+ if (fetched.content.includes("x-scopes")) {
+ const parsedJSON = JSON.parse(fetched.content);
+ map.xScopes = parsedJSON["x-scopes"];
+ }
+ }
+
+ // Extend the default map object with sourceMapBaseURL, used to check further
+ // network requests made for this sourcemap.
+ map.sourceMapBaseURL = generatedSource.sourceMapBaseURL;
+
+ if (map && map.sources) {
+ map.sources.forEach(url => originalURLs.add(url));
+ }
+
+ return map;
+}
+
+function fetchSourceMap(generatedSource) {
+ const existingRequest = getSourceMap(generatedSource.id);
+
+ // If it has already been requested, return the request. Make sure
+ // to do this even if sourcemapping is turned off, because
+ // pretty-printing uses sourcemaps.
+ //
+ // An important behavior here is that if it's in the middle of
+ // requesting it, all subsequent calls will block on the initial
+ // request.
+ if (existingRequest) {
+ return existingRequest;
+ }
+
+ if (!generatedSource.sourceMapURL) {
+ return null;
+ }
+
+ // Fire off the request, set it in the cache, and return it.
+ const req = _resolveAndFetch(generatedSource);
+ // Make sure the cached promise does not reject, because we only
+ // want to report the error once.
+ setSourceMap(
+ generatedSource.id,
+ req.catch(() => null)
+ );
+ return req;
+}
+
+module.exports = { fetchSourceMap, hasOriginalURL, clearOriginalURLs };
diff --git a/devtools/client/shared/source-map-loader/utils/getOriginalStackFrames.js b/devtools/client/shared/source-map-loader/utils/getOriginalStackFrames.js
new file mode 100644
index 0000000000..c1ffee10e4
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/utils/getOriginalStackFrames.js
@@ -0,0 +1,38 @@
+/* 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 {
+ getWasmXScopes,
+} = require("resource://devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes");
+const {
+ getSourceMap,
+} = require("resource://devtools/client/shared/source-map-loader/utils/sourceMapRequests");
+const {
+ generatedToOriginalId,
+} = require("resource://devtools/client/shared/source-map-loader/utils/index");
+
+// Returns expanded stack frames details based on the generated location.
+// The function return null if not information was found.
+async function getOriginalStackFrames(generatedLocation) {
+ const wasmXScopes = await getWasmXScopes(generatedLocation.sourceId, {
+ getSourceMap,
+ generatedToOriginalId,
+ });
+ if (!wasmXScopes) {
+ return null;
+ }
+
+ const scopes = wasmXScopes.search(generatedLocation);
+ if (scopes.length === 0) {
+ console.warn("Something wrong with debug data: none original frames found");
+ return null;
+ }
+ return scopes;
+}
+
+module.exports = {
+ getOriginalStackFrames,
+};
diff --git a/devtools/client/shared/source-map-loader/utils/index.js b/devtools/client/shared/source-map-loader/utils/index.js
new file mode 100644
index 0000000000..1bcfcfac7d
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/utils/index.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 md5 = require("resource://devtools/client/shared/vendor/md5.js");
+
+function originalToGeneratedId(sourceId) {
+ if (isGeneratedId(sourceId)) {
+ return sourceId;
+ }
+
+ const lastIndex = sourceId.lastIndexOf("/originalSource");
+
+ return lastIndex !== -1 ? sourceId.slice(0, lastIndex) : "";
+}
+
+const getMd5 = memoize(url => md5(url));
+
+function generatedToOriginalId(generatedId, url) {
+ return `${generatedId}/originalSource-${getMd5(url)}`;
+}
+
+function isOriginalId(id) {
+ return id.includes("/originalSource");
+}
+
+function isGeneratedId(id) {
+ return !isOriginalId(id);
+}
+
+/**
+ * Trims the query part or reference identifier of a URL string, if necessary.
+ */
+function trimUrlQuery(url) {
+ const length = url.length;
+
+ for (let i = 0; i < length; ++i) {
+ if (url[i] === "?" || url[i] === "&" || url[i] === "#") {
+ return url.slice(0, i);
+ }
+ }
+
+ return url;
+}
+
+// Map suffix to content type.
+const contentMap = {
+ js: "text/javascript",
+ jsm: "text/javascript",
+ mjs: "text/javascript",
+ ts: "text/typescript",
+ tsx: "text/typescript-jsx",
+ jsx: "text/jsx",
+ vue: "text/vue",
+ coffee: "text/coffeescript",
+ elm: "text/elm",
+ cljc: "text/x-clojure",
+ cljs: "text/x-clojurescript",
+};
+
+/**
+ * Returns the content type for the specified URL. If no specific
+ * content type can be determined, "text/plain" is returned.
+ *
+ * @return String
+ * The content type.
+ */
+function getContentType(url) {
+ url = trimUrlQuery(url);
+ const dot = url.lastIndexOf(".");
+ if (dot >= 0) {
+ const name = url.substring(dot + 1);
+ if (name in contentMap) {
+ return contentMap[name];
+ }
+ }
+ return "text/plain";
+}
+
+function memoize(func) {
+ const map = new Map();
+
+ return arg => {
+ if (map.has(arg)) {
+ return map.get(arg);
+ }
+
+ const result = func(arg);
+ map.set(arg, result);
+ return result;
+ };
+}
+
+module.exports = {
+ originalToGeneratedId,
+ generatedToOriginalId,
+ isOriginalId,
+ isGeneratedId,
+ getContentType,
+ contentMapForTesting: contentMap,
+};
diff --git a/devtools/client/shared/source-map-loader/utils/moz.build b/devtools/client/shared/source-map-loader/utils/moz.build
new file mode 100644
index 0000000000..ffa77d72b7
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/utils/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "assert.js",
+ "fetchSourceMap.js",
+ "getOriginalStackFrames.js",
+ "index.js",
+ "network-request.js",
+ "sourceMapRequests.js",
+ "wasmRemap.js",
+)
diff --git a/devtools/client/shared/source-map-loader/utils/network-request.js b/devtools/client/shared/source-map-loader/utils/network-request.js
new file mode 100644
index 0000000000..f331dc446e
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/utils/network-request.js
@@ -0,0 +1,43 @@
+/* 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";
+
+async function networkRequest(url, opts) {
+ const supportedProtocols = ["http:", "https:", "data:"];
+
+ // Add file, chrome or moz-extension if the initial source was served by the
+ // same protocol.
+ const ADDITIONAL_PROTOCOLS = ["chrome:", "file:", "moz-extension:"];
+ for (const protocol of ADDITIONAL_PROTOCOLS) {
+ if (opts.sourceMapBaseURL?.startsWith(protocol)) {
+ supportedProtocols.push(protocol);
+ }
+ }
+
+ if (supportedProtocols.every(protocol => !url.startsWith(protocol))) {
+ throw new Error(`unsupported protocol for sourcemap request ${url}`);
+ }
+
+ const response = await fetch(url, {
+ cache: opts.loadFromCache ? "default" : "no-cache",
+ redirect: opts.allowRedirects ? "follow" : "error",
+ });
+
+ if (response.ok) {
+ if (response.headers.get("Content-Type") === "application/wasm") {
+ const buffer = await response.arrayBuffer();
+ return {
+ content: buffer,
+ isDwarf: true,
+ };
+ }
+ const text = await response.text();
+ return { content: text };
+ }
+
+ throw new Error(`request failed with status ${response.status}`);
+}
+
+module.exports = { networkRequest };
diff --git a/devtools/client/shared/source-map-loader/utils/sourceMapRequests.js b/devtools/client/shared/source-map-loader/utils/sourceMapRequests.js
new file mode 100644
index 0000000000..49ceecd71a
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/utils/sourceMapRequests.js
@@ -0,0 +1,106 @@
+/* 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 {
+ generatedToOriginalId,
+} = require("resource://devtools/client/shared/source-map-loader/utils/index.js");
+
+const sourceMapRequests = new Map();
+
+function clearSourceMaps() {
+ for (const [, metadataPromise] of sourceMapRequests) {
+ // The source-map module leaks memory unless `.destroy` is called on
+ // the consumer instances when they are no longer being used.
+ metadataPromise.then(
+ metadata => {
+ if (metadata) {
+ metadata.map.destroy();
+ }
+ },
+ // We don't want this to cause any unhandled rejection errors.
+ () => {}
+ );
+ }
+
+ sourceMapRequests.clear();
+}
+
+/**
+ * For a given generated source, retrieve an object with many attributes:
+ * @param {String} generatedSourceId
+ * The id of the generated source
+ *
+ * @return {Object} Meta data object with many attributes
+ * - map: The SourceMapConsumer or WasmRemap instance
+ * - urlsById Map of Original Source ID (string) to Source URL (string)
+ * - sources: Array of object with the two following attributes:
+ * - id: Original Source ID (string)
+ * - url: Original Source URL (string)
+ */
+function getSourceMapWithMetadata(generatedSourceId) {
+ return sourceMapRequests.get(generatedSourceId);
+}
+
+/**
+ * Retrieve the SourceMapConsumer or WasmRemap instance for a given generated source.
+ *
+ * @param {String} generatedSourceId
+ * The id of the generated source
+ *
+ * @return null | Promise<SourceMapConsumer | WasmRemap>
+ */
+function getSourceMap(generatedSourceId) {
+ const request = getSourceMapWithMetadata(generatedSourceId);
+ if (!request) {
+ return null;
+ }
+
+ return request.then(result => result?.map);
+}
+
+/**
+ * Record the SourceMapConsumer or WasmRemap instance for a given generated source.
+ *
+ * @param {String} generatedId
+ * The generated source ID.
+ * @param {Promise<SourceMapConsumer or WasmRemap>} request
+ * A promise which should resolve to either a SourceMapConsume or WasmRemap instance.
+ */
+function setSourceMap(generatedId, request) {
+ sourceMapRequests.set(
+ generatedId,
+ request.then(map => {
+ if (!map || !map.sources) {
+ return null;
+ }
+
+ const urlsById = new Map();
+ const sources = [];
+ let ignoreListUrls = [];
+
+ if (map.x_google_ignoreList?.length) {
+ ignoreListUrls = map.x_google_ignoreList.map(
+ sourceIndex => map.sources[sourceIndex]
+ );
+ }
+
+ for (const url of map.sources) {
+ const id = generatedToOriginalId(generatedId, url);
+
+ urlsById.set(id, url);
+ sources.push({ id, url });
+ }
+ return { map, urlsById, sources, ignoreListUrls };
+ })
+ );
+}
+
+module.exports = {
+ clearSourceMaps,
+ getSourceMapWithMetadata,
+ getSourceMap,
+ setSourceMap,
+};
diff --git a/devtools/client/shared/source-map-loader/utils/wasmRemap.js b/devtools/client/shared/source-map-loader/utils/wasmRemap.js
new file mode 100644
index 0000000000..6b831c2919
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/utils/wasmRemap.js
@@ -0,0 +1,107 @@
+/* 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";
+
+/**
+ * SourceMapConsumer for WebAssembly source maps. It transposes columns with
+ * lines, which allows mapping data to be used with SpiderMonkey Debugger API.
+ */
+class WasmRemap {
+ /**
+ * @param map SourceMapConsumer
+ */
+ constructor(map) {
+ this._map = map;
+ this.version = map.version;
+ this.file = map.file;
+ this._computeColumnSpans = false;
+ }
+
+ get sources() {
+ return this._map.sources;
+ }
+
+ get sourceRoot() {
+ return this._map.sourceRoot;
+ }
+
+ get names() {
+ return this._map.names;
+ }
+
+ get sourcesContent() {
+ return this._map.sourcesContent;
+ }
+
+ get mappings() {
+ throw new Error("not supported");
+ }
+
+ computeColumnSpans() {
+ this._computeColumnSpans = true;
+ }
+
+ originalPositionFor(generatedPosition) {
+ const result = this._map.originalPositionFor({
+ line: 1,
+ column: generatedPosition.line,
+ bias: generatedPosition.bias,
+ });
+ return result;
+ }
+
+ _remapGeneratedPosition(position) {
+ const generatedPosition = {
+ line: position.column,
+ column: 0,
+ };
+ if (this._computeColumnSpans) {
+ generatedPosition.lastColumn = 0;
+ }
+ return generatedPosition;
+ }
+
+ generatedPositionFor(originalPosition) {
+ const position = this._map.generatedPositionFor(originalPosition);
+ return this._remapGeneratedPosition(position);
+ }
+
+ allGeneratedPositionsFor(originalPosition) {
+ const positions = this._map.allGeneratedPositionsFor(originalPosition);
+ return positions.map(position => {
+ return this._remapGeneratedPosition(position);
+ });
+ }
+
+ hasContentsOfAllSources() {
+ return this._map.hasContentsOfAllSources();
+ }
+
+ sourceContentFor(source, returnNullOnMissing) {
+ return this._map.sourceContentFor(source, returnNullOnMissing);
+ }
+
+ eachMapping(callback, context, order) {
+ this._map.eachMapping(
+ entry => {
+ const { source, generatedColumn, originalLine, originalColumn, name } =
+ entry;
+ callback({
+ source,
+ generatedLine: generatedColumn,
+ generatedColumn: 0,
+ lastGeneratedColumn: 0,
+ originalLine,
+ originalColumn,
+ name,
+ });
+ },
+ context,
+ order
+ );
+ }
+}
+
+exports.WasmRemap = WasmRemap;
diff --git a/devtools/client/shared/source-map-loader/wasm-dwarf/convertToJSON.js b/devtools/client/shared/source-map-loader/wasm-dwarf/convertToJSON.js
new file mode 100644
index 0000000000..9faae5c901
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/wasm-dwarf/convertToJSON.js
@@ -0,0 +1,66 @@
+/* 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/>. */
+
+/* eslint camelcase: 0*/
+
+"use strict";
+
+const {
+ getDwarfToWasmData,
+} = require("resource://devtools/client/shared/source-map-loader/wasm-dwarf/wasmAsset.js");
+
+let cachedWasmModule;
+let utf8Decoder;
+
+function convertDwarf(wasm, instance) {
+ const { memory, alloc_mem, free_mem, convert_dwarf } = instance.exports;
+ const wasmPtr = alloc_mem(wasm.byteLength);
+ new Uint8Array(memory.buffer, wasmPtr, wasm.byteLength).set(
+ new Uint8Array(wasm)
+ );
+ const resultPtr = alloc_mem(12);
+ const enableXScopes = true;
+ const success = convert_dwarf(
+ wasmPtr,
+ wasm.byteLength,
+ resultPtr,
+ resultPtr + 4,
+ enableXScopes
+ );
+ free_mem(wasmPtr);
+ const resultView = new DataView(memory.buffer, resultPtr, 12);
+ const outputPtr = resultView.getUint32(0, true),
+ outputLen = resultView.getUint32(4, true);
+ free_mem(resultPtr);
+ if (!success) {
+ throw new Error("Unable to convert from DWARF sections");
+ }
+ if (!utf8Decoder) {
+ utf8Decoder = new TextDecoder("utf-8");
+ }
+ const output = utf8Decoder.decode(
+ new Uint8Array(memory.buffer, outputPtr, outputLen)
+ );
+ free_mem(outputPtr);
+ return output;
+}
+
+async function convertToJSON(buffer) {
+ // Note: We don't 'await' here because it could mean that multiple
+ // calls to 'convertToJSON' could cause multiple fetches to be started.
+ cachedWasmModule = cachedWasmModule || loadConverterModule();
+
+ return convertDwarf(buffer, await cachedWasmModule);
+}
+
+async function loadConverterModule() {
+ const wasm = await getDwarfToWasmData();
+ const imports = {};
+ const { instance } = await WebAssembly.instantiate(wasm, imports);
+ return instance;
+}
+
+module.exports = {
+ convertToJSON,
+};
diff --git a/devtools/client/shared/source-map-loader/wasm-dwarf/dwarf_to_json.wasm b/devtools/client/shared/source-map-loader/wasm-dwarf/dwarf_to_json.wasm
new file mode 100644
index 0000000000..c58b189035
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/wasm-dwarf/dwarf_to_json.wasm
Binary files differ
diff --git a/devtools/client/shared/source-map-loader/wasm-dwarf/moz.build b/devtools/client/shared/source-map-loader/wasm-dwarf/moz.build
new file mode 100644
index 0000000000..8bc40676fa
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/wasm-dwarf/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "convertToJSON.js",
+ "wasmAsset.js",
+ "wasmDwarfExpressions.js",
+ "wasmXScopes.js",
+)
diff --git a/devtools/client/shared/source-map-loader/wasm-dwarf/wasmAsset.js b/devtools/client/shared/source-map-loader/wasm-dwarf/wasmAsset.js
new file mode 100644
index 0000000000..b4de822ffd
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/wasm-dwarf/wasmAsset.js
@@ -0,0 +1,17 @@
+/* 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";
+
+async function getDwarfToWasmData(name) {
+ const response = await fetch(
+ "resource://devtools/client/shared/source-map-loader/wasm-dwarf/dwarf_to_json.wasm"
+ );
+
+ return response.arrayBuffer();
+}
+
+module.exports = {
+ getDwarfToWasmData,
+};
diff --git a/devtools/client/shared/source-map-loader/wasm-dwarf/wasmDwarfExpressions.js b/devtools/client/shared/source-map-loader/wasm-dwarf/wasmDwarfExpressions.js
new file mode 100644
index 0000000000..c2f124e99f
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/wasm-dwarf/wasmDwarfExpressions.js
@@ -0,0 +1,260 @@
+/* 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/>. */
+
+/* eslint camelcase: 0*/
+/* eslint-disable no-inline-comments */
+
+"use strict";
+
+class Value {
+ val;
+
+ constructor(val) {
+ this.val = val;
+ }
+ toString() {
+ return `${this.val}`;
+ }
+}
+
+const Int32Formatter = {
+ fromAddr(addr) {
+ return `(new DataView(memory0.buffer).getInt32(${addr}, true))`;
+ },
+ fromValue(value) {
+ return `${value.val}`;
+ },
+};
+
+const Uint32Formatter = {
+ fromAddr(addr) {
+ return `(new DataView(memory0.buffer).getUint32(${addr}, true))`;
+ },
+ fromValue(value) {
+ return `(${value.val} >>> 0)`;
+ },
+};
+
+function createPieceFormatter(bytes) {
+ let getter;
+ switch (bytes) {
+ case 0:
+ case 1:
+ getter = "getUint8";
+ break;
+ case 2:
+ getter = "getUint16";
+ break;
+ case 3:
+ case 4:
+ default:
+ // FIXME 64-bit
+ getter = "getUint32";
+ break;
+ }
+ const mask = (1 << (8 * bytes)) - 1;
+ return {
+ fromAddr(addr) {
+ return `(new DataView(memory0.buffer).${getter}(${addr}, true))`;
+ },
+ fromValue(value) {
+ return `((${value.val} & ${mask}) >>> 0)`;
+ },
+ };
+}
+
+// eslint-disable-next-line complexity
+function toJS(buf, typeFormatter, frame_base = "fp()") {
+ const readU8 = function () {
+ return buf[i++];
+ };
+ const readS8 = function () {
+ return (readU8() << 24) >> 24;
+ };
+ const readU16 = function () {
+ const w = buf[i] | (buf[i + 1] << 8);
+ i += 2;
+ return w;
+ };
+ const readS16 = function () {
+ return (readU16() << 16) >> 16;
+ };
+ const readS32 = function () {
+ const w =
+ buf[i] | (buf[i + 1] << 8) | (buf[i + 2] << 16) | (buf[i + 3] << 24);
+ i += 4;
+ return w;
+ };
+ const readU32 = function () {
+ return readS32() >>> 0;
+ };
+ const readU = function () {
+ let n = 0,
+ shift = 0,
+ b;
+ while ((b = readU8()) & 0x80) {
+ n |= (b & 0x7f) << shift;
+ shift += 7;
+ }
+ return n | (b << shift);
+ };
+ const readS = function () {
+ let n = 0,
+ shift = 0,
+ b;
+ while ((b = readU8()) & 0x80) {
+ n |= (b & 0x7f) << shift;
+ shift += 7;
+ }
+ n |= b << shift;
+ shift += 7;
+ return shift > 32 ? (n << (32 - shift)) >> (32 - shift) : n;
+ };
+ const popValue = function (formatter) {
+ const loc = stack.pop();
+ if (loc instanceof Value) {
+ return formatter.fromValue(loc);
+ }
+ return formatter.fromAddr(loc);
+ };
+ let i = 0,
+ a,
+ b;
+ const stack = [frame_base];
+ while (i < buf.length) {
+ const code = buf[i++];
+ switch (code) {
+ case 0x03 /* DW_OP_addr */:
+ stack.push(Uint32Formatter.fromAddr(readU32()));
+ break;
+ case 0x08 /* DW_OP_const1u 0x08 1 1-byte constant */:
+ stack.push(readU8());
+ break;
+ case 0x09 /* DW_OP_const1s 0x09 1 1-byte constant */:
+ stack.push(readS8());
+ break;
+ case 0x0a /* DW_OP_const2u 0x0a 1 2-byte constant */:
+ stack.push(readU16());
+ break;
+ case 0x0b /* DW_OP_const2s 0x0b 1 2-byte constant */:
+ stack.push(readS16());
+ break;
+ case 0x0c /* DW_OP_const2u 0x0a 1 2-byte constant */:
+ stack.push(readU32());
+ break;
+ case 0x0d /* DW_OP_const2s 0x0b 1 2-byte constant */:
+ stack.push(readS32());
+ break;
+ case 0x10 /* DW_OP_constu 0x10 1 ULEB128 constant */:
+ stack.push(readU());
+ break;
+ case 0x11 /* DW_OP_const2s 0x0b 1 2-byte constant */:
+ stack.push(readS());
+ break;
+
+ case 0x1c /* DW_OP_minus */:
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(`${a} - ${b}`);
+ break;
+
+ case 0x22 /* DW_OP_plus */:
+ b = stack.pop();
+ a = stack.pop();
+ stack.push(`${a} + ${b}`);
+ break;
+
+ case 0x23 /* DW_OP_plus_uconst */:
+ b = readU();
+ a = stack.pop();
+ stack.push(`${a} + ${b}`);
+ break;
+
+ case 0x30 /* DW_OP_lit0 */:
+ case 0x31:
+ case 0x32:
+ case 0x33:
+ case 0x34:
+ case 0x35:
+ case 0x36:
+ case 0x37:
+ case 0x38:
+ case 0x39:
+ case 0x3a:
+ case 0x3b:
+ case 0x3c:
+ case 0x3d:
+ case 0x3e:
+ case 0x3f:
+ case 0x40:
+ case 0x41:
+ case 0x42:
+ case 0x43:
+ case 0x44:
+ case 0x45:
+ case 0x46:
+ case 0x47:
+ case 0x48:
+ case 0x49:
+ case 0x4a:
+ case 0x4b:
+ case 0x4c:
+ case 0x4d:
+ case 0x4e:
+ case 0x4f:
+ stack.push(`${code - 0x30}`);
+ break;
+
+ case 0x93 /* DW_OP_piece */: {
+ a = readU();
+ const formatter = createPieceFormatter(a);
+ stack.push(popValue(formatter));
+ break;
+ }
+
+ case 0x9f /* DW_OP_stack_value */:
+ stack.push(new Value(stack.pop()));
+ break;
+
+ case 0xf6 /* WASM ext (old, FIXME phase out) */:
+ case 0xed /* WASM ext */:
+ b = readU();
+ a = readS();
+ switch (b) {
+ case 0:
+ stack.push(`var${a}`);
+ break;
+ case 1:
+ stack.push(`global${a}`);
+ break;
+ default:
+ stack.push(`ti${b}(${a})`);
+ break;
+ }
+ break;
+
+ default:
+ // Unknown encoding, baling out
+ return null;
+ }
+ }
+ // FIXME use real DWARF type information
+ return popValue(typeFormatter);
+}
+
+function decodeExpr(expr) {
+ if (expr.includes("//")) {
+ expr = expr.slice(0, expr.indexOf("//")).trim();
+ }
+ const code = new Uint8Array(expr.length >> 1);
+ for (let i = 0; i < code.length; i++) {
+ code[i] = parseInt(expr.substr(i << 1, 2), 16);
+ }
+ const typeFormatter = Int32Formatter;
+ return toJS(code, typeFormatter) || `dwarf("${expr}")`;
+}
+
+module.exports = {
+ decodeExpr,
+};
diff --git a/devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes.js b/devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes.js
new file mode 100644
index 0000000000..82faa51dbe
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes.js
@@ -0,0 +1,215 @@
+/* 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/>. */
+
+/* eslint camelcase: 0*/
+
+"use strict";
+
+const {
+ decodeExpr,
+} = require("resource://devtools/client/shared/source-map-loader/wasm-dwarf/wasmDwarfExpressions");
+
+const xScopes = new Map();
+
+function indexLinkingNames(items) {
+ const result = new Map();
+ let queue = [...items];
+ while (queue.length) {
+ const item = queue.shift();
+ if ("uid" in item) {
+ result.set(item.uid, item);
+ } else if ("linkage_name" in item) {
+ // TODO the linkage_name string value is used for compatibility
+ // with old format. Remove in favour of the uid referencing.
+ result.set(item.linkage_name, item);
+ }
+ if ("children" in item) {
+ queue = [...queue, ...item.children];
+ }
+ }
+ return result;
+}
+
+function getIndexedItem(index, key) {
+ if (typeof key === "object" && key != null) {
+ return index.get(key.uid);
+ }
+ if (typeof key === "string") {
+ return index.get(key);
+ }
+ return null;
+}
+
+async function getXScopes(sourceId, getSourceMap) {
+ if (xScopes.has(sourceId)) {
+ return xScopes.get(sourceId);
+ }
+ const map = await getSourceMap(sourceId);
+ if (!map || !map.xScopes) {
+ xScopes.set(sourceId, null);
+ return null;
+ }
+ const { code_section_offset, debug_info } = map.xScopes;
+ const xScope = {
+ code_section_offset,
+ debug_info,
+ idIndex: indexLinkingNames(debug_info),
+ sources: map.sources,
+ };
+ xScopes.set(sourceId, xScope);
+ return xScope;
+}
+
+function isInRange(item, pc) {
+ if ("ranges" in item) {
+ return item.ranges.some(r => r[0] <= pc && pc < r[1]);
+ }
+ if ("high_pc" in item) {
+ return item.low_pc <= pc && pc < item.high_pc;
+ }
+ return false;
+}
+
+function decodeExprAt(expr, pc) {
+ if (typeof expr === "string") {
+ return decodeExpr(expr);
+ }
+ const foundAt = expr.find(i => i.range[0] <= pc && pc < i.range[1]);
+ return foundAt ? decodeExpr(foundAt.expr) : null;
+}
+
+function getVariables(scope, pc) {
+ const vars = scope.children
+ ? scope.children.reduce((result, item) => {
+ switch (item.tag) {
+ case "variable":
+ case "formal_parameter":
+ result.push({
+ name: item.name || "",
+ expr: item.location ? decodeExprAt(item.location, pc) : null,
+ });
+ break;
+ case "lexical_block":
+ // FIXME build scope blocks (instead of combining)
+ const tmp = getVariables(item, pc);
+ result = [...tmp.vars, ...result];
+ break;
+ }
+ return result;
+ }, [])
+ : [];
+ const frameBase = scope.frame_base ? decodeExpr(scope.frame_base) : null;
+ return {
+ vars,
+ frameBase,
+ };
+}
+
+function filterScopes(items, pc, lastItem, index) {
+ if (!items) {
+ return [];
+ }
+ return items.reduce((result, item) => {
+ switch (item.tag) {
+ case "compile_unit":
+ if (isInRange(item, pc)) {
+ result = [
+ ...result,
+ ...filterScopes(item.children, pc, lastItem, index),
+ ];
+ }
+ break;
+ case "namespace":
+ case "structure_type":
+ case "union_type":
+ result = [
+ ...result,
+ ...filterScopes(item.children, pc, lastItem, index),
+ ];
+ break;
+ case "subprogram":
+ if (isInRange(item, pc)) {
+ const s = {
+ id: item.linkage_name,
+ name: item.name,
+ variables: getVariables(item, pc),
+ };
+ result = [...result, s, ...filterScopes(item.children, pc, s, index)];
+ }
+ break;
+ case "inlined_subroutine":
+ if (isInRange(item, pc)) {
+ const linkedItem = getIndexedItem(index, item.abstract_origin);
+ const s = {
+ id: item.abstract_origin,
+ name: linkedItem ? linkedItem.name : void 0,
+ variables: getVariables(item, pc),
+ };
+ if (lastItem) {
+ lastItem.file = item.call_file;
+ lastItem.line = item.call_line;
+ }
+ result = [...result, s, ...filterScopes(item.children, pc, s, index)];
+ }
+ break;
+ }
+ return result;
+ }, []);
+}
+
+class XScope {
+ xScope;
+ sourceMapContext;
+
+ constructor(xScopeData, sourceMapContext) {
+ this.xScope = xScopeData;
+ this.sourceMapContext = sourceMapContext;
+ }
+
+ search(generatedLocation) {
+ const { code_section_offset, debug_info, sources, idIndex } = this.xScope;
+ const pc = generatedLocation.line - (code_section_offset || 0);
+ const scopes = filterScopes(debug_info, pc, null, idIndex);
+ scopes.reverse();
+
+ return scopes.map(i => {
+ if (!("file" in i)) {
+ return {
+ displayName: i.name || "",
+ variables: i.variables,
+ };
+ }
+ const sourceId = this.sourceMapContext.generatedToOriginalId(
+ generatedLocation.sourceId,
+ sources[i.file || 0]
+ );
+ return {
+ displayName: i.name || "",
+ variables: i.variables,
+ location: {
+ line: i.line || 0,
+ sourceId,
+ },
+ };
+ });
+ }
+}
+
+async function getWasmXScopes(sourceId, sourceMapContext) {
+ const { getSourceMap } = sourceMapContext;
+ const xScopeData = await getXScopes(sourceId, getSourceMap);
+ if (!xScopeData) {
+ return null;
+ }
+ return new XScope(xScopeData, sourceMapContext);
+}
+
+function clearWasmXScopes() {
+ xScopes.clear();
+}
+
+module.exports = {
+ getWasmXScopes,
+ clearWasmXScopes,
+};
diff --git a/devtools/client/shared/source-map-loader/worker.js b/devtools/client/shared/source-map-loader/worker.js
new file mode 100644
index 0000000000..45b232a509
--- /dev/null
+++ b/devtools/client/shared/source-map-loader/worker.js
@@ -0,0 +1,52 @@
+/* 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/>. */
+
+/* eslint-env worker */
+
+"use strict";
+
+importScripts("resource://gre/modules/workers/require.js");
+
+const {
+ getOriginalURLs,
+ hasOriginalURL,
+ getOriginalRanges,
+ getGeneratedRanges,
+ getGeneratedLocation,
+ getOriginalLocation,
+ getOriginalLocations,
+ getOriginalSourceText,
+ getGeneratedRangesForOriginal,
+ getFileGeneratedRange,
+ getSourceMapIgnoreList,
+ clearSourceMaps,
+ setSourceMapForGeneratedSources,
+} = require("resource://devtools/client/shared/source-map-loader/source-map.js");
+
+const {
+ getOriginalStackFrames,
+} = require("resource://devtools/client/shared/source-map-loader/utils/getOriginalStackFrames.js");
+
+const {
+ workerHandler,
+} = require("resource://devtools/client/shared/worker-utils.js");
+
+// The interface is implemented in source-map to be
+// easier to unit test.
+self.onmessage = workerHandler({
+ getOriginalURLs,
+ hasOriginalURL,
+ getOriginalRanges,
+ getGeneratedRanges,
+ getGeneratedLocation,
+ getOriginalLocation,
+ getOriginalLocations,
+ getOriginalSourceText,
+ getOriginalStackFrames,
+ getGeneratedRangesForOriginal,
+ getFileGeneratedRange,
+ getSourceMapIgnoreList,
+ setSourceMapForGeneratedSources,
+ clearSourceMaps,
+});