Skip to content

Commit

Permalink
Massive (2.5x-4x) speedup by switching from recast to ast-types +…
Browse files Browse the repository at this point in the history
… `esprima` + `escodegen` (as we don't need to preserve formatting and comments in built file).

Added source map test suite.
Set version to 1.9.0.
Closes #7.
  • Loading branch information
RReverser committed Mar 9, 2014
1 parent a3bcab1 commit b814e31
Show file tree
Hide file tree
Showing 16 changed files with 196 additions and 118 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/.idea
/node_modules
/npm-debug.log
npm-debug.log
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ cjs.transform(options).then(function (result) {
* **map**: `String|Function(input, output)|Boolean` — source map file; optional, doesn't generate source map by default; if `true` is provided, path default to `function (input, output) { return output + '.map' }`.
* **exports**: `String|Function(input, output)` — Exports top module with [UMD](https://github.com/umdjs/umd) with given global object name; optional, doesn't wrap into UMD by default.
* **transform**: `Array|Function(input)` — Array of or single function that returns transformation [through](https://github.com/dominictarr/through)-stream(s) to be used against input files before their usage; optional.
* **dryRun**: `Boolean` — if set to `true`, doesn't write output to disk.
* **dryRun**: `Boolean` — if set to `true`, doesn't write output to disk and doesn't append `//# sourceMappingURL=...` to code so you can handle it differently on your own.

### Result object

Expand Down
10 changes: 5 additions & 5 deletions lib/astConsts.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
var recast = require('recast'),
fs = require('fs'),
b = recast.types.builders;
var astUtils = require('./astUtils'),
b = astUtils.builders,
fs = require('fs');

exports.moduleArgs = [b.identifier('module'), b.identifier('exports')];
exports.require = b.identifier('_require');
exports.factoryArgs = [b.identifier('define')];
exports.localReqModules = b.memberExpression(exports.require, b.identifier('modules'), false);

exports.preamble = recast.parse(fs.readFileSync(__dirname + '/templates/preamble.js'), {sourceFileName: 'preamble.js'}).program.body;
exports.umdWrapper = recast.parse(fs.readFileSync(__dirname + '/templates/umdWrapper.js'), {sourceFileName: 'umdWrapper.js'}).program.body[0].expression;
exports.preamble = astUtils.parse(fs.readFileSync(__dirname + '/templates/preamble.js'), {loc: true, source: 'preamble.js'}).body;
exports.umdWrapper = astUtils.parse(fs.readFileSync(__dirname + '/templates/umdWrapper.js'), {loc: true, source: 'umdWrapper.js'}).body[0].expression;
35 changes: 35 additions & 0 deletions lib/astUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
var parse = require('esprima').parse,
generate = require('escodegen').generate,
types = require('ast-types'),
traverse = types.traverse.fast;

// Polyfill until https://github.com/ariya/esprima/pull/148/files is merged into npm version

exports.parse = function (code, options) {
var ast = parse.apply(this, arguments);

if (options.source) {
traverse(ast, function (node) {
node.loc.source = options;
});
}

return ast;
};

// Workaround until https://github.com/Constellation/escodegen/issues/174 is fixed

exports.generate = function (ast, options) {
var output = generate(ast, options);

if (options.sourceMapWithCode && typeof output === 'string') {
output = {code: output};
}

return output;
};

exports.traverse = traverse;

exports.builders = types.builders;
exports.namedTypes = types.namedTypes;
File renamed without changes.
1 change: 1 addition & 0 deletions lib/promise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('davy');
22 changes: 11 additions & 11 deletions lib/pureCjs.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
var fs = require('fs'),
recast = require('recast'),
Promise = require('davy'),
astUtils = require('./astUtils'),
b = astUtils.builders,
Promise = require('./promise'),
astConsts = require('./astConsts'),
optionsParser = require('./optionsParser'),
parseOptions = require('./parseOptions'),
pathUtils = require('./pathUtils'),
ReplacerMap = require('./replacerMap'),
b = recast.types.builders,
whenWriteFile = Promise.wrap(fs.writeFile);

exports.transform = function (inOptions) {
var options = optionsParser(inOptions),
var options = parseOptions(inOptions),
map = new ReplacerMap(options),
replacer = map.get(options.input);

Expand All @@ -34,14 +34,14 @@ exports.transform = function (inOptions) {
? b.callExpression(astConsts.umdWrapper, [b.literal(options.exports), factoryExpr])
: b.callExpression(factoryExpr, [])
),
_result = recast.print(b.program([b.expressionStatement(expr)]), {sourceMapName: options.map}),
result = {
code: _result.code,
map: _result.map,
options: options
},
result = astUtils.generate(b.program([b.expressionStatement(expr)]), {
sourceMap: options.map,
sourceMapWithCode: true
}),
whenOut;

result.options = options;

if (!options.dryRun) {
if (options.map) {
result.code += '\n//# sourceMappingURL=' + pathUtils.relative(pathUtils.std.dirname(options.output), options.map);
Expand Down
95 changes: 46 additions & 49 deletions lib/replacer.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,51 @@
var recast = require('recast'),
fs = require('fs'),
Promise = require('davy'),
var fs = require('fs'),
Promise = require('./promise'),
es = require('event-stream'),
astConsts = require('./astConsts'),
pathUtils = require('./pathUtils'),
isCoreModule = require('resolve').isCore,

types = recast.types,
b = types.builders,
n = types.namedTypes;

module.exports = recast.Visitor.extend({
init: function (options) {
this.id = options.id;
this.map = options.map;
this.path = options.path;

var pipeline = [fs.createReadStream(this.path, {encoding: 'utf-8'})];

this.map.transform.forEach(function (transform) {
pipeline.push(transform(this.path));
}, this);

var promise = new Promise();
pipeline.push(es.wait(function (err, js) {
err ? promise.reject(err) : promise.fulfill(js);
}));

this.promise = promise.then(function (js) {
var ast = recast.parse(js, {sourceFileName: this.path}).program;
this.visit(ast);
return b.functionExpression(null, astConsts.moduleArgs, b.blockStatement(ast.body));
}.bind(this));

es.pipeline.apply(es, pipeline);
},

getDependency: function (path) {
return this.map.get(pathUtils.getNodePath(this.path, path));
},

visitCallExpression: function (node) {
var func = node.callee,
arg = node.arguments[0];

if (n.Identifier.check(func) && func.name === 'require' && n.Literal.check(arg) && !isCoreModule(arg.value)) {
func.name = astConsts.require.name;
arg.value = this.getDependency(arg.value).id;
astUtils = require('./astUtils'),
astConsts = require('./astConsts'),
b = astUtils.builders,
n = astUtils.namedTypes;

function Replacer(options) {
this.id = options.id;
this.map = options.map;
this.path = options.path;

var pipeline = this.map.transform.reduce(function (stream, transform) {
return stream.pipe(transform(this.path));
}, fs.createReadStream(this.path, {encoding: 'utf-8'}));

var promise = new Promise();

pipeline.pipe(es.wait(function (err, js) {
err ? promise.reject(err) : promise.fulfill(js);
}));

this.promise = promise.then(function (js) {
var ast = astUtils.parse(js, {loc: true, source: this.path});
this.visit(ast);
return b.functionExpression(null, astConsts.moduleArgs, b.blockStatement(ast.body));
}.bind(this));
}

Replacer.prototype.getDependency = function (path) {
return this.map.get(pathUtils.getNodePath(this.path, path));
};

Replacer.prototype.visit = function (ast) {
return astUtils.traverse(ast, function (node) {
if (n.CallExpression.check(node)) {
var func = node.callee,
arg = node.arguments[0];

if (n.Identifier.check(func) && func.name === 'require' && n.Literal.check(arg) && !isCoreModule(arg.value)) {
func.name = astConsts.require.name;
arg.value = this.getDependency(arg.value).id;
}
}
}.bind(this));
};

this.genericVisit(node);
}
});
module.exports = Replacer;
2 changes: 1 addition & 1 deletion lib/replacerMap.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
var Promise = require('davy'),
var Promise = require('./promise'),
Replacer = require('./replacer');

function ReplacerMap(options) {
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pure-cjs",
"version": "1.8.6",
"version": "1.9.0",
"description": "Pure minimalistic CommonJS builder",
"bin": "./bin/pure-cjs",
"main": "./lib/pureCjs",
Expand All @@ -20,8 +20,10 @@
"commander": "~2.1.0",
"resolve": "~0.6.1",
"davy": "0.0.8",
"recast": "~0.5.7",
"event-stream": "~3.1.0"
"event-stream": "~3.1.0",
"escodegen": "~1.2.0",
"ast-types": "~0.3.18",
"esprima": "~1.0.4"
},
"devDependencies": {
"nodeunit": "~0.8.6"
Expand Down
38 changes: 38 additions & 0 deletions test/suites/a (exports A with map)/expected.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/suites/a (exports A with map)/expected.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions test/suites/a (exports A with map)/options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
input: 'fixtures/a.js',
exports: 'A',
map: true
};
45 changes: 20 additions & 25 deletions test/suites/a (exports A)/expected.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,37 @@
(function (name, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like enviroments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is window)
this[name] = factory();
}
})("A", function(define) {
}
}('A', function (define) {
function _require(index) {
var module = _require.cache[index];

if (!module) {
var exports = {};
module = _require.cache[index] = {id: index, exports: exports};
module = _require.cache[index] = {
id: index,
exports: exports
};
_require.modules[index].call(exports, module, exports);
}

return module.exports;
}

_require.cache = [];

_require.modules = [function(module, exports) {
var c = _require(1),
url = require('url');

this.topValue = _require(2) * 2;
}, function(module, exports) {
var a = _require(0);
exports.value = 3;
}, function(module, exports) {
module.exports = _require(1).value * 7;
}];

_require.modules = [
function (module, exports) {
var c = _require(1), url = require('url');
this.topValue = _require(2) * 2;
},
function (module, exports) {
var a = _require(0);
exports.value = 3;
},
function (module, exports) {
module.exports = _require(1).value * 7;
}
];
return _require(0);
});
}));
Loading

0 comments on commit b814e31

Please sign in to comment.