From cd252b99fe04bcde2a31e2314f9eb920b2d38e49 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Thu, 6 May 2021 17:40:53 -0400 Subject: [PATCH] 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 --- .../src/ngtsc/annotations/src/component.ts | 26 ++++++++++------ .../ngtsc/annotations/test/component_spec.ts | 30 +++++++++++++++++++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 9e00e0cc926..2deec129a9e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -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; } /** diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts index d94519770e4..0422821be1c 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts @@ -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 = 'indirect'; + 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([