diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 7c255a4050..07225e8546 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -262,7 +262,7 @@ export class ComponentDecoratorHandler implements } } const templateResource = template.isInline ? - null : + {path: null, expression: component.get('template')!} : {path: absoluteFrom(template.templateUrl), expression: template.sourceMapping.node}; let diagnostics: ts.Diagnostic[]|undefined = undefined; @@ -666,21 +666,30 @@ export class ComponentDecoratorHandler implements private _extractStyleResources(component: Map, containingFile: string): ReadonlySet { - const styleUrlsExpr = component.get('styleUrls'); - // If styleUrls is a literal array of strings, process each resource url individually and - // register them. Otherwise, give up and return an empty set. - if (styleUrlsExpr === undefined || !ts.isArrayLiteralExpression(styleUrlsExpr) || - !styleUrlsExpr.elements.every(e => ts.isStringLiteralLike(e))) { - return new Set(); + const styles = new Set(); + function stringLiteralElements(array: ts.ArrayLiteralExpression): ts.StringLiteralLike[] { + return array.elements.filter( + (e: ts.Expression): e is ts.StringLiteralLike => ts.isStringLiteralLike(e)); } - const externalStyles = new Set(); - for (const expression of styleUrlsExpr.elements) { - const resourceUrl = - this.resourceLoader.resolve((expression as ts.StringLiteralLike).text, containingFile); - externalStyles.add({path: absoluteFrom(resourceUrl), expression}); + // If styleUrls is a literal array, process each resource url individually and + // register ones that are string literals. + const styleUrlsExpr = component.get('styleUrls'); + if (styleUrlsExpr !== undefined && ts.isArrayLiteralExpression(styleUrlsExpr)) { + for (const expression of stringLiteralElements(styleUrlsExpr)) { + const resourceUrl = this.resourceLoader.resolve(expression.text, containingFile); + styles.add({path: absoluteFrom(resourceUrl), expression}); + } } - return externalStyles; + + const stylesExpr = component.get('styles'); + if (stylesExpr !== undefined && ts.isArrayLiteralExpression(stylesExpr)) { + for (const expression of stringLiteralElements(stylesExpr)) { + styles.add({path: null, expression}); + } + } + + return styles; } private _preloadAndParseTemplate( 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 395ff0f14c..903745ba1c 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts @@ -18,18 +18,49 @@ import {getDeclaration, makeProgram} from '../../testing'; import {ResourceLoader} from '../src/api'; import {ComponentDecoratorHandler} from '../src/component'; -export class NoopResourceLoader implements ResourceLoader { - resolve(): string { - throw new Error('Not implemented.'); +export class StubResourceLoader implements ResourceLoader { + resolve(v: string): string { + return v; } canPreload = false; - load(): string { - throw new Error('Not implemented'); + load(v: string): string { + return ''; } preload(): Promise|undefined { throw new Error('Not implemented'); } } + +function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.CompilerHost) { + const checker = program.getTypeChecker(); + const reflectionHost = new TypeScriptReflectionHost(checker); + const evaluator = new PartialEvaluator(reflectionHost, checker, /* dependencyTracker */ null); + const moduleResolver = + new ModuleResolver(program, options, host, /* moduleResolutionCache */ null); + const importGraph = new ImportGraph(moduleResolver); + const cycleAnalyzer = new CycleAnalyzer(importGraph); + const metaRegistry = new LocalMetadataRegistry(); + const dtsReader = new DtsMetadataReader(checker, reflectionHost); + const scopeRegistry = new LocalModuleScopeRegistry( + metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]), + null); + const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]); + const refEmitter = new ReferenceEmitter([]); + const injectableRegistry = new InjectableClassRegistry(reflectionHost); + const resourceRegistry = new ResourceRegistry(); + + const handler = new ComponentDecoratorHandler( + reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry, + resourceRegistry, + /* isCore */ false, new StubResourceLoader(), /* rootDirs */['/'], + /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true, + /* enableI18nLegacyMessageIdFormat */ false, + /* i18nNormalizeLineEndingsInICUs */ undefined, moduleResolver, cycleAnalyzer, refEmitter, + NOOP_DEFAULT_IMPORT_RECORDER, /* depTracker */ null, injectableRegistry, + /* annotateForClosureCompiler */ false); + return {reflectionHost, handler}; +} + runInEachFileSystem(() => { describe('ComponentDecoratorHandler', () => { let _: typeof absoluteFrom; @@ -51,31 +82,7 @@ runInEachFileSystem(() => { ` }, ]); - const checker = program.getTypeChecker(); - const reflectionHost = new TypeScriptReflectionHost(checker); - const evaluator = new PartialEvaluator(reflectionHost, checker, /* dependencyTracker */ null); - const moduleResolver = - new ModuleResolver(program, options, host, /* moduleResolutionCache */ null); - const importGraph = new ImportGraph(moduleResolver); - const cycleAnalyzer = new CycleAnalyzer(importGraph); - const metaRegistry = new LocalMetadataRegistry(); - const dtsReader = new DtsMetadataReader(checker, reflectionHost); - const scopeRegistry = new LocalModuleScopeRegistry( - metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), - new ReferenceEmitter([]), null); - const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]); - const refEmitter = new ReferenceEmitter([]); - const injectableRegistry = new InjectableClassRegistry(reflectionHost); - - const handler = new ComponentDecoratorHandler( - reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry, - new ResourceRegistry(), - /* isCore */ false, new NoopResourceLoader(), /* rootDirs */[''], - /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true, - /* enableI18nLegacyMessageIdFormat */ false, - /* i18nNormalizeLineEndingsInICUs */ undefined, moduleResolver, cycleAnalyzer, refEmitter, - NOOP_DEFAULT_IMPORT_RECORDER, /* depTracker */ null, injectableRegistry, - /* annotateForClosureCompiler */ false); + 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) { @@ -94,6 +101,104 @@ runInEachFileSystem(() => { expect(diag.start).toBe(detected.metadata.args![0].getStart()); } }); + + it('should keep track of inline template', () => { + const template = 'inline'; + 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'; + + @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?.resources.template.path).toBeNull(); + expect(analysis?.resources.template.expression.getText()).toEqual(`'${template}'`); + }); + + it('should keep track of external template', () => { + const templateUrl = '/myTemplate.ng.html'; + const {program, options, host} = makeProgram([ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const Component: any;', + }, + { + name: _(templateUrl), + contents: '
hello world
', + }, + { + name: _('/entry.ts'), + contents: ` + import {Component} from '@angular/core'; + + @Component({ + templateUrl: '${templateUrl}', + }) 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?.resources.template.path).toContain(templateUrl); + expect(analysis?.resources.template.expression.getText()).toContain(`'${templateUrl}'`); + }); + + it('should keep track of internal and external styles', () => { + const {program, options, host} = makeProgram([ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const Component: any;', + }, + { + name: _('/myStyle.css'), + contents: '
hello world
', + }, + { + name: _('/entry.ts'), + contents: ` + import {Component} from '@angular/core'; + + // These are ignored because we only attempt to store string literals + const ignoredStyleUrl = 'asdlfkj'; + const ignoredStyle = ''; + @Component({ + template: '', + styleUrls: ['/myStyle.css', ignoredStyleUrl], + styles: ['a { color: red; }', 'b { color: blue; }', ignoredStyle, ...[ignoredStyle]], + }) 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?.resources.styles.size).toBe(3); + }); }); function ivyCode(code: ErrorCode): number { diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index e4bd76313e..4a26f2f9a8 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -261,6 +261,9 @@ export class NgCompiler { const {resourceRegistry} = this.ensureAnalyzed(); const styles = resourceRegistry.getStyles(classDecl); const template = resourceRegistry.getTemplate(classDecl); + if (template === null) { + return null; + } return {styles, template}; } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/resource_registry.ts b/packages/compiler-cli/src/ngtsc/metadata/src/resource_registry.ts index c52567807a..24547bd968 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/resource_registry.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/resource_registry.ts @@ -12,23 +12,24 @@ import {AbsoluteFsPath} from '../../file_system'; import {ClassDeclaration} from '../../reflection'; /** - * Represents an external resource for a component and contains the `AbsoluteFsPath` + * Represents an resource for a component and contains the `AbsoluteFsPath` * to the file which was resolved by evaluating the `ts.Expression` (generally, a relative or * absolute string path to the resource). + * + * If the resource is inline, the `path` will be `null`. */ export interface Resource { - path: AbsoluteFsPath; + path: AbsoluteFsPath|null; expression: ts.Expression; } /** - * Represents the external resources of a component. + * Represents the either inline or external resources of a component. * - * If the component uses an inline template, the template resource will be `null`. - * If the component does not have external styles, the `styles` `Set` will be empty. + * A resource with a `path` of `null` is considered inline. */ export interface ComponentResources { - template: Resource|null; + template: Resource; styles: ReadonlySet; } @@ -40,17 +41,17 @@ export interface ComponentResources { * assistance. */ export class ResourceRegistry { - private templateToComponentsMap = new Map>(); + private externalTemplateToComponentsMap = new Map>(); private componentToTemplateMap = new Map(); private componentToStylesMap = new Map>(); - private styleToComponentsMap = new Map>(); + private externalStyleToComponentsMap = new Map>(); getComponentsWithTemplate(template: AbsoluteFsPath): ReadonlySet { - if (!this.templateToComponentsMap.has(template)) { + if (!this.externalTemplateToComponentsMap.has(template)) { return new Set(); } - return this.templateToComponentsMap.get(template)!; + return this.externalTemplateToComponentsMap.get(template)!; } registerResources(resources: ComponentResources, component: ClassDeclaration) { @@ -64,10 +65,12 @@ export class ResourceRegistry { registerTemplate(templateResource: Resource, component: ClassDeclaration): void { const {path} = templateResource; - if (!this.templateToComponentsMap.has(path)) { - this.templateToComponentsMap.set(path, new Set()); + if (path !== null) { + if (!this.externalTemplateToComponentsMap.has(path)) { + this.externalTemplateToComponentsMap.set(path, new Set()); + } + this.externalTemplateToComponentsMap.get(path)!.add(component); } - this.templateToComponentsMap.get(path)!.add(component); this.componentToTemplateMap.set(component, templateResource); } @@ -83,10 +86,12 @@ export class ResourceRegistry { if (!this.componentToStylesMap.has(component)) { this.componentToStylesMap.set(component, new Set()); } - if (!this.styleToComponentsMap.has(path)) { - this.styleToComponentsMap.set(path, new Set()); + if (path !== null) { + if (!this.externalStyleToComponentsMap.has(path)) { + this.externalStyleToComponentsMap.set(path, new Set()); + } + this.externalStyleToComponentsMap.get(path)!.add(component); } - this.styleToComponentsMap.get(path)!.add(component); this.componentToStylesMap.get(component)!.add(styleResource); } @@ -98,10 +103,10 @@ export class ResourceRegistry { } getComponentsWithStyle(styleUrl: AbsoluteFsPath): ReadonlySet { - if (!this.styleToComponentsMap.has(styleUrl)) { + if (!this.externalStyleToComponentsMap.has(styleUrl)) { return new Set(); } - return this.styleToComponentsMap.get(styleUrl)!; + return this.externalStyleToComponentsMap.get(styleUrl)!; } }