Skip to content

Commit b4644d7

Browse files
alxhubmatsko
authored andcommitted
fix(ivy): correct timing of NgModuleFactory registration (angular#30706)
Commit 0df719a introduced registration of NgModules with ids when compiled with AOT, and f74373f corrected the timing to avoid issues with tree shaking. Neither of these approaches were correct. This commit fixes the timing to match View Engine and avoid tree shaking issues, as well as fixes a bug with the registration of imported module ids. A new Ivy-only test is added which verifies that modules get registered correctly under real-world conditions. PR Close angular#30706
1 parent 7a0f8ac commit b4644d7

File tree

3 files changed

+70
-12
lines changed

3 files changed

+70
-12
lines changed

packages/core/src/linker/ng_module_factory_registration.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,21 @@ function assertSameOrNotExisting(id: string, type: Type<any>| null, incoming: Ty
3737
}
3838
}
3939

40-
export function registerNgModuleType(id: string, ngModuleType: NgModuleType) {
41-
const existing = modules.get(id) as NgModuleType | null;
42-
assertSameOrNotExisting(id, existing, ngModuleType);
43-
modules.set(id, ngModuleType);
40+
export function registerNgModuleType(ngModuleType: NgModuleType) {
41+
if (ngModuleType.ngModuleDef.id !== null) {
42+
const id = ngModuleType.ngModuleDef.id;
43+
const existing = modules.get(id) as NgModuleType | null;
44+
assertSameOrNotExisting(id, existing, ngModuleType);
45+
modules.set(id, ngModuleType);
46+
}
47+
48+
let imports = ngModuleType.ngModuleDef.imports;
49+
if (imports instanceof Function) {
50+
imports = imports();
51+
}
52+
if (imports) {
53+
imports.forEach((i: NgModuleType<any>) => registerNgModuleType(i));
54+
}
4455
}
4556

4657
export function clearModulesForTest(): void {

packages/core/src/render3/ng_module_ref.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,39 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
9292
}
9393

9494
export class NgModuleFactory<T> extends viewEngine_NgModuleFactory<T> {
95-
constructor(public moduleType: Type<T>) { super(); }
95+
constructor(public moduleType: Type<T>) {
96+
super();
9697

97-
create(parentInjector: Injector|null): viewEngine_NgModuleRef<T> {
98-
const moduleType = this.moduleType;
99-
const moduleRef = new NgModuleRef(moduleType, parentInjector);
10098
const ngModuleDef = getNgModuleDef(moduleType);
101-
ngModuleDef && ngModuleDef.id &&
102-
registerNgModuleType(ngModuleDef.id, moduleType as NgModuleType);
103-
return moduleRef;
99+
if (ngModuleDef !== null) {
100+
// Register the NgModule with Angular's module registry. The location (and hence timing) of
101+
// this call is critical to ensure this works correctly (modules get registered when expected)
102+
// without bloating bundles (modules are registered when otherwise not referenced).
103+
//
104+
// In View Engine, registration occurs in the .ngfactory.js file as a side effect. This has
105+
// several practical consequences:
106+
//
107+
// - If an .ngfactory file is not imported from, the module won't be registered (and can be
108+
// tree shaken).
109+
// - If an .ngfactory file is imported from, the module will be registered even if an instance
110+
// is not actually created (via `create` below).
111+
// - Since an .ngfactory file in View Engine references the .ngfactory files of the NgModule's
112+
// imports,
113+
//
114+
// In Ivy, things are a bit different. .ngfactory files still exist for compatibility, but are
115+
// not a required API to use - there are other ways to obtain an NgModuleFactory for a given
116+
// NgModule. Thus, relying on a side effect in the .ngfactory file is not sufficient. Instead,
117+
// the side effect of registration is added here, in the constructor of NgModuleFactory,
118+
// ensuring no matter how a factory is created, the module is registered correctly.
119+
//
120+
// An alternative would be to include the registration side effect inline following the actual
121+
// NgModule definition. This also has the correct timing, but breaks tree-shaking - modules
122+
// will be registered and retained even if they're otherwise never referenced.
123+
registerNgModuleType(moduleType as NgModuleType);
124+
}
125+
}
126+
127+
create(parentInjector: Injector|null): viewEngine_NgModuleRef<T> {
128+
return new NgModuleRef(this.moduleType, parentInjector);
104129
}
105130
}

packages/core/test/linker/ng_module_integration_spec.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ANALYZE_FOR_ENTRY_COMPONENTS, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef, Compiler, Component, ComponentFactoryResolver, Directive, HostBinding, Inject, Injectable, InjectionToken, Injector, Input, NgModule, NgModuleRef, Optional, Pipe, Provider, Self, Type, forwardRef, getModuleFactory, ɵivyEnabled as ivyEnabled} from '@angular/core';
9+
import {ANALYZE_FOR_ENTRY_COMPONENTS, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef, Compiler, Component, ComponentFactoryResolver, Directive, HostBinding, Inject, Injectable, InjectionToken, Injector, Input, NgModule, NgModuleRef, Optional, Pipe, Provider, Self, Type, forwardRef, getModuleFactory, ɵivyEnabled as ivyEnabled, ɵɵdefineNgModule as defineNgModule} from '@angular/core';
1010
import {Console} from '@angular/core/src/console';
1111
import {ɵɵInjectableDef, ɵɵdefineInjectable} from '@angular/core/src/di/interface/defs';
1212
import {getNgModuleDef} from '@angular/core/src/render3/definition';
@@ -339,6 +339,28 @@ function declareTests(config?: {useJit: boolean}) {
339339
}
340340
}).not.toThrow();
341341
});
342+
343+
onlyInIvy('VE does not allow use of NgModuleFactory without importing the .ngfactory')
344+
.it('should register a module even if not importing the .ngfactory file or calling create()',
345+
() => {
346+
class ChildModule {
347+
static ngModuleDef = defineNgModule({
348+
type: ChildModule,
349+
id: 'child',
350+
});
351+
}
352+
353+
class Module {
354+
static ngModuleDef = defineNgModule({
355+
type: Module,
356+
id: 'test',
357+
imports: [ChildModule],
358+
});
359+
}
360+
361+
createModuleFactory(ChildModule);
362+
expect(getModuleFactory('child')).toBeAnInstanceOf(NgModuleFactory);
363+
});
342364
});
343365

344366
describe('entryComponents', () => {

0 commit comments

Comments
 (0)