Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,17 @@ export function getAotConfig(wco: WebpackConfigOptions, i18nExtract = false) {
}

const test = /(?:\.ngfactory\.js|\.ngstyle\.js|\.tsx?)$/;
const optimize = wco.buildOptions.optimization.scripts;

return {
module: { rules: [{ test, use: loaders }] },
plugins: [_createAotPlugin(wco, { tsConfigPath }, i18nExtract)]
plugins: [
_createAotPlugin(
Comment thread
clydin marked this conversation as resolved.
Resolved
wco,
{ tsConfigPath, emitClassMetadata: !optimize, emitNgModuleScope: !optimize },
i18nExtract,
),
],
};
}

Expand Down
10 changes: 10 additions & 0 deletions packages/ngtools/webpack/src/angular_compiler_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
} from './transformers';
import { collectDeepNodes } from './transformers/ast_helpers';
import { downlevelConstructorParameters } from './transformers/ctor-parameters';
import { removeIvyJitSupportCalls } from './transformers/remove-ivy-jit-support-calls';
import {
AUTO_START_ARG,
} from './type_checker';
Expand Down Expand Up @@ -1002,6 +1003,15 @@ export class AngularCompilerPlugin {
// Remove unneeded angular decorators in VE.
// In Ivy they are removed in ngc directly.
this._transformers.push(removeDecorators(isAppPath, getTypeChecker));
} else {
// Default for both options is to emit (undefined means true)
const removeClassMetadata = this._options.emitClassMetadata === false;
const removeNgModuleScope = this._options.emitNgModuleScope === false;
if (removeClassMetadata || removeNgModuleScope) {
this._transformers.push(
removeIvyJitSupportCalls(removeClassMetadata, removeNgModuleScope, getTypeChecker),
);
}
}
// Import ngfactory in loadChildren import syntax
if (this._useFactories) {
Expand Down
5 changes: 5 additions & 0 deletions packages/ngtools/webpack/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export interface AngularCompilerPluginOptions {
logger?: logging.Logger;
directTemplateLoading?: boolean;

/* @internal */
emitClassMetadata?: boolean;
/* @internal */
emitNgModuleScope?: boolean;

/**
* When using the loadChildren string syntax, @ngtools/webpack must query @angular/compiler-cli
* via a private API to know which lazy routes exist. This increases build and rebuild time.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import { collectDeepNodes } from './ast_helpers';
import { RemoveNodeOperation, StandardTransform, TransformOperation } from './interfaces';
import { makeTransform } from './make_transform';

export function removeIvyJitSupportCalls(
classMetadata: boolean,
ngModuleScope: boolean,
getTypeChecker: () => ts.TypeChecker,
): ts.TransformerFactory<ts.SourceFile> {
const standardTransform: StandardTransform = function(sourceFile: ts.SourceFile) {
const ops: TransformOperation[] = [];

collectDeepNodes<ts.ExpressionStatement>(sourceFile, ts.SyntaxKind.ExpressionStatement)
.filter(statement => {
const innerStatement = getIifeStatement(statement);
if (!innerStatement) {
return false;
}

if (ngModuleScope && ts.isBinaryExpression(innerStatement.expression)) {
return isIvyPrivateCallExpression(innerStatement.expression.right, 'ɵɵsetNgModuleScope');
}

return (
classMetadata &&
isIvyPrivateCallExpression(innerStatement.expression, 'ɵsetClassMetadata')
);
})
.forEach(statement => ops.push(new RemoveNodeOperation(sourceFile, statement)));

return ops;
};

return makeTransform(standardTransform, getTypeChecker);
}

// Each Ivy private call expression is inside an IIFE
function getIifeStatement(exprStmt: ts.ExpressionStatement): null | ts.ExpressionStatement {
const expression = exprStmt.expression;
if (!expression || !ts.isCallExpression(expression) || expression.arguments.length !== 0) {
return null;
}

const parenExpr = expression;
if (!ts.isParenthesizedExpression(parenExpr.expression)) {
return null;
}

const funExpr = parenExpr.expression.expression;
if (!ts.isFunctionExpression(funExpr)) {
return null;
}

const innerStmts = funExpr.body.statements;
if (innerStmts.length !== 1) {
return null;
}

const innerExprStmt = innerStmts[0];
if (!ts.isExpressionStatement(innerExprStmt)) {
return null;
}

return innerExprStmt;
}

function isIvyPrivateCallExpression(expression: ts.Expression, name: string) {
// Now we're in the IIFE and have the inner expression statement. We can check if it matches
// a private Ivy call.
if (!ts.isCallExpression(expression)) {
return false;
}

const propAccExpr = expression.expression;
if (!ts.isPropertyAccessExpression(propAccExpr)) {
return false;
}

if (propAccExpr.name.text != name) {
return false;
}

return true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies
import * as ts from 'typescript';
import { createTypescriptContext, transformTypescript } from './ast_helpers';
import { removeIvyJitSupportCalls } from './remove-ivy-jit-support-calls';

function transform(
input: string,
transformerFactory: (
getTypeChecker: () => ts.TypeChecker,
) => ts.TransformerFactory<ts.SourceFile>,
) {
const { program, compilerHost } = createTypescriptContext(input);
const getTypeChecker = () => program.getTypeChecker();
const transformer = transformerFactory(getTypeChecker);

return transformTypescript(input, [transformer], program, compilerHost);
}

const input = tags.stripIndent`
export class AppModule {
}
AppModule.ɵmod = i0.ɵɵdefineNgModule({ type: AppModule, bootstrap: [AppComponent] });
AppModule.ɵinj = i0.ɵɵdefineInjector({ factory: function AppModule_Factory(t) { return new (t || AppModule)(); }, providers: [], imports: [[
BrowserModule,
AppRoutingModule
]] });
(function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(AppModule, { declarations: [AppComponent,
ExampleComponent], imports: [BrowserModule,
AppRoutingModule] }); })();
/*@__PURE__*/ (function () { i0.ɵsetClassMetadata(AppModule, [{
type: NgModule,
args: [{
declarations: [
AppComponent,
ExampleComponent
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
}]
}], null, null); })();
`;

describe('@ngtools/webpack transformers', () => {
describe('remove-ivy-dev-calls', () => {
it('should allow removing only set class metadata', () => {
const output = tags.stripIndent`
export class AppModule {
}
AppModule.ɵmod = i0.ɵɵdefineNgModule({ type: AppModule, bootstrap: [AppComponent] });
AppModule.ɵinj = i0.ɵɵdefineInjector({ factory: function AppModule_Factory(t) { return new (t || AppModule)(); }, providers: [], imports: [[
BrowserModule,
AppRoutingModule
]] });
(function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(AppModule, { declarations: [AppComponent,
ExampleComponent], imports: [BrowserModule,
AppRoutingModule] }); })();
`;

const result = transform(input, getTypeChecker =>
removeIvyJitSupportCalls(true, false, getTypeChecker),
);

expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});

it('should allow removing only ng module scope', () => {
const output = tags.stripIndent`
export class AppModule {
}
AppModule.ɵmod = i0.ɵɵdefineNgModule({ type: AppModule, bootstrap: [AppComponent] });
AppModule.ɵinj = i0.ɵɵdefineInjector({ factory: function AppModule_Factory(t) { return new (t || AppModule)(); }, providers: [], imports: [[
BrowserModule,
AppRoutingModule
]] });
/*@__PURE__*/ (function () { i0.ɵsetClassMetadata(AppModule, [{
type: NgModule,
args: [{
declarations: [
AppComponent,
ExampleComponent
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
}]
}], null, null); })();
`;

const result = transform(input, getTypeChecker =>
removeIvyJitSupportCalls(false, true, getTypeChecker),
);

expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});

it('should allow removing both set class metadata and ng module scope', () => {
const output = tags.stripIndent`
export class AppModule {
}
AppModule.ɵmod = i0.ɵɵdefineNgModule({ type: AppModule, bootstrap: [AppComponent] });
AppModule.ɵinj = i0.ɵɵdefineInjector({ factory: function AppModule_Factory(t) { return new (t || AppModule)(); }, providers: [], imports: [[
BrowserModule,
AppRoutingModule
]] });
`;

const result = transform(input, getTypeChecker =>
removeIvyJitSupportCalls(true, true, getTypeChecker),
);

expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});

it('should allow removing neither set class metadata nor ng module scope', () => {
const result = transform(input, getTypeChecker =>
removeIvyJitSupportCalls(false, false, getTypeChecker),
);

expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`);
});

