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
This commit is contained in:
Alex Rickabaugh 2020-09-25 16:28:32 -04:00
parent c8958d76db
commit 8f11b516f8
9 changed files with 132 additions and 16 deletions

View File

@ -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<unknown, unknown, unknown>[] = [
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,

View File

@ -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<string>, private defaultPreserveWhitespaces: boolean,
private i18nUseExternalIds: boolean, private enableI18nLegacyMessageIdFormat: boolean,
private templateMapping: TemplateMapping, private isCore: boolean,
private resourceLoader: ResourceLoader, private rootDirs: ReadonlyArray<string>,
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);
}

View File

@ -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,

View File

@ -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<ts.Declaration> {
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<unknown, unknown, unknown>[] = [
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,
};
}
}

View File

@ -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",
],

View File

@ -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<ts.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}`);
}

View File

@ -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",

View File

@ -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';

View File

@ -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<AbsoluteFsPath, Set<ClassDeclaration>>();
getComponentsWithTemplate(template: AbsoluteFsPath): ReadonlySet<ClassDeclaration> {
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);
}
}