refactor(compiler): extra diagnostics for @defer in local compilation mode (#53899)

This commit adds extra logic to produce a diagnostic in case `@Component.deferredImports` contain types from imports that also bring eager symbols. This would result in retaining a regular import and generating a dynamic import, which would not allow to defer-load dependencies.

PR Close #53899
This commit is contained in:
Andrew Kushnir
2024-01-12 12:06:46 -08:00
committed by Dylan Hunn
parent 6f79507ea7
commit 79e8539ce2
10 changed files with 362 additions and 92 deletions
@@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {BoundTarget, ChangeDetectionStrategy, compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, ForwardRefHandling, InterpolationConfig, makeBindingParser, outputAst as o, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3DeclareDirectiveDependencyMetadata, R3DeclarePipeDependencyMetadata, R3DeferBlockMetadata, R3DirectiveDependencyMetadata, R3PartialDeclaration, R3TargetBinder, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SelectorMatcher, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstDeferredTrigger, TmplAstElement, ViewEncapsulation} from '@angular/compiler';
import {BoundTarget, ChangeDetectionStrategy, compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, ForwardRefHandling, InterpolationConfig, makeBindingParser, outputAst as o, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3DeclareDirectiveDependencyMetadata, R3DeclarePipeDependencyMetadata, R3DeferBlockMetadata, R3DirectiveDependencyMetadata, R3PartialDeclaration, R3TargetBinder, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SelectorMatcher, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstDeferredTrigger, TmplAstElement, ViewEncapsulation} from '@angular/compiler';
import semver from 'semver';
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
@@ -183,6 +183,7 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
// Defer blocks are not yet fully supported in partial compilation.
deferrableDeclToImportDecl: new Map(),
deferrableTypes: new Map(),
deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock,
encapsulation: metaObj.has('encapsulation') ?
parseEncapsulation(metaObj.getValue('encapsulation')) :
@@ -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, R3DeferBlockMetadata, 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, DeferBlockDepsEmitMode, 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';
@@ -19,7 +19,7 @@ import {IndexingContext} from '../../../indexer';
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 {ClassDeclaration, DeclarationNode, Decorator, Import, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral} from '../../../reflection';
import {ComponentScopeKind, ComponentScopeReader, DtsModuleScopeResolver, LocalModuleScope, LocalModuleScopeRegistry, makeNotStandaloneDiagnostic, makeUnknownComponentImportDiagnostic, StandaloneScope, TypeCheckScopeRegistry} from '../../../scope';
import {getDiagnosticNode, makeUnknownComponentDeferredImportDiagnostic} from '../../../scope/src/util';
import {AnalysisOutput, CompilationMode, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../../transform';
@@ -477,6 +477,21 @@ export class ComponentDecoratorHandler implements
styles.push(...template.styles);
}
// Collect all explicitly deferred symbols from the `@Component.deferredImports` field
// (if it exists) and populate the `DeferredSymbolTracker` state. These operations are safe
// for the local compilation mode, since they don't require accessing/resolving symbols
// outside of the current source file.
let explicitlyDeferredTypes: Map<string, string>|null = null;
if (metadata.isStandalone && rawDeferredImports !== null) {
const deferredTypes = this.collectExplicitlyDeferredSymbols(rawDeferredImports);
for (const [deferredType, importDetails] of deferredTypes) {
explicitlyDeferredTypes ??= new Map();
explicitlyDeferredTypes.set(importDetails.name, importDetails.from);
this.deferredSymbolTracker.markAsDeferrableCandidate(
deferredType, importDetails.node, node, true /* isExplicitlyDeferred */);
}
}
const output: AnalysisOutput<ComponentAnalysisData> = {
analysis: {
baseClass: readBaseClass(node, this.reflector, this.evaluator),
@@ -524,6 +539,7 @@ export class ComponentDecoratorHandler implements
resolvedImports,
rawDeferredImports,
resolvedDeferredImports,
explicitlyDeferredTypes,
schemas,
decorator: decorator?.node as ts.Decorator | null ?? null,
},
@@ -656,8 +672,43 @@ export class ComponentDecoratorHandler implements
resolve(
node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>,
symbol: ComponentSymbol): ResolveResult<ComponentResolutionData> {
const metadata = analysis.meta as Readonly<R3ComponentMetadata<R3TemplateDependencyMetadata>>;
const diagnostics: ts.Diagnostic[] = [];
const context = getSourceFile(node);
// Check if there are some import declarations that contain symbols used within
// the `@Component.deferredImports` field, but those imports contain other symbols
// and thus the declaration can not be removed.
const nonRemovableImports =
this.deferredSymbolTracker.getNonRemovableDeferredImports(context, node);
if (nonRemovableImports.length > 0) {
for (const importDecl of nonRemovableImports) {
const diagnostic = makeDiagnostic(
ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY, importDecl,
`This import contains symbols used in the \`@Component.deferredImports\` array ` +
`of the \`${node.name.getText()}\` component, but also some other symbols that ` +
`are not in any \`@Component.deferredImports\` array. This renders all these ` +
`defer imports useless as this import remains and its module is eagerly loaded. ` +
`To fix this, make sure that this import contains *only* symbols ` +
`that are used within \`@Component.deferredImports\` arrays.`);
diagnostics.push(diagnostic);
}
return {diagnostics};
}
if (this.compilationMode === CompilationMode.LOCAL) {
return {};
return {
data: {
declarationListEmitMode: (!analysis.meta.isStandalone || analysis.rawImports !== null) ?
DeclarationListEmitMode.RuntimeResolved :
DeclarationListEmitMode.Direct,
declarations: EMPTY_ARRAY,
deferBlocks: this.locateDeferBlocksWithoutScope(analysis.template),
deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerComponent,
deferrableDeclToImportDecl: new Map(),
deferrableTypes: new Map(),
},
};
}
if (this.semanticDepGraphUpdater !== null && analysis.baseClass instanceof Reference) {
@@ -668,18 +719,14 @@ export class ComponentDecoratorHandler implements
return {};
}
const context = getSourceFile(node);
const metadata = analysis.meta as Readonly<R3ComponentMetadata<R3TemplateDependencyMetadata>>;
const data: ComponentResolutionData = {
declarations: EMPTY_ARRAY,
declarationListEmitMode: DeclarationListEmitMode.Direct,
deferBlocks: new Map(),
deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock,
deferrableDeclToImportDecl: new Map(),
deferrableTypes: new Map(),
};
const diagnostics: ts.Diagnostic[] = [];
const scope = this.scopeReader.getScopeForComponent(node);
if (scope !== null) {
@@ -849,7 +896,7 @@ export class ComponentDecoratorHandler implements
eagerlyUsed.has(decl.ref.node));
// Process information related to defer blocks
this.resolveDeferBlocks(deferBlocks, declarations, data, analysis, eagerlyUsed, bound);
this.resolveDeferBlocks(node, deferBlocks, declarations, data, analysis, eagerlyUsed, bound);
const cyclesFromDirectives = new Map<UsedDirective, Cycle>();
const cyclesFromPipes = new Map<UsedPipe, Cycle>();
@@ -1055,13 +1102,13 @@ export class ComponentDecoratorHandler implements
return [];
}
// 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<R3TemplateDependency> = {...analysis.meta, ...resolution};
const meta: R3ComponentMetadata<R3TemplateDependency> = {
...analysis.meta,
...resolution,
deferrableTypes,
};
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
removeDeferrableTypesFromComponentDecorator(analysis, deferrableTypes);
@@ -1113,19 +1160,20 @@ export class ComponentDecoratorHandler implements
return [];
}
const deferrableTypes = this.collectExplicitlyDeferredSymbols(analysis);
const meta: R3ComponentMetadata<R3TemplateDependency> = {
...analysis.meta,
declarationListEmitMode: (!analysis.meta.isStandalone || analysis.rawImports !== null) ?
DeclarationListEmitMode.RuntimeResolved :
DeclarationListEmitMode.Direct,
declarations: EMPTY_ARRAY,
deferBlocks: this.locateDeferBlocksWithoutScope(analysis.template),
deferrableDeclToImportDecl: new Map(),
deferrableTypes,
};
// In the local compilation mode we can only rely on the information available
// within the `@Component.deferredImports` array, because in this mode compiler
// doesn't have information on which dependencies belong to which defer blocks.
const deferrableTypes = analysis.explicitlyDeferredTypes;
removeDeferrableTypesFromComponentDecorator(analysis, deferrableTypes);
const meta = {
...analysis.meta,
...resolution,
deferrableTypes: deferrableTypes ?? new Map(),
} as R3ComponentMetadata<R3TemplateDependency>;
if (analysis.explicitlyDeferredTypes !== null) {
removeDeferrableTypesFromComponentDecorator(analysis, analysis.explicitlyDeferredTypes);
}
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
const def = compileComponentFromMetadata(meta, pool, makeBindingParser());
@@ -1191,16 +1239,16 @@ export class ComponentDecoratorHandler implements
}
/**
* Collects a list of deferrable symbols based on the `@Component.deferredImports` field.
* Collects deferrable symbols from the `@Component.deferredImports` field.
*/
private collectExplicitlyDeferredSymbols(analysis: Readonly<ComponentAnalysisData>):
Map<string, string> {
const deferrableTypes = new Map<string, string>();
if (!analysis.meta.isStandalone || analysis.rawDeferredImports === null ||
!ts.isArrayLiteralExpression(analysis.rawDeferredImports))
return deferrableTypes;
private collectExplicitlyDeferredSymbols(rawDeferredImports: ts.Expression):
Map<ts.Identifier, Import> {
const deferredTypes = new Map<ts.Identifier, Import>();
if (!ts.isArrayLiteralExpression(rawDeferredImports)) {
return deferredTypes;
}
for (const element of analysis.rawDeferredImports.elements) {
for (const element of rawDeferredImports.elements) {
const node = tryUnwrapForwardRef(element, this.reflector) || element;
if (!ts.isIdentifier(node)) {
@@ -1210,11 +1258,10 @@ export class ComponentDecoratorHandler implements
const imp = this.reflector.getImportOfIdentifier(node);
if (imp !== null) {
deferrableTypes.set(imp.name, imp.from);
this.deferredSymbolTracker.markAsExplicitlyDeferred(imp.node);
deferredTypes.set(node, imp);
}
}
return deferrableTypes;
return deferredTypes;
}
/**
@@ -1248,6 +1295,7 @@ export class ComponentDecoratorHandler implements
* available for the final `compile` step.
*/
private resolveDeferBlocks(
componentClassDecl: ClassDeclaration,
deferBlocks: Map<TmplAstDeferredBlock, BoundTarget<DirectiveMeta>>,
deferrableDecls: Map<ClassDeclaration, AnyUsedType>,
resolutionData: ComponentResolutionData,
@@ -1306,13 +1354,13 @@ export class ComponentDecoratorHandler implements
if (analysisData.meta.isStandalone) {
if (analysisData.rawImports !== null) {
this.registerDeferrableCandidates(
analysisData.rawImports, false /* isDeferredImport */, allDeferredDecls,
eagerlyUsedDecls, resolutionData);
componentClassDecl, analysisData.rawImports, false /* isDeferredImport */,
allDeferredDecls, eagerlyUsedDecls, resolutionData);
}
if (analysisData.rawDeferredImports !== null) {
this.registerDeferrableCandidates(
analysisData.rawDeferredImports, true /* isDeferredImport */, allDeferredDecls,
eagerlyUsedDecls, resolutionData);
componentClassDecl, analysisData.rawDeferredImports, true /* isDeferredImport */,
allDeferredDecls, eagerlyUsedDecls, resolutionData);
}
}
}
@@ -1323,7 +1371,7 @@ export class ComponentDecoratorHandler implements
* candidates.
*/
private registerDeferrableCandidates(
importsExpr: ts.Expression, isDeferredImport: boolean,
componentClassDecl: ClassDeclaration, importsExpr: ts.Expression, isDeferredImport: boolean,
allDeferredDecls: Set<ClassDeclaration>, eagerlyUsedDecls: Set<ClassDeclaration>,
resolutionData: ComponentResolutionData) {
if (!ts.isArrayLiteralExpression(importsExpr)) {
@@ -1388,11 +1436,8 @@ export class ComponentDecoratorHandler implements
resolutionData.deferrableDeclToImportDecl.set(
decl.node as unknown as Expression, imp.node as unknown as Expression);
if (isDeferredImport) {
this.deferredSymbolTracker.markAsExplicitlyDeferred(imp.node);
} else {
this.deferredSymbolTracker.markAsDeferrableCandidate(node, imp.node);
}
this.deferredSymbolTracker.markAsDeferrableCandidate(
node, imp.node, componentClassDecl, isDeferredImport);
}
}
@@ -25,7 +25,7 @@ import {ParsedTemplateWithSource, StyleUrlMeta} from './resources';
export type ComponentMetadataResolvedFields = SubsetOfKeys<
R3ComponentMetadata<R3TemplateDependencyMetadata>,
'declarations'|'declarationListEmitMode'|'deferBlocks'|'deferrableDeclToImportDecl'|
'deferrableTypes'>;
'deferrableTypes'|'deferBlockDepsEmitMode'>;
export interface ComponentAnalysisData {
/**
@@ -74,6 +74,11 @@ export interface ComponentAnalysisData {
rawDeferredImports: ts.Expression|null;
resolvedDeferredImports: Reference<ClassDeclaration>[]|null;
/**
* Map of symbol name -> import path for types from `@Component.deferredImports` field.
*/
explicitlyDeferredTypes: Map<string, string>|null;
schemas: SchemaMetadata[]|null;
decorator: ts.Decorator|null;
@@ -8,18 +8,12 @@
import ts from 'typescript';
import {ClassDeclaration} from '../../reflection';
import {getContainingImportDeclaration} from '../../reflection/src/typescript';
const AssumeEager = 'AssumeEager';
type AssumeEager = typeof AssumeEager;
/**
* A marker indicating that a symbol from an import declaration
* was referenced in a `@Component.deferredImports` list.
*/
const ExplicitlyDeferred = 'ExplicitlyDeferred';
type ExplicitlyDeferred = typeof ExplicitlyDeferred;
/**
* Maps imported symbol name to a set of locations where the symbols is used
* in a source file.
@@ -34,7 +28,13 @@ type SymbolMap = Map<string, Set<ts.Identifier>|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, ExplicitlyDeferred|SymbolMap>();
private readonly imports = new Map<ts.ImportDeclaration, SymbolMap>();
/**
* Map of a component class -> all import declarations that bring symbols
* used within `@Component.deferredImports` field.
*/
private readonly explicitlyDeferredImports = new Map<ClassDeclaration, ts.ImportDeclaration[]>();
constructor(
private readonly typeChecker: ts.TypeChecker,
@@ -86,32 +86,47 @@ export class DeferredSymbolTracker {
}
/**
* Marks a given import declaration as explicitly deferred, since it's
* used in the `@Component.deferredImports` field.
* Retrieves a list of import declarations that contain symbols used within
* `@Component.deferredImports` of a specific component class, but those imports
* can not be removed, since there are other symbols imported alongside deferred
* components.
*/
markAsExplicitlyDeferred(importDecl: ts.ImportDeclaration): void {
this.imports.set(importDecl, ExplicitlyDeferred);
getNonRemovableDeferredImports(sourceFile: ts.SourceFile, classDecl: ClassDeclaration):
ts.ImportDeclaration[] {
const affectedImports: ts.ImportDeclaration[] = [];
const importDecls = this.explicitlyDeferredImports.get(classDecl) ?? [];
for (const importDecl of importDecls) {
if (importDecl.getSourceFile() === sourceFile && !this.canDefer(importDecl)) {
affectedImports.push(importDecl);
}
}
return affectedImports;
}
/**
* Marks a given identifier and an associated import declaration as a candidate
* for defer loading.
*/
markAsDeferrableCandidate(identifier: ts.Identifier, importDecl: ts.ImportDeclaration): void {
if (this.onlyExplicitDeferDependencyImports) {
markAsDeferrableCandidate(
identifier: ts.Identifier, importDecl: ts.ImportDeclaration,
componentClassDecl: ClassDeclaration, isExplicitlyDeferred: boolean): void {
if (this.onlyExplicitDeferDependencyImports && !isExplicitlyDeferred) {
// 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;
}
let symbolMap = this.imports.get(importDecl);
// Do we come across this import as a part of `@Component.deferredImports` already?
if (symbolMap === ExplicitlyDeferred) {
return;
if (isExplicitlyDeferred) {
if (this.explicitlyDeferredImports.has(componentClassDecl)) {
this.explicitlyDeferredImports.get(componentClassDecl)!.push(importDecl);
} else {
this.explicitlyDeferredImports.set(componentClassDecl, [importDecl]);
}
}
let symbolMap = this.imports.get(importDecl);
// Do we come across this import for the first time?
if (!symbolMap) {
symbolMap = this.extractImportedSymbols(importDecl);
@@ -147,10 +162,6 @@ 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.
+1 -1
View File
@@ -25,7 +25,7 @@ import {setWrapHostForTest} from '../../src/transformers/compiler_host';
type TsConfigOptionsValue =
string|boolean|number|null|TsConfigOptionsValue[]|{[key: string]: TsConfigOptionsValue};
type TsConfigOptions = {
export type TsConfigOptions = {
[key: string]: TsConfigOptionsValue;
};
@@ -12,7 +12,7 @@ import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics';
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../../src/ngtsc/testing';
import {NgtscTestEnvironment} from './env';
import {NgtscTestEnvironment, TsConfigOptions} from './env';
const testFiles = loadStandardTestFiles();
@@ -20,8 +20,7 @@ runInEachFileSystem(() => {
describe('local compilation', () => {
let env!: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
function tsconfig(extraOpts: TsConfigOptions = {}) {
const tsconfig: {[key: string]: any} = {
extends: '../tsconfig-base.json',
compilerOptions: {
@@ -30,9 +29,15 @@ runInEachFileSystem(() => {
},
angularCompilerOptions: {
compilationMode: 'experimental-local',
...extraOpts,
},
};
env.write('tsconfig.json', JSON.stringify(tsconfig, null, 2));
}
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
tsconfig();
});
it('should produce no TS semantic diagnostics', () => {
@@ -460,8 +465,8 @@ runInEachFileSystem(() => {
env.driveMain();
const jsContents = env.getContents('test.js');
// If there is no style, don't generate css selectors on elements by setting encapsulation
// to none (=2)
// If there is no style, don't generate css selectors on elements by setting
// encapsulation to none (=2)
expect(jsContents).toContain('encapsulation: 2');
});
});
@@ -1148,6 +1153,10 @@ runInEachFileSystem(() => {
});
describe('@defer', () => {
beforeEach(() => {
tsconfig({onlyExplicitDeferDependencyImports: true});
});
it('should handle `@Component.deferredImports` field', () => {
env.write('deferred-a.ts', `
import {Component} from '@angular/core';
@@ -1358,6 +1367,179 @@ runInEachFileSystem(() => {
'import("./deferred-b").then(m => m.DeferredCmpB)], ' +
'(DeferredCmpA, DeferredCmpB) => {');
});
it('should support importing multiple deferrable deps from a single file ' +
'and use them within `@Component.deferrableImports` field',
() => {
env.write('deferred-deps.ts', `
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'deferred-cmp-a',
template: 'DeferredCmpA contents',
})
export class DeferredCmpA {
}
@Component({
standalone: true,
selector: 'deferred-cmp-b',
template: 'DeferredCmpB contents',
})
export class DeferredCmpB {
}
`);
env.write('test.ts', `
import {Component} from '@angular/core';
// This import brings multiple symbols, but all of them are
// used within @Component.deferredImports, thus this import
// can be removed in favor of dynamic imports.
import {DeferredCmpA, DeferredCmpB} from './deferred-deps';
@Component({
standalone: true,
deferredImports: [DeferredCmpA],
template: \`
@defer {
<deferred-cmp-a />
}
\`,
})
export class AppCmpA {}
@Component({
standalone: true,
deferredImports: [DeferredCmpB],
template: \`
@defer {
<deferred-cmp-b />
}
\`,
})
export class AppCmpB {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
// Expect that we generate 2 different defer functions
// (one for each component).
expect(jsContents)
.toContain(
'const AppCmpA_DeferFn = () => [' +
'import("./deferred-deps").then(m => m.DeferredCmpA)]');
expect(jsContents)
.toContain(
'const AppCmpB_DeferFn = () => [' +
'import("./deferred-deps").then(m => m.DeferredCmpB)]');
// Make sure there are no eager imports present in the output.
expect(jsContents).not.toContain(`from './deferred-deps'`);
// Defer instructions use per-component dependency function.
expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmpA_DeferFn)');
expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmpB_DeferFn)');
// Expect `ɵsetClassMetadataAsync` to contain dynamic imports too.
expect(jsContents)
.toContain(
'ɵsetClassMetadataAsync(AppCmpA, () => [' +
'import("./deferred-deps").then(m => m.DeferredCmpA)]');
expect(jsContents)
.toContain(
'ɵsetClassMetadataAsync(AppCmpB, () => [' +
'import("./deferred-deps").then(m => m.DeferredCmpB)]');
});
it('should produce a diagnostic in case imports with symbols used ' +
'in `deferredImports` can not be removed',
() => {
env.write('deferred-deps.ts', `
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'deferred-cmp-a',
template: 'DeferredCmpA contents',
})
export class DeferredCmpA {
}
@Component({
standalone: true,
selector: 'deferred-cmp-b',
template: 'DeferredCmpB contents',
})
export class DeferredCmpB {
}
export function utilityFn() {}
`);
env.write('test.ts', `
import {Component} from '@angular/core';
// This import can not be removed, since it'd contain
// 'utilityFn' symbol once we remove 'DeferredCmpA' and
// 'DeferredCmpB' and generate a dynamic import for it.
// In this situation compiler produces a diagnostic to
// indicate that.
import {DeferredCmpA, DeferredCmpB, utilityFn} from './deferred-deps';
@Component({
standalone: true,
deferredImports: [DeferredCmpA],
template: \`
@defer {
<deferred-cmp-a />
}
\`,
})
export class AppCmpA {
ngOnInit() {
utilityFn();
}
}
@Component({
standalone: true,
deferredImports: [DeferredCmpB],
template: \`
@defer {
<deferred-cmp-b />
}
\`,
})
export class AppCmpB {}
@Component({
standalone: true,
template: 'Component without any dependencies'
})
export class ComponentWithoutDeps {}
`);
const diags = env.driveDiagnostics();
// Expect 2 diagnostics: one for each component `AppCmpA` and `AppCmpB`,
// since both of them refer to symbols from an import declaration that
// can not be removed.
expect(diags.length).toBe(2);
const components = ['AppCmpA', 'AppCmpB'];
for (let i = 0; i < components.length; i++) {
const component = components[i];
const {code, messageText} = diags[i];
expect(code).toBe(ngErrorCode(ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY));
expect(messageText)
.toContain(
'This import contains symbols used in the `@Component.deferredImports` ' +
`array of the \`${component}\` component`);
}
});
});
});
});
+3 -1
View File
@@ -21,7 +21,7 @@ import {R3JitReflector} from './render3/r3_jit';
import {compileNgModule, compileNgModuleDeclarationExpression, R3NgModuleMetadata, R3NgModuleMetadataKind, R3SelectorScopeMode} from './render3/r3_module_compiler';
import {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler';
import {createMayBeForwardRefExpression, ForwardRefHandling, getSafePropertyAccessString, MaybeForwardRefExpression, wrapReference} from './render3/util';
import {DeclarationListEmitMode, R3ComponentMetadata, R3DeferBlockMetadata, R3DirectiveDependencyMetadata, R3DirectiveMetadata, R3HostDirectiveMetadata, R3HostMetadata, R3InputMetadata, R3PipeDependencyMetadata, R3QueryMetadata, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata} from './render3/view/api';
import {DeclarationListEmitMode, DeferBlockDepsEmitMode, R3ComponentMetadata, R3DeferBlockMetadata, R3DirectiveDependencyMetadata, R3DirectiveMetadata, R3HostDirectiveMetadata, R3HostMetadata, R3InputMetadata, R3PipeDependencyMetadata, R3QueryMetadata, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata} from './render3/view/api';
import {compileComponentFromMetadata, compileDirectiveFromMetadata, ParsedHostBindings, parseHostBindings, verifyHostBindings} from './render3/view/compiler';
import type {BoundTarget} from './render3/view/t2_api';
import {R3TargetBinder} from './render3/view/t2_binder';
@@ -195,6 +195,7 @@ export class CompilerFacadeImpl implements CompilerFacade {
deferBlocks,
deferrableTypes: new Map(),
deferrableDeclToImportDecl: new Map(),
deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock,
styles: [...facade.styles, ...template.styles],
encapsulation: facade.encapsulation,
@@ -477,6 +478,7 @@ function convertDeclareComponentFacadeToMetadata(
deferBlocks,
deferrableTypes: new Map(),
deferrableDeclToImportDecl: new Map(),
deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock,
changeDetection: decl.changeDetection ?? ChangeDetectionStrategy.Default,
encapsulation: decl.encapsulation ?? ViewEncapsulation.Emulated,
@@ -72,8 +72,8 @@ export function compileClassMetadata(metadata: R3ClassMetadata): o.Expression {
* check to tree-shake away this code in production mode.
*/
export function compileComponentClassMetadata(
metadata: R3ClassMetadata, deferrableTypes: Map<string, string>): o.Expression {
if (deferrableTypes.size === 0) {
metadata: R3ClassMetadata, deferrableTypes: Map<string, string>|null): o.Expression {
if (deferrableTypes === null || deferrableTypes.size === 0) {
// If there are no deferrable symbols - just generate a regular `setClassMetadata` call.
return compileClassMetadata(metadata);
}
+32 -6
View File
@@ -125,6 +125,31 @@ export interface R3DirectiveMetadata {
hostDirectives: R3HostDirectiveMetadata[]|null;
}
/**
* Defines how dynamic imports for deferred dependencies should be emitted in the
* generated output:
* - either in a function on per-component basis (in case of local compilation)
* - or in a function on per-block basis (in full compilation mode)
*/
export const enum DeferBlockDepsEmitMode {
/**
* Dynamic imports are grouped on per-block basis.
*
* This is used in full compilation mode, when compiler has more information
* about particular dependencies that belong to this block.
*/
PerBlock,
/**
* Dynamic imports are grouped on per-component basis.
*
* In local compilation, compiler doesn't have enough information to determine
* which deferred dependencies belong to which block. In this case we group all
* dynamic imports into a single file on per-component basis.
*/
PerComponent,
}
/**
* Specifies how a list of declaration type references should be emitted into the generated code.
*/
@@ -247,14 +272,15 @@ export interface R3ComponentMetadata<DeclarationT extends R3TemplateDependency>
*/
deferBlocks: Map<t.DeferredBlock, R3DeferBlockMetadata>;
/**
* Defines how dynamic imports for deferred dependencies should be grouped:
* - either in a function on per-component basis (in case of local compilation)
* - or in a function on per-block basis (in full compilation mode)
*/
deferBlockDepsEmitMode: DeferBlockDepsEmitMode;
/**
* Map of deferrable symbol names -> corresponding import paths.
*
* This map is populated **only** in local compilation mode and used by the
* TemplateDefinitionBuilder to produce a defer function that loads
* all dependencies. In full compilation mode this information is defined
* on a `@defer` block level instead and dependency function is generated
* on per-block level.
*/
deferrableTypes: Map<string, string>;
@@ -26,7 +26,7 @@ import {BoundEvent} from '../r3_ast';
import {Identifiers as R3} from '../r3_identifiers';
import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, R3CompiledExpression, typeWithParameters} from '../util';
import {DeclarationListEmitMode, R3ComponentMetadata, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata, R3TemplateDependency} from './api';
import {DeclarationListEmitMode, DeferBlockDepsEmitMode, R3ComponentMetadata, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata, R3TemplateDependency} from './api';
import {MIN_STYLING_BINDING_SLOTS_REQUIRED, StylingBuilder, StylingInstructionCall} from './styling_builder';
import {BindingScope, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn, TemplateDefinitionBuilder, ValueConverter} from './template';
import {asLiteral, conditionallyCreateDirectiveBindingLiteral, CONTEXT_NAME, DefinitionMap, getInstructionStatements, getQueryPredicate, Instruction, RENDER_FLAGS, TEMPORARY_NAME, temporaryAllocator} from './util';
@@ -219,11 +219,9 @@ export function compileComponentFromMetadata(
// This is the main path currently used in compilation, which compiles the template with the
// legacy `TemplateDefinitionBuilder`.
// `deferrableTypes` become available only when local compilation mode is
// activated, in which case we generate a single function with all deferred
// dependencies.
let allDeferrableDepsFn: o.ReadVarExpr|null = null;
if (meta.deferBlocks.size > 0 && meta.deferrableTypes.size > 0) {
if (meta.deferBlocks.size > 0 && meta.deferrableTypes.size > 0 &&
meta.deferBlockDepsEmitMode === DeferBlockDepsEmitMode.PerComponent) {
const fnName = `${templateTypeName}_DeferFn`;
allDeferrableDepsFn = createDeferredDepsFunction(constantPool, fnName, meta.deferrableTypes);
}