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:

committed by
Matias Niemelä

parent
a86a179f45
commit
c4733c15c0
@ -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;
|
||||
|
Reference in New Issue
Block a user