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:

committed by
Ben Lesh

parent
15c065f9a0
commit
c1392ce618
@ -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$/, '');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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>(
|
||||
|
Reference in New Issue
Block a user