From 8f11b516f88e578ac854d40fdb8abf8e10510ef4 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Fri, 25 Sep 2020 16:28:32 -0400 Subject: [PATCH] refactor(compiler-cli): API for getting components from a template file (#39002) This commit adds an API to `NgCompiler`, a method called `getComponentsWithTemplateFile`. Given a filesystem path to an external template file, it retrieves a `Set` (actually a `ReadonlySet`) of component declarations which are using this template. In most cases, this will only be a single component. This information is easily determined by the compiler during analysis, but is hard for a lot of Angular tooling (e.g. the language service) to infer independently. Therefore, it makes sense to expose this as a compiler API. PR Close #39002 --- .../ngcc/src/analysis/decoration_analyzer.ts | 6 +- .../src/ngtsc/annotations/src/component.ts | 15 +++-- .../ngtsc/annotations/test/component_spec.ts | 3 +- .../src/ngtsc/core/src/compiler.ts | 21 +++++-- .../src/ngtsc/core/test/BUILD.bazel | 1 + .../src/ngtsc/core/test/compiler_test.ts | 62 ++++++++++++++++++- .../src/ngtsc/metadata/BUILD.bazel | 1 + .../compiler-cli/src/ngtsc/metadata/index.ts | 3 +- .../ngtsc/metadata/src/template_mapping.ts | 36 +++++++++++ 9 files changed, 132 insertions(+), 16 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/metadata/src/template_mapping.ts diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index 399c3abeb2..e557fa4fe5 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} from '../../../src/ngtsc/metadata'; +import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, TemplateMapping} 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,8 +94,8 @@ export class DecorationAnalyzer { handlers: DecoratorHandler[] = [ new ComponentDecoratorHandler( this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader, - this.scopeRegistry, this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs, - !!this.compilerOptions.preserveWhitespaces, + this.scopeRegistry, this.scopeRegistry, new TemplateMapping(), this.isCore, + this.resourceManager, this.rootDirs, !!this.compilerOptions.preserveWhitespaces, /* i18nUseExternalIds */ true, this.bundle.enableI18nLegacyMessageIdFormat, /* i18nNormalizeLineEndingsInICUs */ false, this.moduleResolver, this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER, NOOP_DEPENDENCY_TRACKER, diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 7c2d673034..1ae175cbce 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -11,11 +11,11 @@ import * as ts from 'typescript'; import {CycleAnalyzer} from '../../cycles'; import {ErrorCode, FatalDiagnosticError, ngErrorCode} from '../../diagnostics'; -import {absoluteFrom, relative} from '../../file_system'; +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} from '../../metadata'; +import {ClassPropertyMapping, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, TemplateMapping} from '../../metadata'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {ComponentScopeReader, LocalModuleScopeRegistry} from '../../scope'; @@ -83,9 +83,10 @@ export class ComponentDecoratorHandler implements private reflector: ReflectionHost, private evaluator: PartialEvaluator, private metaRegistry: MetadataRegistry, private metaReader: MetadataReader, private scopeReader: ComponentScopeReader, private scopeRegistry: LocalModuleScopeRegistry, - private isCore: boolean, private resourceLoader: ResourceLoader, - private rootDirs: ReadonlyArray, private defaultPreserveWhitespaces: boolean, - private i18nUseExternalIds: boolean, private enableI18nLegacyMessageIdFormat: boolean, + private templateMapping: TemplateMapping, private isCore: boolean, + private resourceLoader: ResourceLoader, private rootDirs: ReadonlyArray, + private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean, + private enableI18nLegacyMessageIdFormat: boolean, private i18nNormalizeLineEndingsInICUs: boolean|undefined, private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer, private refEmitter: ReferenceEmitter, private defaultImportRecorder: DefaultImportRecorder, @@ -384,6 +385,10 @@ export class ComponentDecoratorHandler implements ...analysis.typeCheckMeta, }); + if (!analysis.template.isInline) { + this.templateMapping.register(resolve(analysis.template.templateUrl), node); + } + this.injectableRegistry.registerInjectable(node); } 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 5fc7139e28..14ceb191c7 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} from '../../metadata'; +import {CompoundMetadataReader, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, TemplateMapping} from '../../metadata'; import {PartialEvaluator} from '../../partial_evaluator'; import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; @@ -69,6 +69,7 @@ runInEachFileSystem(() => { const handler = new ComponentDecoratorHandler( reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry, + new TemplateMapping(), /* 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 48857f59b7..fc91e6a281 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -13,11 +13,11 @@ import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecorato import {CycleAnalyzer, ImportGraph} from '../../cycles'; import {ErrorCode, ngErrorCode} from '../../diagnostics'; import {checkForPrivateExports, ReferenceGraph} from '../../entry_point'; -import {LogicalFileSystem} from '../../file_system'; +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} from '../../metadata'; +import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, TemplateMapping} from '../../metadata'; import {ModuleWithProvidersScanner} from '../../modulewithproviders'; import {PartialEvaluator} from '../../partial_evaluator'; import {NOOP_PERF_RECORDER, PerfRecorder} from '../../perf'; @@ -52,6 +52,7 @@ interface LazyCompilationState { aliasingHost: AliasingHost|null; refEmitter: ReferenceEmitter; templateTypeChecker: TemplateTypeChecker; + templateMapping: TemplateMapping; } /** @@ -223,6 +224,14 @@ export class NgCompiler { return this.ensureAnalyzed().templateTypeChecker; } + /** + * 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)); + } + /** * Perform Angular's analysis step (as a precursor to `getDiagnostics` or `prepareEmit`) * asynchronously. @@ -714,13 +723,14 @@ export class NgCompiler { const isCore = isAngularCorePackage(this.tsProgram); const defaultImportTracker = new DefaultImportTracker(); + const templateMapping = new TemplateMapping(); // Set up the IvyCompilation, which manages state for the Ivy transformer. const handlers: DecoratorHandler[] = [ new ComponentDecoratorHandler( - reflector, evaluator, metaRegistry, metaReader, scopeReader, scopeRegistry, isCore, - this.resourceManager, this.adapter.rootDirs, this.options.preserveWhitespaces || false, - this.options.i18nUseExternalIds !== false, + reflector, evaluator, metaRegistry, metaReader, scopeReader, scopeRegistry, + templateMapping, 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, refEmitter, defaultImportTracker, this.incrementalDriver.depGraph, injectableRegistry, @@ -773,6 +783,7 @@ export class NgCompiler { aliasingHost, refEmitter, templateTypeChecker, + templateMapping, }; } } diff --git a/packages/compiler-cli/src/ngtsc/core/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/core/test/BUILD.bazel index ec34344c91..54dcaad509 100644 --- a/packages/compiler-cli/src/ngtsc/core/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/core/test/BUILD.bazel @@ -15,6 +15,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/incremental", + "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/typecheck", "@npm//typescript", ], 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 16e9fea106..0a6b3ac118 100644 --- a/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts +++ b/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts @@ -11,12 +11,14 @@ import * as ts from 'typescript'; import {absoluteFrom as _, FileSystem, getFileSystem, getSourceFileOrError, NgtscCompilerHost, setFileSystem} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; import {NoopIncrementalBuildStrategy} from '../../incremental'; +import {ClassDeclaration, isNamedClassDeclaration} from '../../reflection'; import {ReusedProgramStrategy} from '../../typecheck'; + import {NgCompilerOptions} from '../api'; + import {NgCompiler} from '../src/compiler'; import {NgCompilerHost} from '../src/host'; - runInEachFileSystem(() => { describe('NgCompiler', () => { let fs: FileSystem; @@ -55,5 +57,63 @@ runInEachFileSystem(() => { expect(diags.length).toBe(1); expect(diags[0].messageText).toContain('does_not_exist'); }); + + describe('getComponentsWithTemplateFile', () => { + it('should return the component(s) using a template file', () => { + const templateFile = _('/template.html'); + fs.writeFile(templateFile, `This is the template, used by components CmpA and CmpC`); + const cmpAFile = _('/cmp-a.ts'); + fs.writeFile(cmpAFile, ` + import {Component} from '@angular/core'; + @Component({ + selector: 'cmp-a', + templateUrl: './template.html', + }) + export class CmpA {} + `); + const cmpBFile = _('/cmp-b.ts'); + fs.writeFile(cmpBFile, ` + import {Component} from '@angular/core'; + @Component({ + selector: 'cmp-b', + template: 'CmpB does not use an external template', + }) + export class CmpB {} + `); + const cmpCFile = _('/cmp-c.ts'); + fs.writeFile(cmpCFile, ` + import {Component} from '@angular/core'; + @Component({ + selector: 'cmp-c', + templateUrl: './template.html', + }) + 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.getComponentsWithTemplateFile(templateFile); + expect(components).toEqual(new Set([CmpA, CmpC])); + }); + }); }); }); + + +function getClass(sf: ts.SourceFile, name: string): ClassDeclaration { + for (const stmt of sf.statements) { + if (isNamedClassDeclaration(stmt) && stmt.name.text === name) { + return stmt; + } + } + throw new Error(`Class ${name} not found in file: ${sf.fileName}: ${sf.text}`); +} diff --git a/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel b/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel index 6c36eb70d3..2cf7890186 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel @@ -9,6 +9,7 @@ ts_library( ]), deps = [ "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/util", diff --git a/packages/compiler-cli/src/ngtsc/metadata/index.ts b/packages/compiler-cli/src/ngtsc/metadata/index.ts index 9ee4e1a977..b452071875 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/index.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/index.ts @@ -10,5 +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 {extractDirectiveTypeCheckMeta, CompoundMetadataReader} from './src/util'; -export {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, InputOrOutput} from './src/property_mapping'; +export {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, InputOrOutput} from './src/property_mapping'; \ No newline at end of file diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/template_mapping.ts b/packages/compiler-cli/src/ngtsc/metadata/src/template_mapping.ts new file mode 100644 index 0000000000..2a5d49613e --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/metadata/src/template_mapping.ts @@ -0,0 +1,36 @@ +/** + * @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); + } +}