Skip to content

Commit

Permalink
feat(compiler-cli): generate extra imports for component local depend…
Browse files Browse the repository at this point in the history
…encies in local mode (#53543)

With option `generateExtraImportsInLocalMode` set, in local mode the compiler generates extra imports for each component local dependencies. Here local dependencies means all component's dependencies within the same compilation unit.

To achieve this, the compiler performs a "local version" of its regular static analysis to find each component's deps, and these deps are used to generate extra side effect imports.

PR Close #53543
  • Loading branch information
pmvald authored and thePunderWoman committed Jan 30, 2024
1 parent f6e9dbb commit 7e861c6
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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 {AnimationTriggerNames, BoundTarget, compileClassDebugInfo, compileComponentClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, DomElementSchemaRegistry, Expression, ExternalExpr, 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';
Expand Down Expand Up @@ -960,6 +960,25 @@ export class ComponentDecoratorHandler implements
}

data.declarations = eagerDeclarations;

// Register extra local imports.
if (this.compilationMode === CompilationMode.LOCAL &&
this.localCompilationExtraImportsTracker !== null) {
// In global compilation mode `eagerDeclarations` contains "all" the component
// dependencies, whose import statements will be added to the file. In local compilation
// mode `eagerDeclarations` only includes the "local" dependencies, meaning those that are
// declared inside this compilation unit.Here the import info of these local dependencies
// are added to the tracker so that we can generate extra imports representing these local
// dependencies. For non-local dependencies we use another technique of adding some
// best-guess extra imports globally to all files using
// `localCompilationExtraImportsTracker.addGlobalImportFromIdentifier`.
for (const {type} of eagerDeclarations) {
if (type instanceof ExternalExpr && type.value.moduleName) {
this.localCompilationExtraImportsTracker.addImportForFile(
context, type.value.moduleName);
}
}
}
} else {
if (this.cycleHandlingStrategy === CycleHandlingStrategy.UseRemoteScoping) {
// Declaring the directiveDefs/pipeDefs arrays directly would require imports that would
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,11 @@ export class NgModuleDecoratorHandler implements
// Resolving declarations
let declarationRefs: Reference<ClassDeclaration>[] = [];
const rawDeclarations: ts.Expression|null = ngModule.get('declarations') ?? null;
if (this.compilationMode !== CompilationMode.LOCAL && rawDeclarations !== null) {
if (rawDeclarations !== null) {
const declarationMeta = this.evaluator.evaluate(rawDeclarations, forwardRefResolver);
declarationRefs = this.resolveTypeList(
rawDeclarations, declarationMeta, name, 'declarations', 0,
/* allowUnresolvedReferences */ false)
this.compilationMode === CompilationMode.LOCAL)
.references;

// Look through the declarations to make sure they're all a part of the current compilation.
Expand Down Expand Up @@ -299,11 +299,11 @@ export class NgModuleDecoratorHandler implements
// Resolving exports
let exportRefs: Reference<ClassDeclaration>[] = [];
const rawExports: ts.Expression|null = ngModule.get('exports') ?? null;
if (this.compilationMode !== CompilationMode.LOCAL && rawExports !== null) {
if (rawExports !== null) {
const exportsMeta = this.evaluator.evaluate(rawExports, moduleResolvers);
exportRefs = this.resolveTypeList(
rawExports, exportsMeta, name, 'exports', 0,
/* allowUnresolvedReferences */ false)
this.compilationMode === CompilationMode.LOCAL)
.references;
this.referencesRegistry.add(node, ...exportRefs);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {getContainingImportDeclaration} from '../../reflection/src/typescript';
*
*/
export class LocalCompilationExtraImportsTracker {
private readonly localImportsMap = new Map<string, Set<string>>();
private readonly globalImportsSet = new Set<string>();

constructor(private readonly typeChecker: ts.TypeChecker) {}
Expand All @@ -42,7 +43,11 @@ export class LocalCompilationExtraImportsTracker {
* Adds an extra import to be added to the generated file of a specific source file.
*/
addImportForFile(sf: ts.SourceFile, moduleName: string): void {
// TODO(pmvald): Implement this method.
if (!this.localImportsMap.has(sf.fileName)) {
this.localImportsMap.set(sf.fileName, new Set<string>());
}

this.localImportsMap.get(sf.fileName)!.add(moduleName);
}

/**
Expand Down Expand Up @@ -88,6 +93,7 @@ export class LocalCompilationExtraImportsTracker {
getImportsForFile(sf: ts.SourceFile): string[] {
return [
...this.globalImportsSet,
...(this.localImportsMap.get(sf.fileName) ?? []),
];
}
}
Expand Down
62 changes: 62 additions & 0 deletions packages/compiler-cli/test/ngtsc/local_compilation_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,68 @@ runInEachFileSystem(
expect(env.getContents('test.js')).toContain('import "/some_external_file"');
expect(env.getContents('test.js')).toContain('import "/some_external_file2"');
});

it('should include extra import for the local component dependencies (component, directive and pipe)',
() => {
env.write('internal_comp.ts', `
import {Component} from '@angular/core';
@Component({template: '...', selector: 'internal-comp'})
export class InternalComp {
}
`);
env.write('internal_dir.ts', `
import {Directive} from '@angular/core';
@Directive({selector: '[internal-dir]'})
export class InternalDir {
}
`);
env.write('internal_pipe.ts', `
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({name: 'internalPipe'})
export class InternalPipe implements PipeTransform {
transform(value: number): number {
return value*2;
}
}
`);
env.write('internal_module.ts', `
import {NgModule} from '@angular/core';
import {InternalComp} from 'internal_comp';
import {InternalDir} from 'internal_dir';
import {InternalPipe} from 'internal_pipe';
@NgModule({declarations: [InternalComp, InternalDir, InternalPipe], exports: [InternalComp, InternalDir, InternalPipe]})
export class InternalModule {
}
`);
env.write('main_comp.ts', `
import {Component} from '@angular/core';
@Component({template: '<internal-comp></internal-comp> <span internal-dir></span> <span>{{2 | internalPipe}}</span>'})
export class MainComp {
}
`);
env.write('main_module.ts', `
import {NgModule} from '@angular/core';
import {MainComp} from 'main_comp';
import {InternalModule} from 'internal_module';
@NgModule({declarations: [MainComp], imports: [InternalModule]})
export class MainModule {
}
`);

env.driveMain();

expect(env.getContents('main_comp.js')).toContain('import "internal_comp"');
expect(env.getContents('main_comp.js')).toContain('import "internal_dir"');
expect(env.getContents('main_comp.js')).toContain('import "internal_pipe"');
});
});

describe('ng module injector def', () => {
Expand Down

0 comments on commit 7e861c6

Please sign in to comment.