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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user