/* 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 . */ // @flow /** * Source Map Worker * @module utils/source-map-worker */ const { networkRequest } = require("devtools-utils"); const { SourceMapConsumer, SourceMapGenerator } = require("source-map"); const assert = require("./utils/assert"); const { fetchSourceMap } = require("./utils/fetchSourceMap"); const { getSourceMap, setSourceMap, clearSourceMaps } = require("./utils/sourceMapRequests"); const { originalToGeneratedId, generatedToOriginalId, isGeneratedId, isOriginalId, getContentType } = require("./utils"); import type { Location, Source, SourceId } from "debugger-html"; async function getOriginalURLs(generatedSource: Source) { const map = await fetchSourceMap(generatedSource); return map && map.sources; } const COMPUTED_SPANS = new WeakSet(); const SOURCE_MAPPINGS = new WeakMap(); async function getOriginalRanges(sourceId: SourceId, url: string) { if (!isOriginalId(sourceId)) { return []; } const generatedSourceId = originalToGeneratedId(sourceId); const map = await getSourceMap(generatedSourceId); if (!map) { return []; } 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: Location, originalSource: Source ): Promise< Array<{ line: number, columnStart: number, columnEnd: number }> > { if (!isOriginalId(location.sourceId)) { return []; } const generatedSourceId = originalToGeneratedId(location.sourceId); const map = await getSourceMap(generatedSourceId); if (!map) { return []; } 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: originalSource.url, 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: Location, originalSource: Source ): Promise { 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, bias: SourceMapConsumer.LEAST_UPPER_BOUND }); return { sourceId: generatedSourceId, line, column }; } async function getAllGeneratedLocations( location: Location, originalSource: Source ): Promise> { if (!isOriginalId(location.sourceId)) { return []; } const generatedSourceId = originalToGeneratedId(location.sourceId); const map = await getSourceMap(generatedSourceId); if (!map) { return []; } const positions = map.allGeneratedPositionsFor({ source: originalSource.url, line: location.line, column: location.column == null ? 0 : location.column }); return positions.map(({ line, column }) => ({ sourceId: generatedSourceId, line, column })); } type locationOptions = { search?: "LEAST_UPPER_BOUND" | "GREATEST_LOWER_BOUND" }; async function getOriginalLocation( location: Location, { search }: locationOptions = {} ): Promise { if (!isGeneratedId(location.sourceId)) { return location; } const map = await getSourceMap(location.sourceId); if (!map) { return location; } // 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 location; } return { sourceId: generatedToOriginalId(location.sourceId, sourceUrl), sourceUrl, line, column }; } async function getOriginalSourceText(originalSource: Source) { 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 networkRequest(originalSource.url, { loadFromCache: false })) .content; } return { text, contentType: getContentType(originalSource.url || "") }; } async function hasMappedSource(location: Location): Promise { if (isOriginalId(location.sourceId)) { return true; } const loc = await getOriginalLocation(location); return loc.sourceId !== location.sourceId; } function applySourceMap( generatedId: string, url: string, code: string, mappings: Object ) { const generator = new SourceMapGenerator({ file: url }); mappings.forEach(mapping => generator.addMapping(mapping)); generator.setSourceContent(url, code); const map = SourceMapConsumer(generator.toJSON()); setSourceMap(generatedId, Promise.resolve(map)); } module.exports = { getOriginalURLs, getOriginalRanges, getGeneratedRanges, getGeneratedLocation, getAllGeneratedLocations, getOriginalLocation, getOriginalSourceText, applySourceMap, clearSourceMaps, hasMappedSource };