Skip to content

Commit

Permalink
fixup! WIP: support local compilation for @defer blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewKushnir committed Dec 11, 2023
1 parent 1236560 commit c1d6d0b
Show file tree
Hide file tree
Showing 19 changed files with 328 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -295,6 +295,8 @@ export class ComponentDecoratorHandler implements
}

let resolvedImports: Reference<ClassDeclaration>[]|null = null;
let resolvedDeferredImports: Reference<ClassDeclaration>[]|null = null;

let rawImports: ts.Expression|null = component.get('imports') ?? null;
let rawDeferredImports: ts.Expression|null = component.get('deferredImports') ?? null;

Expand All @@ -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) {
Expand Down Expand Up @@ -517,6 +522,7 @@ export class ComponentDecoratorHandler implements
rawImports,
resolvedImports,
rawDeferredImports,
resolvedDeferredImports,
schemas,
decorator: decorator?.node as ts.Decorator | null ?? null,
},
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -693,9 +701,8 @@ export class ComponentDecoratorHandler implements

const pipes = new Map<string, PipeMeta>();

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) {
Expand All @@ -713,8 +720,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<TmplAstDeferredBlock, BoundTarget<DirectiveMeta>>();
let deferBlockBinder = binder;
// debugger;
if (!isModuleScope) {
const deferredDeps = (scope as StandaloneScope).deferredDependencies;
if (Array.isArray(deferredDeps) && deferredDeps.length > 0) {
const matcher = new SelectorMatcher<DirectiveMeta[]>();
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
Expand Down Expand Up @@ -921,17 +943,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<ClassDeclaration>[], 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);
}
}
Expand Down Expand Up @@ -1072,6 +1093,10 @@ export class ComponentDecoratorHandler implements
}
}

// TODO: consider throwing an error is defer blocks are present, but there were no
// `@Component.deferredImports` defined. For this we'd need to pass a flag to the
// Component decorator handler on which mode we operate in.

const meta: R3ComponentMetadata<R3TemplateDependency> = {...analysis.meta, ...resolution};
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));

Expand Down Expand Up @@ -1144,6 +1169,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface ComponentAnalysisData {
rawImports: ts.Expression|null;
resolvedImports: Reference<ClassDeclaration>[]|null;
rawDeferredImports: ts.Expression|null;
resolvedDeferredImports: Reference<ClassDeclaration>[]|null;

schemas: SchemaMetadata[]|null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,15 @@ export class DirectiveDecoratorHandler implements
isStandalone: analysis.meta.isStandalone,
isSignal: analysis.meta.isSignal,
imports: null,
deferredImports: null,
schemas: null,
ngContentSelectors: null,
decorator: analysis.decorator,
preserveWhitespaces: false,
// 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, {
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export class PipeDecoratorHandler implements
nameExpr: analysis.pipeNameExpr,
isStandalone: analysis.meta.isStandalone,
decorator: analysis.decorator,
isOnlyDeferred: false,
});

this.injectableRegistry.registerInjectable(node, {
Expand Down
10 changes: 10 additions & 0 deletions packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions packages/compiler-cli/src/ngtsc/metadata/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta {
*/
imports: Reference<ClassDeclaration>[]|null;

/**
* For standalone components, the list of imported types that can be used
* in `@defer` blocks (when only explicit dependencies are allowed).
*/
deferredImports: Reference<ClassDeclaration>[]|null;

/**
* For standalone components, the list of schemas declared.
*/
Expand All @@ -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. */
Expand Down Expand Up @@ -268,6 +276,7 @@ export interface PipeMeta {
nameExpr: ts.Expression|null;
isStandalone: boolean;
decorator: ts.Decorator|null;
isOnlyDeferred: boolean;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/compiler-cli/src/ngtsc/metadata/src/dts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
};
}

Expand Down Expand Up @@ -178,6 +180,7 @@ export class DtsMetadataReader implements MetadataReader {
nameExpr: null,
isStandalone,
decorator: null,
isOnlyDeferred: false,
};
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/scope/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface LocalModuleScope extends ExportScope {
export interface StandaloneScope {
kind: ComponentScopeKind.Standalone;
dependencies: Array<DirectiveMeta|PipeMeta|NgModuleMeta>;
deferredDependencies: Array<DirectiveMeta|PipeMeta>;
component: ClassDeclaration;
schemas: SchemaMetadata[];
isPoisoned: boolean;
Expand Down
20 changes: 20 additions & 0 deletions packages/compiler-cli/src/ngtsc/scope/src/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DirectiveMeta|PipeMeta|NgModuleMeta>([clazzMeta]);
const deferredDependencies = new Set<DirectiveMeta|PipeMeta>();
const seen = new Set<ClassDeclaration>([clazz]);
let isPoisoned = clazzMeta.isPoisoned;

Expand Down Expand Up @@ -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 ?? [],
});
Expand Down
31 changes: 20 additions & 11 deletions packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -33,7 +33,7 @@ export interface TypeCheckScope {
/**
* The pipes that are available in the compilation scope.
*/
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>;
pipes: Map<string, PipeMeta>;

/**
* The schemas that are used in this scope.
Expand Down Expand Up @@ -74,7 +74,7 @@ export class TypeCheckScopeRegistry {
getTypeCheckScope(node: ClassDeclaration): TypeCheckScope {
const matcher = new SelectorMatcher<DirectiveMeta[]>();
const directives: DirectiveMeta[] = [];
const pipes = new Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>();
const pipes = new Map<string, PipeMeta>();

const scope = this.scopeReader.getScopeForComponent(node);
if (scope === null) {
Expand All @@ -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<ClassDeclaration<ts.ClassDeclaration>>);
pipes.set(meta.name, {...meta, isOnlyDeferred: meta.isOnlyDeferred});
}
}

Expand Down
Loading

0 comments on commit c1d6d0b

Please sign in to comment.