272 lines
12 KiB
JavaScript
Executable file
272 lines
12 KiB
JavaScript
Executable file
"use strict";
|
|
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.getKeywords = void 0;
|
|
const core_1 = require("@babel/core");
|
|
const helper_annotate_as_pure_1 = __importDefault(require("@babel/helper-annotate-as-pure"));
|
|
/**
|
|
* The name of the Typescript decorator helper function created by the TypeScript compiler.
|
|
*/
|
|
const TSLIB_DECORATE_HELPER_NAME = '__decorate';
|
|
/**
|
|
* The set of Angular static fields that should always be wrapped.
|
|
* These fields may appear to have side effects but are safe to remove if the associated class
|
|
* is otherwise unused within the output.
|
|
*/
|
|
const angularStaticsToWrap = new Set([
|
|
'ɵcmp',
|
|
'ɵdir',
|
|
'ɵfac',
|
|
'ɵinj',
|
|
'ɵmod',
|
|
'ɵpipe',
|
|
'ɵprov',
|
|
'INJECTOR_KEY',
|
|
]);
|
|
/**
|
|
* An object map of static fields and related value checks for discovery of Angular generated
|
|
* JIT related static fields.
|
|
*/
|
|
const angularStaticsToElide = {
|
|
'ctorParameters'(path) {
|
|
return path.isFunctionExpression() || path.isArrowFunctionExpression();
|
|
},
|
|
'decorators'(path) {
|
|
return path.isArrayExpression();
|
|
},
|
|
'propDecorators'(path) {
|
|
return path.isObjectExpression();
|
|
},
|
|
};
|
|
/**
|
|
* 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.
|
|
*
|
|
* @returns An a string iterable containing one or more keywords.
|
|
*/
|
|
function getKeywords() {
|
|
return ['class'];
|
|
}
|
|
exports.getKeywords = getKeywords;
|
|
/**
|
|
* Determines whether a property and its initializer value can be safely wrapped in a pure
|
|
* annotated IIFE. Values that may cause side effects are not considered safe to wrap.
|
|
* Wrapping such values may cause runtime errors and/or incorrect runtime behavior.
|
|
*
|
|
* @param propertyName The name of the property to analyze.
|
|
* @param assignmentValue The initializer value that will be assigned to the property.
|
|
* @returns If the property can be safely wrapped, then true; otherwise, false.
|
|
*/
|
|
function canWrapProperty(propertyName, assignmentValue) {
|
|
if (angularStaticsToWrap.has(propertyName)) {
|
|
return true;
|
|
}
|
|
const { leadingComments } = assignmentValue.node;
|
|
if (leadingComments === null || leadingComments === void 0 ? void 0 : leadingComments.some(
|
|
// `@pureOrBreakMyCode` is used by closure and is present in Angular code
|
|
({ value }) => value.includes('#__PURE__') || value.includes('@pureOrBreakMyCode'))) {
|
|
return true;
|
|
}
|
|
return assignmentValue.isPure();
|
|
}
|
|
/**
|
|
* Analyze the sibling nodes of a class to determine if any downlevel elements should be
|
|
* wrapped in a pure annotated IIFE. Also determines if any elements have potential side
|
|
* effects.
|
|
*
|
|
* @param origin The starting NodePath location for analyzing siblings.
|
|
* @param classIdentifier The identifier node that represents the name of the class.
|
|
* @param allowWrappingDecorators Whether to allow decorators to be wrapped.
|
|
* @returns An object containing the results of the analysis.
|
|
*/
|
|
function analyzeClassSiblings(origin, classIdentifier, allowWrappingDecorators) {
|
|
var _a;
|
|
const wrapStatementPaths = [];
|
|
let hasPotentialSideEffects = false;
|
|
for (let i = 1;; ++i) {
|
|
const nextStatement = origin.getSibling(+origin.key + i);
|
|
if (!nextStatement.isExpressionStatement()) {
|
|
break;
|
|
}
|
|
// Valid sibling statements for class declarations are only assignment expressions
|
|
// and TypeScript decorator helper call expressions
|
|
const nextExpression = nextStatement.get('expression');
|
|
if (nextExpression.isCallExpression()) {
|
|
if (!core_1.types.isIdentifier(nextExpression.node.callee) ||
|
|
nextExpression.node.callee.name !== TSLIB_DECORATE_HELPER_NAME) {
|
|
break;
|
|
}
|
|
if (allowWrappingDecorators) {
|
|
wrapStatementPaths.push(nextStatement);
|
|
}
|
|
else {
|
|
// Statement cannot be safely wrapped which makes wrapping the class unneeded.
|
|
// The statement will prevent even a wrapped class from being optimized away.
|
|
hasPotentialSideEffects = true;
|
|
}
|
|
continue;
|
|
}
|
|
else if (!nextExpression.isAssignmentExpression()) {
|
|
break;
|
|
}
|
|
// Valid assignment expressions should be member access expressions using the class
|
|
// name as the object and an identifier as the property for static fields or only
|
|
// the class name for decorators.
|
|
const left = nextExpression.get('left');
|
|
if (left.isIdentifier()) {
|
|
if (!left.scope.bindingIdentifierEquals(left.node.name, classIdentifier) ||
|
|
!core_1.types.isCallExpression(nextExpression.node.right) ||
|
|
!core_1.types.isIdentifier(nextExpression.node.right.callee) ||
|
|
nextExpression.node.right.callee.name !== TSLIB_DECORATE_HELPER_NAME) {
|
|
break;
|
|
}
|
|
if (allowWrappingDecorators) {
|
|
wrapStatementPaths.push(nextStatement);
|
|
}
|
|
else {
|
|
// Statement cannot be safely wrapped which makes wrapping the class unneeded.
|
|
// The statement will prevent even a wrapped class from being optimized away.
|
|
hasPotentialSideEffects = true;
|
|
}
|
|
continue;
|
|
}
|
|
else if (!left.isMemberExpression() ||
|
|
!core_1.types.isIdentifier(left.node.object) ||
|
|
!left.scope.bindingIdentifierEquals(left.node.object.name, classIdentifier) ||
|
|
!core_1.types.isIdentifier(left.node.property)) {
|
|
break;
|
|
}
|
|
const propertyName = left.node.property.name;
|
|
const assignmentValue = nextExpression.get('right');
|
|
if ((_a = angularStaticsToElide[propertyName]) === null || _a === void 0 ? void 0 : _a.call(angularStaticsToElide, assignmentValue)) {
|
|
nextStatement.remove();
|
|
--i;
|
|
}
|
|
else if (canWrapProperty(propertyName, assignmentValue)) {
|
|
wrapStatementPaths.push(nextStatement);
|
|
}
|
|
else {
|
|
// Statement cannot be safely wrapped which makes wrapping the class unneeded.
|
|
// The statement will prevent even a wrapped class from being optimized away.
|
|
hasPotentialSideEffects = true;
|
|
}
|
|
}
|
|
return { hasPotentialSideEffects, wrapStatementPaths };
|
|
}
|
|
/**
|
|
* The set of classed already visited and analyzed during the plugin's execution.
|
|
* This is used to prevent adjusted classes from being repeatedly analyzed which can lead
|
|
* to an infinite loop.
|
|
*/
|
|
const visitedClasses = new WeakSet();
|
|
/**
|
|
* A babel plugin factory function for adjusting classes; primarily with Angular metadata.
|
|
* The adjustments include wrapping classes with known safe or no side effects with pure
|
|
* annotations to support dead code removal of unused classes. Angular compiler generated
|
|
* metadata static fields not required in AOT mode are also elided to better support bundler-
|
|
* level treeshaking.
|
|
*
|
|
* @returns A babel plugin object instance.
|
|
*/
|
|
function default_1() {
|
|
return {
|
|
visitor: {
|
|
ClassDeclaration(path, state) {
|
|
const { node: classNode, parentPath } = path;
|
|
const { wrapDecorators } = state.opts;
|
|
if (visitedClasses.has(classNode)) {
|
|
return;
|
|
}
|
|
// Analyze sibling statements for elements of the class that were downleveled
|
|
const hasExport = parentPath.isExportNamedDeclaration() || parentPath.isExportDefaultDeclaration();
|
|
const origin = hasExport ? parentPath : path;
|
|
const { wrapStatementPaths, hasPotentialSideEffects } = analyzeClassSiblings(origin, classNode.id, wrapDecorators);
|
|
visitedClasses.add(classNode);
|
|
if (hasPotentialSideEffects || wrapStatementPaths.length === 0) {
|
|
return;
|
|
}
|
|
const wrapStatementNodes = [];
|
|
for (const statementPath of wrapStatementPaths) {
|
|
wrapStatementNodes.push(statementPath.node);
|
|
statementPath.remove();
|
|
}
|
|
// Wrap class and safe static assignments in a pure annotated IIFE
|
|
const container = core_1.types.arrowFunctionExpression([], core_1.types.blockStatement([
|
|
classNode,
|
|
...wrapStatementNodes,
|
|
core_1.types.returnStatement(core_1.types.cloneNode(classNode.id)),
|
|
]));
|
|
const replacementInitializer = core_1.types.callExpression(core_1.types.parenthesizedExpression(container), []);
|
|
helper_annotate_as_pure_1.default(replacementInitializer);
|
|
// Replace class with IIFE wrapped class
|
|
const declaration = core_1.types.variableDeclaration('let', [
|
|
core_1.types.variableDeclarator(core_1.types.cloneNode(classNode.id), replacementInitializer),
|
|
]);
|
|
if (parentPath.isExportDefaultDeclaration()) {
|
|
// When converted to a variable declaration, the default export must be moved
|
|
// to a subsequent statement to prevent a JavaScript syntax error.
|
|
parentPath.replaceWithMultiple([
|
|
declaration,
|
|
core_1.types.exportNamedDeclaration(undefined, [
|
|
core_1.types.exportSpecifier(core_1.types.cloneNode(classNode.id), core_1.types.identifier('default')),
|
|
]),
|
|
]);
|
|
}
|
|
else {
|
|
path.replaceWith(declaration);
|
|
}
|
|
},
|
|
ClassExpression(path, state) {
|
|
const { node: classNode, parentPath } = path;
|
|
const { wrapDecorators } = state.opts;
|
|
// Class expressions are used by TypeScript to represent downlevel class/constructor decorators.
|
|
// If not wrapping decorators, they do not need to be processed.
|
|
if (!wrapDecorators || visitedClasses.has(classNode)) {
|
|
return;
|
|
}
|
|
if (!classNode.id ||
|
|
!parentPath.isVariableDeclarator() ||
|
|
!core_1.types.isIdentifier(parentPath.node.id) ||
|
|
parentPath.node.id.name !== classNode.id.name) {
|
|
return;
|
|
}
|
|
const origin = parentPath.parentPath;
|
|
if (!origin.isVariableDeclaration() || origin.node.declarations.length !== 1) {
|
|
return;
|
|
}
|
|
const { wrapStatementPaths, hasPotentialSideEffects } = analyzeClassSiblings(origin, parentPath.node.id, wrapDecorators);
|
|
visitedClasses.add(classNode);
|
|
if (hasPotentialSideEffects || wrapStatementPaths.length === 0) {
|
|
return;
|
|
}
|
|
const wrapStatementNodes = [];
|
|
for (const statementPath of wrapStatementPaths) {
|
|
wrapStatementNodes.push(statementPath.node);
|
|
statementPath.remove();
|
|
}
|
|
// Wrap class and safe static assignments in a pure annotated IIFE
|
|
const container = core_1.types.arrowFunctionExpression([], core_1.types.blockStatement([
|
|
core_1.types.variableDeclaration('let', [
|
|
core_1.types.variableDeclarator(core_1.types.cloneNode(classNode.id), classNode),
|
|
]),
|
|
...wrapStatementNodes,
|
|
core_1.types.returnStatement(core_1.types.cloneNode(classNode.id)),
|
|
]));
|
|
const replacementInitializer = core_1.types.callExpression(core_1.types.parenthesizedExpression(container), []);
|
|
helper_annotate_as_pure_1.default(replacementInitializer);
|
|
// Add the wrapped class directly to the variable declaration
|
|
parentPath.get('init').replaceWith(replacementInitializer);
|
|
},
|
|
},
|
|
};
|
|
}
|
|
exports.default = default_1;
|