From 3531f17d922df8ec883ff31d783a141098b87623 Mon Sep 17 00:00:00 2001 From: John Harrison Date: Fri, 6 May 2022 14:43:35 -0700 Subject: [PATCH 1/3] Adding support for indexed source maps. Indexed source maps, described in https://sourcemaps.info/spec.html#h.535es3xeprgt this should allow users to debug bundled output, like that produced by the closure-compiler when producing bundled output. --- src/adapter/sources.ts | 2 +- src/common/sourceMaps/sourceMap.ts | 20 ++++------------ src/common/sourceMaps/sourceMapFactory.ts | 28 +++++++++++++++++------ 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/adapter/sources.ts b/src/adapter/sources.ts index b95a2129d..808775f7e 100644 --- a/src/adapter/sources.ts +++ b/src/adapter/sources.ts @@ -1067,7 +1067,7 @@ export class SourceContainer { const fileUrl = absolutePath && utils.absolutePathToFileUrl(absolutePath); const content = this.sourceMapFactory.guardSourceMapFn( map, - () => map.sourceContentFor(url), + () => map.sourceContentFor(url, true), () => null, ); diff --git a/src/common/sourceMaps/sourceMap.ts b/src/common/sourceMaps/sourceMap.ts index 66aeccdf0..d3891e9e6 100644 --- a/src/common/sourceMaps/sourceMap.ts +++ b/src/common/sourceMaps/sourceMap.ts @@ -4,11 +4,13 @@ import { BasicSourceMapConsumer, + IndexedSourceMapConsumer, MappedPosition, MappingItem, NullableMappedPosition, NullablePosition, Position, + SourceMapConsumer, } from 'source-map'; import { fixDriveLetterAndSlashes } from '../pathUtils'; import { completeUrlEscapingRoot } from '../urlUtils'; @@ -22,7 +24,7 @@ export interface ISourceMapMetadata { /** * Wrapper for a parsed sourcemap. */ -export class SourceMap implements BasicSourceMapConsumer { +export class SourceMap implements SourceMapConsumer { private static idCounter = 0; /** @@ -37,7 +39,7 @@ export class SourceMap implements BasicSourceMapConsumer { public readonly id = SourceMap.idCounter++; constructor( - private readonly original: BasicSourceMapConsumer, + private readonly original: BasicSourceMapConsumer | IndexedSourceMapConsumer, public readonly metadata: Readonly, private readonly actualRoot: string, public readonly actualSources: ReadonlyArray, @@ -64,13 +66,6 @@ export class SourceMap implements BasicSourceMapConsumer { return this.actualSources.slice(); } - /** - * Gets the optional name of the generated code that this source map is associated with - */ - public get file() { - return this.metadata.compiledPath ?? this.original.file; - } - /** * Gets the source root of the sourcemap. */ @@ -79,13 +74,6 @@ export class SourceMap implements BasicSourceMapConsumer { return this.actualRoot; } - /** - * Gets the sources content. - */ - public get sourcesContent() { - return this.original.sourcesContent; - } - /** * Gets the source URL computed from the compiled path and the source root. */ diff --git a/src/common/sourceMaps/sourceMapFactory.ts b/src/common/sourceMaps/sourceMapFactory.ts index f066470e4..779ddccb9 100644 --- a/src/common/sourceMaps/sourceMapFactory.ts +++ b/src/common/sourceMaps/sourceMapFactory.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------*/ import { inject, injectable } from 'inversify'; -import { BasicSourceMapConsumer, RawSourceMap, SourceMapConsumer } from 'source-map'; +import { RawIndexMap, RawSourceMap, SourceMapConsumer } from 'source-map'; import { IResourceProvider } from '../../adapter/resourceProvider'; import Dap from '../../dap/api'; import { IRootDapApi } from '../../dap/connection'; @@ -57,7 +57,7 @@ export class SourceMapFactory implements ISourceMapFactory { * @inheritdoc */ public async load(metadata: ISourceMapMetadata): Promise { - let basic: RawSourceMap | undefined; + let basic: RawSourceMap | RawIndexMap | undefined; try { basic = await this.parseSourceMap(metadata.sourceMapUrl); } catch (e) { @@ -75,18 +75,32 @@ export class SourceMapFactory implements ISourceMapFactory { const actualRoot = basic.sourceRoot; basic.sourceRoot = undefined; + let hasNames = false; + // The source map library (also) "helpfully" normalizes source URLs, so // preserve them in the same way. Then, rename the sources to prevent any // of their names colliding (e.g. "webpack://./index.js" and "webpack://../index.js") - const actualSources = basic.sources; - basic.sources = basic.sources.map((_, i) => `source${i}.js`); + let actualSources: string[] = []; + if ('sections' in basic && Array.isArray(basic.sections)) { + actualSources = []; + let i = 0; + for (const section of basic.sections) { + actualSources.push(...section.map.sources); + section.map.sources = section.map.sources.map(() => `source${i++}.js`); + hasNames ||= !!section.map.names?.length; + } + } else if ('sources' in basic && Array.isArray(basic.sources)) { + actualSources = basic.sources; + basic.sources = basic.sources.map((_, i) => `source${i}.js`); + hasNames = !!basic.names?.length; + } return new SourceMap( - (await new SourceMapConsumer(basic)) as BasicSourceMapConsumer, + await new SourceMapConsumer(basic), metadata, actualRoot ?? '', actualSources, - !!basic.names?.length, + hasNames, ); } @@ -136,7 +150,7 @@ export class SourceMapFactory implements ISourceMapFactory { // no-op } - private async parseSourceMap(sourceMapUrl: string): Promise { + private async parseSourceMap(sourceMapUrl: string): Promise { let absolutePath = fileUrlToAbsolutePath(sourceMapUrl); if (absolutePath) { absolutePath = this.pathResolve.rebaseRemoteToLocal(absolutePath); From 7e8678bf64fb0728fad511b407fe9c3dc1fe5533 Mon Sep 17 00:00:00 2001 From: John Harrison Date: Fri, 6 May 2022 16:40:12 -0700 Subject: [PATCH 2/3] Adding new unit tests to the SourceMap and SourceMapFactory classes. --- src/common/sourceMaps/sourceMap.test.ts | 45 +++++++++++++++ .../sourceMaps/sourceMapFactory.test.ts | 56 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 src/common/sourceMaps/sourceMap.test.ts create mode 100644 src/common/sourceMaps/sourceMapFactory.test.ts diff --git a/src/common/sourceMaps/sourceMap.test.ts b/src/common/sourceMaps/sourceMap.test.ts new file mode 100644 index 000000000..0f5caf110 --- /dev/null +++ b/src/common/sourceMaps/sourceMap.test.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ +import { expect } from 'chai'; +import { + RawIndexMap, RawSourceMap, SourceMapConsumer +} from 'source-map'; +import { SourceMap } from './sourceMap'; + +const sampleSource = 'console.log(123)'; +const basicSourceMap: RawSourceMap = { + version: 3, + sources: ['one.js'], + sourcesContent: [sampleSource], + names: [], + file: '', + mappings: '', +}; +const indexedSourceMap: RawIndexMap = { + version: 3, + sections: [{ + offset: { line: 0, column: 100}, + map: basicSourceMap, + }], +}; + +describe('SourceMap', () => { + it('loads basic source-maps', async () => { + const map = new SourceMap(await new SourceMapConsumer(basicSourceMap), { + sourceMapUrl: JSON.stringify(basicSourceMap), + compiledPath: 'one.js', + }, '', ['one.js'], false); + + expect(map.sourceContentFor('one.js')).to.eq(sampleSource); + }); + + it('loads indexed source-maps', async () => { + const map = new SourceMap(await new SourceMapConsumer(indexedSourceMap), { + sourceMapUrl: JSON.stringify(indexedSourceMap), + compiledPath: 'one.js', + }, '', ['one.js'], false); + + expect(map.sourceContentFor('one.js')).to.eq(sampleSource); + }); +}); diff --git a/src/common/sourceMaps/sourceMapFactory.test.ts b/src/common/sourceMaps/sourceMapFactory.test.ts new file mode 100644 index 000000000..560496ddf --- /dev/null +++ b/src/common/sourceMaps/sourceMapFactory.test.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { expect } from 'chai'; +import dataUriToBuffer from 'data-uri-to-buffer'; +import { RawIndexMap, RawSourceMap } from 'source-map'; +import Dap from '../../dap/api'; +import { stubbedDapApi, StubDapApi } from '../../dap/stubbedApi'; +import { Logger } from '../logging/logger'; +import { SourceMapFactory } from './sourceMapFactory'; + +const sampleSource = 'console.log(123)'; +const basicSourceMap: RawSourceMap = { + version: 3, + sources: ['one.js'], + sourcesContent: [sampleSource], + names: [], + file: '', + mappings: '', +}; +const indexedSourceMap: RawIndexMap = { + version: 3, + sections: [{ + offset: { line: 0, column: 100}, + map: basicSourceMap, + }], +}; + +describe('SourceMapFactory', () => { + let stubDap: StubDapApi; + + beforeEach(() => { + stubDap = stubbedDapApi(); + }) + + it('loads source-maps', async () => { + const factory = new SourceMapFactory({ + rebaseRemoteToLocal() { return '/tmp/local'; }, + rebaseLocalToRemote() { return '/tmp/remote'; }, + shouldResolveSourceMap() { return true; }, + urlToAbsolutePath() { return Promise.resolve('/tmp/abs');}, + absolutePathToUrlRegexp() { return undefined; }, + }, { + fetch(url) { return Promise.resolve({ ok: true, body: dataUriToBuffer(url).toString('utf8'), url: url, statusCode: 500 }); }, + fetchJson() { return Promise.resolve({ ok: true, body: {} as T, url: '', statusCode: 200 }); }, + }, stubDap as unknown as Dap.Api, Logger.null); + + const map = await factory.load({ + sourceMapUrl: 'data:application/json;base64,' + Buffer.from(JSON.stringify(indexedSourceMap)).toString('base64'), + compiledPath: '/tmp/local/one.js', + }); + + expect(map.sources).to.eql(['one.js']); + }); +}); From 1241ce2100144238b946f5ec3d5bf7d42f234cae Mon Sep 17 00:00:00 2001 From: John Harrison Date: Fri, 6 May 2022 16:40:12 -0700 Subject: [PATCH 3/3] Formatting new tests. --- src/common/sourceMaps/sourceMap.test.ts | 42 ++++++++----- .../sourceMaps/sourceMapFactory.test.ts | 60 ++++++++++++++----- 2 files changed, 71 insertions(+), 31 deletions(-) diff --git a/src/common/sourceMaps/sourceMap.test.ts b/src/common/sourceMaps/sourceMap.test.ts index 0f5caf110..719831c49 100644 --- a/src/common/sourceMaps/sourceMap.test.ts +++ b/src/common/sourceMaps/sourceMap.test.ts @@ -2,9 +2,7 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ import { expect } from 'chai'; -import { - RawIndexMap, RawSourceMap, SourceMapConsumer -} from 'source-map'; +import { RawIndexMap, RawSourceMap, SourceMapConsumer } from 'source-map'; import { SourceMap } from './sourceMap'; const sampleSource = 'console.log(123)'; @@ -18,27 +16,41 @@ const basicSourceMap: RawSourceMap = { }; const indexedSourceMap: RawIndexMap = { version: 3, - sections: [{ - offset: { line: 0, column: 100}, - map: basicSourceMap, - }], + sections: [ + { + offset: { line: 0, column: 100 }, + map: basicSourceMap, + }, + ], }; describe('SourceMap', () => { it('loads basic source-maps', async () => { - const map = new SourceMap(await new SourceMapConsumer(basicSourceMap), { - sourceMapUrl: JSON.stringify(basicSourceMap), - compiledPath: 'one.js', - }, '', ['one.js'], false); + const map = new SourceMap( + await new SourceMapConsumer(basicSourceMap), + { + sourceMapUrl: JSON.stringify(basicSourceMap), + compiledPath: 'one.js', + }, + '', + ['one.js'], + false, + ); expect(map.sourceContentFor('one.js')).to.eq(sampleSource); }); it('loads indexed source-maps', async () => { - const map = new SourceMap(await new SourceMapConsumer(indexedSourceMap), { - sourceMapUrl: JSON.stringify(indexedSourceMap), - compiledPath: 'one.js', - }, '', ['one.js'], false); + const map = new SourceMap( + await new SourceMapConsumer(indexedSourceMap), + { + sourceMapUrl: JSON.stringify(indexedSourceMap), + compiledPath: 'one.js', + }, + '', + ['one.js'], + false, + ); expect(map.sourceContentFor('one.js')).to.eq(sampleSource); }); diff --git a/src/common/sourceMaps/sourceMapFactory.test.ts b/src/common/sourceMaps/sourceMapFactory.test.ts index 560496ddf..af92d2dba 100644 --- a/src/common/sourceMaps/sourceMapFactory.test.ts +++ b/src/common/sourceMaps/sourceMapFactory.test.ts @@ -21,10 +21,12 @@ const basicSourceMap: RawSourceMap = { }; const indexedSourceMap: RawIndexMap = { version: 3, - sections: [{ - offset: { line: 0, column: 100}, - map: basicSourceMap, - }], + sections: [ + { + offset: { line: 0, column: 100 }, + map: basicSourceMap, + }, + ], }; describe('SourceMapFactory', () => { @@ -32,22 +34,48 @@ describe('SourceMapFactory', () => { beforeEach(() => { stubDap = stubbedDapApi(); - }) + }); it('loads source-maps', async () => { - const factory = new SourceMapFactory({ - rebaseRemoteToLocal() { return '/tmp/local'; }, - rebaseLocalToRemote() { return '/tmp/remote'; }, - shouldResolveSourceMap() { return true; }, - urlToAbsolutePath() { return Promise.resolve('/tmp/abs');}, - absolutePathToUrlRegexp() { return undefined; }, - }, { - fetch(url) { return Promise.resolve({ ok: true, body: dataUriToBuffer(url).toString('utf8'), url: url, statusCode: 500 }); }, - fetchJson() { return Promise.resolve({ ok: true, body: {} as T, url: '', statusCode: 200 }); }, - }, stubDap as unknown as Dap.Api, Logger.null); + const factory = new SourceMapFactory( + { + rebaseRemoteToLocal() { + return '/tmp/local'; + }, + rebaseLocalToRemote() { + return '/tmp/remote'; + }, + shouldResolveSourceMap() { + return true; + }, + urlToAbsolutePath() { + return Promise.resolve('/tmp/abs'); + }, + absolutePathToUrlRegexp() { + return undefined; + }, + }, + { + fetch(url) { + return Promise.resolve({ + ok: true, + body: dataUriToBuffer(url).toString('utf8'), + url: url, + statusCode: 500, + }); + }, + fetchJson() { + return Promise.resolve({ ok: true, body: {} as T, url: '', statusCode: 200 }); + }, + }, + stubDap as unknown as Dap.Api, + Logger.null, + ); const map = await factory.load({ - sourceMapUrl: 'data:application/json;base64,' + Buffer.from(JSON.stringify(indexedSourceMap)).toString('base64'), + sourceMapUrl: + 'data:application/json;base64,' + + Buffer.from(JSON.stringify(indexedSourceMap)).toString('base64'), compiledPath: '/tmp/local/one.js', });