Skip to content

Commit

Permalink
Merge pull request #1275 from microsoft/feat/support-unresolved-index…
Browse files Browse the repository at this point in the history
…ed-map

feat: support unresolved indexed sourcemaps
  • Loading branch information
connor4312 authored May 18, 2022
2 parents 7b174a0 + e45e195 commit 577851f
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 23 deletions.
57 changes: 47 additions & 10 deletions src/common/sourceMaps/sourceMapFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

import { expect } from 'chai';
import dataUriToBuffer from 'data-uri-to-buffer';
import { stub } from 'sinon';
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';
import { RawIndexMapUnresolved, SourceMapFactory } from './sourceMapFactory';

const toDataUri = (obj: unknown) =>
'data:application/json;base64,' + Buffer.from(JSON.stringify(obj)).toString('base64');

const sampleSource = 'console.log(123)';
const basicSourceMap: RawSourceMap = {
Expand All @@ -28,16 +31,23 @@ const indexedSourceMap: RawIndexMap = {
},
],
};
const unresolvedIndexedSourceMap: RawIndexMapUnresolved = {
version: 3,
sections: [
{
offset: { line: 0, column: 100 },
url: toDataUri(basicSourceMap),
},
],
};

describe('SourceMapFactory', () => {
let stubDap: StubDapApi;
let factory: SourceMapFactory;

beforeEach(() => {
stubDap = stubbedDapApi();
});

it('loads source-maps', async () => {
const factory = new SourceMapFactory(
factory = new SourceMapFactory(
{
rebaseRemoteToLocal() {
return '/tmp/local';
Expand Down Expand Up @@ -68,17 +78,44 @@ describe('SourceMapFactory', () => {
return Promise.resolve({ ok: true, body: {} as T, url: '', statusCode: 200 });
},
},
stubDap as unknown as Dap.Api,
stubDap.actual,
Logger.null,
);
});

it('loads indexed source-maps', async () => {
const map = await factory.load({
sourceMapUrl: toDataUri(indexedSourceMap),
compiledPath: '/tmp/local/one.js',
});

expect(map.sources).to.eql(['one.js']);
});

it('loads indexed source-maps with unresolved children', async () => {
const map = await factory.load({
sourceMapUrl: toDataUri(unresolvedIndexedSourceMap),
compiledPath: '/tmp/local/one.js',
});

expect(map.sources).to.eql(['one.js']);
});

it('warns without failure if a single nested child fails', async () => {
const warn = stub(Logger.null, 'warn');
const map = await factory.load({
sourceMapUrl:
'data:application/json;base64,' +
Buffer.from(JSON.stringify(indexedSourceMap)).toString('base64'),
sourceMapUrl: toDataUri({
...unresolvedIndexedSourceMap,
sections: [
...unresolvedIndexedSourceMap.sections,
{ url: 'invalid', offset: { line: 0, column: 0 } },
],
}),
compiledPath: '/tmp/local/one.js',
});

expect(map.sources).to.eql(['one.js']);
expect(warn.called).to.be.true;
warn.restore();
});
});
73 changes: 60 additions & 13 deletions src/common/sourceMaps/sourceMapFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@
*--------------------------------------------------------*/

import { inject, injectable } from 'inversify';
import { RawIndexMap, RawSourceMap, SourceMapConsumer } from 'source-map';
import {
Position,
RawIndexMap,
RawSection,
RawSourceMap,
SourceMapConsumer,
StartOfSourceMap,
} from 'source-map';
import { IResourceProvider } from '../../adapter/resourceProvider';
import Dap from '../../dap/api';
import { IRootDapApi } from '../../dap/connection';
import { sourceMapParseFailed } from '../../dap/errors';
import { MapUsingProjection } from '../datastructure/mapUsingProjection';
import { IDisposable } from '../disposable';
import { ILogger, LogTag } from '../logging';
import { truthy } from '../objUtils';
import { ISourcePathResolver } from '../sourcePathResolver';
import { fileUrlToAbsolutePath, isDataUri } from '../urlUtils';
import { ISourceMapMetadata, SourceMap } from './sourceMap';
Expand All @@ -35,6 +43,20 @@ export interface ISourceMapFactory extends IDisposable {
guardSourceMapFn<T>(sourceMap: SourceMap, fn: () => T, defaultValue: () => T): T;
}

interface RawExternalSection {
offset: Position;
url: string;
}

/**
* The typings for source-map don't support this, but the spec does.
* @see https://sourcemaps.info/spec.html#h.535es3xeprgt
*/
export interface RawIndexMapUnresolved extends StartOfSourceMap {
version: number;
sections: (RawExternalSection | RawSection)[];
}

/**
* Base implementation of the ISourceMapFactory.
*/
Expand All @@ -57,15 +79,7 @@ export class SourceMapFactory implements ISourceMapFactory {
* @inheritdoc
*/
public async load(metadata: ISourceMapMetadata): Promise<SourceMap> {
let basic: RawSourceMap | RawIndexMap | undefined;
try {
basic = await this.parseSourceMap(metadata.sourceMapUrl);
} catch (e) {
basic = await this.parsePathMappedSourceMap(metadata.sourceMapUrl);
if (!basic) {
throw e;
}
}
const basic = await this.parseSourceMap(metadata.sourceMapUrl);

// The source-map library is destructive with its sources parsing. If the
// source root is '/', it'll "helpfully" resolve a source like `../foo.ts`
Expand All @@ -81,7 +95,7 @@ export class SourceMapFactory implements ISourceMapFactory {
// 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")
let actualSources: string[] = [];
if ('sections' in basic && Array.isArray(basic.sections)) {
if ('sections' in basic) {
actualSources = [];
let i = 0;
for (const section of basic.sections) {
Expand All @@ -104,6 +118,37 @@ export class SourceMapFactory implements ISourceMapFactory {
);
}

private async parseSourceMap(sourceMapUrl: string): Promise<RawSourceMap | RawIndexMap> {
let sm: RawSourceMap | RawIndexMapUnresolved | undefined;
try {
sm = await this.parseSourceMapDirect(sourceMapUrl);
} catch (e) {
sm = await this.parsePathMappedSourceMap(sourceMapUrl);
if (!sm) {
throw e;
}
}

if ('sections' in sm) {
const resolved = await Promise.all(
sm.sections.map((s, i) =>
'url' in s
? this.parseSourceMap(s.url)
.then(map => ({ offset: s.offset, map: map as RawSourceMap }))
.catch(e => {
this.logger.warn(LogTag.SourceMapParsing, `Error parsing nested map ${i}: ${e}`);
return undefined;
})
: s,
),
);

sm.sections = resolved.filter(truthy);
}

return sm as RawSourceMap | RawIndexMap;
}

public async parsePathMappedSourceMap(url: string) {
if (isDataUri(url)) {
return;
Expand All @@ -113,7 +158,7 @@ export class SourceMapFactory implements ISourceMapFactory {
if (!localSourceMapUrl) return;

try {
return this.parseSourceMap(localSourceMapUrl);
return this.parseSourceMapDirect(localSourceMapUrl);
} catch (error) {
this.logger.info(LogTag.SourceMapParsing, 'Parsing path mapped source map failed.', error);
}
Expand Down Expand Up @@ -150,7 +195,9 @@ export class SourceMapFactory implements ISourceMapFactory {
// no-op
}

private async parseSourceMap(sourceMapUrl: string): Promise<RawSourceMap | RawIndexMap> {
private async parseSourceMapDirect(
sourceMapUrl: string,
): Promise<RawSourceMap | RawIndexMapUnresolved> {
let absolutePath = fileUrlToAbsolutePath(sourceMapUrl);
if (absolutePath) {
absolutePath = this.pathResolve.rebaseRemoteToLocal(absolutePath);
Expand Down

0 comments on commit 577851f

Please sign in to comment.