@angular-devkit/build-optimizer
Version:
Angular Build Optimizer
497 lines (496 loc) • 18.9 kB
JavaScript
"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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.expect = exports.createScrubFileTransformerFactory = exports.testScrubFile = void 0;
const ts = __importStar(require("typescript"));
const ast_utils_1 = require("../helpers/ast-utils");
function testScrubFile(content) {
const markers = [
'decorators',
'__decorate',
'propDecorators',
'ctorParameters',
'ɵsetClassMetadata',
];
return markers.some((marker) => content.includes(marker));
}
exports.testScrubFile = testScrubFile;
function createScrubFileTransformerFactory(isAngularCoreFile) {
return (program) => scrubFileTransformer(program, isAngularCoreFile);
}
exports.createScrubFileTransformerFactory = createScrubFileTransformerFactory;
function scrubFileTransformer(program, isAngularCoreFile) {
if (!program) {
throw new Error('scrubFileTransformer requires a TypeScript Program.');
}
const checker = program.getTypeChecker();
return (context) => {
const transformer = (sf) => {
const ngMetadata = findAngularMetadata(sf, isAngularCoreFile);
const tslibImports = findTslibImports(sf);
const nodes = [];
ts.forEachChild(sf, checkNodeForDecorators);
function checkNodeForDecorators(node) {
var _a;
if (!ts.isExpressionStatement(node)) {
return ts.forEachChild(node, checkNodeForDecorators);
}
const exprStmt = node;
const iife = (_a = getIifeStatement(exprStmt)) === null || _a === void 0 ? void 0 : _a.expression;
// Do checks that don't need the typechecker first and bail early.
if (isCtorParamsAssignmentExpression(exprStmt)) {
nodes.push(node);
}
else if (iife && isIvyPrivateCallExpression(iife, 'ɵsetClassMetadata')) {
nodes.push(node);
}
else if (iife &&
ts.isBinaryExpression(iife) &&
isIvyPrivateCallExpression(iife.right, 'ɵsetClassMetadata')) {
nodes.push(node);
}
else if (isDecoratorAssignmentExpression(exprStmt)) {
nodes.push(...pickDecorationNodesToRemove(exprStmt, ngMetadata, checker));
}
else if (isDecorateAssignmentExpression(exprStmt, tslibImports, checker) ||
isAngularDecoratorExpression(exprStmt, ngMetadata, tslibImports, checker)) {
nodes.push(...pickDecorateNodesToRemove(exprStmt, tslibImports, ngMetadata, checker));
}
else if (isPropDecoratorAssignmentExpression(exprStmt)) {
nodes.push(...pickPropDecorationNodesToRemove(exprStmt, ngMetadata, checker));
}
}
const visitor = (node) => {
// Check if node is a statement to be dropped.
if (nodes.find((n) => n === node)) {
return undefined;
}
// Otherwise return node as is.
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(sf, visitor);
};
return transformer;
};
}
function expect(node, kind) {
if (node.kind !== kind) {
throw new Error('Invalid node type.');
}
return node;
}
exports.expect = expect;
function findAngularMetadata(node, isAngularCoreFile) {
let specs = [];
// Find all specifiers from imports of `@angular/core`.
ts.forEachChild(node, (child) => {
if (child.kind === ts.SyntaxKind.ImportDeclaration) {
const importDecl = child;
if (isAngularCoreImport(importDecl, isAngularCoreFile)) {
specs.push(...(0, ast_utils_1.collectDeepNodes)(importDecl, ts.SyntaxKind.ImportSpecifier));
}
}
});
// If the current module is a Angular core file, we also consider all declarations in it to
// potentially be Angular metadata.
if (isAngularCoreFile) {
const localDecl = findAllDeclarations(node);
specs = specs.concat(localDecl);
}
return specs;
}
function findAllDeclarations(node) {
const nodes = [];
ts.forEachChild(node, (child) => {
if (child.kind === ts.SyntaxKind.VariableStatement) {
const vStmt = child;
vStmt.declarationList.declarations.forEach((decl) => {
if (decl.name.kind !== ts.SyntaxKind.Identifier) {
return;
}
nodes.push(decl);
});
}
});
return nodes;
}
function isAngularCoreImport(node, isAngularCoreFile) {
if (!(node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier))) {
return false;
}
const importText = node.moduleSpecifier.text;
// Imports to `@angular/core` are always core imports.
if (importText === '@angular/core') {
return true;
}
// Relative imports from a Angular core file are also core imports.
if (isAngularCoreFile && importText.startsWith('.')) {
return true;
}
return false;
}
// Check if assignment is `Clazz.decorators = [...];`.
function isDecoratorAssignmentExpression(exprStmt) {
if (!isAssignmentExpressionTo(exprStmt, 'decorators')) {
return false;
}
const expr = exprStmt.expression;
if (!ts.isArrayLiteralExpression(expr.right)) {
return false;
}
return true;
}
// Check if assignment is `Clazz = __decorate([...], Clazz)`.
function isDecorateAssignmentExpression(exprStmt, tslibImports, checker) {
if (!ts.isBinaryExpression(exprStmt.expression)) {
return false;
}
const expr = exprStmt.expression;
if (!ts.isIdentifier(expr.left)) {
return false;
}
const classIdent = expr.left;
let callExpr;
if (ts.isCallExpression(expr.right)) {
callExpr = expr.right;
}
else if (ts.isBinaryExpression(expr.right)) {
// `Clazz = Clazz_1 = __decorate([...], Clazz)` can be found when there are static property
// accesses.
const innerExpr = expr.right;
if (!ts.isIdentifier(innerExpr.left) || !ts.isCallExpression(innerExpr.right)) {
return false;
}
callExpr = innerExpr.right;
}
else {
return false;
}
if (!isTslibHelper(callExpr, '__decorate', tslibImports, checker)) {
return false;
}
if (callExpr.arguments.length !== 2) {
return false;
}
const classArg = callExpr.arguments[1];
if (!ts.isIdentifier(classArg)) {
return false;
}
if (classIdent.text !== classArg.text) {
return false;
}
if (!ts.isArrayLiteralExpression(callExpr.arguments[0])) {
return false;
}
return true;
}
// Check if expression is `__decorate([smt, __metadata("design:type", Object)], ...)`.
function isAngularDecoratorExpression(exprStmt, ngMetadata, tslibImports, checker) {
if (!ts.isCallExpression(exprStmt.expression)) {
return false;
}
const callExpr = exprStmt.expression;
if (!isTslibHelper(callExpr, '__decorate', tslibImports, checker)) {
return false;
}
if (callExpr.arguments.length !== 4) {
return false;
}
const decorateArray = callExpr.arguments[0];
if (!ts.isArrayLiteralExpression(decorateArray)) {
return false;
}
// Check first array entry for Angular decorators.
if (decorateArray.elements.length === 0 || !ts.isCallExpression(decorateArray.elements[0])) {
return false;
}
return decorateArray.elements.some((decoratorCall) => {
if (!ts.isCallExpression(decoratorCall) || !ts.isIdentifier(decoratorCall.expression)) {
return false;
}
const decoratorId = decoratorCall.expression;
return identifierIsMetadata(decoratorId, ngMetadata, checker);
});
}
// Check if assignment is `Clazz.propDecorators = [...];`.
function isPropDecoratorAssignmentExpression(exprStmt) {
if (!isAssignmentExpressionTo(exprStmt, 'propDecorators')) {
return false;
}
const expr = exprStmt.expression;
if (expr.right.kind !== ts.SyntaxKind.ObjectLiteralExpression) {
return false;
}
return true;
}
// Check if assignment is `Clazz.ctorParameters = [...];`.
function isCtorParamsAssignmentExpression(exprStmt) {
if (!isAssignmentExpressionTo(exprStmt, 'ctorParameters')) {
return false;
}
const expr = exprStmt.expression;
if (expr.right.kind !== ts.SyntaxKind.FunctionExpression &&
expr.right.kind !== ts.SyntaxKind.ArrowFunction) {
return false;
}
return true;
}
function isAssignmentExpressionTo(exprStmt, name) {
if (!ts.isBinaryExpression(exprStmt.expression)) {
return false;
}
const expr = exprStmt.expression;
if (!ts.isPropertyAccessExpression(expr.left)) {
return false;
}
const propAccess = expr.left;
if (propAccess.name.text !== name) {
return false;
}
if (!ts.isIdentifier(propAccess.expression)) {
return false;
}
if (expr.operatorToken.kind !== ts.SyntaxKind.FirstAssignment) {
return false;
}
return true;
}
// Each Ivy private call expression is inside an IIFE
function getIifeStatement(exprStmt) {
const expression = exprStmt.expression;
if (!expression || !ts.isCallExpression(expression) || expression.arguments.length !== 0) {
return null;
}
const parenExpr = expression;
if (!ts.isParenthesizedExpression(parenExpr.expression)) {
return null;
}
const funExpr = parenExpr.expression.expression;
if (!ts.isFunctionExpression(funExpr)) {
return null;
}
const innerStmts = funExpr.body.statements;
if (innerStmts.length !== 1) {
return null;
}
const innerExprStmt = innerStmts[0];
if (!ts.isExpressionStatement(innerExprStmt)) {
return null;
}
return innerExprStmt;
}
function isIvyPrivateCallExpression(expression, name) {
// Now we're in the IIFE and have the inner expression statement. We can check if it matches
// a private Ivy call.
if (!ts.isCallExpression(expression)) {
return false;
}
const propAccExpr = expression.expression;
if (!ts.isPropertyAccessExpression(propAccExpr)) {
return false;
}
if (propAccExpr.name.text != name) {
return false;
}
return true;
}
// Remove Angular decorators from`Clazz.decorators = [...];`, or expression itself if all are
// removed.
function pickDecorationNodesToRemove(exprStmt, ngMetadata, checker) {
const expr = expect(exprStmt.expression, ts.SyntaxKind.BinaryExpression);
const literal = expect(expr.right, ts.SyntaxKind.ArrayLiteralExpression);
if (!literal.elements.every((elem) => ts.isObjectLiteralExpression(elem))) {
return [];
}
const elements = literal.elements;
const ngDecorators = elements.filter((elem) => isAngularDecorator(elem, ngMetadata, checker));
return elements.length > ngDecorators.length ? ngDecorators : [exprStmt];
}
// Remove Angular decorators from `Clazz = __decorate([...], Clazz)`, or expression itself if all
// are removed.
function pickDecorateNodesToRemove(exprStmt, tslibImports, ngMetadata, checker) {
let callExpr;
if (ts.isCallExpression(exprStmt.expression)) {
callExpr = exprStmt.expression;
}
else if (ts.isBinaryExpression(exprStmt.expression)) {
const expr = exprStmt.expression;
if (ts.isCallExpression(expr.right)) {
callExpr = expr.right;
}
else if (ts.isBinaryExpression(expr.right) && ts.isCallExpression(expr.right.right)) {
callExpr = expr.right.right;
}
}
if (!callExpr) {
return [];
}
const arrLiteral = expect(callExpr.arguments[0], ts.SyntaxKind.ArrayLiteralExpression);
if (!arrLiteral.elements.every((elem) => ts.isCallExpression(elem))) {
return [];
}
const elements = arrLiteral.elements;
const ngDecoratorCalls = elements.filter((el) => {
if (!ts.isIdentifier(el.expression)) {
return false;
}
return identifierIsMetadata(el.expression, ngMetadata, checker);
});
// Remove __metadata calls of type 'design:paramtypes'.
const metadataCalls = elements.filter((el) => {
if (!isTslibHelper(el, '__metadata', tslibImports, checker)) {
return false;
}
if (el.arguments.length < 2 || !ts.isStringLiteral(el.arguments[0])) {
return false;
}
return true;
});
// Remove all __param calls.
const paramCalls = elements.filter((el) => {
if (!isTslibHelper(el, '__param', tslibImports, checker)) {
return false;
}
if (el.arguments.length !== 2 || !ts.isNumericLiteral(el.arguments[0])) {
return false;
}
return true;
});
if (ngDecoratorCalls.length === 0) {
return [];
}
const callCount = ngDecoratorCalls.length + metadataCalls.length + paramCalls.length;
// If all decorators are metadata decorators then return the whole `Class = __decorate([...])'`
// statement so that it is removed in entirety.
// If not then only remove the Angular decorators.
// The metadata and param calls may be used by the non-Angular decorators.
return elements.length === callCount ? [exprStmt] : ngDecoratorCalls;
}
// Remove Angular decorators from`Clazz.propDecorators = [...];`, or expression itself if all
// are removed.
function pickPropDecorationNodesToRemove(exprStmt, ngMetadata, checker) {
const expr = expect(exprStmt.expression, ts.SyntaxKind.BinaryExpression);
const literal = expect(expr.right, ts.SyntaxKind.ObjectLiteralExpression);
if (!literal.properties.every((elem) => ts.isPropertyAssignment(elem) && ts.isArrayLiteralExpression(elem.initializer))) {
return [];
}
const assignments = literal.properties;
// Consider each assignment individually. Either the whole assignment will be removed or
// a particular decorator within will.
const toRemove = assignments
.map((assign) => {
const decorators = expect(assign.initializer, ts.SyntaxKind.ArrayLiteralExpression).elements;
if (!decorators.every((el) => ts.isObjectLiteralExpression(el))) {
return [];
}
const decsToRemove = decorators.filter((expression) => {
const lit = expect(expression, ts.SyntaxKind.ObjectLiteralExpression);
return isAngularDecorator(lit, ngMetadata, checker);
});
if (decsToRemove.length === decorators.length) {
return [assign];
}
return decsToRemove;
})
.reduce((accum, toRm) => accum.concat(toRm), []);
// If every node to be removed is a property assignment (full property's decorators) and
// all properties are accounted for, remove the whole assignment. Otherwise, remove the
// nodes which were marked as safe.
if (toRemove.length === assignments.length &&
toRemove.every((node) => ts.isPropertyAssignment(node))) {
return [exprStmt];
}
return toRemove;
}
function isAngularDecorator(literal, ngMetadata, checker) {
const types = literal.properties.filter(isTypeProperty);
if (types.length !== 1) {
return false;
}
const assign = expect(types[0], ts.SyntaxKind.PropertyAssignment);
if (!ts.isIdentifier(assign.initializer)) {
return false;
}
const id = assign.initializer;
const res = identifierIsMetadata(id, ngMetadata, checker);
return res;
}
function isTypeProperty(prop) {
if (!ts.isPropertyAssignment(prop)) {
return false;
}
if (!ts.isIdentifier(prop.name)) {
return false;
}
return prop.name.text === 'type';
}
// Check if an identifier is part of the known Angular Metadata.
function identifierIsMetadata(id, metadata, checker) {
const symbol = checker.getSymbolAtLocation(id);
if (!symbol || !symbol.declarations || !symbol.declarations.length) {
return false;
}
return symbol.declarations.some((spec) => metadata.includes(spec));
}
// Find all named imports for `tslib`.
function findTslibImports(node) {
const imports = [];
ts.forEachChild(node, (child) => {
var _a, _b;
if (ts.isImportDeclaration(child) &&
child.moduleSpecifier &&
ts.isStringLiteral(child.moduleSpecifier) &&
child.moduleSpecifier.text === 'tslib' &&
((_a = child.importClause) === null || _a === void 0 ? void 0 : _a.namedBindings) &&
ts.isNamedImports((_b = child.importClause) === null || _b === void 0 ? void 0 : _b.namedBindings)) {
imports.push(child.importClause.namedBindings);
}
});
return imports;
}
// Check if a function call is a tslib helper.
function isTslibHelper(callExpr, helper, tslibImports, checker) {
var _a;
if (!ts.isIdentifier(callExpr.expression) || callExpr.expression.text !== helper) {
return false;
}
const symbol = checker.getSymbolAtLocation(callExpr.expression);
if (!((_a = symbol === null || symbol === void 0 ? void 0 : symbol.declarations) === null || _a === void 0 ? void 0 : _a.length)) {
return false;
}
for (const dec of symbol.declarations) {
if (ts.isImportSpecifier(dec) && tslibImports.some((name) => name.elements.includes(dec))) {
return true;
}
// Handle inline helpers `var __decorate = (this...`
if (ts.isVariableDeclaration(dec)) {
return true;
}
}
return false;
}