UNPKG

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
'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;