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
104 lines
3.4 KiB
TypeScript
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;
|
|
}
|