Previously when rendering flattened source-maps, it was assumed that no mapping would come from a line that is outside the lines of the actual source content. It turns out this is not a valid assumption. Now the code that renders flattened source-maps will handle such mappings, with the additional benefit that the rendered source-map will only contain mapping lines up to the last mapping, rather than a mapping line for every content line. Fixes #35709 PR Close #35718
		
			
				
	
	
		
			276 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			276 lines
		
	
	
		
			11 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 {encode} from 'sourcemap-codec';
 | 
						|
 | 
						|
import {FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
 | 
						|
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
 | 
						|
import {RawSourceMap} from '../../src/sourcemaps/raw_source_map';
 | 
						|
import {SourceFile, computeLineLengths, extractOriginalSegments, parseMappings} from '../../src/sourcemaps/source_file';
 | 
						|
 | 
						|
runInEachFileSystem(() => {
 | 
						|
  describe('SourceFile and utilities', () => {
 | 
						|
    let fs: FileSystem;
 | 
						|
    let _: typeof absoluteFrom;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      fs = getFileSystem();
 | 
						|
      _ = absoluteFrom;
 | 
						|
    });
 | 
						|
 | 
						|
    describe('parseMappings()', () => {
 | 
						|
      it('should be an empty array for source files with no source map', () => {
 | 
						|
        const mappings = parseMappings(null, []);
 | 
						|
        expect(mappings).toEqual([]);
 | 
						|
      });
 | 
						|
 | 
						|
      it('should be empty array for source files with no source map mappings', () => {
 | 
						|
        const rawSourceMap: RawSourceMap = {mappings: '', names: [], sources: [], version: 3};
 | 
						|
        const mappings = parseMappings(rawSourceMap, []);
 | 
						|
        expect(mappings).toEqual([]);
 | 
						|
      });
 | 
						|
 | 
						|
      it('should parse the mappings from the raw source map', () => {
 | 
						|
        const rawSourceMap: RawSourceMap = {
 | 
						|
          mappings: encode([[[0, 0, 0, 0], [6, 0, 0, 3]]]),
 | 
						|
          names: [],
 | 
						|
          sources: ['a.js'],
 | 
						|
          version: 3
 | 
						|
        };
 | 
						|
        const originalSource = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, []);
 | 
						|
        const mappings = parseMappings(rawSourceMap, [originalSource]);
 | 
						|
        expect(mappings).toEqual([
 | 
						|
          {
 | 
						|
            generatedSegment: {line: 0, column: 0},
 | 
						|
            originalSource,
 | 
						|
            originalSegment: {line: 0, column: 0},
 | 
						|
            name: undefined
 | 
						|
          },
 | 
						|
          {
 | 
						|
            generatedSegment: {line: 0, column: 6},
 | 
						|
            originalSource,
 | 
						|
            originalSegment: {line: 0, column: 3},
 | 
						|
            name: undefined
 | 
						|
          },
 | 
						|
        ]);
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    describe('extractOriginalSegments()', () => {
 | 
						|
      it('should return an empty array for source files with no source map',
 | 
						|
         () => { expect(extractOriginalSegments(parseMappings(null, []))).toEqual([]); });
 | 
						|
 | 
						|
      it('should be empty array for source files with no source map mappings', () => {
 | 
						|
        const rawSourceMap: RawSourceMap = {mappings: '', names: [], sources: [], version: 3};
 | 
						|
        expect(extractOriginalSegments(parseMappings(rawSourceMap, []))).toEqual([]);
 | 
						|
      });
 | 
						|
 | 
						|
      it('should parse the segments in ascending order of original position from the raw source map',
 | 
						|
         () => {
 | 
						|
           const originalSource = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, []);
 | 
						|
           const rawSourceMap: RawSourceMap = {
 | 
						|
             mappings: encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2]]]),
 | 
						|
             names: [],
 | 
						|
             sources: ['a.js'],
 | 
						|
             version: 3
 | 
						|
           };
 | 
						|
           expect(extractOriginalSegments(parseMappings(rawSourceMap, [originalSource]))).toEqual([
 | 
						|
             {line: 0, column: 0},
 | 
						|
             {line: 0, column: 2},
 | 
						|
             {line: 0, column: 3},
 | 
						|
           ]);
 | 
						|
         });
 | 
						|
    });
 | 
						|
 | 
						|
    describe('SourceFile', () => {
 | 
						|
      describe('flattenedMappings', () => {
 | 
						|
        it('should be an empty array for source files with no source map', () => {
 | 
						|
          const sourceFile =
 | 
						|
              new SourceFile(_('/foo/src/index.js'), 'index contents', null, false, []);
 | 
						|
          expect(sourceFile.flattenedMappings).toEqual([]);
 | 
						|
        });
 | 
						|
 | 
						|
        it('should be empty array for source files with no source map mappings', () => {
 | 
						|
          const rawSourceMap: RawSourceMap = {mappings: '', names: [], sources: [], version: 3};
 | 
						|
          const sourceFile =
 | 
						|
              new SourceFile(_('/foo/src/index.js'), 'index contents', rawSourceMap, false, []);
 | 
						|
          expect(sourceFile.flattenedMappings).toEqual([]);
 | 
						|
        });
 | 
						|
 | 
						|
        it('should be the same as non-flat mappings if there is only one level of source map',
 | 
						|
           () => {
 | 
						|
             const rawSourceMap: RawSourceMap = {
 | 
						|
               mappings: encode([[[0, 0, 0, 0], [6, 0, 0, 3]]]),
 | 
						|
               names: [],
 | 
						|
               sources: ['a.js'],
 | 
						|
               version: 3
 | 
						|
             };
 | 
						|
             const originalSource = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, []);
 | 
						|
             const sourceFile = new SourceFile(
 | 
						|
                 _('/foo/src/index.js'), 'abc123defg', rawSourceMap, false, [originalSource]);
 | 
						|
             expect(sourceFile.flattenedMappings)
 | 
						|
                 .toEqual(parseMappings(rawSourceMap, [originalSource]));
 | 
						|
           });
 | 
						|
 | 
						|
        it('should merge mappings from flattened original source files', () => {
 | 
						|
          const cSource = new SourceFile(_('/foo/src/c.js'), 'bcd123e', null, false, []);
 | 
						|
          const bSourceMap: RawSourceMap = {
 | 
						|
            mappings: encode([[[1, 0, 0, 0], [4, 0, 0, 3], [4, 0, 0, 6], [5, 0, 0, 7]]]),
 | 
						|
            names: [],
 | 
						|
            sources: ['c.js'],
 | 
						|
            version: 3
 | 
						|
          };
 | 
						|
          const bSource =
 | 
						|
              new SourceFile(_('/foo/src/b.js'), 'abcdef', bSourceMap, false, [cSource]);
 | 
						|
          const aSourceMap: RawSourceMap = {
 | 
						|
            mappings: encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5]]]),
 | 
						|
            names: [],
 | 
						|
            sources: ['b.js'],
 | 
						|
            version: 3
 | 
						|
          };
 | 
						|
          const aSource =
 | 
						|
              new SourceFile(_('/foo/src/a.js'), 'abdecf', aSourceMap, false, [bSource]);
 | 
						|
 | 
						|
          expect(aSource.flattenedMappings).toEqual([
 | 
						|
            {
 | 
						|
              generatedSegment: {line: 0, column: 1},
 | 
						|
              originalSource: cSource,
 | 
						|
              originalSegment: {line: 0, column: 0},
 | 
						|
              name: undefined
 | 
						|
            },
 | 
						|
            {
 | 
						|
              generatedSegment: {line: 0, column: 2},
 | 
						|
              originalSource: cSource,
 | 
						|
              originalSegment: {line: 0, column: 2},
 | 
						|
              name: undefined
 | 
						|
            },
 | 
						|
            {
 | 
						|
              generatedSegment: {line: 0, column: 3},
 | 
						|
              originalSource: cSource,
 | 
						|
              originalSegment: {line: 0, column: 3},
 | 
						|
              name: undefined
 | 
						|
            },
 | 
						|
            {
 | 
						|
              generatedSegment: {line: 0, column: 3},
 | 
						|
              originalSource: cSource,
 | 
						|
              originalSegment: {line: 0, column: 6},
 | 
						|
              name: undefined
 | 
						|
            },
 | 
						|
            {
 | 
						|
              generatedSegment: {line: 0, column: 4},
 | 
						|
              originalSource: cSource,
 | 
						|
              originalSegment: {line: 0, column: 1},
 | 
						|
              name: undefined
 | 
						|
            },
 | 
						|
            {
 | 
						|
              generatedSegment: {line: 0, column: 5},
 | 
						|
              originalSource: cSource,
 | 
						|
              originalSegment: {line: 0, column: 7},
 | 
						|
              name: undefined
 | 
						|
            },
 | 
						|
          ]);
 | 
						|
        });
 | 
						|
 | 
						|
        it('should ignore mappings to missing source files', () => {
 | 
						|
          const bSourceMap: RawSourceMap = {
 | 
						|
            mappings: encode([[[1, 0, 0, 0], [4, 0, 0, 3], [4, 0, 0, 6], [5, 0, 0, 7]]]),
 | 
						|
            names: [],
 | 
						|
            sources: ['c.js'],
 | 
						|
            version: 3
 | 
						|
          };
 | 
						|
          const bSource = new SourceFile(_('/foo/src/b.js'), 'abcdef', bSourceMap, false, [null]);
 | 
						|
          const aSourceMap: RawSourceMap = {
 | 
						|
            mappings: encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5]]]),
 | 
						|
            names: [],
 | 
						|
            sources: ['b.js'],
 | 
						|
            version: 3
 | 
						|
          };
 | 
						|
          const aSource =
 | 
						|
              new SourceFile(_('/foo/src/a.js'), 'abdecf', aSourceMap, false, [bSource]);
 | 
						|
 | 
						|
          // These flattened mappings are just the mappings from a to b.
 | 
						|
          // (The mappings to c are dropped since there is no source file to map to.)
 | 
						|
          expect(aSource.flattenedMappings).toEqual(parseMappings(aSourceMap, [bSource]));
 | 
						|
        });
 | 
						|
      });
 | 
						|
 | 
						|
      describe('renderFlattenedSourceMap()', () => {
 | 
						|
        it('should convert the flattenedMappings into a raw source-map object', () => {
 | 
						|
          const cSource = new SourceFile(_('/foo/src/c.js'), 'bcd123e', null, false, []);
 | 
						|
          const bToCSourceMap: RawSourceMap = {
 | 
						|
            mappings: encode([[[1, 0, 0, 0], [4, 0, 0, 3], [4, 0, 0, 6], [5, 0, 0, 7]]]),
 | 
						|
            names: [],
 | 
						|
            sources: ['c.js'],
 | 
						|
            version: 3
 | 
						|
          };
 | 
						|
          const bSource =
 | 
						|
              new SourceFile(_('/foo/src/b.js'), 'abcdef', bToCSourceMap, false, [cSource]);
 | 
						|
          const aToBSourceMap: RawSourceMap = {
 | 
						|
            mappings: encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5]]]),
 | 
						|
            names: [],
 | 
						|
            sources: ['b.js'],
 | 
						|
            version: 3
 | 
						|
          };
 | 
						|
          const aSource =
 | 
						|
              new SourceFile(_('/foo/src/a.js'), 'abdecf', aToBSourceMap, false, [bSource]);
 | 
						|
 | 
						|
          const aTocSourceMap = aSource.renderFlattenedSourceMap();
 | 
						|
          expect(aTocSourceMap.version).toEqual(3);
 | 
						|
          expect(aTocSourceMap.file).toEqual('a.js');
 | 
						|
          expect(aTocSourceMap.names).toEqual([]);
 | 
						|
          expect(aTocSourceMap.sourceRoot).toBeUndefined();
 | 
						|
          expect(aTocSourceMap.sources).toEqual(['c.js']);
 | 
						|
          expect(aTocSourceMap.sourcesContent).toEqual(['bcd123e']);
 | 
						|
          expect(aTocSourceMap.mappings).toEqual(encode([
 | 
						|
            [[1, 0, 0, 0], [2, 0, 0, 2], [3, 0, 0, 3], [3, 0, 0, 6], [4, 0, 0, 1], [5, 0, 0, 7]]
 | 
						|
          ]));
 | 
						|
        });
 | 
						|
 | 
						|
        it('should handle mappings that map from lines outside of the actual content lines', () => {
 | 
						|
          const bSource = new SourceFile(_('/foo/src/b.js'), 'abcdef', null, false, []);
 | 
						|
          const aToBSourceMap: RawSourceMap = {
 | 
						|
            mappings: encode([
 | 
						|
              [[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5]],
 | 
						|
              [
 | 
						|
                [0, 0, 0, 0],  // Extra mapping from a non-existent line
 | 
						|
              ]
 | 
						|
            ]),
 | 
						|
            names: [],
 | 
						|
            sources: ['b.js'],
 | 
						|
            version: 3
 | 
						|
          };
 | 
						|
          const aSource =
 | 
						|
              new SourceFile(_('/foo/src/a.js'), 'abdecf', aToBSourceMap, false, [bSource]);
 | 
						|
 | 
						|
          const aTocSourceMap = aSource.renderFlattenedSourceMap();
 | 
						|
          expect(aTocSourceMap.version).toEqual(3);
 | 
						|
          expect(aTocSourceMap.file).toEqual('a.js');
 | 
						|
          expect(aTocSourceMap.names).toEqual([]);
 | 
						|
          expect(aTocSourceMap.sourceRoot).toBeUndefined();
 | 
						|
          expect(aTocSourceMap.sources).toEqual(['b.js']);
 | 
						|
          expect(aTocSourceMap.sourcesContent).toEqual(['abcdef']);
 | 
						|
          expect(aTocSourceMap.mappings).toEqual(aToBSourceMap.mappings);
 | 
						|
        });
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    describe('computeLineLengths()', () => {
 | 
						|
      it('should compute the length of each line in the given string', () => {
 | 
						|
        expect(computeLineLengths('')).toEqual([0]);
 | 
						|
        expect(computeLineLengths('abc')).toEqual([3]);
 | 
						|
        expect(computeLineLengths('\n')).toEqual([0, 0]);
 | 
						|
        expect(computeLineLengths('\n\n')).toEqual([0, 0, 0]);
 | 
						|
        expect(computeLineLengths('abc\n')).toEqual([3, 0]);
 | 
						|
        expect(computeLineLengths('\nabc')).toEqual([0, 3]);
 | 
						|
        expect(computeLineLengths('abc\ndefg')).toEqual([3, 4]);
 | 
						|
        expect(computeLineLengths('abc\r\n')).toEqual([3, 0]);
 | 
						|
        expect(computeLineLengths('abc\r\ndefg')).toEqual([3, 4]);
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
});
 |