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