feat(ivy): produce and consume ES2015 re-exports for NgModule re-exports (#28852)

In certain configurations (such as the g3 repository) which have lots of
small compilation units as well as strict dependency checking on generated
code, ngtsc's default strategy of directly importing directives/pipes into
components will not work. To handle these cases, an additional mode is
introduced, and is enabled when using the FileToModuleHost provided by such
compilation environments.

In this mode, when ngtsc encounters an NgModule which re-exports another
from a different file, it will re-export all the directives it contains at
the ES2015 level. The exports will have a predictable name based on the
FileToModuleHost. For example, if the host says that a directive Foo is
from the 'root/external/foo' module, ngtsc will add:

```
export {Foo as ɵng$root$external$foo$$Foo} from 'root/external/foo';
```

Consumers of the re-exported directive will then import it via this path
instead of directly from root/external/foo, preserving strict dependency
semantics.

PR Close #28852
This commit is contained in:
Alex Rickabaugh 2019-02-19 17:36:26 -08:00 committed by Ben Lesh
parent 15c065f9a0
commit c1392ce618
21 changed files with 608 additions and 60 deletions

View File

@ -72,9 +72,10 @@ export class DecorationAnalyzer {
// on whether a bestGuessOwningModule is present in the Reference.
new LogicalProjectStrategy(this.typeChecker, new LogicalFileSystem(this.rootDirs)),
]);
dtsModuleScopeResolver =
new MetadataDtsModuleScopeResolver(this.typeChecker, this.reflectionHost);
scopeRegistry = new LocalModuleScopeRegistry(this.dtsModuleScopeResolver);
dtsModuleScopeResolver = new MetadataDtsModuleScopeResolver(
this.typeChecker, this.reflectionHost, /* aliasGenerator */ null);
scopeRegistry = new LocalModuleScopeRegistry(
this.dtsModuleScopeResolver, this.refEmitter, /* aliasGenerator */ null);
evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker);
moduleResolver = new ModuleResolver(this.program, this.options, this.host);
importGraph = new ImportGraph(this.moduleResolver);

View File

@ -16,7 +16,7 @@ import {ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection';
import {LocalModuleScopeRegistry, ScopeDirective, extractDirectiveGuards} from '../../scope';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform';
import {TypeCheckContext} from '../../typecheck';
import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
@ -326,7 +326,7 @@ export class ComponentDecoratorHandler implements
}
}
resolve(node: ts.ClassDeclaration, analysis: ComponentHandlerData): void {
resolve(node: ts.ClassDeclaration, analysis: ComponentHandlerData): ResolveResult {
const context = node.getSourceFile();
// Check whether this component was registered with an NgModule. If so, it should be compiled
// under that module's compilation scope.
@ -361,6 +361,7 @@ export class ComponentDecoratorHandler implements
this.scopeRegistry.setComponentAsRequiringRemoteScoping(node);
}
}
return {};
}
compile(node: ts.ClassDeclaration, analysis: ComponentHandlerData, pool: ConstantPool):

View File

