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:
parent
2843f15e8c
commit
cd252b99fe
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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([
|
||||
|
|
Loading…
Reference in New Issue