Skip to content

Commit 577851f

Browse files
authored
Merge pull request #1275 from microsoft/feat/support-unresolved-indexed-map
feat: support unresolved indexed sourcemaps
2 parents 7b174a0 + e45e195 commit 577851f

File tree

2 files changed

+107
-23
lines changed

2 files changed

+107
-23
lines changed

src/common/sourceMaps/sourceMapFactory.test.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44

55
import { expect } from 'chai';
66
import dataUriToBuffer from 'data-uri-to-buffer';
7+
import { stub } from 'sinon';
78
import { RawIndexMap, RawSourceMap } from 'source-map';
8-
import Dap from '../../dap/api';
99
import { stubbedDapApi, StubDapApi } from '../../dap/stubbedApi';
1010
import { Logger } from '../logging/logger';
11-
import { SourceMapFactory } from './sourceMapFactory';
11+
import { RawIndexMapUnresolved, SourceMapFactory } from './sourceMapFactory';
12+
13+
const toDataUri = (obj: unknown) =>
14+
'data:application/json;base64,' + Buffer.from(JSON.stringify(obj)).toString('base64');
1215

1316
const sampleSource = 'console.log(123)';
1417
const basicSourceMap: RawSourceMap = {
@@ -28,16 +31,23 @@ const indexedSourceMap: RawIndexMap = {
2831
},
2932
],
3033
};
34+
const unresolvedIndexedSourceMap: RawIndexMapUnresolved = {
35+
version: 3,
36+
sections: [
37+
{
38+
offset: { line: 0, column: 100 },
39+
url: toDataUri(basicSourceMap),
40+
},
41+
],
42+
};
3143

