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([