it('should strip unused imports when removing set class metadata and ng module scope', () => {
const imports = tags.stripIndent`
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ExampleComponent } from './example/example.component';
import * as i0 from "@angular/core";
`;

const output = tags.stripIndent`
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import * as i0 from "@angular/core";
export class AppModule {
}
AppModule.ɵmod = i0.ɵɵdefineNgModule({ type: AppModule, bootstrap: [AppComponent] });
AppModule.ɵinj = i0.ɵɵdefineInjector({ factory: function AppModule_Factory(t) { return new (t || AppModule)(); }, providers: [], imports: [[
BrowserModule,
AppRoutingModule
]] });
`;

const result = transform(imports + input, getTypeChecker =>
removeIvyJitSupportCalls(true, true, getTypeChecker),
);

expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
});

});
});
7 changes: 7 additions & 0 deletions tests/legacy-cli/e2e/tests/build/prod-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getGlobalVariable } from '../../utils/env';
import { expectFileToExist, expectFileToMatch, readFile } from '../../utils/fs';
import { expectGitToBeClean } from '../../utils/git';
import { ng } from '../../utils/process';
import { expectToFail } from '../../utils/utils';


function verifySize(bundle: string, baselineBytes: number) {
Expand Down Expand Up @@ -51,6 +52,12 @@ export default async function () {
// Content checks
await expectFileToMatch(`dist/test-project/${mainES5Path}`, bootstrapRegExp);
await expectFileToMatch(`dist/test-project/${mainES2015Path}`, bootstrapRegExp);
await expectToFail(() =>
expectFileToMatch(`dist/test-project/${mainES5Path}`, 'setNgModuleScope'),
);
await expectToFail(() =>
expectFileToMatch(`dist/test-project/${mainES5Path}`, 'setClassMetadata'),
);

// Size checks in bytes
if (veProject) {
Expand Down