From 99b6a471b8ceeb9e5afda30cb40437b21f5621a4 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Mon, 4 Mar 2024 22:35:15 -0500 Subject: [PATCH] Fix map resolution (#36) * Fix map resolution You prefix `source` with `sourceRoot`, then resolve it relative to the map URL. Before we incorrectly allowed a map's source to be absolute after the `sourceRoot` was specified. This matches `source-map` and Chrome's behaviors. * Eliminate arrow fn --- src/resolve.ts | 14 ++++++++++---- src/trace-mapping.ts | 7 +++---- src/types.ts | 5 +---- test/resolve.test.ts | 34 +++++++++++++++++++++++++--------- test/trace-mapping.test.ts | 3 +-- 5 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/resolve.ts b/src/resolve.ts index 8ef7db7..30bfa3b 100644 --- a/src/resolve.ts +++ b/src/resolve.ts @@ -1,10 +1,16 @@ import resolveUri from '@jridgewell/resolve-uri'; +import stripFilename from './strip-filename'; -export default function resolve(input: string, base: string | undefined): string { - // The base is always treated as a directory, if it's not empty. +type Resolve = (source: string | null) => string; +export default function resolver( + mapUrl: string | null | undefined, + sourceRoot: string | undefined, +): Resolve { + const from = stripFilename(mapUrl); + // The sourceRoot is always treated as a directory, if it's not empty. // https://github.com/mozilla/source-map/blob/8cb3ee57/lib/util.js#L327 // https://github.com/chromium/chromium/blob/da4adbb3/third_party/blink/renderer/devtools/front_end/sdk/SourceMap.js#L400-L401 - if (base && !base.endsWith('/')) base += '/'; + const prefix = sourceRoot ? sourceRoot + '/' : ''; - return resolveUri(input, base); + return (source) => resolveUri(prefix + (source || ''), from); } diff --git a/src/trace-mapping.ts b/src/trace-mapping.ts index a5e85bf..520481e 100644 --- a/src/trace-mapping.ts +++ b/src/trace-mapping.ts @@ -1,7 +1,6 @@ import { encode, decode } from '@jridgewell/sourcemap-codec'; -import resolve from './resolve'; -import stripFilename from './strip-filename'; +import resolver from './resolve'; import maybeSort from './sort'; import buildBySources from './by-source'; import { @@ -119,8 +118,8 @@ export class TraceMap implements SourceMap { this.sourcesContent = sourcesContent; this.ignoreList = parsed.ignoreList || (parsed as XInput).x_google_ignoreList || undefined; - const from = resolve(sourceRoot || '', stripFilename(mapUrl)); - this.resolvedSources = sources.map((s) => resolve(s || '', from)); + const resolve = resolver(mapUrl, sourceRoot); + this.resolvedSources = sources.map(resolve); const { mappings } = parsed; if (typeof mappings === 'string') { diff --git a/src/types.ts b/src/types.ts index 96a4370..8c4a1e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,10 +20,7 @@ export interface DecodedSourceMap extends SourceMapV3 { } export interface Section { - offset: { - line: number; - column: number; - }; + offset: { line: number; column: number }; map: EncodedSourceMap | DecodedSourceMap | SectionedSourceMap; } diff --git a/test/resolve.test.ts b/test/resolve.test.ts index e1348e4..22c7f0d 100644 --- a/test/resolve.test.ts +++ b/test/resolve.test.ts @@ -1,18 +1,34 @@ import { strict as assert } from 'assert'; -import resolve from '../src/resolve'; +import resolver from '../src/resolve'; describe('resolve', () => { - it('resolves input relative to base', () => { - const base = 'bar/'; - const input = 'foo'; + it('unresolved without sourceRoot', () => { + const resolve = resolver(undefined, undefined); + assert.equal(resolve('input.js'), 'input.js'); + }); + + it('relative to mapUrl', () => { + const resolve = resolver('foo/script.js.map', undefined); + assert.equal(resolve('input.js'), 'foo/input.js'); + }); - assert.equal(resolve(input, base), 'bar/foo'); + it('relative to sourceRoot', () => { + const resolve = resolver(undefined, 'foo'); + assert.equal(resolve('input.js'), 'foo/input.js'); }); - it('treats base as a directory regardless of slash', () => { - const base = 'bar'; - const input = 'foo'; + it('relative to mapUrl then sourceRoot', () => { + const resolve = resolver('foo/script.js.map', 'bar'); + assert.equal(resolve('input.js'), 'foo/bar/input.js'); + }); + + it('prepends sourceRoot to source before resolving', () => { + const resolve = resolver('foo/script.js.map', 'bar'); + assert.equal(resolve('/input.js'), 'foo/bar/input.js'); + }); - assert.equal(resolve(input, base), 'bar/foo'); + it('skips undefined sourceRoot before resolving', () => { + const resolve = resolver('foo/script.js.map', undefined); + assert.equal(resolve('/input.js'), '/input.js'); }); }); diff --git a/test/trace-mapping.test.ts b/test/trace-mapping.test.ts index a05e947..4b2e35b 100644 --- a/test/trace-mapping.test.ts +++ b/test/trace-mapping.test.ts @@ -31,8 +31,7 @@ describe('TraceMap', () => { const decodedMap: DecodedSourceMap = { version: 3, sources: ['input.js'], - sourceRoot: - 'https://astexplorer.net/#/gist/d91f04e37e8e12eec06f2886e6bc3a4d/56cd06cd895d3b638b4100658b0027787ca5e5f1', + sourceRoot: 'https://astexplorer.net/', names: ['foo', 'bar', 'Error'], mappings: [ [