Skip to content

Commit 45c6360

Browse files
JoostKjasonaden
authored andcommitted
feat(ivy): emit module scope metadata using pure function call (angular#29598)
Prior to this change, all module metadata would be included in the `defineNgModule` call that is set as the `ngModuleDef` field of module types. Part of the metadata is scope information like declarations, imports and exports that is used for computing the transitive module scope in JIT environments, preventing those references from being tree-shaken for production builds. This change moves the metadata for scope computations to a pure function call that patches the scope references onto the module type. Because the function is marked pure, it may be tree-shaken out during production builds such that references to declarations and exports are dropped, which in turn allows for tree-shaken any declaration that is not otherwise referenced. Fixes angular#28077, FW-1035 PR Close angular#29598
1 parent 6b39c9c commit 45c6360

File tree

11 files changed

+213
-37
lines changed

11 files changed

+213
-37
lines changed

packages/compiler-cli/ngcc/src/rendering/renderer.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -487,16 +487,15 @@ export function renderDefinitions(
487487
const name = compiledClass.declaration.name;
488488
const translate = (stmt: Statement) =>
489489
translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER);
490-
const definitions =
491-
compiledClass.compilation
492-
.map(
493-
c => c.statements.map(statement => translate(statement))
494-
.concat(translate(createAssignmentStatement(name, c.name, c.initializer)))
495-
.map(
496-
statement =>
497-
printer.printNode(ts.EmitHint.Unspecified, statement, sourceFile))
498-
.join('\n'))
499-
.join('\n');
490+
const print = (stmt: Statement) =>
491+
printer.printNode(ts.EmitHint.Unspecified, translate(stmt), sourceFile);
492+
const definitions = compiledClass.compilation
493+
.map(
494+
c => [createAssignmentStatement(name, c.name, c.initializer)]
495+
.concat(c.statements)
496+
.map(print)
497+
.join('\n'))
498+
.join('\n');
500499
return definitions;
501500
}
502501

packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,17 @@ describe('Renderer', () => {
151151
moduleWithProvidersAnalyses);
152152
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
153153
expect(addDefinitionsSpy.calls.first().args[2])
154-
.toEqual(`/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
155-
type: Component,
156-
args: [{ selector: 'a', template: '{{ person!.name }}' }]
157-
}], null, null);
158-
A.ngComponentDef = ɵngcc0.ɵdefineComponent({ type: A, selectors: [["a"]], factory: function A_Factory(t) { return new (t || A)(); }, consts: 1, vars: 1, template: function A_Template(rf, ctx) { if (rf & 1) {
154+
.toEqual(
155+
`A.ngComponentDef = ɵngcc0.ɵdefineComponent({ type: A, selectors: [["a"]], factory: function A_Factory(t) { return new (t || A)(); }, consts: 1, vars: 1, template: function A_Template(rf, ctx) { if (rf & 1) {
159156
ɵngcc0.ɵtext(0);
160157
} if (rf & 2) {
161158
ɵngcc0.ɵselect(0);
162159
ɵngcc0.ɵtextBinding(0, ɵngcc0.ɵinterpolation1("", ctx.person.name, ""));
163-
} }, encapsulation: 2 });`);
160+
} }, encapsulation: 2 });
161+
/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
162+
type: Component,
163+
args: [{ selector: 'a', template: '{{ person!.name }}' }]
164+
}], null, null);`);
164165
});
165166

166167

@@ -195,11 +196,12 @@ A.ngComponentDef = ɵngcc0.ɵdefineComponent({ type: A, selectors: [["a"]], fact
195196
decorators: [jasmine.objectContaining({name: 'Directive'})],
196197
}));
197198
expect(addDefinitionsSpy.calls.first().args[2])
198-
.toEqual(`/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
199+
.toEqual(
200+
`A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""]], factory: function A_Factory(t) { return new (t || A)(); } });
201+
/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
199202
type: Directive,
200203
args: [{ selector: '[a]' }]
201-
}], null, { foo: [] });
202-
A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""]], factory: function A_Factory(t) { return new (t || A)(); } });`);
204+
}], null, { foo: [] });`);
203205
});
204206

