diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index 343f04c578dac7..cf8a8421f705f4 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationTriggerNames, BoundTarget, compileClassDebugInfo, compileComponentClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, FactoryTarget, makeBindingParser, R3ComponentMetadata, R3DeferBlockMetadata, R3DeferBlockTemplateDependency, R3DirectiveDependencyMetadata, R3NgModuleDependencyMetadata, R3PipeDependencyMetadata, R3TargetBinder, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SchemaMetadata, SelectorMatcher, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstDeferredTrigger, TmplAstElement, ViewEncapsulation, WrappedNodeExpr} from '@angular/compiler'; +import {AnimationTriggerNames, BoundTarget, compileClassDebugInfo, compileComponentClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, DomElementSchemaRegistry, Expression, FactoryTarget, makeBindingParser, R3ComponentMetadata, R3DeferBlockMetadata, R3DeferBlockTemplateDependency, R3DirectiveDependencyMetadata, R3NgModuleDependencyMetadata, R3PipeDependencyMetadata, R3TargetBinder, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SchemaMetadata, SelectorMatcher, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstDeferredTrigger, TmplAstElement, ViewEncapsulation, WrappedNodeExpr} from '@angular/compiler'; import ts from 'typescript'; import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../../cycles'; @@ -477,6 +477,15 @@ export class ComponentDecoratorHandler implements styles.push(...template.styles); } + // Collect all explicitly deferred symbols from the `@Component.deferredImports` field + // if it exists. As a part of that process we also populate the `DeferredSymbolTracker` state. + // This operation is safe in local compilation mode, since it doesn't require + // accessing/resolving symbols outside of the current source file. + let explicitlyDeferredTypes: Map|null = null; + if (metadata.isStandalone && rawDeferredImports !== null) { + explicitlyDeferredTypes = this.collectExplicitlyDeferredSymbols(rawDeferredImports); + } + const output: AnalysisOutput = { analysis: { baseClass: readBaseClass(node, this.reflector, this.evaluator), @@ -524,6 +533,7 @@ export class ComponentDecoratorHandler implements resolvedImports, rawDeferredImports, resolvedDeferredImports, + explicitlyDeferredTypes, schemas, decorator: decorator?.node as ts.Decorator | null ?? null, }, @@ -652,6 +662,43 @@ export class ComponentDecoratorHandler implements resolve( node: ClassDeclaration, analysis: Readonly, symbol: ComponentSymbol): ResolveResult { + const metadata = analysis.meta as Readonly>; + const diagnostics: ts.Diagnostic[] = []; + const context = getSourceFile(node); + + // Check if there are some import declarations that contain symbols used within + // the `@Component.deferredImports` field, but those imports contain other symbols + // and thus the declaration can not be removed. + const nonRemovableImports = this.deferredSymbolTracker.getNonRemovableDeferredImports(context); + if (nonRemovableImports.length > 0) { + for (const importDecl of nonRemovableImports) { + const diagnostic = makeDiagnostic( + ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY, importDecl, + `This import contains symbols used in the \`@Component.deferredImports\` array, ` + + `but also some other symbols, which prevents Angular compiler from ` + + `generating dynamic imports for deferred dependencies. ` + + `To fix this, make sure that this import contains *only* symbols ` + + `that are used within the \`@Component.deferredImports\` array.`); + diagnostics.push(diagnostic); + } + return {diagnostics}; + } + + if (this.compilationMode === CompilationMode.LOCAL) { + return { + data: { + declarationListEmitMode: (!analysis.meta.isStandalone || analysis.rawImports !== null) ? + DeclarationListEmitMode.RuntimeResolved : + DeclarationListEmitMode.Direct, + declarations: EMPTY_ARRAY, + deferBlocks: this.locateDeferBlocksWithoutScope(analysis.template), + deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerComponent, + deferrableDeclToImportDecl: new Map(), + deferrableTypes: new Map(), + }, + }; + } + if (this.semanticDepGraphUpdater !== null && analysis.baseClass instanceof Reference) { symbol.baseClass = this.semanticDepGraphUpdater.getSymbol(analysis.baseClass.node); } @@ -660,18 +707,14 @@ export class ComponentDecoratorHandler implements return {}; } - const context = getSourceFile(node); - const metadata = analysis.meta as Readonly>; - - const data: ComponentResolutionData = { declarations: EMPTY_ARRAY, declarationListEmitMode: DeclarationListEmitMode.Direct, deferBlocks: new Map(), + deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock, deferrableDeclToImportDecl: new Map(), deferrableTypes: new Map(), }; - const diagnostics: ts.Diagnostic[] = []; const scope = this.scopeReader.getScopeForComponent(node); if (scope !== null) { @@ -1047,13 +1090,13 @@ export class ComponentDecoratorHandler implements return []; } - // Collect all explicitly deferred symbols from the `@Component.deferredImports` field - // if it exists. As a part of that process we also populate the `DeferredSymbolTracker` state, - // which is then used within the `collectDeferredSymbols` call. - this.collectExplicitlyDeferredSymbols(analysis); const deferrableTypes = this.collectDeferredSymbols(resolution); - const meta: R3ComponentMetadata = {...analysis.meta, ...resolution}; + const meta: R3ComponentMetadata = { + ...analysis.meta, + ...resolution, + deferrableTypes, + }; const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component)); removeDeferrableTypesFromComponentDecorator(analysis, deferrableTypes); @@ -1100,24 +1143,25 @@ export class ComponentDecoratorHandler implements compileLocal( node: ClassDeclaration, analysis: Readonly, - pool: ConstantPool): CompileResult[] { + resolution: Readonly, pool: ConstantPool): CompileResult[] { if (analysis.template.errors !== null && analysis.template.errors.length > 0) { return []; } - const deferrableTypes = this.collectExplicitlyDeferredSymbols(analysis); + // In the local compilation mode we can only rely on the information available + // within the `@Component.deferredImports` array, because in this mode compiler + // doesn't have information on which dependencies belong to which defer blocks. + const deferrableTypes = analysis.explicitlyDeferredTypes; + const meta: R3ComponentMetadata = { ...analysis.meta, - declarationListEmitMode: (!analysis.meta.isStandalone || analysis.rawImports !== null) ? - DeclarationListEmitMode.RuntimeResolved : - DeclarationListEmitMode.Direct, - declarations: EMPTY_ARRAY, - deferBlocks: this.locateDeferBlocksWithoutScope(analysis.template), - deferrableDeclToImportDecl: new Map(), - deferrableTypes, + ...resolution, + deferrableTypes: deferrableTypes ?? new Map(), }; - removeDeferrableTypesFromComponentDecorator(analysis, deferrableTypes); + if (analysis.explicitlyDeferredTypes !== null) { + removeDeferrableTypesFromComponentDecorator(analysis, analysis.explicitlyDeferredTypes); + } const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component)); const def = compileComponentFromMetadata(meta, pool, makeBindingParser()); @@ -1185,14 +1229,13 @@ export class ComponentDecoratorHandler implements /** * Collects a list of deferrable symbols based on the `@Component.deferredImports` field. */ - private collectExplicitlyDeferredSymbols(analysis: Readonly): - Map { + private collectExplicitlyDeferredSymbols(rawDeferredImports: ts.Expression): Map { const deferrableTypes = new Map(); - if (!analysis.meta.isStandalone || analysis.rawDeferredImports === null || - !ts.isArrayLiteralExpression(analysis.rawDeferredImports)) + if (!ts.isArrayLiteralExpression(rawDeferredImports)) { return deferrableTypes; + } - for (const element of analysis.rawDeferredImports.elements) { + for (const element of rawDeferredImports.elements) { const node = tryUnwrapForwardRef(element, this.reflector) || element; if (!ts.isIdentifier(node)) { @@ -1203,7 +1246,7 @@ export class ComponentDecoratorHandler implements const imp = this.reflector.getImportOfIdentifier(node); if (imp !== null) { deferrableTypes.set(imp.name, imp.from); - this.deferredSymbolTracker.markAsExplicitlyDeferred(imp.node); + this.deferredSymbolTracker.markAsDeferrableCandidate(node, imp.node, true); } } return deferrableTypes; @@ -1380,11 +1423,7 @@ export class ComponentDecoratorHandler implements resolutionData.deferrableDeclToImportDecl.set( decl.node as unknown as Expression, imp.node as unknown as Expression); - if (isDeferredImport) { - this.deferredSymbolTracker.markAsExplicitlyDeferred(imp.node); - } else { - this.deferredSymbolTracker.markAsDeferrableCandidate(node, imp.node); - } + this.deferredSymbolTracker.markAsDeferrableCandidate(node, imp.node, isDeferredImport); } } diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts index 1302530bfeb0e7..1a6cc81a679795 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts @@ -25,7 +25,7 @@ import {ParsedTemplateWithSource, StyleUrlMeta} from './resources'; export type ComponentMetadataResolvedFields = SubsetOfKeys< R3ComponentMetadata, 'declarations'|'declarationListEmitMode'|'deferBlocks'|'deferrableDeclToImportDecl'| - 'deferrableTypes'>; + 'deferrableTypes'|'deferBlockDepsEmitMode'>; export interface ComponentAnalysisData { /** @@ -74,6 +74,11 @@ export interface ComponentAnalysisData { rawDeferredImports: ts.Expression|null; resolvedDeferredImports: Reference[]|null; + /** + * Map of symbol name -> import path for types from `@Component.deferredImports` field. + */ + explicitlyDeferredTypes: Map|null; + schemas: SchemaMetadata[]|null; decorator: ts.Decorator|null; diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts index c69d809547eae4..f8b3ea20948d41 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts @@ -186,6 +186,10 @@ export class DirectiveDecoratorHandler implements resolve(node: ClassDeclaration, analysis: DirectiveHandlerData, symbol: DirectiveSymbol): ResolveResult { + if (this.compilationMode === CompilationMode.LOCAL) { + return {}; + } + if (this.semanticDepGraphUpdater !== null && analysis.baseClass instanceof Reference) { symbol.baseClass = this.semanticDepGraphUpdater.getSymbol(analysis.baseClass.node); } @@ -246,7 +250,7 @@ export class DirectiveDecoratorHandler implements compileLocal( node: ClassDeclaration, analysis: Readonly, - pool: ConstantPool): CompileResult[] { + resolution: Readonly, pool: ConstantPool): CompileResult[] { const fac = compileNgFactoryDefField(toFactoryMetadata(analysis.meta, FactoryTarget.Directive)); const def = compileDirectiveFromMetadata(analysis.meta, pool, makeBindingParser()); const inputTransformFields = compileInputTransformFields(analysis.inputs); diff --git a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts index 6b846cbb6b793b..742e0b4cbe0d54 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts @@ -555,6 +555,10 @@ export class NgModuleDecoratorHandler implements resolve(node: ClassDeclaration, analysis: Readonly): ResolveResult { + if (this.compilationMode === CompilationMode.LOCAL) { + return {}; + } + const scope = this.scopeRegistry.getScopeOfModule(node); const diagnostics: ts.Diagnostic[] = []; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index be7b3793bcc7ac..22a7d0df4936c8 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts @@ -96,6 +96,10 @@ export class InjectableDecoratorHandler implements resolve(node: ClassDeclaration, analysis: Readonly, symbol: null): ResolveResult { + if (this.compilationMode === CompilationMode.LOCAL) { + return {}; + } + if (requiresValidCtor(analysis.meta)) { const diagnostic = checkInheritanceOfInjectable( node, this.injectableRegistry, this.reflector, this.evaluator, this.strictCtorDeps, diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts index b0307fe8a9b983..dc6e40dd8958e6 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts @@ -170,6 +170,10 @@ export class PipeDecoratorHandler implements } resolve(node: ClassDeclaration): ResolveResult { + if (this.compilationMode === CompilationMode.LOCAL) { + return {}; + } + const duplicateDeclData = this.scopeRegistry.getDuplicateDeclarations(node); if (duplicateDeclData !== null) { // This pipe was declared twice (or more). diff --git a/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts b/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts index 2953c5b4173563..32bf660b613e63 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts @@ -13,13 +13,6 @@ import {getContainingImportDeclaration} from '../../reflection/src/typescript'; const AssumeEager = 'AssumeEager'; type AssumeEager = typeof AssumeEager; -/** - * A marker indicating that a symbol from an import declaration - * was referenced in a `@Component.deferredImports` list. - */ -const ExplicitlyDeferred = 'ExplicitlyDeferred'; -type ExplicitlyDeferred = typeof ExplicitlyDeferred; - /** * Maps imported symbol name to a set of locations where the symbols is used * in a source file. @@ -34,7 +27,8 @@ type SymbolMap = Map|AssumeEager>; * in favor of using a dynamic import for cases when defer blocks are used. */ export class DeferredSymbolTracker { - private readonly imports = new Map(); + private readonly imports = new Map(); + private readonly explicitlyDeferredImports = new Set(); constructor( private readonly typeChecker: ts.TypeChecker, @@ -86,32 +80,40 @@ export class DeferredSymbolTracker { } /** - * Marks a given import declaration as explicitly deferred, since it's - * used in the `@Component.deferredImports` field. + * Retrieves a list of import declarations that contain symbols used within + * `@Component.deferredImports`, but those imports can not be removed, since + * there are other symbols imported alongside deferred components. */ - markAsExplicitlyDeferred(importDecl: ts.ImportDeclaration): void { - this.imports.set(importDecl, ExplicitlyDeferred); + getNonRemovableDeferredImports(sourceFile: ts.SourceFile): ts.ImportDeclaration[] { + const affectedImports: ts.ImportDeclaration[] = []; + for (const importDecl of this.explicitlyDeferredImports) { + if (importDecl.getSourceFile() === sourceFile && !this.canDefer(importDecl)) { + affectedImports.push(importDecl); + } + } + return affectedImports; } /** * Marks a given identifier and an associated import declaration as a candidate * for defer loading. */ - markAsDeferrableCandidate(identifier: ts.Identifier, importDecl: ts.ImportDeclaration): void { - if (this.onlyExplicitDeferDependencyImports) { + markAsDeferrableCandidate( + identifier: ts.Identifier, importDecl: ts.ImportDeclaration, + isExplicitlyDeferred: boolean): void { + if (this.onlyExplicitDeferDependencyImports && !isExplicitlyDeferred) { // Ignore deferrable candidates when only explicit deferred imports mode is enabled. // In that mode only dependencies from the `@Component.deferredImports` field are // defer-loadable. return; } - let symbolMap = this.imports.get(importDecl); - - // Do we come across this import as a part of `@Component.deferredImports` already? - if (symbolMap === ExplicitlyDeferred) { - return; + if (isExplicitlyDeferred) { + this.explicitlyDeferredImports.add(importDecl); } + let symbolMap = this.imports.get(importDecl); + // Do we come across this import for the first time? if (!symbolMap) { symbolMap = this.extractImportedSymbols(importDecl); @@ -147,10 +149,6 @@ export class DeferredSymbolTracker { } const symbolsMap = this.imports.get(importDecl)!; - if (symbolsMap === ExplicitlyDeferred) { - return true; - } - for (const [symbol, refs] of symbolsMap) { if (refs === AssumeEager || refs.size > 0) { // There may be still eager references to this symbol. diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index 8bec70c4272ba8..61b8c70dc1ae95 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -202,8 +202,9 @@ export interface DecoratorHandler { * Generates code based on each individual source file without using its * dependencies (suitable for local dev edit/refresh workflow) */ - compileLocal(node: ClassDeclaration, analysis: Readonly, constantPool: ConstantPool): - CompileResult|CompileResult[]; + compileLocal( + node: ClassDeclaration, analysis: Readonly, resolution: Readonly, + constantPool: ConstantPool): CompileResult|CompileResult[]; } /** diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index 270dc7919b21a2..d65c088e604ecf 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -419,12 +419,6 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { } resolve(): void { - // No resolving needed for local compilation (only analysis and compile will be done in this - // mode) - if (this.compilationMode === CompilationMode.LOCAL) { - return; - } - const classes = this.classes.keys(); for (const clazz of classes) { const record = this.classes.get(clazz)!; @@ -465,14 +459,16 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { trait = trait.toResolved(result.data ?? null, result.diagnostics ?? null); - if (result.reexports !== undefined) { - const fileName = clazz.getSourceFile().fileName; - if (!this.reexportMap.has(fileName)) { - this.reexportMap.set(fileName, new Map()); - } - const fileReexports = this.reexportMap.get(fileName)!; - for (const reexport of result.reexports) { - fileReexports.set(reexport.asAlias, [reexport.fromModule, reexport.symbolName]); + if (this.compilationMode !== CompilationMode.LOCAL) { + if (result.reexports !== undefined) { + const fileName = clazz.getSourceFile().fileName; + if (!this.reexportMap.has(fileName)) { + this.reexportMap.set(fileName, new Map()); + } + const fileReexports = this.reexportMap.get(fileName)!; + for (const reexport of result.reexports) { + fileReexports.set(reexport.asAlias, [reexport.fromModule, reexport.symbolName]); + } } } } @@ -484,7 +480,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { * `ts.SourceFile`. */ typeCheck(sf: ts.SourceFile, ctx: TypeCheckContext): void { - if (!this.fileToClasses.has(sf)) { + if (!this.fileToClasses.has(sf) || this.compilationMode === CompilationMode.LOCAL) { return; } @@ -596,24 +592,19 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { for (const trait of record.traits) { let compileRes: CompileResult|CompileResult[]; + if (trait.state !== TraitState.Resolved || containsErrors(trait.analysisDiagnostics) || + containsErrors(trait.resolveDiagnostics)) { + // Cannot compile a trait that is not analyzed, or had any errors in its + // declaration. + continue; + } if (this.compilationMode === CompilationMode.LOCAL) { - if (trait.state !== TraitState.Analyzed || trait.analysis === null || - containsErrors(trait.analysisDiagnostics)) { - // Cannot compile a trait in local mode that is not analyzed, or had any errors in its - // declaration. - continue; - } // `trait.analysis` is non-null asserted here because TypeScript does not recognize that // `Readonly` is nullable (as `unknown` itself is nullable) due to the way that // `Readonly` works. - compileRes = trait.handler.compileLocal(clazz, trait.analysis!, constantPool); + compileRes = + trait.handler.compileLocal(clazz, trait.analysis!, trait.resolution!, constantPool); } else { - if (trait.state !== TraitState.Resolved || containsErrors(trait.analysisDiagnostics) || - containsErrors(trait.resolveDiagnostics)) { - // Cannot compile a trait in global mode that is not resolved, or had any errors in its - // declaration. - continue; - } // `trait.resolution` is non-null asserted below because TypeScript does not recognize that // `Readonly` is nullable (as `unknown` itself is nullable) due to the way that // `Readonly` works. diff --git a/packages/compiler-cli/test/ngtsc/env.ts b/packages/compiler-cli/test/ngtsc/env.ts index 41212f193b985a..1b2beacaca1060 100644 --- a/packages/compiler-cli/test/ngtsc/env.ts +++ b/packages/compiler-cli/test/ngtsc/env.ts @@ -25,7 +25,7 @@ import {setWrapHostForTest} from '../../src/transformers/compiler_host'; type TsConfigOptionsValue = string|boolean|number|null|TsConfigOptionsValue[]|{[key: string]: TsConfigOptionsValue}; -type TsConfigOptions = { +export type TsConfigOptions = { [key: string]: TsConfigOptionsValue; }; diff --git a/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts b/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts index 4e38f058af9324..54596265f5f632 100644 --- a/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts +++ b/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts @@ -12,7 +12,7 @@ import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics'; import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing'; import {loadStandardTestFiles} from '../../src/ngtsc/testing'; -import {NgtscTestEnvironment} from './env'; +import {NgtscTestEnvironment, TsConfigOptions} from './env'; const testFiles = loadStandardTestFiles(); @@ -20,8 +20,7 @@ runInEachFileSystem(() => { describe('local compilation', () => { let env!: NgtscTestEnvironment; - beforeEach(() => { - env = NgtscTestEnvironment.setup(testFiles); + function tsconfig(extraOpts: TsConfigOptions = {}) { const tsconfig: {[key: string]: any} = { extends: '../tsconfig-base.json', compilerOptions: { @@ -30,9 +29,15 @@ runInEachFileSystem(() => { }, angularCompilerOptions: { compilationMode: 'experimental-local', + ...extraOpts, }, }; env.write('tsconfig.json', JSON.stringify(tsconfig, null, 2)); + } + + beforeEach(() => { + env = NgtscTestEnvironment.setup(testFiles); + tsconfig(); }); it('should produce no TS semantic diagnostics', () => { @@ -460,8 +465,8 @@ runInEachFileSystem(() => { env.driveMain(); const jsContents = env.getContents('test.js'); - // If there is no style, don't generate css selectors on elements by setting encapsulation - // to none (=2) + // If there is no style, don't generate css selectors on elements by setting + // encapsulation to none (=2) expect(jsContents).toContain('encapsulation: 2'); }); }); @@ -1148,6 +1153,10 @@ runInEachFileSystem(() => { }); describe('@defer', () => { + beforeEach(() => { + tsconfig({onlyExplicitDeferDependencyImports: true}); + }); + it('should handle `@Component.deferredImports` field', () => { env.write('deferred-a.ts', ` import {Component} from '@angular/core'; @@ -1358,6 +1367,164 @@ runInEachFileSystem(() => { 'import("./deferred-b").then(m => m.DeferredCmpB)], ' + '(DeferredCmpA, DeferredCmpB) => {'); }); + + it('should support importing multiple deferrable deps from a single file ' + + 'and use them within `@Component.deferrableImports` field', + () => { + env.write('deferred-deps.ts', ` + import {Component} from '@angular/core'; + + @Component({ + standalone: true, + selector: 'deferred-cmp-a', + template: 'DeferredCmpA contents', + }) + export class DeferredCmpA { + } + + @Component({ + standalone: true, + selector: 'deferred-cmp-b', + template: 'DeferredCmpB contents', + }) + export class DeferredCmpB { + } + `); + + env.write('test.ts', ` + import {Component} from '@angular/core'; + + // This import brings multiple symbols, but all of them are + // used within @Component.deferredImports, thus this import + // can be removed in favor of dynamic imports. + import {DeferredCmpA, DeferredCmpB} from './deferred-deps'; + + @Component({ + standalone: true, + deferredImports: [DeferredCmpA], + template: \` + @defer { + + } + \`, + }) + export class AppCmpA {} + + @Component({ + standalone: true, + deferredImports: [DeferredCmpB], + template: \` + @defer { + + } + \`, + }) + export class AppCmpB {} + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // Expect that we generate 2 different defer functions + // (one for each component). + expect(jsContents) + .toContain( + 'const AppCmpA_DeferFn = () => [' + + 'import("./deferred-deps").then(m => m.DeferredCmpA)]'); + expect(jsContents) + .toContain( + 'const AppCmpB_DeferFn = () => [' + + 'import("./deferred-deps").then(m => m.DeferredCmpB)]'); + + // Make sure there are no eager imports present in the output. + expect(jsContents).not.toContain(`from './deferred-deps'`); + + // Defer instructions use per-component dependency function. + expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmpA_DeferFn)'); + expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmpB_DeferFn)'); + + // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too. + expect(jsContents) + .toContain( + 'ɵsetClassMetadataAsync(AppCmpA, () => [' + + 'import("./deferred-deps").then(m => m.DeferredCmpA)]'); + expect(jsContents) + .toContain( + 'ɵsetClassMetadataAsync(AppCmpB, () => [' + + 'import("./deferred-deps").then(m => m.DeferredCmpB)]'); + }); + + it('should produce a diagnostic in case imports with symbols used ' + + 'in `deferredImports` can not be removed', + () => { + env.write('deferred-deps.ts', ` + import {Component} from '@angular/core'; + + @Component({ + standalone: true, + selector: 'deferred-cmp-a', + template: 'DeferredCmpA contents', + }) + export class DeferredCmpA { + } + + @Component({ + standalone: true, + selector: 'deferred-cmp-b', + template: 'DeferredCmpB contents', + }) + export class DeferredCmpB { + } + + export function utilityFn() {} + `); + + env.write('test.ts', ` + import {Component} from '@angular/core'; + + // This import can not be removed, since it'd contain + // 'utilityFn' symbol once we remove 'DeferredCmpA' and + // 'DeferredCmpB' and generate a dynamic import for it. + // In this situation compiler produces a diagnostic to + // indicate that. + import {DeferredCmpA, DeferredCmpB, utilityFn} from './deferred-deps'; + + @Component({ + standalone: true, + deferredImports: [DeferredCmpA], + template: \` + @defer { + + } + \`, + }) + export class AppCmpA { + ngOnInit() { + utilityFn(); + } + } + + @Component({ + standalone: true, + deferredImports: [DeferredCmpB], + template: \` + @defer { + + } + \`, + }) + export class AppCmpB {} + `); + + const diags = env.driveDiagnostics(); + expect(diags.length > 0).toBe(true); + + const {code, messageText} = diags[0]; + expect(code).toBe(ngErrorCode(ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY)); + expect(messageText) + .toContain( + 'This import contains symbols used in the `@Component.deferredImports` array'); + }); }); }); }); diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index c22114ad1ca19a..97c210b7939aec 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -21,7 +21,7 @@ import {R3JitReflector} from './render3/r3_jit'; import {compileNgModule, compileNgModuleDeclarationExpression, R3NgModuleMetadata, R3NgModuleMetadataKind, R3SelectorScopeMode} from './render3/r3_module_compiler'; import {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler'; import {createMayBeForwardRefExpression, ForwardRefHandling, getSafePropertyAccessString, MaybeForwardRefExpression, wrapReference} from './render3/util'; -import {DeclarationListEmitMode, R3ComponentMetadata, R3DeferBlockMetadata, R3DirectiveDependencyMetadata, R3DirectiveMetadata, R3HostDirectiveMetadata, R3HostMetadata, R3InputMetadata, R3PipeDependencyMetadata, R3QueryMetadata, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata} from './render3/view/api'; +import {DeclarationListEmitMode, DeferBlockDepsEmitMode, R3ComponentMetadata, R3DeferBlockMetadata, R3DirectiveDependencyMetadata, R3DirectiveMetadata, R3HostDirectiveMetadata, R3HostMetadata, R3InputMetadata, R3PipeDependencyMetadata, R3QueryMetadata, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata} from './render3/view/api'; import {compileComponentFromMetadata, compileDirectiveFromMetadata, ParsedHostBindings, parseHostBindings, verifyHostBindings} from './render3/view/compiler'; import type {BoundTarget} from './render3/view/t2_api'; import {R3TargetBinder} from './render3/view/t2_binder'; @@ -195,6 +195,7 @@ export class CompilerFacadeImpl implements CompilerFacade { deferBlocks, deferrableTypes: new Map(), deferrableDeclToImportDecl: new Map(), + deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock, styles: [...facade.styles, ...template.styles], encapsulation: facade.encapsulation, @@ -477,6 +478,7 @@ function convertDeclareComponentFacadeToMetadata( deferBlocks, deferrableTypes: new Map(), deferrableDeclToImportDecl: new Map(), + deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock, changeDetection: decl.changeDetection ?? ChangeDetectionStrategy.Default, encapsulation: decl.encapsulation ?? ViewEncapsulation.Emulated, diff --git a/packages/compiler/src/render3/r3_class_metadata_compiler.ts b/packages/compiler/src/render3/r3_class_metadata_compiler.ts index 061df903286040..ef390aca8226da 100644 --- a/packages/compiler/src/render3/r3_class_metadata_compiler.ts +++ b/packages/compiler/src/render3/r3_class_metadata_compiler.ts @@ -72,8 +72,8 @@ export function compileClassMetadata(metadata: R3ClassMetadata): o.Expression { * check to tree-shake away this code in production mode. */ export function compileComponentClassMetadata( - metadata: R3ClassMetadata, deferrableTypes: Map): o.Expression { - if (deferrableTypes.size === 0) { + metadata: R3ClassMetadata, deferrableTypes: Map|null): o.Expression { + if (deferrableTypes === null || deferrableTypes.size === 0) { // If there are no deferrable symbols - just generate a regular `setClassMetadata` call. return compileClassMetadata(metadata); } diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 6367d076148e7a..dec1ac43ce8d6b 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -125,6 +125,31 @@ export interface R3DirectiveMetadata { hostDirectives: R3HostDirectiveMetadata[]|null; } +/** + * Defines how dynamic imports for deferred dependencies should be emitted in the + * generated output: + * - either in a function on per-component basis (in case of local compilation) + * - or in a function on per-block basis (in full compilation mode) + */ +export const enum DeferBlockDepsEmitMode { + /** + * Dynamic imports are grouped on per-block basis. + * + * This is used in full compilation mode, when compiler has more information + * about particular dependencies that belong to this block. + */ + PerBlock, + + /** + * Dynamic imports are grouped on per-component basis. + * + * In local compilation, compiler doesn't have enough information to determine + * which deferred dependencies belong to which block. In this case we group all + * dynamic imports into a single file on per-component basis. + */ + PerComponent, +} + /** * Specifies how a list of declaration type references should be emitted into the generated code. */ @@ -247,14 +272,15 @@ export interface R3ComponentMetadata */ deferBlocks: Map; + /** + * Defines how dynamic imports for deferred dependencies should be grouped: + * - either in a function on per-component basis (in case of local compilation) + * - or in a function on per-block basis (in full compilation mode) + */ + deferBlockDepsEmitMode: DeferBlockDepsEmitMode; + /** * Map of deferrable symbol names -> corresponding import paths. - * - * This map is populated **only** in local compilation mode and used by the - * TemplateDefinitionBuilder to produce a defer function that loads - * all dependencies. In full compilation mode this information is defined - * on a `@defer` block level instead and dependency function is generated - * on per-block level. */ deferrableTypes: Map; diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 80cb8a13fb838f..f37ed514cf7b97 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -26,7 +26,7 @@ import {BoundEvent} from '../r3_ast'; import {Identifiers as R3} from '../r3_identifiers'; import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, R3CompiledExpression, typeWithParameters} from '../util'; -import {DeclarationListEmitMode, R3ComponentMetadata, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata, R3TemplateDependency} from './api'; +import {DeclarationListEmitMode, DeferBlockDepsEmitMode, R3ComponentMetadata, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata, R3TemplateDependency} from './api'; import {MIN_STYLING_BINDING_SLOTS_REQUIRED, StylingBuilder, StylingInstructionCall} from './styling_builder'; import {BindingScope, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn, TemplateDefinitionBuilder, ValueConverter} from './template'; import {asLiteral, conditionallyCreateDirectiveBindingLiteral, CONTEXT_NAME, DefinitionMap, getInstructionStatements, getQueryPredicate, Instruction, RENDER_FLAGS, TEMPORARY_NAME, temporaryAllocator} from './util'; @@ -219,11 +219,9 @@ export function compileComponentFromMetadata( // This is the main path currently used in compilation, which compiles the template with the // legacy `TemplateDefinitionBuilder`. - // `deferrableTypes` become available only when local compilation mode is - // activated, in which case we generate a single function with all deferred - // dependencies. let allDeferrableDepsFn: o.ReadVarExpr|null = null; - if (meta.deferBlocks.size > 0 && meta.deferrableTypes.size > 0) { + if (meta.deferBlocks.size > 0 && meta.deferrableTypes.size > 0 && + meta.deferBlockDepsEmitMode === DeferBlockDepsEmitMode.PerComponent) { const fnName = `${templateTypeName}_DeferFn`; allDeferrableDepsFn = createDeferredDepsFunction(constantPool, fnName, meta.deferrableTypes); }