babel-plugin-transform-remove-polyfill
Version:
Babel plugin that removes polyfills and transforms feature detection patterns for modern JavaScript environments
314 lines (306 loc) • 13.5 kB
JavaScript
'use strict';
const isMember = node => 'MemberExpression' === node.type && false === node.computed;
const isIdent = node => 'Identifier' === node.type;
const isIdentName = (node, name) => 'Identifier' === node.type && node.name === name;
const isString = node => 'StringLiteral' === node.type;
const isBoolean = node => 'BooleanLiteral' === node.type;
const isNumeric = (node, value) => 'NumericLiteral' === node.type && node.value === value;
const isPrototype = node => isMember(node) && isIdentName(node.property, 'prototype') && isIdent(node.object);
const isUnary = (node, operator) => 'UnaryExpression' === node.type && node.operator === operator;
const isUndefined = node => isIdentName(node, 'undefined') || isUnary(node, 'void') && isNumeric(node.argument, 0);
const bool = value => ({
type: 'BooleanLiteral',
value
});
const matchesPattern = path => {
const parts = path.split('.').reverse();
const length = parts.length;
return node => {
if (!isMember(node) || !isIdentName(node.property, parts[0])) {
return false;
}
let index = 1;
for (; index < length && isMember(node = node.object); index++) {
if (!isIdentName(node.property, parts[index])) {
return false;
}
}
return index + 1 === length && isIdentName(node, parts[index]);
};
};
const transformerCallExpression = options => {
const memberExpression = (object, property) => ({
type: 'MemberExpression',
object: {
type: 'Identifier',
name: object
},
property: {
type: 'Identifier',
name: property
},
computed: false,
optional: false
});
const isObjectHasOwn = matchesPattern('Object.prototype.hasOwnProperty.call');
const transformers = [(ident, node) => {
if (2 === node.arguments.length && isObjectHasOwn(node.callee) && ident.isGlobal(node.callee.object)) {
node.callee = memberExpression('Object', 'hasOwn');
return true;
}
return false;
}];
if (null == options || false === options) {
return transformers;
}
const useAll = true === options;
if (useAll || !!options['optimize:Object.assign']) {
const isObjectAssign = matchesPattern('Object.assign');
transformers.push((ident, node) => {
if (node.arguments.length > 1 && isObjectAssign(node.callee) && ident.isGlobal(node.callee.object)) {
const arg = node.arguments[0];
if ('CallExpression' === arg.type && isObjectAssign(arg.callee)) {
node.arguments.splice(0, 1, ...arg.arguments);
}
return true;
}
return false;
});
}
return transformers;
};
const cache = new WeakMap();
class GlobalIdentifier {
#scope;
constructor(path) {
this.#scope = path.scope;
}
#setScope() {
const map = new Map();
cache.set(this.#scope, map);
return map;
}
isGlobal(ident) {
const id = ident.name;
const map = cache.get(this.#scope) ?? this.#setScope();
const cached = map.get(id);
if ('boolean' === typeof cached) {
return cached;
}
const result = !this.#scope.getBinding(id);
map.set(id, result);
return result;
}
}
const arrayLike = ['Int8Array', 'Int16Array', 'Int32Array', 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Uint8ClampedArray', 'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array'];
const arrayLikeMethods = ['toString', 'join', 'reverse', 'slice', 'sort', 'indexOf', 'lastIndexOf', 'every', 'some', 'forEach', 'map', 'filter', 'reduce', 'reduceRight', 'find', 'findIndex', 'fill', 'copyWithin', 'entries', 'keys', 'values', 'at', 'includes'];
const arrayLikeConstructor = new Set(['of', 'from']);
const arrayLikePrototype = new Set(arrayLikeMethods);
const literals = new Set(['NumericLiteral', 'NullLiteral', 'BooleanLiteral', 'BigIntLiteral']);
const wellKnownSymbols = new Set(['unscopables', 'iterator', 'toPrimitive', 'isConcatSpreadable', 'toStringTag', 'hasInstance', 'match', 'replace', 'search', 'split', 'species', 'asyncIterator', 'matchAll']);
const builtInConstructor = new Set(['Blob', 'ArrayBuffer', 'Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array', 'DataView', 'URL', 'Promise', 'WeakMap', 'WeakSet', 'Set', 'Map', 'Symbol', 'Proxy', 'URLSearchParams', 'BigInt', 'BigInt64Array', 'BigUint64Array', 'WeakRef', 'FinalizationRegistry', 'AggregateError']);
const builtInMember = new Set(['Math', 'JSON', 'Intl', 'Reflect', 'Atomics', 'globalThis']);
const keys = new Map([...arrayLike.map(i => [i, arrayLikeConstructor]), ['Object', new Set(['create', 'keys', 'getPrototypeOf', 'defineProperty', 'defineProperties', 'getOwnPropertyDescriptor', 'getOwnPropertyNames', 'freeze', 'isFrozen', 'seal', 'isSealed', 'isExtensible', 'preventExtensions', 'is', 'setPrototypeOf', 'getOwnPropertySymbols', 'assign', 'values', 'entries', 'getOwnPropertyDescriptors', 'fromEntries', 'hasOwn'])], ['String', new Set(['fromCharCode', 'fromCodePoint', 'raw'])], ['Array', new Set(['isArray', 'of', 'from'])], ['Symbol', new Set(['for', 'keyFor'])], ['Reflect', new Set(['apply', 'construct', 'defineProperty', 'deleteProperty', 'get', 'getOwnPropertyDescriptor', 'getPrototypeOf', 'has', 'isExtensible', 'ownKeys', 'preventExtensions', 'set', 'setPrototypeOf'])], ['ArrayBuffer', new Set(['isView'])], ['Promise', new Set(['all', 'race', 'reject', 'resolve', 'allSettled', 'any'])], ['Math', new Set(['abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp', 'floor', 'log', 'max', 'min', 'pow', 'random', 'round', 'sin', 'sqrt', 'tan', 'imul', 'acosh', 'asinh', 'atanh', 'cbrt', 'clz32', 'cosh', 'expm1', 'fround', 'hypot', 'log10', 'log1p', 'log2', 'sign', 'sinh', 'tanh', 'trunc'])], ['Date', new Set(['now', 'parse', 'UTC'])], ['Number', new Set(['isFinite', 'isNaN', 'isInteger', 'isSafeInteger', 'parseFloat', 'parseInt'])], ['JSON', new Set(['parse', 'stringify'])], ['Proxy', new Set(['revocable'])], ['Error', new Set(['captureStackTrace'])]]);
const prototypeKeys = new Map([...arrayLike.map(i => [i, arrayLikePrototype]), ['Array', new Set([...arrayLikeMethods, 'concat', 'shift', 'unshift', 'splice', 'pop', 'push', 'toLocaleString', 'flat', 'flatMap'])], ['String', new Set(['indexOf', 'localeCompare', 'match', 'replace', 'split', 'substring', 'search', 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toUpperCase', 'toString', 'valueOf', 'trim', 'normalize', 'includes', 'startsWith', 'endsWith', 'repeat', 'codePointAt', 'padStart', 'padEnd', 'trimStart', 'trimEnd', 'trimLeft', 'trimRight', 'matchAll', 'replaceAll', 'at'])], ['Promise', new Set(['then', 'catch', 'finally'])], ['ArrayBuffer', new Set(['slice'])], ['Function', new Set(['bind'])], ['Blob', new Set(['slice', 'arrayBuffer', 'stream', 'text'])], ['DataView', new Set(['getInt8', 'setInt8', 'getInt16', 'setInt16', 'getInt32', 'setInt32', 'getUint8', 'setUint8', 'getUint16', 'setUint16', 'getUint32', 'setUint32', 'getFloat32', 'setFloat32', 'getFloat64', 'setFloat64', 'getBigInt64', 'setBigInt64', 'getBigUint64', 'setBigUint64'])]]);
const getFunctionGroup = node => {
if (isIdent(node)) {
return builtInConstructor.has(node.name) ? node : null;
}
if (!isMember(node) || !isIdent(node.property)) {
return null;
}
if (isIdent(node.object)) {
return keys.get(node.object.name)?.has(node.property.name) ? node.object : null;
}
if (isPrototype(node.object)) {
return prototypeKeys.get(node.object.object.name)?.has(node.property.name) ? node.object.object : null;
}
return null;
};
const getBuiltInMember = node => isIdent(node) && builtInMember.has(node.name) ? node : null;
const getWellKnownSymbol = node => isMember(node) && isIdentName(node.object, 'Symbol') && isIdent(node.property) && wellKnownSymbols.has(node.property.name) ? node.object : null;
class KeyChecker {
#cache;
constructor(path) {
this.#cache = new GlobalIdentifier(path);
}
isGlobalIdent(ident) {
return this.#cache.isGlobal(ident);
}
functionGroup(node) {
const ident = getFunctionGroup(node);
return null !== ident && this.#cache.isGlobal(ident);
}
isBuiltInMember(node) {
const ident = getBuiltInMember(node);
return null !== ident && this.#cache.isGlobal(ident);
}
isWellKnownSymbol(node) {
const ident = getWellKnownSymbol(node);
return null !== ident && this.#cache.isGlobal(ident);
}
}
const equalities = new Set(['==', '===', '!=', '!==']);
const matchTypeof = node => {
if (isUnary(node.left, 'typeof')) {
if (isString(node.right)) {
return {
match: true,
target: node.left.argument,
expect: node.right.value
};
}
} else if (isUnary(node.right, 'typeof') && isString(node.left)) {
return {
match: true,
target: node.right.argument,
expect: node.left.value
};
}
return {
match: false
};
};
const evaluate = (checker, node) => {
if (equalities.has(node.operator)) {
const tyof = matchTypeof(node);
if (tyof.match) {
if (checker.functionGroup(tyof.target)) {
return node.operator.charCodeAt(0) === ('function' === tyof.expect ? 61 : 33);
}
if (checker.isBuiltInMember(tyof.target)) {
return node.operator.charCodeAt(0) === ('object' === tyof.expect ? 61 : 33);
}
if (checker.isWellKnownSymbol(tyof.target)) {
return node.operator.charCodeAt(0) === ('symbol' === tyof.expect ? 61 : 33);
}
} else if (isUndefined(node.left) && checker.functionGroup(node.right) || isUndefined(node.right) && checker.functionGroup(node.left)) {
return 33 === node.operator.charCodeAt(0);
}
} else if ('in' === node.operator && isString(node.left)) {
const key = node.left.value;
if (isIdent(node.right)) {
if (keys.get(node.right.name)?.has(key)) {
return checker.isGlobalIdent(node.right);
}
} else if (isPrototype(node.right)) {
const name = node.right.object.name;
if (prototypeKeys.get(name)?.has(key)) {
return checker.isGlobalIdent(node.right.object);
}
if ('Symbol' === name && 'description' === key) {
return checker.isGlobalIdent(node.right.object);
}
}
}
return null;
};
const plugin = (api, options = {}) => {
api.assertVersion(7);
if (Array.isArray(options.globalObjects)) {
options.globalObjects.forEach(key => {
builtInMember.add(key);
});
}
if (Array.isArray(options.globalFunctions)) {
options.globalFunctions.forEach(key => {
builtInConstructor.add(key);
});
}
const transformers = transformerCallExpression(options.transform);
const visitor = {
IfStatement: {
exit(path) {
const node = path.node;
const checker = new KeyChecker(path);
if (checker.functionGroup(node.test)) {
node.test = bool(true);
}
if (isBoolean(node.test)) {
if (node.test.value) {
if (null != node.alternate) {
node.alternate = undefined;
}
} else {
if (null == node.alternate) {
path.remove();
} else {
node.consequent = {
type: 'EmptyStatement'
};
}
}
}
}
},
LogicalExpression: {
exit(path) {
const node = path.node;
const checker = new KeyChecker(path);
if (checker.functionGroup(node.left) || checker.isWellKnownSymbol(node.left)) {
path.replaceWith('&&' === node.operator ? node.right : node.left);
} else if (isBoolean(node.left)) {
path.replaceWith('&&' === node.operator ? node.left.value ? node.right : node.left : '||' === node.operator ? node.left.value ? node.left : node.right : node.left);
}
}
},
ConditionalExpression: {
exit(path) {
const node = path.node;
const checker = new KeyChecker(path);
if (checker.functionGroup(node.test)) {
path.replaceWith(node.consequent);
} else if (isBoolean(node.test)) {
path.replaceWith(node.test.value ? node.consequent : node.alternate);
}
}
},
UnaryExpression: {
exit(path) {
const node = path.node;
if ('!' !== node.operator) {
return;
}
const checker = new KeyChecker(path);
if (checker.functionGroup(node.argument) || checker.isWellKnownSymbol(node.argument)) {
path.replaceWith(bool(false));
} else if (isBoolean(node.argument)) {
path.replaceWith(bool(!node.argument.value));
}
}
},
BinaryExpression(path) {
const checker = new KeyChecker(path);
const value = evaluate(checker, path.node);
if (null !== value) {
path.replaceWith(bool(value));
}
},
ExpressionStatement: {
exit(path) {
const exp = path.node.expression;
const checker = new KeyChecker(path);
if (checker.functionGroup(exp) || literals.has(exp.type)) {
path.remove();
}
}
}
};
if (transformers.length > 0) {
visitor.CallExpression = {
exit(path) {
const ident = new GlobalIdentifier(path);
for (let i = 0; i < transformers.length;) {
if (transformers[i++](ident, path.node)) {
return;
}
}
}
};
}
return {
name: 'transform-remove-polyfill',
visitor
};
};
module.exports = plugin;