|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright Google LLC All Rights Reserved. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license that can be |
| 6 | + * found in the LICENSE file at https://angular.io/license |
| 7 | + */ |
| 8 | + |
| 9 | +import { NodePath, PluginObj, PluginPass, types } from '@babel/core'; |
| 10 | +import annotateAsPure from '@babel/helper-annotate-as-pure'; |
| 11 | + |
| 12 | +/** |
| 13 | + * The name of the Typescript decorator helper function created by the TypeScript compiler. |
| 14 | + */ |
| 15 | +const TSLIB_DECORATE_HELPER_NAME = '__decorate'; |
| 16 | + |
| 17 | +/** |
| 18 | + * The set of Angular static fields that should always be wrapped. |
| 19 | + * These fields may appear to have side effects but are safe to remove if the associated class |
| 20 | + * is otherwise unused within the output. |
| 21 | + */ |
| 22 | +const angularStaticsToWrap = new Set([ |
| 23 | + 'ɵcmp', |
| 24 | + 'ɵdir', |
| 25 | + 'ɵfac', |
| 26 | + 'ɵinj', |
| 27 | + 'ɵmod', |
| 28 | + 'ɵpipe', |
| 29 | + 'ɵprov', |
| 30 | + 'INJECTOR_KEY', |
| 31 | +]); |
| 32 | + |
| 33 | +/** |
| 34 | + * An object map of static fields and related value checks for discovery of Angular generated |
| 35 | + * JIT related static fields. |
| 36 | + */ |
| 37 | +const angularStaticsToElide: Record<string, (path: NodePath<types.Expression>) => boolean> = { |
| 38 | + 'ctorParameters'(path) { |
| 39 | + return path.isFunctionExpression() || path.isArrowFunctionExpression(); |
| 40 | + }, |
| 41 | + 'decorators'(path) { |
| 42 | + return path.isArrayExpression(); |
| 43 | + }, |
| 44 | + 'propDecorators'(path) { |
| 45 | + return path.isObjectExpression(); |
| 46 | + }, |
| 47 | +}; |
| 48 | + |
| 49 | +/** |
| 50 | + * Provides one or more keywords that if found within the content of a source file indicate |
| 51 | + * that this plugin should be used with a source file. |
| 52 | + * |
| 53 | + * @returns An a string iterable containing one or more keywords. |
| 54 | + */ |
| 55 | +export function getKeywords(): Iterable<string> { |
| 56 | + return ['class']; |
| 57 | +} |
| 58 | + |
| 59 | +/** |
| 60 | + * Analyze the sibling nodes of a class to determine if any downlevel elements should be |
| 61 | + * wrapped in a pure annotated IIFE. Also determines if any elements have potential side |
| 62 | + * effects. |
| 63 | + * |
| 64 | + * @param origin The starting NodePath location for analyzing siblings. |
| 65 | + * @param classIdentifier The identifier node that represents the name of the class. |
| 66 | + * @param allowWrappingDecorators Whether to allow decorators to be wrapped. |
| 67 | + * @returns An object containing the results of the analysis. |
| 68 | + */ |
| 69 | +function analyzeClassSiblings( |
| 70 | + origin: NodePath, |
| 71 | + classIdentifier: types.Identifier, |
| 72 | + allowWrappingDecorators: boolean, |
| 73 | +): { hasPotentialSideEffects: boolean; wrapStatementPaths: NodePath<types.Statement>[] } { |
| 74 | + const wrapStatementPaths: NodePath<types.Statement>[] = []; |
| 75 | + let hasPotentialSideEffects = false; |
| 76 | + for (let i = 1; ; ++i) { |
| 77 | + const nextStatement = origin.getSibling(+origin.key + i); |
| 78 | + if (!nextStatement.isExpressionStatement()) { |
| 79 | + break; |
| 80 | + } |
| 81 | + |
| 82 | + // Valid sibling statements for class declarations are only assignment expressions |
| 83 | + // and TypeScript decorator helper call expressions |
| 84 | + const nextExpression = nextStatement.get('expression'); |
| 85 | + if (nextExpression.isCallExpression()) { |
| 86 | + if ( |
| 87 | + !types.isIdentifier(nextExpression.node.callee) || |
| 88 | + nextExpression.node.callee.name !== TSLIB_DECORATE_HELPER_NAME |
| 89 | + ) { |
| 90 | + break; |
| 91 | + } |
| 92 | + |
| 93 | + if (allowWrappingDecorators) { |
| 94 | + wrapStatementPaths.push(nextStatement); |
| 95 | + } else { |
| 96 | + // Statement cannot be safely wrapped which makes wrapping the class unneeded. |
| 97 | + // The statement will prevent even a wrapped class from being optimized away. |
| 98 | + hasPotentialSideEffects = true; |
| 99 | + } |
| 100 | + |
| 101 | + continue; |
| 102 | + } else if (!nextExpression.isAssignmentExpression()) { |
| 103 | + break; |
| 104 | + } |
| 105 | + |
| 106 | + // Valid assignment expressions should be member access expressions using the class |
| 107 | + // name as the object and an identifier as the property for static fields or only |
| 108 | + // the class name for decorators. |
| 109 | + const left = nextExpression.get('left'); |
| 110 | + if (left.isIdentifier()) { |
| 111 | + if ( |
| 112 | + !left.scope.bindingIdentifierEquals(left.node.name, classIdentifier) || |
| 113 | + !types.isCallExpression(nextExpression.node.right) || |
| 114 | + !types.isIdentifier(nextExpression.node.right.callee) || |
| 115 | + nextExpression.node.right.callee.name !== TSLIB_DECORATE_HELPER_NAME |
| 116 | + ) { |
| 117 | + break; |
| 118 | + } |
| 119 | + |
| 120 | + if (allowWrappingDecorators) { |
| 121 | + wrapStatementPaths.push(nextStatement); |
| 122 | + } else { |
| 123 | + // Statement cannot be safely wrapped which makes wrapping the class unneeded. |
| 124 | + // The statement will prevent even a wrapped class from being optimized away. |
| 125 | + hasPotentialSideEffects = true; |
| 126 | + } |
| 127 | + |
| 128 | + continue; |
| 129 | + } else if ( |
| 130 | + !left.isMemberExpression() || |
| 131 | + !types.isIdentifier(left.node.object) || |
| 132 | + !left.scope.bindingIdentifierEquals(left.node.object.name, classIdentifier) || |
| 133 | + !types.isIdentifier(left.node.property) |
| 134 | + ) { |
| 135 | + break; |
| 136 | + } |
| 137 | + |
| 138 | + const propertyName = left.node.property.name; |
| 139 | + const assignmentValue = nextExpression.get('right'); |
| 140 | + if (angularStaticsToElide[propertyName]?.(assignmentValue)) { |
| 141 | + nextStatement.remove(); |
| 142 | + --i; |
| 143 | + } else if (angularStaticsToWrap.has(propertyName) || assignmentValue.isPure()) { |
| 144 | + wrapStatementPaths.push(nextStatement); |
| 145 | + } else { |
| 146 | + // Statement cannot be safely wrapped which makes wrapping the class unneeded. |
| 147 | + // The statement will prevent even a wrapped class from being optimized away. |
| 148 | + hasPotentialSideEffects = true; |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + return { hasPotentialSideEffects, wrapStatementPaths }; |
| 153 | +} |
| 154 | + |
| 155 | +/** |
| 156 | + * The set of classed already visited and analyzed during the plugin's execution. |
| 157 | + * This is used to prevent adjusted classes from being repeatedly analyzed which can lead |
| 158 | + * to an infinite loop. |
| 159 | + */ |
| 160 | +const visitedClasses = new WeakSet<types.Class>(); |
| 161 | + |
| 162 | +/** |
| 163 | + * A babel plugin factory function for adjusting classes; primarily with Angular metadata. |
| 164 | + * The adjustments include wrapping classes with known safe or no side effects with pure |
| 165 | + * annotations to support dead code removal of unused classes. Angular compiler generated |
| 166 | + * metadata static fields not required in AOT mode are also elided to better support bundler- |
| 167 | + * level treeshaking. |
| 168 | + * |
| 169 | + * @returns A babel plugin object instance. |
| 170 | + */ |
| 171 | +export default function (): PluginObj { |
| 172 | + return { |
| 173 | + visitor: { |
| 174 | + ClassDeclaration(path: NodePath<types.ClassDeclaration>, state: PluginPass) { |
| 175 | + const { node: classNode, parentPath } = path; |
| 176 | + const { wrapDecorators } = state.opts as { wrapDecorators: boolean }; |
| 177 | + |
| 178 | + if (visitedClasses.has(classNode)) { |
| 179 | + return; |
| 180 | + } |
| 181 | + |
| 182 | + // Analyze sibling statements for elements of the class that were downleveled |
| 183 | + const hasExport = |
| 184 | + parentPath.isExportNamedDeclaration() || parentPath.isExportDefaultDeclaration(); |
| 185 | + const origin = hasExport ? parentPath : path; |
| 186 | + const { wrapStatementPaths, hasPotentialSideEffects } = analyzeClassSiblings( |
| 187 | + origin, |
| 188 | + classNode.id, |
| 189 | + wrapDecorators, |
| 190 | + ); |
| 191 | + |
| 192 | + visitedClasses.add(classNode); |
| 193 | + |
| 194 | + if (hasPotentialSideEffects || wrapStatementPaths.length === 0) { |
| 195 | + return; |
| 196 | + } |
| 197 | + |
| 198 | + const wrapStatementNodes: types.Statement[] = []; |
| 199 | + for (const statementPath of wrapStatementPaths) { |
| 200 | + wrapStatementNodes.push(statementPath.node); |
| 201 | + statementPath.remove(); |
| 202 | + } |
| 203 | + |
| 204 | + // Wrap class and safe static assignments in a pure annotated IIFE |
| 205 | + const container = types.arrowFunctionExpression( |
| 206 | + [], |
| 207 | + types.blockStatement([ |
| 208 | + classNode, |
| 209 | + ...wrapStatementNodes, |
| 210 | + types.returnStatement(types.cloneNode(classNode.id)), |
| 211 | + ]), |
| 212 | + ); |
| 213 | + const replacementInitializer = types.callExpression( |
| 214 | + types.parenthesizedExpression(container), |
| 215 | + [], |
| 216 | + ); |
| 217 | + annotateAsPure(replacementInitializer); |
| 218 | + |
| 219 | + // Replace class with IIFE wrapped class |
| 220 | + const declaration = types.variableDeclaration('let', [ |
| 221 | + types.variableDeclarator(types.cloneNode(classNode.id), replacementInitializer), |
| 222 | + ]); |
| 223 | + if (parentPath.isExportDefaultDeclaration()) { |
| 224 | + // When converted to a variable declaration, the default export must be moved |
| 225 | + // to a subsequent statement to prevent a JavaScript syntax error. |
| 226 | + parentPath.replaceWithMultiple([ |
| 227 | + declaration, |
| 228 | + types.exportNamedDeclaration(undefined, [ |
| 229 | + types.exportSpecifier(types.cloneNode(classNode.id), types.identifier('default')), |
| 230 | + ]), |
| 231 | + ]); |
| 232 | + } else { |
| 233 | + path.replaceWith(declaration); |
| 234 | + } |
| 235 | + }, |
| 236 | + ClassExpression(path: NodePath<types.ClassExpression>, state: PluginPass) { |
| 237 | + const { node: classNode, parentPath } = path; |
| 238 | + const { wrapDecorators } = state.opts as { wrapDecorators: boolean }; |
| 239 | + |
| 240 | + // Class expressions are used by TypeScript to represent downlevel class/constructor decorators. |
| 241 | + // If not wrapping decorators, they do not need to be processed. |
| 242 | + if (!wrapDecorators || visitedClasses.has(classNode)) { |
| 243 | + return; |
| 244 | + } |
| 245 | + |
| 246 | + if ( |
| 247 | + !classNode.id || |
| 248 | + !parentPath.isVariableDeclarator() || |
| 249 | + !types.isIdentifier(parentPath.node.id) || |
| 250 | + parentPath.node.id.name !== classNode.id.name |
| 251 | + ) { |
| 252 | + return; |
| 253 | + } |
| 254 | + |
| 255 | + const origin = parentPath.parentPath; |
| 256 | + if (!origin.isVariableDeclaration() || origin.node.declarations.length !== 1) { |
| 257 | + return; |
| 258 | + } |
| 259 | + |
| 260 | + const { wrapStatementPaths, hasPotentialSideEffects } = analyzeClassSiblings( |
| 261 | + origin, |
| 262 | + parentPath.node.id, |
| 263 | + wrapDecorators, |
| 264 | + ); |
| 265 | + |
| 266 | + visitedClasses.add(classNode); |
| 267 | + |
| 268 | + if (hasPotentialSideEffects || wrapStatementPaths.length === 0) { |
| 269 | + return; |
| 270 | + } |
| 271 | + |
| 272 | + const wrapStatementNodes: types.Statement[] = []; |
| 273 | + for (const statementPath of wrapStatementPaths) { |
| 274 | + wrapStatementNodes.push(statementPath.node); |
| 275 | + statementPath.remove(); |
| 276 | + } |
| 277 | + |
| 278 | + // Wrap class and safe static assignments in a pure annotated IIFE |
| 279 | + const container = types.arrowFunctionExpression( |
| 280 | + [], |
| 281 | + types.blockStatement([ |
| 282 | + types.variableDeclaration('let', [ |
| 283 | + types.variableDeclarator(types.cloneNode(classNode.id), classNode), |
| 284 | + ]), |
| 285 | + ...wrapStatementNodes, |
| 286 | + types.returnStatement(types.cloneNode(classNode.id)), |
| 287 | + ]), |
| 288 | + ); |
| 289 | + const replacementInitializer = types.callExpression( |
| 290 | + types.parenthesizedExpression(container), |
| 291 | + [], |
| 292 | + ); |
| 293 | + annotateAsPure(replacementInitializer); |
| 294 | + |
| 295 | + // Add the wrapped class directly to the variable declaration |
| 296 | + parentPath.get('init').replaceWith(replacementInitializer); |
| 297 | + }, |
| 298 | + }, |
| 299 | + }; |
| 300 | +} |
0 commit comments