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 7, 2023
1 parent 1236560 commit 03b583c
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 36 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,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,
Expand Down Expand Up @@ -693,9 +700,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 +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<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 +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<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 @@ -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,
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,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,
Expand Down
6 changes: 6 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 Down
1 change: 1 addition & 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 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});
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 ?? [],
});
Expand Down
14 changes: 9 additions & 5 deletions packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
...this.getTemplateDiagnostics(parseErrors, templateId, sourceMapping));
}

// ??
const boundTarget = binder.bind({template});

if (this.inlining === InliningMode.InlineOps) {
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
55 changes: 55 additions & 0 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: \`
<deferred-cmp-a />
@defer {
<deferred-cmp-b />
}
\`,
})
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));
});
});
});

Expand Down

0 comments on commit 03b583c

Please sign in to comment.