Skip to content

Commit decd95e

Browse files
petebacondarwinAndrewKushnir
authored andcommitted
fix(compiler-cli): ensure source-maps can handle webpack:// protocol (angular#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 angular#32912
1 parent 6abb8d0 commit decd95e

File tree

3 files changed

+77
-5
lines changed

3 files changed

+77
-5
lines changed

packages/compiler-cli/ngcc/src/rendering/source_maps.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function renderSourceAndMap(
3535
{file: generatedPath, source: generatedPath, includeContent: true});
3636

3737
try {
38-
const loader = new SourceFileLoader(fs, logger);
38+
const loader = new SourceFileLoader(fs, logger, {});
3939
const generatedFile = loader.loadSourceFile(
4040
generatedPath, generatedContent, {map: generatedMap, mapPath: generatedMapPath});
4141

packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file_loader.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {Logger} from '../../logging';
1313
import {RawSourceMap} from './raw_source_map';
1414
import {SourceFile} from './source_file';
1515

16+
const SCHEME_MATCHER = /^([a-z][a-z0-9.-]*):\/\//i;
17+
1618
/**
1719
* This class can be used to load a source file, its associated source map and any upstream sources.
1820
*
@@ -25,7 +27,10 @@ import {SourceFile} from './source_file';
2527
export class SourceFileLoader {
2628
private currentPaths: AbsoluteFsPath[] = [];
2729

28-
constructor(private fs: FileSystem, private logger: Logger) {}
30+
constructor(
31+
private fs: FileSystem, private logger: Logger,
32+
/** A map of URL schemes to base paths. The scheme name should be lowercase. */
33+
private schemeMap: Record<string, AbsoluteFsPath>) {}
2934

3035
/**
3136
* Load a source file, compute its source map, and recursively load any referenced source files.
@@ -128,9 +133,10 @@ export class SourceFileLoader {
128133
* source file and its associated source map.
129134
*/
130135
private processSources(basePath: AbsoluteFsPath, map: RawSourceMap): (SourceFile|null)[] {
131-
const sourceRoot = this.fs.resolve(this.fs.dirname(basePath), map.sourceRoot || '');
136+
const sourceRoot = this.fs.resolve(
137+
this.fs.dirname(basePath), this.replaceSchemeWithPath(map.sourceRoot || ''));
132138
return map.sources.map((source, index) => {
133-
const path = this.fs.resolve(sourceRoot, source);
139+
const path = this.fs.resolve(sourceRoot, this.replaceSchemeWithPath(source));
134140
const content = map.sourcesContent && map.sourcesContent[index] || null;
135141
return this.loadSourceFile(path, content, null);
136142
});
@@ -168,6 +174,19 @@ export class SourceFileLoader {
168174
}
169175
this.currentPaths.push(path);
170176
}
177+
178+
/**
179+
* Replace any matched URL schemes with their corresponding path held in the schemeMap.
180+
*
181+
* Some build tools replace real file paths with scheme prefixed paths - e.g. `webpack://`.
182+
* We use the `schemeMap` passed to this class to convert such paths to "real" file paths.
183+
* In some cases, this is not possible, since the file was actually synthesized by the build tool.
184+
* But the end result is better than prefixing the sourceRoot in front of the scheme.
185+
*/
186+
private replaceSchemeWithPath(path: string): string {
187+
return path.replace(
188+
SCHEME_MATCHER, (_: string, scheme: string) => this.schemeMap[scheme.toLowerCase()] || '');
189+
}
171190
}
172191

173192
/** A small helper structure that is returned from `loadSourceMap()`. */

packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_loader_spec.ts

+54-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ runInEachFileSystem(() => {
2323
fs = getFileSystem();
2424
logger = new MockLogger();
2525
_ = absoluteFrom;
26-
registry = new SourceFileLoader(fs, logger);
26+
registry = new SourceFileLoader(fs, logger, {webpack: _('/foo')});
2727
});
2828

2929
describe('loadSourceFile', () => {
@@ -279,6 +279,59 @@ runInEachFileSystem(() => {
279279

280280
expect(() => registry.loadSourceFile(aPath)).not.toThrow();
281281
});
282+
283+
for (const {scheme, mappedPath} of
284+
[{scheme: 'WEBPACK://', mappedPath: '/foo/src/index.ts'},
285+
{scheme: 'webpack://', mappedPath: '/foo/src/index.ts'},
286+
{scheme: 'missing://', mappedPath: '/src/index.ts'},
287+
]) {
288+
it(`should handle source paths that are protocol mapped [scheme:"${scheme}"]`, () => {
289+
fs.ensureDir(_('/foo/src'));
290+
291+
const indexSourceMap = createRawSourceMap({
292+
file: 'index.js',
293+
sources: [`${scheme}/src/index.ts`],
294+
'sourcesContent': ['original content']
295+
});
296+
fs.writeFile(_('/foo/src/index.js.map'), JSON.stringify(indexSourceMap));
297+
const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'generated content');
298+
if (sourceFile === null) {
299+
return fail('Expected source file to be defined');
300+
}
301+
const originalSource = sourceFile.sources[0];
302+
if (originalSource === null) {
303+
return fail('Expected source file to be defined');
304+
}
305+
expect(originalSource.contents).toEqual('original content');
306+
expect(originalSource.sourcePath).toEqual(_(mappedPath));
307+
expect(originalSource.rawMap).toEqual(null);
308+
expect(originalSource.sources).toEqual([]);
309+
});
310+
311+
it(`should handle source roots that are protocol mapped [scheme:"${scheme}"]`, () => {
312+
fs.ensureDir(_('/foo/src'));
313+
314+
const indexSourceMap = createRawSourceMap({
315+
file: 'index.js',
316+
sources: ['index.ts'],
317+
'sourcesContent': ['original content'],
318+
sourceRoot: `${scheme}/src`,
319+
});
320+
fs.writeFile(_('/foo/src/index.js.map'), JSON.stringify(indexSourceMap));
321+
const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'generated content');
322+
if (sourceFile === null) {
323+
return fail('Expected source file to be defined');
324+
}
325+
const originalSource = sourceFile.sources[0];
326+
if (originalSource === null) {
327+
return fail('Expected source file to be defined');
328+
}
329+
expect(originalSource.contents).toEqual('original content');
330+
expect(originalSource.sourcePath).toEqual(_(mappedPath));
331+
expect(originalSource.rawMap).toEqual(null);
332+
expect(originalSource.sources).toEqual([]);
333+
});
334+
}
282335
});
283336
});
284337

0 commit comments

Comments
 (0)