fix(compiler-cli): ensure source-maps can handle webpack:// protocol (#32912)

Webpack and other build tools sometimes inline the contents of the
source files in their generated source-maps, and at the same time
change the paths to be prefixed with a protocol, such as `webpack://`.

This can confuse tools that need to read these paths, so now it is
possible to provide a mapping to where these files originated.

PR Close #32912
This commit is contained in:
Pete Bacon Darwin 2020-06-14 23:19:36 +01:00 committed by Andrew Kushnir
parent 6abb8d0d91
commit decd95e7f0
3 changed files with 77 additions and 5 deletions

View File

@ -35,7 +35,7 @@ export function renderSourceAndMap(
{file: generatedPath, source: generatedPath, includeContent: true});
try {
const loader = new SourceFileLoader(fs, logger);
const loader = new SourceFileLoader(fs, logger, {});
const generatedFile = loader.loadSourceFile(
generatedPath, generatedContent, {map: generatedMap, mapPath: generatedMapPath});

View File

@ -13,6 +13,8 @@ import {Logger} from '../../logging';
import {RawSourceMap} from './raw_source_map';
import {SourceFile} from './source_file';
const SCHEME_MATCHER = /^([a-z][a-z0-9.-]*):\/\//i;
/**
* This class can be used to load a source file, its associated source map and any upstream sources.
*
@ -25,7 +27,10 @@ import {SourceFile} from './source_file';
export class SourceFileLoader {
private currentPaths: AbsoluteFsPath[] = [];
constructor(private fs: FileSystem, private logger: Logger) {}
constructor(
private fs: FileSystem, private logger: Logger,
/** A map of URL schemes to base paths. The scheme name should be lowercase. */
private schemeMap: Record<string, AbsoluteFsPath>) {}
/**
* Load a source file, compute its source map, and recursively load any referenced source files.
@ -128,9 +133,10 @@ export class SourceFileLoader {
* source file and its associated source map.
*/
private processSources(basePath: AbsoluteFsPath, map: RawSourceMap): (SourceFile|null)[] {
const sourceRoot = this.fs.resolve(this.fs.dirname(basePath), map.sourceRoot || '');
const sourceRoot = this.fs.resolve(
this.fs.dirname(basePath), this.replaceSchemeWithPath(map.sourceRoot || ''));
return map.sources.map((source, index) => {
const path = this.fs.resolve(sourceRoot, source);
const path = this.fs.resolve(sourceRoot, this.replaceSchemeWithPath(source));
const content = map.sourcesContent && map.sourcesContent[index] || null;
return this.loadSourceFile(path, content, null);
});
@ -168,6 +174,19 @@ export class SourceFileLoader {
}
this.currentPaths.push(path);
}
/**
* Replace any matched URL schemes with their corresponding path held in the schemeMap.
*
* Some build tools replace real file paths with scheme prefixed paths - e.g. `webpack://`.
* We use the `schemeMap` passed to this class to convert such paths to "real" file paths.
* In some cases, this is not possible, since the file was actually synthesized by the build tool.
* But the end result is better than prefixing the sourceRoot in front of the scheme.
*/
private replaceSchemeWithPath(path: string): string {
return path.replace(
SCHEME_MATCHER, (_: string, scheme: string) => this.schemeMap[scheme.toLowerCase()] || '');
}
}
/** A small helper structure that is returned from `loadSourceMap()`. */

View File

@ -23,7 +23,7 @@ runInEachFileSystem(() => {
fs = getFileSystem();
logger = new MockLogger();
_ = absoluteFrom;
registry = new SourceFileLoader(fs, logger);
registry = new SourceFileLoader(fs, logger, {webpack: _('/foo')});
});
describe('loadSourceFile', () => {
@ -279,6 +279,59 @@ runInEachFileSystem(() => {
expect(() => registry.loadSourceFile(aPath)).not.toThrow();
});
for (const {scheme, mappedPath} of
[{scheme: 'WEBPACK://', mappedPath: '/foo/src/index.ts'},
{scheme: 'webpack://', mappedPath: '/foo/src/index.ts'},
{scheme: 'missing://', mappedPath: '/src/index.ts'},
]) {
it(`should handle source paths that are protocol mapped [scheme:"${scheme}"]`, () => {
fs.ensureDir(_('/foo/src'));
const indexSourceMap = createRawSourceMap({
file: 'index.js',
sources: [`${scheme}/src/index.ts`],
'sourcesContent': ['original content']
});
fs.writeFile(_('/foo/src/index.js.map'), JSON.stringify(indexSourceMap));
const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'generated content');
if (sourceFile === null) {
return fail('Expected source file to be defined');
}
const originalSource = sourceFile.sources[0];
if (originalSource === null) {
return fail('Expected source file to be defined');
}
expect(originalSource.contents).toEqual('original content');
expect(originalSource.sourcePath).toEqual(_(mappedPath));
expect(originalSource.rawMap).toEqual(null);
expect(originalSource.sources).toEqual([]);
});
it(`should handle source roots that are protocol mapped [scheme:"${scheme}"]`, () => {
fs.ensureDir(_('/foo/src'));
const indexSourceMap = createRawSourceMap({
file: 'index.js',
sources: ['index.ts'],
'sourcesContent': ['original content'],
sourceRoot: `${scheme}/src`,
});
fs.writeFile(_('/foo/src/index.js.map'), JSON.stringify(indexSourceMap));
const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'generated content');
if (sourceFile === null) {
return fail('Expected source file to be defined');
}
const originalSource = sourceFile.sources[0];
if (originalSource === null) {
return fail('Expected source file to be defined');
}
expect(originalSource.contents).toEqual('original content');
expect(originalSource.sourcePath).toEqual(_(mappedPath));
expect(originalSource.rawMap).toEqual(null);
expect(originalSource.sources).toEqual([]);
});
}
});
});