From 03b583c6585f03a5f6ea023cce04eda77bba20f5 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Thu, 7 Dec 2023 15:22:09 -0800 Subject: [PATCH] fixup! WIP: support local compilation for `@defer` blocks --- .../annotations/component/src/handler.ts | 84 ++++++++++++------- .../annotations/component/src/metadata.ts | 1 + .../annotations/directive/src/handler.ts | 1 + .../src/ngtsc/metadata/src/api.ts | 6 ++ .../src/ngtsc/metadata/src/dts.ts | 1 + .../compiler-cli/src/ngtsc/scope/src/api.ts | 1 + .../src/ngtsc/scope/src/standalone.ts | 20 +++++ .../src/ngtsc/scope/src/typecheck.ts | 14 ++-- .../src/ngtsc/typecheck/src/context.ts | 1 + .../src/ngtsc/typecheck/testing/index.ts | 1 + .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 55 ++++++++++++ 11 files changed, 149 insertions(+), 36 deletions(-) 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..22d643cd5d37e0 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -20,7 +20,7 @@ import {DirectiveMeta, extractDirectiveTypeCheckMeta, HostDirectivesResolver, Ma 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, 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,6 +563,7 @@ 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, @@ -693,9 +700,8 @@ export class ComponentDecoratorHandler implements const pipes = new Map(); - const dependencies = scope.kind === ComponentScopeKind.NgModule ? - scope.compilation.dependencies : - scope.dependencies; + const isModuleScope = scope.kind === ComponentScopeKind.NgModule; + const dependencies = isModuleScope ? scope.compilation.dependencies : scope.dependencies; for (const dep of dependencies) { if (dep.kind === MetaKind.Directive && dep.selector !== null) { @@ -713,8 +719,23 @@ export class ComponentDecoratorHandler implements // Find all defer blocks used in the template and for each block // bind its own scope. const deferBlocks = new Map>(); + let deferBlockBinder = binder; + // debugger; + if (!isModuleScope) { + const deferredDeps = (scope as StandaloneScope).deferredDependencies; + if (Array.isArray(deferredDeps) && deferredDeps.length > 0) { + const matcher = new SelectorMatcher(); + const allDeps = [...dependencies, ...deferredDeps]; + for (const dep of allDeps) { + if (dep.kind === MetaKind.Directive && dep.selector !== null) { + matcher.addSelectables(CssSelector.parse(dep.selector), [dep]); + } + } + deferBlockBinder = new R3TargetBinder(matcher); + } + } 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 @@ -921,17 +942,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); } } @@ -1144,6 +1164,8 @@ export class ComponentDecoratorHandler implements deferrableTypes: new Map(), }; + // TODO: this function should probably be used for both local and full compilations + // if a config options is set to use `deferredImports`. // TODO: move to a separate function // TODO: produce a diagnostic when: // - a symbol from the `deferredImports` is used outside of `@defer` blocks, 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..d6f329f2d9d15f 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, diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts index 968f464a99a85f..07a84a6c36a7fc 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. */ diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts index 3f3d4ce29ce642..0c1194e418d432 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, 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..55dd6bc0bfd9f2 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}); + isPoisoned = isPoisoned || dirMeta.isPoisoned || !dirMeta.isStandalone; + continue; + } + + const pipeMeta = this.metaReader.getPipeMetadata(ref); + if (pipeMeta !== null) { + deferredDependencies.add({...pipeMeta, ref}); + 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..20b5648543fa60 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts @@ -87,16 +87,20 @@ 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) { + 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) { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index c9fdd2e2fb9344..045bc9f72679d2 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -226,6 +226,7 @@ export class TypeCheckContextImpl implements TypeCheckContext { ...this.getTemplateDiagnostics(parseErrors, templateId, sourceMapping)); } + // ?? const boundTarget = binder.bind({template}); if (this.inlining === InliningMode.InlineOps) { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts index f72cde49c7edb6..718a71ffc94455 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts @@ -734,6 +734,7 @@ function makeScope(program: ts.Program, sf: ts.SourceFile, decls: TestDeclaratio isStandalone: false, isSignal: false, imports: null, + deferredImports: null, schemas: null, decorator: null, assumedToExportProviders: false, diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index a09a57d99683c1..f78aad0efd5291 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -9702,6 +9702,61 @@ function allTests(os: string) { expect(diags.length).toBe(1); expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT)); }); + + fit('should produce an error when deps 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(); + + debugger; + // TODO: there should be a single error saying that `deferred-cmp-a` is + // a deferred component, but used outside of the `@defer` block. + expect(diags.length).toBe(1); + expect(diags[0].code) + .toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT)); + }); }); });