Skip to content

Commit cd252b9

Browse files
committed
fix(compiler-cli): use '' for the source map URL of indirect templates (angular#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 angular#40854 PR Close angular#41973
1 parent 2843f15 commit cd252b9

File tree

2 files changed

+47
-9
lines changed

2 files changed

+47
-9
lines changed

packages/compiler-cli/src/ngtsc/annotations/src/component.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,7 @@ export class ComponentDecoratorHandler implements
10691069
let templateContent: string;
10701070
let sourceMapping: TemplateSourceMapping;
10711071
let escapedString = false;
1072+
let sourceMapUrl: string|null;
10721073
// We only support SourceMaps for inline templates that are simple string literals.
10731074
if (ts.isStringLiteral(template.expression) ||
10741075
ts.isNoSubstitutionTemplateLiteral(template.expression)) {
@@ -1082,6 +1083,7 @@ export class ComponentDecoratorHandler implements
10821083
type: 'direct',
10831084
node: template.expression,
10841085
};
1086+
sourceMapUrl = template.potentialSourceMapUrl;
10851087
} else {
10861088
const resolvedTemplate = this.evaluator.evaluate(template.expression);
10871089
if (typeof resolvedTemplate !== 'string') {
@@ -1098,10 +1100,15 @@ export class ComponentDecoratorHandler implements
10981100
componentClass: node,
10991101
template: templateContent,
11001102
};
1103+
1104+
// Indirect templates cannot be mapped to a particular byte range of any input file, since
1105+
// they're computed by expressions that may span many files. Don't attempt to map them back
1106+
// to a given file.
1107+
sourceMapUrl = null;
11011108
}
11021109

11031110
return {
1104-
...this._parseTemplate(template, sourceStr, sourceParseRange, escapedString),
1111+
...this._parseTemplate(template, sourceStr, sourceParseRange, escapedString, sourceMapUrl),
11051112
content: templateContent,
11061113
sourceMapping,
11071114
declaration: template,
@@ -1116,7 +1123,8 @@ export class ComponentDecoratorHandler implements
11161123
return {
11171124
...this._parseTemplate(
11181125
template, /* sourceStr */ templateContent, /* sourceParseRange */ null,
1119-
/* escapedString */ false),
1126+
/* escapedString */ false,
1127+
/* sourceMapUrl */ template.potentialSourceMapUrl),
11201128
content: templateContent,
11211129
sourceMapping: {
11221130
type: 'external',
@@ -1134,11 +1142,11 @@ export class ComponentDecoratorHandler implements
11341142

11351143
private _parseTemplate(
11361144
template: TemplateDeclaration, sourceStr: string, sourceParseRange: LexerRange|null,
1137-
escapedString: boolean): ParsedComponentTemplate {
1145+
escapedString: boolean, sourceMapUrl: string|null): ParsedComponentTemplate {
11381146
// We always normalize line endings if the template has been escaped (i.e. is inline).
11391147
const i18nNormalizeLineEndingsInICUs = escapedString || this.i18nNormalizeLineEndingsInICUs;
11401148

1141-
const parsedTemplate = parseTemplate(sourceStr, template.sourceMapUrl, {
1149+
const parsedTemplate = parseTemplate(sourceStr, sourceMapUrl ?? '', {
11421150
preserveWhitespaces: template.preserveWhitespaces,
11431151
interpolationConfig: template.interpolationConfig,
11441152
range: sourceParseRange ?? undefined,
@@ -1163,7 +1171,7 @@ export class ComponentDecoratorHandler implements
11631171
// In order to guarantee the correctness of diagnostics, templates are parsed a second time
11641172
// with the above options set to preserve source mappings.
11651173

1166-
const {nodes: diagNodes} = parseTemplate(sourceStr, template.sourceMapUrl, {
1174+
const {nodes: diagNodes} = parseTemplate(sourceStr, sourceMapUrl ?? '', {
11671175
preserveWhitespaces: true,
11681176
preserveLineEndings: true,
11691177
interpolationConfig: template.interpolationConfig,
@@ -1178,7 +1186,7 @@ export class ComponentDecoratorHandler implements
11781186
return {
11791187
...parsedTemplate,
11801188
diagNodes,
1181-
file: new ParseSourceFile(sourceStr, template.resolvedTemplateUrl),
1189+
file: new ParseSourceFile(sourceStr, sourceMapUrl ?? ''),
11821190
};
11831191
}
11841192

@@ -1223,7 +1231,7 @@ export class ComponentDecoratorHandler implements
12231231
templateUrl,
12241232
templateUrlExpression: templateUrlExpr,
12251233
resolvedTemplateUrl: resourceUrl,
1226-
sourceMapUrl: sourceMapUrl(resourceUrl),
1234+
potentialSourceMapUrl: sourceMapUrl(resourceUrl),
12271235
};
12281236
} catch (e) {
12291237
throw this.makeResourceNotFoundError(
@@ -1237,7 +1245,7 @@ export class ComponentDecoratorHandler implements
12371245
expression: component.get('template')!,
12381246
templateUrl: containingFile,
12391247
resolvedTemplateUrl: containingFile,
1240-
sourceMapUrl: containingFile,
1248+
potentialSourceMapUrl: containingFile,
12411249
};
12421250
} else {
12431251
throw new FatalDiagnosticError(
@@ -1398,7 +1406,7 @@ interface CommonTemplateDeclaration {
13981406
interpolationConfig: InterpolationConfig;
13991407
templateUrl: string;
14001408
resolvedTemplateUrl: string;
1401-
sourceMapUrl: string;
1409+
potentialSourceMapUrl: string;
14021410
}
14031411

14041412
/**

packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,36 @@ runInEachFileSystem(() => {
229229
expect(analysis?.resources.styles.size).toBe(3);
230230
});
231231

232+
it('should use an empty source map URL for an indirect template', () => {
233+
const template = '<span>indirect</span>';
234+
const {program, options, host} = makeProgram([
235+
{
236+
name: _('/node_modules/@angular/core/index.d.ts'),
237+
contents: 'export const Component: any;',
238+
},
239+
{
240+
name: _('/entry.ts'),
241+
contents: `
242+
import {Component} from '@angular/core';
243+
244+
const TEMPLATE = '${template}';
245+
246+
@Component({
247+
template: TEMPLATE,
248+
}) class TestCmp {}
249+
`
250+
},
251+
]);
252+
const {reflectionHost, handler} = setup(program, options, host);
253+
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
254+
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
255+
if (detected === undefined) {
256+
return fail('Failed to recognize @Component');
257+
}
258+
const {analysis} = handler.analyze(TestCmp, detected.metadata);
259+
expect(analysis?.template.file?.url).toEqual('');
260+
});
261+
232262
it('does not emit a program with template parse errors', () => {
233263
const template = '{{x ? y }}';
234264
const {program, options, host} = makeProgram([

0 commit comments

Comments
 (0)