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}); | ||||
| 
 | ||||
|   try { | ||||
|     const loader = new SourceFileLoader(fs, logger); | ||||
|     const loader = new SourceFileLoader(fs, logger, {}); | ||||
|     const generatedFile = loader.loadSourceFile( | ||||
|         generatedPath, generatedContent, {map: generatedMap, mapPath: generatedMapPath}); | ||||
| 
 | ||||
|  | ||||
| @ -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()`. */ | ||||
|  | ||||
| @ -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([]); | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user