From d32171444237b28000d451e4e7cba7aa257c34aa Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 4 Sep 2023 10:51:15 +0200 Subject: [PATCH] fix(@angular-devkit/build-angular): elide setClassMetadataAsync calls Updates the logic that elides `setClassMetadata` calls to also elide `setClassMetadataAsync`. The latter will be emitted when the component uses the new `defer` block syntax. --- .../babel/plugins/elide-angular-metadata.ts | 73 +++++++++++----- .../plugins/elide-angular-metadata_spec.ts | 84 +++++++++++++++++++ 2 files changed, 137 insertions(+), 20 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/tools/babel/plugins/elide-angular-metadata.ts b/packages/angular_devkit/build_angular/src/tools/babel/plugins/elide-angular-metadata.ts index 5d6a54ae611e..af17d4d4359f 100644 --- a/packages/angular_devkit/build_angular/src/tools/babel/plugins/elide-angular-metadata.ts +++ b/packages/angular_devkit/build_angular/src/tools/babel/plugins/elide-angular-metadata.ts @@ -13,6 +13,11 @@ import { NodePath, PluginObj, types } from '@babel/core'; */ const SET_CLASS_METADATA_NAME = 'ɵsetClassMetadata'; +/** + * Name of the asynchronous Angular class metadata function created by the Angular compiler. + */ +const SET_CLASS_METADATA_ASYNC_NAME = 'ɵsetClassMetadataAsync'; + /** * Provides one or more keywords that if found within the content of a source file indicate * that this plugin should be used with a source file. @@ -20,7 +25,7 @@ const SET_CLASS_METADATA_NAME = 'ɵsetClassMetadata'; * @returns An a string iterable containing one or more keywords. */ export function getKeywords(): Iterable { - return [SET_CLASS_METADATA_NAME]; + return [SET_CLASS_METADATA_NAME, SET_CLASS_METADATA_ASYNC_NAME]; } /** @@ -33,6 +38,7 @@ export default function (): PluginObj { visitor: { CallExpression(path: NodePath) { const callee = path.node.callee; + const callArguments = path.node.arguments; // The function being called must be the metadata function name let calleeName; @@ -41,31 +47,58 @@ export default function (): PluginObj { } else if (types.isIdentifier(callee)) { calleeName = callee.name; } - if (calleeName !== SET_CLASS_METADATA_NAME) { - return; - } - // There must be four arguments that meet the following criteria: - // * First must be an identifier - // * Second must be an array literal - const callArguments = path.node.arguments; if ( - callArguments.length !== 4 || - !types.isIdentifier(callArguments[0]) || - !types.isArrayExpression(callArguments[1]) + calleeName !== undefined && + (isRemoveClassMetadataCall(calleeName, callArguments) || + isRemoveClassmetadataAsyncCall(calleeName, callArguments)) ) { - return; - } + // The metadata function is always emitted inside a function expression + const parent = path.getFunctionParent(); - // The metadata function is always emitted inside a function expression - const parent = path.getFunctionParent(); - - if (parent?.isFunctionExpression() || parent?.isArrowFunctionExpression()) { - // Replace the metadata function with `void 0` which is the equivalent return value - // of the metadata function. - path.replaceWith(path.scope.buildUndefinedNode()); + if (parent?.isFunctionExpression() || parent?.isArrowFunctionExpression()) { + // Replace the metadata function with `void 0` which is the equivalent return value + // of the metadata function. + path.replaceWith(path.scope.buildUndefinedNode()); + } } }, }, }; } + +/** Determines if a function call is a call to `setClassMetadata`. */ +function isRemoveClassMetadataCall(name: string, args: types.CallExpression['arguments']): boolean { + // `setClassMetadata` calls have to meet the following criteria: + // * First must be an identifier + // * Second must be an array literal + return ( + name === SET_CLASS_METADATA_NAME && + args.length === 4 && + types.isIdentifier(args[0]) && + types.isArrayExpression(args[1]) + ); +} + +/** Determines if a function call is a call to `setClassMetadataAsync`. */ +function isRemoveClassmetadataAsyncCall( + name: string, + args: types.CallExpression['arguments'], +): boolean { + // `setClassMetadataAsync` calls have to meet the following criteria: + // * First argument must be an identifier. + // * Second argument must be an inline function. + // * Third argument must be an inline function. + return ( + name === SET_CLASS_METADATA_ASYNC_NAME && + args.length === 3 && + types.isIdentifier(args[0]) && + isInlineFunction(args[1]) && + isInlineFunction(args[2]) + ); +} + +/** Determines if a node is an inline function expression. */ +function isInlineFunction(node: types.Node): boolean { + return types.isFunctionExpression(node) || types.isArrowFunctionExpression(node); +} diff --git a/packages/angular_devkit/build_angular/src/tools/babel/plugins/elide-angular-metadata_spec.ts b/packages/angular_devkit/build_angular/src/tools/babel/plugins/elide-angular-metadata_spec.ts index c7f0af207063..ef73e1336487 100644 --- a/packages/angular_devkit/build_angular/src/tools/babel/plugins/elide-angular-metadata_spec.ts +++ b/packages/angular_devkit/build_angular/src/tools/babel/plugins/elide-angular-metadata_spec.ts @@ -102,4 +102,88 @@ describe('elide-angular-metadata Babel plugin', () => { `, }), ); + + it( + 'elides pure annotated ɵsetClassMetadataAsync', + testCase({ + input: ` + import { Component } from '@angular/core'; + export class SomeClass {} + /*@__PURE__*/ (function () { + i0.ɵsetClassMetadataAsync(SomeClass, + function () { return [import("./cmp-a").then(function (m) { return m.CmpA; })]; }, + function (CmpA) { i0.ɵsetClassMetadata(SomeClass, [{ + type: Component, + args: [{ + selector: 'test-cmp', + standalone: true, + imports: [CmpA, LocalDep], + template: '{#defer}{/defer}', + }] + }], null, null); }); + })(); + `, + expected: ` + import { Component } from '@angular/core'; + export class SomeClass {} + /*@__PURE__*/ (function () { void 0 })(); + `, + }), + ); + + it( + 'elides JIT mode protected ɵsetClassMetadataAsync', + testCase({ + input: ` + import { Component } from '@angular/core'; + export class SomeClass {} + (function () { + (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵsetClassMetadataAsync(SomeClass, + function () { return [import("./cmp-a").then(function (m) { return m.CmpA; })]; }, + function (CmpA) { i0.ɵsetClassMetadata(SomeClass, [{ + type: Component, + args: [{ + selector: 'test-cmp', + standalone: true, + imports: [CmpA, LocalDep], + template: '{#defer}{/defer}', + }] + }], null, null); }); + })(); + `, + expected: ` + import { Component } from '@angular/core'; + export class SomeClass {} + (function () { (typeof ngJitMode === "undefined" || ngJitMode) && void 0 })(); + `, + }), + ); + + it( + 'elides arrow-function-based ɵsetClassMetadataAsync', + testCase({ + input: ` + import { Component } from '@angular/core'; + export class SomeClass {} + /*@__PURE__*/ (() => { + i0.ɵsetClassMetadataAsync(SomeClass, + () => [import("./cmp-a").then(m => m.CmpA)], + (CmpA) => { i0.ɵsetClassMetadata(SomeClass, [{ + type: Component, + args: [{ + selector: 'test-cmp', + standalone: true, + imports: [CmpA, LocalDep], + template: '{#defer}{/defer}', + }] + }], null, null); }); + })(); + `, + expected: ` + import { Component } from '@angular/core'; + export class SomeClass {} + /*@__PURE__*/ (() => { void 0 })(); + `, + }), + ); });