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 be3b67eb3d9672..d4476ebb4e8533 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, 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, 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'; @@ -16,11 +16,11 @@ import {assertSuccessfulReferenceEmit, DeferredSymbolTracker, ImportedFile, Modu import {DependencyTracker} from '../../../incremental/api'; import {extractSemanticTypeParameters, SemanticDepGraphUpdater} from '../../../incremental/semantic_graph'; import {IndexingContext} from '../../../indexer'; -import {DirectiveMeta, extractDirectiveTypeCheckMeta, HostDirectivesResolver, MatchSource, MetadataReader, MetadataRegistry, MetaKind, PipeMeta, ResourceRegistry} from '../../../metadata'; +import {DirectiveMeta, extractDirectiveTypeCheckMeta, HostDirectivesResolver, MatchSource, MetadataReader, MetadataRegistry, MetaKind, NgModuleMeta, PipeMeta, ResourceRegistry} from '../../../metadata'; import {PartialEvaluator} from '../../../partial_evaluator'; import {PerfEvent, PerfRecorder} from '../../../perf'; import {ClassDeclaration, DeclarationNode, Decorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral} from '../../../reflection'; -import {ComponentScopeKind, ComponentScopeReader, DtsModuleScopeResolver, LocalModuleScopeRegistry, makeNotStandaloneDiagnostic, makeUnknownComponentImportDiagnostic, TypeCheckScopeRegistry} from '../../../scope'; +import {ComponentScopeKind, ComponentScopeReader, DtsModuleScopeResolver, LocalModuleScope, LocalModuleScopeRegistry, makeNotStandaloneDiagnostic, makeUnknownComponentImportDiagnostic, StandaloneScope, TypeCheckScopeRegistry} from '../../../scope'; import {makeUnknownComponentDeferredImportDiagnostic} from '../../../scope/src/util'; import {AnalysisOutput, CompilationMode, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../../transform'; import {TypeCheckableDirectiveMeta, TypeCheckContext} from '../../../typecheck/api'; @@ -295,6 +295,8 @@ export class ComponentDecoratorHandler implements } let resolvedImports: Reference[]|null = null; + let resolvedDeferredImports: Reference[]|null = null; + let rawImports: ts.Expression|null = component.get('imports') ?? null; let rawDeferredImports: ts.Expression|null = component.get('deferredImports') ?? null; @@ -316,24 +318,27 @@ export class ComponentDecoratorHandler implements createModuleWithProvidersResolver(this.reflector, this.isCore), forwardRefResolver, ]); - let importDiagnostics: ts.Diagnostic[] = []; - const allImports = [ - [rawImports, false /* isDeferredImport */], - [rawDeferredImports, true /* isDeferredImport */] - ] as Array<[ts.Expression | null, boolean]>; - for (const [rawExpression, isDeferredImport] of allImports) { - if (rawExpression) { - const imported = this.evaluator.evaluate(rawExpression, importResolvers); - const {imports, diagnostics} = - validateAndFlattenComponentImports(imported, rawExpression, isDeferredImport); - - if (!resolvedImports) { - resolvedImports = []; - } - resolvedImports = [...resolvedImports, ...imports]; - importDiagnostics = [...importDiagnostics, ...diagnostics]; - } + const importDiagnostics: ts.Diagnostic[] = []; + + if (rawImports) { + const expr = rawImports; + const imported = this.evaluator.evaluate(expr, importResolvers); + const {imports: flattened, diagnostics} = + validateAndFlattenComponentImports(imported, expr, false /* isDeferred */); + importDiagnostics.push(...diagnostics); + resolvedImports = flattened; + rawImports = expr; + } + + if (rawDeferredImports) { + const expr = rawDeferredImports; + const imported = this.evaluator.evaluate(expr, importResolvers); + const {imports: flattened, diagnostics} = + validateAndFlattenComponentImports(imported, expr, true /* isDeferred */); + importDiagnostics.push(...diagnostics); + resolvedDeferredImports = flattened; + rawDeferredImports = expr; } if (importDiagnostics.length > 0) { @@ -517,6 +522,7 @@ export class ComponentDecoratorHandler implements rawImports, resolvedImports, rawDeferredImports, + resolvedDeferredImports, schemas, decorator: decorator?.node as ts.Decorator | null ?? null, }, @@ -557,12 +563,14 @@ export class ComponentDecoratorHandler implements isStandalone: analysis.meta.isStandalone, isSignal: analysis.meta.isSignal, imports: analysis.resolvedImports, + deferredImports: analysis.resolvedDeferredImports, animationTriggerNames: analysis.animationTriggerNames, schemas: analysis.schemas, decorator: analysis.decorator, assumedToExportProviders: false, ngContentSelectors: analysis.template.ngContentSelectors, preserveWhitespaces: analysis.template.preserveWhitespaces ?? false, + isOnlyDeferred: false, }); this.resourceRegistry.registerResources(analysis.resources, node); @@ -686,35 +694,37 @@ export class ComponentDecoratorHandler implements // is an alternative implementation of template matching which is used for template // type-checking and will eventually replace matching in the TemplateDefinitionBuilder. + const isModuleScope = scope.kind === ComponentScopeKind.NgModule; + // Dependencies coming from the regular `imports` field. + const dependencies = isModuleScope ? scope.compilation.dependencies : scope.dependencies; + // Dependencies from the `@Component.deferredImports` field. + const explicitlyDeferredDependencies = getExplicitlyDeferredDeps(scope); + // Set up the R3TargetBinder, as well as a 'directives' array and a 'pipes' map that are // later fed to the TemplateDefinitionBuilder. First, a SelectorMatcher is constructed to // match directives that are in scope. - const matcher = new SelectorMatcher(); - - const pipes = new Map(); + const binder = createTargetBinder(dependencies); + const pipes = extractPipes(dependencies); - const dependencies = scope.kind === ComponentScopeKind.NgModule ? - scope.compilation.dependencies : - scope.dependencies; + let allDependencies = dependencies; + let deferBlockBinder = binder; - for (const dep of dependencies) { - if (dep.kind === MetaKind.Directive && dep.selector !== null) { - matcher.addSelectables(CssSelector.parse(dep.selector), [dep]); - } else if (dep.kind === MetaKind.Pipe) { - pipes.set(dep.name, dep); - } + // If there are any explicitly deferred dependencies (via `@Component.deferredImports`), + // re-compute the list of dependencies and create a new binder for defer blocks. + if (explicitlyDeferredDependencies.length > 0) { + allDependencies = [...explicitlyDeferredDependencies, ...dependencies]; + deferBlockBinder = createTargetBinder(allDependencies); } // Next, the component template AST is bound using the R3TargetBinder. This produces a // BoundTarget, which is similar to a ts.TypeChecker. - const binder = new R3TargetBinder(matcher); const bound = binder.bind({template: metadata.template.nodes}); // Find all defer blocks used in the template and for each block // bind its own scope. const deferBlocks = new Map>(); for (const deferBlock of bound.getDeferBlocks()) { - deferBlocks.set(deferBlock, binder.bind({template: deferBlock.children})); + deferBlocks.set(deferBlock, deferBlockBinder.bind({template: deferBlock.children})); } // Register all Directives and Pipes used at the top level (outside @@ -748,7 +758,7 @@ export class ComponentDecoratorHandler implements const declarations = new Map(); // Transform the dependencies list, filtering out unused dependencies. - for (const dep of dependencies) { + for (const dep of allDependencies) { // Only emit references to each dependency once. if (declarations.has(dep.ref.node)) { continue; @@ -921,17 +931,16 @@ export class ComponentDecoratorHandler implements data.deferBlocks = this.locateDeferBlocksWithoutScope(metadata.template); } - if (analysis.resolvedImports !== null && - (analysis.rawImports !== null || analysis.rawDeferredImports !== null)) { + if ((analysis.resolvedImports !== null && analysis.rawImports !== null) || + (analysis.resolvedDeferredImports !== null && analysis.rawDeferredImports !== null)) { const allImports = [ - [analysis.rawImports, false /* isDeferredImport */], - [analysis.rawDeferredImports, true /* isDeferredImport */] - ] as Array<[ts.Expression | null, boolean]>; - for (const [imports, isDeferredImport] of allImports) { - if (imports) { + [analysis.rawImports, analysis.resolvedImports, false /* isDeferredImport */], + [analysis.rawDeferredImports, analysis.resolvedDeferredImports, true /* isDeferredImport */] + ] as Array<[ts.Expression | null, Reference[], boolean]>; + for (const [rawImports, resolvedImports, isDeferredImport] of allImports) { + if (rawImports !== null && resolvedImports !== null) { const standaloneDiagnostics = validateStandaloneImports( - analysis.resolvedImports, imports, this.metaReader, this.scopeReader, - isDeferredImport); + resolvedImports, rawImports, this.metaReader, this.scopeReader, isDeferredImport); diagnostics.push(...standaloneDiagnostics); } } @@ -960,12 +969,12 @@ export class ComponentDecoratorHandler implements diagnostics.push(...directiveDiagnostics); } - const hostDirectivesDiagnotics = analysis.hostDirectives && analysis.rawHostDirectives ? + const hostDirectivesDiagnostics = analysis.hostDirectives && analysis.rawHostDirectives ? validateHostDirectives( analysis.rawHostDirectives, analysis.hostDirectives, this.metaReader) : null; - if (hostDirectivesDiagnotics !== null) { - diagnostics.push(...hostDirectivesDiagnotics); + if (hostDirectivesDiagnostics !== null) { + diagnostics.push(...hostDirectivesDiagnostics); } if (diagnostics.length > 0) { @@ -980,9 +989,8 @@ export class ComponentDecoratorHandler implements * For example, this happens in the local compilation mode. */ private locateDeferBlocksWithoutScope(template: ComponentTemplate): - Map { - // TODO: apply correct types instead of `any` - const deferBlocks = new Map(); + Map { + const deferBlocks = new Map(); const directivelessBinder = new R3TargetBinder(new SelectorMatcher()); const bound = directivelessBinder.bind({template: template.nodes}); const deferredBlocks = bound.getDeferBlocks(); @@ -1050,39 +1058,16 @@ export class ComponentDecoratorHandler implements return []; } - // Deferrable symbol name -> corresponding import path. - const deferrableTypes = new Map(); - - // Go over all dependencies of all defer blocks and update the value of - // the `isDeferrable` flag and the `importPath` to reflect the current - // state after visiting all components during the `resolve` phase. - for (const [_, metadata] of resolution.deferBlocks) { - for (const deferBlockDep of metadata.deps) { - const dep = deferBlockDep as unknown as {classDeclaration: ts.ClassDeclaration}; - const classDecl = dep.classDeclaration as unknown as Expression; - const importDecl = resolution.deferrableDeclToImportDecl.get(classDecl) as unknown as - ts.ImportDeclaration ?? - null; - if (importDecl && this.deferredSymbolTracker.canDefer(importDecl)) { - deferBlockDep.isDeferrable = true; - deferBlockDep.importPath = (importDecl.moduleSpecifier as ts.StringLiteral).text; - - deferrableTypes.set(deferBlockDep.symbolName, deferBlockDep.importPath); - } - } - } + // 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 fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component)); - if (analysis.classMetadata) { - // Drop references to existing imports for deferrable symbols that should be present - // in the `setClassMetadataAsync` call. Otherwise, an import declaration gets retained. - const deferrableSymbols = new Set(deferrableTypes.keys()); - const rewrittenDecoratorsNode = removeIdentifierReferences( - (analysis.classMetadata.decorators as WrappedNodeExpr).node, deferrableSymbols); - analysis.classMetadata.decorators = new WrappedNodeExpr(rewrittenDecoratorsNode); - } + removeDeferrableTypesFromDecorator(analysis, deferrableTypes); const def = compileComponentFromMetadata(meta, pool, makeBindingParser()); const inputTransformFields = compileInputTransformFields(analysis.inputs); @@ -1131,6 +1116,7 @@ export class ComponentDecoratorHandler implements return []; } + const deferrableTypes = this.collectExplicitlyDeferredSymbols(analysis); const meta: R3ComponentMetadata = { ...analysis.meta, declarationListEmitMode: (!analysis.meta.isStandalone || analysis.rawImports !== null) ? @@ -1139,49 +1125,81 @@ export class ComponentDecoratorHandler implements declarations: EMPTY_ARRAY, deferBlocks: this.locateDeferBlocksWithoutScope(analysis.template), deferrableDeclToImportDecl: new Map(), - - // Deferrable symbol name -> corresponding import path. - deferrableTypes: new Map(), + deferrableTypes, }; - // TODO: move to a separate function - // TODO: produce a diagnostic when: - // - a symbol from the `deferredImports` is used outside of `@defer` blocks, - // either in a template as an eager component or in TS code (e.g. in a @ViewChild) - // - when an import that brings a deferred symbol **also** contains some other symbols, - // thus we can not drop it - const deferrableImports = new Set(); - if (analysis.meta.isStandalone && analysis.rawDeferredImports !== null && - ts.isArrayLiteralExpression(analysis.rawDeferredImports)) { - for (const element of analysis.rawDeferredImports.elements) { - const node = tryUnwrapForwardRef(element, this.reflector) || element; - - if (!ts.isIdentifier(node)) { - // Can't defer-load non-literal references. - continue; - } - - const imp = this.reflector.getImportOfIdentifier(node); - if (imp !== null) { - deferrableImports.add(imp.node); - meta.deferrableTypes.set(imp.name, imp.from); - } - } - } + removeDeferrableTypesFromDecorator(analysis, deferrableTypes); const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component)); const def = compileComponentFromMetadata(meta, pool, makeBindingParser()); const inputTransformFields = compileInputTransformFields(analysis.inputs); const classMetadata = analysis.classMetadata !== null ? - compileComponentClassMetadata(analysis.classMetadata, meta.deferrableTypes).toStmt() : + compileComponentClassMetadata(analysis.classMetadata, deferrableTypes).toStmt() : null; const debugInfo = analysis.classDebugInfo !== null ? compileClassDebugInfo(analysis.classDebugInfo).toStmt() : null; + const deferrableImports = this.deferredSymbolTracker.getDeferrableImportDecls(); return compileResults( fac, def, classMetadata, 'ɵcmp', inputTransformFields, deferrableImports, debugInfo); } + /** + * Collects a list of deferrable symbols based on the `@Component.deferredImports` field. + */ + private collectExplicitlyDeferredSymbols(analysis: Readonly): + Map { + const deferrableTypes = new Map(); + if (!analysis.meta.isStandalone || analysis.rawDeferredImports === null || + !ts.isArrayLiteralExpression(analysis.rawDeferredImports)) + return deferrableTypes; + + for (const element of analysis.rawDeferredImports.elements) { + const node = tryUnwrapForwardRef(element, this.reflector) || element; + + if (!ts.isIdentifier(node)) { + // Can't defer-load non-literal references. + continue; + } + + const imp = this.reflector.getImportOfIdentifier(node); + if (imp !== null) { + deferrableTypes.set(imp.name, imp.from); + this.deferredSymbolTracker.markAsExplicitlyDeferred(imp.node); + } + } + return deferrableTypes; + } + + /** + * Computes a list of deferrable symbols based on dependencies from + * the `@Component.imports` field and their usage in `@defer` blocks. + */ + private collectDeferredSymbols(resolution: Readonly): + Map { + const deferrableTypes = new Map(); + // Go over all dependencies of all defer blocks and update the value of + // the `isDeferrable` flag and the `importPath` to reflect the current + // state after visiting all components during the `resolve` phase. + for (const [_, metadata] of resolution.deferBlocks) { + for (const deferBlockDep of metadata.deps) { + const dep = deferBlockDep as unknown as { + classDeclaration: ts.ClassDeclaration; + }; + const classDecl = dep.classDeclaration as unknown as Expression; + const importDecl = resolution.deferrableDeclToImportDecl.get(classDecl) as unknown as + ts.ImportDeclaration ?? + null; + if (importDecl && this.deferredSymbolTracker.canDefer(importDecl)) { + deferBlockDep.isDeferrable = true; + deferBlockDep.importPath = (importDecl.moduleSpecifier as ts.StringLiteral).text; + deferrableTypes.set(deferBlockDep.symbolName, deferBlockDep.importPath); + } + } + } + return deferrableTypes; + } + /** * Check whether adding an import from `origin` to the source-file corresponding to `expr` would * create a cyclic import. @@ -1271,12 +1289,12 @@ export class ComponentDecoratorHandler implements if (analysisData.meta.isStandalone) { if (analysisData.rawImports !== null) { this.registerDeferrableCandidates( - analysisData.rawImports, true /* isRegularImport */, allDeferredDecls, eagerlyUsedDecls, - resolutionData); + analysisData.rawImports, false /* isDeferredImport */, allDeferredDecls, + eagerlyUsedDecls, resolutionData); } if (analysisData.rawDeferredImports !== null) { this.registerDeferrableCandidates( - analysisData.rawDeferredImports, false /* isRegularImport */, allDeferredDecls, + analysisData.rawDeferredImports, true /* isDeferredImport */, allDeferredDecls, eagerlyUsedDecls, resolutionData); } } @@ -1288,8 +1306,9 @@ export class ComponentDecoratorHandler implements * candidates. */ private registerDeferrableCandidates( - importsExpr: ts.Expression, isRegularImport: boolean, allDeferredDecls: Set, - eagerlyUsedDecls: Set, resolutionData: ComponentResolutionData) { + importsExpr: ts.Expression, isDeferredImport: boolean, + allDeferredDecls: Set, eagerlyUsedDecls: Set, + resolutionData: ComponentResolutionData) { if (!ts.isArrayLiteralExpression(importsExpr)) { return; } @@ -1321,9 +1340,6 @@ export class ComponentDecoratorHandler implements // Are we even trying to defer-load this symbol? if (!allDeferredDecls.has(decl.node)) { - // TODO: if `isRegularImport` is `false` - produce a diagnostic that - // a symbol from `deferredImports` is used outside of `@defer` block, - // so it will **not** be defer-loaded. continue; } @@ -1355,9 +1371,11 @@ export class ComponentDecoratorHandler implements resolutionData.deferrableDeclToImportDecl.set( decl.node as unknown as Expression, imp.node as unknown as Expression); - // We're okay deferring this reference to the imported symbol. - this.deferredSymbolTracker.markAsDeferrableCandidate( - node, imp.node, isRegularImport /* usedInRegularImports */); + if (isDeferredImport) { + this.deferredSymbolTracker.markAsExplicitlyDeferred(imp.node); + } else { + this.deferredSymbolTracker.markAsDeferrableCandidate(node, imp.node); + } } } @@ -1373,6 +1391,53 @@ export class ComponentDecoratorHandler implements } } +/** + * Creates an instance of a target binder based on provided dependencies. + */ +function createTargetBinder(dependencies: Array) { + const matcher = new SelectorMatcher(); + for (const dep of dependencies) { + if (dep.kind === MetaKind.Directive && dep.selector !== null) { + matcher.addSelectables(CssSelector.parse(dep.selector), [dep]); + } + } + return new R3TargetBinder(matcher); +} + +/** + * Returns the list of dependencies from `@Component.deferredImports` if provided. + */ +function getExplicitlyDeferredDeps(scope: LocalModuleScope|StandaloneScope) { + return scope.kind === ComponentScopeKind.NgModule ? + [] : + (scope as StandaloneScope).deferredDependencies; +} + +function extractPipes(dependencies: Array): + Map { + const pipes = new Map(); + for (const dep of dependencies) { + if (dep.kind === MetaKind.Pipe) { + pipes.set(dep.name, dep); + } + } + return pipes; +} + +/** + * Drop references to existing imports for deferrable symbols that should be present + * in the `setClassMetadataAsync` call. Otherwise, an import declaration gets retained. + */ +function removeDeferrableTypesFromDecorator( + analysis: Readonly, deferrableTypes: Map) { + if (analysis.classMetadata) { + const deferrableSymbols = new Set(deferrableTypes.keys()); + const rewrittenDecoratorsNode = removeIdentifierReferences( + (analysis.classMetadata.decorators as WrappedNodeExpr).node, deferrableSymbols); + analysis.classMetadata.decorators = new WrappedNodeExpr(rewrittenDecoratorsNode); + } +} + function validateStandaloneImports( importRefs: Reference[], importExpr: ts.Expression, metaReader: MetadataReader, scopeReader: ComponentScopeReader, 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 a4de4c5d4e671b..1302530bfeb0e7 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts @@ -72,6 +72,7 @@ export interface ComponentAnalysisData { rawImports: ts.Expression|null; resolvedImports: Reference[]|null; rawDeferredImports: ts.Expression|null; + resolvedDeferredImports: Reference[]|null; schemas: SchemaMetadata[]|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 b17630770cbf23..f3fbfbb46ddc51 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts @@ -168,6 +168,7 @@ export class DirectiveDecoratorHandler implements isStandalone: analysis.meta.isStandalone, isSignal: analysis.meta.isSignal, imports: null, + deferredImports: null, schemas: null, ngContentSelectors: null, decorator: analysis.decorator, @@ -175,6 +176,7 @@ export class DirectiveDecoratorHandler implements // Directives analyzed within our own compilation are not _assumed_ to export providers. // Instead, we statically analyze their imports to make a direct determination. assumedToExportProviders: false, + isOnlyDeferred: false, }); this.injectableRegistry.registerInjectable(node, { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts index 53604937f94585..ef00d7a17b2d07 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts @@ -161,6 +161,7 @@ export class PipeDecoratorHandler implements nameExpr: analysis.pipeNameExpr, isStandalone: analysis.meta.isStandalone, decorator: analysis.decorator, + isOnlyDeferred: false, }); this.injectableRegistry.registerInjectable(node, { diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts index f9863c96edf944..7c0f88718a9ef1 100644 --- a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts @@ -308,6 +308,16 @@ export enum ErrorCode { */ CONTROL_FLOW_PREVENTING_CONTENT_PROJECTION = 8011, + /** + * TODO: add docs. + */ + DEFERRED_PIPE_USED_EAGERLY = 8012, + + /** + * TODO: add docs. + */ + DEFERRED_DIRECTIVE_USED_EAGERLY = 8013, + /** * A two way binding in a template has an incorrect syntax, * parentheses outside brackets. For example: 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 859600e38411bc..23023620148d0b 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,6 +13,9 @@ import {getContainingImportDeclaration} from '../../reflection/src/typescript'; const AssumeEager = 'AssumeEager'; type AssumeEager = typeof AssumeEager; +const ExplicitlyDeferred = 'ExplicitlyDeferred'; +type ExplicitlyDeferred = typeof ExplicitlyDeferred; + /** * Allows to register a symbol as deferrable and keep track of its usage. * @@ -21,14 +24,8 @@ type AssumeEager = typeof AssumeEager; * in favor of using a dynamic import for cases when defer blocks are used. */ export class DeferredSymbolTracker { - private readonly imports = - new Map|AssumeEager>>(); - - // Set of import declarations used within the `@Component.imports` field. - // We need this information to detect whether a given reference can be defer-loadable, - // when we run in a mode when auto-extraction is disabled. In that mode we want to - // make sure that those symbols are *not* used within the `@Component.imports` field. - private readonly usedInRegularImports = new Set(); + private readonly imports = new Map< + ts.ImportDeclaration, ExplicitlyDeferred|Map|AssumeEager>>(); constructor( private readonly typeChecker: ts.TypeChecker, @@ -79,24 +76,35 @@ export class DeferredSymbolTracker { return symbolMap; } + /** + * TODO: add docs. + */ + markAsExplicitlyDeferred(importDecl: ts.ImportDeclaration): void { + this.imports.set(importDecl, ExplicitlyDeferred); + } + /** * Marks a given identifier and an associated import declaration as a candidate * for defer loading. */ - markAsDeferrableCandidate( - identifier: ts.Identifier, importDecl: ts.ImportDeclaration, - usedInRegularImports: boolean): void { - // Do we come across this import for the first time? - if (!this.imports.has(importDecl)) { - const symbolMap = this.extractImportedSymbols(importDecl); - this.imports.set(importDecl, symbolMap); + markAsDeferrableCandidate(identifier: ts.Identifier, importDecl: ts.ImportDeclaration): void { + if (this.onlyExplicitDeferDependencyImports) { + // TODO: add a comment. + return; } - if (usedInRegularImports) { - this.usedInRegularImports.add(importDecl); + let symbolMap = this.imports.get(importDecl); + + // Do we come across this import as a part of `@Component.deferredImports` already? + if (symbolMap === ExplicitlyDeferred) { + return; } - const symbolMap = this.imports.get(importDecl)!; + // Do we come across this import for the first time? + if (!symbolMap) { + symbolMap = this.extractImportedSymbols(importDecl); + this.imports.set(importDecl, symbolMap); + } if (!symbolMap.has(identifier.text)) { throw new Error( @@ -127,6 +135,10 @@ 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. @@ -134,13 +146,6 @@ export class DeferredSymbolTracker { } } - // If we only require explicit dependencies and symbols from this import - // declaration were used in the `@Component.imports` field - treat this - // import declaration as non-deferrable. - if (this.onlyExplicitDeferDependencyImports && this.usedInRegularImports.has(importDecl)) { - return false; - } - return true; } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts index 968f464a99a85f..6b6fdaf6d0175f 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts @@ -203,6 +203,12 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta { */ imports: Reference[]|null; + /** + * For standalone components, the list of imported types that can be used + * in `@defer` blocks (when only explicit dependencies are allowed). + */ + deferredImports: Reference[]|null; + /** * For standalone components, the list of schemas declared. */ @@ -222,6 +228,8 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta { * Whether the directive should be assumed to export providers if imported as a standalone type. */ assumedToExportProviders: boolean; + + isOnlyDeferred: boolean; } /** Metadata collected about an additional directive that is being applied to a directive host. */ @@ -268,6 +276,7 @@ export interface PipeMeta { nameExpr: ts.Expression|null; isStandalone: boolean; decorator: ts.Decorator|null; + isOnlyDeferred: boolean; } /** diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts index 3f3d4ce29ce642..143539ca62910a 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts @@ -135,6 +135,7 @@ export class DtsMetadataReader implements MetadataReader { // Imports are tracked in metadata only for template type-checking purposes, // so standalone components from .d.ts files don't have any. imports: null, + deferredImports: null, // The same goes for schemas. schemas: null, decorator: null, @@ -143,6 +144,7 @@ export class DtsMetadataReader implements MetadataReader { // `preserveWhitespaces` isn't encoded in the .d.ts and is only // used to increase the accuracy of a diagnostic. preserveWhitespaces: false, + isOnlyDeferred: false, }; } @@ -178,6 +180,7 @@ export class DtsMetadataReader implements MetadataReader { nameExpr: null, isStandalone, decorator: null, + isOnlyDeferred: false, }; } } diff --git a/packages/compiler-cli/src/ngtsc/scope/src/api.ts b/packages/compiler-cli/src/ngtsc/scope/src/api.ts index 592482e4801042..3a1716e28a1180 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/api.ts @@ -70,6 +70,7 @@ export interface LocalModuleScope extends ExportScope { export interface StandaloneScope { kind: ComponentScopeKind.Standalone; dependencies: Array; + deferredDependencies: Array; component: ClassDeclaration; schemas: SchemaMetadata[]; isPoisoned: boolean; diff --git a/packages/compiler-cli/src/ngtsc/scope/src/standalone.ts b/packages/compiler-cli/src/ngtsc/scope/src/standalone.ts index 13ad60126e5287..035f0273e427c9 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/standalone.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/standalone.ts @@ -38,6 +38,7 @@ export class StandaloneComponentScopeReader implements ComponentScopeReader { // A standalone component always has itself in scope, so add `clazzMeta` during // initialization. const dependencies = new Set([clazzMeta]); + const deferredDependencies = new Set(); const seen = new Set([clazz]); let isPoisoned = clazzMeta.isPoisoned; @@ -95,10 +96,29 @@ export class StandaloneComponentScopeReader implements ComponentScopeReader { } } + if (clazzMeta.deferredImports !== null) { + for (const ref of clazzMeta.deferredImports) { + const dirMeta = this.metaReader.getDirectiveMetadata(ref); + if (dirMeta !== null) { + deferredDependencies.add({...dirMeta, ref, isOnlyDeferred: true}); + isPoisoned = isPoisoned || dirMeta.isPoisoned || !dirMeta.isStandalone; + continue; + } + + const pipeMeta = this.metaReader.getPipeMetadata(ref); + if (pipeMeta !== null) { + deferredDependencies.add({...pipeMeta, ref, isOnlyDeferred: true}); + isPoisoned = isPoisoned || !pipeMeta.isStandalone; + continue; + } + } + } + this.cache.set(clazz, { kind: ComponentScopeKind.Standalone, component: clazz, dependencies: Array.from(dependencies), + deferredDependencies: Array.from(deferredDependencies), isPoisoned, schemas: clazzMeta.schemas ?? [], }); diff --git a/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts b/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts index ace986ea88da5d..39581621d3e5cb 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts @@ -10,7 +10,7 @@ import {CssSelector, SchemaMetadata, SelectorMatcher} from '@angular/compiler'; import ts from 'typescript'; import {Reference} from '../../imports'; -import {DirectiveMeta, flattenInheritedDirectiveMetadata, HostDirectivesResolver, MetadataReader, MetaKind} from '../../metadata'; +import {DirectiveMeta, flattenInheritedDirectiveMetadata, HostDirectivesResolver, MetadataReader, MetaKind, PipeMeta} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; import {ComponentScopeKind, ComponentScopeReader} from './api'; @@ -33,7 +33,7 @@ export interface TypeCheckScope { /** * The pipes that are available in the compilation scope. */ - pipes: Map>>; + pipes: Map; /** * The schemas that are used in this scope. @@ -74,7 +74,7 @@ export class TypeCheckScopeRegistry { getTypeCheckScope(node: ClassDeclaration): TypeCheckScope { const matcher = new SelectorMatcher(); const directives: DirectiveMeta[] = []; - const pipes = new Map>>(); + const pipes = new Map(); const scope = this.scopeReader.getScopeForComponent(node); if (scope === null) { @@ -87,31 +87,40 @@ export class TypeCheckScopeRegistry { }; } - const cacheKey = scope.kind === ComponentScopeKind.NgModule ? scope.ngModule : scope.component; - const dependencies = scope.kind === ComponentScopeKind.NgModule ? - scope.compilation.dependencies : - scope.dependencies; + const isNgModuleScope = scope.kind === ComponentScopeKind.NgModule; + const cacheKey = isNgModuleScope ? scope.ngModule : scope.component; + const dependencies = isNgModuleScope ? scope.compilation.dependencies : scope.dependencies; if (this.scopeCache.has(cacheKey)) { return this.scopeCache.get(cacheKey)!; } - for (const meta of dependencies) { + let allDependencies = dependencies; + if (!isNgModuleScope && Array.isArray(scope.deferredDependencies) && + scope.deferredDependencies.length > 0) { + // TODO: what happens with duplicates? Should take care of it in handler.ts. + allDependencies = [...allDependencies, ...scope.deferredDependencies]; + } + for (const meta of allDependencies) { if (meta.kind === MetaKind.Directive && meta.selector !== null) { const extMeta = this.getTypeCheckDirectiveMetadata(meta.ref); if (extMeta === null) { continue; } + // TODO: rename + const contextualizedMeta = {...extMeta, isOnlyDeferred: meta.isOnlyDeferred}; matcher.addSelectables( CssSelector.parse(meta.selector), - [...this.hostDirectivesResolver.resolve(extMeta), extMeta]); - directives.push(extMeta); + [...this.hostDirectivesResolver.resolve(contextualizedMeta), contextualizedMeta]); + + // Carry over the `isOnlyDeferred` flag from the dependency info. + directives.push(contextualizedMeta); } else if (meta.kind === MetaKind.Pipe) { if (!ts.isClassDeclaration(meta.ref.node)) { throw new Error(`Unexpected non-class declaration ${ ts.SyntaxKind[meta.ref.node.kind]} for pipe ${meta.ref.debugName}`); } - pipes.set(meta.name, meta.ref as Reference>); + pipes.set(meta.name, {...meta, isOnlyDeferred: meta.isOnlyDeferred}); } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts index 11e82931d560f8..65d035cb64ba50 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts @@ -11,7 +11,7 @@ import ts from 'typescript'; import {ErrorCode} from '../../diagnostics'; import {Reference} from '../../imports'; -import {ClassPropertyMapping, DirectiveTypeCheckMeta, HostDirectiveMeta, InputMapping} from '../../metadata'; +import {ClassPropertyMapping, DirectiveTypeCheckMeta, HostDirectiveMeta, InputMapping, PipeMeta} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; @@ -28,6 +28,9 @@ export interface TypeCheckableDirectiveMeta extends DirectiveMeta, DirectiveType isSignal: boolean; hostDirectives: HostDirectiveMeta[]|null; decorator: ts.Decorator|null; + // TODO: add docs. + // TODO: rename to `isExplicitlyDeferred`? + isOnlyDeferred: boolean; } export type TemplateId = string&{__brand: 'TemplateId'}; @@ -73,7 +76,7 @@ export interface TypeCheckBlockMetadata { /* * Pipes used in the template of the component. */ - pipes: Map>>; + pipes: Map; /** * Schemas that apply to this template. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts index 7967fb007dc198..830d1e78f888f7 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts @@ -10,6 +10,7 @@ import {ParseError, ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode import ts from 'typescript'; import {Reference} from '../../imports'; +import {PipeMeta} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; import {TemplateSourceMapping, TypeCheckableDirectiveMeta} from './api'; @@ -43,9 +44,9 @@ export interface TypeCheckContext { addTemplate( ref: Reference>, binder: R3TargetBinder, template: TmplAstNode[], - pipes: Map>>, - schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile, - parseErrors: ParseError[]|null, isStandalone: boolean, preserveWhitespaces: boolean): void; + pipes: Map, schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, + file: ParseSourceFile, parseErrors: ParseError[]|null, isStandalone: boolean, + preserveWhitespaces: boolean): void; } /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index c9fdd2e2fb9344..7606e3bfbd09ee 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -12,6 +12,7 @@ import ts from 'typescript'; import {ErrorCode, ngErrorCode} from '../../../../src/ngtsc/diagnostics'; import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system'; import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports'; +import {PipeMeta} from '../../metadata'; import {PerfEvent, PerfRecorder} from '../../perf'; import {FileUpdate} from '../../program_driver'; import {ClassDeclaration, ReflectionHost} from '../../reflection'; @@ -208,9 +209,9 @@ export class TypeCheckContextImpl implements TypeCheckContext { addTemplate( ref: Reference>, binder: R3TargetBinder, template: TmplAstNode[], - pipes: Map>>, - schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile, - parseErrors: ParseError[]|null, isStandalone: boolean, preserveWhitespaces: boolean): void { + pipes: Map, schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, + file: ParseSourceFile, parseErrors: ParseError[]|null, isStandalone: boolean, + preserveWhitespaces: boolean): void { if (!this.host.shouldCheckComponent(ref.node)) { return; } @@ -226,6 +227,7 @@ export class TypeCheckContextImpl implements TypeCheckContext { ...this.getTemplateDiagnostics(parseErrors, templateId, sourceMapping)); } + // ?? const boundTarget = binder.bind({template}); if (this.inlining === InliningMode.InlineOps) { @@ -267,7 +269,7 @@ export class TypeCheckContextImpl implements TypeCheckContext { if (!pipes.has(name)) { continue; } - usedPipes.push(pipes.get(name)!); + usedPipes.push(pipes.get(name)!.ref as Reference>); } const inliningRequirement = diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts index 8adee446fb9b49..120929c9efa9da 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {BindingPipe, PropertyRead, PropertyWrite, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstForLoopBlock, TmplAstHoverDeferredTrigger, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstReference, TmplAstTemplate, TmplAstVariable, TmplAstViewportDeferredTrigger} from '@angular/compiler'; +import {AbsoluteSourceSpan, BindingPipe, PropertyRead, PropertyWrite, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstForLoopBlock, TmplAstHoverDeferredTrigger, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstReference, TmplAstTemplate, TmplAstVariable, TmplAstViewportDeferredTrigger} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, makeDiagnostic, makeRelatedInformation, ngErrorCode} from '../../diagnostics'; @@ -50,6 +50,24 @@ export interface OutOfBandDiagnosticRecorder { */ missingPipe(templateId: TemplateId, ast: BindingPipe): void; + /** + * TODO: add docs. + * + * @param templateId the template type-checking ID of the template which contains the unknown + * pipe. + * @param ast the `BindingPipe` invocation of the pipe which could not be found. + */ + deferredPipeUsedEagerly(templateId: TemplateId, ast: BindingPipe): void; + + /** + * TODO: add docs. + * + * @param templateId the template type-checking ID of the template which contains the unknown + * pipe. + * @param element the element which hosts a component that was defer-loaded. + */ + deferredComponentUsedEagerly(templateId: TemplateId, element: TmplAstElement): void; + illegalAssignmentToTemplateVar( templateId: TemplateId, assignment: PropertyWrite, target: TmplAstVariable): void; @@ -154,6 +172,44 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor this.recordedPipes.add(ast); } + deferredPipeUsedEagerly(templateId: TemplateId, ast: BindingPipe): void { + if (this.recordedPipes.has(ast)) { + return; + } + + const mapping = this.resolver.getSourceMapping(templateId); + // TODO: improve error message. + const errorMsg = `Deferred pipe '${ast.name}' was used eagerly in a template.`; + + const sourceSpan = this.resolver.toParseSourceSpan(templateId, ast.nameSpan); + if (sourceSpan === null) { + throw new Error( + `Assertion failure: no SourceLocation found for usage of pipe '${ast.name}'.`); + } + this._diagnostics.push(makeTemplateDiagnostic( + templateId, mapping, sourceSpan, ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.DEFERRED_PIPE_USED_EAGERLY), errorMsg)); + this.recordedPipes.add(ast); + } + + deferredComponentUsedEagerly(templateId: TemplateId, element: TmplAstElement): void { + const mapping = this.resolver.getSourceMapping(templateId); + // TODO: improve error message. + const errorMsg = `Element '${element.name}' was used eagerly, ` + + `but refers to a deferred component or directive.`; + + const {start, end} = element.startSourceSpan; + const absoluteSourceSpan = new AbsoluteSourceSpan(start.offset, end.offset); + const sourceSpan = this.resolver.toParseSourceSpan(templateId, absoluteSourceSpan); + if (sourceSpan === null) { + throw new Error( + `Assertion failure: no SourceLocation found for usage of pipe '${element.name}'.`); + } + this._diagnostics.push(makeTemplateDiagnostic( + templateId, mapping, sourceSpan, ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.DEFERRED_DIRECTIVE_USED_EAGERLY), errorMsg)); + } + illegalAssignmentToTemplateVar( templateId: TemplateId, assignment: PropertyWrite, target: TmplAstVariable): void { const mapping = this.resolver.getSourceMapping(templateId); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 5cab0f599f4de4..e3ac44a206f439 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -10,7 +10,7 @@ import {AST, BindingPipe, BindingType, BoundTarget, Call, createCssSelectorFromN import ts from 'typescript'; import {Reference} from '../../imports'; -import {BindingPropertyName, ClassPropertyName} from '../../metadata'; +import {BindingPropertyName, ClassPropertyName, PipeMeta} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; import {TemplateId, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata} from '../api'; @@ -1586,9 +1586,8 @@ export class Context { readonly env: Environment, readonly domSchemaChecker: DomSchemaChecker, readonly oobRecorder: OutOfBandDiagnosticRecorder, readonly id: TemplateId, readonly boundTarget: BoundTarget, - private pipes: Map>>, - readonly schemas: SchemaMetadata[], readonly hostIsStandalone: boolean, - readonly hostPreserveWhitespaces: boolean) {} + private pipes: Map, readonly schemas: SchemaMetadata[], + readonly hostIsStandalone: boolean, readonly hostPreserveWhitespaces: boolean) {} /** * Allocate a new variable name for use within the `Context`. @@ -1600,7 +1599,7 @@ export class Context { return ts.factory.createIdentifier(`_t${this.nextId++}`); } - getPipeByName(name: string): Reference>|null { + getPipeByName(name: string): PipeMeta|null { if (!this.pipes.has(name)) { return null; } @@ -2015,6 +2014,16 @@ class Scope { new TcbDomSchemaCheckerOp(this.tcb, node, /* checkElement */ true, claimedInputs)); } return; + } else { + if (node instanceof TmplAstElement) { + const isDeferred = this.tcb.boundTarget.isDeferred(node); + if (!isDeferred && directives.some((dirMeta) => dirMeta.isOnlyDeferred)) { + // This node has directives/components that were defer-loaded (included into + // `@Component.deferredImports`), but the node itself was used outside of a + // `@defer` block, which is the error. + this.tcb.oobRecorder.deferredComponentUsedEagerly(this.tcb.id, node); + } + } } const dirMap = new Map(); @@ -2282,21 +2291,31 @@ class TcbExpressionTranslator { return ts.factory.createThis(); } else if (ast instanceof BindingPipe) { const expr = this.translate(ast.exp); - const pipeRef = this.tcb.getPipeByName(ast.name); + const pipeMeta = this.tcb.getPipeByName(ast.name); let pipe: ts.Expression|null; - if (pipeRef === null) { + if (pipeMeta === null) { // No pipe by that name exists in scope. Record this as an error. this.tcb.oobRecorder.missingPipe(this.tcb.id, ast); + // Use an 'any' value to at least allow the rest of the expression to be checked. + pipe = NULL_AS_ANY; + } else if ( + pipeMeta.isOnlyDeferred && + this.tcb.boundTarget.getEagerlyUsedPipes().includes(ast.name)) { + // This pipe was defer-loaded (included into `@Component.deferredImports`), + // but was used outside of a `@defer` block, which is the error. + this.tcb.oobRecorder.deferredPipeUsedEagerly(this.tcb.id, ast); + // Use an 'any' value to at least allow the rest of the expression to be checked. pipe = NULL_AS_ANY; } else { // Use a variable declared as the pipe's type. - pipe = this.tcb.env.pipeInst(pipeRef); + pipe = + this.tcb.env.pipeInst(pipeMeta.ref as Reference>); } const args = ast.args.map(arg => this.translate(arg)); let methodAccess: ts.Expression = - ts.factory.createPropertyAccessExpression(pipe, 'transform'); + ts.factory.createPropertyAccessExpression(pipe!, 'transform'); addParseSpanInfo(methodAccess, ast.nameSpan); if (!this.tcb.env.config.checkTypeOfPipes) { methodAccess = ts.factory.createAsExpression( diff --git a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts index f72cde49c7edb6..2f9a9012679a1b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CssSelector, ParseSourceFile, ParseSourceSpan, parseTemplate, ParseTemplateOptions, R3TargetBinder, SchemaMetadata, SelectorMatcher, TmplAstElement} from '@angular/compiler'; +import {BindingPipe, CssSelector, ParseSourceFile, ParseSourceSpan, parseTemplate, ParseTemplateOptions, R3TargetBinder, SchemaMetadata, SelectorMatcher, TmplAstElement} from '@angular/compiler'; import ts from 'typescript'; import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError, LogicalFileSystem} from '../../file_system'; @@ -682,6 +682,7 @@ function getDirectiveMetaFromDeclaration( decorator: null, ngContentSelectors: decl.ngContentSelectors || null, preserveWhitespaces: decl.preserveWhitespaces ?? false, + isOnlyDeferred: false, hostDirectives: decl.hostDirectives === undefined ? null : decl.hostDirectives.map(hostDecl => { return { directive: new Reference(resolveDeclaration(hostDecl.directive)), @@ -734,11 +735,13 @@ function makeScope(program: ts.Program, sf: ts.SourceFile, decls: TestDeclaratio isStandalone: false, isSignal: false, imports: null, + deferredImports: null, schemas: null, decorator: null, assumedToExportProviders: false, ngContentSelectors: decl.ngContentSelectors || null, preserveWhitespaces: decl.preserveWhitespaces ?? false, + isOnlyDeferred: false, hostDirectives: decl.hostDirectives === undefined ? null : decl.hostDirectives.map(hostDecl => { return { @@ -762,6 +765,7 @@ function makeScope(program: ts.Program, sf: ts.SourceFile, decls: TestDeclaratio nameExpr: null, isStandalone: false, decorator: null, + isOnlyDeferred: false, }); } } @@ -798,6 +802,8 @@ export class NoopOobRecorder implements OutOfBandDiagnosticRecorder { } missingReferenceTarget(): void {} missingPipe(): void {} + deferredPipeUsedEagerly(templateId: TemplateId, ast: BindingPipe): void {} + deferredComponentUsedEagerly(templateId: TemplateId, element: TmplAstElement): void {} illegalAssignmentToTemplateVar(): void {} duplicateTemplateVar(): void {} requiresInlineTcb(): void {} diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index a09a57d99683c1..6d349103da0c3f 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -9559,9 +9559,9 @@ function allTests(os: string) { '(DeferredCmpA, DeferredCmpB) => {'); }); - it('should handle defer blocks that rely on deps from `deferredImports` and `imports`', - () => { - env.write('eager-a.ts', ` + fit('should handle defer blocks that rely on deps from `deferredImports` and `imports`', + () => { + env.write('eager-a.ts', ` import {Component} from '@angular/core'; @Component({ @@ -9573,7 +9573,7 @@ function allTests(os: string) { } `); - env.write('deferred-a.ts', ` + env.write('deferred-a.ts', ` import {Component} from '@angular/core'; @Component({ @@ -9585,7 +9585,7 @@ function allTests(os: string) { } `); - env.write('deferred-b.ts', ` + env.write('deferred-b.ts', ` import {Component} from '@angular/core'; @Component({ @@ -9597,7 +9597,7 @@ function allTests(os: string) { } `); - env.write('test.ts', ` + env.write('test.ts', ` import {Component} from '@angular/core'; import {DeferredCmpA} from './deferred-a'; import {DeferredCmpB} from './deferred-b'; @@ -9622,41 +9622,43 @@ function allTests(os: string) { } `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - - // Expect that all deferrableImports to become dynamic imports. - // Other imported symbols remain eager. - expect(jsContents) - .toContain( - 'const AppCmp_Defer_1_DepsFn = () => [' + - 'EagerCmpA, ' + - 'import("./deferred-a").then(m => m.DeferredCmpA)];'); - expect(jsContents) - .toContain( - 'const AppCmp_Defer_4_DepsFn = () => [' + - 'EagerCmpA, ' + - 'import("./deferred-b").then(m => m.DeferredCmpB)];'); - - // Make sure there are no eager imports present in the output. - expect(jsContents).not.toContain(`from './deferred-a'`); - expect(jsContents).not.toContain(`from './deferred-b'`); - - // Eager dependencies retain their imports. - expect(jsContents).toContain(`from './eager-a';`); - - // Defer blocks would have their own dependency functions in full mode. - expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_Defer_1_DepsFn);'); - expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_Defer_4_DepsFn);'); - - // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too. - expect(jsContents) - .toContain( - 'ɵsetClassMetadataAsync(AppCmp, () => [' + - 'import("./deferred-a").then(m => m.DeferredCmpA), ' + - 'import("./deferred-b").then(m => m.DeferredCmpB)], ' + - '(DeferredCmpA, DeferredCmpB) => {'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + + debugger; + + // Expect that all deferrableImports to become dynamic imports. + // Other imported symbols remain eager. + expect(jsContents) + .toContain( + 'const AppCmp_Defer_1_DepsFn = () => [' + + 'EagerCmpA, ' + + 'import("./deferred-a").then(m => m.DeferredCmpA)];'); + expect(jsContents) + .toContain( + 'const AppCmp_Defer_4_DepsFn = () => [' + + 'EagerCmpA, ' + + 'import("./deferred-b").then(m => m.DeferredCmpB)];'); + + // Make sure there are no eager imports present in the output. + expect(jsContents).not.toContain(`from './deferred-a'`); + expect(jsContents).not.toContain(`from './deferred-b'`); + + // Eager dependencies retain their imports. + expect(jsContents).toContain(`from './eager-a';`); + + // Defer blocks would have their own dependency functions in full mode. + expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_Defer_1_DepsFn);'); + expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_Defer_4_DepsFn);'); + + // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too. + expect(jsContents) + .toContain( + 'ɵsetClassMetadataAsync(AppCmp, () => [' + + 'import("./deferred-a").then(m => m.DeferredCmpA), ' + + 'import("./deferred-b").then(m => m.DeferredCmpB)], ' + + '(DeferredCmpA, DeferredCmpB) => {'); + }); describe('error handling', () => { it('should produce an error when unsupported type (@Injectable) is used in `deferredImports`', @@ -9702,6 +9704,149 @@ function allTests(os: string) { expect(diags.length).toBe(1); expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT)); }); + + // TODO: fix the test + xit('should produce an error when components from `deferredImports` are used outside of defer blocks', + () => { + env.write('deferred-a.ts', ` + import {Component} from '@angular/core'; + + export const TokenA = 'test'; + + @Component({ + standalone: true, + selector: 'deferred-cmp-a', + template: 'DeferredCmpA contents', + }) + export class DeferredCmpA { + } + `); + + env.write('test.ts', ` + import {Component} from '@angular/core'; + import {DeferredCmpA, TokenA} from './deferred-a'; + + @Component({ + standalone: true, + deferredImports: [DeferredCmpA], + template: \` + @defer { + + } + \`, + }) + export class AppCmp { + } + `); + + /* + const diags = env.driveDiagnostics(); + + expect(diags.length).toBe(1); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_DIRECTIVE_USED_EAGERLY)); + */ + }); + + it('should produce an error when components from `deferredImports` are used outside of defer blocks', + () => { + env.write('deferred-a.ts', ` + import {Component} from '@angular/core'; + + @Component({ + standalone: true, + selector: 'deferred-cmp-a', + template: 'DeferredCmpA contents', + }) + export class DeferredCmpA { + } + `); + + env.write('deferred-b.ts', ` + import {Component} from '@angular/core'; + + @Component({ + standalone: true, + selector: 'deferred-cmp-b', + template: 'DeferredCmpB contents', + }) + export class DeferredCmpB { + } + `); + + env.write('test.ts', ` + import {Component} from '@angular/core'; + import {DeferredCmpA} from './deferred-a'; + import {DeferredCmpB} from './deferred-b'; + + @Component({ + standalone: true, + deferredImports: [DeferredCmpA, DeferredCmpB], + template: \` + + @defer { + + } + \`, + }) + export class AppCmp { + } + `); + + const diags = env.driveDiagnostics(); + + expect(diags.length).toBe(1); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_DIRECTIVE_USED_EAGERLY)); + }); + + it('should produce an error when pipes from `deferredImports` are used outside of defer blocks', + () => { + env.write('deferred-pipe-a.ts', ` + import {Pipe} from '@angular/core'; + + @Pipe({ + standalone: true, + name: 'deferredPipeA' + }) + export class DeferredPipeA { + transform() {} + } + `); + + env.write('deferred-pipe-b.ts', ` + import {Pipe} from '@angular/core'; + + @Pipe({ + standalone: true, + name: 'deferredPipeB' + }) + export class DeferredPipeB { + transform() {} + } + `); + + env.write('test.ts', ` + import {Component} from '@angular/core'; + import {DeferredPipeA} from './deferred-pipe-a'; + import {DeferredPipeB} from './deferred-pipe-b'; + + @Component({ + standalone: true, + deferredImports: [DeferredPipeA, DeferredPipeB], + template: \` + {{ 'Eager' | deferredPipeA }} + @defer { + {{ 'Deferred' | deferredPipeB }} + } + \`, + }) + export class AppCmp { + } + `); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_PIPE_USED_EAGERLY)); + }); }); }); diff --git a/packages/compiler/src/render3/view/t2_api.ts b/packages/compiler/src/render3/view/t2_api.ts index 689d28cc381ca9..8cdcfad2faee05 100644 --- a/packages/compiler/src/render3/view/t2_api.ts +++ b/packages/compiler/src/render3/view/t2_api.ts @@ -227,4 +227,10 @@ export interface BoundTarget { * @param trigger Trigger whose target is being looked up. */ getDeferredTriggerTarget(block: DeferredBlock, trigger: DeferredTrigger): Element|null; + + /** + * TODO: add docs. + * @param node + */ + isDeferred(node: Element): boolean; } diff --git a/packages/compiler/src/render3/view/t2_binder.ts b/packages/compiler/src/render3/view/t2_binder.ts index 2ef5e5fe574507..290a131b59fc23 100644 --- a/packages/compiler/src/render3/view/t2_binder.ts +++ b/packages/compiler/src/render3/view/t2_binder.ts @@ -51,7 +51,7 @@ export class R3TargetBinder implements TargetB TemplateBinder.applyWithScope(target.template, scope); return new R3BoundTarget( target, directives, eagerDirectives, bindings, references, expressions, symbols, - nestingLevel, scopedNodeEntities, usedPipes, eagerPipes, deferBlocks); + nestingLevel, scopedNodeEntities, usedPipes, eagerPipes, deferBlocks, scope); } } @@ -68,6 +68,11 @@ class Scope implements Visitor { */ readonly namedEntities = new Map(); + /** + * Set of elements that belong to this scope. + */ + readonly elementsInScope = new WeakSet(); + /** * Child `Scope`s for immediately nested `ScopedNode`s. */ @@ -132,6 +137,8 @@ class Scope implements Visitor { // Recurse into the `Element`'s children. element.children.forEach(node => node.visit(this)); + + this.elementsInScope.add(element); } visitTemplate(template: Template) { @@ -731,7 +738,7 @@ export class R3BoundTarget implements BoundTar private nestingLevel: Map, private scopedNodeEntities: Map>, private usedPipes: Set, private eagerPipes: Set, - private deferredBlocks: Set) {} + private deferredBlocks: Set, private rootScope: Scope) {} getEntitiesInScope(node: ScopedNode|null): ReadonlySet { return this.scopedNodeEntities.get(node) ?? new Set(); @@ -785,7 +792,6 @@ export class R3BoundTarget implements BoundTar return Array.from(this.deferredBlocks); } - getDeferredTriggerTarget(block: DeferredBlock, trigger: DeferredTrigger): Element|null { // Only triggers that refer to DOM nodes can be resolved. if (!(trigger instanceof InteractionDeferredTrigger) && @@ -849,6 +855,16 @@ export class R3BoundTarget implements BoundTar return null; } + isDeferred(element: Element): boolean { + for (const deferBlock of this.deferredBlocks) { + const scope = this.rootScope.getChildScope(deferBlock); + if (scope.elementsInScope.has(element)) { + return true; + } + } + return false; + } + /** * Finds an entity with a specific name in a scope. * @param rootNode Root node of the scope.