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
This commit is contained in:
Pete Bacon Darwin
2019-02-08 22:10:21 +00:00
committed by Misko Hevery
parent cffd86260a
commit 08de52b9f0
7 changed files with 272 additions and 33 deletions

View File

@ -18,6 +18,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/routing",
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/util",
"@ngdeps//@types/node",
"@ngdeps//typescript",
],

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, ElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, R3ComponentMetadata, R3DirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, LexerRange, R3ComponentMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
@ -16,7 +16,8 @@ import {ModuleResolver, Reference, ResolvedReference} from '../../imports';
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {TypeCheckContext, TypeCheckableDirectiveMeta} from '../../typecheck';
import {TypeCheckContext} from '../../typecheck';
import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
import {ResourceLoader} from './api';
import {extractDirectiveMetadata, extractQueriesFromDecorator, parseFieldArrayValue, queriesFromFields} from './directive';
@ -119,24 +120,56 @@ export class ComponentDecoratorHandler implements
// Next, read the `@Component`-specific fields.
const {decoratedElements, decorator: component, metadata} = directiveResult;
// Go through the root directories for this project, and select the one with the smallest
// relative path representation.
const filePath = node.getSourceFile().fileName;
const relativeContextFilePath = this.rootDirs.reduce<string|undefined>((previous, rootDir) => {
const candidate = path.posix.relative(rootDir, filePath);
if (previous === undefined || candidate.length < previous.length) {
return candidate;
} else {
return previous;
}
}, undefined) !;
let templateStr: string|null = null;
let templateUrl: string = '';
let templateRange: LexerRange|undefined;
let escapedString: boolean = false;
if (component.has('templateUrl')) {
const templateUrlExpr = component.get('templateUrl') !;
const templateUrl = this.evaluator.evaluate(templateUrlExpr);
if (typeof templateUrl !== 'string') {
const evalTemplateUrl = this.evaluator.evaluate(templateUrlExpr);
if (typeof evalTemplateUrl !== 'string') {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string');
}
const resolvedTemplateUrl = this.resourceLoader.resolve(templateUrl, containingFile);
templateStr = this.resourceLoader.load(resolvedTemplateUrl);
templateUrl = this.resourceLoader.resolve(evalTemplateUrl, containingFile);
templateStr = this.resourceLoader.load(templateUrl);
if (!tsSourceMapBug29300Fixed()) {
// By removing the template URL we are telling the translator not to try to
// map the external source file to the generated code, since the version
// of TS that is running does not support it.
templateUrl = '';
}
} else if (component.has('template')) {
const templateExpr = component.get('template') !;
const resolvedTemplate = this.evaluator.evaluate(templateExpr);
if (typeof resolvedTemplate !== 'string') {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateExpr, 'template must be a string');
// We only support SourceMaps for inline templates that are simple string literals.
if (ts.isStringLiteral(templateExpr) || ts.isNoSubstitutionTemplateLiteral(templateExpr)) {
// the start and end of the `templateExpr` node includes the quotation marks, which we must
// strip
templateRange = getTemplateRange(templateExpr);
templateStr = templateExpr.getSourceFile().text;
templateUrl = relativeContextFilePath;
escapedString = true;
} else {
const resolvedTemplate = this.evaluator.evaluate(templateExpr);
if (typeof resolvedTemplate !== 'string') {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateExpr, 'template must be a string');
}
templateStr = resolvedTemplate;
}
templateStr = resolvedTemplate;
} else {
throw new FatalDiagnosticError(
ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node, 'component is missing a template');
@ -157,18 +190,6 @@ export class ComponentDecoratorHandler implements
new WrappedNodeExpr(component.get('viewProviders') !) :
null;
// Go through the root directories for this project, and select the one with the smallest
// relative path representation.
const filePath = node.getSourceFile().fileName;
const relativeContextFilePath = this.rootDirs.reduce<string|undefined>((previous, rootDir) => {
const candidate = path.posix.relative(rootDir, filePath);
if (previous === undefined || candidate.length < previous.length) {
return candidate;
} else {
return previous;
}
}, undefined) !;
let interpolation: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG;
if (component.has('interpolation')) {
const expr = component.get('interpolation') !;
@ -182,9 +203,11 @@ export class ComponentDecoratorHandler implements
interpolation = InterpolationConfig.fromArray(value as[string, string]);
}
const template = parseTemplate(
templateStr, `${node.getSourceFile().fileName}#${node.name!.text}/template.html`,
{preserveWhitespaces, interpolationConfig: interpolation});
const template = parseTemplate(templateStr, templateUrl, {
preserveWhitespaces,
interpolationConfig: interpolation,
range: templateRange, escapedString
});
if (template.errors !== undefined) {
throw new Error(
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
@ -402,3 +425,15 @@ export class ComponentDecoratorHandler implements
return this.cycleAnalyzer.wouldCreateCycle(origin, imported);
}
}
function getTemplateRange(templateExpr: ts.Expression) {
const startPos = templateExpr.getStart() + 1;
const {line, character} =
ts.getLineAndCharacterOfPosition(templateExpr.getSourceFile(), startPos);
return {
startPos,
startLine: line,
startCol: character,
endPos: templateExpr.getEnd() - 1,
};
}