feat(ngcc): implement source-map flattening (#35132)
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
This commit is contained in:

committed by
Miško Hevery

parent
2a8dd4758c
commit
df816c9c80
@ -20,7 +20,7 @@ import {EntryPointBundle} from '../packages/entry_point_bundle';
|
||||
import {Logger} from '../logging/logger';
|
||||
import {FileToWrite, getImportRewriter} from './utils';
|
||||
import {RenderingFormatter} from './rendering_formatter';
|
||||
import {extractSourceMap, renderSourceAndMap} from './source_maps';
|
||||
import {renderSourceAndMap} from './source_maps';
|
||||
|
||||
/**
|
||||
* A structure that captures information about what needs to be rendered
|
||||
@ -81,8 +81,7 @@ export class DtsRenderer {
|
||||
}
|
||||
|
||||
renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileToWrite[] {
|
||||
const input = extractSourceMap(this.fs, this.logger, dtsFile);
|
||||
const outputText = new MagicString(input.source);
|
||||
const outputText = new MagicString(dtsFile.text);
|
||||
const printer = ts.createPrinter();
|
||||
const importManager = new ImportManager(
|
||||
getImportRewriter(this.bundle.dts !.r3SymbolsFile, this.bundle.isCore, false),
|
||||
@ -112,7 +111,7 @@ export class DtsRenderer {
|
||||
this.dtsFormatter.addImports(
|
||||
outputText, importManager.getAllImports(dtsFile.fileName), dtsFile);
|
||||
|
||||
return renderSourceAndMap(dtsFile, input, outputText);
|
||||
return renderSourceAndMap(this.fs, dtsFile, outputText);
|
||||
}
|
||||
|
||||
private getTypingsFilesToRender(
|
||||
|
@ -18,7 +18,7 @@ import {NgccReflectionHost} from '../host/ngcc_host';
|
||||
import {Logger} from '../logging/logger';
|
||||
import {EntryPointBundle} from '../packages/entry_point_bundle';
|
||||
import {RenderingFormatter, RedundantDecoratorMap} from './rendering_formatter';
|
||||
import {extractSourceMap, renderSourceAndMap} from './source_maps';
|
||||
import {renderSourceAndMap} from './source_maps';
|
||||
import {FileToWrite, getImportRewriter, stripExtension} from './utils';
|
||||
|
||||
/**
|
||||
@ -61,8 +61,7 @@ export class Renderer {
|
||||
switchMarkerAnalysis: SwitchMarkerAnalysis|undefined,
|
||||
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileToWrite[] {
|
||||
const isEntryPoint = sourceFile === this.bundle.src.file;
|
||||
const input = extractSourceMap(this.fs, this.logger, sourceFile);
|
||||
const outputText = new MagicString(input.source);
|
||||
const outputText = new MagicString(sourceFile.text);
|
||||
|
||||
if (switchMarkerAnalysis) {
|
||||
this.srcFormatter.rewriteSwitchableDeclarations(
|
||||
@ -115,7 +114,7 @@ export class Renderer {
|
||||
}
|
||||
|
||||
if (compiledFile || switchMarkerAnalysis || isEntryPoint) {
|
||||
return renderSourceAndMap(sourceFile, input, outputText);
|
||||
return renderSourceAndMap(this.fs, sourceFile, outputText);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
@ -5,13 +5,13 @@
|
||||
* 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 {SourceMapConverter, commentRegex, fromJSON, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map';
|
||||
import {SourceMapConverter, fromObject, generateMapFileComment} from 'convert-source-map';
|
||||
import MagicString from 'magic-string';
|
||||
import {RawSourceMap, SourceMapConsumer, SourceMapGenerator} from 'source-map';
|
||||
import * as ts from 'typescript';
|
||||
import {resolve, FileSystem, absoluteFromSourceFile, dirname, basename, absoluteFrom} from '../../../src/ngtsc/file_system';
|
||||
import {Logger} from '../logging/logger';
|
||||
import {FileSystem, absoluteFromSourceFile, basename, absoluteFrom} from '../../../src/ngtsc/file_system';
|
||||
import {FileToWrite} from './utils';
|
||||
import {SourceFileLoader} from '../sourcemaps/source_file_loader';
|
||||
import {RawSourceMap} from '../sourcemaps/raw_source_map';
|
||||
|
||||
export interface SourceMapInfo {
|
||||
source: string;
|
||||
@ -19,117 +19,33 @@ export interface SourceMapInfo {
|
||||
isInline: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the map from the source (note whether it is inline or external)
|
||||
*/
|
||||
export function extractSourceMap(
|
||||
fs: FileSystem, logger: Logger, file: ts.SourceFile): SourceMapInfo {
|
||||
const inline = commentRegex.test(file.text);
|
||||
const external = mapFileCommentRegex.exec(file.text);
|
||||
|
||||
if (inline) {
|
||||
const inlineSourceMap = fromSource(file.text);
|
||||
return {
|
||||
source: removeComments(file.text).replace(/\n\n$/, '\n'),
|
||||
map: inlineSourceMap,
|
||||
isInline: true,
|
||||
};
|
||||
} else if (external) {
|
||||
let externalSourceMap: SourceMapConverter|null = null;
|
||||
try {
|
||||
const fileName = external[1] || external[2];
|
||||
const filePath = resolve(dirname(absoluteFromSourceFile(file)), fileName);
|
||||
const mappingFile = fs.readFile(filePath);
|
||||
externalSourceMap = fromJSON(mappingFile);
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
logger.warn(
|
||||
`The external map file specified in the source code comment "${e.path}" was not found on the file system.`);
|
||||
const mapPath = absoluteFrom(file.fileName + '.map');
|
||||
if (basename(e.path) !== basename(mapPath) && fs.exists(mapPath) &&
|
||||
fs.stat(mapPath).isFile()) {
|
||||
logger.warn(
|
||||
`Guessing the map file name from the source file name: "${basename(mapPath)}"`);
|
||||
try {
|
||||
externalSourceMap = fromObject(JSON.parse(fs.readFile(mapPath)));
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
source: removeMapFileComments(file.text).replace(/\n\n$/, '\n'),
|
||||
map: externalSourceMap,
|
||||
isInline: false,
|
||||
};
|
||||
} else {
|
||||
return {source: file.text, map: null, isInline: false};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the input and output source-maps, replacing the source-map comment in the output file
|
||||
* with an appropriate source-map comment pointing to the merged source-map.
|
||||
*/
|
||||
export function renderSourceAndMap(
|
||||
sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileToWrite[] {
|
||||
const outputPath = absoluteFromSourceFile(sourceFile);
|
||||
const outputMapPath = absoluteFrom(`${outputPath}.map`);
|
||||
const relativeSourcePath = basename(outputPath);
|
||||
const relativeMapPath = `${relativeSourcePath}.map`;
|
||||
fs: FileSystem, sourceFile: ts.SourceFile, generatedMagicString: MagicString): FileToWrite[] {
|
||||
const generatedPath = absoluteFromSourceFile(sourceFile);
|
||||
const generatedMapPath = absoluteFrom(`${generatedPath}.map`);
|
||||
const generatedContent = generatedMagicString.toString();
|
||||
const generatedMap: RawSourceMap = generatedMagicString.generateMap(
|
||||
{file: generatedPath, source: generatedPath, includeContent: true});
|
||||
|
||||
const outputMap = output.generateMap({
|
||||
source: outputPath,
|
||||
includeContent: true,
|
||||
// hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix
|
||||
// the merge algorithm.
|
||||
});
|
||||
const loader = new SourceFileLoader(fs);
|
||||
const generatedFile = loader.loadSourceFile(
|
||||
generatedPath, generatedContent, {map: generatedMap, mapPath: generatedMapPath});
|
||||
|
||||
// we must set this after generation as magic string does "manipulation" on the path
|
||||
outputMap.file = relativeSourcePath;
|
||||
const rawMergedMap: RawSourceMap = generatedFile.renderFlattenedSourceMap();
|
||||
const mergedMap = fromObject(rawMergedMap);
|
||||
|
||||
const mergedMap =
|
||||
mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString()));
|
||||
|
||||
const result: FileToWrite[] = [];
|
||||
if (input.isInline) {
|
||||
result.push({path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`});
|
||||
if (generatedFile.sources[0]?.inline) {
|
||||
// The input source-map was inline so make the output one inline too.
|
||||
return [{path: generatedPath, contents: `${generatedFile.contents}\n${mergedMap.toComment()}`}];
|
||||
} else {
|
||||
result.push({
|
||||
path: outputPath,
|
||||
contents: `${output.toString()}\n${generateMapFileComment(relativeMapPath)}`
|
||||
});
|
||||
result.push({path: outputMapPath, contents: mergedMap.toJSON()});
|
||||
const sourceMapComment = generateMapFileComment(`${basename(generatedPath)}.map`);
|
||||
return [
|
||||
{path: generatedPath, contents: `${generatedFile.contents}\n${sourceMapComment}`},
|
||||
{path: generatedMapPath, contents: mergedMap.toJSON()}
|
||||
];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Merge the two specified source-maps into a single source-map that hides the intermediate
|
||||
* source-map.
|
||||
* E.g. Consider these mappings:
|
||||
*
|
||||
* ```
|
||||
* OLD_SRC -> OLD_MAP -> INTERMEDIATE_SRC -> NEW_MAP -> NEW_SRC
|
||||
* ```
|
||||
*
|
||||
* this will be replaced with:
|
||||
*
|
||||
* ```
|
||||
* OLD_SRC -> MERGED_MAP -> NEW_SRC
|
||||
* ```
|
||||
*/
|
||||
export function mergeSourceMaps(
|
||||
oldMap: RawSourceMap | null, newMap: RawSourceMap): SourceMapConverter {
|
||||
if (!oldMap) {
|
||||
return fromObject(newMap);
|
||||
}
|
||||
const oldMapConsumer = new SourceMapConsumer(oldMap);
|
||||
const newMapConsumer = new SourceMapConsumer(newMap);
|
||||
const mergedMapGenerator = SourceMapGenerator.fromSourceMap(newMapConsumer);
|
||||
mergedMapGenerator.applySourceMap(oldMapConsumer);
|
||||
const merged = fromJSON(mergedMapGenerator.toString());
|
||||
return merged;
|
||||
}
|
||||
|
Reference in New Issue
Block a user