From decd95e7f0ec934c82f5b280205768fed055ec3c Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 14 Jun 2020 23:19:36 +0100 Subject: [PATCH] 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 --- .../ngcc/src/rendering/source_maps.ts | 2 +- .../sourcemaps/src/source_file_loader.ts | 25 ++++++++- .../test/source_file_loader_spec.ts | 55 ++++++++++++++++++- 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/rendering/source_maps.ts b/packages/compiler-cli/ngcc/src/rendering/source_maps.ts index e15e2cdaf5..ec1b64c1f4 100644 --- a/packages/compiler-cli/ngcc/src/rendering/source_maps.ts +++ b/packages/compiler-cli/ngcc/src/rendering/source_maps.ts @@ -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}); diff --git a/packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file_loader.ts b/packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file_loader.ts index 1b7d49333e..f51e903b92 100644 --- a/packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file_loader.ts +++ b/packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file_loader.ts @@ -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) {} /** * 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()`. */ diff --git a/packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_loader_spec.ts b/packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_loader_spec.ts index 3d68671674..72a9201e32 100644 --- a/packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_loader_spec.ts +++ b/packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_loader_spec.ts @@ -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([]); + }); + } }); });