diff --git a/packages/babel-plugin-minify-builtins/.npmignore b/packages/babel-plugin-minify-builtins/.npmignore new file mode 100644 index 000000000..22250660e --- /dev/null +++ b/packages/babel-plugin-minify-builtins/.npmignore @@ -0,0 +1,4 @@ +src +__tests__ +node_modules +*.log diff --git a/packages/babel-plugin-minify-builtins/README.md b/packages/babel-plugin-minify-builtins/README.md new file mode 100644 index 000000000..2e23c7e0c --- /dev/null +++ b/packages/babel-plugin-minify-builtins/README.md @@ -0,0 +1,51 @@ +# babel-plugin-minify-builtins + +Minify Standard built-in Objects + +## Example + +**In** + +```javascript +Math.floor(a) + Math.floor(b) +``` + +**Out** + +```javascript +var _Mathfloor = Math.floor; + +_Mathfloor(a) + _Mathfloor(b); +``` + +## Installation + +```sh +npm install babel-plugin-minify-builtins +``` + +## Usage + +### Via `.babelrc` (Recommended) + +**.babelrc** + +```json +{ + "plugins": ["minify-builtins"] +} +``` + +### Via CLI + +```sh +babel --plugins minify-builtins script.js +``` + +### Via Node API + +```javascript +require("babel-core").transform("code", { + plugins: ["minify-builtins"] +}); +``` diff --git a/packages/babel-plugin-minify-builtins/__tests__/__snapshots__/minify-builtins.js.snap b/packages/babel-plugin-minify-builtins/__tests__/__snapshots__/minify-builtins.js.snap new file mode 100644 index 000000000..8a69fb0ed --- /dev/null +++ b/packages/babel-plugin-minify-builtins/__tests__/__snapshots__/minify-builtins.js.snap @@ -0,0 +1,100 @@ +exports[`minify-builtins should collect and minify no matter any depth 1`] = ` +Object { + "_source": "Math.max(a, b) + Math.max(a, b); +function a (){ + Math.max(b, a); + return function b() { + const a = Math.floor(c); + Math.min(b, a) * Math.floor(b); + } +}", + "expected": "var _Mathfloor = Math.floor; +var _Mathmax = Math.max; +_Mathmax(a, b) + _Mathmax(a, b); +function a() { + _Mathmax(b, a); + return function b() { + const a = _Mathfloor(c); + Math.min(b, a) * _Mathfloor(b); + }; +}", +} +`; + +exports[`minify-builtins should evalaute expressions if applicable and optimize it 1`] = ` +Object { + "_source": "const a = Math.max(Math.floor(2), 5); +let b = 1.8; +let x = Math.floor(Math.max(a, b)); +foo(x);", + "expected": "const a = 5; +let b = 1.8; +let x = 5; +foo(x);", +} +`; + +exports[`minify-builtins should minify standard built in methods 1`] = ` +Object { + "_source": "Math.max(a, b) + Math.max(b, a); +function c() { + let a = 10; + const d = Number.isNaN(a); + return d && Number.isFinite(a); +}", + "expected": "var _Mathmax = Math.max; +_Mathmax(a, b) + _Mathmax(b, a); +function c() { + let a = 10; + const d = false; + return d && true; +}", +} +`; + +exports[`minify-builtins should minify standard built in properties 1`] = ` +Object { + "_source": "Number.NAN + Number.NAN; +function a () { + return Math.PI + Math.PI + Number.EPSILON + Number.NAN; +}", + "expected": "var _MathPI = Math.PI; +var _NumberNAN = Number.NAN; +_NumberNAN + _NumberNAN; +function a() { + return _MathPI + _MathPI + Number.EPSILON + _NumberNAN; +}", +} +`; + +exports[`minify-builtins should not evaluate if its side effecty 1`] = ` +Object { + "_source": "Math.max(foo(), 1); +Math.random();", + "expected": "Math.max(foo(), 1); +Math.random();", +} +`; + +exports[`minify-builtins should not minify for computed properties 1`] = ` +Object { + "_source": "let max = \"floor\"; +Math[max](1.5);", + "expected": "let max = \"floor\"; +Math[max](1.5);", +} +`; + +exports[`minify-builtins should take no of occurences in to account 1`] = ` +Object { + "_source": "function a() { + return Math.floor(a) + Math.floor(b) + Math.min(a, b); +} +Math.floor(a) + Math.max(a, b);", + "expected": "var _Mathfloor = Math.floor; +function a() { + return _Mathfloor(a) + _Mathfloor(b) + Math.min(a, b); +} +_Mathfloor(a) + Math.max(a, b);", +} +`; diff --git a/packages/babel-plugin-minify-builtins/__tests__/minify-builtins.js b/packages/babel-plugin-minify-builtins/__tests__/minify-builtins.js new file mode 100644 index 000000000..d18fcd4e1 --- /dev/null +++ b/packages/babel-plugin-minify-builtins/__tests__/minify-builtins.js @@ -0,0 +1,87 @@ +jest.autoMockOff(); + +const babel = require("babel-core"); +const unpad = require("../../../utils/unpad"); +const plugin = require("../src/index"); + +function transform(code) { + return babel.transform(code, { + plugins: [plugin], + }).code; +} + +describe("minify-builtins", () => { + it("should minify standard built in methods", () => { + const source = unpad(` + Math.max(a, b) + Math.max(b, a); + function c() { + let a = 10; + const d = Number.isNaN(a); + return d && Number.isFinite(a); + } + `); + // Jest arranges in alphabetical order, So keeping it as _source + expect({_source: source, expected: transform(source)}).toMatchSnapshot(); + }); + + it("should minify standard built in properties", () => { + const source = unpad(` + Number.NAN + Number.NAN; + function a () { + return Math.PI + Math.PI + Number.EPSILON + Number.NAN; + } + `); + expect({_source: source, expected: transform(source)}).toMatchSnapshot(); + }); + + it("should take no of occurences in to account", () => { + const source = unpad(` + function a() { + return Math.floor(a) + Math.floor(b) + Math.min(a, b); + } + Math.floor(a) + Math.max(a, b); + `); + expect({_source: source, expected: transform(source)}).toMatchSnapshot(); + }); + + it("should collect and minify no matter any depth", () => { + const source = unpad(` + Math.max(a, b) + Math.max(a, b); + function a (){ + Math.max(b, a); + return function b() { + const a = Math.floor(c); + Math.min(b, a) * Math.floor(b); + } + } + `); + expect({_source: source, expected: transform(source)}).toMatchSnapshot(); + }); + + it("should evalaute expressions if applicable and optimize it", () => { + const source = unpad(` + const a = Math.max(Math.floor(2), 5); + let b = 1.8; + let x = Math.floor(Math.max(a, b)); + foo(x); + `); + expect({_source: source, expected: transform(source)}).toMatchSnapshot(); + }); + + it("should not evaluate if its side effecty", () => { + const source = unpad(` + Math.max(foo(), 1); + Math.random(); + `); + expect({_source: source, expected: transform(source)}).toMatchSnapshot(); + }); + + it("should not minify for computed properties", () => { + const source = unpad(` + let max = "floor"; + Math[max](1.5); + `); + expect({_source: source, expected: transform(source)}).toMatchSnapshot(); + }); + +}); diff --git a/packages/babel-plugin-minify-builtins/package.json b/packages/babel-plugin-minify-builtins/package.json new file mode 100644 index 000000000..9af2775ab --- /dev/null +++ b/packages/babel-plugin-minify-builtins/package.json @@ -0,0 +1,19 @@ +{ + "name": "babel-plugin-minify-builtins", + "version": "0.0.1", + "description": "Minify Standard built-in Objects", + "homepage": "https://github.com/babel/babili#readme", + "repository": "https://github.com/babel/babili/tree/master/packages/babel-plugin-minify-builtins", + "main": "lib/index.js", + "bugs": "https://github.com/babel/babili/issues", + "keywords": [ + "babel-plugin", + "transform-built-ins" + ], + "author": "Vignesh Shanmugam (https://vigneshh.in)", + "license": "MIT", + "dependencies": { + "babel-helper-evaluate-path": "^0.0.3" + }, + "devDependencies": {} +} diff --git a/packages/babel-plugin-minify-builtins/src/index.js b/packages/babel-plugin-minify-builtins/src/index.js new file mode 100644 index 000000000..4f0921400 --- /dev/null +++ b/packages/babel-plugin-minify-builtins/src/index.js @@ -0,0 +1,137 @@ +"use strict"; + +const evaluate = require("babel-helper-evaluate-path"); +// Assuming all the static methods from below array are side effect free evaluation +// except Math.random +const VALID_CALLEES = ["String", "Number", "Math"]; +const INVALID_METHODS = ["random"]; + +module.exports = function({ types: t }) { + + class BuiltInReplacer { + constructor(program) { + this.program = program; + this.pathsToUpdate = new Map; + } + + run() { + this.collect(); + this.replace(); + } + + collect() { + const context = this; + + const collectVisitor = { + MemberExpression(path) { + if (path.parentPath.isCallExpression()) { + return; + } + + const expName = memberToString(path); + if (!isComputed(path) && isBuiltin(path)) { + if (!context.pathsToUpdate.has(expName)) { + context.pathsToUpdate.set(expName, []); + } + context.pathsToUpdate.get(expName).push(path); + } + }, + + CallExpression: { + exit(path) { + const callee = path.get("callee"); + if (!callee.isMemberExpression()) { + return; + } + + const expName = memberToString(callee); + // computed property should be not optimized + // Math[max]() -> Math.max() + if (!isComputed(callee) && isBuiltin(callee)) { + const result = evaluate(path); + // deopt when we have side effecty evaluate-able arguments + // Math.max(foo(), 1) --> untouched + // Math.floor(1) --> 1 + if (result.confident && hasPureArgs(path)) { + path.replaceWith(t.valueToNode(result.value)); + } else { + if (!context.pathsToUpdate.has(expName)) { + context.pathsToUpdate.set(expName, []); + } + context.pathsToUpdate.get(expName).push(callee); + } + } + } + } + }; + + this.program.traverse(collectVisitor); + } + + replace() { + for (const [ expName, paths ] of this.pathsToUpdate) { + // Should only transform if there is more than 1 occurence + if (paths.length > 1) { + const uniqueIdentifier = this.program.scope.generateUidIdentifier(expName); + const newNode = t.variableDeclaration("var", [ + t.variableDeclarator(uniqueIdentifier, paths[0].node) + ]); + + for (const path of paths) { + path.replaceWith(uniqueIdentifier); + } + // hoist the created var to top of the program + this.program.unshiftContainer("body", newNode); + } + } + } + } + + return { + name: "minify-builtins", + visitor: { + Program(path) { + const builtInReplacer = new BuiltInReplacer(path); + builtInReplacer.run(); + } + }, + }; + + function memberToString(memberExpr) { + const { object, property } = memberExpr.node; + let result = ""; + + if (t.isIdentifier(object)) result += object.name; + if (t.isMemberExpression(object)) result += memberToString(object); + if (t.isIdentifier(property)) result += property.name; + + return result; + } + + function isBuiltin(memberExpr) { + const { object, property } = memberExpr.node; + + if (t.isIdentifier(object) && t.isIdentifier(property) + && VALID_CALLEES.indexOf(object.name) >= 0 + && INVALID_METHODS.indexOf(property.name) < 0) { + return true; + } + return false; + } + +}; + +function hasPureArgs(path) { + const args = path.get("arguments"); + for (const arg of args) { + if (!arg.isPure()) { + return false; + } + } + return true; +} + +function isComputed(path) { + const { node } = path; + return node.computed; +} diff --git a/packages/babel-preset-babili/__tests__/__snapshots__/options-tests.js.snap b/packages/babel-preset-babili/__tests__/__snapshots__/options-tests.js.snap index 3dad35702..7f58a6ae9 100644 --- a/packages/babel-preset-babili/__tests__/__snapshots__/options-tests.js.snap +++ b/packages/babel-preset-babili/__tests__/__snapshots__/options-tests.js.snap @@ -11,6 +11,7 @@ Object { "babel-plugin-minify-numeric-literals", "babel-plugin-minify-replace", "babel-plugin-minify-simplify", + "babel-plugin-minify-builtins", "babel-plugin-transform-inline-consecutive-adds", "babel-plugin-transform-member-expression-literals", "babel-plugin-transform-property-literals", @@ -41,6 +42,7 @@ Object { "babel-plugin-minify-numeric-literals", "babel-plugin-minify-replace", "babel-plugin-minify-simplify", + "babel-plugin-minify-builtins", "babel-plugin-transform-inline-consecutive-adds", "babel-plugin-transform-member-expression-literals", "babel-plugin-transform-property-literals", @@ -83,6 +85,7 @@ Object { "babel-plugin-minify-numeric-literals", "babel-plugin-minify-replace", "babel-plugin-minify-simplify", + "babel-plugin-minify-builtins", "babel-plugin-transform-inline-consecutive-adds", "babel-plugin-transform-member-expression-literals", "babel-plugin-transform-property-literals", @@ -135,6 +138,7 @@ Object { "babel-plugin-minify-numeric-literals", "babel-plugin-minify-replace", "babel-plugin-minify-simplify", + "babel-plugin-minify-builtins", "babel-plugin-transform-inline-consecutive-adds", "babel-plugin-transform-member-expression-literals", "babel-plugin-transform-property-literals", @@ -187,6 +191,7 @@ Object { "babel-plugin-minify-numeric-literals", "babel-plugin-minify-replace", "babel-plugin-minify-simplify", + "babel-plugin-minify-builtins", "babel-plugin-transform-inline-consecutive-adds", "babel-plugin-transform-member-expression-literals", "babel-plugin-transform-property-literals", @@ -215,6 +220,7 @@ Object { "babel-plugin-minify-numeric-literals", "babel-plugin-minify-replace", "babel-plugin-minify-simplify", + "babel-plugin-minify-builtins", "babel-plugin-transform-inline-consecutive-adds", "babel-plugin-transform-member-expression-literals", "babel-plugin-transform-property-literals", @@ -257,6 +263,7 @@ Object { "babel-plugin-minify-numeric-literals", "babel-plugin-minify-replace", "babel-plugin-minify-simplify", + "babel-plugin-minify-builtins", "babel-plugin-transform-inline-consecutive-adds", "babel-plugin-transform-member-expression-literals", "babel-plugin-transform-property-literals", @@ -282,6 +289,7 @@ Array [ "babel-plugin-minify-numeric-literals", "babel-plugin-minify-replace", "babel-plugin-minify-simplify", + "babel-plugin-minify-builtins", "babel-plugin-transform-inline-consecutive-adds", "babel-plugin-transform-member-expression-literals", "babel-plugin-transform-property-literals", @@ -306,6 +314,7 @@ Array [ "babel-plugin-minify-numeric-literals", "babel-plugin-minify-replace", "babel-plugin-minify-simplify", + "babel-plugin-minify-builtins", "babel-plugin-transform-inline-consecutive-adds", "babel-plugin-transform-member-expression-literals", "babel-plugin-transform-property-literals", @@ -330,6 +339,7 @@ Array [ "babel-plugin-minify-numeric-literals", "babel-plugin-minify-replace", "babel-plugin-minify-simplify", + "babel-plugin-minify-builtins", "babel-plugin-transform-inline-consecutive-adds", "babel-plugin-transform-member-expression-literals", "babel-plugin-transform-property-literals", diff --git a/packages/babel-preset-babili/__tests__/options-tests.js b/packages/babel-preset-babili/__tests__/options-tests.js index 2c83098e0..e67b1a1fb 100644 --- a/packages/babel-preset-babili/__tests__/options-tests.js +++ b/packages/babel-preset-babili/__tests__/options-tests.js @@ -1,6 +1,7 @@ jest.autoMockOff(); const mocks = [ + "babel-plugin-minify-builtins", "babel-plugin-minify-constant-folding", "babel-plugin-minify-dead-code-elimination", "babel-plugin-minify-flip-comparisons", @@ -21,7 +22,7 @@ const mocks = [ "babel-plugin-transform-remove-debugger", "babel-plugin-transform-remove-undefined", "babel-plugin-transform-simplify-comparison-operators", - "babel-plugin-transform-undefined-to-void", + "babel-plugin-transform-undefined-to-void" ]; mocks.forEach((mockName) => { diff --git a/packages/babel-preset-babili/package.json b/packages/babel-preset-babili/package.json index 383b8ae41..4ccd821a7 100644 --- a/packages/babel-preset-babili/package.json +++ b/packages/babel-preset-babili/package.json @@ -12,6 +12,7 @@ "babel-preset" ], "dependencies": { + "babel-plugin-minify-builtins": "^0.0.1", "babel-plugin-minify-constant-folding": "^0.0.4", "babel-plugin-minify-dead-code-elimination": "^0.1.3", "babel-plugin-minify-flip-comparisons": "^0.0.2", diff --git a/packages/babel-preset-babili/src/index.js b/packages/babel-preset-babili/src/index.js index 1ae6ba704..837585cb6 100644 --- a/packages/babel-preset-babili/src/index.js +++ b/packages/babel-preset-babili/src/index.js @@ -26,6 +26,7 @@ const PLUGINS = [ ["simplifyComparisons", require("babel-plugin-transform-simplify-comparison-operators"), true], ["typeConstructors", require("babel-plugin-minify-type-constructors"), true], ["undefinedToVoid", require("babel-plugin-transform-undefined-to-void"), true], + ["builtIns", require("babel-plugin-minify-builtins"), true], ]; module.exports = preset; @@ -66,6 +67,7 @@ function preset(context, _opts = {}) { optionsMap.numericLiterals, optionsMap.replace, optionsMap.simplify, + optionsMap.builtIns, group("properties", [ optionsMap.consecutiveAdds,