Skip to content

Commit 3263df2

Browse files
pmvaldthePunderWoman
authored andcommitted
feat(compiler-cli): generate global imports in local compilation mode (#53543)
With option `generateExtraImportsInLocalMode` set in local compilation mode, the compiler generates extra side effect imports using this rule: any external module from which an identifier is imported into an NgModule will be added as side effect import to every file in the compilation unit. To illustrate this better assume the compilation unit has source files `a.ts` and `b.ts`, and: ``` // a.ts import {SomeExternalStuff} from 'path/to/some_where'; import {SomeExternalStuff2} from 'path/to/some_where2'; ... @NgModule({imports: [SomeExternalStuff]}) ``` then the extra import `import "path/to/some_where"` will be added to both `a.js` and `b.js`. Note that this is not the case for `import "path/to/some_where2"` though, since the symbol `SomeExternalStuff2` is not imported into any NgModule. The math behind this mechanism is, in local compilation mode we cannot resolve component external dependencies fully. For example if a component in `a.ts` uses an external component defined in an external file `some_external_comp.ts` then we can generate the import to this file in `a.js`. Instead, we want to generate an import to a file that "gurantees" that `a.js` is placed after `some_external_comp.js` in the bundle. Now since the component in `some_external_comp.ts` is used in `a.ts`, then there must be a chain of imports starting from the NgModule that declares the component in `a.ts` to the component in `some_external_comp.ts`. This chain means some file in the same compilation unit as `a.ts` should import some external NgModule which includes `some_external_comp.ts` in its transitive closure and import it to some NgModule. So by adding this import to `a.js` we ensure that the bundling will have the right order. PR Close #53543
1 parent 5feed80 commit 3263df2

File tree

5 files changed

+257
-23
lines changed

5 files changed

+257
-23
lines changed

packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import {compileClassMetadata, compileDeclareClassMetadata, compileDeclareInjecto
1010
import ts from 'typescript';
1111

1212
import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../../diagnostics';
13-
import {assertSuccessfulReferenceEmit, Reference, ReferenceEmitter} from '../../../imports';
13+
import {assertSuccessfulReferenceEmit, LocalCompilationExtraImportsTracker, Reference, ReferenceEmitter} from '../../../imports';
1414
import {isArrayEqual, isReferenceEqual, isSymbolEqual, SemanticDepGraphUpdater, SemanticReference, SemanticSymbol,} from '../../../incremental/semantic_graph';
1515
import {ExportedProviderStatusResolver, MetadataReader, MetadataRegistry, MetaKind} from '../../../metadata';
16-
import {PartialEvaluator, ResolvedValue, SyntheticValue} from '../../../partial_evaluator';
16+
import {DynamicValue, PartialEvaluator, ResolvedValue, SyntheticValue} from '../../../partial_evaluator';
1717
import {PerfEvent, PerfRecorder} from '../../../perf';
1818
import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost, reflectObjectLiteral,} from '../../../reflection';
1919
import {LocalModuleScopeRegistry, ScopeData} from '../../../scope';
@@ -179,7 +179,9 @@ export class NgModuleDecoratorHandler implements
179179
private onlyPublishPublicTypings: boolean,
180180
private injectableRegistry: InjectableClassRegistry, private perf: PerfRecorder,
181181
private includeClassMetadata: boolean, private includeSelectorScope: boolean,
182-
private readonly compilationMode: CompilationMode) {}
182+
private readonly compilationMode: CompilationMode,
183+
private readonly localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker|
184+
null) {}
183185

