Skip to content

fix(@angular-devkit/build-optimizer): wrap es2015 class expressions for better tree-shaking #14585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 6, 2019
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 @@ -11,7 +11,6 @@ import {
TransformJavascriptOutput,
transformJavascript,
} from '../helpers/transform-javascript';
import { getFoldFileTransformer } from '../transforms/class-fold';
import { getImportTslibTransformer, testImportTslib } from '../transforms/import-tslib';
import { getPrefixClassesTransformer, testPrefixClasses } from '../transforms/prefix-classes';
import { getPrefixFunctionsTransformer } from '../transforms/prefix-functions';
Expand Down Expand Up @@ -124,14 +123,12 @@ export function buildOptimizer(options: BuildOptimizerOptions): TransformJavascr
// getPrefixFunctionsTransformer needs to be before getFoldFileTransformer.
getPrefixFunctionsTransformer,
selectedGetScrubFileTransformer,
getFoldFileTransformer,
);
typeCheck = true;
} else if (testScrubFile(content)) {
// Always test as these require the type checker
getTransforms.push(
selectedGetScrubFileTransformer,
getFoldFileTransformer,
);
typeCheck = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ import { buildOptimizer } from './build-optimizer';
describe('build-optimizer', () => {
const imports = 'import { Injectable, Input, Component } from \'@angular/core\';';
const clazz = 'var Clazz = (function () { function Clazz() { } return Clazz; }());';
const staticProperty = 'Clazz.prop = 1;';
const decorators = 'Clazz.decorators = [ { type: Injectable } ];';

describe('basic functionality', () => {
it('applies class-fold, scrub-file and prefix-functions to side-effect free modules', () => {
it('applies scrub-file and prefix-functions to side-effect free modules', () => {
const input = tags.stripIndent`
${imports}
var __extends = (this && this.__extends) || function (d, b) {
Expand All @@ -34,7 +33,6 @@ describe('build-optimizer', () => {
ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
})(ChangeDetectionStrategy || (ChangeDetectionStrategy = {}));
${clazz}
${staticProperty}
${decorators}
Clazz.propDecorators = { 'ngIf': [{ type: Input }] };
Clazz.ctorParameters = function () { return [{type: Injectable}]; };
Expand Down Expand Up @@ -65,7 +63,7 @@ describe('build-optimizer', () => {
ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
return ChangeDetectionStrategy;
})({});
var Clazz = /*@__PURE__*/ (function () { function Clazz() { } ${staticProperty} return Clazz; }());
var Clazz = /*@__PURE__*/ (function () { function Clazz() { } return Clazz; }());
var ComponentClazz = /*@__PURE__*/ (function () {
function ComponentClazz() { }
return ComponentClazz;
Expand Down Expand Up @@ -202,7 +200,6 @@ describe('build-optimizer', () => {
xit('doesn\'t produce sourcemaps when emitting was skipped', () => {
const ignoredInput = tags.oneLine`
var Clazz = (function () { function Clazz() { } return Clazz; }());
${staticProperty}
`;
const invalidInput = tags.oneLine`
))))invalid syntax
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface StatementData {
hostClass: ClassData;
}

/** @deprecated Since version 8 */
export function getFoldFileTransformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
const checker = program.getTypeChecker();

Expand Down
219 changes: 182 additions & 37 deletions packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ function visitBlockStatements(
};

// 'oIndex' is the original statement index; 'uIndex' is the updated statement index
for (let oIndex = 0, uIndex = 0; oIndex < statements.length; oIndex++, uIndex++) {
for (let oIndex = 0, uIndex = 0; oIndex < statements.length - 1; oIndex++, uIndex++) {
const currentStatement = statements[oIndex];
let newStatement: ts.Statement | undefined;
let oldStatementsLength = 0;

// these can't contain an enum declaration
if (currentStatement.kind === ts.SyntaxKind.ImportDeclaration) {
Expand All @@ -74,53 +76,52 @@ function visitBlockStatements(
// * be a variable statement
// * have only one declaration
// * have an identifer as a declaration name
if (oIndex < statements.length - 1
&& ts.isVariableStatement(currentStatement)
&& currentStatement.declarationList.declarations.length === 1) {

// ClassExpression declarations must:
// * not be last statement
// * be a variable statement
// * have only one declaration
// * have an ClassExpression or BinaryExpression and a right
// of kind ClassExpression as a initializer
if (ts.isVariableStatement(currentStatement)
&& currentStatement.declarationList.declarations.length === 1) {

const variableDeclaration = currentStatement.declarationList.declarations[0];
const initializer = variableDeclaration.initializer;
if (ts.isIdentifier(variableDeclaration.name)) {
const name = variableDeclaration.name.text;

if (!variableDeclaration.initializer) {
if (!initializer) {
const iife = findTs2_3EnumIife(name, statements[oIndex + 1]);
if (iife) {
// found an enum
if (!updatedStatements) {
updatedStatements = statements.slice();
}
// update IIFE and replace variable statement and old IIFE
updatedStatements.splice(uIndex, 2, updateEnumIife(
oldStatementsLength = 2;
newStatement = updateEnumIife(
currentStatement,
iife[0],
iife[1],
));
);
// skip IIFE statement
oIndex++;
continue;
}
} else if (ts.isObjectLiteralExpression(variableDeclaration.initializer)
&& variableDeclaration.initializer.properties.length === 0) {
} else if (ts.isObjectLiteralExpression(initializer)
&& initializer.properties.length === 0) {
const enumStatements = findTs2_2EnumStatements(name, statements, oIndex + 1);
if (enumStatements.length > 0) {
// found an enum
if (!updatedStatements) {
updatedStatements = statements.slice();
}
// create wrapper and replace variable statement and enum member statements
updatedStatements.splice(uIndex, enumStatements.length + 1, createWrappedEnum(
oldStatementsLength = enumStatements.length + 1;
newStatement = createWrappedEnum(
name,
currentStatement,
enumStatements,
variableDeclaration.initializer,
));
initializer,
);
// skip enum member declarations
oIndex += enumStatements.length;
continue;
}
} else if (ts.isObjectLiteralExpression(variableDeclaration.initializer)
&& variableDeclaration.initializer.properties.length !== 0) {
const literalPropertyCount = variableDeclaration.initializer.properties.length;
} else if (ts.isObjectLiteralExpression(initializer)
&& initializer.properties.length !== 0) {
const literalPropertyCount = initializer.properties.length;

// tsickle es2015 enums first statement is an export declaration
const isPotentialEnumExport = ts.isExportDeclaration(statements[oIndex + 1]);
Expand All @@ -131,25 +132,61 @@ function visitBlockStatements(

const enumStatements = findEnumNameStatements(name, statements, oIndex + 1);
if (enumStatements.length === literalPropertyCount) {
// found an enum
if (!updatedStatements) {
updatedStatements = statements.slice();
}
// create wrapper and replace variable statement and enum member statements
const deleteCount = enumStatements.length + (isPotentialEnumExport ? 2 : 1);
updatedStatements.splice(uIndex, deleteCount, createWrappedEnum(
oldStatementsLength = enumStatements.length + (isPotentialEnumExport ? 2 : 1);
newStatement = createWrappedEnum(
name,
currentStatement,
enumStatements,
variableDeclaration.initializer,
initializer,
isPotentialEnumExport,
));
);
// skip enum member declarations
oIndex += enumStatements.length;
}
} else if (
ts.isClassExpression(initializer)
|| (
ts.isBinaryExpression(initializer)
&& ts.isClassExpression(initializer.right)
)
) {
const classStatements = findClassStatements(name, statements, oIndex);
if (!classStatements) {
continue;
}

oldStatementsLength = classStatements.length;
newStatement = createWrappedClass(
variableDeclaration,
classStatements,
);

oIndex += classStatements.length - 1;
}
}
} else if (ts.isClassDeclaration(currentStatement)) {
const name = (currentStatement.name as ts.Identifier).text;
const classStatements = findClassStatements(name, statements, oIndex);
if (!classStatements) {
continue;
}

oldStatementsLength = classStatements.length;
newStatement = createWrappedClass(
currentStatement,
classStatements,
);

oIndex += classStatements.length - 1;
}

if (newStatement) {
if (!updatedStatements) {
updatedStatements = [...statements];
}

updatedStatements.splice(uIndex, oldStatementsLength, newStatement);
}

const result = ts.visitNode(currentStatement, visitor);
Expand Down Expand Up @@ -389,7 +426,6 @@ function updateHostNode(
hostNode: ts.VariableStatement,
expression: ts.Expression,
): ts.Statement {

// Update existing host node with the pure comment before the variable declaration initializer.
const variableDeclaration = hostNode.declarationList.declarations[0];
const outerVarStmt = ts.updateVariableStatement(
Expand All @@ -411,6 +447,81 @@ function updateHostNode(
return outerVarStmt;
}

/**
* Find class expression or declaration statements.
*
* The classExpressions block to wrap in an iife must
* - end with an ExpressionStatement
* - it's expression must be a BinaryExpression
* - have the same name
*
* ```
let Foo = class Foo {};
Foo = __decorate([]);
```
*/
function findClassStatements(
name: string,
statements: ts.NodeArray<ts.Statement>,
statementIndex: number,
): ts.Statement[] | undefined {
let count = 1;

for (let index = statementIndex + 1; index < statements.length; ++index) {
const statement = statements[index];
if (!ts.isExpressionStatement(statement)) {
break;
}

const expression = statement.expression;

if (ts.isCallExpression(expression)) {
// Ex:
// setClassMetadata(FooClass, [{}], void 0);
// __decorate([propDecorator()], FooClass.prototype, "propertyName", void 0);
// __decorate([propDecorator()], FooClass, "propertyName", void 0);
// __decorate$1([propDecorator()], FooClass, "propertyName", void 0);
const args = expression.arguments;

if (args.length > 2) {
const isReferenced = args.some(arg => {
const potentialIdentifier = ts.isPropertyAccessExpression(arg) ? arg.expression : arg;

return ts.isIdentifier(potentialIdentifier) && potentialIdentifier.text === name;
});

if (isReferenced) {
count++;
continue;
}
}
} else if (ts.isBinaryExpression(expression)) {
const node = ts.isBinaryExpression(expression.left)
? expression.left.left
: expression.left;

const leftExpression = ts.isPropertyAccessExpression(node)
// Static Properties // Ex: Foo.bar = 'value';
? node.expression
// Ex: FooClass = __decorate([Component()], FooClass);
: node;

if (ts.isIdentifier(leftExpression) && leftExpression.text === name) {
count++;
continue;
}
}

break;
}

if (count > 1) {
return statements.slice(statementIndex, statementIndex + count);
}

return undefined;
}

function updateEnumIife(
hostNode: ts.VariableStatement,
iife: ts.CallExpression,
Expand Down Expand Up @@ -474,11 +585,9 @@ function createWrappedEnum(
name: string,
hostNode: ts.VariableStatement,
statements: Array<ts.Statement>,
literalInitializer: ts.ObjectLiteralExpression | undefined,
literalInitializer: ts.ObjectLiteralExpression = ts.createObjectLiteral(),
addExportModifier = false,
): ts.Statement {
literalInitializer = literalInitializer || ts.createObjectLiteral();

const node = addExportModifier
? ts.updateVariableStatement(
hostNode,
Expand All @@ -504,3 +613,39 @@ function createWrappedEnum(

return updateHostNode(node, addPureComment(ts.createParen(iife)));
}

function createWrappedClass(
hostNode: ts.ClassDeclaration | ts.VariableDeclaration,
statements: ts.Statement[],
): ts.Statement {
const name = (hostNode.name as ts.Identifier).text;

const updatedStatements = [...statements];

if (ts.isClassDeclaration(hostNode)) {
updatedStatements[0] = ts.createClassDeclaration(
hostNode.decorators,
undefined,
hostNode.name,
hostNode.typeParameters,
hostNode.heritageClauses,
hostNode.members,
);
}

const pureIife = addPureComment(
ts.createImmediatelyInvokedArrowFunction([
...updatedStatements,
ts.createReturn(ts.createIdentifier(name)),
]),
);

return ts.createVariableStatement(
hostNode.modifiers,
ts.createVariableDeclarationList([
ts.createVariableDeclaration(name, undefined, pureIife),
],
ts.NodeFlags.Const,
),
);
}
Loading