Pete Bacon Darwin 08de52b9f0 feat(ivy): add source mappings to compiled Angular templates (#28055)
During analysis, the `ComponentDecoratorHandler` passes the component
template to the `parseTemplate()` function. Previously, there was little or
no information about the original source file, where the template is found,
passed when calling this function.

Now, we correctly compute the URL of the source of the template, both
for external `templateUrl` and in-line `template` cases. Further in the
in-line template case we compute the character range of the template
in its containing source file; *but only in the case that the template is
a simple string literal*. If the template is actually a dynamic value like
an interpolated string or a function call, then we do not try to add the
originating source file information.

The translator that converts Ivy AST nodes to TypeScript now adds these
template specific source mappings, which account for the file where
the template was found, to the templates to support stepping through the
template creation and update code when debugging an Angular application.

Note that some versions of TypeScript have a bug which means they cannot
support external template source-maps. We check for this via the
`canSourceMapExternalTemplates()` helper function and avoid trying to
add template mappings to external templates if not supported.

PR Close #28055
2019-02-12 20:58:28 -08:00

104 lines
3.4 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 {MappingItem, SourceMapConsumer} from 'source-map';
import {NgtscTestEnvironment} from './env';
class TestSourceFile {
private lineStarts: number[];
constructor(public url: string, public contents: string) {
this.lineStarts = this.getLineStarts();
}
getSegment(key: 'generated'|'original', start: MappingItem|any, end: MappingItem|any): string {
const startLine = start[key + 'Line'];
const startCol = start[key + 'Column'];
const endLine = end[key + 'Line'];
const endCol = end[key + 'Column'];
return this.contents.substring(
this.lineStarts[startLine - 1] + startCol, this.lineStarts[endLine - 1] + endCol);
}
getSourceMapFileName(generatedContents: string): string {
const match = /\/\/# sourceMappingURL=(.+)/.exec(generatedContents);
if (!match) {
throw new Error('Generated contents does not contain a sourceMappingURL');
}
return match[1];
}
private getLineStarts(): number[] {
const lineStarts = [0];
let currentPos = 0;
const lines = this.contents.split('\n');
lines.forEach(line => {
currentPos += line.length + 1;
lineStarts.push(currentPos);
});
return lineStarts;
}
}
/**
* A mapping of a segment of generated text to a segment of source text.
*/
export interface SegmentMapping {
/** The generated text in this segment. */
generated: string;
/** The source text in this segment. */
source: string;
/** The URL of the source file for this segment. */
sourceUrl: string;
}
/**
* Process a generated file to extract human understandable segment mappings.
* These mappings are easier to compare in unit tests that the raw SourceMap mappings.
* @param env the environment that holds the source and generated files.
* @param generatedFileName The name of the generated file to process.
* @returns An array of segment mappings for each mapped segment in the given generated file.
*/
export function getMappedSegments(
env: NgtscTestEnvironment, generatedFileName: string): SegmentMapping[] {
const generated = new TestSourceFile(generatedFileName, env.getContents(generatedFileName));
const sourceMapFileName = generated.getSourceMapFileName(generated.contents);
const sources = new Map<string, TestSourceFile>();
const mappings: MappingItem[] = [];
const mapContents = env.getContents(sourceMapFileName);
const sourceMapConsumer = new SourceMapConsumer(JSON.parse(mapContents));
sourceMapConsumer.eachMapping(item => {
if (!sources.has(item.source)) {
sources.set(item.source, new TestSourceFile(item.source, env.getContents(item.source)));
}
mappings.push(item);
});
const segments: SegmentMapping[] = [];
let currentMapping = mappings.shift();
while (currentMapping) {
const nextMapping = mappings.shift();
if (nextMapping) {
const source = sources.get(currentMapping.source) !;
const segment = {
generated: generated.getSegment('generated', currentMapping, nextMapping),
source: source.getSegment('original', currentMapping, nextMapping),
sourceUrl: source.url
};
if (segment.generated !== segment.source) {
segments.push(segment);
}
}
currentMapping = nextMapping;
}
return segments;
}