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

@ -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>(