fix(compiler-cli): use '' for the source map URL of indirect templates (#41973)

Indirect templates are templates produced by a non-literal expression value
of the `template` field in `@Component`. The compiler can statically
determine the template string, but there is not guaranteed to be a physical
file which contains the bytes of the template string. For example, the
template string may be computed by a concatenation expression: 'a' + 'b'.

Previously, the compiler would use the TS file path as the source map path
for indirect templates. This is incorrect, however, and breaks source
mapping for such templates, since the offsets within the template string do
not correspond to bytes of the TS file.

This commit returns the compiler to its old behavior for indirect templates,
which is to use `''` as the source map URL for such templates.

Fixes #40854

PR Close #41973
This commit is contained in:
Alex Rickabaugh 2021-05-06 17:40:53 -04:00
parent 2843f15e8c
commit cd252b99fe
2 changed files with 47 additions and 9 deletions

View File

@ -1069,6 +1069,7 @@ export class ComponentDecoratorHandler implements
let templateContent: string;
let sourceMapping: TemplateSourceMapping;
let escapedString = false;
let sourceMapUrl: string|null;
// We only support SourceMaps for inline templates that are simple string literals.
if (ts.isStringLiteral(template.expression) ||
ts.isNoSubstitutionTemplateLiteral(template.expression)) {
@ -1082,6 +1083,7 @@ export class ComponentDecoratorHandler implements
type: 'direct',
node: template.expression,
};
sourceMapUrl = template.potentialSourceMapUrl;
} else {
const resolvedTemplate = this.evaluator.evaluate(template.expression);
if (typeof resolvedTemplate !== 'string') {
@ -1098,10 +1100,15 @@ export class ComponentDecoratorHandler implements
componentClass: node,
template: templateContent,
};
// Indirect templates cannot be mapped to a particular byte range of any input file, since
// they're computed by expressions that may span many files. Don't attempt to map them back
// to a given file.
sourceMapUrl = null;
}
return {
...this._parseTemplate(template, sourceStr, sourceParseRange, escapedString),
...this._parseTemplate(template, sourceStr, sourceParseRange, escapedString, sourceMapUrl),
content: templateContent,
sourceMapping,
declaration: template,
@ -1116,7 +1123,8 @@ export class ComponentDecoratorHandler implements
return {
...this._parseTemplate(
template, /* sourceStr */ templateContent, /* sourceParseRange */ null,
/* escapedString */ false),
/* escapedString */ false,
/* sourceMapUrl */ template.potentialSourceMapUrl),
content: templateContent,
sourceMapping: {
type: 'external',
@ -1134,11 +1142,11 @@ export class ComponentDecoratorHandler implements
private _parseTemplate(
template: TemplateDeclaration, sourceStr: string, sourceParseRange: LexerRange|null,
escapedString: boolean): ParsedComponentTemplate {
escapedString: boolean, sourceMapUrl: string|null): ParsedComponentTemplate {
// We always normalize line endings if the template has been escaped (i.e. is inline).
const i18nNormalizeLineEndingsInICUs = escapedString || this.i18nNormalizeLineEndingsInICUs;
const parsedTemplate = parseTemplate(sourceStr, template.sourceMapUrl, {
const parsedTemplate = parseTemplate(sourceStr, sourceMapUrl ?? '', {
preserveWhitespaces: template.preserveWhitespaces,
interpolationConfig: template.interpolationConfig,
range: sourceParseRange ?? undefined,
@ -1163,7 +1171,7 @@ export class ComponentDecoratorHandler implements
// In order to guarantee the correctness of diagnostics, templates are parsed a second time
// with the above options set to preserve source mappings.
const {nodes: diagNodes} = parseTemplate(sourceStr, template.sourceMapUrl, {
const {nodes: diagNodes} = parseTemplate(sourceStr, sourceMapUrl ?? '', {
preserveWhitespaces: true,
preserveLineEndings: true,
interpolationConfig: template.interpolationConfig,
@ -1178,7 +1186,7 @@ export class ComponentDecoratorHandler implements
return {
...parsedTemplate,
diagNodes,
file: new ParseSourceFile(sourceStr, template.resolvedTemplateUrl),
file: new ParseSourceFile(sourceStr, sourceMapUrl ?? ''),
};
}
@ -1223,7 +1231,7 @@ export class ComponentDecoratorHandler implements
templateUrl,
templateUrlExpression: templateUrlExpr,
resolvedTemplateUrl: resourceUrl,
sourceMapUrl: sourceMapUrl(resourceUrl),
potentialSourceMapUrl: sourceMapUrl(resourceUrl),
};
} catch (e) {
throw this.makeResourceNotFoundError(
@ -1237,7 +1245,7 @@ export class ComponentDecoratorHandler implements
expression: component.get('template')!,
templateUrl: containingFile,
resolvedTemplateUrl: containingFile,
sourceMapUrl: containingFile,
potentialSourceMapUrl: containingFile,
};
} else {
throw new FatalDiagnosticError(
@ -1398,7 +1406,7 @@ interface CommonTemplateDeclaration {
interpolationConfig: InterpolationConfig;
templateUrl: string;
resolvedTemplateUrl: string;
sourceMapUrl: string;
potentialSourceMapUrl: string;
}
/**

View File

@ -229,6 +229,36 @@ runInEachFileSystem(() => {
expect(analysis?.resources.styles.size).toBe(3);
});
it('should use an empty source map URL for an indirect template', () => {
const template = '<span>indirect</span>';
const {program, options, host} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Component: any;',
},
{
name: _('/entry.ts'),
contents: `
import {Component} from '@angular/core';
const TEMPLATE = '${template}';
@Component({
template: TEMPLATE,
}) class TestCmp {}
`
},
]);
const {reflectionHost, handler} = setup(program, options, host);
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) {
return fail('Failed to recognize @Component');
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
expect(analysis?.template.file?.url).toEqual('');
});
it('does not emit a program with template parse errors', () => {
const template = '{{x ? y }}';
const {program, options, host} = makeProgram([