The library used by ngcc to update the source files (MagicString) is able to generate a source-map but it is not able to account for any previous source-map that the input text is already associated with. There have been various attempts to fix this but none have been very successful, since it is not a trivial problem to solve. This commit contains a novel approach that is able to load up a tree of source-files connected by source-maps and flatten them down into a single source-map that maps directly from the final generated file to the original sources referenced by the intermediate source-maps. PR Close #35132
		
			
				
	
	
		
			243 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			243 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @license
 | |
|  * Copyright Google Inc. All Rights Reserved.
 | |
|  *
 | |
|  * Use of this source code is governed by an MIT-style license that can be
 | |
|  * found in the LICENSE file at https://angular.io/license
 | |
|  */
 | |
| import {FileSystem, absoluteFrom, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
 | |
| import {fromObject} from 'convert-source-map';
 | |
| 
 | |
| import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
 | |
| import {RawSourceMap} from '../../src/sourcemaps/raw_source_map';
 | |
| import {SourceFileLoader as SourceFileLoader} from '../../src/sourcemaps/source_file_loader';
 | |
| 
 | |
| runInEachFileSystem(() => {
 | |
|   describe('SourceFileLoader', () => {
 | |
|     let fs: FileSystem;
 | |
|     let _: typeof absoluteFrom;
 | |
|     let registry: SourceFileLoader;
 | |
|     beforeEach(() => {
 | |
|       fs = getFileSystem();
 | |
|       _ = absoluteFrom;
 | |
|       registry = new SourceFileLoader(fs);
 | |
|     });
 | |
| 
 | |
|     describe('loadSourceFile', () => {
 | |
|       it('should load a file with no source map and inline contents', () => {
 | |
|         const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'some inline content');
 | |
|         if (sourceFile === null) {
 | |
|           return fail('Expected source file to be defined');
 | |
|         }
 | |
|         expect(sourceFile.contents).toEqual('some inline content');
 | |
|         expect(sourceFile.sourcePath).toEqual(_('/foo/src/index.js'));
 | |
|         expect(sourceFile.rawMap).toEqual(null);
 | |
|         expect(sourceFile.sources).toEqual([]);
 | |
|       });
 | |
| 
 | |
|       it('should load a file with no source map and read its contents from disk', () => {
 | |
|         fs.ensureDir(_('/foo/src'));
 | |
|         fs.writeFile(_('/foo/src/index.js'), 'some external content');
 | |
|         const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'));
 | |
|         if (sourceFile === null) {
 | |
|           return fail('Expected source file to be defined');
 | |
|         }
 | |
|         expect(sourceFile.contents).toEqual('some external content');
 | |
|         expect(sourceFile.sourcePath).toEqual(_('/foo/src/index.js'));
 | |
|         expect(sourceFile.rawMap).toEqual(null);
 | |
|         expect(sourceFile.sources).toEqual([]);
 | |
|       });
 | |
| 
 | |
|       it('should load a file with an external source map', () => {
 | |
|         fs.ensureDir(_('/foo/src'));
 | |
|         const sourceMap = createRawSourceMap({file: 'index.js'});
 | |
|         fs.writeFile(_('/foo/src/external.js.map'), JSON.stringify(sourceMap));
 | |
|         const sourceFile = registry.loadSourceFile(
 | |
|             _('/foo/src/index.js'), 'some inline content\n//# sourceMappingURL=external.js.map');
 | |
|         if (sourceFile === null) {
 | |
|           return fail('Expected source file to be defined');
 | |
|         }
 | |
|         expect(sourceFile.rawMap).toEqual(sourceMap);
 | |
|       });
 | |
| 
 | |
|       it('should handle a missing external source map', () => {
 | |
|         fs.ensureDir(_('/foo/src'));
 | |
|         const sourceFile = registry.loadSourceFile(
 | |
|             _('/foo/src/index.js'), 'some inline content\n//# sourceMappingURL=external.js.map');
 | |
|         if (sourceFile === null) {
 | |
|           return fail('Expected source file to be defined');
 | |
|         }
 | |
|         expect(sourceFile.rawMap).toBe(null);
 | |
|       });
 | |
| 
 | |
|       it('should load a file with an inline encoded source map', () => {
 | |
|         const sourceMap = createRawSourceMap({file: 'index.js'});
 | |
|         const encodedSourceMap = Buffer.from(JSON.stringify(sourceMap)).toString('base64');
 | |
|         const sourceFile = registry.loadSourceFile(
 | |
|             _('/foo/src/index.js'),
 | |
|             `some inline content\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${encodedSourceMap}`);
 | |
|         if (sourceFile === null) {
 | |
|           return fail('Expected source file to be defined');
 | |
|         }
 | |
|         expect(sourceFile.rawMap).toEqual(sourceMap);
 | |
|       });
 | |
| 
 | |
|       it('should load a file with an implied source map', () => {
 | |
|         const sourceMap = createRawSourceMap({file: 'index.js'});
 | |
|         fs.ensureDir(_('/foo/src'));
 | |
|         fs.writeFile(_('/foo/src/index.js.map'), JSON.stringify(sourceMap));
 | |
|         const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'some inline content');
 | |
|         if (sourceFile === null) {
 | |
|           return fail('Expected source file to be defined');
 | |
|         }
 | |
|         expect(sourceFile.rawMap).toEqual(sourceMap);
 | |
|       });
 | |
| 
 | |
|       it('should handle missing implied source-map file', () => {
 | |
|         fs.ensureDir(_('/foo/src'));
 | |
|         const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'some inline content');
 | |
|         if (sourceFile === null) {
 | |
|           return fail('Expected source file to be defined');
 | |
|         }
 | |
|         expect(sourceFile.rawMap).toBe(null);
 | |
|       });
 | |
| 
 | |
|       it('should recurse into external original source files that are referenced from source maps',
 | |
|          () => {
 | |
|            // Setup a scenario where the generated files reference previous files:
 | |
|            //
 | |
|            // index.js
 | |
|            //  -> x.js
 | |
|            //  -> y.js
 | |
|            //       -> a.js
 | |
|            //  -> z.js (inline content)
 | |
|            fs.ensureDir(_('/foo/src'));
 | |
| 
 | |
|            const indexSourceMap = createRawSourceMap({
 | |
|              file: 'index.js',
 | |
|              sources: ['x.js', 'y.js', 'z.js'],
 | |
|              'sourcesContent': [null, null, 'z content']
 | |
|            });
 | |
|            fs.writeFile(_('/foo/src/index.js.map'), JSON.stringify(indexSourceMap));
 | |
| 
 | |
|            fs.writeFile(_('/foo/src/x.js'), 'x content');
 | |
| 
 | |
|            const ySourceMap = createRawSourceMap({file: 'y.js', sources: ['a.js']});
 | |
|            fs.writeFile(_('/foo/src/y.js'), 'y content');
 | |
|            fs.writeFile(_('/foo/src/y.js.map'), JSON.stringify(ySourceMap));
 | |
|            fs.writeFile(_('/foo/src/z.js'), 'z content');
 | |
|            fs.writeFile(_('/foo/src/a.js'), 'a content');
 | |
| 
 | |
|            const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'index content');
 | |
|            if (sourceFile === null) {
 | |
|              return fail('Expected source file to be defined');
 | |
|            }
 | |
| 
 | |
|            expect(sourceFile.contents).toEqual('index content');
 | |
|            expect(sourceFile.sourcePath).toEqual(_('/foo/src/index.js'));
 | |
|            expect(sourceFile.rawMap).toEqual(indexSourceMap);
 | |
| 
 | |
|            expect(sourceFile.sources.length).toEqual(3);
 | |
| 
 | |
|            expect(sourceFile.sources[0] !.contents).toEqual('x content');
 | |
|            expect(sourceFile.sources[0] !.sourcePath).toEqual(_('/foo/src/x.js'));
 | |
|            expect(sourceFile.sources[0] !.rawMap).toEqual(null);
 | |
|            expect(sourceFile.sources[0] !.sources).toEqual([]);
 | |
| 
 | |
| 
 | |
|            expect(sourceFile.sources[1] !.contents).toEqual('y content');
 | |
|            expect(sourceFile.sources[1] !.sourcePath).toEqual(_('/foo/src/y.js'));
 | |
|            expect(sourceFile.sources[1] !.rawMap).toEqual(ySourceMap);
 | |
| 
 | |
|            expect(sourceFile.sources[1] !.sources.length).toEqual(1);
 | |
|            expect(sourceFile.sources[1] !.sources[0] !.contents).toEqual('a content');
 | |
|            expect(sourceFile.sources[1] !.sources[0] !.sourcePath).toEqual(_('/foo/src/a.js'));
 | |
|            expect(sourceFile.sources[1] !.sources[0] !.rawMap).toEqual(null);
 | |
|            expect(sourceFile.sources[1] !.sources[0] !.sources).toEqual([]);
 | |
| 
 | |
|            expect(sourceFile.sources[2] !.contents).toEqual('z content');
 | |
|            expect(sourceFile.sources[2] !.sourcePath).toEqual(_('/foo/src/z.js'));
 | |
|            expect(sourceFile.sources[2] !.rawMap).toEqual(null);
 | |
|            expect(sourceFile.sources[2] !.sources).toEqual([]);
 | |
|          });
 | |
| 
 | |
|       it('should handle a missing source file referenced from a source-map', () => {
 | |
|         fs.ensureDir(_('/foo/src'));
 | |
| 
 | |
|         const indexSourceMap =
 | |
|             createRawSourceMap({file: 'index.js', sources: ['x.js'], 'sourcesContent': [null]});
 | |
|         fs.writeFile(_('/foo/src/index.js.map'), JSON.stringify(indexSourceMap));
 | |
| 
 | |
|         const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'index content');
 | |
|         if (sourceFile === null) {
 | |
|           return fail('Expected source file to be defined');
 | |
|         }
 | |
| 
 | |
|         expect(sourceFile.contents).toEqual('index content');
 | |
|         expect(sourceFile.sourcePath).toEqual(_('/foo/src/index.js'));
 | |
|         expect(sourceFile.rawMap).toEqual(indexSourceMap);
 | |
|         expect(sourceFile.sources.length).toEqual(1);
 | |
|         expect(sourceFile.sources[0]).toBe(null);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('should fail if there is a cyclic dependency in files loaded from disk', () => {
 | |
|       fs.ensureDir(_('/foo/src'));
 | |
| 
 | |
|       const aPath = _('/foo/src/a.js');
 | |
|       fs.writeFile(
 | |
|           aPath, 'a content\n' +
 | |
|               fromObject(createRawSourceMap({file: 'a.js', sources: ['b.js']})).toComment());
 | |
| 
 | |
|       const bPath = _('/foo/src/b.js');
 | |
|       fs.writeFile(
 | |
|           bPath, 'b content\n' +
 | |
|               fromObject(createRawSourceMap({file: 'b.js', sources: ['c.js']})).toComment());
 | |
| 
 | |
|       const cPath = _('/foo/src/c.js');
 | |
|       fs.writeFile(
 | |
|           cPath, 'c content\n' +
 | |
|               fromObject(createRawSourceMap({file: 'c.js', sources: ['a.js']})).toComment());
 | |
| 
 | |
|       expect(() => registry.loadSourceFile(aPath))
 | |
|           .toThrowError(
 | |
|               `Circular source file mapping dependency: ${aPath} -> ${bPath} -> ${cPath} -> ${aPath}`);
 | |
|     });
 | |
| 
 | |
|     it('should not fail if there is a cyclic dependency in filenames of inline sources', () => {
 | |
|       fs.ensureDir(_('/foo/src'));
 | |
| 
 | |
|       const aPath = _('/foo/src/a.js');
 | |
|       fs.writeFile(
 | |
|           aPath, 'a content\n' +
 | |
|               fromObject(createRawSourceMap({file: 'a.js', sources: ['b.js']})).toComment());
 | |
| 
 | |
|       const bPath = _('/foo/src/b.js');
 | |
|       fs.writeFile(bPath, 'b content');
 | |
|       fs.writeFile(
 | |
|           _('/foo/src/b.js.map'),
 | |
|           JSON.stringify(createRawSourceMap({file: 'b.js', sources: ['c.js']})));
 | |
| 
 | |
|       const cPath = _('/foo/src/c.js');
 | |
|       fs.writeFile(cPath, 'c content');
 | |
|       fs.writeFile(
 | |
|           _('/foo/src/c.js.map'),
 | |
|           JSON.stringify(createRawSourceMap(
 | |
|               {file: 'c.js', sources: ['a.js'], sourcesContent: ['inline a.js content']})));
 | |
| 
 | |
|       expect(() => registry.loadSourceFile(aPath)).not.toThrow();
 | |
|     });
 | |
|   });
 | |
| });
 | |
| 
 | |
| 
 | |
| function createRawSourceMap(custom: Partial<RawSourceMap>): RawSourceMap {
 | |
|   return {
 | |
|     'version': 3,
 | |
|     'sourceRoot': '',
 | |
|     'sources': [],
 | |
|     'sourcesContent': [],
 | |
|     'names': [],
 | |
|     'mappings': '', ...custom
 | |
|   };
 | |
| } |