diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index 26df1db3f7..bf9efbdcc5 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -108,7 +108,9 @@ export class DecorationAnalyzer { new NgModuleDecoratorHandler( this.reflectionHost, this.evaluator, this.fullMetaReader, this.fullRegistry, this.scopeRegistry, this.referencesRegistry, this.isCore, /* routeAnalyzer */ null, - this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false), + this.refEmitter, + /* factoryTracker */ null, NOOP_DEFAULT_IMPORT_RECORDER, + /* annotateForClosureCompiler */ false), ]; migrations: Migration[] = [ new UndecoratedParentMigration(), diff --git a/packages/compiler-cli/ngcc/src/analysis/util.ts b/packages/compiler-cli/ngcc/src/analysis/util.ts index 7e04b25373..0f8ae97515 100644 --- a/packages/compiler-cli/ngcc/src/analysis/util.ts +++ b/packages/compiler-cli/ngcc/src/analysis/util.ts @@ -80,7 +80,12 @@ export function analyzeDecorators( if (diagnostics !== undefined) { allDiagnostics.push(...diagnostics); } - match.analysis = analysis !; + if (analysis !== undefined) { + match.analysis = analysis; + if (match.handler.register !== undefined) { + match.handler.register(declaration, analysis); + } + } matches.push(match); } catch (e) { if (isFatalDiagnosticError(e)) { diff --git a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel index ad7acdbe12..928789d69e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel @@ -19,6 +19,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/routing", "//packages/compiler-cli/src/ngtsc/scope", + "//packages/compiler-cli/src/ngtsc/shims", "//packages/compiler-cli/src/ngtsc/transform", "//packages/compiler-cli/src/ngtsc/typecheck", "//packages/compiler-cli/src/ngtsc/util", diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 7637299f36..aad5b959c8 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -49,6 +49,8 @@ export interface ComponentAnalysisData { * (not during resolve). */ meta: Omit; + baseClass: Reference|'dynamic'|null; + guards: ReturnType; parsedTemplate: ParsedTemplate; templateSourceMapping: TemplateSourceMapping; metadataStmt: Statement|null; @@ -239,21 +241,6 @@ export class ComponentDecoratorHandler implements `Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`); } - // Register this component's information with the `MetadataRegistry`. This ensures that - // the information about the component is available during the compile() phase. - const ref = new Reference(node); - this.metaRegistry.registerDirectiveMetadata({ - ref, - name: node.name.text, - selector: metadata.selector, - exportAs: metadata.exportAs, - inputs: metadata.inputs, - outputs: metadata.outputs, - queries: metadata.queries.map(query => query.propertyName), - isComponent: true, ...extractDirectiveGuards(node, this.reflector), - baseClass: readBaseClass(node, this.reflector, this.evaluator), - }); - // Figure out the set of styles. The ordering here is important: external resources (styleUrls) // precede inline styles, and styles defined in the template override styles defined in the // component. @@ -302,6 +289,7 @@ export class ComponentDecoratorHandler implements const output: AnalysisOutput = { analysis: { + baseClass: readBaseClass(node, this.reflector, this.evaluator), meta: { ...metadata, template, @@ -315,12 +303,12 @@ export class ComponentDecoratorHandler implements viewProviders, i18nUseExternalIds: this.i18nUseExternalIds, relativeContextFilePath, }, + guards: extractDirectiveGuards(node, this.reflector), metadataStmt: generateSetClassMetadataCall( node, this.reflector, this.defaultImportRecorder, this.isCore, this.annotateForClosureCompiler), parsedTemplate: template, parseTemplate, templateSourceMapping, }, - typeCheck: true, }; if (changeDetection !== null) { output.analysis !.meta.changeDetection = changeDetection; @@ -328,6 +316,23 @@ export class ComponentDecoratorHandler implements return output; } + register(node: ClassDeclaration, analysis: ComponentAnalysisData): void { + // Register this component's information with the `MetadataRegistry`. This ensures that + // the information about the component is available during the compile() phase. + const ref = new Reference(node); + this.metaRegistry.registerDirectiveMetadata({ + ref, + name: node.name.text, + selector: analysis.meta.selector, + exportAs: analysis.meta.exportAs, + inputs: analysis.meta.inputs, + outputs: analysis.meta.outputs, + queries: analysis.meta.queries.map(query => query.propertyName), + isComponent: true, + baseClass: analysis.baseClass, ...analysis.guards, + }); + } + index( context: IndexingContext, node: ClassDeclaration, analysis: Readonly) { // The component template may have been previously parsed without preserving whitespace or with diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 66a26c5f69..032948c579 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -32,6 +32,8 @@ const LIFECYCLE_HOOKS = new Set([ ]); export interface DirectiveHandlerData { + baseClass: Reference|'dynamic'|null; + guards: ReturnType; meta: R3DirectiveMetadata; metadataStmt: Statement|null; } @@ -83,31 +85,35 @@ export class DirectiveDecoratorHandler implements return {}; } - // Register this directive's information with the `MetadataRegistry`. This ensures that - // the information about the directive is available during the compile() phase. - const ref = new Reference(node); - this.metaRegistry.registerDirectiveMetadata({ - ref, - name: node.name.text, - selector: analysis.selector, - exportAs: analysis.exportAs, - inputs: analysis.inputs, - outputs: analysis.outputs, - queries: analysis.queries.map(query => query.propertyName), - isComponent: false, ...extractDirectiveGuards(node, this.reflector), - baseClass: readBaseClass(node, this.reflector, this.evaluator), - }); - return { analysis: { meta: analysis, metadataStmt: generateSetClassMetadataCall( node, this.reflector, this.defaultImportRecorder, this.isCore, this.annotateForClosureCompiler), + baseClass: readBaseClass(node, this.reflector, this.evaluator), + guards: extractDirectiveGuards(node, this.reflector), } }; } + register(node: ClassDeclaration, analysis: Readonly): void { + // Register this directive's information with the `MetadataRegistry`. This ensures that + // the information about the directive is available during the compile() phase. + const ref = new Reference(node); + this.metaRegistry.registerDirectiveMetadata({ + ref, + name: node.name.text, + selector: analysis.meta.selector, + exportAs: analysis.meta.exportAs, + inputs: analysis.meta.inputs, + outputs: analysis.meta.outputs, + queries: analysis.meta.queries.map(query => query.propertyName), + isComponent: false, + baseClass: analysis.baseClass, ...analysis.guards, + }); + } + compile( node: ClassDeclaration, analysis: Readonly, resolution: Readonly, pool: ConstantPool): CompileResult[] { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index 8ad262d966..ff20441fc6 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -16,6 +16,7 @@ import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection'; import {NgModuleRouteAnalyzer} from '../../routing'; import {LocalModuleScopeRegistry, ScopeData} from '../../scope'; +import {FactoryTracker} from '../../shims'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform'; import {getSourceFile} from '../../util/src/typescript'; @@ -28,8 +29,11 @@ export interface NgModuleAnalysis { inj: R3InjectorMetadata; metadataStmt: Statement|null; declarations: Reference[]; + schemas: SchemaMetadata[]; + imports: Reference[]; exports: Reference[]; id: Expression|null; + factorySymbolName: string; } /** @@ -45,6 +49,7 @@ export class NgModuleDecoratorHandler implements private scopeRegistry: LocalModuleScopeRegistry, private referencesRegistry: ReferencesRegistry, private isCore: boolean, private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter, + private factoryTracker: FactoryTracker|null, private defaultImportRecorder: DefaultImportRecorder, private annotateForClosureCompiler: boolean, private localeId?: string) {} @@ -166,18 +171,6 @@ export class NgModuleDecoratorHandler implements const id: Expression|null = ngModule.has('id') ? new WrappedNodeExpr(ngModule.get('id') !) : null; - - // Register this module's information with the LocalModuleScopeRegistry. This ensures that - // during the compile() phase, the module's metadata is available for selector scope - // computation. - this.metaRegistry.registerNgModuleMetadata({ - ref: new Reference(node), - schemas, - declarations: declarationRefs, - imports: importRefs, - exports: exportRefs, - }); - const valueContext = node.getSourceFile(); let typeContext = valueContext; @@ -246,18 +239,37 @@ export class NgModuleDecoratorHandler implements return { analysis: { id, + schemas: schemas, mod: ngModuleDef, inj: ngInjectorDef, declarations: declarationRefs, + imports: importRefs, exports: exportRefs, metadataStmt: generateSetClassMetadataCall( node, this.reflector, this.defaultImportRecorder, this.isCore, this.annotateForClosureCompiler), + factorySymbolName: node.name.text, }, - factorySymbolName: node.name.text, }; } + register(node: ClassDeclaration, analysis: NgModuleAnalysis): void { + // Register this module's information with the LocalModuleScopeRegistry. This ensures that + // during the compile() phase, the module's metadata is available for selector scope + // computation. + this.metaRegistry.registerNgModuleMetadata({ + ref: new Reference(node), + schemas: analysis.schemas, + declarations: analysis.declarations, + imports: analysis.imports, + exports: analysis.exports, + }); + + if (this.factoryTracker !== null) { + this.factoryTracker.track(node.getSourceFile(), analysis.factorySymbolName); + } + } + resolve(node: ClassDeclaration, analysis: Readonly): ResolveResult { const scope = this.scopeRegistry.getScopeOfModule(node); const diagnostics = this.scopeRegistry.getDiagnosticsOfModule(node) || undefined; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts index 1cd5b78d71..7c97e5599b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts @@ -80,8 +80,6 @@ export class PipeDecoratorHandler implements DecoratorHandler): void { + const ref = new Reference(node); + this.metaRegistry.registerPipeMetadata({ref, name: analysis.meta.pipeName}); + } + compile(node: ClassDeclaration, analysis: Readonly): CompileResult[] { const meta = analysis.meta; const res = compilePipeFromMetadata(meta); diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts index 97901fd5c1..23589cdc8d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts @@ -69,8 +69,8 @@ runInEachFileSystem(() => { const handler = new NgModuleDecoratorHandler( reflectionHost, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry, - false, null, refEmitter, NOOP_DEFAULT_IMPORT_RECORDER, - /* annotateForClosureCompiler */ false); + /* isCore */ false, /* routeAnalyzer */ null, refEmitter, /* factoryTracker */ null, + NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false); const TestModule = getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); const detected = diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 858b44485a..123251b2f5 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {GeneratedFile} from '@angular/compiler'; +import {GeneratedFile, Type} from '@angular/compiler'; import * as ts from 'typescript'; import * as api from '../transformers/api'; @@ -30,7 +30,7 @@ import {TypeScriptReflectionHost} from './reflection'; import {HostResourceLoader} from './resource_loader'; import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing'; import {ComponentScopeReader, CompoundComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from './scope'; -import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, TypeCheckShimGenerator, generatedFactoryTransform} from './shims'; +import {FactoryGenerator, FactoryTracker, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, TypeCheckShimGenerator, generatedFactoryTransform} from './shims'; import {ivySwitchTransform} from './switch'; import {DecoratorHandler, DtsTransformRegistry, TraitCompiler, declarationTransformFactory, ivyTransformFactory} from './transform'; import {aliasTransformFactory} from './transform/src/alias'; @@ -43,8 +43,6 @@ export class NgtscProgram implements api.Program { private reuseTsProgram: ts.Program; private resourceManager: HostResourceLoader; private compilation: TraitCompiler|undefined = undefined; - private factoryToSourceInfo: Map|null = null; - private sourceToFactorySymbols: Map>|null = null; private _coreImportsFrom: ts.SourceFile|null|undefined = undefined; private _importRewriter: ImportRewriter|undefined = undefined; private _reflector: TypeScriptReflectionHost|undefined = undefined; @@ -55,6 +53,7 @@ export class NgtscProgram implements api.Program { private exportReferenceGraph: ReferenceGraph|null = null; private flatIndexGenerator: FlatIndexGenerator|null = null; private routeAnalyzer: NgModuleRouteAnalyzer|null = null; + private scopeRegistry: LocalModuleScopeRegistry|null = null; private constructionDiagnostics: ts.Diagnostic[] = []; private moduleResolver: ModuleResolver; @@ -69,6 +68,8 @@ export class NgtscProgram implements api.Program { private perfTracker: PerfTracker|null = null; private incrementalDriver: IncrementalDriver; private typeCheckFilePath: AbsoluteFsPath; + private factoryTracker: FactoryTracker|null = null; + private modifiedResourceFiles: Set|null; private dtsTransforms: DtsTransformRegistry|null = null; private mwpScanner: ModuleWithProvidersScanner|null = null; @@ -117,17 +118,12 @@ export class NgtscProgram implements api.Program { // Factory generation. const factoryGenerator = FactoryGenerator.forRootFiles(normalizedRootNames); const factoryFileMap = factoryGenerator.factoryFileMap; - this.factoryToSourceInfo = new Map(); - this.sourceToFactorySymbols = new Map>(); - factoryFileMap.forEach((sourceFilePath, factoryPath) => { - const moduleSymbolNames = new Set(); - this.sourceToFactorySymbols !.set(sourceFilePath, moduleSymbolNames); - this.factoryToSourceInfo !.set(factoryPath, {sourceFilePath, moduleSymbolNames}); - }); const factoryFileNames = Array.from(factoryFileMap.keys()); rootFiles.push(...factoryFileNames); generators.push(factoryGenerator); + + this.factoryTracker = new FactoryTracker(factoryGenerator); } // Done separately to preserve the order of factory files before summary files in rootFiles. @@ -247,6 +243,7 @@ export class NgtscProgram implements api.Program { const analyzeFileSpan = this.perfRecorder.start('analyzeFile', sf); let analysisPromise = this.compilation !.analyzeAsync(sf); + this.scanForMwp(sf); if (analysisPromise === undefined) { this.perfRecorder.stop(analyzeFileSpan); } else if (this.perfRecorder.enabled) { @@ -262,6 +259,8 @@ export class NgtscProgram implements api.Program { this.perfRecorder.stop(analyzeSpan); this.compilation.resolve(); + this.recordNgModuleScopeDependencies(); + // At this point, analysis is complete and the compiler can now calculate which files need to be // emitted, so do that. this.incrementalDriver.recordSuccessfulAnalysis(); @@ -316,6 +315,16 @@ export class NgtscProgram implements api.Program { throw new Error('Method not implemented.'); } + private scanForMwp(sf: ts.SourceFile): void { + this.mwpScanner !.scan(sf, { + addTypeReplacement: (node: ts.Declaration, type: Type): void => { + // Only obtain the return type transform for the source file once there's a type to replace, + // so that no transform is allocated when there's nothing to do. + this.dtsTransforms !.getReturnTypeTransform(sf).addTypeReplacement(node, type); + } + }); + } + private ensureAnalyzed(): TraitCompiler { if (this.compilation === undefined) { const analyzeSpan = this.perfRecorder.start('analyze'); @@ -326,11 +335,14 @@ export class NgtscProgram implements api.Program { } const analyzeFileSpan = this.perfRecorder.start('analyzeFile', sf); this.compilation !.analyzeSync(sf); + this.scanForMwp(sf); this.perfRecorder.stop(analyzeFileSpan); } this.perfRecorder.stop(analyzeSpan); this.compilation.resolve(); + this.recordNgModuleScopeDependencies(); + // At this point, analysis is complete and the compiler can now calculate which files need to // be emitted, so do that. this.incrementalDriver.recordSuccessfulAnalysis(); @@ -391,9 +403,9 @@ export class NgtscProgram implements api.Program { afterDeclarationsTransforms.push(aliasTransformFactory(compilation.exportStatements)); } - if (this.factoryToSourceInfo !== null) { + if (this.factoryTracker !== null) { beforeTransforms.push( - generatedFactoryTransform(this.factoryToSourceInfo, this.importRewriter)); + generatedFactoryTransform(this.factoryTracker.sourceInfo, this.importRewriter)); } beforeTransforms.push(ivySwitchTransform); if (customTransforms && customTransforms.beforeTs) { @@ -609,10 +621,10 @@ export class NgtscProgram implements api.Program { const localMetaRegistry = new LocalMetadataRegistry(); const localMetaReader: MetadataReader = localMetaRegistry; const depScopeReader = new MetadataDtsModuleScopeResolver(dtsReader, this.aliasingHost); - const scopeRegistry = new LocalModuleScopeRegistry( + this.scopeRegistry = new LocalModuleScopeRegistry( localMetaReader, depScopeReader, this.refEmitter, this.aliasingHost); - const scopeReader: ComponentScopeReader = scopeRegistry; - const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry]); + const scopeReader: ComponentScopeReader = this.scopeRegistry; + const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, this.scopeRegistry]); this.metaReader = new CompoundMetadataReader([localMetaReader, dtsReader]); @@ -637,8 +649,8 @@ export class NgtscProgram implements api.Program { // Set up the IvyCompilation, which manages state for the Ivy transformer. const handlers: DecoratorHandler[] = [ new ComponentDecoratorHandler( - this.reflector, evaluator, metaRegistry, this.metaReader !, scopeReader, scopeRegistry, - this.isCore, this.resourceManager, this.rootDirs, + this.reflector, evaluator, metaRegistry, this.metaReader !, scopeReader, + this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs, this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false, this.options.enableI18nLegacyMessageIdFormat !== false, this.moduleResolver, this.cycleAnalyzer, this.refEmitter, this.defaultImportTracker, @@ -656,15 +668,57 @@ export class NgtscProgram implements api.Program { this.reflector, this.defaultImportTracker, this.isCore, this.options.strictInjectionParameters || false), new NgModuleDecoratorHandler( - this.reflector, evaluator, this.metaReader, metaRegistry, scopeRegistry, - referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter, + this.reflector, evaluator, this.metaReader, metaRegistry, this.scopeRegistry, + referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter, this.factoryTracker, this.defaultImportTracker, this.closureCompilerEnabled, this.options.i18nInLocale), ]; return new TraitCompiler( - handlers, this.reflector, this.importRewriter, this.incrementalDriver, this.perfRecorder, - this.sourceToFactorySymbols, scopeRegistry, - this.options.compileNonExportedClasses !== false, this.dtsTransforms, this.mwpScanner); + handlers, this.reflector, this.perfRecorder, + this.options.compileNonExportedClasses !== false, this.dtsTransforms); + } + + /** + * Reifies the inter-dependencies of NgModules and the components within their compilation scopes + * into the `IncrementalDriver`'s dependency graph. + */ + private recordNgModuleScopeDependencies() { + const recordSpan = this.perfRecorder.start('recordDependencies'); + for (const scope of this.scopeRegistry !.getCompilationScopes()) { + const file = scope.declaration.getSourceFile(); + const ngModuleFile = scope.ngModule.getSourceFile(); + + // A change to any dependency of the declaration causes the declaration to be invalidated, + // which requires the NgModule to be invalidated as well. + const deps = this.incrementalDriver.getFileDependencies(file); + this.incrementalDriver.trackFileDependencies(deps, ngModuleFile); + + // A change to the NgModule file should cause the declaration itself to be invalidated. + this.incrementalDriver.trackFileDependency(ngModuleFile, file); + + // A change to any directive/pipe in the compilation scope should cause the declaration to be + // invalidated. + for (const directive of scope.directives) { + const dirSf = directive.ref.node.getSourceFile(); + + // When a directive in scope is updated, the declaration needs to be recompiled as e.g. + // a selector may have changed. + this.incrementalDriver.trackFileDependency(dirSf, file); + + // When any of the dependencies of the declaration changes, the NgModule scope may be + // affected so a component within scope must be recompiled. Only components need to be + // recompiled, as directives are not dependent upon the compilation scope. + if (directive.isComponent) { + this.incrementalDriver.trackFileDependencies(deps, dirSf); + } + } + for (const pipe of scope.pipes) { + // When a pipe in scope is updated, the declaration needs to be recompiled as e.g. + // the pipe's name may have changed. + this.incrementalDriver.trackFileDependency(pipe.ref.node.getSourceFile(), file); + } + } + this.perfRecorder.stop(recordSpan); } private get reflector(): TypeScriptReflectionHost { diff --git a/packages/compiler-cli/src/ngtsc/shims/index.ts b/packages/compiler-cli/src/ngtsc/shims/index.ts index 96d6e22bd1..7d05c29a8e 100644 --- a/packages/compiler-cli/src/ngtsc/shims/index.ts +++ b/packages/compiler-cli/src/ngtsc/shims/index.ts @@ -9,6 +9,7 @@ /// export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator'; +export {FactoryTracker} from './src/factory_tracker'; export {GeneratedShimsHostWrapper, ShimGenerator} from './src/host'; export {SummaryGenerator} from './src/summary_generator'; export {TypeCheckShimGenerator} from './src/typecheck_shim'; diff --git a/packages/compiler-cli/src/ngtsc/shims/src/factory_tracker.ts b/packages/compiler-cli/src/ngtsc/shims/src/factory_tracker.ts new file mode 100644 index 0000000000..a9b58b37e4 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/shims/src/factory_tracker.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright Google Inc. 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 {FactoryGenerator, FactoryInfo} from './factory_generator'; + +/** + * Maintains a mapping of which symbols in a .ngfactory file have been used. + * + * .ngfactory files are generated with one symbol per defined class in the source file, regardless + * of whether the classes in the source files are NgModules (because that isn't known at the time + * the factory files are generated). The `FactoryTracker` exists to support removing factory symbols + * which didn't end up being NgModules, by tracking the ones which are. + */ +export class FactoryTracker { + readonly sourceInfo = new Map(); + private sourceToFactorySymbols = new Map>(); + + constructor(generator: FactoryGenerator) { + generator.factoryFileMap.forEach((sourceFilePath, factoryPath) => { + const moduleSymbolNames = new Set(); + this.sourceToFactorySymbols.set(sourceFilePath, moduleSymbolNames); + this.sourceInfo.set(factoryPath, {sourceFilePath, moduleSymbolNames}); + }); + } + + track(sf: ts.SourceFile, factorySymbolName: string): void { + if (this.sourceToFactorySymbols.has(sf.fileName)) { + this.sourceToFactorySymbols.get(sf.fileName) !.add(factorySymbolName); + } + } +} diff --git a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel index 4a528c2b6f..4cbfd86fd9 100644 --- a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel @@ -11,12 +11,10 @@ ts_library( "//packages/compiler", "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/imports", - "//packages/compiler-cli/src/ngtsc/incremental", "//packages/compiler-cli/src/ngtsc/indexer", "//packages/compiler-cli/src/ngtsc/modulewithproviders", "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", - "//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/translator", "//packages/compiler-cli/src/ngtsc/typecheck", "//packages/compiler-cli/src/ngtsc/util", diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index 4dc1745fe9..58965b5a1c 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -97,13 +97,29 @@ export interface DecoratorHandler { preanalyze?(node: ClassDeclaration, metadata: Readonly): Promise|undefined; /** - * Perform analysis on the decorator/class combination, producing instructions for compilation - * if successful, or an array of diagnostic messages if the analysis fails or the decorator - * isn't valid. + * Perform analysis on the decorator/class combination, extracting information from the class + * required for compilation. + * + * Returns analyzed metadata if successful, or an array of diagnostic messages if the analysis + * fails or the decorator isn't valid. + * + * Analysis should always be a "pure" operation, with no side effects. This is because the + * detect/analysis steps might be skipped for files which have not changed during incremental + * builds. Any side effects required for compilation (e.g. registration of metadata) should happen + * in the `register` phase, which is guaranteed to run even for incremental builds. */ analyze(node: ClassDeclaration, metadata: Readonly, handlerFlags?: HandlerFlags): AnalysisOutput; + /** + * Post-process the analysis of a decorator/class combination and record any necessary information + * in the larger compilation. + * + * Registration always occurs for a given decorator/class, regardless of whether analysis was + * performed directly or whether the analysis results were reused from the previous program. + */ + register?(node: ClassDeclaration, analysis: A): void; + /** * Registers information about the decorator for the indexing phase in a * `IndexingContext`, which stores information about components discovered in the @@ -148,8 +164,6 @@ export interface DetectResult { export interface AnalysisOutput { analysis?: Readonly; diagnostics?: ts.Diagnostic[]; - factorySymbolName?: string; - typeCheck?: boolean; } /** diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index be9728bce6..2933d0f9fd 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -11,12 +11,10 @@ import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ImportRewriter} from '../../imports'; -import {IncrementalDriver} from '../../incremental'; import {IndexingContext} from '../../indexer'; import {ModuleWithProvidersScanner} from '../../modulewithproviders'; import {PerfRecorder} from '../../perf'; import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration} from '../../reflection'; -import {LocalModuleScopeRegistry} from '../../scope'; import {TypeCheckContext} from '../../typecheck'; import {getSourceFile, isExported} from '../../util/src/typescript'; @@ -24,8 +22,6 @@ import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPr import {DtsTransformRegistry} from './declaration'; import {Trait, TraitState} from './trait'; -const EMPTY_ARRAY: any = []; - /** * Records information about a specific class that has matched traits. */ @@ -91,20 +87,18 @@ export class TraitCompiler { */ constructor( private handlers: DecoratorHandler[], - private reflector: ReflectionHost, private importRewriter: ImportRewriter, - private incrementalDriver: IncrementalDriver, private perf: PerfRecorder, - private sourceToFactorySymbols: Map>|null, - private scopeRegistry: LocalModuleScopeRegistry, private compileNonExportedClasses: boolean, - private dtsTransforms: DtsTransformRegistry, private mwpScanner: ModuleWithProvidersScanner) { - } + private reflector: ReflectionHost, private perf: PerfRecorder, + private compileNonExportedClasses: boolean, private dtsTransforms: DtsTransformRegistry) {} analyzeSync(sf: ts.SourceFile): void { this.analyze(sf, false); } - analyzeAsync(sf: ts.SourceFile): Promise|void { return this.analyze(sf, true); } + analyzeAsync(sf: ts.SourceFile): Promise|undefined { return this.analyze(sf, true); } private analyze(sf: ts.SourceFile, preanalyze: false): void; - private analyze(sf: ts.SourceFile, preanalyze: true): Promise|void; - private analyze(sf: ts.SourceFile, preanalyze: boolean): Promise|void { + private analyze(sf: ts.SourceFile, preanalyze: true): Promise|undefined; + private analyze(sf: ts.SourceFile, preanalyze: boolean): Promise|undefined { + // analyze() really wants to return `Promise|void`, but TypeScript cannot narrow a return + // type of 'void', so `undefined` is used instead. const promises: Promise[] = []; const visit = (node: ts.Node): void => { @@ -116,18 +110,10 @@ export class TraitCompiler { visit(sf); - this.mwpScanner.scan(sf, { - addTypeReplacement: (node: ts.Declaration, type: Type): void => { - // Only obtain the return type transform for the source file once there's a type to replace, - // so that no transform is allocated when there's nothing to do. - this.dtsTransforms.getReturnTypeTransform(sf).addTypeReplacement(node, type); - } - }); - if (preanalyze && promises.length > 0) { return Promise.all(promises).then(() => undefined as void); } else { - return; + return undefined; } } @@ -260,13 +246,13 @@ export class TraitCompiler { if (result.diagnostics !== undefined) { trait = trait.toErrored(result.diagnostics); } else if (result.analysis !== undefined) { - trait = trait.toAnalyzed(result.analysis); - - const sf = clazz.getSourceFile(); - if (result.factorySymbolName !== undefined && this.sourceToFactorySymbols !== null && - this.sourceToFactorySymbols.has(sf.fileName)) { - this.sourceToFactorySymbols.get(sf.fileName) !.add(result.factorySymbolName); + // Analysis was successful. Trigger registration. + if (trait.handler.register !== undefined) { + trait.handler.register(clazz, result.analysis); } + + // Successfully analyzed and registered. + trait = trait.toAnalyzed(result.analysis); } else { trait = trait.toSkipped(); } @@ -329,8 +315,6 @@ export class TraitCompiler { } } } - - this.recordNgModuleScopeDependencies(); } typeCheck(ctx: TypeCheckContext): void { @@ -443,43 +427,4 @@ export class TraitCompiler { } get exportStatements(): Map> { return this.reexportMap; } - - private recordNgModuleScopeDependencies() { - const recordSpan = this.perf.start('recordDependencies'); - for (const scope of this.scopeRegistry.getCompilationScopes()) { - const file = scope.declaration.getSourceFile(); - const ngModuleFile = scope.ngModule.getSourceFile(); - - // A change to any dependency of the declaration causes the declaration to be invalidated, - // which requires the NgModule to be invalidated as well. - const deps = this.incrementalDriver.getFileDependencies(file); - this.incrementalDriver.trackFileDependencies(deps, ngModuleFile); - - // A change to the NgModule file should cause the declaration itself to be invalidated. - this.incrementalDriver.trackFileDependency(ngModuleFile, file); - - // A change to any directive/pipe in the compilation scope should cause the declaration to be - // invalidated. - for (const directive of scope.directives) { - const dirSf = directive.ref.node.getSourceFile(); - - // When a directive in scope is updated, the declaration needs to be recompiled as e.g. - // a selector may have changed. - this.incrementalDriver.trackFileDependency(dirSf, file); - - // When any of the dependencies of the declaration changes, the NgModule scope may be - // affected so a component within scope must be recompiled. Only components need to be - // recompiled, as directives are not dependent upon the compilation scope. - if (directive.isComponent) { - this.incrementalDriver.trackFileDependencies(deps, dirSf); - } - } - for (const pipe of scope.pipes) { - // When a pipe in scope is updated, the declaration needs to be recompiled as e.g. - // the pipe's name may have changed. - this.incrementalDriver.trackFileDependency(pipe.ref.node.getSourceFile(), file); - } - } - this.perf.stop(recordSpan); - } }