184186
readonly precedence = HandlerPrecedence.PRIMARY;
185187
readonly name = 'NgModuleDecoratorHandler';
@@ -240,9 +242,10 @@ export class NgModuleDecoratorHandler implements
240242
const rawDeclarations: ts.Expression|null = ngModule.get('declarations') ?? null;
241243
if (this.compilationMode !== CompilationMode.LOCAL && rawDeclarations !== null) {
242244
const declarationMeta = this.evaluator.evaluate(rawDeclarations, forwardRefResolver);
243-
declarationRefs =
244-
this.resolveTypeList(rawDeclarations, declarationMeta, name, 'declarations', 0)
245-
.references;
245+
declarationRefs = this.resolveTypeList(
246+
rawDeclarations, declarationMeta, name, 'declarations', 0,
247+
/* allowUnresolvedReferences */ false)
248+
.references;
246249

247250
// Look through the declarations to make sure they're all a part of the current compilation.
248251
for (const ref of declarationRefs) {
@@ -267,17 +270,41 @@ export class NgModuleDecoratorHandler implements
267270
// Resolving imports
268271
let importRefs: Reference<ClassDeclaration>[] = [];
269272
let rawImports: ts.Expression|null = ngModule.get('imports') ?? null;
270-
if (this.compilationMode !== CompilationMode.LOCAL && rawImports !== null) {
273+
if (rawImports !== null) {
271274
const importsMeta = this.evaluator.evaluate(rawImports, moduleResolvers);
272-
importRefs = this.resolveTypeList(rawImports, importsMeta, name, 'imports', 0).references;
275+
276+
const result = this.resolveTypeList(
277+
rawImports, importsMeta, name, 'imports', 0,
278+
this.compilationMode === CompilationMode.LOCAL);
279+
280+
if (this.compilationMode === CompilationMode.LOCAL &&
281+
this.localCompilationExtraImportsTracker !== null) {
282+
// For generating extra imports in local mode, the NgModule imports that are from external
283+
// files (i.e., outside of the compilation unit) are to be added to all the files in the
284+
// compilation unit. This is because any external component that is a dependency of some
285+
// component in the compilation unit must be imported by one of these NgModule's external
286+
// imports (or the external component cannot be a dependency of that internal component).
287+
// This approach can be further optimized by adding these NgModule external imports to a
288+
// subset of files in the compilation unit and not all. See comments in {@link
289+
// LocalCompilationExtraImportsTracker} and {@link
290+
// LocalCompilationExtraImportsTracker#addGlobalImportFromIdentifier} for more details.
291+
for (const d of result.dynamicValues) {
292+
this.localCompilationExtraImportsTracker.addGlobalImportFromIdentifier(d.node);
293+
}
294+
}
295+
296+
importRefs = result.references;
273297
}
274298

275299
// Resolving exports
276300
let exportRefs: Reference<ClassDeclaration>[] = [];
277301
const rawExports: ts.Expression|null = ngModule.get('exports') ?? null;
278302
if (this.compilationMode !== CompilationMode.LOCAL && rawExports !== null) {
279303
const exportsMeta = this.evaluator.evaluate(rawExports, moduleResolvers);
280-
exportRefs = this.resolveTypeList(rawExports, exportsMeta, name, 'exports', 0).references;
304+
exportRefs = this.resolveTypeList(
305+
rawExports, exportsMeta, name, 'exports', 0,
306+
/* allowUnresolvedReferences */ false)
307+
.references;
281308
this.referencesRegistry.add(node, ...exportRefs);
282309
}
283310

@@ -286,8 +313,10 @@ export class NgModuleDecoratorHandler implements
286313
const rawBootstrap: ts.Expression|null = ngModule.get('bootstrap') ?? null;
287314
if (this.compilationMode !== CompilationMode.LOCAL && rawBootstrap !== null) {
288315
const bootstrapMeta = this.evaluator.evaluate(rawBootstrap, forwardRefResolver);
289-
bootstrapRefs =
290-
this.resolveTypeList(rawBootstrap, bootstrapMeta, name, 'bootstrap', 0).references;
316+
bootstrapRefs = this.resolveTypeList(
317+
rawBootstrap, bootstrapMeta, name, 'bootstrap', 0,
318+
/* allowUnresolvedReferences */ false)
319+
.references;
291320

292321
// Verify that the `@NgModule.bootstrap` list doesn't have Standalone Components.
293322
for (const ref of bootstrapRefs) {
@@ -425,8 +454,9 @@ export class NgModuleDecoratorHandler implements
425454
for (const importExpr of topLevelExpressions) {
426455
const resolved = this.evaluator.evaluate(importExpr, moduleResolvers);
427456

428-
const {references, hasModuleWithProviders} =
429-
this.resolveTypeList(importExpr, [resolved], node.name.text, 'imports', absoluteIndex);
457+
const {references, hasModuleWithProviders} = this.resolveTypeList(
458+
importExpr, [resolved], node.name.text, 'imports', absoluteIndex,
459+
/* allowUnresolvedReferences */ false);
430460
absoluteIndex += references.length;
431461

432462
topLevelImports.push({
@@ -840,11 +870,24 @@ export class NgModuleDecoratorHandler implements
840870
*/
841871
private resolveTypeList(
842872
expr: ts.Node, resolvedList: ResolvedValue, className: string, arrayName: string,
843-
absoluteIndex: number):
844-
{references: Reference<ClassDeclaration>[], hasModuleWithProviders: boolean} {
873+
absoluteIndex: number, allowUnresolvedReferences: boolean): {
874+
references: Reference<ClassDeclaration>[],
875+
hasModuleWithProviders: boolean,
876+
dynamicValues: DynamicValue[]
877+
} {
845878
let hasModuleWithProviders = false;
846879
const refList: Reference<ClassDeclaration>[] = [];
880+
const dynamicValueSet = new Set<DynamicValue>();
881+
847882
if (!Array.isArray(resolvedList)) {
883+
if (allowUnresolvedReferences) {
884+
return {
885+
references: [],
886+
hasModuleWithProviders: false,
887+
dynamicValues: [],
888+
};
889+
}
890+
848891
throw createValueHasWrongTypeError(
849892
expr, resolvedList,
850893
`Expected array when reading the NgModule.${arrayName} of ${className}`);
@@ -864,9 +907,14 @@ export class NgModuleDecoratorHandler implements
864907

865908
if (Array.isArray(entry)) {
866909
// Recurse into nested arrays.
867-
const recursiveResult =
868-
this.resolveTypeList(expr, entry, className, arrayName, absoluteIndex);
910+
const recursiveResult = this.resolveTypeList(
911+
expr, entry, className, arrayName, absoluteIndex, allowUnresolvedReferences);
869912
refList.push(...recursiveResult.references);
913+
914+
for (const d of recursiveResult.dynamicValues) {
915+
dynamicValueSet.add(d);
916+
}
917+
870918
absoluteIndex += recursiveResult.references.length;
871919
hasModuleWithProviders = hasModuleWithProviders || recursiveResult.hasModuleWithProviders;
872920
} else if (entry instanceof Reference) {
@@ -878,6 +926,9 @@ export class NgModuleDecoratorHandler implements
878926
}
879927
refList.push(entry);
880928
absoluteIndex += 1;
929+
} else if (entry instanceof DynamicValue && allowUnresolvedReferences) {
930+
dynamicValueSet.add(entry);
931+
continue;
881932
} else {
882933
// TODO(alxhub): Produce a better diagnostic here - the array index may be an inner array.
883934
throw createValueHasWrongTypeError(
@@ -890,6 +941,7 @@ export class NgModuleDecoratorHandler implements
890941
return {
891942
references: refList,
892943
hasModuleWithProviders,
944+
dynamicValues: [...dynamicValueSet],
893945
};
894946
}
895947
}

packages/compiler-cli/src/ngtsc/annotations/ng_module/test/ng_module_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function setup(program: ts.Program, compilationMode = CompilationMode.FULL) {
4444
/* isCore */ false, refEmitter,
4545
/* annotateForClosureCompiler */ false,
4646
/* onlyPublishPublicTypings */ false, injectableRegistry, NOOP_PERF_RECORDER, true, true,
47-
compilationMode);
47+
compilationMode, /* localCompilationExtraImportsTracker */ null);
4848

4949
return {handler, reflectionHost};
5050
}

packages/compiler-cli/src/ngtsc/core/src/compiler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,7 @@ export class NgCompiler {
10971097

10981098
let localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker|null = null;
10991099
if (compilationMode === CompilationMode.LOCAL && this.options.generateExtraImportsInLocalMode) {
1100-
localCompilationExtraImportsTracker = new LocalCompilationExtraImportsTracker();
1100+
localCompilationExtraImportsTracker = new LocalCompilationExtraImportsTracker(checker);
11011101
}
11021102

11031103
// Cycles are handled in full compilation mode by "remote scoping".
@@ -1172,7 +1172,7 @@ export class NgCompiler {
11721172
exportedProviderStatusResolver, semanticDepGraphUpdater, isCore, refEmitter,
11731173
this.closureCompilerEnabled, this.options.onlyPublishPublicTypingsForNgModules ?? false,
11741174
injectableRegistry, this.delegatingPerfRecorder, supportTestBed, supportJitMode,
1175-
compilationMode),
1175+
compilationMode, localCompilationExtraImportsTracker),
11761176
];
11771177

11781178
const traitCompiler = new TraitCompiler(

packages/compiler-cli/src/ngtsc/imports/src/local_compilation_extra_imports_tracker.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import ts from 'typescript';
1010

11+
import {getContainingImportDeclaration} from '../../reflection/src/typescript';
12+
1113

1214
/**
1315
* A tool to track extra imports to be added to the generated files in the local compilation mode.
@@ -32,6 +34,10 @@ import ts from 'typescript';
3234
*
3335
*/
3436
export class LocalCompilationExtraImportsTracker {
37+
private readonly globalImportsSet = new Set<string>();
38+
39+
constructor(private readonly typeChecker: ts.TypeChecker) {}
40+
3541
/**
3642
* Adds an extra import to be added to the generated file of a specific source file.
3743
*/
@@ -51,14 +57,41 @@ export class LocalCompilationExtraImportsTracker {
5157
* to smallest possible candidate files instead of all files.
5258
*/
5359
addGlobalImportFromIdentifier(node: ts.Node): void {
54-
// TODO(pmvald): Implement this method.
60+
let identifier: ts.Identifier|null = null;
61+
if (ts.isIdentifier(node)) {
62+
identifier = node;
63+
} else if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
64+
identifier = node.expression;
65+
}
66+
67+
if (identifier === null) {
68+
return;
69+
}
70+
71+
const sym = this.typeChecker.getSymbolAtLocation(identifier);
72+
if (!sym?.declarations?.length) {
73+
return;
74+
}
75+
76+
77+
const importClause = sym.declarations[0];
78+
const decl = getContainingImportDeclaration(importClause);
79+
80+
if (decl !== null) {
81+
this.globalImportsSet.add(removeQuotations(decl.moduleSpecifier.getText()));
82+
}
5583
}
5684

5785
/**
5886
* Returns the list of all module names that the given file should include as its extra imports.
5987
*/
6088
getImportsForFile(sf: ts.SourceFile): string[] {
61-
// TODO(pmvald): Implement this method.
62-
return [];
89+
return [
90+
...this.globalImportsSet,
91+
];
6392
}
6493
}
94+
95+
function removeQuotations(s: string): string {
96+
return s.substring(1, s.length - 1).trim();
97+
}

0 commit comments

Comments
 (0)