@ -15,7 +15,7 @@ import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator';
import {Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection';
import {NgModuleRouteAnalyzer} from '../../routing';
import {LocalModuleScopeRegistry} from '../../scope';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform';
import {getSourceFile} from '../../util/src/typescript';
import {generateSetClassMetadataCall} from './metadata';
@ -175,6 +175,17 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
};
}
resolve(node: ts.Declaration, analysis: NgModuleAnalysis): ResolveResult {
const scope = this.scopeRegistry.getScopeOfModule(node);
if (scope === null || scope.reexports === null) {
return {};
} else {
return {
reexports: scope.reexports,
};
}
}
compile(node: ts.ClassDeclaration, analysis: NgModuleAnalysis): CompileResult[] {
const ngInjectorDef = compileInjector(analysis.ngInjectorDef);
const ngModuleDef = compileNgModule(analysis.ngModuleDef);

View File

@ -48,8 +48,9 @@ describe('ComponentDecoratorHandler', () => {
const moduleResolver = new ModuleResolver(program, options, host);
const importGraph = new ImportGraph(moduleResolver);
const cycleAnalyzer = new CycleAnalyzer(importGraph);
const scopeRegistry =
new LocalModuleScopeRegistry(new MetadataDtsModuleScopeResolver(checker, reflectionHost));
const scopeRegistry = new LocalModuleScopeRegistry(
new MetadataDtsModuleScopeResolver(checker, reflectionHost, null), new ReferenceEmitter([]),
null);
const refEmitter = new ReferenceEmitter([]);
const handler = new ComponentDecoratorHandler(

View File

@ -8,6 +8,7 @@
import * as ts from 'typescript';
import {ReferenceEmitter} from '../../imports';
import {PartialEvaluator} from '../../partial_evaluator';
import {TypeScriptReflectionHost} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
@ -39,8 +40,9 @@ describe('DirectiveDecoratorHandler', () => {
const checker = program.getTypeChecker();
const reflectionHost = new TestReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const scopeRegistry =
new LocalModuleScopeRegistry(new MetadataDtsModuleScopeResolver(checker, reflectionHost));
const scopeRegistry = new LocalModuleScopeRegistry(
new MetadataDtsModuleScopeResolver(checker, reflectionHost, null), new ReferenceEmitter([]),
null);
const handler = new DirectiveDecoratorHandler(reflectionHost, evaluator, scopeRegistry, false);
const analyzeDirective = (dirName: string) => {

View File

@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
export {AliasGenerator, AliasStrategy} from './src/alias';
export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core';
export {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter} from './src/emitter';
export {Reexport} from './src/reexport';
export {ImportMode, OwningModule, Reference} from './src/references';
export {ModuleResolver} from './src/resolver';

View File

@ -0,0 +1,46 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {Expression, ExternalExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {FileToModuleHost, ReferenceEmitStrategy} from './emitter';
import {ImportMode, Reference} from './references';
// Escape anything that isn't alphanumeric, '/', '_', '.', or '$'.
const CHARS_TO_ESCAPE = /[^a-zA-Z0-9/_\.$]/g;
export class AliasGenerator {
constructor(private fileToModuleHost: FileToModuleHost) {}
aliasSymbolName(decl: ts.Declaration, context: ts.SourceFile): string {
if (!ts.isClassDeclaration(decl)) {
throw new Error(`Attempt to write an alias to something which isn't a class`);
}
// The declared module is used to get the name of the alias.
const declModule =
this.fileToModuleHost.fileNameToModuleName(decl.getSourceFile().fileName, context.fileName);
const replaced = declModule.replace(CHARS_TO_ESCAPE, '_').replace(/\//g, '$');
return 'ɵng$' + replaced + '$$' + decl.name !.text;
}
aliasTo(decl: ts.Declaration, via: ts.SourceFile): Expression {
const name = this.aliasSymbolName(decl, via);
// viaModule is the module it'll actually be imported from.
const moduleName = this.fileToModuleHost.fileNameToModuleName(via.fileName, via.fileName);
return new ExternalExpr({moduleName, name});
}
}
export class AliasStrategy implements ReferenceEmitStrategy {
emit(ref: Reference<ts.Node>, context: ts.SourceFile, importMode: ImportMode): Expression|null {
return ref.alias;
}
}

View File

@ -0,0 +1,13 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
export interface Reexport {
symbolName: string;
asAlias: string;
fromModule: string;
}

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Expression} from '@angular/compiler';
import * as ts from 'typescript';
import {identifierOfNode} from '../../util/src/typescript';
@ -48,6 +49,8 @@ export class Reference<T extends ts.Node = ts.Node> {
private identifiers: ts.Identifier[] = [];
private _alias: Expression|null = null;
constructor(readonly node: T, bestGuessOwningModule: OwningModule|null = null) {
this.bestGuessOwningModule = bestGuessOwningModule;
@ -87,6 +90,9 @@ export class Reference<T extends ts.Node = ts.Node> {
return id !== null ? id.text : null;
}
get alias(): Expression|null { return this._alias; }
/**
* Record a `ts.Identifier` by which it's valid to refer to this node, within the context of this
* `Reference`.
@ -100,4 +106,18 @@ export class Reference<T extends ts.Node = ts.Node> {
getIdentityIn(context: ts.SourceFile): ts.Identifier|null {
return this.identifiers.find(id => id.getSourceFile() === context) || null;
}
cloneWithAlias(alias: Expression): Reference<T> {
const ref = new Reference(this.node, this.bestGuessOwningModule);
ref.identifiers = [...this.identifiers];
ref._alias = alias;
return ref;
}
cloneWithNoIdentifiers(): Reference<T> {
const ref = new Reference(this.node, this.bestGuessOwningModule);
ref._alias = this._alias;
ref.identifiers = [];
return ref;
}
}

View File

@ -17,7 +17,7 @@ import {BaseDefDecoratorHandler} from './annotations/src/base_def';
import {CycleAnalyzer, ImportGraph} from './cycles';
import {ErrorCode, ngErrorCode} from './diagnostics';
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
import {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
import {AbsoluteModuleStrategy, AliasGenerator, AliasStrategy, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
import {PartialEvaluator} from './partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from './path';
import {TypeScriptReflectionHost} from './reflection';
@ -27,6 +27,7 @@ import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from './scope'
import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, generatedFactoryTransform} from './shims';
import {ivySwitchTransform} from './switch';
import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform';
import {aliasTransformFactory} from './transform/src/alias';
import {TypeCheckContext, TypeCheckProgramHost} from './typecheck';
import {normalizeSeparators} from './util/src/path';
import {getRootDirs, isDtsPath} from './util/src/typescript';
@ -272,10 +273,16 @@ export class NgtscProgram implements api.Program {
};
const customTransforms = opts && opts.customTransformers;
const beforeTransforms = [ivyTransformFactory(
const beforeTransforms = [
ivyTransformFactory(
compilation, this.reflector, this.importRewriter, this.isCore,
this.closureCompilerEnabled)];
const afterDeclarationsTransforms = [declarationTransformFactory(compilation)];
this.closureCompilerEnabled),
aliasTransformFactory(compilation.exportStatements) as ts.TransformerFactory<ts.SourceFile>,
];
const afterDeclarationsTransforms = [
declarationTransformFactory(compilation),
];
if (this.factoryToSourceInfo !== null) {
beforeTransforms.push(
@ -314,6 +321,8 @@ export class NgtscProgram implements api.Program {
private makeCompilation(): IvyCompilation {
const checker = this.tsProgram.getTypeChecker();
let aliasGenerator: AliasGenerator|null = null;
// Construct the ReferenceEmitter.
if (this.fileToModuleHost === null || !this.options._useHostForImportGeneration) {
// The CompilerHost doesn't have fileNameToModuleName, so build an NPM-centric reference
@ -333,14 +342,19 @@ export class NgtscProgram implements api.Program {
this.refEmitter = new ReferenceEmitter([
// First, try to use local identifiers if available.
new LocalIdentifierStrategy(),
// Then use aliased references (this is a workaround to StrictDeps checks).
new AliasStrategy(),
// Then use fileNameToModuleName to emit imports.
new FileToModuleStrategy(checker, this.fileToModuleHost),
]);
aliasGenerator = new AliasGenerator(this.fileToModuleHost);
}
const evaluator = new PartialEvaluator(this.reflector, checker);
const depScopeReader = new MetadataDtsModuleScopeResolver(checker, this.reflector);
const scopeRegistry = new LocalModuleScopeRegistry(depScopeReader);
const depScopeReader =
new MetadataDtsModuleScopeResolver(checker, this.reflector, aliasGenerator);
const scopeRegistry =
new LocalModuleScopeRegistry(depScopeReader, this.refEmitter, aliasGenerator);
// If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {Reference} from '../../imports';
import {AliasGenerator, Reference} from '../../imports';
import {ReflectionHost} from '../../reflection';
import {ExportScope, ScopeDirective, ScopePipe} from './api';
@ -31,7 +31,9 @@ export class MetadataDtsModuleScopeResolver {
*/
private cache = new Map<ts.ClassDeclaration, ExportScope|null>();
constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost) {}
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private aliasGenerator: AliasGenerator|null) {}
/**
* Resolve a `Reference`'d NgModule from a .d.ts file and produce a transitive `ExportScope`
@ -42,9 +44,10 @@ export class MetadataDtsModuleScopeResolver {
*/
resolve(ref: Reference<ts.ClassDeclaration>): ExportScope|null {
const clazz = ref.node;
if (!clazz.getSourceFile().isDeclarationFile) {
const sourceFile = clazz.getSourceFile();
if (!sourceFile.isDeclarationFile) {
throw new Error(
`Debug error: DtsModuleScopeResolver.read(${ref.debugName} from ${clazz.getSourceFile().fileName}), but not a .d.ts file`);
`Debug error: DtsModuleScopeResolver.read(${ref.debugName} from ${sourceFile.fileName}), but not a .d.ts file`);
}
if (this.cache.has(clazz)) {
@ -61,31 +64,64 @@ export class MetadataDtsModuleScopeResolver {
return null;
}
const declarations = new Set<ts.Declaration>();
for (const declRef of meta.declarations) {
declarations.add(declRef.node);
}
// Only the 'exports' field of the NgModule's metadata is important. Imports and declarations
// don't affect the export scope.
for (const exportRef of meta.exports) {
// Attempt to process the export as a directive.
const directive = this.readScopeDirectiveFromClassWithDef(exportRef);
if (directive !== null) {
if (!declarations.has(exportRef.node)) {
directives.push(this.maybeAlias(directive, sourceFile));
} else {
directives.push(directive);
}
continue;
}
// Attempt to process the export as a pipe.
const pipe = this.readScopePipeFromClassWithDef(exportRef);
if (pipe !== null) {
if (!declarations.has(exportRef.node)) {
pipes.push(this.maybeAlias(pipe, sourceFile));
} else {
pipes.push(pipe);
}
continue;
}
// Attempt to process the export as a module.
const exportScope = this.resolve(exportRef);
if (exportScope !== null) {
// It is a module. Add exported directives and pipes to the current scope.
// It is a module. Add exported directives and pipes to the current scope. This might
// involve rewriting the `Reference`s to those types to have an alias expression if one is
// required.
if (this.aliasGenerator === null) {
// Fast path when aliases aren't required.
directives.push(...exportScope.exported.directives);
pipes.push(...exportScope.exported.pipes);
continue;
} else {
// It's necessary to rewrite the `Reference`s to add alias expressions. This way, imports
// generated to these directives and pipes will use a shallow import to `sourceFile`
// instead of a deep import directly to the directive or pipe class.
//
// One important check here is whether the directive/pipe is declared in the same
// source file as the re-exporting NgModule. This can happen if both a directive, its
// NgModule, and the re-exporting NgModule are all in the same file. In this case,
// no import alias is needed as it would go to the same file anyway.
for (const directive of exportScope.exported.directives) {
directives.push(this.maybeAlias(directive, sourceFile));
}
for (const pipe of exportScope.exported.pipes) {
pipes.push(this.maybeAlias(pipe, sourceFile));
}
}
}
continue;
// The export was not a directive, a pipe, or a module. This is an error.
// TODO(alxhub): produce a ts.Diagnostic
@ -190,6 +226,22 @@ export class MetadataDtsModuleScopeResolver {
const name = type.literal.text;
return {ref, name};
}
private maybeAlias<T extends ScopeDirective|ScopePipe>(
dirOrPipe: T, maybeAliasFrom: ts.SourceFile): T {
if (this.aliasGenerator === null) {
return dirOrPipe;
}
const ref = dirOrPipe.ref;
if (ref.node.getSourceFile() !== maybeAliasFrom) {
return {
...dirOrPipe,
ref: ref.cloneWithAlias(this.aliasGenerator.aliasTo(ref.node, maybeAliasFrom)),
};
} else {
return dirOrPipe;
}
}
}
/**

View File

@ -6,9 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ExternalExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {Reference} from '../../imports';
import {AliasGenerator, Reexport, Reference, ReferenceEmitter} from '../../imports';
import {ExportScope, ScopeData, ScopeDirective, ScopePipe} from './api';
import {DtsModuleScopeResolver} from './dependency';
@ -19,14 +20,10 @@ export interface LocalNgModuleData {
exports: Reference<ts.Declaration>[];
}
/**
* A scope produced for an NgModule declared locally (in the current program being compiled).
*
* The `LocalModuleScope` contains the compilation scope, the transitive set of directives and pipes
* visible to any component declared in this module. It also contains an `ExportScope`, the
* transitive set of directives and pipes
*/
export interface LocalModuleScope extends ExportScope { compilation: ScopeData; }
export interface LocalModuleScope extends ExportScope {
compilation: ScopeData;
reexports: Reexport[]|null;
}
/**
* A registry which collects information about NgModules, Directives, Components, and Pipes which
@ -91,7 +88,9 @@ export class LocalModuleScopeRegistry {
*/
private remoteScoping = new Set<ts.Declaration>();
constructor(private dependencyScopeReader: DtsModuleScopeResolver) {}
constructor(
private dependencyScopeReader: DtsModuleScopeResolver, private refEmitter: ReferenceEmitter,
private aliasGenerator: AliasGenerator|null) {}
/**
* Add an NgModule's data to the registry.
@ -151,6 +150,9 @@ export class LocalModuleScopeRegistry {
const compilationDirectives = new Map<ts.Declaration, ScopeDirective>();
const compilationPipes = new Map<ts.Declaration, ScopePipe>();
const declared = new Set<ts.Declaration>();
const sourceFile = clazz.getSourceFile();
// Directives and pipes exported to any importing NgModules.
const exportDirectives = new Map<ts.Declaration, ScopeDirective>();
const exportPipes = new Map<ts.Declaration, ScopePipe>();
@ -181,6 +183,8 @@ export class LocalModuleScopeRegistry {
// ngtools tests rely on analysis of broken components.
continue;
}
declared.add(decl.node);
}
// 2) process imports.
@ -229,16 +233,55 @@ export class LocalModuleScopeRegistry {
}
}
const exported = {
directives: Array.from(exportDirectives.values()),
pipes: Array.from(exportPipes.values()),
};
let reexports: Reexport[]|null = null;
if (this.aliasGenerator !== null) {
reexports = [];
const addReexport = (ref: Reference<ts.Declaration>) => {
if (!declared.has(ref.node) && ref.node.getSourceFile() !== sourceFile) {
const exportName = this.aliasGenerator !.aliasSymbolName(ref.node, sourceFile);
if (ref.alias && ref.alias instanceof ExternalExpr) {
reexports !.push({
fromModule: ref.alias.value.moduleName !,
symbolName: ref.alias.value.name !,
asAlias: exportName,
});
} else {
const expr = this.refEmitter.emit(ref.cloneWithNoIdentifiers(), sourceFile);
if (!(expr instanceof ExternalExpr) || expr.value.moduleName === null ||
expr.value.name === null) {
throw new Error('Expected ExternalExpr');
}
reexports !.push({
fromModule: expr.value.moduleName,
symbolName: expr.value.name,
asAlias: exportName,
});
}
}
};
for (const {ref} of exported.directives) {
addReexport(ref);
}
for (const {ref} of exported.pipes) {
addReexport(ref);
}
}
// Finally, produce the `LocalModuleScope` with both the compilation and export scopes.
const scope = {
compilation: {
directives: Array.from(compilationDirectives.values()),
pipes: Array.from(compilationPipes.values()),
},
exported: {
directives: Array.from(exportDirectives.values()),
pipes: Array.from(exportPipes.values()),
},
exported,
reexports,
};
this.cache.set(clazz, scope);
return scope;

View File

@ -10,6 +10,7 @@ ts_library(
]),
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/scope",

View File

@ -6,17 +6,24 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ExternalExpr, ExternalReference} from '@angular/compiler';
import * as ts from 'typescript';
import {Reference} from '../../imports';
import {AliasGenerator, FileToModuleHost, Reference} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection';
import {makeProgram} from '../../testing/in_memory_typescript';
import {ExportScope} from '../src/api';
import {MetadataDtsModuleScopeResolver} from '../src/dependency';
const MODULE_FROM_NODE_MODULES_PATH = /.*node_modules\/(\w+)\/index\.d\.ts$/;
const testHost: FileToModuleHost = {
fileNameToModuleName: function(imported: string): string {
const res = MODULE_FROM_NODE_MODULES_PATH.exec(imported) !;
return 'root/' + res[1];
}
};
/**
* Simple metadata types are added to the top of each testing file, for convenience.
*/
@ -33,7 +40,8 @@ export declare type PipeMeta<A, B> = never;
* This returns both the `MetadataDtsModuleScopeResolver` and a `refs` object which can be
* destructured to retrieve references to specific declared classes.
*/
function makeTestEnv(modules: {[module: string]: string}): {
function makeTestEnv(
modules: {[module: string]: string}, aliasGenerator: AliasGenerator | null = null): {
refs: {[name: string]: Reference<ts.ClassDeclaration>},
resolver: MetadataDtsModuleScopeResolver,
} {
@ -46,8 +54,8 @@ function makeTestEnv(modules: {[module: string]: string}): {
});
const {program} = makeProgram(files);
const checker = program.getTypeChecker();
const resolver =
new MetadataDtsModuleScopeResolver(checker, new TypeScriptReflectionHost(checker));
const resolver = new MetadataDtsModuleScopeResolver(
checker, new TypeScriptReflectionHost(checker), aliasGenerator);
// Resolver for the refs object.
const get = (target: {}, name: string): Reference<ts.ClassDeclaration> => {
@ -135,10 +143,143 @@ describe('MetadataDtsModuleScopeResolver', () => {
// Explicitly verify that the directive has the correct owning module.
expect(scope.exported.directives[0].ref.ownedByModuleGuess).toBe('declaration');
});
it('should write correct aliases for deep dependencies', () => {
const {resolver, refs} = makeTestEnv(
{
'deep': `
export declare class DeepDir {
static ngDirectiveDef: DirectiveMeta<DeepDir, '[deep]', never, never, never, never>;
}
export declare class DeepModule {
static ngModuleDef: ModuleMeta<DeepModule, [typeof DeepDir], never, [typeof DeepDir]>;
}
`,
'middle': `
import * as deep from 'deep';
export declare class MiddleDir {
static ngDirectiveDef: DirectiveMeta<MiddleDir, '[middle]', never, never, never, never>;
}
export declare class MiddleModule {
static ngModuleDef: ModuleMeta<MiddleModule, [typeof MiddleDir], never, [typeof MiddleDir, typeof deep.DeepModule]>;
}
`,
'shallow': `
import * as middle from 'middle';
export declare class ShallowDir {
static ngDirectiveDef: DirectiveMeta<ShallowDir, '[middle]', never, never, never, never>;
}
export declare class ShallowModule {
static ngModuleDef: ModuleMeta<ShallowModule, [typeof ShallowDir], never, [typeof ShallowDir, typeof middle.MiddleModule]>;
}
`,
},
new AliasGenerator(testHost));
const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
expect(getAlias(DeepDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$deep$$DeepDir',
});
expect(getAlias(MiddleDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$middle$$MiddleDir',
});
expect(getAlias(ShallowDir)).toBeNull();
});
it('should write correct aliases for bare directives in exports', () => {
const {resolver, refs} = makeTestEnv(
{
'deep': `
export declare class DeepDir {
static ngDirectiveDef: DirectiveMeta<DeepDir, '[deep]', never, never, never, never>;
}
export declare class DeepModule {
static ngModuleDef: ModuleMeta<DeepModule, [typeof DeepDir], never, [typeof DeepDir]>;
}
`,
'middle': `
import * as deep from 'deep';
export declare class MiddleDir {
static ngDirectiveDef: DirectiveMeta<MiddleDir, '[middle]', never, never, never, never>;
}
export declare class MiddleModule {
static ngModuleDef: ModuleMeta<MiddleModule, [typeof MiddleDir], [typeof deep.DeepModule], [typeof MiddleDir, typeof deep.DeepDir]>;
}
`,
'shallow': `
import * as middle from 'middle';
export declare class ShallowDir {
static ngDirectiveDef: DirectiveMeta<ShallowDir, '[middle]', never, never, never, never>;
}
export declare class ShallowModule {
static ngModuleDef: ModuleMeta<ShallowModule, [typeof ShallowDir], never, [typeof ShallowDir, typeof middle.MiddleModule]>;
}
`,
},
new AliasGenerator(testHost));
const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
expect(getAlias(DeepDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$deep$$DeepDir',
});
expect(getAlias(MiddleDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$middle$$MiddleDir',
});
expect(getAlias(ShallowDir)).toBeNull();
});
it('should not use an alias if a directive is declared in the same file as the re-exporting module',
() => {
const {resolver, refs} = makeTestEnv(
{
'module': `
export declare class DeepDir {
static ngDirectiveDef: DirectiveMeta<DeepDir, '[deep]', never, never, never, never>;
}
export declare class DeepModule {
static ngModuleDef: ModuleMeta<DeepModule, [typeof DeepDir], never, [typeof DeepDir]>;
}
export declare class DeepExportModule {
static ngModuleDef: ModuleMeta<DeepExportModule, never, never, [typeof DeepModule]>;
}
`,
},
new AliasGenerator(testHost));
const {DeepExportModule} = refs;
const scope = resolver.resolve(DeepExportModule) !;
const [DeepDir] = scopeToRefs(scope);
expect(getAlias(DeepDir)).toBeNull();
});
});
function scopeToRefs(scope: ExportScope): Reference<ts.Declaration>[] {
function scopeToRefs(scope: ExportScope): Reference<ts.ClassDeclaration>[] {
const directives = scope.exported.directives.map(dir => dir.ref);
const pipes = scope.exported.pipes.map(pipe => pipe.ref);
const pipes = scope.exported.pipes.map(pipe => pipe.ref as Reference<ts.ClassDeclaration>);
return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !));
}
function getAlias(ref: Reference<ts.ClassDeclaration>): ExternalReference|null {
if (ref.alias === null) {
return null;
} else {
return (ref.alias as ExternalExpr).value;
}
}

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {Reference} from '../../imports';
import {Reference, ReferenceEmitter} from '../../imports';
import {ScopeData, ScopeDirective, ScopePipe} from '../src/api';
import {DtsModuleScopeResolver} from '../src/dependency';
import {LocalModuleScopeRegistry} from '../src/local';
@ -31,9 +31,12 @@ function registerFakeRefs(registry: LocalModuleScopeRegistry):
}
describe('LocalModuleScopeRegistry', () => {
const refEmitter = new ReferenceEmitter([]);
let registry !: LocalModuleScopeRegistry;
beforeEach(() => { registry = new LocalModuleScopeRegistry(new MockDtsModuleScopeResolver()); });
beforeEach(() => {
registry = new LocalModuleScopeRegistry(new MockDtsModuleScopeResolver(), refEmitter, null);
});
it('should produce an accurate LocalModuleScope for a basic NgModule', () => {
const {Dir1, Dir2, Pipe1, Module} = registerFakeRefs(registry);

View File

@ -0,0 +1,36 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 * as ts from 'typescript';
export function aliasTransformFactory(exportStatements: Map<string, Map<string, [string, string]>>):
ts.TransformerFactory<ts.Bundle|ts.SourceFile> {
return (context: ts.TransformationContext) => {
return (file: ts.SourceFile | ts.Bundle) => {
if (ts.isBundle(file) || !exportStatements.has(file.fileName)) {
return file;
}
const statements = [...file.statements];
exportStatements.get(file.fileName) !.forEach(([moduleName, symbolName], aliasName) => {
const stmt = ts.createExportDeclaration(
/* decorators */ undefined,
/* modifiers */ undefined,
/* exportClause */ ts.createNamedExports([ts.createExportSpecifier(
/* propertyName */ symbolName,
/* name */ aliasName)]),
/* moduleSpecifier */ ts.createStringLiteral(moduleName));
statements.push(stmt);
});
file = ts.getMutableClone(file);
file.statements = ts.createNodeArray(statements);
return file;
};
};
}

View File

@ -9,6 +9,7 @@
import {ConstantPool, Expression, Statement, Type} from '@angular/compiler';
import * as ts from 'typescript';
import {Reexport} from '../../imports';
import {Decorator} from '../../reflection';
import {TypeCheckContext} from '../../typecheck';
@ -82,7 +83,7 @@ export interface DecoratorHandler<A, M> {
* `DecoratorHandler` a chance to leverage information from the whole compilation unit to enhance
* the `analysis` before the emit phase.
*/
resolve?(node: ts.Declaration, analysis: A): void;
resolve?(node: ts.Declaration, analysis: A): ResolveResult;
typeCheck?(ctx: TypeCheckContext, node: ts.Declaration, metadata: A): void;
@ -121,3 +122,5 @@ export interface CompileResult {
statements: Statement[];
type: Type;
}
export interface ResolveResult { reexports?: Reexport[]; }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool} from '@angular/compiler';
import {ConstantPool, ExternalExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@ -58,6 +58,8 @@ export class IvyCompilation {
* Tracks the `DtsFileTransformer`s for each TS file that needs .d.ts transformations.
*/
private dtsMap = new Map<string, DtsFileTransformer>();
private reexportMap = new Map<string, Map<string, [string, string]>>();
private _diagnostics: ts.Diagnostic[] = [];
@ -76,6 +78,8 @@ export class IvyCompilation {
private sourceToFactorySymbols: Map<string, Set<string>>|null) {}
get exportStatements(): Map<string, Map<string, [string, string]>> { return this.reexportMap; }
analyzeSync(sf: ts.SourceFile): void { return this.analyze(sf, false); }
analyzeAsync(sf: ts.SourceFile): Promise<void>|undefined { return this.analyze(sf, true); }
@ -243,7 +247,17 @@ export class IvyCompilation {
for (const match of ivyClass.matchedHandlers) {
if (match.handler.resolve !== undefined && match.analyzed !== null &&
match.analyzed.analysis !== undefined) {
match.handler.resolve(node, match.analyzed.analysis);
const res = match.handler.resolve(node, match.analyzed.analysis);
if (res.reexports !== undefined) {
const fileName = node.getSourceFile().fileName;
if (!this.reexportMap.has(fileName)) {
this.reexportMap.set(fileName, new Map<string, [string, string]>());
}
const fileReexports = this.reexportMap.get(fileName) !;
for (const reexport of res.reexports) {
fileReexports.set(reexport.asAlias, [reexport.fromModule, reexport.symbolName]);
}
}
}
}
});

View File

@ -21,9 +21,20 @@ const NODE_MODULES_PACKAGE_NAME = /node_modules\/((\w|-|\.)+|(@(\w|-|\.)+\/(\w|-
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const CSS_PREPROCESSOR_EXT = /(\.scss|\.less|\.styl)$/;
let augmentHostForTest: {[name: string]: Function}|null = null;
export function setAugmentHostForTest(augmentation: {[name: string]: Function} | null): void {
augmentHostForTest = augmentation;
}
export function createCompilerHost(
{options, tsHost = ts.createCompilerHost(options, true)}:
{options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost {
if (augmentHostForTest !== null) {
for (const name of Object.keys(augmentHostForTest)) {
(tsHost as any)[name] = augmentHostForTest[name];
}
}
return tsHost;
}

View File

@ -7,6 +7,7 @@
*/
import {CustomTransformers} from '@angular/compiler-cli';
import {setAugmentHostForTest} from '@angular/compiler-cli/src/transformers/compiler_host';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
@ -49,6 +50,7 @@ export class NgtscTestEnvironment {
process.chdir(support.basePath);
setupFakeCore(support);
setAugmentHostForTest(null);
const env = new NgtscTestEnvironment(support, outDir);
@ -108,6 +110,15 @@ export class NgtscTestEnvironment {
};
}
this.write('tsconfig.json', JSON.stringify(tsconfig, null, 2));
if (extraOpts['_useHostForImportGeneration'] === true) {
const cwd = process.cwd();
setAugmentHostForTest({
fileNameToModuleName: (importedFilePath: string) => {
return 'root' + importedFilePath.substr(cwd.length).replace(/(\.d)?.ts$/, '');
}
});
}
}
/**

View File

@ -532,10 +532,14 @@ describe('ngtsc behavioral tests', () => {
})
export class FooModule {}
`);
env.write('node_modules/foo/index.d.ts', `
import * as i0 from '@angular/core';
env.write('node_modules/foo/index.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'foo',
template: '',
})
export class Foo {
static ngComponentDef: i0.ɵComponentDef<Foo, 'foo'>;
}
`);
@ -950,10 +954,11 @@ describe('ngtsc behavioral tests', () => {
`);
env.write('node_modules/router/index.d.ts', `
import {ModuleWithProviders} from '@angular/core';
import {ModuleWithProviders, ɵNgModuleDefWithMeta} from '@angular/core';
declare class RouterModule {
static forRoot(): ModuleWithProviders<RouterModule>;
static ngModuleDef: ɵNgModuleDefWithMeta<RouterModule, never, never, never>;
}
`);
@ -990,7 +995,10 @@ describe('ngtsc behavioral tests', () => {
`);
env.write('node_modules/router/internal.d.ts', `
export declare class InternalRouterModule {}
import {ɵNgModuleDefWithMeta} from '@angular/core';
export declare class InternalRouterModule {
static ngModuleDef: ɵNgModuleDefWithMeta<InternalRouterModule, never, never, never>;
}
`);
env.driveMain();
@ -1018,12 +1026,13 @@ describe('ngtsc behavioral tests', () => {
`);
env.write('node_modules/router/index.d.ts', `
import {ModuleWithProviders} from '@angular/core';
import {ModuleWithProviders, ɵNgModuleDefWithMeta} from '@angular/core';
export interface MyType extends ModuleWithProviders {}
declare class RouterModule {
static forRoot(): (MyType)&{ngModule:RouterModule};
static ngModuleDef: ɵNgModuleDefWithMeta<RouterModule, never, never, never>;
}
`);
@ -2571,7 +2580,7 @@ describe('ngtsc behavioral tests', () => {
export declare class RouterModule {
static forRoot(arg1: any, arg2: any): ModuleWithProviders<RouterModule>;
static forChild(arg1: any): ModuleWithProviders<RouterModule>;
static ngModuleDef: NgModuleDefWithMeta<RouterModule, never, never, never>
static ngModuleDef: NgModuleDefWithMeta<RouterModule, never, never, never>;
}
`);
});
@ -2853,6 +2862,7 @@ describe('ngtsc behavioral tests', () => {
Test2Module,
],
imports: [
Test2Module,
RouterModule.forRoot([
{path: '', loadChildren: './lazy-1/lazy-1#Lazy1Module'},
]),
@ -3193,6 +3203,118 @@ export const Foo = Foo__PRE_R3__;
expect(sourceTestInsideAngularCore).toContain(sourceTestOutsideAngularCore);
});
});
describe('NgModule export aliasing', () => {
it('should use an alias to import a directive from a deep dependency', () => {
env.tsconfig({'_useHostForImportGeneration': true});
// 'alpha' declares the directive which will ultimately be imported.
env.write('alpha.d.ts', `
import {ɵDirectiveDefWithMeta, ɵNgModuleDefWithMeta} from '@angular/core';
export declare class ExternalDir {
static ngDirectiveDef: ɵDirectiveDefWithMeta<ExternalDir, '[test]', never, never, never, never>;
}
export declare class AlphaModule {
static ngModuleDef: ɵNgModuleDefWithMeta<AlphaModule, [typeof ExternalDir], never, [typeof ExternalDir]>;
}
`);
// 'beta' re-exports AlphaModule from alpha.
env.write('beta.d.ts', `
import {ɵNgModuleDefWithMeta} from '@angular/core';
import {AlphaModule} from './alpha';
export declare class BetaModule {
static ngModuleDef: ɵNgModuleDefWithMeta<AlphaModule, never, never, [typeof AlphaModule]>;
}
`);
// The application imports BetaModule from beta, gaining visibility of ExternalDir from alpha.
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
import {BetaModule} from './beta';
@Component({
selector: 'cmp',
template: '<div test></div>',
})
export class Cmp {}
@NgModule({
declarations: [Cmp],
imports: [BetaModule],
})
export class Module {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
// Expect that ExternalDir from alpha is imported via the re-export from beta.
expect(jsContents).toContain('import * as i1 from "root/beta";');
expect(jsContents).toContain('directives: [i1.ɵng$root$alpha$$ExternalDir]');
});
it('should write alias ES2015 exports for NgModule exported directives', () => {
env.tsconfig({'_useHostForImportGeneration': true});
env.write('external.d.ts', `
import {ɵDirectiveDefWithMeta, ɵNgModuleDefWithMeta} from '@angular/core';
import {LibModule} from './lib';
export declare class ExternalDir {
static ngDirectiveDef: ɵDirectiveDefWithMeta<ExternalDir, '[test]', never, never, never, never>;
}
export declare class ExternalModule {
static ngModuleDef: ɵNgModuleDefWithMeta<ExternalModule, [typeof ExternalDir], never, [typeof ExternalDir, typeof LibModule]>;
}
`);
env.write('lib.d.ts', `
import {ɵDirectiveDefWithMeta, ɵNgModuleDefWithMeta} from '@angular/core';
export declare class LibDir {
static ngDirectiveDef: ɵDirectiveDefWithMeta<LibDir, '[lib]', never, never, never, never>;
}
export declare class LibModule {
static ngModuleDef: ɵNgModuleDefWithMeta<LibModule, [typeof LibDir], never, [typeof LibDir]>;
}
`);
env.write('foo.ts', `
import {Directive, NgModule} from '@angular/core';
import {ExternalModule} from './external';
@Directive({selector: '[foo]'})
export class FooDir {}
@NgModule({
declarations: [FooDir],
exports: [FooDir, ExternalModule]
})
export class FooModule {}
`);
env.write('index.ts', `
import {Component, NgModule} from '@angular/core';
import {FooModule} from './foo';
@Component({
selector: 'index',
template: '<div foo test lib></div>',
})
export class IndexCmp {}
@NgModule({
declarations: [IndexCmp],
exports: [FooModule],
})
export class IndexModule {}
`);
env.driveMain();
const jsContents = env.getContents('index.js');
expect(jsContents).toContain('export { FooDir as ɵng$root$foo$$FooDir } from "root/foo";');
});
});
});
function expectTokenAtPosition<T extends ts.Node>(