205207
it('should call removeDecorators with the source code, a map of class decorators that have been analyzed',

packages/compiler-cli/src/ngtsc/imports/src/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const CORE_SUPPORTED_SYMBOLS = new Map<string, string>([
5151
['defineInjectable', 'defineInjectable'],
5252
['defineInjector', 'defineInjector'],
5353
['ɵdefineNgModule', 'defineNgModule'],
54+
['ɵsetNgModuleScope', 'setNgModuleScope'],
5455
['inject', 'inject'],
5556
['ɵsetClassMetadata', 'setClassMetadata'],
5657
['ɵInjectableDef', 'InjectableDef'],

packages/compiler-cli/test/ngtsc/ngtsc_spec.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -442,10 +442,9 @@ describe('ngtsc behavioral tests', () => {
442442
env.driveMain();
443443

444444
const jsContents = env.getContents('test.js');
445+
expect(jsContents).toContain('i0.ɵdefineNgModule({ type: TestModule, bootstrap: [TestCmp] });');
445446
expect(jsContents)
446-
.toContain(
447-
'i0.ɵdefineNgModule({ type: TestModule, bootstrap: [TestCmp], ' +
448-
'declarations: [TestCmp] })');
447+
.toContain('/*@__PURE__*/ i0.ɵsetNgModuleScope(TestModule, { declarations: [TestCmp] });');
449448

450449
const dtsContents = env.getContents('test.d.ts');
451450
expect(dtsContents)
@@ -457,6 +456,22 @@ describe('ngtsc behavioral tests', () => {
457456
expect(dtsContents).not.toContain('__decorate');
458457
});
459458

459+
it('should not emit a setNgModuleScope call when no scope metadata is present', () => {
460+
env.tsconfig();
461+
env.write('test.ts', `
462+
import {NgModule} from '@angular/core';
463+
464+
@NgModule({})
465+
export class TestModule {}
466+
`);
467+
468+
env.driveMain();
469+
470+
const jsContents = env.getContents('test.js');
471+
expect(jsContents).toContain('i0.ɵdefineNgModule({ type: TestModule });');
472+
expect(jsContents).not.toContain('ɵsetNgModuleScope(TestModule,');
473+
});
474+
460475
it('should compile NgModules with services without errors', () => {
461476
env.tsconfig();
462477
env.write('test.ts', `
@@ -484,7 +499,7 @@ describe('ngtsc behavioral tests', () => {
484499
env.driveMain();
485500

486501
const jsContents = env.getContents('test.js');
487-
expect(jsContents).toContain('i0.ɵdefineNgModule({ type: TestModule,');
502+
expect(jsContents).toContain('i0.ɵdefineNgModule({ type: TestModule });');
488503
expect(jsContents)
489504
.toContain(
490505
`TestModule.ngInjectorDef = i0.defineInjector({ factory: ` +
@@ -525,7 +540,7 @@ describe('ngtsc behavioral tests', () => {
525540
env.driveMain();
526541

527542
const jsContents = env.getContents('test.js');
528-
expect(jsContents).toContain('i0.ɵdefineNgModule({ type: TestModule,');
543+
expect(jsContents).toContain('i0.ɵdefineNgModule({ type: TestModule });');
529544
expect(jsContents)
530545
.toContain(
531546
`TestModule.ngInjectorDef = i0.defineInjector({ factory: ` +
@@ -570,7 +585,7 @@ describe('ngtsc behavioral tests', () => {
570585
env.driveMain();
571586

572587
const jsContents = env.getContents('test.js');
573-
expect(jsContents).toContain('i0.ɵdefineNgModule({ type: TestModule,');
588+
expect(jsContents).toContain('i0.ɵdefineNgModule({ type: TestModule });');
574589
expect(jsContents)
575590
.toContain(
576591
`TestModule.ngInjectorDef = i0.defineInjector({ factory: ` +

packages/compiler-cli/test/ngtsc/scope_spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,32 @@ describe('ngtsc module scopes', () => {
2121

2222
describe('diagnostics', () => {
2323
describe('imports', () => {
24+
it('should emit imports in a pure function call', () => {
25+
env.tsconfig();
26+
env.write('test.ts', `
27+
import {NgModule} from '@angular/core';
28+
29+
@NgModule({})
30+
export class OtherModule {}
31+
32+
@NgModule({imports: [OtherModule]})
33+
export class TestModule {}
34+
`);
35+
36+
env.driveMain();
37+
38+
const jsContents = env.getContents('test.js');
39+
expect(jsContents).toContain('i0.ɵdefineNgModule({ type: TestModule });');
40+
expect(jsContents)
41+
.toContain(
42+
'/*@__PURE__*/ i0.ɵsetNgModuleScope(TestModule, { imports: [OtherModule] });');
43+
44+
const dtsContents = env.getContents('test.d.ts');
45+
expect(dtsContents)
46+
.toContain(
47+
'static ngModuleDef: i0.ɵNgModuleDefWithMeta<TestModule, never, [typeof OtherModule], never>');
48+
});
49+
2450
it('should produce an error when an invalid class is imported', () => {
2551
env.write('test.ts', `
2652
import {NgModule} from '@angular/core';
@@ -57,6 +83,32 @@ describe('ngtsc module scopes', () => {
5783
});
5884

5985
describe('exports', () => {
86+
it('should emit exports in a pure function call', () => {
87+
env.tsconfig();
88+
env.write('test.ts', `
89+
import {NgModule} from '@angular/core';
90+
91+
@NgModule({})
92+
export class OtherModule {}
93+
94+
@NgModule({exports: [OtherModule]})
95+
export class TestModule {}
96+
`);
97+
98+
env.driveMain();
99+
100+
const jsContents = env.getContents('test.js');
101+
expect(jsContents).toContain('i0.ɵdefineNgModule({ type: TestModule });');
102+
expect(jsContents)
103+
.toContain(
104+
'/*@__PURE__*/ i0.ɵsetNgModuleScope(TestModule, { exports: [OtherModule] });');
105+
106+
const dtsContents = env.getContents('test.d.ts');
107+
expect(dtsContents)
108+
.toContain(
109+
'static ngModuleDef: i0.ɵNgModuleDefWithMeta<TestModule, never, never, [typeof OtherModule]>');
110+
});
111+
60112
it('should produce an error when a non-NgModule class is exported', () => {
61113
env.write('test.ts', `
62114
import {NgModule} from '@angular/core';

packages/compiler/src/render3/r3_identifiers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ export class Identifiers {
195195
};
196196

197197
static defineNgModule: o.ExternalReference = {name: 'ɵdefineNgModule', moduleName: CORE};
198+
static setNgModuleScope: o.ExternalReference = {name: 'ɵsetNgModuleScope', moduleName: CORE};
198199

199200
static PipeDefWithMeta: o.ExternalReference = {name: 'ɵPipeDefWithMeta', moduleName: CORE};
200201

packages/compiler/src/render3/r3_module_compiler.ts

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@ export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef {
8080
imports,
8181
exports,
8282
schemas,
83-
containsForwardDecls
83+
containsForwardDecls,
84+
emitInline
8485
} = meta;
8586

87+
const additionalStatements: o.Statement[] = [];
8688
const definitionMap = {
8789
type: moduleType
8890
} as{
@@ -99,16 +101,29 @@ export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef {
99101
definitionMap.bootstrap = refsToArray(bootstrap, containsForwardDecls);
100102
}
101103

102-
if (declarations.length) {
103-
definitionMap.declarations = refsToArray(declarations, containsForwardDecls);
104-
}
104+
// If requested to emit scope information inline, pass the declarations, imports and exports to
105+
// the `defineNgModule` call. The JIT compilation uses this.
106+
if (emitInline) {
107+
if (declarations.length) {
108+
definitionMap.declarations = refsToArray(declarations, containsForwardDecls);
109+
}
105110

106-
if (imports.length) {
107-
definitionMap.imports = refsToArray(imports, containsForwardDecls);
111+
if (imports.length) {
112+
definitionMap.imports = refsToArray(imports, containsForwardDecls);
113+
}
114+
115+
if (exports.length) {
116+
definitionMap.exports = refsToArray(exports, containsForwardDecls);
117+
}
108118
}
109119

110-
if (exports.length) {
111-
definitionMap.exports = refsToArray(exports, containsForwardDecls);
120+
// If not emitting inline, the scope information is not passed into `defineNgModule` as it would
121+
// prevent tree-shaking of the declarations, imports and exports references.
122+
else {
123+
const setNgModuleScopeCall = generateSetNgModuleScopeCall(meta);
124+
if (setNgModuleScopeCall !== null) {
125+
additionalStatements.push(setNgModuleScopeCall);
126+
}
112127
}
113128

114129
if (schemas && schemas.length) {
@@ -121,10 +136,50 @@ export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef {
121136
tupleTypeOf(exports)
122137
]));
123138

124-
const additionalStatements: o.Statement[] = [];
139+
125140
return {expression, type, additionalStatements};
126141
}
127142

143+
/**
144+
* Generates a function call to `setNgModuleScope` with all necessary information so that the
145+
* transitive module scope can be computed during runtime in JIT mode. This call is marked pure
146+
* such that the references to declarations, imports and exports may be elided causing these
147+
* symbols to become tree-shakeable.
148+
*/
149+
function generateSetNgModuleScopeCall(meta: R3NgModuleMetadata): o.Statement|null {
150+
const {type: moduleType, declarations, imports, exports, containsForwardDecls} = meta;
151+
152+
const scopeMap = {} as{
153+
declarations: o.Expression,
154+
imports: o.Expression,
155+
exports: o.Expression,
156+
};
157+
158+
if (declarations.length) {
159+
scopeMap.declarations = refsToArray(declarations, containsForwardDecls);
160+
}
161+
162+
if (imports.length) {
163+
scopeMap.imports = refsToArray(imports, containsForwardDecls);
164+
}
165+
166+
if (exports.length) {
167+
scopeMap.exports = refsToArray(exports, containsForwardDecls);
168+
}
169+
170+
if (Object.keys(scopeMap).length === 0) {
171+
return null;
172+
}
173+
174+
const fnCall = new o.InvokeFunctionExpr(
175+
/* fn */ o.importExpr(R3.setNgModuleScope),
176+
/* args */[moduleType, mapToMapExpression(scopeMap)],
177+
/* type */ undefined,
178+
/* sourceSpan */ undefined,
179+
/* pure */ true);
180+
return fnCall.toStmt();
181+
}
182+
128183
export interface R3InjectorDef {
129184
expression: o.Expression;
130185
type: o.Type;
@@ -200,4 +255,4 @@ function tupleTypeOf(exp: R3Reference[]): o.Type {
200255
function refsToArray(refs: R3Reference[], shouldForwardDeclare: boolean): o.Expression {
201256
const values = o.literalArr(refs.map(ref => ref.value));
202257
return shouldForwardDeclare ? o.fn([], [new o.ReturnStatement(values)]) : values;
203-
}
258+
}

packages/core/src/core_render3_private_export.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export {
2626
getFactoryOf as ɵgetFactoryOf,
2727
getInheritedFactory as ɵgetInheritedFactory,
2828
setComponentScope as ɵsetComponentScope,
29+
setNgModuleScope as ɵsetNgModuleScope,
2930
templateRefExtractor as ɵtemplateRefExtractor,
3031
ProvidersFeature as ɵProvidersFeature,
3132
InheritDefinitionFeature as ɵInheritDefinitionFeature,

packages/core/src/render3/definition.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,28 @@ export function extractPipeDef(type: PipeType<any>): PipeDef<any> {
327327
return def !;
328328
}
329329

330-
export function defineNgModule<T>(def: {type: T} & Partial<NgModuleDef<T>>): never {
330+
export function defineNgModule<T>(def: {
331+
/** Token representing the module. Used by DI. */
332+
type: T;
333+
334+
/** List of components to bootstrap. */
335+
bootstrap?: Type<any>[] | (() => Type<any>[]);
336+
337+
/** List of components, directives, and pipes declared by this module. */
338+
declarations?: Type<any>[] | (() => Type<any>[]);
339+
340+
/** List of modules or `ModuleWithProviders` imported by this module. */
341+
imports?: Type<any>[] | (() => Type<any>[]);
342+
343+
/**
344+
* List of modules, `ModuleWithProviders`, components, directives, or pipes exported by this
345+
* module.
346+
*/
347+
exports?: Type<any>[] | (() => Type<any>[]);
348+
349+
/** The set of schemas that declare elements to be allowed in the NgModule. */
350+
schemas?: SchemaMetadata[] | null;
351+
}): never {
331352
const res: NgModuleDef<T> = {
332353
type: def.type,
333354
bootstrap: def.bootstrap || EMPTY_ARRAY,
@@ -340,6 +361,33 @@ export function defineNgModule<T>(def: {type: T} & Partial<NgModuleDef<T>>): nev
340361
return res as never;
341362
}
342363

364+
/**
365+
* Adds the module metadata that is necessary to compute the module's transitive scope to an
366+
* existing module definition.
367+
*
368+
* Scope metadata of modules is not used in production builds, so calls to this function can be
369+
* marked pure to tree-shake it from the bundle, allowing for all referenced declarations
370+
* to become eligible for tree-shaking as well.
371+
*/
372+
export function setNgModuleScope(type: any, scope: {
373+
/** List of components, directives, and pipes declared by this module. */
374+
declarations?: Type<any>[] | (() => Type<any>[]);
375+
376+
/** List of modules or `ModuleWithProviders` imported by this module. */
377+
imports?: Type<any>[] | (() => Type<any>[]);
378+
379+
/**
380+
* List of modules, `ModuleWithProviders`, components, directives, or pipes exported by this
381+
* module.
382+
*/
383+
exports?: Type<any>[] | (() => Type<any>[]);
384+
}): void {
385+
const ngModuleDef = getNgModuleDef(type, true);
386+
ngModuleDef.declarations = scope.declarations || EMPTY_ARRAY;
387+
ngModuleDef.imports = scope.imports || EMPTY_ARRAY;
388+
ngModuleDef.exports = scope.exports || EMPTY_ARRAY;
389+
}
390+
343391
/**
344392
* Inverts an inputs or outputs lookup such that the keys, which were the
345393
* minified keys, are part of the values, and the values are parsed so that

0 commit comments

Comments
 (0)