feat(ivy): enable re-export of the compilation scope of NgModules privately (#33177)

This commit refactors the aliasing system to support multiple different
AliasingHost implementations, which control specific aliasing behavior
in ngtsc (see the README.md).

A new host is introduced, the `PrivateExportAliasingHost`. This solves a
longstanding problem in ngtsc regarding support for "monorepo" style private
libraries. These are libraries which are compiled separately from the main
application, and depended upon through TypeScript path mappings. Such
libraries are frequently not in the Angular Package Format and do not have
entrypoints, but rather make use of deep import style module specifiers.
This can cause issues with ngtsc's ability to import a directive given the
module specifier of its NgModule.

For example, if the application uses a directive `Foo` from such a library
`foo`, the user might write:

```typescript
import {FooModule} from 'foo/module';
```

In this case, foo/module.d.ts is path-mapped into the program. Ordinarily
the compiler would see this as an absolute module specifier, and assume that
the `Foo` directive can be imported from the same specifier. For such non-
APF libraries, this assumption fails. Really `Foo` should be imported from
the file which declares it, but there are two problems with this:

1. The compiler would have to reverse the path mapping in order to determine
   a path-mapped path to the file (maybe foo/dir.d.ts).
2. There is no guarantee that the file containing the directive is path-
   mapped in the program at all.

The compiler would effectively have to "guess" 'foo/dir' as a module
specifier, which may or may not be accurate depending on how the library and
path mapping are set up.

It's strongly desirable that the compiler not break its current invariant
that the module specifier given by the user for the NgModule is always the
module specifier from which directives/pipes are imported. Thus, for any
given NgModule from a particular module specifier, it must always be
possible to import any directives/pipes from the same specifier, no matter
how it's packaged.

To make this possible, when compiling a file containing an NgModule, ngtsc
will automatically add re-exports for any directives/pipes not yet exported
by the user, with a name of the form: ɵngExportɵModuleNameɵDirectiveName

This has several effects:

1. It guarantees anyone depending on the NgModule will be able to import its
   directives/pipes from the same specifier.
2. It maintains a stable name for the exported symbol that is safe to depend
   on from code on NPM. Effectively, this private exported name will be a
   part of the package's .d.ts API, and cannot be changed in a non-breaking
   fashion.

Fixes #29361
FW-1610 #resolve

PR Close #33177
This commit is contained in:
Alex Rickabaugh 2019-10-14 12:03:29 -07:00 committed by Matias Niemelä
parent a86a179f45
commit c4733c15c0
13 changed files with 894 additions and 96 deletions

View File

@ -64,6 +64,12 @@ export enum ErrorCode {
*/
NGMODULE_MODULE_WITH_PROVIDERS_MISSING_GENERIC = 6005,
/**
* Raised when an NgModule exports multiple directives/pipes of the same name and the compiler
* attempts to generate private re-exports within the NgModule file.
*/
NGMODULE_REEXPORT_NAME_COLLISION = 6006,
/**
* Raised when ngcc tries to inject a synthetic decorator over one that already exists.
*/

View File

@ -23,16 +23,32 @@ export class FatalDiagnosticError {
}
}
export function makeDiagnostic(
code: ErrorCode, node: ts.Node, messageText: string): ts.DiagnosticWithLocation {
export function makeDiagnostic(code: ErrorCode, node: ts.Node, messageText: string, relatedInfo?: {
node: ts.Node,
messageText: string,
}[]): ts.DiagnosticWithLocation {
node = ts.getOriginalNode(node);
return {
const diag: ts.DiagnosticWithLocation = {
category: ts.DiagnosticCategory.Error,
code: Number('-99' + code.valueOf()),
file: ts.getOriginalNode(node).getSourceFile(),
start: node.getStart(undefined, false),
length: node.getWidth(), messageText,
};
if (relatedInfo !== undefined) {
diag.relatedInformation = relatedInfo.map(info => {
const infoNode = ts.getOriginalNode(info.node);
return {
category: ts.DiagnosticCategory.Message,
code: 0,
file: infoNode.getSourceFile(),
start: infoNode.getStart(),
length: infoNode.getWidth(),
messageText: info.messageText,
};
});
}
return diag;
}
export function isFatalDiagnosticError(err: any): err is FatalDiagnosticError {

View File

@ -0,0 +1,209 @@
# `imports`
The `imports` module attempts to unify all import handling in ngtsc. It powers the compiler's reference system - how the compiler tracks classes like components, directives, NgModules, etc, and how it generates imports to them in user code. At its heart is the `Reference` abstraction, which combines a class `ts.Declaration` with any additional context needed to generate an import of that class in different situations.
In Angular, users do not import the directives and pipes they use in templates directly. Instead, they import an NgModule, and the NgModule exports a set of directives/pipes which will be available in the template of any consumer. When generating code for the template, though, the directives/pipes used there need to be imported directly. This creates a challenge for the compiler: it must choose an ES module specifier from which they can be imported, since the user never provided it.
Much of the logic around imports and references in the compiler is dedicated to answering this question. The compiler has two major modes of operation here:
1. Module specifier (import path) tracking
If a directive/pipe is within the user's program, then it can be imported directly. If not (e.g. the directive came from a library in `node_modules`), the compiler will look at the NgModule that caused the directive to be available in the template, look at its import, and attempt to use the same module specifier.
This logic is based on the Angular Package Format, which dictates that libraries are organized into entrypoints, and both an NgModule and its directives/pipes must be exported from the same entrypoint (usually an `index.ts` file).
Thus, if `CommonModule` is imported from the specifier '@angular/common', and its `NgIf` directive is used in a template, the compiler will always import `NgIf` from '@angular/common' as well.
It's important to note that this logic is transitive. If the user instead imported `BrowserModule` from '@angular/platform-browser' (which re-exports `CommonModule` and thus `NgIf`), the compiler will note that `BrowserModule` itself imported `CommonModule` from '@angular/common', and so `NgIf` will be imported from '@angular/common' still.
This logic of course breaks down for non-Angular Package Format libraries, such as "internal" libraries within a monorepo, which frequently don't use `index.ts` files or entrypoints. In this case, the user will likely import NgModules directly from their declaration (e.g. via a 'lib/module' specifier), and the compiler cannot simply assume that the user has exported all of the directives/pipes from the NgModule via this same specifier. In this case a compiler feature called "aliasing" kicks in (see below) and generates private exports from the NgModule file.
2. Using a `FileToModuleHost`
The `ts.CompilerHost` given to the compiler may optionally implement an interface called `FileToModuleHost`, which allows an absolute module specifier to be generated for any file. If a `FileToModuleHost` is present, the compiler will attempt to directly import all directives and pipes from the file which declares them, instead of going via the specifier of the NgModule as in the first mode described above. This logic is used internally in the Google monorepo.
This approach comes with a significant caveat: the build system may prevent importing from files which are not directly declared dependencies of the current compilation (this is known as "strict dependency checking"). This is a problem when attempting to consume a re-exported directive. For example, if the user depends only on '@angular/platform-browser', imports `BrowserModule` from '@angular/platform-browser' and attempts to use the re-exported `NgIf`, the compiler cannot import `NgIf` directly from its declaration within '@angular/common', which is a transitive (but not direct) dependency.
To support these re-exports, a compiler feature called "aliasing" will create a re-export of `NgIf` from within @angular/platform-browser when compiling that package. Then, the downstream application compiler can import `NgIf` via this "alias" re-export from a direct dependency, instead of needing to import it from a transitive dependency.
## References
At its heart, the compiler keeps track of the types (classes) it's operating on using the `ts.Declaration` of that class. This suffices to _identify_ a class; however, the compiler frequently needs to track not only the class itself, but how that class came to be known in a particular context. For example, the compiler might see that `CommonModule` is included in `AppModule`'s imports, but it also needs to keep track of from where `CommonModule` was imported to apply the logic of "module specifier tracking" described above.
To do this, the compiler will wrap the `ts.Declaration` of `CommonModule` into a `Reference`. A `Reference` is a pointer to a `ts.Declaration` plus any additional information and context about _how_ that reference came to be.
### Identifier tracking
Where possible, the compiler tries to use existing user-provided imports to refer to classes, instead of generating new imports. This is possible because `Reference`s keep track of any `ts.Identifier`s encountered which refer to the referenced class. If Angular, in the course of processing a `ts.Expression` (such as the `declarations` array of an NgModule), determines that the `ts.Identifier` points to a `Reference`, it adds the `ts.Identifier` to that `Reference` for future use.
The `Reference.getIdentityIn` method queries the `Reference` for a `ts.Identifier` that's valid in a given `ts.SourceFile`. This is used by the `LocalIdentifierStrategy` when emitting an `Expression` for the `Reference` (see the description of `ReferenceEmitter` below).
#### Synthetic references
In some cases, identifier tracking needs to be disabled for a `Reference`. For example, when the compiler synthesizes a `Reference` as part of "foreign function evaluation", the evaluated `ts.Identifier` may not be a direct reference to the `Reference`'s class at runtime, even if logically that interpretation makes sense in the context of the current expression.
In these cases, the `Reference`s are marked as `synthetic`, which disables all `ts.Identifier` tracking.
### Owning modules
As described above, one piece of information the compiler tracks about a `Reference` is the module specifier from which it came. This is known as its "owning module".
For a `Reference`, the compiler tracks both the module specifier itself as well as the context file which contained this module specifier (which is important for TypeScript module resolution operations).
This information is tracked in `Reference.bestGuessOwningModule`. This field carries the "best guess" prefix because the compiler cannot verify that each `Reference` which was extracted from a given ES module is actually exported via that module specifier. This depends on the packaging convention the user chose to use. Since a `Reference` may not belong to any external module, `bestGuessOwningModule` may be `null`.
For convenience, the module specifier as a string is also made available as `Reference.ownedByModuleGuess`.
## ReferenceEmitter
During evaluation of `ts.Expression`s, `Reference`s to specific directives/pipes/etc are created. During code generation, imports need to be generated for a particular component's template function, based on these `Reference`s. This job falls to the `ReferenceEmitter`.
A `ReferenceEmitter` takes a `Reference` as well as a `ts.SourceFile` which will contain the import, and generates an `Expression` which can be used to refer to the referenced class within that file. This may or may not be an `ExternalExpression` (which would generate an import statement), depending on whether it's possible to rely on an existing import of the class within that file.
`ReferenceEmitter` is a wrapper around one or more `ReferenceEmitStrategy` instances. Each strategy is tried in succession until an `Expression` can be determined. An error is produced if no valid mechanism of referring to the referenced class can be found.
### `LocalIdentifierStrategy`
This `ReferenceEmitStrategy` queries the `Reference` for a `ts.Identifier` that's valid in the requested file (see "identifier tracking" for `Reference`s above).
### `LogicalProjectStrategy`
This `ReferenceEmitStrategy` is used to import referenced classes that are declared in the current project, and not in any third-party or external libraries. It constructs an import path that's valid within the logical filesystem of the project, even if the project has multiple `rootDirs`.
### `AbsoluteModuleStrategy`
This `ReferenceEmitStrategy` uses the `bestGuessOwningModule` of a `Reference` to generate an import of the referenced class.
Note that the `bestGuessOwningModule` only gives the module specifier for the import, not the symbol name. The user may have renamed the class as part of re-exporting it from an entrypoint, so the `AbsoluteModuleStrategy` searches the exports of the target module and finds the symbol name by which the class is re-exported, if it exists.
### `FileToModuleStrategy`
This `ReferenceEmitStrategy` uses a `FileToModuleHost` to implement the major import mode #2 described at the beginning of this document.
Under this strategy, direct imports to referenced classes are constructed using globally valid absolute module specifiers determined by the `FileToModuleHost`.
Like with `AbsoluteModuleStrategy`, the `FileToModuleHost` only gives the module specifier and not the symbol name, so an appropriate symbol name must be determined by searching the exports of the module.
### `AliasStrategy`
The `AliasStrategy` will choose the alias `Expression` of a `Reference`. This strategy is used before the `FileToModuleStrategy` to guarantee aliases are preferred to direct imports when available.
See the description of aliasing in the case of `FileToModuleAliasingHost` below.
## Aliasing and re-exports
In certain cases, the exports written by the user are not sufficient to guarantee that a downstream compiler will be able to depend on directives/pipes correctly. In these circumstances the compiler's "aliasing" system creates new exports to bridge the gaps.
An `AliasingHost` interface allows different aliasing strategies to be chosen based on the needs of the current compilation. It supports two operations:
1. Determination of a re-export name, if needed, for a given directive/pipe.
When compiling an NgModule, the compiler will consult the `AliasingHost` via its `maybeAliasSymbolAs` method to determine whether to add re-exports of any directives/pipes exported (directly or indirectly) by the NgModule.
2. Determination of an alias `Expression` for a directive/pipe, based on a re-export that was expected to have been generated previously.
When the user imports an NgModule from an external library (via a `.d.ts` file), the compiler will construct a "scope" of exported directives/pipes that this NgModule makes available to any templates. In the process of constructing this scope, the compiler creates `Reference`s for each directive/pipe.
As part of this operation, the compiler will consult the `AliasingHost` via its `getAliasIn` method to determine whether an alias `Expression` should be used to refer to each class instead of going through other import generation logic. This alias is saved on the `Reference`.
Because the first import of an NgModule from a user library to a `.d.ts` is always from a direct dependency, the result is that all `Reference`s to directives/pipes which can be used from this module will have an associated alias `Expression` specifying how to import them from that direct dependency, instead of from a transitive dependency.
Aliasing is currently used in two cases:
1. To address strict dependency checking issues when using a `FileToModuleHost`.
2. To support dependening on non-Angular Package Format packages (e.g. private libraries in monorepos) which do not have an entrypoint file through which all directives/pipes/modules are exported.
In environments with "strict dependency checking" as described above, an NgModule which exports another NgModule from one of its dependencies needs to export its directives/pipes as well, in order to make them available to the downstream compiler.
### Aliasing under `FileToModuleHost`
A `FileToModuleAliasingHost` implements `AliasingHost` and makes full use of the aliasing system in the case of a `FileToModuleHost`.
When compiling an NgModule, re-exports are added under a stable name for each directive/pipe that's re-exported by the NgModule.
When importing that NgModule, alias `Expression`s are added to all the `Reference`s for those directives/pipes that are guaranteed to be from a direct dependency.
### Private re-exports for non-APF packages
A `PrivateExportAliasingHost` is used to add re-exports of directives/pipes in the case where the compiler cannot determine that all directives/pipes are re-exported from a common entrypoint (like in the case of an Angular Package Format compilation).
In this case, aliasing is used to proactively add re-exports of directives/pipes to the file of any NgModule which exports them, ensuring they can be imported from the same module specifier as the NgModule itself. This is only done if the user has not already added such exports directly.
This `AliasingHost` does not tag any `Reference`s with aliases, and relies on the action of the `AbsoluteModuleStrategy` described above to find and select the alias re-export when attempting to write an import for a given `Reference`.
## Default imports
This aspect of the `imports` package is a little different than the rest of the code as it's not concerned with directive/pipe imports. Instead, it's concerned with a different problem: preventing the removal of default import statements which were converted from type-only to value imports through compilation.
### Type-to-value compilation
This occurs when a default import is used as a type parameter in a service constructor:
```typescript
import Foo from 'foo';
@Injectable()
export class Svc {
constructor(private foo: Foo) {}
}
```
Here, `Foo` is used in the type position only, but the compiler will eventually generate an `inject(Foo)` call in the factory function for this service. The use of `Foo` like this in the output depends on the import statement surviving compilation.
Due to quirks in TypeScript transformers (see below), TypeScript considers the import to be type-only and does not notice the additional usage as a value added during transformation, and so will attempt to remove the import. The default import managing system exists to prevent this.
It consists of two mechanisms:
1. A `DefaultImportTracker`, which records information about both default imports encountered in the program as well as usages of those imports added during compilation.
A `DefaultImportRecorder` interface is used to allow for a noop implementation in cases (like ngcc) where this tracking isn't necessary.
2. A TypeScript transformer which processes default import statements and can preserve those which are actually used.
This is accessed via `DefaultImportTracker.importPreservingTransformer`.
### Why default imports are problematic
This section is the result of speculation, as we have not traced the TypeScript compiler thoroughly.
Consider the class:
```typescript
import {Foo} from './foo';
class X {
constructor(foo: Foo) {}
}
```
Angular wants to generate a value expression (`inject(Foo)`), using the value side of the `Foo` type from the constructor.
After transforms, this roughly looks like:
```javascript
let foo_1 = require('./foo');
inject(foo_1.Foo);
```
The Angular compiler takes the `Foo` `ts.Identifier` from the import statement `import {Foo} from './foo'`, which has a "provenance" in TypeScript that indicates it's associated with the import statement. After transforms, TypeScript will scan the output code and notice this `ts.Identifier` is still present, and so it will choose to preserve the import statement.
If, however, `Foo` was a default import:
```typescript
import Foo from './foo';
```
Then the generated code depends on a few factors (target/module/esModuleInterop settings), but roughly looks like:
```javascript
let foo_1 = require('./foo');
inject(foo_1.default);
```
Note in this output, the `Foo` identifier from before has disappeared. TypeScript then does not find any `ts.Identifier`s which point back to the original import statement, and thus it concludes that the import is unused.
It's likely that this case was overlooked in the design of the transformers API.

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
export {AliasGenerator, AliasStrategy} from './src/alias';
export {AliasStrategy, AliasingHost, FileToModuleAliasingHost, PrivateExportAliasingHost} from './src/alias';
export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core';
export {DefaultImportRecorder, DefaultImportTracker, NOOP_DEFAULT_IMPORT_RECORDER} from './src/default';
export {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter} from './src/emitter';

View File

@ -9,17 +9,126 @@
import {Expression, ExternalExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {ClassDeclaration} from '../../reflection';
import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration} from '../../reflection';
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 {
/**
* A host for the aliasing system, which allows for alternative exports/imports of directives/pipes.
*
* Given an import of an NgModule (e.g. `CommonModule`), the compiler must generate imports to the
* directives and pipes exported by this module (e.g. `NgIf`) when they're used in a particular
* template. In its default configuration, if the compiler is not directly able to import the
* component from another file within the same project, it will attempt to import the component
* from the same (absolute) path by which the module was imported. So in the above example if
* `CommonModule` was imported from '@angular/common', the compiler will attempt to import `NgIf`
* from '@angular/common' as well.
*
* The aliasing system interacts with the above logic in two distinct ways.
*
* 1) It can be used to create "alias" re-exports from different files, which can be used when the
* user hasn't exported the directive(s) from the ES module containing the NgModule. These re-
* exports can also be helpful when using a `FileToModuleHost`, which overrides the import logic
* described above.
*
* 2) It can be used to get an alternative import expression for a directive or pipe, instead of
* the import that the normal logic would apply. The alias used depends on the provenance of the
* `Reference` which was obtained for the directive/pipe, which is usually a property of how it
* came to be in a template's scope (e.g. by which NgModule).
*
* See the README.md for more information on how aliasing works within the compiler.
*/
export interface AliasingHost {
/**
* Controls whether any alias re-exports are rendered into .d.ts files.
*
* This is not always necessary for aliasing to function correctly, so this flag allows an
* `AliasingHost` to avoid cluttering the .d.ts files if exports are not strictly needed.
*/
readonly aliasExportsInDts: boolean;
/**
* Determine a name by which `decl` should be re-exported from `context`, depending on the
* particular set of aliasing rules in place.
*
* `maybeAliasSymbolAs` can return `null`, in which case no alias export should be generated.
*
* @param ref a `Reference` to the directive/pipe to consider for aliasing.
* @param context the `ts.SourceFile` in which the alias re-export might need to be generated.
* @param ngModuleName the declared name of the `NgModule` within `context` for which the alias
* would be generated.
* @param isReExport whether the directive/pipe under consideration is re-exported from another
* NgModule (as opposed to being declared by it directly).
*/
maybeAliasSymbolAs(
ref: Reference<ClassDeclaration>, context: ts.SourceFile, ngModuleName: string,
isReExport: boolean): string|null;
/**
* Determine an `Expression` by which `decl` should be imported from `via` using an alias export
* (which should have been previously created when compiling `via`).
*
* `getAliasIn` can return `null`, in which case no alias is needed to import `decl` from `via`
* (and the normal import rules should be used).
*
* @param decl the declaration of the directive/pipe which is being imported, and which might be
* aliased.
* @param via the `ts.SourceFile` which might contain an alias to the
*/
getAliasIn(decl: ClassDeclaration, via: ts.SourceFile, isReExport: boolean): Expression|null;
}
/**
* An `AliasingHost` which generates and consumes alias re-exports when module names for each file
* are determined by a `FileToModuleHost`.
*
* When using a `FileToModuleHost`, aliasing prevents issues with transitive dependencies. See the
* README.md for more details.
*/
export class FileToModuleAliasingHost implements AliasingHost {
constructor(private fileToModuleHost: FileToModuleHost) {}
aliasSymbolName(decl: ClassDeclaration, context: ts.SourceFile): string {
/**
* With a `FileToModuleHost`, aliases are chosen automatically without the need to look through
* the exports present in a .d.ts file, so we can avoid cluttering the .d.ts files.
*/
readonly aliasExportsInDts = false;
maybeAliasSymbolAs(
ref: Reference<ClassDeclaration>, context: ts.SourceFile, ngModuleName: string,
isReExport: boolean): string|null {
if (!isReExport) {
// Aliasing is used with a FileToModuleHost to prevent transitive dependencies. Thus, aliases
// only need to be created for directives/pipes which are not direct declarations of an
// NgModule which exports them.
return null;
}
return this.aliasName(ref.node, context);
}
/**
* Generates an `Expression` to import `decl` from `via`, assuming an export was added when `via`
* was compiled per `maybeAliasSymbolAs` above.
*/
getAliasIn(decl: ClassDeclaration, via: ts.SourceFile, isReExport: boolean): Expression|null {
if (!isReExport) {
// Directly exported directives/pipes don't require an alias, per the logic in
// `maybeAliasSymbolAs`.
return null;
}
// viaModule is the module it'll actually be imported from.
const moduleName = this.fileToModuleHost.fileNameToModuleName(via.fileName, via.fileName);
return new ExternalExpr({moduleName, name: this.aliasName(decl, via)});
}
/**
* Generates an alias name based on the full module name of the file which declares the aliased
* directive/pipe.
*/
private aliasName(decl: ClassDeclaration, context: ts.SourceFile): string {
// The declared module is used to get the name of the alias.
const declModule =
this.fileToModuleHost.fileNameToModuleName(decl.getSourceFile().fileName, context.fileName);
@ -27,15 +136,75 @@ export class AliasGenerator {
const replaced = declModule.replace(CHARS_TO_ESCAPE, '_').replace(/\//g, '$');
return 'ɵng$' + replaced + '$$' + decl.name.text;
}
aliasTo(decl: ClassDeclaration, 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});
}
}
/**
* An `AliasingHost` which exports directives from any file containing an NgModule in which they're
* declared/exported, under a private symbol name.
*
* These exports support cases where an NgModule is imported deeply from an absolute module path
* (that is, it's not part of an Angular Package Format entrypoint), and the compiler needs to
* import any matched directives/pipes from the same path (to the NgModule file). See README.md for
* more details.
*/
export class PrivateExportAliasingHost implements AliasingHost {
constructor(private host: ReflectionHost) {}
/**
* Under private export aliasing, the `AbsoluteModuleStrategy` used for emitting references will
* will select aliased exports that it finds in the .d.ts file for an NgModule's file. Thus,
* emitting these exports in .d.ts is a requirement for the `PrivateExportAliasingHost` to
* function correctly.
*/
readonly aliasExportsInDts = true;
maybeAliasSymbolAs(
ref: Reference<ClassDeclaration>, context: ts.SourceFile, ngModuleName: string): string|null {
if (ref.hasOwningModuleGuess) {
// Skip nodes that already have an associated absolute module specifier, since they can be
// safely imported from that specifier.
return null;
}
// Look for a user-provided export of `decl` in `context`. If one exists, then an alias export
// is not needed.
// TODO(alxhub): maybe add a host method to check for the existence of an export without going
// through the entire list of exports.
const exports = this.host.getExportsOfModule(context);
if (exports === null) {
// Something went wrong, and no exports were available at all. Bail rather than risk creating
// re-exports when they're not needed.
throw new Error(`Could not determine the exports of: ${context.fileName}`);
}
let found: boolean = false;
exports.forEach(value => {
if (value.node === ref.node) {
found = true;
}
});
if (found) {
// The module exports the declared class directly, no alias is necessary.
return null;
}
return `ɵngExportɵ${ngModuleName}ɵ${ref.node.name.text}`;
}
/**
* A `PrivateExportAliasingHost` only generates re-exports and does not direct the compiler to
* directly consume the aliases it creates.
*
* Instead, they're consumed indirectly: `AbsoluteModuleStrategy` `ReferenceEmitterStrategy` will
* select these alias exports automatically when looking for an export of the directive/pipe from
* the same path as the NgModule was imported.
*
* Thus, `getAliasIn` always returns `null`.
*/
getAliasIn(): null { return null; }
}
/**
* A `ReferenceEmitStrategy` which will consume the alias attached to a particular `Reference` to a
* directive or pipe, if it exists.
*/
export class AliasStrategy implements ReferenceEmitStrategy {
emit(ref: Reference<ts.Node>, context: ts.SourceFile, importMode: ImportMode): Expression|null {
return ref.alias;

View File

@ -70,7 +70,7 @@ export const NOOP_DEFAULT_IMPORT_RECORDER: DefaultImportRecorder = {
* a dangling reference, as TS will elide the import because it was only used in the type position
* originally.
*
* To avoid this, the compiler must "touch" the imports with `ts.updateImportClause`, and should
* To avoid this, the compiler must "touch" the imports with `ts.getMutableClone`, and should
* only do this for imports which are actually consumed. The `DefaultImportTracker` keeps track of
* these imports as they're encountered and emitted, and implements a transform which can correctly
* flag the imports as required.

View File

@ -18,7 +18,7 @@ import {CycleAnalyzer, ImportGraph} from './cycles';
import {ErrorCode, ngErrorCode} from './diagnostics';
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
import {AbsoluteFsPath, LogicalFileSystem, absoluteFrom} from './file_system';
import {AbsoluteModuleStrategy, AliasGenerator, AliasStrategy, DefaultImportTracker, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
import {AbsoluteModuleStrategy, AliasStrategy, AliasingHost, DefaultImportTracker, FileToModuleAliasingHost, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
import {IncrementalState} from './incremental';
import {IndexedComponent, IndexingContext} from './indexer';
import {generateAnalysis} from './indexer/src/transform';
@ -60,6 +60,7 @@ export class NgtscProgram implements api.Program {
private cycleAnalyzer: CycleAnalyzer;
private metaReader: MetadataReader|null = null;
private aliasingHost: AliasingHost|null = null;
private refEmitter: ReferenceEmitter|null = null;
private fileToModuleHost: FileToModuleHost|null = null;
private defaultImportTracker: DefaultImportTracker;
@ -351,6 +352,10 @@ export class NgtscProgram implements api.Program {
declarationTransformFactory(compilation),
];
// Only add aliasing re-exports to the .d.ts output if the `AliasingHost` requests it.
if (this.aliasingHost !== null && this.aliasingHost.aliasExportsInDts) {
afterDeclarationsTransforms.push(aliasTransformFactory(compilation.exportStatements));
}
if (this.factoryToSourceInfo !== null) {
beforeTransforms.push(
@ -475,7 +480,6 @@ 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
@ -491,6 +495,15 @@ export class NgtscProgram implements api.Program {
// an error.
new LogicalProjectStrategy(this.reflector, new LogicalFileSystem(this.rootDirs)),
]);
// If an entrypoint is present, then all user imports should be directed through the
// entrypoint and private exports are not needed. The compiler will validate that all publicly
// visible directives/pipes are importable via this entrypoint.
if (this.entryPoint === null && this.options.generateDeepReexports === true) {
// No entrypoint is present and deep re-exports were requested, so configure the aliasing
// system to generate them.
this.aliasingHost = new PrivateExportAliasingHost(this.reflector);
}
} else {
// The CompilerHost supports fileNameToModuleName, so use that to emit imports.
this.refEmitter = new ReferenceEmitter([
@ -501,16 +514,16 @@ export class NgtscProgram implements api.Program {
// Then use fileNameToModuleName to emit imports.
new FileToModuleStrategy(this.reflector, this.fileToModuleHost),
]);
aliasGenerator = new AliasGenerator(this.fileToModuleHost);
this.aliasingHost = new FileToModuleAliasingHost(this.fileToModuleHost);
}
const evaluator = new PartialEvaluator(this.reflector, checker, this.incrementalState);
const dtsReader = new DtsMetadataReader(checker, this.reflector);
const localMetaRegistry = new LocalMetadataRegistry();
const localMetaReader = new CompoundMetadataReader([localMetaRegistry, this.incrementalState]);
const depScopeReader = new MetadataDtsModuleScopeResolver(dtsReader, aliasGenerator);
const depScopeReader = new MetadataDtsModuleScopeResolver(dtsReader, this.aliasingHost);
const scopeRegistry = new LocalModuleScopeRegistry(
localMetaReader, depScopeReader, this.refEmitter, aliasGenerator, this.incrementalState);
localMetaReader, depScopeReader, this.refEmitter, this.aliasingHost, this.incrementalState);
const scopeReader = new CompoundComponentScopeReader([scopeRegistry, this.incrementalState]);
const metaRegistry =
new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry, this.incrementalState]);

View File

@ -272,21 +272,24 @@ export class TypeScriptReflectionHost implements ReflectionHost {
private getDeclarationOfSymbol(symbol: ts.Symbol, originalId: ts.Identifier|null): Declaration
|null {
// If the symbol points to a ShorthandPropertyAssignment, resolve it.
if (symbol.valueDeclaration !== undefined &&
ts.isShorthandPropertyAssignment(symbol.valueDeclaration)) {
const shorthandSymbol =
this.checker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration);
let valueDeclaration: ts.Declaration|undefined = undefined;
if (symbol.valueDeclaration !== undefined) {
valueDeclaration = symbol.valueDeclaration;
} else if (symbol.declarations.length > 0) {
valueDeclaration = symbol.declarations[0];
}
if (valueDeclaration !== undefined && ts.isShorthandPropertyAssignment(valueDeclaration)) {
const shorthandSymbol = this.checker.getShorthandAssignmentValueSymbol(valueDeclaration);
if (shorthandSymbol === undefined) {
return null;
}
return this.getDeclarationOfSymbol(shorthandSymbol, originalId);
} else if (
symbol.valueDeclaration !== undefined && ts.isExportSpecifier(symbol.valueDeclaration)) {
const localTarget = this.checker.getExportSpecifierLocalTargetSymbol(symbol.valueDeclaration);
if (localTarget === undefined) {
} else if (valueDeclaration !== undefined && ts.isExportSpecifier(valueDeclaration)) {
const targetSymbol = this.checker.getExportSpecifierLocalTargetSymbol(valueDeclaration);
if (targetSymbol === undefined) {
return null;
}
return this.getDeclarationOfSymbol(localTarget, originalId);
return this.getDeclarationOfSymbol(targetSymbol, originalId);
}
const importInfo = originalId && this.getImportOfIdentifier(originalId);

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {AliasGenerator, Reference} from '../../imports';
import {AliasingHost, Reference} from '../../imports';
import {DirectiveMeta, MetadataReader, PipeMeta} from '../../metadata';
import {ClassDeclaration} from '../../reflection';
@ -34,7 +34,7 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver {
/**
* @param dtsMetaReader a `MetadataReader` which can read metadata from `.d.ts` files.
*/
constructor(private dtsMetaReader: MetadataReader, private aliasGenerator: AliasGenerator|null) {}
constructor(private dtsMetaReader: MetadataReader, private aliasingHost: AliasingHost|null) {}
/**
* Resolve a `Reference`'d NgModule from a .d.ts file and produce a transitive `ExportScope`
@ -76,22 +76,16 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver {
// Attempt to process the export as a directive.
const directive = this.dtsMetaReader.getDirectiveMetadata(exportRef);
if (directive !== null) {
if (!declarations.has(exportRef.node)) {
directives.push(this.maybeAlias(directive, sourceFile));
} else {
directives.push(directive);
}
const isReExport = !declarations.has(exportRef.node);
directives.push(this.maybeAlias(directive, sourceFile, isReExport));
continue;
}
// Attempt to process the export as a pipe.
const pipe = this.dtsMetaReader.getPipeMetadata(exportRef);
if (pipe !== null) {
if (!declarations.has(exportRef.node)) {
pipes.push(this.maybeAlias(pipe, sourceFile));
} else {
pipes.push(pipe);
}
const isReExport = !declarations.has(exportRef.node);
pipes.push(this.maybeAlias(pipe, sourceFile, isReExport));
continue;
}
@ -101,7 +95,7 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver {
// 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) {
if (this.aliasingHost === null) {
// Fast path when aliases aren't required.
directives.push(...exportScope.exported.directives);
pipes.push(...exportScope.exported.pipes);
@ -115,10 +109,10 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver {
// 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));
directives.push(this.maybeAlias(directive, sourceFile, /* isReExport */ true));
}
for (const pipe of exportScope.exported.pipes) {
pipes.push(this.maybeAlias(pipe, sourceFile));
pipes.push(this.maybeAlias(pipe, sourceFile, /* isReExport */ true));
}
}
}
@ -134,19 +128,21 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver {
};
}
private maybeAlias<T extends DirectiveMeta|PipeMeta>(dirOrPipe: T, maybeAliasFrom: ts.SourceFile):
T {
if (this.aliasGenerator === null) {
private maybeAlias<T extends DirectiveMeta|PipeMeta>(
dirOrPipe: T, maybeAliasFrom: ts.SourceFile, isReExport: boolean): T {
const ref = dirOrPipe.ref;
if (this.aliasingHost === null || ref.node.getSourceFile() === maybeAliasFrom) {
return dirOrPipe;
}
const ref = dirOrPipe.ref;
if (ref.node.getSourceFile() !== maybeAliasFrom) {
const alias = this.aliasingHost.getAliasIn(ref.node, maybeAliasFrom, isReExport);
if (alias === null) {
return dirOrPipe;
}
return {
...dirOrPipe,
ref: ref.cloneWithAlias(this.aliasGenerator.aliasTo(ref.node, maybeAliasFrom)),
ref: ref.cloneWithAlias(alias),
};
} else {
return dirOrPipe;
}
}
}

View File

@ -10,7 +10,7 @@ import {ExternalExpr, SchemaMetadata} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, makeDiagnostic} from '../../diagnostics';
import {AliasGenerator, Reexport, Reference, ReferenceEmitter} from '../../imports';
import {AliasingHost, Reexport, Reference, ReferenceEmitter} from '../../imports';
import {DirectiveMeta, MetadataReader, MetadataRegistry, NgModuleMeta, PipeMeta} from '../../metadata';
import {ClassDeclaration} from '../../reflection';
import {identifierOfNode, nodeNameForError} from '../../util/src/typescript';
@ -104,7 +104,7 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
constructor(
private localReader: MetadataReader, private dependencyScopeReader: DtsModuleScopeResolver,
private refEmitter: ReferenceEmitter, private aliasGenerator: AliasGenerator|null,
private refEmitter: ReferenceEmitter, private aliasingHost: AliasingHost|null,
private componentScopeRegistry: ComponentScopeRegistry = new NoopComponentScopeRegistry()) {}
/**
@ -217,7 +217,6 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
const compilationPipes = new Map<ts.Declaration, PipeMeta>();
const declared = new Set<ts.Declaration>();
const sourceFile = ref.node.getSourceFile();
// Directives and pipes exported to any importing NgModules.
const exportDirectives = new Map<ts.Declaration, DirectiveMeta>();
@ -321,39 +320,7 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
pipes: Array.from(exportPipes.values()),
};
let reexports: Reexport[]|null = null;
if (this.aliasGenerator !== null) {
reexports = [];
const addReexport = (ref: Reference<ClassDeclaration>) => {
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);
}
}
const reexports = this.getReexports(ngModule, ref, declared, exported, diagnostics);
// Check if this scope had any errors during production.
if (diagnostics.length > 0) {
@ -429,6 +396,66 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
}
}
private getReexports(
ngModule: NgModuleMeta, ref: Reference<ClassDeclaration>, declared: Set<ts.Declaration>,
exported: {directives: DirectiveMeta[], pipes: PipeMeta[]},
diagnostics: ts.Diagnostic[]): Reexport[]|null {
let reexports: Reexport[]|null = null;
const sourceFile = ref.node.getSourceFile();
if (this.aliasingHost === null) {
return null;
}
reexports = [];
// Track re-exports by symbol name, to produce diagnostics if two alias re-exports would share
// the same name.
const reexportMap = new Map<string, Reference<ClassDeclaration>>();
// Alias ngModuleRef added for readability below.
const ngModuleRef = ref;
const addReexport = (exportRef: Reference<ClassDeclaration>) => {
if (exportRef.node.getSourceFile() === sourceFile) {
return;
}
const isReExport = !declared.has(exportRef.node);
const exportName = this.aliasingHost !.maybeAliasSymbolAs(
exportRef, sourceFile, ngModule.ref.node.name.text, isReExport);
if (exportName === null) {
return;
}
if (!reexportMap.has(exportName)) {
if (exportRef.alias && exportRef.alias instanceof ExternalExpr) {
reexports !.push({
fromModule: exportRef.alias.value.moduleName !,
symbolName: exportRef.alias.value.name !,
asAlias: exportName,
});
} else {
const expr = this.refEmitter.emit(exportRef.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,
});
}
reexportMap.set(exportName, exportRef);
} else {
// Another re-export already used this name. Produce a diagnostic.
const prevRef = reexportMap.get(exportName) !;
diagnostics.push(reexportCollision(ngModuleRef.node, prevRef, exportRef));
}
};
for (const {ref} of exported.directives) {
addReexport(ref);
}
for (const {ref} of exported.pipes) {
addReexport(ref);
}
return reexports;
}
private assertCollecting(): void {
if (this.sealed) {
throw new Error(`Assertion: LocalModuleScopeRegistry is not COLLECTING`);
@ -472,3 +499,25 @@ function invalidReexport(clazz: ts.Declaration, decl: Reference<ts.Declaration>)
ErrorCode.NGMODULE_INVALID_REEXPORT, identifierOfNode(decl.node) || decl.node,
`Present in the NgModule.exports of ${nodeNameForError(clazz)} but neither declared nor imported`);
}
/**
* Produce a `ts.Diagnostic` for a collision in re-export names between two directives/pipes.
*/
function reexportCollision(
module: ClassDeclaration, refA: Reference<ClassDeclaration>,
refB: Reference<ClassDeclaration>): ts.Diagnostic {
const childMessageText =
`This directive/pipe is part of the exports of '${module.name.text}' and shares the same name as another exported directive/pipe.`;
return makeDiagnostic(
ErrorCode.NGMODULE_REEXPORT_NAME_COLLISION, module.name, `
There was a name collision between two classes named '${refA.node.name.text}', which are both part of the exports of '${module.name.text}'.
Angular generates re-exports of an NgModule's exported directives/pipes from the module's source file in certain cases, using the declared name of the class. If two classes of the same name are exported, this automatic naming does not work.
To fix this problem please re-export one or both classes directly from this file.
`.trim(),
[
{node: refA.node.name, messageText: childMessageText},
{node: refB.node.name, messageText: childMessageText},
]);
}

View File

@ -7,9 +7,10 @@
*/
import {ExternalExpr, ExternalReference} from '@angular/compiler';
import * as ts from 'typescript';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {AliasGenerator, FileToModuleHost, Reference} from '../../imports';
import {AliasingHost, FileToModuleAliasingHost, FileToModuleHost, Reference} from '../../imports';
import {DtsMetadataReader} from '../../metadata';
import {ClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {makeProgram} from '../../testing';
@ -42,7 +43,7 @@ export declare type PipeMeta<A, B> = never;
* destructured to retrieve references to specific declared classes.
*/
function makeTestEnv(
modules: {[module: string]: string}, aliasGenerator: AliasGenerator | null = null): {
modules: {[module: string]: string}, aliasGenerator: AliasingHost | null = null): {
refs: {[name: string]: Reference<ClassDeclaration>},
resolver: MetadataDtsModuleScopeResolver,
} {
@ -182,7 +183,7 @@ runInEachFileSystem(() => {
}
`,
},
new AliasGenerator(testHost));
new FileToModuleAliasingHost(testHost));
const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
@ -232,7 +233,7 @@ runInEachFileSystem(() => {
}
`,
},
new AliasGenerator(testHost));
new FileToModuleAliasingHost(testHost));
const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
@ -265,7 +266,7 @@ runInEachFileSystem(() => {
}
`,
},
new AliasGenerator(testHost));
new FileToModuleAliasingHost(testHost));
const {DeepExportModule} = refs;
const scope = resolver.resolve(DeepExportModule) !;
const [DeepDir] = scopeToRefs(scope);

View File

@ -260,6 +260,37 @@ export interface CompilerOptions extends ts.CompilerOptions {
* @internal
*/
ivyTemplateTypeCheck?: boolean;
/**
* Enables the generation of alias re-exports of directives/pipes that are visible from an
* NgModule from that NgModule's file.
*
* This option should be disabled for application builds or for Angular Package Format libraries
* (where NgModules along with their directives/pipes are exported via a single entrypoint).
*
* For other library compilations which are intended to be path-mapped into an application build
* (or another library), enabling this option enables the resulting deep imports to work
* correctly.
*
* A consumer of such a path-mapped library will write an import like:
*
* ```typescript
* import {LibModule} from 'lib/deep/path/to/module';
* ```
*
* The compiler will attempt to generate imports of directives/pipes from that same module
* specifier (the compiler does not rewrite the user's given import path, unlike View Engine).
*
* ```typescript
* import {LibDir, LibCmp, LibPipe} from 'lib/deep/path/to/module';
* ```
*
* It would be burdensome for users to have to re-export all directives/pipes alongside each
* NgModule to support this import model. Enabling this option tells the compiler to generate
* private re-exports alongside the NgModule of all the directives/pipes it makes available, to
* support these future imports.
*/
generateDeepReexports?: boolean;
}
export interface CompilerHost extends ts.CompilerHost {

View File

@ -3302,6 +3302,311 @@ runInEachFileSystem(os => {
});
});
describe('aliasing re-exports', () => {
beforeEach(() => {
env.tsconfig({
'generateDeepReexports': true,
});
});
it('should re-export a directive from a different file under a private symbol name', () => {
env.write('dir.ts', `
import {Directive} from '@angular/core';
@Directive({
selector: 'dir',
})
export class Dir {}
`);
env.write('module.ts', `
import {Directive, NgModule} from '@angular/core';
import {Dir} from './dir';
@Directive({selector: '[inline]'})
export class InlineDir {}
@NgModule({
declarations: [Dir, InlineDir],
exports: [Dir, InlineDir],
})
export class Module {}
`);
env.driveMain();
const jsContents = env.getContents('module.js');
const dtsContents = env.getContents('module.d.ts');
expect(jsContents).toContain('export { Dir as ɵngExportɵModuleɵDir } from "./dir";');
expect(jsContents).not.toContain('ɵngExportɵModuleɵInlineDir');
expect(dtsContents).toContain('export { Dir as ɵngExportɵModuleɵDir } from "./dir";');
expect(dtsContents).not.toContain('ɵngExportɵModuleɵInlineDir');
});
it('should re-export a directive from an exported NgModule under a private symbol name',
() => {
env.write('dir.ts', `
import {Directive, NgModule} from '@angular/core';
@Directive({
selector: 'dir',
})
export class Dir {}
@NgModule({
declarations: [Dir],
exports: [Dir],
})
export class DirModule {}
`);
env.write('module.ts', `
import {NgModule} from '@angular/core';
import {DirModule} from './dir';
@NgModule({
exports: [DirModule],
})
export class Module {}
`);
env.driveMain();
const jsContents = env.getContents('module.js');
const dtsContents = env.getContents('module.d.ts');
expect(jsContents).toContain('export { Dir as ɵngExportɵModuleɵDir } from "./dir";');
expect(dtsContents).toContain('export { Dir as ɵngExportɵModuleɵDir } from "./dir";');
});
it('should not re-export a directive that\'s not exported from the NgModule', () => {
env.write('dir.ts', `
import {Directive} from '@angular/core';
@Directive({
selector: 'dir',
})
export class Dir {}
`);
env.write('module.ts', `
import {NgModule} from '@angular/core';
import {Dir} from './dir';
@NgModule({
declarations: [Dir],
exports: [],
})
export class Module {}
`);
env.driveMain();
const jsContents = env.getContents('module.js');
const dtsContents = env.getContents('module.d.ts');
expect(jsContents).not.toContain('ɵngExportɵModuleɵDir');
expect(dtsContents).not.toContain('ɵngExportɵModuleɵDir');
});
it('should not re-export a directive that\'s already exported', () => {
env.write('dir.ts', `
import {Directive} from '@angular/core';
@Directive({
selector: 'dir',
})
export class Dir {}
`);
env.write('module.ts', `
import {NgModule} from '@angular/core';
import {Dir} from './dir';
@NgModule({
declarations: [Dir],
exports: [Dir],
})
export class Module {}
export {Dir};
`);
env.driveMain();
const jsContents = env.getContents('module.js');
const dtsContents = env.getContents('module.d.ts');
expect(jsContents).not.toContain('ɵngExportɵModuleɵDir');
expect(dtsContents).not.toContain('ɵngExportɵModuleɵDir');
});
it('should not re-export a directive from an exported, external NgModule', () => {
env.write(`node_modules/external/index.d.ts`, `
import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core';
export declare class ExternalDir {
static ɵdir: ɵɵDirectiveDefWithMeta<ExternalDir, '[test]', never, never, never, never>;
}
export declare class ExternalModule {
static ɵmod: ɵɵNgModuleDefWithMeta<ExternalModule, [typeof ExternalDir], never, [typeof ExternalDir]>;
}
`);
env.write('module.ts', `
import {NgModule} from '@angular/core';
import {ExternalModule} from 'external';
@NgModule({
exports: [ExternalModule],
})
export class Module {}
`);
env.driveMain();
const jsContents = env.getContents('module.js');
expect(jsContents).not.toContain('ɵngExportɵExternalModuleɵExternalDir');
});
it('should error when two directives with the same declared name are exported from the same NgModule',
() => {
env.write('dir.ts', `
import {Directive} from '@angular/core';
@Directive({
selector: 'dir',
})
export class Dir {}
`);
env.write('dir2.ts', `
import {Directive} from '@angular/core';
@Directive({
selector: 'dir',
})
export class Dir {}
`);
env.write('module.ts', `
import {NgModule} from '@angular/core';
import {Dir} from './dir';
import {Dir as Dir2} from './dir2';
@NgModule({
declarations: [Dir, Dir2],
exports: [Dir, Dir2],
})
export class Module {}
`);
const diag = env.driveDiagnostics();
expect(diag.length).toBe(1);
expect(diag[0] !.code).toEqual(ngErrorCode(ErrorCode.NGMODULE_REEXPORT_NAME_COLLISION));
});
it('should not error when two directives with the same declared name are exported from the same NgModule, but one is exported from the file directly',
() => {
env.write('dir.ts', `
import {Directive} from '@angular/core';
@Directive({
selector: 'dir',
})
export class Dir {}
`);
env.write('dir2.ts', `
import {Directive} from '@angular/core';
@Directive({
selector: 'dir',
})
export class Dir {}
`);
env.write('module.ts', `
import {NgModule} from '@angular/core';
import {Dir} from './dir';
import {Dir as Dir2} from './dir2';
@NgModule({
declarations: [Dir, Dir2],
exports: [Dir, Dir2],
})
export class Module {}
export {Dir} from './dir2';
`);
env.driveMain();
const jsContents = env.getContents('module.js');
expect(jsContents).toContain('export { Dir as ɵngExportɵModuleɵDir } from "./dir";');
});
it('should choose a re-exported symbol if one is present', () => {
env.write(`node_modules/external/dir.d.ts`, `
import {ɵɵDirectiveDefWithMeta} from '@angular/core';
export declare class ExternalDir {
static ɵdir: ɵɵDirectiveDefWithMeta<ExternalDir, '[test]', never, never, never, never>;
}
`);
env.write('node_modules/external/module.d.ts', `
import {ɵɵNgModuleDefWithMeta} from '@angular/core';
import {ExternalDir} from './dir';
export declare class ExternalModule {
static ɵmod: ɵɵNgModuleDefWithMeta<ExternalModule, [typeof ExternalDir], never, [typeof ExternalDir]>;
}
export {ExternalDir as ɵngExportɵExternalModuleɵExternalDir};
`);
env.write('test.ts', `
import {Component, Directive, NgModule} from '@angular/core';
import {ExternalModule} from 'external/module';
@Component({
selector: 'test-cmp',
template: '<div test></div>',
})
class Cmp {}
@NgModule({
declarations: [Cmp],
imports: [ExternalModule],
})
class Module {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('import * as i1 from "external/module";');
expect(jsContents).toContain('directives: [i1.ɵngExportɵExternalModuleɵExternalDir]');
});
it('should not generate re-exports when disabled', () => {
// Return to the default configuration, which has re-exports disabled.
env.tsconfig();
env.write('dir.ts', `
import {Directive} from '@angular/core';
@Directive({
selector: 'dir',
})
export class Dir {}
`);
env.write('module.ts', `
import {NgModule} from '@angular/core';
import {Dir} from './dir';
@NgModule({
declarations: [Dir],
exports: [Dir],
})
export class Module {}
`);
env.driveMain();
const jsContents = env.getContents('module.js');
const dtsContents = env.getContents('module.d.ts');
expect(jsContents).not.toContain('ɵngExportɵModuleɵDir');
expect(dtsContents).not.toContain('ɵngExportɵModuleɵDir');
});
});
it('should execute custom transformers', () => {
let beforeCount = 0;
let afterCount = 0;