3244
describe('SourceMapFactory', () => {
3345
let stubDap: StubDapApi;
46+
let factory: SourceMapFactory;
3447

3548
beforeEach(() => {
3649
stubDap = stubbedDapApi();
37-
});
38-
39-
it('loads source-maps', async () => {
40-
const factory = new SourceMapFactory(
50+
factory = new SourceMapFactory(
4151
{
4252
rebaseRemoteToLocal() {
4353
return '/tmp/local';
@@ -68,17 +78,44 @@ describe('SourceMapFactory', () => {
6878
return Promise.resolve({ ok: true, body: {} as T, url: '', statusCode: 200 });
6979
},
7080
},
71-
stubDap as unknown as Dap.Api,
81+
stubDap.actual,
7282
Logger.null,
7383
);
84+
});
85+
86+
it('loads indexed source-maps', async () => {
87+
const map = await factory.load({
88+
sourceMapUrl: toDataUri(indexedSourceMap),
89+
compiledPath: '/tmp/local/one.js',
90+
});
91+
92+
expect(map.sources).to.eql(['one.js']);
93+
});
94+
95+
it('loads indexed source-maps with unresolved children', async () => {
96+
const map = await factory.load({
97+
sourceMapUrl: toDataUri(unresolvedIndexedSourceMap),
98+
compiledPath: '/tmp/local/one.js',
99+
});
100+
101+
expect(map.sources).to.eql(['one.js']);
102+
});
74103

104+
it('warns without failure if a single nested child fails', async () => {
105+
const warn = stub(Logger.null, 'warn');
75106
const map = await factory.load({
76-
sourceMapUrl:
77-
'data:application/json;base64,' +
78-
Buffer.from(JSON.stringify(indexedSourceMap)).toString('base64'),
107+
sourceMapUrl: toDataUri({
108+
...unresolvedIndexedSourceMap,
109+
sections: [
110+
...unresolvedIndexedSourceMap.sections,
111+
{ url: 'invalid', offset: { line: 0, column: 0 } },
112+
],
113+
}),
79114
compiledPath: '/tmp/local/one.js',
80115
});
81116

82117
expect(map.sources).to.eql(['one.js']);
118+
expect(warn.called).to.be.true;
119+
warn.restore();
83120
});
84121
});

src/common/sourceMaps/sourceMapFactory.ts

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@
33
*--------------------------------------------------------*/
44

55
import { inject, injectable } from 'inversify';
6-
import { RawIndexMap, RawSourceMap, SourceMapConsumer } from 'source-map';
6+
import {
7+
Position,
8+
RawIndexMap,
9+
RawSection,
10+
RawSourceMap,
11+
SourceMapConsumer,
12+
StartOfSourceMap,
13+
} from 'source-map';
714
import { IResourceProvider } from '../../adapter/resourceProvider';
815
import Dap from '../../dap/api';
916
import { IRootDapApi } from '../../dap/connection';
1017
import { sourceMapParseFailed } from '../../dap/errors';
1118
import { MapUsingProjection } from '../datastructure/mapUsingProjection';
1219
import { IDisposable } from '../disposable';
1320
import { ILogger, LogTag } from '../logging';
21+
import { truthy } from '../objUtils';
1422
import { ISourcePathResolver } from '../sourcePathResolver';
1523
import { fileUrlToAbsolutePath, isDataUri } from '../urlUtils';
1624
import { ISourceMapMetadata, SourceMap } from './sourceMap';
@@ -35,6 +43,20 @@ export interface ISourceMapFactory extends IDisposable {
3543
guardSourceMapFn<T>(sourceMap: SourceMap, fn: () => T, defaultValue: () => T): T;
3644
}
3745

46+
interface RawExternalSection {
47+
offset: Position;
48+
url: string;
49+
}
50+
51+
/**
52+
* The typings for source-map don't support this, but the spec does.
53+
* @see https://sourcemaps.info/spec.html#h.535es3xeprgt
54+
*/
55+
export interface RawIndexMapUnresolved extends StartOfSourceMap {
56+
version: number;
57+
sections: (RawExternalSection | RawSection)[];
58+
}
59+
3860
/**
3961
* Base implementation of the ISourceMapFactory.
4062
*/
@@ -57,15 +79,7 @@ export class SourceMapFactory implements ISourceMapFactory {
5779
* @inheritdoc
5880
*/
5981
public async load(metadata: ISourceMapMetadata): Promise<SourceMap> {
60-
let basic: RawSourceMap | RawIndexMap | undefined;
61-
try {
62-
basic = await this.parseSourceMap(metadata.sourceMapUrl);
63-
} catch (e) {
64-
basic = await this.parsePathMappedSourceMap(metadata.sourceMapUrl);
65-
if (!basic) {
66-
throw e;
67-
}
68-
}
82+
const basic = await this.parseSourceMap(metadata.sourceMapUrl);
6983

7084
// The source-map library is destructive with its sources parsing. If the
7185
// source root is '/', it'll "helpfully" resolve a source like `../foo.ts`
@@ -81,7 +95,7 @@ export class SourceMapFactory implements ISourceMapFactory {
8195
// preserve them in the same way. Then, rename the sources to prevent any
8296
// of their names colliding (e.g. "webpack://./index.js" and "webpack://../index.js")
8397
let actualSources: string[] = [];
84-
if ('sections' in basic && Array.isArray(basic.sections)) {
98+
if ('sections' in basic) {
8599
actualSources = [];
86100
let i = 0;
87101
for (const section of basic.sections) {
@@ -104,6 +118,37 @@ export class SourceMapFactory implements ISourceMapFactory {
104118
);
105119
}
106120

121+
private async parseSourceMap(sourceMapUrl: string): Promise<RawSourceMap | RawIndexMap> {
122+
let sm: RawSourceMap | RawIndexMapUnresolved | undefined;
123+
try {
124+
sm = await this.parseSourceMapDirect(sourceMapUrl);
125+
} catch (e) {
126+
sm = await this.parsePathMappedSourceMap(sourceMapUrl);
127+
if (!sm) {
128+
throw e;
129+
}
130+
}
131+
132+
if ('sections' in sm) {
133+
const resolved = await Promise.all(
134+
sm.sections.map((s, i) =>
135+
'url' in s
136+
? this.parseSourceMap(s.url)
137+
.then(map => ({ offset: s.offset, map: map as RawSourceMap }))
138+
.catch(e => {
139+
this.logger.warn(LogTag.SourceMapParsing, `Error parsing nested map ${i}: ${e}`);
140+
return undefined;
141+
})
142+
: s,
143+
),
144+
);
145+
146+
sm.sections = resolved.filter(truthy);
147+
}
148+
149+
return sm as RawSourceMap | RawIndexMap;
150+
}
151+
107152
public async parsePathMappedSourceMap(url: string) {
108153
if (isDataUri(url)) {
109154
return;
@@ -113,7 +158,7 @@ export class SourceMapFactory implements ISourceMapFactory {
113158
if (!localSourceMapUrl) return;
114159

115160
try {
116-
return this.parseSourceMap(localSourceMapUrl);
161+
return this.parseSourceMapDirect(localSourceMapUrl);
117162
} catch (error) {
118163
this.logger.info(LogTag.SourceMapParsing, 'Parsing path mapped source map failed.', error);
119164
}
@@ -150,7 +195,9 @@ export class SourceMapFactory implements ISourceMapFactory {
150195
// no-op
151196
}
152197

153-
private async parseSourceMap(sourceMapUrl: string): Promise<RawSourceMap | RawIndexMap> {
198+
private async parseSourceMapDirect(
199+
sourceMapUrl: string,
200+
): Promise<RawSourceMap | RawIndexMapUnresolved> {
154201
let absolutePath = fileUrlToAbsolutePath(sourceMapUrl);
155202
if (absolutePath) {
156203
absolutePath = this.pathResolve.rebaseRemoteToLocal(absolutePath);

0 commit comments

Comments
 (0)