diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index e557fa4fe5..b6cd153955 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -14,7 +14,7 @@ import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles'; import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics'; import {absoluteFrom, absoluteFromSourceFile, dirname, FileSystem, LogicalFileSystem, resolve} from '../../../src/ngtsc/file_system'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports'; -import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, TemplateMapping} from '../../../src/ngtsc/metadata'; +import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, ResourceRegistry} from '../../../src/ngtsc/metadata'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope'; import {DecoratorHandler} from '../../../src/ngtsc/transform'; @@ -94,7 +94,7 @@ export class DecorationAnalyzer { handlers: DecoratorHandler[] = [ new ComponentDecoratorHandler( this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader, - this.scopeRegistry, this.scopeRegistry, new TemplateMapping(), this.isCore, + this.scopeRegistry, this.scopeRegistry, new ResourceRegistry(), this.isCore, this.resourceManager, this.rootDirs, !!this.compilerOptions.preserveWhitespaces, /* i18nUseExternalIds */ true, this.bundle.enableI18nLegacyMessageIdFormat, /* i18nNormalizeLineEndingsInICUs */ false, this.moduleResolver, this.cycleAnalyzer, diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 74c5c73b83..7c255a4050 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -15,7 +15,7 @@ import {absoluteFrom, relative, resolve} from '../../file_system'; import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; import {DependencyTracker} from '../../incremental/api'; import {IndexingContext} from '../../indexer'; -import {ClassPropertyMapping, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, TemplateMapping} from '../../metadata'; +import {ClassPropertyMapping, ComponentResources, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, Resource, ResourceRegistry} from '../../metadata'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {ComponentScopeReader, LocalModuleScopeRegistry} from '../../scope'; @@ -70,6 +70,8 @@ export interface ComponentAnalysisData { * require an Angular factory definition at runtime. */ viewProvidersRequiringFactory: Set>|null; + + resources: ComponentResources; } export type ComponentResolutionData = Pick; @@ -83,7 +85,7 @@ export class ComponentDecoratorHandler implements private reflector: ReflectionHost, private evaluator: PartialEvaluator, private metaRegistry: MetadataRegistry, private metaReader: MetadataReader, private scopeReader: ComponentScopeReader, private scopeRegistry: LocalModuleScopeRegistry, - private templateMapping: TemplateMapping, private isCore: boolean, + private resourceRegistry: ResourceRegistry, private isCore: boolean, private resourceLoader: ResourceLoader, private rootDirs: ReadonlyArray, private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean, private enableI18nLegacyMessageIdFormat: boolean, @@ -259,6 +261,9 @@ export class ComponentDecoratorHandler implements template = this._extractInlineTemplate(node, decorator, component, containingFile); } } + const templateResource = template.isInline ? + null : + {path: absoluteFrom(template.templateUrl), expression: template.sourceMapping.node}; let diagnostics: ts.Diagnostic[]|undefined = undefined; @@ -287,6 +292,7 @@ export class ComponentDecoratorHandler implements // component. let styles: string[]|null = null; + const styleResources = this._extractStyleResources(component, containingFile); const styleUrls = this._extractStyleUrls(component, template.styleUrls); if (styleUrls !== null) { if (styles === null) { @@ -359,6 +365,10 @@ export class ComponentDecoratorHandler implements template, providersRequiringFactory, viewProvidersRequiringFactory, + resources: { + styles: styleResources, + template: templateResource, + }, }, diagnostics, }; @@ -385,10 +395,7 @@ export class ComponentDecoratorHandler implements ...analysis.typeCheckMeta, }); - if (!analysis.template.isInline) { - this.templateMapping.register(resolve(analysis.template.templateUrl), node); - } - + this.resourceRegistry.registerResources(analysis.resources, node); this.injectableRegistry.registerInjectable(node); } @@ -657,6 +664,25 @@ export class ComponentDecoratorHandler implements return styleUrls as string[]; } + 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 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}); + } + return externalStyles; + } + private _preloadAndParseTemplate( node: ClassDeclaration, decorator: Decorator, component: Map, containingFile: string): Promise { 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 14ceb191c7..395ff0f14c 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts @@ -10,7 +10,7 @@ import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {absoluteFrom} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; import {ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; -import {CompoundMetadataReader, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, TemplateMapping} from '../../metadata'; +import {CompoundMetadataReader, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, ResourceRegistry} from '../../metadata'; import {PartialEvaluator} from '../../partial_evaluator'; import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; @@ -69,7 +69,7 @@ runInEachFileSystem(() => { const handler = new ComponentDecoratorHandler( reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry, - new TemplateMapping(), + new ResourceRegistry(), /* isCore */ false, new NoopResourceLoader(), /* rootDirs */[''], /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true, /* enableI18nLegacyMessageIdFormat */ false, diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index 0350590747..e4bd76313e 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -17,11 +17,11 @@ import {LogicalFileSystem, resolve} from '../../file_system'; import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports'; import {IncrementalBuildStrategy, IncrementalDriver} from '../../incremental'; import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer'; -import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, TemplateMapping} from '../../metadata'; +import {ComponentResources, CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, ResourceRegistry} from '../../metadata'; import {ModuleWithProvidersScanner} from '../../modulewithproviders'; import {PartialEvaluator} from '../../partial_evaluator'; import {NOOP_PERF_RECORDER, PerfRecorder} from '../../perf'; -import {DeclarationNode, TypeScriptReflectionHost} from '../../reflection'; +import {DeclarationNode, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {AdapterResourceLoader} from '../../resource'; import {entryPointKeyFor, NgModuleRouteAnalyzer} from '../../routing'; import {ComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; @@ -52,7 +52,7 @@ interface LazyCompilationState { aliasingHost: AliasingHost|null; refEmitter: ReferenceEmitter; templateTypeChecker: TemplateTypeChecker; - templateMapping: TemplateMapping; + resourceRegistry: ResourceRegistry; } /** @@ -239,8 +239,30 @@ export class NgCompiler { * Retrieves the `ts.Declaration`s for any component(s) which use the given template file. */ getComponentsWithTemplateFile(templateFilePath: string): ReadonlySet { - const {templateMapping} = this.ensureAnalyzed(); - return templateMapping.getComponentsWithTemplate(resolve(templateFilePath)); + const {resourceRegistry} = this.ensureAnalyzed(); + return resourceRegistry.getComponentsWithTemplate(resolve(templateFilePath)); + } + + /** + * Retrieves the `ts.Declaration`s for any component(s) which use the given template file. + */ + getComponentsWithStyleFile(styleFilePath: string): ReadonlySet { + const {resourceRegistry} = this.ensureAnalyzed(); + return resourceRegistry.getComponentsWithStyle(resolve(styleFilePath)); + } + + /** + * Retrieves external resources for the given component. + */ + getComponentResources(classDecl: DeclarationNode): ComponentResources|null { + if (!isNamedClassDeclaration(classDecl)) { + return null; + } + const {resourceRegistry} = this.ensureAnalyzed(); + const styles = resourceRegistry.getStyles(classDecl); + const template = resourceRegistry.getTemplate(classDecl); + + return {styles, template}; } /** @@ -734,13 +756,13 @@ export class NgCompiler { const isCore = isAngularCorePackage(this.tsProgram); const defaultImportTracker = new DefaultImportTracker(); - const templateMapping = new TemplateMapping(); + const resourceRegistry = new ResourceRegistry(); // Set up the IvyCompilation, which manages state for the Ivy transformer. const handlers: DecoratorHandler[] = [ new ComponentDecoratorHandler( reflector, evaluator, metaRegistry, metaReader, scopeReader, scopeRegistry, - templateMapping, isCore, this.resourceManager, this.adapter.rootDirs, + resourceRegistry, isCore, this.resourceManager, this.adapter.rootDirs, this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false, this.options.enableI18nLegacyMessageIdFormat !== false, this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer, @@ -797,7 +819,7 @@ export class NgCompiler { aliasingHost, refEmitter, templateTypeChecker, - templateMapping, + resourceRegistry, }; } } diff --git a/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts b/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts index 9e873e720e..37be7020c6 100644 --- a/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts +++ b/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts @@ -106,6 +106,127 @@ runInEachFileSystem(() => { }); }); + describe('getComponentsWithStyle', () => { + it('should return the component(s) using a style file', () => { + const styleFile = _('/style.css'); + fs.writeFile(styleFile, `/* This is the style, used by components CmpA and CmpC */`); + const cmpAFile = _('/cmp-a.ts'); + fs.writeFile(cmpAFile, ` + import {Component} from '@angular/core'; + @Component({ + selector: 'cmp-a', + template: '', + styleUrls: ['./style.css'], + }) + export class CmpA {} + `); + const cmpBFile = _('/cmp-b.ts'); + fs.writeFile(cmpBFile, ` + import {Component} from '@angular/core'; + @Component({ + selector: 'cmp-b', + template: '', + styles: ['/* CmpB does not use external style */'], + }) + export class CmpB {} + `); + const cmpCFile = _('/cmp-c.ts'); + fs.writeFile(cmpCFile, ` + import {Component} from '@angular/core'; + @Component({ + selector: 'cmp-c', + template: '', + styleUrls: ['./style.css'] + }) + export class CmpC {} + `); + + const options: NgCompilerOptions = {}; + + const baseHost = new NgtscCompilerHost(getFileSystem(), options); + const host = NgCompilerHost.wrap( + baseHost, [cmpAFile, cmpBFile, cmpCFile], options, /* oldProgram */ null); + const program = ts.createProgram({host, options, rootNames: host.inputFiles}); + const CmpA = getClass(getSourceFileOrError(program, cmpAFile), 'CmpA'); + const CmpC = getClass(getSourceFileOrError(program, cmpCFile), 'CmpC'); + const compiler = new NgCompiler( + host, options, program, new ReusedProgramStrategy(program, host, options, []), + new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false); + const components = compiler.getComponentsWithStyleFile(styleFile); + expect(components).toEqual(new Set([CmpA, CmpC])); + }); + }); + + describe('getComponentResources', () => { + it('should return the component resources', () => { + const styleFile = _('/style.css'); + fs.writeFile(styleFile, `/* This is the template, used by components CmpA */`); + const styleFile2 = _('/style2.css'); + fs.writeFile(styleFile2, `/* This is the template, used by components CmpA */`); + const templateFile = _('/template.ng.html'); + fs.writeFile(templateFile, `This is the template, used by components CmpA`); + const cmpAFile = _('/cmp-a.ts'); + fs.writeFile(cmpAFile, ` + import {Component} from '@angular/core'; + @Component({ + selector: 'cmp-a', + templateUrl: './template.ng.html', + styleUrls: ['./style.css', '../../style2.css'], + }) + export class CmpA {} + `); + + const options: NgCompilerOptions = {}; + + const baseHost = new NgtscCompilerHost(getFileSystem(), options); + const host = NgCompilerHost.wrap(baseHost, [cmpAFile], options, /* oldProgram */ null); + const program = ts.createProgram({host, options, rootNames: host.inputFiles}); + const CmpA = getClass(getSourceFileOrError(program, cmpAFile), 'CmpA'); + const compiler = new NgCompiler( + host, options, program, new ReusedProgramStrategy(program, host, options, []), + new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false); + const resources = compiler.getComponentResources(CmpA); + expect(resources).not.toBeNull(); + const {template, styles} = resources!; + expect(template !.path).toEqual(templateFile); + expect(styles.size).toEqual(2); + const actualPaths = new Set(Array.from(styles).map(r => r.path)); + expect(actualPaths).toEqual(new Set([styleFile, styleFile2])); + }); + + it('does not return component style resources if not an array of strings', () => { + const styleFile = _('/style.css'); + fs.writeFile(styleFile, `/* This is the template, used by components CmpA */`); + const styleFile2 = _('/style2.css'); + fs.writeFile(styleFile2, `/* This is the template, used by components CmpA */`); + const cmpAFile = _('/cmp-a.ts'); + fs.writeFile(cmpAFile, ` + import {Component} from '@angular/core'; + const STYLES = ['./style.css', '../../style2.css']; + @Component({ + selector: 'cmp-a', + template: '', + styleUrls: STYLES, + }) + export class CmpA {} + `); + + const options: NgCompilerOptions = {}; + + const baseHost = new NgtscCompilerHost(getFileSystem(), options); + const host = NgCompilerHost.wrap(baseHost, [cmpAFile], options, /* oldProgram */ null); + const program = ts.createProgram({host, options, rootNames: host.inputFiles}); + const CmpA = getClass(getSourceFileOrError(program, cmpAFile), 'CmpA'); + const compiler = new NgCompiler( + host, options, program, new ReusedProgramStrategy(program, host, options, []), + new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false); + const resources = compiler.getComponentResources(CmpA); + expect(resources).not.toBeNull(); + const {styles} = resources!; + expect(styles.size).toEqual(0); + }); + }); + describe('getResourceDependencies', () => { it('should return resource dependencies of a component source file', () => { const COMPONENT = _('/cmp.ts'); diff --git a/packages/compiler-cli/src/ngtsc/metadata/index.ts b/packages/compiler-cli/src/ngtsc/metadata/index.ts index b452071875..653465c772 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/index.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/index.ts @@ -10,6 +10,6 @@ export * from './src/api'; export {DtsMetadataReader} from './src/dts'; export {flattenInheritedDirectiveMetadata} from './src/inheritance'; export {CompoundMetadataRegistry, LocalMetadataRegistry, InjectableClassRegistry} from './src/registry'; -export {TemplateRegistry as TemplateMapping} from './src/template_mapping'; +export {ResourceRegistry, Resource, ComponentResources} from './src/resource_registry'; export {extractDirectiveTypeCheckMeta, CompoundMetadataReader} from './src/util'; -export {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, InputOrOutput} from './src/property_mapping'; \ No newline at end of file +export {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, InputOrOutput} from './src/property_mapping'; diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/resource_registry.ts b/packages/compiler-cli/src/ngtsc/metadata/src/resource_registry.ts new file mode 100644 index 0000000000..c52567807a --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/metadata/src/resource_registry.ts @@ -0,0 +1,107 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {AbsoluteFsPath} from '../../file_system'; +import {ClassDeclaration} from '../../reflection'; + +/** + * Represents an external 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). + */ +export interface Resource { + path: AbsoluteFsPath; + expression: ts.Expression; +} + +/** + * Represents the 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. + */ +export interface ComponentResources { + template: Resource|null; + styles: ReadonlySet; +} + +/** + * Tracks the mapping between external template/style files and the component(s) which use them. + * + * This information is produced during analysis of the program and is used mainly to support + * external tooling, for which such a mapping is challenging to determine without compiler + * assistance. + */ +export class ResourceRegistry { + private templateToComponentsMap = new Map>(); + private componentToTemplateMap = new Map(); + private componentToStylesMap = new Map>(); + private styleToComponentsMap = new Map>(); + + getComponentsWithTemplate(template: AbsoluteFsPath): ReadonlySet { + if (!this.templateToComponentsMap.has(template)) { + return new Set(); + } + + return this.templateToComponentsMap.get(template)!; + } + + registerResources(resources: ComponentResources, component: ClassDeclaration) { + if (resources.template !== null) { + this.registerTemplate(resources.template, component); + } + for (const style of resources.styles) { + this.registerStyle(style, component); + } + } + + registerTemplate(templateResource: Resource, component: ClassDeclaration): void { + const {path} = templateResource; + if (!this.templateToComponentsMap.has(path)) { + this.templateToComponentsMap.set(path, new Set()); + } + this.templateToComponentsMap.get(path)!.add(component); + this.componentToTemplateMap.set(component, templateResource); + } + + getTemplate(component: ClassDeclaration): Resource|null { + if (!this.componentToTemplateMap.has(component)) { + return null; + } + return this.componentToTemplateMap.get(component)!; + } + + registerStyle(styleResource: Resource, component: ClassDeclaration): void { + const {path} = styleResource; + if (!this.componentToStylesMap.has(component)) { + this.componentToStylesMap.set(component, new Set()); + } + if (!this.styleToComponentsMap.has(path)) { + this.styleToComponentsMap.set(path, new Set()); + } + this.styleToComponentsMap.get(path)!.add(component); + this.componentToStylesMap.get(component)!.add(styleResource); + } + + getStyles(component: ClassDeclaration): Set { + if (!this.componentToStylesMap.has(component)) { + return new Set(); + } + return this.componentToStylesMap.get(component)!; + } + + getComponentsWithStyle(styleUrl: AbsoluteFsPath): ReadonlySet { + if (!this.styleToComponentsMap.has(styleUrl)) { + return new Set(); + } + + return this.styleToComponentsMap.get(styleUrl)!; + } +} diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/template_mapping.ts b/packages/compiler-cli/src/ngtsc/metadata/src/template_mapping.ts deleted file mode 100644 index 2a5d49613e..0000000000 --- a/packages/compiler-cli/src/ngtsc/metadata/src/template_mapping.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {AbsoluteFsPath} from '../../file_system'; -import {ClassDeclaration} from '../../reflection'; - -/** - * Tracks the mapping between external template files and the component(s) which use them. - * - * This information is produced during analysis of the program and is used mainly to support - * external tooling, for which such a mapping is challenging to determine without compiler - * assistance. - */ -export class TemplateRegistry { - private map = new Map>(); - - getComponentsWithTemplate(template: AbsoluteFsPath): ReadonlySet { - if (!this.map.has(template)) { - return new Set(); - } - - return this.map.get(template)!; - } - - register(template: AbsoluteFsPath, component: ClassDeclaration): void { - if (!this.map.has(template)) { - this.map.set(template, new Set()); - } - this.map.get(template)!.add(component); - } -}