diff options
Diffstat (limited to 'devtools/client/framework/source-map-worker.js')
-rw-r--r-- | devtools/client/framework/source-map-worker.js | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/devtools/client/framework/source-map-worker.js b/devtools/client/framework/source-map-worker.js new file mode 100644 index 000000000..c68732f38 --- /dev/null +++ b/devtools/client/framework/source-map-worker.js @@ -0,0 +1,220 @@ +const { fetch, assert } = require("devtools/shared/DevToolsUtils"); +const { joinURI } = require("devtools/shared/path"); +const path = require("sdk/fs/path"); +const { SourceMapConsumer, SourceMapGenerator } = require("source-map"); +const { isJavaScript } = require("./source"); +const { + originalToGeneratedId, + generatedToOriginalId, + isGeneratedId, + isOriginalId +} = require("./source-map-util"); + +let sourceMapRequests = new Map(); +let sourceMapsEnabled = false; + +function clearSourceMaps() { + sourceMapRequests.clear(); +} + +function enableSourceMaps() { + sourceMapsEnabled = true; +} + +function _resolveSourceMapURL(source) { + const { url = "", sourceMapURL = "" } = source; + if (path.isURL(sourceMapURL) || url == "") { + // If it's already a full URL or the source doesn't have a URL, + // don't resolve anything. + return sourceMapURL; + } else if (path.isAbsolute(sourceMapURL)) { + // If it's an absolute path, it should be resolved relative to the + // host of the source. + const { protocol = "", host = "" } = parse(url); + return `${protocol}//${host}${sourceMapURL}`; + } + // Otherwise, it's a relative path and should be resolved relative + // to the source. + return dirname(url) + "/" + sourceMapURL; +} + +/** + * Sets the source map's sourceRoot to be relative to the source map url. + * @memberof utils/source-map-worker + * @static + */ +function _setSourceMapRoot(sourceMap, absSourceMapURL, source) { + // No need to do this fiddling if we won't be fetching any sources over the + // wire. + if (sourceMap.hasContentsOfAllSources()) { + return; + } + + const base = dirname( + (absSourceMapURL.indexOf("data:") === 0 && source.url) ? + source.url : + absSourceMapURL + ); + + if (sourceMap.sourceRoot) { + sourceMap.sourceRoot = joinURI(base, sourceMap.sourceRoot); + } else { + sourceMap.sourceRoot = base; + } + + return sourceMap; +} + +function _getSourceMap(generatedSourceId) + : ?Promise<SourceMapConsumer> { + return sourceMapRequests.get(generatedSourceId); +} + +async function _resolveAndFetch(generatedSource) : SourceMapConsumer { + // Fetch the sourcemap over the network and create it. + const sourceMapURL = _resolveSourceMapURL(generatedSource); + const fetched = await fetch( + sourceMapURL, { loadFromCache: false } + ); + + // Create the source map and fix it up. + const map = new SourceMapConsumer(fetched.content); + _setSourceMapRoot(map, sourceMapURL, generatedSource); + return map; +} + +function _fetchSourceMap(generatedSource) { + const existingRequest = sourceMapRequests.get(generatedSource.id); + if (existingRequest) { + // 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. + return existingRequest; + } else if (!generatedSource.sourceMapURL || !sourceMapsEnabled) { + return Promise.resolve(null); + } + + // Fire off the request, set it in the cache, and return it. + // Suppress any errors and just return null (ignores bogus + // sourcemaps). + const req = _resolveAndFetch(generatedSource).catch(() => null); + sourceMapRequests.set(generatedSource.id, req); + return req; +} + +async function getOriginalURLs(generatedSource) { + const map = await _fetchSourceMap(generatedSource); + return map && map.sources; +} + +async function getGeneratedLocation(location: Location, originalSource: Source) + : Promise<Location> { + if (!isOriginalId(location.sourceId)) { + return location; + } + + const generatedSourceId = originalToGeneratedId(location.sourceId); + const map = await _getSourceMap(generatedSourceId); + if (!map) { + return location; + } + + const { line, column } = map.generatedPositionFor({ + source: originalSource.url, + line: location.line, + column: location.column == null ? 0 : location.column + }); + + return { + sourceId: generatedSourceId, + line: line, + // Treat 0 as no column so that line breakpoints work correctly. + column: column === 0 ? undefined : column + }; +} + +async function getOriginalLocation(location) { + if (!isGeneratedId(location.sourceId)) { + return location; + } + + const map = await _getSourceMap(location.sourceId); + if (!map) { + return location; + } + + const { source: url, line, column } = map.originalPositionFor({ + line: location.line, + column: location.column == null ? Infinity : location.column + }); + + if (url == null) { + // No url means the location didn't map. + return location; + } + + return { + sourceId: generatedToOriginalId(location.sourceId, url), + line, + column + }; +} + +async function getOriginalSourceText(originalSource) { + assert(isOriginalId(originalSource.id), + "Source is not an original source"); + + const generatedSourceId = originalToGeneratedId(originalSource.id); + const map = await _getSourceMap(generatedSourceId); + if (!map) { + return null; + } + + let text = map.sourceContentFor(originalSource.url); + if (!text) { + text = (await fetch( + originalSource.url, { loadFromCache: false } + )).content; + } + + return { + text, + contentType: isJavaScript(originalSource.url || "") ? + "text/javascript" : + "text/plain" + }; +} + +function applySourceMap(generatedId, url, code, mappings) { + const generator = new SourceMapGenerator({ file: url }); + mappings.forEach(mapping => generator.addMapping(mapping)); + generator.setSourceContent(url, code); + + const map = SourceMapConsumer(generator.toJSON()); + sourceMapRequests.set(generatedId, Promise.resolve(map)); +} + +const publicInterface = { + getOriginalURLs, + getGeneratedLocation, + getOriginalLocation, + getOriginalSourceText, + enableSourceMaps, + applySourceMap, + clearSourceMaps +}; + +self.onmessage = function(msg) { + const { id, method, args } = msg.data; + const response = publicInterface[method].apply(undefined, args); + if (response instanceof Promise) { + response.then(val => self.postMessage({ id, response: val }), + err => self.postMessage({ id, error: err })); + } else { + self.postMessage({ id, response }); + } +}; |