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 13, 2023
1 parent 1236560 commit a1e9f50
Show file tree
Hide file tree
Showing 20 changed files with 607 additions and 224 deletions.
313 changes: 189 additions & 124 deletions packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts

Large diffs are not rendered by default.

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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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<ts.ImportDeclaration, Map<string, Set<ts.Identifier>|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<ts.ImportDeclaration>();
private readonly imports = new Map<
ts.ImportDeclaration, ExplicitlyDeferred|Map<string, Set<ts.Identifier>|AssumeEager>>();

constructor(
private readonly typeChecker: ts.TypeChecker,
Expand Down Expand Up @@ -79,24 +76,37 @@ 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) {
// Ignore deferrable candidates when only explicit deferred imports mode is enabled.
// In that mode only dependencies from the `@Component.deferredImports` field are
// defer-loadable.
return;
}

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(
Expand Down Expand Up @@ -127,20 +137,17 @@ 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.
return false;
}
}

// 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;
}

Expand Down
11 changes: 11 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,10 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta {
* Whether the directive should be assumed to export providers if imported as a standalone type.
*/
assumedToExportProviders: boolean;

// TODO: add docs.
// TODO: rename to `isExplicitlyDeferred`?
isOnlyDeferred: boolean;
}

/** Metadata collected about an additional directive that is being applied to a directive host. */
Expand Down Expand Up @@ -268,6 +278,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
30 changes: 19 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,39 @@ 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) {
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
7 changes: 5 additions & 2 deletions packages/compiler-cli/src/ngtsc/typecheck/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';


Expand All @@ -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'};
Expand Down Expand Up @@ -73,7 +76,7 @@ export interface TypeCheckBlockMetadata {
/*
* Pipes used in the template of the component.
*/
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>;
pipes: Map<string, PipeMeta>;

/**
* Schemas that apply to this template.
Expand Down
7 changes: 4 additions & 3 deletions packages/compiler-cli/src/ngtsc/typecheck/api/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -43,9 +44,9 @@ export interface TypeCheckContext {
addTemplate(
ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
binder: R3TargetBinder<TypeCheckableDirectiveMeta>, template: TmplAstNode[],
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>,
schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile,
parseErrors: ParseError[]|null, isStandalone: boolean, preserveWhitespaces: boolean): void;
pipes: Map<string, PipeMeta>, schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping,
file: ParseSourceFile, parseErrors: ParseError[]|null, isStandalone: boolean,
preserveWhitespaces: boolean): void;
}

/**
Expand Down
Loading

0 comments on commit a1e9f50

Please sign in to comment.