Skip to content

Commit

Permalink
Merge pull request #309 from modosc/tree-shaking-2
Browse files Browse the repository at this point in the history
Refactor mark declarations and add #__PURE__ comments (#286) for better tree-shakability.
  • Loading branch information
benjamn committed Aug 15, 2017
2 parents 9b6ebc0 + 4848531 commit 10cbffe
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 45 deletions.
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -56,7 +56,8 @@
"lerna": "^2.0.0",
"mocha": "^2.3.4",
"promise": "^7.0.4",
"semver": "^5.0.3"
"semver": "^5.0.3",
"uglify-js": "^3.0.27"
},
"license": "BSD",
"engines": {
Expand Down
89 changes: 45 additions & 44 deletions packages/regenerator-transform/src/visit.js
Expand Up @@ -8,15 +8,15 @@
* the same directory.
*/

"use strict";

import assert from "assert";
import * as t from "babel-types";
import { hoist } from "./hoist";
import { Emitter } from "./emit";
import replaceShorthandObjectMethod from "./replaceShorthandObjectMethod";
import * as util from "./util";

let getMarkInfo = require("private").makeAccessor();

exports.name = "regenerator-transform";

exports.visitor = {
Expand Down Expand Up @@ -153,6 +153,7 @@ exports.visitor = {

if (wasGeneratorFunction && t.isExpression(node)) {
util.replaceWithOrRemove(path, t.callExpression(util.runtimeProperty("mark"), [node]))
path.addComment("leading", "#__PURE__");
}

// Generators are processed in 'exit' handlers so that regenerator only has to run on
Expand All @@ -179,58 +180,58 @@ function getOuterFnExpr(funPath) {

if (node.generator && // Non-generator functions don't need to be marked.
t.isFunctionDeclaration(node)) {
let pp = funPath.findParent(function (path) {
return path.isProgram() || path.isBlockStatement();
});
// Return the identifier returned by runtime.mark(<node.id>).
return getMarkedFunctionId(funPath);
}

if (!pp) {
return node.id;
}
return node.id;
}

let markDecl = getRuntimeMarkDecl(pp);
let markedArray = markDecl.declarations[0].id;
let funDeclIdArray = markDecl.declarations[0].init.callee.object;
t.assertArrayExpression(funDeclIdArray);
const getMarkInfo = require("private").makeAccessor();

let index = funDeclIdArray.elements.length;
funDeclIdArray.elements.push(node.id);
function getMarkedFunctionId(funPath) {
const node = funPath.node;
t.assertIdentifier(node.id);

return t.memberExpression(
markedArray,
t.numericLiteral(index),
true
);
}
const blockPath = funPath.findParent(function (path) {
return path.isProgram() || path.isBlockStatement();
});

return node.id;
}
if (!blockPath) {
return node.id;
}

function getRuntimeMarkDecl(blockPath) {
let block = blockPath.node;
const block = blockPath.node;
assert.ok(Array.isArray(block.body));

let info = getMarkInfo(block);
if (info.decl) {
return info.decl;
const info = getMarkInfo(block);
if (!info.decl) {
info.decl = t.variableDeclaration("var", []);
blockPath.unshiftContainer("body", info.decl);
info.declPath = blockPath.get("body.0");
}

info.decl = t.variableDeclaration("var", [
t.variableDeclarator(
blockPath.scope.generateUidIdentifier("marked"),
t.callExpression(
t.memberExpression(
t.arrayExpression([]),
t.identifier("map"),
false
),
[util.runtimeProperty("mark")]
)
)
]);

blockPath.unshiftContainer("body", info.decl);

return info.decl;
assert.strictEqual(info.declPath.node, info.decl);

// Get a new unique identifier for our marked variable.
const markedId = blockPath.scope.generateUidIdentifier("marked");
const markCallExp = t.callExpression(
util.runtimeProperty("mark"),
[node.id]
);

const index = info.decl.declarations.push(
t.variableDeclarator(markedId, markCallExp)
) - 1;

const markCallExpPath =
info.declPath.get("declarations." + index + ".init");

assert.strictEqual(markCallExpPath.node, markCallExp);

markCallExpPath.addComment("leading", "#__PURE__");

return markedId;
}

function renameArguments(funcPath, argsId) {
Expand Down
184 changes: 184 additions & 0 deletions test/tests.transform.js
Expand Up @@ -3,6 +3,9 @@ var recast = require("recast");
var types = recast.types;
var n = types.namedTypes;
var transform = require("..").transform;
var compile = require("..").compile;

var UglifyJS = require("uglify-js");

describe("_blockHoist nodes", function() {
it("should be hoisted to the outer body", function() {
Expand Down Expand Up @@ -47,3 +50,184 @@ describe("_blockHoist nodes", function() {
assert.deepEqual(names, ["hoistMe", "doNotHoistMe", "oyez"]);
});
});

describe("uglifyjs dead code removal", function() {
function uglifyAndParse(file1, file2) {
var code = {
"file1.js": file1,
"file2.js": file2
};

var options = {
toplevel: true,
// don't mangle function or variable names so we can find them
mangle: false,
output: {
// make it easier to parse the output
beautify: true
}
};

// uglify our code
var result = UglifyJS.minify(code, options);

// parse and return the output
return recast.parse(result.code, {
parser: require("babylon")
});
}

it("works with function expressions", function() {
var file1 = compile([
'var foo = function* () {};',
'var bar = function* () {};'
].join("\n")).code;
var file2 = compile('console.log(foo());').code;

var ast = uglifyAndParse(file1, file2);

// the results should have a single variable declaration
var variableDeclarations = ast.program.body.filter(function(b) {
return b.type === 'VariableDeclaration';
});
assert.strictEqual(variableDeclarations.length, 1);
assert.strictEqual(variableDeclarations[0].declarations.length, 1);
var declaration = variableDeclarations[0].declarations[0];

// named foo
assert.strictEqual(declaration.id.name, 'foo');
});

it("works with function declarations", function() {
var file1 = compile([
'function* foo() {};',
'function* bar() {};'
].join("\n")).code;

var file2 = compile('console.log(foo());').code;

var ast = uglifyAndParse(file1, file2);

// the results should have our foo() function
assert.ok(ast.program.body.some(function(b) {
return b.type === 'FunctionDeclaration' && b.id.name === 'foo';
}));

// but not our bar() function
assert.ok(!ast.program.body.some(function(b) {
return b.type === 'FunctionDeclaration' && b.id.name === 'bar';
}));

// and a single mark declaration
var variableDeclarations = ast.program.body.filter(function(b) {
return b.type === 'VariableDeclaration';
});
assert.strictEqual(variableDeclarations.length, 1);
var declarations = variableDeclarations[0].declarations;
assert.strictEqual(declarations.length, 1);
var declaration = declarations[0];

// with our function name as an argument'
assert.strictEqual(declaration.init.arguments.length, 1);
assert.strictEqual(declaration.init.arguments[0].name, 'foo');
});
})

context("functions", function() {
function marksCorrectly(marked, varName) {
// marked should be a VariableDeclarator
n.VariableDeclarator.assert(marked);

// using our variable name
assert.strictEqual(marked.id.name, varName);

// assiging a call expression to regeneratorRuntime.mark()
n.CallExpression.assert(marked.init);
assert.strictEqual(marked.init.callee.object.name, 'regeneratorRuntime')
assert.strictEqual(marked.init.callee.property.name, 'mark')

// with said call expression marked as a pure function
assert.strictEqual(marked.init.leadingComments[0].value, '#__PURE__');
}

describe("function declarations", function() {
it("should work with a single function", function() {
var ast = recast.parse('function* foo(){};', {
parser: require("babylon")
});

// get our declarations
const declaration = transform(ast).program.body[0];
n.VariableDeclaration.assert(declaration);
const declarations = declaration.declarations;

// verify our declaration is marked correctly
marksCorrectly(declarations[0], '_marked');

// and has our function name as its first argument
assert.strictEqual(declarations[0].init.arguments[0].name, 'foo');
});

it("should work with multiple functions", function() {
var ast = recast.parse([
'function* foo() {};',
'function* bar() {};'
].join("\n"), {
parser: require("babylon")
});

// get our declarations
const declaration = transform(ast).program.body[0];
n.VariableDeclaration.assert(declaration);
const declarations = declaration.declarations;

// verify our declarations are marked correctly and have our function name
// as their first argument
marksCorrectly(declarations[0], '_marked');
n.Identifier.assert(declarations[0].init.arguments[0]);
assert.strictEqual(declarations[0].init.arguments[0].name, 'foo');

marksCorrectly(declarations[1], '_marked2');
n.Identifier.assert(declarations[1].init.arguments[0]);
assert.strictEqual(declarations[1].init.arguments[0].name, 'bar');
});
});

describe("function expressions", function() {
it("should work with a named function", function() {
var ast = recast.parse('var a = function* foo(){};', {
parser: require("babylon")
});

// get our declarations
const declaration = transform(ast).program.body[0];
n.VariableDeclaration.assert(declaration);
const declarator = declaration.declarations[0];

// verify our declaration is marked correctly
marksCorrectly(declarator, 'a');

// and that our first argument is our original function expression
n.FunctionExpression.assert(declarator.init.arguments[0]);
assert.strictEqual(declarator.init.arguments[0].id.name, 'foo');
});

it("should work with an anonymous function", function() {
var ast = recast.parse('var a = function* (){};', {
parser: require("babylon")
});

// get our declarations
const declaration = transform(ast).program.body[0];
n.VariableDeclaration.assert(declaration);
const declarator = declaration.declarations[0];

// verify our declaration is marked correctly
marksCorrectly(declarator, 'a');

// and that our first argument is our original function expression
n.FunctionExpression.assert(declarator.init.arguments[0]);
assert.strictEqual(declarator.init.arguments[0].id.name, '_callee');
});
});
});

0 comments on commit 10cbffe

Please sign in to comment.