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:
parent
6abb8d0d91
commit
decd95e7f0
|
@ -35,7 +35,7 @@ export function renderSourceAndMap(
|
||||||
{file: generatedPath, source: generatedPath, includeContent: true});
|
{file: generatedPath, source: generatedPath, includeContent: true});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loader = new SourceFileLoader(fs, logger);
|
const loader = new SourceFileLoader(fs, logger, {});
|
||||||
const generatedFile = loader.loadSourceFile(
|
const generatedFile = loader.loadSourceFile(
|
||||||
generatedPath, generatedContent, {map: generatedMap, mapPath: generatedMapPath});
|
generatedPath, generatedContent, {map: generatedMap, mapPath: generatedMapPath});
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {Logger} from '../../logging';
|
||||||
import {RawSourceMap} from './raw_source_map';
|
import {RawSourceMap} from './raw_source_map';
|
||||||
import {SourceFile} from './source_file';
|
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.
|
* 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 {
|
export class SourceFileLoader {
|
||||||
private currentPaths: AbsoluteFsPath[] = [];
|
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.
|
* 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.
|
* source file and its associated source map.
|
||||||
*/
|
*/
|
||||||
private processSources(basePath: AbsoluteFsPath, map: RawSourceMap): (SourceFile|null)[] {
|
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) => {
|
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;
|
const content = map.sourcesContent && map.sourcesContent[index] || null;
|
||||||
return this.loadSourceFile(path, content, null);
|
return this.loadSourceFile(path, content, null);
|
||||||
});
|
});
|
||||||
|
@ -168,6 +174,19 @@ export class SourceFileLoader {
|
||||||
}
|
}
|
||||||
this.currentPaths.push(path);
|
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()`. */
|
/** A small helper structure that is returned from `loadSourceMap()`. */
|
||||||
|
|
|
@ -23,7 +23,7 @@ runInEachFileSystem(() => {
|
||||||
fs = getFileSystem();
|
fs = getFileSystem();
|
||||||
logger = new MockLogger();
|
logger = new MockLogger();
|
||||||
_ = absoluteFrom;
|
_ = absoluteFrom;
|
||||||
registry = new SourceFileLoader(fs, logger);
|
registry = new SourceFileLoader(fs, logger, {webpack: _('/foo')});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loadSourceFile', () => {
|
describe('loadSourceFile', () => {
|
||||||
|
@ -279,6 +279,59 @@ runInEachFileSystem(() => {
|
||||||
|
|
||||||
expect(() => registry.loadSourceFile(aPath)).not.toThrow();
|
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([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue