From 0873e7585780a64fe615899580846d75fe381a7e Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Sat, 1 Jun 2019 17:57:30 +0200 Subject: [PATCH 1/3] fix(@angular-devkit/build-optimizer): wrap es2015 class expressions for 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 https://github.com/angular/angular-cli/issues/14610 Fixes #14577 --- .../src/transforms/wrap-enums.ts | 104 ++- .../src/transforms/wrap-enums_spec.ts | 631 +++++++++++------- 2 files changed, 480 insertions(+), 255 deletions(-) diff --git a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts index 8b2341cdd00f..5cd0fcdfbf48 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts @@ -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 @@ -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)) { @@ -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; } } } @@ -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( @@ -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, + 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, @@ -474,11 +543,9 @@ function createWrappedEnum( name: string, hostNode: ts.VariableStatement, statements: Array, - literalInitializer: ts.ObjectLiteralExpression | undefined, + literalInitializer: ts.ObjectLiteralExpression = ts.createObjectLiteral(), addExportModifier = false, ): ts.Statement { - literalInitializer = literalInitializer || ts.createObjectLiteral(); - const node = addExportModifier ? ts.updateVariableStatement( hostNode, @@ -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, + ), + ); +} diff --git a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts index 8c72b04ead37..31acd5bb0e5c 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts @@ -15,200 +15,324 @@ const transform = (content: string) => transformJavascript( { content, getTransforms: [getWrapEnumsTransformer] }).content; // tslint:disable-next-line:no-big-function -describe('wrap-enums', () => { - it('wraps ts 2.2 enums in IIFE', () => { - const input = tags.stripIndent` - export var ChangeDetectionStrategy = {}; - ChangeDetectionStrategy.OnPush = 0; - ChangeDetectionStrategy.Default = 1; - ChangeDetectionStrategy[ChangeDetectionStrategy.OnPush] = "OnPush"; - ChangeDetectionStrategy[ChangeDetectionStrategy.Default] = "Default"; - `; - const output = tags.stripIndent` - export var ChangeDetectionStrategy = /*@__PURE__*/ (function () { - var ChangeDetectionStrategy = {}; +describe('wrap enums and classes transformer', () => { + describe('wrap class expressions', () => { + it('should wrap ClassExpression without property decorators in IIFE', () => { + const input = tags.stripIndent` + let AggregateColumnDirective = class AggregateColumnDirective { + constructor(viewContainerRef) { } + }; + AggregateColumnDirective = __decorate([ + Directive({}), + __metadata("design:paramtypes", [ViewContainerRef]) + ], AggregateColumnDirective); + `; + + const output = tags.stripIndent` + const AggregateColumnDirective = /*@__PURE__*/ (() => { + ${input} + + return AggregateColumnDirective; + })(); + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('should wrap ClassExpression with property decorators in IIFE', () => { + const input = tags.stripIndent` + let ChipList = class ChipList extends Component { + constructor(options, element) { + super(options, element); + } + }; + __decorate$4([Property([])], ChipList.prototype, "chips", void 0); + ChipList = __decorate$4([NotifyPropertyChanges], ChipList); + `; + + const output = tags.stripIndent` + const ChipList = /*@__PURE__*/ (() => { + ${input} + return ChipList; + })();`; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not wrap ClassExpression without decorators', () => { + const input = tags.stripIndent` + let ChipList = class ChipList extends Component { + constructor(options, element) { + super(options, element); + } + }; + fooBar(); + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${input}`); + }); + + it('should wrap ClassExpression with property decorators and static property in IIFE', () => { + const input = tags.stripIndent` + let ChipList = class ChipList extends Component { + constructor(options, element) { + super(options, element); + } + }; + ChipList.prop = 1; + __decorate$4([Property([])], ChipList.prototype, "chips", void 0); + ChipList = __decorate$4([NotifyPropertyChanges], ChipList);`; + + const output = tags.stripIndent` + const ChipList = /*@__PURE__*/ (() => { + ${input} + return ChipList; + })();`; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('should wrap multiple ClassExpression in IIFE', () => { + const firstClass = ` + let AggregateColumnDirective = class AggregateColumnDirective { + constructor(viewContainerRef) { } + }; + AggregateColumnDirective = __decorate([ + Directive({}), + __metadata("design:paramtypes", [ViewContainerRef]) + ], AggregateColumnDirective); + `; + + const secondClass = ` + let ChipList = class ChipList extends Component { + constructor(options, element) { + super(options, element); + } + }; + __decorate$4([Property([])], ChipList.prototype, "chips", void 0); + ChipList = __decorate$4([NotifyPropertyChanges], ChipList); + `; + + const input = tags.stripIndent` + const minutesMilliSeconds = 60000; + + ${firstClass} + + const CSS = 'e-css'; + const PRIMARY = 'e-primary'; + + ${secondClass} + + const chipList = new ChipList({}, {}); + `; + + const output = tags.stripIndent` + const minutesMilliSeconds = 60000; + + const AggregateColumnDirective = /*@__PURE__*/ (() => { + ${firstClass} + + return AggregateColumnDirective; + })(); + + const CSS = 'e-css'; + const PRIMARY = 'e-primary'; + + const ChipList = /*@__PURE__*/ (() => { + ${secondClass} + + return ChipList; + })(); + + const chipList = new ChipList({}, {}); + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + }); + + // tslint:disable-next-line:no-big-function + describe('wrap enums', () => { + it('wraps ts 2.2 enums in IIFE', () => { + const input = tags.stripIndent` + export var ChangeDetectionStrategy = {}; ChangeDetectionStrategy.OnPush = 0; ChangeDetectionStrategy.Default = 1; ChangeDetectionStrategy[ChangeDetectionStrategy.OnPush] = "OnPush"; ChangeDetectionStrategy[ChangeDetectionStrategy.Default] = "Default"; - return ChangeDetectionStrategy; - }()); - `; + `; + const output = tags.stripIndent` + export var ChangeDetectionStrategy = /*@__PURE__*/ (function () { + var ChangeDetectionStrategy = {}; + ChangeDetectionStrategy.OnPush = 0; + ChangeDetectionStrategy.Default = 1; + ChangeDetectionStrategy[ChangeDetectionStrategy.OnPush] = "OnPush"; + ChangeDetectionStrategy[ChangeDetectionStrategy.Default] = "Default"; + return ChangeDetectionStrategy; + }()); + `; - expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); - }); + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); - it('wraps ES2015 tsickle enums in IIFE', () => { - const input = tags.stripIndent` - const ChangeDetectionStrategy = { - OnPush: 0, - Default: 1, - }; - export { ChangeDetectionStrategy }; - ChangeDetectionStrategy[ChangeDetectionStrategy.OnPush] = 'OnPush'; - ChangeDetectionStrategy[ChangeDetectionStrategy.Default] = 'Default'; - `; - - const output = tags.stripIndent` - export const ChangeDetectionStrategy = /*@__PURE__*/ (function () { - var ChangeDetectionStrategy = { OnPush: 0, Default: 1, }; - - ChangeDetectionStrategy[ChangeDetectionStrategy.OnPush] = 'OnPush'; - ChangeDetectionStrategy[ChangeDetectionStrategy.Default] = 'Default'; - return ChangeDetectionStrategy; - }()); - `; + it('wraps ES2015 tsickle enums in IIFE', () => { + const input = tags.stripIndent` + const ChangeDetectionStrategy = { + OnPush: 0, + Default: 1, + }; + export { ChangeDetectionStrategy }; + ChangeDetectionStrategy[ChangeDetectionStrategy.OnPush] = 'OnPush'; + ChangeDetectionStrategy[ChangeDetectionStrategy.Default] = 'Default'; + `; - expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); - }); + const output = tags.stripIndent` + export const ChangeDetectionStrategy = /*@__PURE__*/ (function () { + var ChangeDetectionStrategy = { OnPush: 0, Default: 1, }; - it('wraps only ES2015 tsickle enums in IIFE', () => { - const input = tags.stripIndent` - const RendererStyleFlags3 = { - Important: 1, - DashCase: 2, - }; - export { RendererStyleFlags3 }; - RendererStyleFlags3[RendererStyleFlags3.Important] = 'Important'; - RendererStyleFlags3[RendererStyleFlags3.DashCase] = 'DashCase'; - - export const domRendererFactory3 = { - createRenderer: (hostElement, rendererType) => { return document; } - }; + ChangeDetectionStrategy[ChangeDetectionStrategy.OnPush] = 'OnPush'; + ChangeDetectionStrategy[ChangeDetectionStrategy.Default] = 'Default'; + return ChangeDetectionStrategy; + }()); + `; - export const unusedValueExportToPlacateAjd = 1; - `; - const output = tags.stripIndent` - export const RendererStyleFlags3 = /*@__PURE__*/ (function () { - var RendererStyleFlags3 = { Important: 1, DashCase: 2, }; + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('wraps only ES2015 tsickle enums in IIFE', () => { + const input = tags.stripIndent` + const RendererStyleFlags3 = { + Important: 1, + DashCase: 2, + }; + export { RendererStyleFlags3 }; RendererStyleFlags3[RendererStyleFlags3.Important] = 'Important'; RendererStyleFlags3[RendererStyleFlags3.DashCase] = 'DashCase'; - return RendererStyleFlags3; - }()); - export const domRendererFactory3 = { - createRenderer: (hostElement, rendererType) => { return document; } - }; + export const domRendererFactory3 = { + createRenderer: (hostElement, rendererType) => { return document; } + }; - export const unusedValueExportToPlacateAjd = 1; - `; + export const unusedValueExportToPlacateAjd = 1; + `; + const output = tags.stripIndent` + export const RendererStyleFlags3 = /*@__PURE__*/ (function () { + var RendererStyleFlags3 = { Important: 1, DashCase: 2, }; + RendererStyleFlags3[RendererStyleFlags3.Important] = 'Important'; + RendererStyleFlags3[RendererStyleFlags3.DashCase] = 'DashCase'; + return RendererStyleFlags3; + }()); - expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); - }); + export const domRendererFactory3 = { + createRenderer: (hostElement, rendererType) => { return document; } + }; + + export const unusedValueExportToPlacateAjd = 1; + `; - it('wraps ts >2.3 enums in IIFE', () => { - const input = tags.stripIndent` - export var ChangeDetectionStrategy; - (function (ChangeDetectionStrategy) { + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('wraps ts >2.3 enums in IIFE', () => { + const input = tags.stripIndent` + export var ChangeDetectionStrategy; + (function (ChangeDetectionStrategy) { + ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush"; + ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default"; + })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {})); + `; + const output = tags.stripIndent` + export var ChangeDetectionStrategy = /*@__PURE__*/ (function (ChangeDetectionStrategy) { ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush"; ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default"; - })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {})); - `; - const output = tags.stripIndent` - export var ChangeDetectionStrategy = /*@__PURE__*/ (function (ChangeDetectionStrategy) { - ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush"; - ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default"; - return ChangeDetectionStrategy; - })({}); - `; - - expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); - }); + return ChangeDetectionStrategy; + })({}); + `; - it('wraps ts >2.3 enums in IIFE, even if they have funny numbers', () => { - const input = tags.stripIndent` - export var AnimatorControlState; - (function (AnimatorControlState) { - AnimatorControlState[AnimatorControlState["INITIALIZED"] = 1] = "INITIALIZED"; - AnimatorControlState[AnimatorControlState["STARTED"] = 2] = "STARTED"; - AnimatorControlState[AnimatorControlState["FINISHED"] = 3] = "FINISHED"; - AnimatorControlState[AnimatorControlState["DESTROYED"] = 4] = "DESTROYED"; - })(AnimatorControlState || (AnimatorControlState = {})); - `; - const output = tags.stripIndent` - export var AnimatorControlState = /*@__PURE__*/ (function (AnimatorControlState) { - AnimatorControlState[AnimatorControlState["INITIALIZED"] = 1] = "INITIALIZED"; - AnimatorControlState[AnimatorControlState["STARTED"] = 2] = "STARTED"; - AnimatorControlState[AnimatorControlState["FINISHED"] = 3] = "FINISHED"; - AnimatorControlState[AnimatorControlState["DESTROYED"] = 4] = "DESTROYED"; - return AnimatorControlState; - })({}); - `; - - expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); - }); + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); - it('wraps ts >2.3 enums in IIFE, even if they were renamed due to scope hoisting', () => { - const input = tags.stripIndent` - var TokenType$1; - (function (TokenType) { - TokenType[TokenType["TAG_OPEN_START"] = 0] = "TAG_OPEN_START"; - TokenType[TokenType["TAG_OPEN_END"] = 1] = "TAG_OPEN_END"; - TokenType[TokenType["TAG_OPEN_END_VOID"] = 2] = "TAG_OPEN_END_VOID"; - TokenType[TokenType["TAG_CLOSE"] = 3] = "TAG_CLOSE"; - TokenType[TokenType["TEXT"] = 4] = "TEXT"; - TokenType[TokenType["ESCAPABLE_RAW_TEXT"] = 5] = "ESCAPABLE_RAW_TEXT"; - TokenType[TokenType["RAW_TEXT"] = 6] = "RAW_TEXT"; - TokenType[TokenType["COMMENT_START"] = 7] = "COMMENT_START"; - TokenType[TokenType["COMMENT_END"] = 8] = "COMMENT_END"; - TokenType[TokenType["CDATA_START"] = 9] = "CDATA_START"; - TokenType[TokenType["CDATA_END"] = 10] = "CDATA_END"; - TokenType[TokenType["ATTR_NAME"] = 11] = "ATTR_NAME"; - TokenType[TokenType["ATTR_VALUE"] = 12] = "ATTR_VALUE"; - TokenType[TokenType["DOC_TYPE"] = 13] = "DOC_TYPE"; - TokenType[TokenType["EXPANSION_FORM_START"] = 14] = "EXPANSION_FORM_START"; - TokenType[TokenType["EXPANSION_CASE_VALUE"] = 15] = "EXPANSION_CASE_VALUE"; - TokenType[TokenType["EXPANSION_CASE_EXP_START"] = 16] = "EXPANSION_CASE_EXP_START"; - TokenType[TokenType["EXPANSION_CASE_EXP_END"] = 17] = "EXPANSION_CASE_EXP_END"; - TokenType[TokenType["EXPANSION_FORM_END"] = 18] = "EXPANSION_FORM_END"; - TokenType[TokenType["EOF"] = 19] = "EOF"; - })(TokenType$1 || (TokenType$1 = {})); - `; - const output = tags.stripIndent` - var TokenType$1 = /*@__PURE__*/ (function (TokenType) { - TokenType[TokenType["TAG_OPEN_START"] = 0] = "TAG_OPEN_START"; - TokenType[TokenType["TAG_OPEN_END"] = 1] = "TAG_OPEN_END"; - TokenType[TokenType["TAG_OPEN_END_VOID"] = 2] = "TAG_OPEN_END_VOID"; - TokenType[TokenType["TAG_CLOSE"] = 3] = "TAG_CLOSE"; - TokenType[TokenType["TEXT"] = 4] = "TEXT"; - TokenType[TokenType["ESCAPABLE_RAW_TEXT"] = 5] = "ESCAPABLE_RAW_TEXT"; - TokenType[TokenType["RAW_TEXT"] = 6] = "RAW_TEXT"; - TokenType[TokenType["COMMENT_START"] = 7] = "COMMENT_START"; - TokenType[TokenType["COMMENT_END"] = 8] = "COMMENT_END"; - TokenType[TokenType["CDATA_START"] = 9] = "CDATA_START"; - TokenType[TokenType["CDATA_END"] = 10] = "CDATA_END"; - TokenType[TokenType["ATTR_NAME"] = 11] = "ATTR_NAME"; - TokenType[TokenType["ATTR_VALUE"] = 12] = "ATTR_VALUE"; - TokenType[TokenType["DOC_TYPE"] = 13] = "DOC_TYPE"; - TokenType[TokenType["EXPANSION_FORM_START"] = 14] = "EXPANSION_FORM_START"; - TokenType[TokenType["EXPANSION_CASE_VALUE"] = 15] = "EXPANSION_CASE_VALUE"; - TokenType[TokenType["EXPANSION_CASE_EXP_START"] = 16] = "EXPANSION_CASE_EXP_START"; - TokenType[TokenType["EXPANSION_CASE_EXP_END"] = 17] = "EXPANSION_CASE_EXP_END"; - TokenType[TokenType["EXPANSION_FORM_END"] = 18] = "EXPANSION_FORM_END"; - TokenType[TokenType["EOF"] = 19] = "EOF"; - return TokenType; - })({}); - `; - - expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); - }); + it('wraps ts >2.3 enums in IIFE, even if they have funny numbers', () => { + const input = tags.stripIndent` + export var AnimatorControlState; + (function (AnimatorControlState) { + AnimatorControlState[AnimatorControlState["INITIALIZED"] = 1] = "INITIALIZED"; + AnimatorControlState[AnimatorControlState["STARTED"] = 2] = "STARTED"; + AnimatorControlState[AnimatorControlState["FINISHED"] = 3] = "FINISHED"; + AnimatorControlState[AnimatorControlState["DESTROYED"] = 4] = "DESTROYED"; + })(AnimatorControlState || (AnimatorControlState = {})); + `; + const output = tags.stripIndent` + export var AnimatorControlState = /*@__PURE__*/ (function (AnimatorControlState) { + AnimatorControlState[AnimatorControlState["INITIALIZED"] = 1] = "INITIALIZED"; + AnimatorControlState[AnimatorControlState["STARTED"] = 2] = "STARTED"; + AnimatorControlState[AnimatorControlState["FINISHED"] = 3] = "FINISHED"; + AnimatorControlState[AnimatorControlState["DESTROYED"] = 4] = "DESTROYED"; + return AnimatorControlState; + })({}); + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('wraps ts >2.3 enums in IIFE, even if they were renamed due to scope hoisting', () => { + const input = tags.stripIndent` + var TokenType$1; + (function (TokenType) { + TokenType[TokenType["TAG_OPEN_START"] = 0] = "TAG_OPEN_START"; + TokenType[TokenType["TAG_OPEN_END"] = 1] = "TAG_OPEN_END"; + TokenType[TokenType["TAG_OPEN_END_VOID"] = 2] = "TAG_OPEN_END_VOID"; + TokenType[TokenType["TAG_CLOSE"] = 3] = "TAG_CLOSE"; + TokenType[TokenType["TEXT"] = 4] = "TEXT"; + TokenType[TokenType["ESCAPABLE_RAW_TEXT"] = 5] = "ESCAPABLE_RAW_TEXT"; + TokenType[TokenType["RAW_TEXT"] = 6] = "RAW_TEXT"; + TokenType[TokenType["COMMENT_START"] = 7] = "COMMENT_START"; + TokenType[TokenType["COMMENT_END"] = 8] = "COMMENT_END"; + TokenType[TokenType["CDATA_START"] = 9] = "CDATA_START"; + TokenType[TokenType["CDATA_END"] = 10] = "CDATA_END"; + TokenType[TokenType["ATTR_NAME"] = 11] = "ATTR_NAME"; + TokenType[TokenType["ATTR_VALUE"] = 12] = "ATTR_VALUE"; + TokenType[TokenType["DOC_TYPE"] = 13] = "DOC_TYPE"; + TokenType[TokenType["EXPANSION_FORM_START"] = 14] = "EXPANSION_FORM_START"; + TokenType[TokenType["EXPANSION_CASE_VALUE"] = 15] = "EXPANSION_CASE_VALUE"; + TokenType[TokenType["EXPANSION_CASE_EXP_START"] = 16] = "EXPANSION_CASE_EXP_START"; + TokenType[TokenType["EXPANSION_CASE_EXP_END"] = 17] = "EXPANSION_CASE_EXP_END"; + TokenType[TokenType["EXPANSION_FORM_END"] = 18] = "EXPANSION_FORM_END"; + TokenType[TokenType["EOF"] = 19] = "EOF"; + })(TokenType$1 || (TokenType$1 = {})); + `; + const output = tags.stripIndent` + var TokenType$1 = /*@__PURE__*/ (function (TokenType) { + TokenType[TokenType["TAG_OPEN_START"] = 0] = "TAG_OPEN_START"; + TokenType[TokenType["TAG_OPEN_END"] = 1] = "TAG_OPEN_END"; + TokenType[TokenType["TAG_OPEN_END_VOID"] = 2] = "TAG_OPEN_END_VOID"; + TokenType[TokenType["TAG_CLOSE"] = 3] = "TAG_CLOSE"; + TokenType[TokenType["TEXT"] = 4] = "TEXT"; + TokenType[TokenType["ESCAPABLE_RAW_TEXT"] = 5] = "ESCAPABLE_RAW_TEXT"; + TokenType[TokenType["RAW_TEXT"] = 6] = "RAW_TEXT"; + TokenType[TokenType["COMMENT_START"] = 7] = "COMMENT_START"; + TokenType[TokenType["COMMENT_END"] = 8] = "COMMENT_END"; + TokenType[TokenType["CDATA_START"] = 9] = "CDATA_START"; + TokenType[TokenType["CDATA_END"] = 10] = "CDATA_END"; + TokenType[TokenType["ATTR_NAME"] = 11] = "ATTR_NAME"; + TokenType[TokenType["ATTR_VALUE"] = 12] = "ATTR_VALUE"; + TokenType[TokenType["DOC_TYPE"] = 13] = "DOC_TYPE"; + TokenType[TokenType["EXPANSION_FORM_START"] = 14] = "EXPANSION_FORM_START"; + TokenType[TokenType["EXPANSION_CASE_VALUE"] = 15] = "EXPANSION_CASE_VALUE"; + TokenType[TokenType["EXPANSION_CASE_EXP_START"] = 16] = "EXPANSION_CASE_EXP_START"; + TokenType[TokenType["EXPANSION_CASE_EXP_END"] = 17] = "EXPANSION_CASE_EXP_END"; + TokenType[TokenType["EXPANSION_FORM_END"] = 18] = "EXPANSION_FORM_END"; + TokenType[TokenType["EOF"] = 19] = "EOF"; + return TokenType; + })({}); + `; - it('wraps tsickle enums in IIFE', () => { - const input = tags.stripIndent` - /** @enum {number} */ - var FormatWidth = { - Short: 0, - Medium: 1, - Long: 2, - Full: 3, - }; - FormatWidth[FormatWidth.Short] = "Short"; - FormatWidth[FormatWidth.Medium] = "Medium"; - FormatWidth[FormatWidth.Long] = "Long"; - FormatWidth[FormatWidth.Full] = "Full"; - `; - const output = tags.stripIndent` - /** @enum {number} */ var FormatWidth = /*@__PURE__*/ (function () { + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('wraps tsickle enums in IIFE', () => { + const input = tags.stripIndent` + /** @enum {number} */ var FormatWidth = { Short: 0, Medium: 1, @@ -219,81 +343,94 @@ describe('wrap-enums', () => { FormatWidth[FormatWidth.Medium] = "Medium"; FormatWidth[FormatWidth.Long] = "Long"; FormatWidth[FormatWidth.Full] = "Full"; - return FormatWidth; - }()); - `; + `; + const output = tags.stripIndent` + /** @enum {number} */ var FormatWidth = /*@__PURE__*/ (function () { + var FormatWidth = { + Short: 0, + Medium: 1, + Long: 2, + Full: 3, + }; + FormatWidth[FormatWidth.Short] = "Short"; + FormatWidth[FormatWidth.Medium] = "Medium"; + FormatWidth[FormatWidth.Long] = "Long"; + FormatWidth[FormatWidth.Full] = "Full"; + return FormatWidth; + }()); + `; - expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); - }); + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); - it('wraps enums with multi-line comments in IIFE', () => { - const input = tags.stripIndent` - /** - * Supported http methods. - * @deprecated use @angular/common/http instead - */ - var RequestMethod; - /** - * Supported http methods. - * @deprecated use @angular/common/http instead - */ - (function (RequestMethod) { - RequestMethod[RequestMethod["Get"] = 0] = "Get"; - RequestMethod[RequestMethod["Post"] = 1] = "Post"; - RequestMethod[RequestMethod["Put"] = 2] = "Put"; - RequestMethod[RequestMethod["Delete"] = 3] = "Delete"; - RequestMethod[RequestMethod["Options"] = 4] = "Options"; - RequestMethod[RequestMethod["Head"] = 5] = "Head"; - RequestMethod[RequestMethod["Patch"] = 6] = "Patch"; - })(RequestMethod || (RequestMethod = {})); - `; - // We need to interpolate this space because our editorconfig automatically strips - // trailing whitespace. - const space = ' '; - const output = tags.stripIndent` - /** - * Supported http methods. - * @deprecated use @angular/common/http instead - */ - var RequestMethod =${space} - /** - * Supported http methods. - * @deprecated use @angular/common/http instead - */ - /*@__PURE__*/ (function (RequestMethod) { - RequestMethod[RequestMethod["Get"] = 0] = "Get"; - RequestMethod[RequestMethod["Post"] = 1] = "Post"; - RequestMethod[RequestMethod["Put"] = 2] = "Put"; - RequestMethod[RequestMethod["Delete"] = 3] = "Delete"; - RequestMethod[RequestMethod["Options"] = 4] = "Options"; - RequestMethod[RequestMethod["Head"] = 5] = "Head"; - RequestMethod[RequestMethod["Patch"] = 6] = "Patch"; - return RequestMethod; - })({}); - `; - - expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); - }); + it('wraps enums with multi-line comments in IIFE', () => { + const input = tags.stripIndent` + /** + * Supported http methods. + * @deprecated use @angular/common/http instead + */ + var RequestMethod; + /** + * Supported http methods. + * @deprecated use @angular/common/http instead + */ + (function (RequestMethod) { + RequestMethod[RequestMethod["Get"] = 0] = "Get"; + RequestMethod[RequestMethod["Post"] = 1] = "Post"; + RequestMethod[RequestMethod["Put"] = 2] = "Put"; + RequestMethod[RequestMethod["Delete"] = 3] = "Delete"; + RequestMethod[RequestMethod["Options"] = 4] = "Options"; + RequestMethod[RequestMethod["Head"] = 5] = "Head"; + RequestMethod[RequestMethod["Patch"] = 6] = "Patch"; + })(RequestMethod || (RequestMethod = {})); + `; + // We need to interpolate this space because our editorconfig automatically strips + // trailing whitespace. + const space = ' '; + const output = tags.stripIndent` + /** + * Supported http methods. + * @deprecated use @angular/common/http instead + */ + var RequestMethod =${space} + /** + * Supported http methods. + * @deprecated use @angular/common/http instead + */ + /*@__PURE__*/ (function (RequestMethod) { + RequestMethod[RequestMethod["Get"] = 0] = "Get"; + RequestMethod[RequestMethod["Post"] = 1] = "Post"; + RequestMethod[RequestMethod["Put"] = 2] = "Put"; + RequestMethod[RequestMethod["Delete"] = 3] = "Delete"; + RequestMethod[RequestMethod["Options"] = 4] = "Options"; + RequestMethod[RequestMethod["Head"] = 5] = "Head"; + RequestMethod[RequestMethod["Patch"] = 6] = "Patch"; + return RequestMethod; + })({}); + `; - it('wraps exported enums in IIFE', () => { - const input = tags.stripIndent` - var ExportEnum; - (function (ExportEnum) { - ExportEnum[ExportEnum["A"] = 0] = "A"; - ExportEnum[ExportEnum["B"] = 1] = "B"; - ExportEnum[ExportEnum["C"] = 2] = "C"; - })(ExportEnum = exports.ExportEnum || (exports.ExportEnum = {})); - `; - const output = tags.stripIndent` - var ExportEnum = exports.ExportEnum = /*@__PURE__*/ (function (ExportEnum) { - ExportEnum[ExportEnum["A"] = 0] = "A"; - ExportEnum[ExportEnum["B"] = 1] = "B"; - ExportEnum[ExportEnum["C"] = 2] = "C"; - return ExportEnum; - })(exports.ExportEnum || {}); - `; - - expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); - }); + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('wraps exported enums in IIFE', () => { + const input = tags.stripIndent` + var ExportEnum; + (function (ExportEnum) { + ExportEnum[ExportEnum["A"] = 0] = "A"; + ExportEnum[ExportEnum["B"] = 1] = "B"; + ExportEnum[ExportEnum["C"] = 2] = "C"; + })(ExportEnum = exports.ExportEnum || (exports.ExportEnum = {})); + `; + const output = tags.stripIndent` + var ExportEnum = exports.ExportEnum = /*@__PURE__*/ (function (ExportEnum) { + ExportEnum[ExportEnum["A"] = 0] = "A"; + ExportEnum[ExportEnum["B"] = 1] = "B"; + ExportEnum[ExportEnum["C"] = 2] = "C"; + return ExportEnum; + })(exports.ExportEnum || {}); + `; + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + }); }); From 1b0d5b6117e0160a1ca0dbe6154f596284a42498 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 3 Jun 2019 14:53:03 +0200 Subject: [PATCH 2/3] fix(@angular-devkit/build-optimizer): wrap ClassDeclarations in an IIFE for better treeshaking With this change we wrap ClassDeclarations inside an IIFE, also we move some code from the class fold into the wrap-enums. This changes the below code: ```js export class Foo { method() { } } Foo.bar = 'barValue'; __decorate([ methodDecorator ], Foo.prototype, "method", null); ``` to ```js export const Foo = /*@__PURE__*/ (() => { class Foo { method() { } } Foo.bar = 'barValue'; __decorate([ methodDecorator ], Foo.prototype, "method", null); return Foo; })(); ``` Fixes #14610 --- .../src/build-optimizer/build-optimizer.ts | 3 - .../build-optimizer/build-optimizer_spec.ts | 7 +- .../src/transforms/class-fold.ts | 1 + .../src/transforms/wrap-enums.ts | 185 ++++++++++++------ .../src/transforms/wrap-enums_spec.ts | 162 ++++++++++++++- 5 files changed, 282 insertions(+), 76 deletions(-) diff --git a/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts b/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts index 2ca30df2b7af..437229b2af87 100644 --- a/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts +++ b/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts @@ -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'; @@ -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; } diff --git a/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts b/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts index 42768c1b15d7..05a322c6c2a7 100644 --- a/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer_spec.ts @@ -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) { @@ -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}]; }; @@ -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; @@ -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 diff --git a/packages/angular_devkit/build_optimizer/src/transforms/class-fold.ts b/packages/angular_devkit/build_optimizer/src/transforms/class-fold.ts index 206a1b4ab420..634f195c92df 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/class-fold.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/class-fold.ts @@ -20,6 +20,7 @@ interface StatementData { hostClass: ClassData; } +/** @deprecated Since version 8 */ export function getFoldFileTransformer(program: ts.Program): ts.TransformerFactory { const checker = program.getTypeChecker(); diff --git a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts index 5cd0fcdfbf48..f6400a7a4de4 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts @@ -63,6 +63,8 @@ function visitBlockStatements( // 'oIndex' is the original statement index; 'uIndex' is the updated statement index 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) { @@ -79,53 +81,47 @@ function visitBlockStatements( // * not be last statement // * be a variable statement // * have only one declaration - // * have an ClassExpression as a initializer + // * 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]); @@ -136,42 +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; - continue; } - } else if (ts.isClassExpression(variableDeclaration.initializer)) { - const classStatements = findClassExpressionStatements(name, statements, oIndex); + } else if ( + ts.isClassExpression(initializer) + || ( + ts.isBinaryExpression(initializer) + && ts.isClassExpression(initializer.right) + ) + ) { + const classStatements = findClassStatements(name, statements, oIndex); if (!classStatements) { continue; } - if (!updatedStatements) { - updatedStatements = [...statements]; - } - - updatedStatements.splice(uIndex, classStatements.length, createWrappedClass( - name, + oldStatementsLength = classStatements.length; + newStatement = createWrappedClass( + variableDeclaration, classStatements, - )); + ); oIndex += classStatements.length - 1; - continue; } } + } 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); @@ -433,7 +448,7 @@ function updateHostNode( } /** - * Find class expression statements. + * Find class expression or declaration statements. * * The classExpressions block to wrap in an iife must * - end with an ExpressionStatement @@ -445,36 +460,63 @@ function updateHostNode( Foo = __decorate([]); ``` */ -function findClassExpressionStatements( +function findClassStatements( name: string, statements: ts.NodeArray, statementIndex: number, ): ts.Statement[] | undefined { - let index = statementIndex + 1; - let statement = statements[index]; + let count = 1; + + for (let index = statementIndex + 1; index < statements.length; ++index) { + const statement = statements[index]; + if (!ts.isExpressionStatement(statement)) { + break; + } - 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; + // 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; } } - if ( - ts.isBinaryExpression(expression) - && ts.isIdentifier(expression.left) - && expression.left.getText() === name - ) { - // Ex: FooClass = __decorate([Component()], FooClass); - return statements.slice(statementIndex, index + 1); - } + break; + } - statement = statements[++index]; + if (count > 1) { + return statements.slice(statementIndex, statementIndex + count); } return undefined; @@ -573,18 +615,33 @@ function createWrappedEnum( } function createWrappedClass( - name: string, + 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([ - ...statements, + ...updatedStatements, ts.createReturn(ts.createIdentifier(name)), ]), ); return ts.createVariableStatement( - undefined, + hostNode.modifiers, ts.createVariableDeclarationList([ ts.createVariableDeclaration(name, undefined, pureIife), ], diff --git a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts index 31acd5bb0e5c..7a82b4f88642 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts @@ -16,8 +16,129 @@ const transform = (content: string) => transformJavascript( // tslint:disable-next-line:no-big-function describe('wrap enums and classes transformer', () => { + describe('wraps class declarations', () => { + it('should ClassDeclarations that are referenced with in CallExpressions', () => { + const input = tags.stripIndent` + class ApplicationModule { + constructor(appRef) { } + } + ApplicationModule.ngModuleDef = ɵɵdefineNgModule({ type: ApplicationModule }); + /*@__PURE__*/ setClassMetadata(ApplicationModule, [{ + type: NgModule, + args: [{ providers: APPLICATION_MODULE_PROVIDERS }] + }], function () { return [{ type: ApplicationRef }]; }, { constructor: [] }); + ApplicationModule.ctorParameters = () => [ + { type: ApplicationRef } + ]; + `; + + const output = tags.stripIndent` + const ApplicationModule = /*@__PURE__*/ (() => { + ${input} + + return ApplicationModule; + })(); + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('with nested static properties in IIFE', () => { + const input = tags.stripIndent` + class CommonModule { } + CommonModule.ngModuleDef = defineNgModule({ + type: CommonModule + }), CommonModule.ngInjectorDef = defineInjector({ + factory: function (t) { + return new (t || CommonModule)(); + }, + providers: [{ + provide: NgLocalization, + useClass: NgLocaleLocalization + }] + }); + `; + + const output = tags.stripIndent` + const CommonModule = /*@__PURE__*/ (() => { + ${input} + + return CommonModule; + })(); + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('with property decorators in IIFE', () => { + const input = tags.stripIndent` + export class Foo { + method() { + } + } + Foo.bar = 'barValue'; + __decorate([ + methodDecorator + ], Foo.prototype, "method", null); + `; + + const output = tags.stripIndent` + export const Foo = /*@__PURE__*/ (() => { + ${input.replace('export ', '')} + + return Foo; + })(); + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('folds static properties in IIFE', () => { + const input = tags.stripIndent` + export class TemplateRef { } + TemplateRef.__NG_ELEMENT_ID__ = () => SWITCH_TEMPLATE_REF_FACTORY(TemplateRef, ElementRef); + `; + const output = tags.stripIndent` + export const TemplateRef = /*@__PURE__*/ (() => { + class TemplateRef { } + TemplateRef.__NG_ELEMENT_ID__ = () => SWITCH_TEMPLATE_REF_FACTORY(TemplateRef, ElementRef); + return TemplateRef; + })(); + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('folds multiple static properties into class', () => { + const input = tags.stripIndent` + export class TemplateRef { } + TemplateRef.__NG_ELEMENT_ID__ = () => SWITCH_TEMPLATE_REF_FACTORY(TemplateRef, ElementRef); + TemplateRef.somethingElse = true; + `; + const output = tags.stripIndent` + export const TemplateRef = /*@__PURE__*/ (() => { + class TemplateRef { + } + TemplateRef.__NG_ELEMENT_ID__ = () => SWITCH_TEMPLATE_REF_FACTORY(TemplateRef, ElementRef); + TemplateRef.somethingElse = true; + return TemplateRef; + })(); + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it(`doesn't wrap classes without static properties in IIFE`, () => { + const input = tags.stripIndent` + export class TemplateRef { } + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${input}`); + }); + }); + describe('wrap class expressions', () => { - it('should wrap ClassExpression without property decorators in IIFE', () => { + it('without property decorators in IIFE', () => { const input = tags.stripIndent` let AggregateColumnDirective = class AggregateColumnDirective { constructor(viewContainerRef) { } @@ -39,7 +160,40 @@ describe('wrap enums and classes transformer', () => { expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); }); - it('should wrap ClassExpression with property decorators in IIFE', () => { + it('with forwardRef in IIFE', () => { + const classContent = tags.stripIndent` + let FooDirective = FooDirective_1 = class FooDirective { + constructor(parent) { } + }; + FooDirective = FooDirective_1 = __decorate([ + Directive({ + selector: '[libUnshakeable2]', + }), + __param(0, SkipSelf()), __param(0, Inject(forwardRef(() => FooDirective_1))), + __metadata("design:paramtypes", [FooDirective]) + ], FooDirective); + `; + + const input = tags.stripIndent` + var FooDirective_1; + ${classContent} + export { FooDirective }; + `; + + const output = tags.stripIndent` + var FooDirective_1; + const FooDirective = /*@__PURE__*/ (() => { + ${classContent} + + return FooDirective; + })(); + export { FooDirective }; + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + + it('with property decorators in IIFE', () => { const input = tags.stripIndent` let ChipList = class ChipList extends Component { constructor(options, element) { @@ -58,8 +212,8 @@ describe('wrap enums and classes transformer', () => { expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); }); - - it('should not wrap ClassExpression without decorators', () => { + + it('should not wrap without decorators', () => { const input = tags.stripIndent` let ChipList = class ChipList extends Component { constructor(options, element) { From f83588f68223d94c1dd517d159778e76b611e33a Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 5 Jun 2019 09:01:56 +0200 Subject: [PATCH 3/3] test: add test to verify that the new es2015 class wrapping logic handles wrapping of tslib and tsickle classes Related to https://github.com/ngrx/platform/issues/1905 and https://github.com/ng-packagr/ng-packagr/issues/1307 Fixes #14613 --- .../src/transforms/wrap-enums_spec.ts | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts index 7a82b4f88642..c5cc64241e23 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts @@ -14,9 +14,40 @@ import { getWrapEnumsTransformer } from './wrap-enums'; const transform = (content: string) => transformJavascript( { content, getTransforms: [getWrapEnumsTransformer] }).content; -// tslint:disable-next-line:no-big-function +// tslint:disable:no-big-function describe('wrap enums and classes transformer', () => { describe('wraps class declarations', () => { + it('should wrap tsickle emitted classes which followed by metadata', () => { + const input = tags.stripIndent` + class CustomComponentEffects { + constructor(_actions) { + this._actions = _actions; + this.doThis = this._actions; + } + } + CustomComponentEffects.decorators = [{ type: Injectable }]; + CustomComponentEffects.ctorParameters = () => [{ type: Actions }]; + tslib_1.__decorate([ + Effect(), + tslib_1.__metadata("design:type", Object) + ], CustomComponentEffects.prototype, "doThis", void 0); + tslib_1.__decorate([ + Effect({ dispatch: false }), + tslib_1.__metadata("design:type", Object) + ], CustomComponentEffects.prototype, "doThat", void 0); + `; + + const output = tags.stripIndent` + const CustomComponentEffects = /*@__PURE__*/ (() => { + ${input} + + return CustomComponentEffects; + })(); + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + it('should ClassDeclarations that are referenced with in CallExpressions', () => { const input = tags.stripIndent` class ApplicationModule { @@ -305,7 +336,6 @@ describe('wrap enums and classes transformer', () => { }); }); - // tslint:disable-next-line:no-big-function describe('wrap enums', () => { it('wraps ts 2.2 enums in IIFE', () => { const input = tags.stripIndent`