Skip to content

Commit

Permalink
Implement tree-shaking plugin as interlock-dce.
Browse files Browse the repository at this point in the history
Closes #11.
  • Loading branch information
divmain committed Mar 29, 2016
1 parent 5a6f791 commit 360d7b3
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/dce/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
src/
example/
1 change: 1 addition & 0 deletions packages/dce/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# interlock-dce
5 changes: 5 additions & 0 deletions packages/dce/example/app/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { foo, biz, default as def } from "./lib";

console.log("foo", foo);
console.log("biz", biz("msg"));
console.log("default", def());
17 changes: 17 additions & 0 deletions packages/dce/example/app/lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const foo = "foo";

export function bar () {
return "bar-bar-bar";
}

export function baz (msg) {
return "baz-baz-baz " + msg;
}

export const biz = function (msg) {
return "biz-biz-biz " + baz(msg);
};

export default function () {
return "default";
}
14 changes: 14 additions & 0 deletions packages/dce/example/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
var path = require("path");

var Interlock = require("interlock");
var dce = require("..");

var ilk = new Interlock({
srcRoot: __dirname,
destRoot: path.join(__dirname, "dist"),
entry: { "./app/app.js": "app.bundle.js" },
pretty: true,
plugins: [ dce() ]
});

ilk.build();
11 changes: 11 additions & 0 deletions packages/dce/example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "interlock-dce-example",
"version": "0.0.1",
"description": "",
"main": "app/example.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Dale Bustad <[email protected]>",
"license": "MIT"
}
19 changes: 19 additions & 0 deletions packages/dce/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "interlock-dce",
"version": "0.1.0",
"description": "",
"main": "lib/index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/interlockjs/plugins.git"
},
"author": "Dale Bustad <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/interlockjs/plugins/issues"
},
"homepage": "https://github.com/interlockjs/plugins/packages/dce/#readme",
"dependencies": {
"babel-traverse": "^6.7.4"
}
}
120 changes: 120 additions & 0 deletions packages/dce/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import traverse from "babel-traverse";


export default function (/* opts = {} */) {
const metadata = {};

return (override, transform) => {

/**
* Record all ES6 exports and imports for each module.
*
* When an export is found, create a corresponding property in the
* module's `exports` object, set to `false`. These will later be
* toggled to `true` for exports that are imported.
*
* When an import is found, create a corresponding property for the
* _source_ module (whatever is in the import source string) in the
* module's `imports` object. The value of this property will be
* an array containing all imported identifiers.
*/
transform("parseModule", module => {
const moduleMetadata = metadata[module.path] = { exports: {}, imports: {} };

traverse.cheap(module.ast, node => {
// Look for named exports.
if (node.type === "ExportNamedDeclaration") {
if (node.declaration && node.declaration.type === "FunctionDeclaration") {
moduleMetadata.exports[node.declaration.id.name] = false;
} else if (node.declaration && node.declaration.type === "VariableDeclaration") {
node.declaration.declarations.forEach(decl => {
moduleMetadata.exports[decl.id.name] = false;
});
}

// Look for default export.
} else if (node.type === "ExportDefaultDeclaration") {
moduleMetadata.exports.default = false;

// Look for imports.
} else if (node.type === "ImportDeclaration") {
const imports = moduleMetadata.imports[node.source.value] =
moduleMetadata.imports[node.source.value] || [];

node.specifiers.forEach(specifier => {
imports.push(specifier.imported.name);
});

// Look for default imports.
} else if (node.type === "ImportDefaultDeclaration") {
const imports = moduleMetadata.imports[node.source.value] =
moduleMetadata.imports[node.source.value] || [];
imports.push("default");
}
});

return module;
});

/**
* First, iterate over each module's internal references (any import
* sources or require() arguments). Then, find the corresponding export
* metadata for that dependency, and mark its exports as used if
* they are imported in the current module.
*
* After this has happened for all modules, iterate over each module
* again. This time remove `exports.foo` for any "foo" that was unused.
*
* Any function/variable declarations that are 1) unexported, and
* 2) unused elsewhere in the module, will be stripped out by a minifier.
*/
transform("compileModules", modules => {
// Mark used exports.
modules.forEach(module => {
const moduleMetadata = metadata[module.path];

// Iterate over all dependencies.
Object.keys(module.dependenciesByInternalRef).forEach(internalRef => {
const dep = module.dependenciesByInternalRef[internalRef];
const depMetadata = metadata[dep.path];

// Iterate over all identifiers imported from this dependency, marking
// _its_ export as used.
moduleMetadata.imports[internalRef].forEach(identifier => {
depMetadata.exports[identifier] = true;
});
});
});

// Remove `exports.foo` for any unused exports.
modules.forEach(module => {
const shouldExport = metadata[module.path].exports;

traverse(module.ast, {
noScope: true,
enter: path => {
if (
path.node.type === "AssignmentExpression" &&
path.node.left.type === "MemberExpression" &&
path.node.left.object.name === "exports"
) {
if (!shouldExport[path.node.left.property.name]) {
// const thing = exports.thing = "thing";
// --> const thign = "thing";
// exports.foo = function () { /* ... */ };
// --> foo;
// --> function foo () { /* ... */ };
// exports.default = "default";
// --> "default";
//
path.replaceWith(path.node.right);
}
}
}
});
});

return modules;
});
};
}

0 comments on commit 360d7b3

Please sign in to comment.