From acd7b3af587e03b4b66a6e718c263cc4ed883fbc Mon Sep 17 00:00:00 2001 From: Alexandre Poirot Date: Wed, 20 Dec 2023 23:01:21 +0000 Subject: [PATCH] Bug 1834729 - [devtools] Provide link to the related original file in the footer. r=devtools-reviewers,nchevobbe For now, we were only showing the related bundle file, so that we can easily navigate from original file to bundle file. But we could also show the related original file for a location. This is slightly more complex because the mapped location can now be different depending on the precise location in the selected file. Also, there is no synchronous API to compute this mapped location. We have to involve an asynchronous action to compute this data for bundles. Tweak Source Map Loader in order to do a loose bundle to original mapping in case we aren't in a cursor location that has a perfect match. Try mapping the first column of the same line and then all first column of the next 10 following lines. Differential Revision: https://phabricator.services.mozilla.com/D187573 --- .../debugger/src/actions/pause/mapFrames.js | 4 +- .../debugger/src/actions/sources/select.js | 32 +++++++++- .../debugger/src/components/Editor/Footer.js | 31 +++++----- .../debugger/src/components/Editor/index.js | 28 ++++----- .../client/debugger/src/reducers/sources.js | 26 ++++++++ .../client/debugger/src/selectors/sources.js | 53 ++++++++++++++++ .../client/debugger/src/utils/location.js | 6 +- .../client/debugger/src/utils/source-maps.js | 11 +++- .../debugger/test/mochitest/browser_aj.toml | 1 + .../mochitest/browser_dbg-pretty-print.js | 8 +++ .../browser_dbg-sourcemaps-indexed.js | 4 +- .../test/mochitest/browser_dbg-sourcemaps.js | 25 +++++++- .../test/mochitest/browser_dbg-sourcemaps2.js | 17 ++++-- .../debugger/test/mochitest/shared-head.js | 2 +- .../client/locales/en-US/debugger.properties | 20 +++++-- .../shared/source-map-loader/source-map.js | 60 ++++++++++++++++--- 16 files changed, 269 insertions(+), 59 deletions(-) diff --git a/devtools/client/debugger/src/actions/pause/mapFrames.js b/devtools/client/debugger/src/actions/pause/mapFrames.js index e51f1066f462..89d834bb8837 100644 --- a/devtools/client/debugger/src/actions/pause/mapFrames.js +++ b/devtools/client/debugger/src/actions/pause/mapFrames.js @@ -41,7 +41,9 @@ async function updateFrameLocationAndDisplayName(frame, thunkArgs) { return frame; } - const location = await getOriginalLocation(frame.location, thunkArgs, true); + const location = await getOriginalLocation(frame.location, thunkArgs, { + waitForSource: true, + }); // Avoid instantiating new frame objects if the frame location isn't mapped if (location == frame.location) { return frame; diff --git a/devtools/client/debugger/src/actions/sources/select.js b/devtools/client/debugger/src/actions/sources/select.js index de82c9919e1f..ef39d7cece44 100644 --- a/devtools/client/debugger/src/actions/sources/select.js +++ b/devtools/client/debugger/src/actions/sources/select.js @@ -17,7 +17,10 @@ import { setBreakableLines } from "."; import { prefs } from "../../utils/prefs"; import { isMinified } from "../../utils/source"; import { createLocation } from "../../utils/location"; -import { getRelatedMapLocation } from "../../utils/source-maps"; +import { + getRelatedMapLocation, + getOriginalLocation, +} from "../../utils/source-maps"; import { getSource, @@ -32,6 +35,7 @@ import { hasSource, hasSourceActor, hasPrettyTab, + isSourceActorWithSourceMap, } from "../../selectors"; // This is only used by jest tests (and within this module) @@ -230,6 +234,32 @@ export function selectLocation(location, { keepContext = true } = {}) { // /!\ we don't historicaly wait for this async action dispatch(setInScopeLines()); + + // When we select a generated source which has a sourcemap, + // asynchronously fetch the related original location in order to display + // the mapped location in the editor's footer. + if ( + !location.source.isOriginal && + isSourceActorWithSourceMap(getState(), sourceActor.id) + ) { + let originalLocation = await getOriginalLocation(location, thunkArgs, { + looseSearch: true, + }); + // We pass a null original location when the location doesn't map + // in order to know when we are done processing the source map. + // * `getOriginalLocation` would return the exact same location if it doesn't map + // * `getOriginalLocation` may also return a distinct location object, + // but refering to the same `source` object (which is the bundle) when it doesn't + // map to any known original location. + if (originalLocation.source === location.source) { + originalLocation = null; + } + dispatch({ + type: "SET_ORIGINAL_SELECTED_LOCATION", + location, + originalLocation, + }); + } }; } diff --git a/devtools/client/debugger/src/components/Editor/Footer.js b/devtools/client/debugger/src/components/Editor/Footer.js index 2e92fed24448..626200a9c153 100644 --- a/devtools/client/debugger/src/components/Editor/Footer.js +++ b/devtools/client/debugger/src/components/Editor/Footer.js @@ -6,7 +6,6 @@ import React, { PureComponent } from "react"; import { div, button, span } from "react-dom-factories"; import PropTypes from "prop-types"; import { connect } from "../../utils/connect"; -import { createLocation } from "../../utils/location"; import actions from "../../actions"; import { getSelectedSource, @@ -14,12 +13,12 @@ import { getSelectedSourceTextContent, getPrettySource, getPaneCollapse, - getGeneratedSource, isSourceBlackBoxed, canPrettyPrintSource, getPrettyPrintMessage, isSourceOnSourceMapIgnoreList, isSourceMapIgnoreListEnabled, + getSelectedMappedSource, } from "../../selectors"; import { isPretty, getFilename, shouldBlackbox } from "../../utils/source"; @@ -169,30 +168,32 @@ class SourceFooter extends PureComponent { } renderSourceSummary() { - const { mappedSource, jumpToMappedLocation, selectedSource } = this.props; + const { mappedSource, jumpToMappedLocation, selectedLocation } = this.props; - if (!mappedSource || !selectedSource || !selectedSource.isOriginal) { + if (!mappedSource) { return null; } - const filename = getFilename(mappedSource); const tooltip = L10N.getFormatStr( - "sourceFooter.mappedSourceTooltip", + mappedSource.isOriginal + ? "sourceFooter.mappedOriginalSource.tooltip" + : "sourceFooter.mappedGeneratedSource.tooltip", + mappedSource.url + ); + const filename = getFilename(mappedSource); + const label = L10N.getFormatStr( + mappedSource.isOriginal + ? "sourceFooter.mappedOriginalSource.title" + : "sourceFooter.mappedGeneratedSource.title", filename ); - const title = L10N.getFormatStr("sourceFooter.mappedSource", filename); - const mappedSourceLocation = createLocation({ - source: selectedSource, - line: 1, - column: 1, - }); return button( { className: "mapped-source", - onClick: () => jumpToMappedLocation(mappedSourceLocation), + onClick: () => jumpToMappedLocation(selectedLocation), title: tooltip, }, - span(null, title) + span(null, label) ); } @@ -262,7 +263,7 @@ const mapStateToProps = state => { isSourceMapIgnoreListEnabled(state) && isSourceOnSourceMapIgnoreList(state, selectedSource), sourceLoaded: !!sourceTextContent, - mappedSource: getGeneratedSource(state, selectedSource), + mappedSource: getSelectedMappedSource(state), prettySource: getPrettySource( state, selectedSource ? selectedSource.id : null diff --git a/devtools/client/debugger/src/components/Editor/index.js b/devtools/client/debugger/src/components/Editor/index.js index 98f0a5321b3d..5deb2cd02d0c 100644 --- a/devtools/client/debugger/src/components/Editor/index.js +++ b/devtools/client/debugger/src/components/Editor/index.js @@ -445,21 +445,19 @@ class Editor extends PureComponent { onCursorChange = event => { const { line, ch } = event.doc.getCursor(); this.props.selectLocation( - createLocation( - { - source: this.props.selectedSource, - // CodeMirror cursor location is all 0-based. - // Whereast in DevTools frontend and backend, - // only colunm is 0-based, the line is 1 based. - line: line + 1, - column: ch, - }, - { - // Reset the context, so that we don't switch to original - // while moving the cursor within a bundle - keepContext: false, - } - ) + createLocation({ + source: this.props.selectedSource, + // CodeMirror cursor location is all 0-based. + // Whereast in DevTools frontend and backend, + // only colunm is 0-based, the line is 1 based. + line: line + 1, + column: ch, + }), + { + // Reset the context, so that we don't switch to original + // while moving the cursor within a bundle + keepContext: false, + } ); }; diff --git a/devtools/client/debugger/src/reducers/sources.js b/devtools/client/debugger/src/reducers/sources.js index 11b78ffb7de1..2429cee666b3 100644 --- a/devtools/client/debugger/src/reducers/sources.js +++ b/devtools/client/debugger/src/reducers/sources.js @@ -11,6 +11,9 @@ import { originalToGeneratedId } from "devtools/client/shared/source-map-loader/ import { prefs } from "../utils/prefs"; import { createPendingSelectedLocation } from "../utils/location"; +export const UNDEFINED_LOCATION = Symbol("Undefined location"); +export const NO_LOCATION = Symbol("No location"); + export function initialSourcesState(state) { /* eslint sort-keys: "error" */ return { @@ -97,6 +100,15 @@ export function initialSourcesState(state) { */ selectedLocation: undefined, + /** + * When selectedLocation refers to a generated source mapping to an original source + * via a source-map, refers to the related original location. + * + * This is UNDEFINED_LOCATION by default and will switch to NO_LOCATION asynchronously after location + * selection if there is no valid original location to map to. + */ + selectedOriginalLocation: UNDEFINED_LOCATION, + /** * By default, if we have a source-mapped source, we would automatically try * to select and show the content of the original source. But, if we explicitly @@ -134,6 +146,7 @@ function update(state = initialSourcesState(), action) { return { ...state, selectedLocation: action.location, + selectedOriginalLocation: UNDEFINED_LOCATION, pendingSelectedLocation, shouldSelectOriginalLocation: action.shouldSelectOriginalLocation, }; @@ -146,10 +159,21 @@ function update(state = initialSourcesState(), action) { return { ...state, selectedLocation: null, + selectedOriginalLocation: UNDEFINED_LOCATION, pendingSelectedLocation, }; } + case "SET_ORIGINAL_SELECTED_LOCATION": { + if (action.location != state.selectedLocation) { + return state; + } + return { + ...state, + selectedOriginalLocation: action.originalLocation, + }; + } + case "SET_PENDING_SELECTED_LOCATION": { const pendingSelectedLocation = { url: action.url, @@ -295,6 +319,7 @@ function removeSourcesAndActors(state, action) { if (newState.selectedLocation?.source == removedSource) { newState.selectedLocation = null; + newState.selectedOriginalLocation = UNDEFINED_LOCATION; } } @@ -319,6 +344,7 @@ function removeSourcesAndActors(state, action) { if (newState.selectedLocation?.sourceActor == removedActor) { newState.selectedLocation = null; + newState.selectedOriginalLocation = UNDEFINED_LOCATION; } } diff --git a/devtools/client/debugger/src/selectors/sources.js b/devtools/client/debugger/src/selectors/sources.js index 74df8164291a..4e8f3cfffc58 100644 --- a/devtools/client/debugger/src/selectors/sources.js +++ b/devtools/client/debugger/src/selectors/sources.js @@ -11,6 +11,7 @@ import { isFulfilled } from "../utils/async-value"; import { originalToGeneratedId } from "devtools/client/shared/source-map-loader/index"; import { prefs } from "../utils/prefs"; +import { UNDEFINED_LOCATION, NO_LOCATION } from "../reducers/sources"; import { hasSourceActor, @@ -110,6 +111,58 @@ export function getSelectedLocation(state) { return state.sources.selectedLocation; } +/** + * Return the "mapped" location for the currently selected location: + * - When selecting a location in an original source, returns + * the related location in the bundle source. + * + * - When selecting a location in a bundle source, returns + * the related location in the original source. This may return undefined + * while we are still computing this information. (we need to query the asynchronous SourceMap service) + * + * - Otherwise, when selecting a location in a source unrelated to source map + * or a pretty printed source, returns null. + */ +export function getSelectedMappedSource(state) { + const selectedLocation = getSelectedLocation(state); + if (!selectedLocation) { + return null; + } + + // Don't map pretty printed to its related compressed source + if (selectedLocation.source.isPrettyPrinted) { + return null; + } + + // If we are on a bundle with a functional source-map, + // the `selectLocation` action should compute the `selectedOriginalLocation` field. + if ( + !selectedLocation.source.isOriginal && + isSourceActorWithSourceMap(state, selectedLocation.sourceActor.id) + ) { + const { selectedOriginalLocation } = state.sources; + // Return undefined if we are still loading the source map. + // `selectedOriginalLocation` will be set to undefined instead of null + if ( + selectedOriginalLocation && + selectedOriginalLocation != UNDEFINED_LOCATION && + selectedOriginalLocation != NO_LOCATION + ) { + return selectedOriginalLocation.source; + } + return null; + } + + const mappedSource = getGeneratedSource(state, selectedLocation.source); + // getGeneratedSource will return the exact same source object on sources + // that don't map to any original source. In this case, return null + // as that's most likely a regular source, not using source maps. + if (mappedSource == selectedLocation.source) { + return null; + } + return mappedSource || null; +} + export const getSelectedSource = createSelector( getSelectedLocation, selectedLocation => { diff --git a/devtools/client/debugger/src/utils/location.js b/devtools/client/debugger/src/utils/location.js index 00c63f860400..00090ca46ef1 100644 --- a/devtools/client/debugger/src/utils/location.js +++ b/devtools/client/debugger/src/utils/location.js @@ -44,8 +44,10 @@ export function createLocation({ export function debuggerToSourceMapLocation(location) { return { sourceId: location.source.id, - line: location.line, - column: location.column, + // In case of errors loading the source, we might not have a precise location. + // Defaults to first line and column. + line: location.line || 1, + column: location.column || 0, }; } diff --git a/devtools/client/debugger/src/utils/source-maps.js b/devtools/client/debugger/src/utils/source-maps.js index 7a718d2accd1..7a77639522ce 100644 --- a/devtools/client/debugger/src/utils/source-maps.js +++ b/devtools/client/debugger/src/utils/source-maps.js @@ -48,10 +48,14 @@ export async function getGeneratedLocation(location, thunkArgs) { * @param {Object} location * @param {Object} thunkArgs * Redux action thunk arguments - * @param {boolean} waitForSource + * @param {Object} options + * @param {boolean} options.waitForSource * Default to false. If true is passed, this function will * ensure waiting, possibly asynchronously for the related original source * to be registered in the redux store. + * @param {boolean} options.looseSearch + * Default to false. If true, this won't query an exact mapping, + * but will also lookup for a loose match at the first column and next lines. * * @param {Object} * The matching original location. @@ -59,14 +63,15 @@ export async function getGeneratedLocation(location, thunkArgs) { export async function getOriginalLocation( location, thunkArgs, - waitForSource = false + { waitForSource = false, looseSearch = false } = {} ) { if (location.source.isOriginal) { return location; } const { getState, sourceMapLoader } = thunkArgs; const originalLocation = await sourceMapLoader.getOriginalLocation( - debuggerToSourceMapLocation(location) + debuggerToSourceMapLocation(location), + { looseSearch } ); if (!originalLocation) { return location; diff --git a/devtools/client/debugger/test/mochitest/browser_aj.toml b/devtools/client/debugger/test/mochitest/browser_aj.toml index 8f8ee5fe962d..58ff97092000 100644 --- a/devtools/client/debugger/test/mochitest/browser_aj.toml +++ b/devtools/client/debugger/test/mochitest/browser_aj.toml @@ -163,6 +163,7 @@ fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled ["browser_dbg-editor-select.js"] +fail-if = ["a11y_checks"] # Bug 1870062 clicked element may not be focusable and/or labeled ["browser_dbg-ember-original-variable-mapping-notifications.js"] fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled diff --git a/devtools/client/debugger/test/mochitest/browser_dbg-pretty-print.js b/devtools/client/debugger/test/mochitest/browser_dbg-pretty-print.js index 293cabb75c87..2b767bd6b540 100644 --- a/devtools/client/debugger/test/mochitest/browser_dbg-pretty-print.js +++ b/devtools/client/debugger/test/mochitest/browser_dbg-pretty-print.js @@ -15,6 +15,10 @@ add_task(async function () { clickElement(dbg, "prettyPrintButton"); await waitForSelectedSource(dbg, "math.min.js:formatted"); + ok( + !findElement(dbg, "mappedSourceLink"), + "When we are on the pretty printed source, we don't show the link to the minified source" + ); const ppSrc = findSource(dbg, "math.min.js:formatted"); ok(ppSrc, "Pretty-printed source exists"); @@ -43,6 +47,10 @@ add_task(async function () { await selectSource(dbg, "math.min.js"); await waitForSelectedSource(dbg, "math.min.js"); + ok( + !findElement(dbg, "mappedSourceLink"), + "When we are on the minified source, we don't show the link to the pretty printed source" + ); ok( !findElement(dbg, "prettyPrintButton").disabled, diff --git a/devtools/client/debugger/test/mochitest/browser_dbg-sourcemaps-indexed.js b/devtools/client/debugger/test/mochitest/browser_dbg-sourcemaps-indexed.js index 3c78c58fe509..2c39494b7650 100644 --- a/devtools/client/debugger/test/mochitest/browser_dbg-sourcemaps-indexed.js +++ b/devtools/client/debugger/test/mochitest/browser_dbg-sourcemaps-indexed.js @@ -41,11 +41,11 @@ add_task(async function () { assertPausedAtSourceAndLine(dbg, mainSrc.id, 4, 3); // Tests the existence of the sourcemap link in the original source. - ok(findElement(dbg, "sourceMapLink"), "Sourcemap link in original source"); + ok(findElement(dbg, "mappedSourceLink"), "Sourcemap link in original source"); await selectSource(dbg, "main.min.js"); ok( - !findElement(dbg, "sourceMapLink"), + !findElement(dbg, "mappedSourceLink"), "No Sourcemap link exists in generated source" ); }); diff --git a/devtools/client/debugger/test/mochitest/browser_dbg-sourcemaps.js b/devtools/client/debugger/test/mochitest/browser_dbg-sourcemaps.js index 77bcf982468b..7ed70b33109f 100644 --- a/devtools/client/debugger/test/mochitest/browser_dbg-sourcemaps.js +++ b/devtools/client/debugger/test/mochitest/browser_dbg-sourcemaps.js @@ -99,10 +99,33 @@ add_task(async function () { ); info("Click on jump to generated source link from editor's footer"); - findElement(dbg, "sourceMapLink").click(); + let mappedSourceLink = findElement(dbg, "mappedSourceLink"); + is( + mappedSourceLink.textContent, + "To bundle.js", + "The link to mapped source mentions the bundle" + ); + mappedSourceLink.click(); await waitForSelectedSource(dbg, bundleSrc); assertPausedAtSourceAndLine(dbg, bundleSrc.id, 62); + // The mapped source link is computed asynchronously when we are on the bundle + mappedSourceLink = await waitFor(() => findElement(dbg, "mappedSourceLink")); + mappedSourceLink = findElement(dbg, "mappedSourceLink"); + is( + mappedSourceLink.textContent, + "From entry.js", + "The link to mapped source mentions the original source" + ); + + info("Move the cursor within the bundle to another original source"); + getCM(dbg).setCursor({ line: 70, ch: 0 }); + mappedSourceLink = await waitFor(() => findElement(dbg, "mappedSourceLink")); + is( + mappedSourceLink.textContent, + "From times2.js", + "The link to mapped source updates to the newly selected original source within the bundle" + ); }); function assertBreakpointExists(dbg, source, line) { diff --git a/devtools/client/debugger/test/mochitest/browser_dbg-sourcemaps2.js b/devtools/client/debugger/test/mochitest/browser_dbg-sourcemaps2.js index a984476d25b5..aaad44b1e89a 100644 --- a/devtools/client/debugger/test/mochitest/browser_dbg-sourcemaps2.js +++ b/devtools/client/debugger/test/mochitest/browser_dbg-sourcemaps2.js @@ -41,11 +41,20 @@ add_task(async function () { assertPausedAtSourceAndLine(dbg, mainSrc.id, 4); // Tests the existence of the sourcemap link in the original source. - ok(findElement(dbg, "sourceMapLink"), "Sourcemap link in original source"); + let sourceMapLink = findElement(dbg, "mappedSourceLink"); + is( + sourceMapLink.textContent, + "To main.min.js", + "Sourcemap link in original source refers to the bundle" + ); + await selectSource(dbg, "main.min.js"); - ok( - !findElement(dbg, "sourceMapLink"), - "No Sourcemap link exists in generated source" + // The mapped source link is computed asynchronously when we are on the bundle + sourceMapLink = await waitFor(() => findElement(dbg, "mappedSourceLink")); + is( + sourceMapLink.textContent, + "From main.js", + "Sourcemap link in bundle refers to the original source" ); }); diff --git a/devtools/client/debugger/test/mochitest/shared-head.js b/devtools/client/debugger/test/mochitest/shared-head.js index 86d066ba2335..8734144a12c9 100644 --- a/devtools/client/debugger/test/mochitest/shared-head.js +++ b/devtools/client/debugger/test/mochitest/shared-head.js @@ -1770,7 +1770,7 @@ const selectors = { stepIn: ".stepIn.active", trace: ".debugger-trace-menu-button", prettyPrintButton: ".source-footer .prettyPrint", - sourceMapLink: ".source-footer .mapped-source", + mappedSourceLink: ".source-footer .mapped-source", sourcesFooter: ".sources-panel .source-footer", editorFooter: ".editor-pane .source-footer", sourceNode: i => `.sources-list .tree-node:nth-child(${i}) .node`, diff --git a/devtools/client/locales/en-US/debugger.properties b/devtools/client/locales/en-US/debugger.properties index 06289c098b80..fb0dd72dbf1b 100644 --- a/devtools/client/locales/en-US/debugger.properties +++ b/devtools/client/locales/en-US/debugger.properties @@ -780,13 +780,21 @@ ignoreContextItem.ignoreLines.accesskey=i ignoreContextItem.unignoreLines=Unignore lines ignoreContextItem.unignoreLines.accesskey=u -# LOCALIZATION NOTE (sourceFooter.mappedSource): Text associated -# with a mapped source. %S is replaced by the source map origin. -sourceFooter.mappedSource=(From %S) +# LOCALIZATION NOTE (sourceFooter.mappedOriginalSource.title): Text associated +# with an original source mapped to a bundle. %S is replaced by the bundle url. +sourceFooter.mappedOriginalSource.title=From %S -# LOCALIZATION NOTE (sourceFooter.mappedSourceTooltip): Tooltip text associated -# with a mapped source. %S is replaced by the source map origin. -sourceFooter.mappedSourceTooltip=(Source mapped from %S) +# LOCALIZATION NOTE (sourceFooter.mappedOriginalSource.tooltip): Tooltip text associated +# with an original source mapped to a bundle. %S is replaced by bundle url. +sourceFooter.mappedOriginalSource.tooltip=Open related bundle (%S) + +# LOCALIZATION NOTE (sourceFooter.mappedGeneratedSource.title): Text associated +# with a bundled source mapped to an original source. %S is replaced by the original source url. +sourceFooter.mappedGeneratedSource.title=To %S + +# LOCALIZATION NOTE (sourceFooter.mappedGeneratedSource.tooltip): Tooltip text associated +# with a bundled source mapped to an original source. %S is replaced by the original source url. +sourceFooter.mappedGeneratedSource.tooltip=Open related original source (%S) # LOCALIZATION NOTE (sourceFooter.mappedSuffix): Text associated # with a mapped source. Displays next to URLs in tree and tabs. diff --git a/devtools/client/shared/source-map-loader/source-map.js b/devtools/client/shared/source-map-loader/source-map.js index 75c5fe71cc38..d5d4fdfd6664 100644 --- a/devtools/client/shared/source-map-loader/source-map.js +++ b/devtools/client/shared/source-map-loader/source-map.js @@ -255,17 +255,51 @@ async function getOriginalLocations(breakpointPositions, sourceId) { return breakpointPositions; } -function getOriginalLocationSync(map, location) { +/** + * Query the source map for a mapping from bundle location to original location. + * + * @param {SourceMapConsumer} map + * The source map for the bundle source. + * @param {Object} location + * A location within a bundle to map to an original location. + * @param {Object} options + * @param {Boolean} options.looseSearch + * Optional, if true, will do a loose search on first column and next lines + * until a mapping is found. + * @return {location} + * The mapped location in the original source. + */ +function getOriginalLocationSync(map, location, { looseSearch = false } = {}) { // First check for an exact match - const { - source: sourceUrl, - line, - column, - } = map.originalPositionFor({ + let match = map.originalPositionFor({ line: location.line, column: location.column == null ? 0 : location.column, }); + // Then check for a loose match by sliding to first column and next lines + if (match.sourceUrl == null && looseSearch) { + let line = location.line; + // if a non-0 column was passed, we want to do the search from the beginning of the line, + // otherwise, we can start looking into next lines + let firstLineChecked = (location.column || 0) !== 0; + + // Avoid looping through the whole file and limit the sliding search to the next 10 lines. + while (match.sourceUrl === null && line < location.line + 10) { + if (firstLineChecked) { + line++; + } else { + firstLineChecked = true; + } + match = map.originalPositionFor({ + line, + column: 0, + bias: SourceMapConsumer.LEAST_UPPER_BOUND, + }); + } + } + + const { source: sourceUrl, line, column } = match; + if (sourceUrl == null) { // No url means the location didn't map. return null; @@ -279,7 +313,17 @@ function getOriginalLocationSync(map, location) { }; } -async function getOriginalLocation(location) { +/** + * Map a bundle location to an original one. + * + * @param {Object} location + * Bundle location + * @param {Object} options + * See getORiginalLocationSync. + * @return {Object} + * Original location + */ +async function getOriginalLocation(location, options) { if (!isGeneratedId(location.sourceId)) { return null; } @@ -289,7 +333,7 @@ async function getOriginalLocation(location) { return null; } - return getOriginalLocationSync(map, location); + return getOriginalLocationSync(map, location, options); } async function getOriginalSourceText(originalSourceId) {