Skip to content

Commit

Permalink
fix(@angular-devkit/build-optimizer): wrap es2015 class expressions f…
Browse files Browse the repository at this point in the history
…or better tree-shaking

ClassExpressions such as the below are not treeshakable unless we wrap them in an IIFE

```js
let AggregateColumnDirective = class AggregateColumnDirective {
	constructor(viewContainerRef) { }
};
AggregateColumnDirective = __decorate([
	Directive({}),
	__metadata("design:paramtypes", [ViewContainerRef])
], AggregateColumnDirective);
```

With this change we wrap the above in an IIFE and mark it as a PURE function.
```js
const AggregateColumnDirective = /*@__PURE__*/ (() => {
	let AggregateColumnDirective = class AggregateColumnDirective {
		constructor(viewContainerRef) { }
	};
	AggregateColumnDirective = __decorate([
		Directive({}),
		__metadata("design:paramtypes", [ViewContainerRef])
	], AggregateColumnDirective);

	return AggregateColumnDirective;
})();
```

With this pattern if the class is unused it will be dropped.

Note: In future we should rename `wrap-enums` to something more generic, and combine class-fold with this transformer especially considering the future fix that needs to be done for #14610

Fixes #14577
  • Loading branch information
alan-agius4 authored and alexeagle committed Jun 6, 2019
1 parent 7371032 commit b6b74d2
Show file tree
Hide file tree
Showing 2 changed files with 480 additions and 255 deletions.
104 changes: 96 additions & 8 deletions packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ 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];

// these can't contain an enum declaration
Expand All @@ -74,9 +74,14 @@ 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 as a initializer
if (ts.isVariableStatement(currentStatement)
&& currentStatement.declarationList.declarations.length === 1) {

const variableDeclaration = currentStatement.declarationList.declarations[0];
if (ts.isIdentifier(variableDeclaration.name)) {
Expand Down Expand Up @@ -148,6 +153,23 @@ function visitBlockStatements(
oIndex += enumStatements.length;
continue;
}
} else if (ts.isClassExpression(variableDeclaration.initializer)) {
const classStatements = findClassExpressionStatements(name, statements, oIndex);
if (!classStatements) {
continue;
}

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

updatedStatements.splice(uIndex, classStatements.length, createWrappedClass(
name,
classStatements,
));

oIndex += classStatements.length - 1;
continue;
}
}
}
Expand Down Expand Up @@ -389,7 +411,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 +432,54 @@ function updateHostNode(
return outerVarStmt;
}

/**
* Find class expression 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 findClassExpressionStatements(
name: string,
statements: ts.NodeArray<ts.Statement>,
statementIndex: number,
): ts.Statement[] | undefined {
let index = statementIndex + 1;
let statement = statements[index];

while (ts.isExpressionStatement(statement)) {
const expression = statement.expression;
if (ts.isCallExpression(expression)) {
// Ex:
// __decorate([propDecorator()], FooClass, "propertyName", void 0);
// __decorate$1([propDecorator()], FooClass, "propertyName", void 0);
const callExpression = expression.expression;
if (!ts.isIdentifier(callExpression) || !/^__decorate(\$\d+)?$/.test(callExpression.text)) {
break;
}
}

if (
ts.isBinaryExpression(expression)
&& ts.isIdentifier(expression.left)
&& expression.left.getText() === name
) {
// Ex: FooClass = __decorate([Component()], FooClass);
return statements.slice(statementIndex, index + 1);
}

statement = statements[++index];
}

return undefined;
}

function updateEnumIife(
hostNode: ts.VariableStatement,
iife: ts.CallExpression,
Expand Down Expand Up @@ -474,11 +543,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 +571,24 @@ function createWrappedEnum(

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

function createWrappedClass(
name: string,
statements: ts.Statement[],
): ts.Statement {
const pureIife = addPureComment(
ts.createImmediatelyInvokedArrowFunction([
...statements,
ts.createReturn(ts.createIdentifier(name)),
]),
);

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

0 comments on commit b6b74d2

Please sign in to comment.