Skip to content

Commit 34c37b0

Browse files
clydinkyliau
authored andcommitted
fix(@ngtools/webpack): control the presence of Ivy class metadata and module scope
This change allows the import eliding capabilities of the plugin to more completely remove the effects of the `setClassMetadata` and `setNgModuleScope` functions. This provides equivalent behavior to the Decorator removal functionality for ViewEngine.
1 parent 203f48a commit 34c37b0

File tree

5 files changed

+282
-0
lines changed

5 files changed

+282
-0
lines changed

packages/ngtools/webpack/src/angular_compiler_plugin.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
} from './transformers';
6262
import { collectDeepNodes } from './transformers/ast_helpers';
6363
import { downlevelConstructorParameters } from './transformers/ctor-parameters';
64+
import { removeIvyJitSupportCalls } from './transformers/remove-ivy-jit-support-calls';
6465
import {
6566
AUTO_START_ARG,
6667
} from './type_checker';
@@ -1002,6 +1003,15 @@ export class AngularCompilerPlugin {
10021003
// Remove unneeded angular decorators in VE.
10031004
// In Ivy they are removed in ngc directly.
10041005
this._transformers.push(removeDecorators(isAppPath, getTypeChecker));
1006+
} else {
1007+
// Default for both options is to emit (undefined means true)
1008+
const removeClassMetadata = this._options.emitClassMetadata === false;
1009+
const removeNgModuleScope = this._options.emitNgModuleScope === false;
1010+
if (removeClassMetadata || removeNgModuleScope) {
1011+
this._transformers.push(
1012+
removeIvyJitSupportCalls(removeClassMetadata, removeNgModuleScope, getTypeChecker),
1013+
);
1014+
}
10051015
}
10061016
// Import ngfactory in loadChildren import syntax
10071017
if (this._useFactories) {

packages/ngtools/webpack/src/interfaces.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ export interface AngularCompilerPluginOptions {
4545
logger?: logging.Logger;
4646
directTemplateLoading?: boolean;
4747

48+
/* @internal */
49+
emitClassMetadata?: boolean;
50+
/* @internal */
51+
emitNgModuleScope?: boolean;
52+
4853
/**
4954
* When using the loadChildren string syntax, @ngtools/webpack must query @angular/compiler-cli
5055
* via a private API to know which lazy routes exist. This increases build and rebuild time.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import * as ts from 'typescript';
9+
import { collectDeepNodes } from './ast_helpers';
10+
import { RemoveNodeOperation, StandardTransform, TransformOperation } from './interfaces';
11+
import { makeTransform } from './make_transform';
12+
13+
export function removeIvyJitSupportCalls(
14+
classMetadata: boolean,
15+
ngModuleScope: boolean,
16+
getTypeChecker: () => ts.TypeChecker,
17+
): ts.TransformerFactory<ts.SourceFile> {
18+
const standardTransform: StandardTransform = function(sourceFile: ts.SourceFile) {
19+
const ops: TransformOperation[] = [];
20+
21+
collectDeepNodes<ts.ExpressionStatement>(sourceFile, ts.SyntaxKind.ExpressionStatement)
22+
.filter(statement => {
23+
const innerStatement = getIifeStatement(statement);
24+
if (!innerStatement) {
25+
return false;
26+
}
27+
28+
if (ngModuleScope && ts.isBinaryExpression(innerStatement.expression)) {
29+
return isIvyPrivateCallExpression(innerStatement.expression.right, 'ɵɵsetNgModuleScope');
30+
}
31+
32+
return (
33+
classMetadata &&
34+
isIvyPrivateCallExpression(innerStatement.expression, 'ɵsetClassMetadata')
35+
);
36+
})
37+
.forEach(statement => ops.push(new RemoveNodeOperation(sourceFile, statement)));
38+
39+
return ops;
40+
};
41+
42+
return makeTransform(standardTransform, getTypeChecker);
43+
}
44+
45+
// Each Ivy private call expression is inside an IIFE
46+
function getIifeStatement(exprStmt: ts.ExpressionStatement): null | ts.ExpressionStatement {
47+
const expression = exprStmt.expression;
48+
if (!expression || !ts.isCallExpression(expression) || expression.arguments.length !== 0) {
49+
return null;
50+
}
51+
52+
const parenExpr = expression;
53+
if (!ts.isParenthesizedExpression(parenExpr.expression)) {
54+
return null;
55+
}
56+
57+
const funExpr = parenExpr.expression.expression;
58+
if (!ts.isFunctionExpression(funExpr)) {
59+
return null;
60+
}
61+
62+
const innerStmts = funExpr.body.statements;
63+
if (innerStmts.length !== 1) {
64+
return null;
65+
}
66+
67+
const innerExprStmt = innerStmts[0];
68+
if (!ts.isExpressionStatement(innerExprStmt)) {
69+
return null;
70+
}
71+
72+
return innerExprStmt;
73+
}
74+
75+
function isIvyPrivateCallExpression(expression: ts.Expression, name: string) {
76+
// Now we're in the IIFE and have the inner expression statement. We can check if it matches
77+
// a private Ivy call.
78+
if (!ts.isCallExpression(expression)) {
79+
return false;
80+
}
81+
82+
const propAccExpr = expression.expression;
83+
if (!ts.isPropertyAccessExpression(propAccExpr)) {
84+
return false;
85+
}
86+
87+
if (propAccExpr.name.text != name) {
88+
return false;
89+
}
90+
91+
return true;
92+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies
9+
import * as ts from 'typescript';
10+
import { createTypescriptContext, transformTypescript } from './ast_helpers';
11+
import { removeIvyJitSupportCalls } from './remove-ivy-jit-support-calls';
12+
13+
function transform(
14+
input: string,
15+
transformerFactory: (
16+
getTypeChecker: () => ts.TypeChecker,
17+
) => ts.TransformerFactory<ts.SourceFile>,
18+
) {
19+
const { program, compilerHost } = createTypescriptContext(input);
20+
const getTypeChecker = () => program.getTypeChecker();
21+
const transformer = transformerFactory(getTypeChecker);
22+
23+
return transformTypescript(input, [transformer], program, compilerHost);
24+
}
25+
26+
const input = tags.stripIndent`
27+
export class AppModule {
28+
}
29+
AppModule.ɵmod = i0.ɵɵdefineNgModule({ type: AppModule, bootstrap: [AppComponent] });
30+
AppModule.ɵinj = i0.ɵɵdefineInjector({ factory: function AppModule_Factory(t) { return new (t || AppModule)(); }, providers: [], imports: [[
31+
BrowserModule,
32+
AppRoutingModule
33+
]] });
34+
(function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(AppModule, { declarations: [AppComponent,
35+
ExampleComponent], imports: [BrowserModule,
36+
AppRoutingModule] }); })();
37+
/*@__PURE__*/ (function () { i0.ɵsetClassMetadata(AppModule, [{
38+
type: NgModule,
39+
args: [{
40+
declarations: [
41+
AppComponent,
42+
ExampleComponent
43+
],
44+
imports: [
45+
BrowserModule,
46+
AppRoutingModule
47+
],
48+
providers: [],
49+
bootstrap: [AppComponent]
50+
}]
51+
}], null, null); })();
52+
`;
53+
54+
describe('@ngtools/webpack transformers', () => {
55+
describe('remove-ivy-dev-calls', () => {
56+
it('should allow removing only set class metadata', () => {
57+
const output = tags.stripIndent`
58+
export class AppModule {
59+
}
60+
AppModule.ɵmod = i0.ɵɵdefineNgModule({ type: AppModule, bootstrap: [AppComponent] });
61+
AppModule.ɵinj = i0.ɵɵdefineInjector({ factory: function AppModule_Factory(t) { return new (t || AppModule)(); }, providers: [], imports: [[
62+
BrowserModule,
63+
AppRoutingModule
64+
]] });
65+
(function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(AppModule, { declarations: [AppComponent,
66+
ExampleComponent], imports: [BrowserModule,
67+
AppRoutingModule] }); })();
68+
`;
69+
70+
const result = transform(input, getTypeChecker =>
71+
removeIvyJitSupportCalls(true, false, getTypeChecker),
72+
);
73+
74+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
75+
});
76+
77+
it('should allow removing only ng module scope', () => {
78+
const output = tags.stripIndent`
79+
export class AppModule {
80+
}
81+
AppModule.ɵmod = i0.ɵɵdefineNgModule({ type: AppModule, bootstrap: [AppComponent] });
82+
AppModule.ɵinj = i0.ɵɵdefineInjector({ factory: function AppModule_Factory(t) { return new (t || AppModule)(); }, providers: [], imports: [[
83+
BrowserModule,
84+
AppRoutingModule
85+
]] });
86+
/*@__PURE__*/ (function () { i0.ɵsetClassMetadata(AppModule, [{
87+
type: NgModule,
88+
args: [{
89+
declarations: [
90+
AppComponent,
91+
ExampleComponent
92+
],
93+
imports: [
94+
BrowserModule,
95+
AppRoutingModule
96+
],
97+
providers: [],
98+
bootstrap: [AppComponent]
99+
}]
100+
}], null, null); })();
101+
`;
102+
103+
const result = transform(input, getTypeChecker =>
104+
removeIvyJitSupportCalls(false, true, getTypeChecker),
105+
);
106+
107+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
108+
});
109+
110+
it('should allow removing both set class metadata and ng module scope', () => {
111+
const output = tags.stripIndent`
112+
export class AppModule {
113+
}
114+
AppModule.ɵmod = i0.ɵɵdefineNgModule({ type: AppModule, bootstrap: [AppComponent] });
115+
AppModule.ɵinj = i0.ɵɵdefineInjector({ factory: function AppModule_Factory(t) { return new (t || AppModule)(); }, providers: [], imports: [[
116+
BrowserModule,
117+
AppRoutingModule
118+
]] });
119+
`;
120+
121+
const result = transform(input, getTypeChecker =>
122+
removeIvyJitSupportCalls(true, true, getTypeChecker),
123+
);
124+
125+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
126+
});
127+
128+
it('should allow removing neither set class metadata nor ng module scope', () => {
129+
const result = transform(input, getTypeChecker =>
130+
removeIvyJitSupportCalls(false, false, getTypeChecker),
131+
);
132+
133+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`);
134+
});
135+
136+
it('should strip unused imports when removing set class metadata and ng module scope', () => {
137+
const imports = tags.stripIndent`
138+
import { BrowserModule } from '@angular/platform-browser';
139+
import { NgModule } from '@angular/core';
140+
import { AppRoutingModule } from './app-routing.module';
141+
import { AppComponent } from './app.component';
142+
import { ExampleComponent } from './example/example.component';
143+
import * as i0 from "@angular/core";
144+
`;
145+
146+
const output = tags.stripIndent`
147+
import { BrowserModule } from '@angular/platform-browser';
148+
import { AppRoutingModule } from './app-routing.module';
149+
import { AppComponent } from './app.component';
150+
import * as i0 from "@angular/core";
151+
export class AppModule {
152+
}
153+
AppModule.ɵmod = i0.ɵɵdefineNgModule({ type: AppModule, bootstrap: [AppComponent] });
154+
AppModule.ɵinj = i0.ɵɵdefineInjector({ factory: function AppModule_Factory(t) { return new (t || AppModule)(); }, providers: [], imports: [[
155+
BrowserModule,
156+
AppRoutingModule
157+
]] });
158+
`;
159+
160+
const result = transform(imports + input, getTypeChecker =>
161+
removeIvyJitSupportCalls(true, true, getTypeChecker),
162+
);
163+
164+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
165+
});
166+
167+
});
168+
});

tests/legacy-cli/e2e/tests/build/prod-build.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getGlobalVariable } from '../../utils/env';
44
import { expectFileToExist, expectFileToMatch, readFile } from '../../utils/fs';
55
import { expectGitToBeClean } from '../../utils/git';
66
import { ng } from '../../utils/process';
7+
import { expectToFail } from '../../utils/utils';
78

89

910
function verifySize(bundle: string, baselineBytes: number) {
@@ -51,6 +52,12 @@ export default async function () {
5152
// Content checks
5253
await expectFileToMatch(`dist/test-project/${mainES5Path}`, bootstrapRegExp);
5354
await expectFileToMatch(`dist/test-project/${mainES2015Path}`, bootstrapRegExp);
55+
await expectToFail(() =>
56+
expectFileToMatch(`dist/test-project/${mainES5Path}`, 'setNgModuleScope'),
57+
);
58+
await expectToFail(() =>
59+
expectFileToMatch(`dist/test-project/${mainES5Path}`, 'setClassMetadata'),
60+
);
5461

5562
// Size checks in bytes
5663
if (veProject) {

0 commit comments

Comments
 (0)