Skip to content

Commit

Permalink
Consolidate and change strategy with the export formatters.
Browse files Browse the repository at this point in the history
Now there are two formatters, since module-variable has been removed:

- commonjs: This is essentially unchanged. It still does rewriting of
  references to imports, but does not rewrite references to exports or
  locally-declared variables & functions.
- bundle: This is almost the same as what was the 'export-variable'
  formatter. The difference is that all locally-declared variables &
  functions at the top level will be renamed to avoid collision with
  top-level declarations in other modules. We do this because we've
  dropped the IFFEs that previously surrounded each module's contents,
  which will hopefully allow for even better tree-shaking and
  minification.
  • Loading branch information
eventualbuddha committed Jun 12, 2014
1 parent 0a5275e commit 16cb321
Show file tree
Hide file tree
Showing 11 changed files with 379 additions and 609 deletions.
3 changes: 1 addition & 2 deletions lib/cli/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,7 @@ function usage(puts) {
puts(bold('Formats'));
puts();
puts(' commonjs - convert modules to files using CommonJS `require` and `exports` objects.');
puts(' module-variable - concatenate modules optimizing for runtime performance.');
puts(' export-variable - concatenate modules optimizing for compressibility (enables better tree-shaking).');
puts(' bundle - concatenate modules into a single file.');
puts();
puts(' You may provide custom a formatter by passing the path to your module to the `--format` option. See the');
puts(' source of any of the built-in formatters for details on how to build your own.');
Expand Down
5 changes: 2 additions & 3 deletions lib/formatters.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* jshint node:true, undef:true, unused:true */

exports.DEFAULT = 'module-variable';
exports['module-variable'] = require('./formatters/module_variable_formatter');
exports['export-variable'] = require('./formatters/export_variable_formatter');
exports.DEFAULT = 'bundle';
exports.bundle = require('./formatters/bundle_formatter');
exports.commonjs = require('./formatters/commonjs_formatter');
315 changes: 315 additions & 0 deletions lib/formatters/bundle_formatter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
/* jshint node:true, undef:true, unused:true */

var recast = require('recast');
var types = recast.types;
var b = types.builders;
var n = types.namedTypes;

var Replacement = require('../replacement');
var utils = require('../utils');
var IFFE = utils.IFFE;
var sort = require('../sorting').sort;


/**
* The 'bundle' formatter aims to increase the compressibility of the generated
* source, especially by tools such as Google Closure Compiler or UglifyJS.
* For example, given these modules:
*
* // a.js
* import { b } from './b';
* console.log(b);
*
* // b.js
* export var b = 3;
* export var b2 = 6;
*
* The final output will be a single file looking something like this:
*
* (function() {
* "use strict";
* // b.js
* var b$$b = 3;
* var b$$b2 = 6;
*
* // a.js
* console.log(b$$b);
* }).call(this);
*
* @constructor
*/
function BundleFormatter() {}

/**
* This hook is called by the container before it converts its modules. We use
* it to ensure all of the imports are included because we need to know about
* them at compile time.
*
* @param {Container} container
*/
BundleFormatter.prototype.beforeConvert = function(container) {
container.findImportedModules();
};

/**
* Returns an expression which globally references the export named by
* `identifier` for the given module `mod`.
*
* @param {Module} mod
* @param {ast-types.Identifier|string} identifier
* @return {ast-types.MemberExpression}
*/
BundleFormatter.prototype.reference = function(/* mod, identifier */) {
throw new Error('#reference must be implemented in subclasses');
};

/**
* Process a variable declaration found at the top level of the module. We need
* to ensure that exported variables are rewritten appropriately, so we may
* need to rewrite some or all of this variable declaration. For example:
*
* var a = 1, b, c = 3;
* ...
* export { a, b };
*
* We turn those being exported into assignments as needed, e.g.
*
* var c = 3;
* mod$$a = 1;
*
* @param {Module} mod
* @param {ast-types.NodePath} nodePath
* @return {?Replacement}
*/
BundleFormatter.prototype.processVariableDeclaration = function(mod, nodePath) {
var self = this;
return Replacement.map(
nodePath.get('declarations'),
function(declaratorPath) {
return Replacement.swaps(
declaratorPath.get('id'),
self.reference(mod, declaratorPath.get('id').node)
);
}
);
};

/**
* Rename the top-level function declaration to a unique name.
*
* function foo() {}
*
* Becomes e.g.
*
* function mod$$foo() {}
*
* @param {Module} mod
* @param {ast-types.NodePath} nodePath
* @return {?Replacement}
*/
BundleFormatter.prototype.processFunctionDeclaration = function(mod, nodePath) {
return Replacement.swaps(
nodePath.get('id'),
this.reference(mod, nodePath.node.id)
);
};

/**
* Replaces non-default exports. Exported bindings do not need to be
* replaced with actual statements since they only control how local references
* are renamed within the module.
*
* @param {Module} mod
* @param {ast-types.NodePath} nodePath
* @return {?Replacement}
*/
BundleFormatter.prototype.processExportDeclaration = function(mod, nodePath) {
var node = nodePath.node;
if (n.FunctionDeclaration.check(node.declaration)) {
return Replacement.swaps(
// drop `export`
nodePath, node.declaration
).and(
// transform the function
this.processFunctionDeclaration(mod, nodePath.get('declaration'))
);
} else if (n.VariableDeclaration.check(node.declaration)) {
return Replacement.swaps(
// drop `export`
nodePath, node.declaration
).and(
// transform the variables
this.processVariableDeclaration(mod, nodePath.get('declaration'))
);
} else if (node.declaration) {
throw new Error(
'unexpected export style, found a declaration of type: ' +
node.declaration.type
);
} else {
/**
* This node looks like this:
*
* export { foo, bar };
*
* Which means that it has no value in the generated code as its only
* function is to control how imports are rewritten.
*/
return Replacement.removes(nodePath);
}
};

/**
* Since import declarations only control how we rewrite references we can just
* remove them -- they don't turn into any actual statements.
*
* @param {Module} mod
* @param {ast-types.NodePath} nodePath
* @return {?Replacement}
*/
BundleFormatter.prototype.processImportDeclaration = function(mod, nodePath) {
return Replacement.removes(nodePath);
};

/**
* Get a reference to the original exported value referenced in `mod` at
* `referencePath`. If the given reference path does not correspond to an
* export, we do not need to rewrite the reference. For example, since `value`
* is not exported it does not need to be rewritten:
*
* // a.js
* var value = 99;
* console.log(value);
*
* If `value` was exported then we would need to rewrite it:
*
* // a.js
* export var value = 3;
* console.log(value);
*
* In this case we re-write both `value` references to something like
* `a$$value`. The tricky part happens when we re-export an imported binding:
*
* // a.js
* export var value = 11;
*
* // b.js
* import { value } from './a';
* export { value };
*
* // c.js
* import { value } from './b';
* console.log(value);
*
* The `value` reference in a.js will be rewritten as something like `a$$value`
* as expected. The `value` reference in c.js will not be rewritten as
* `b$$value` despite the fact that it is imported from b.js. This is because
* we must follow the binding through to its import from a.js. Thus, our
* `value` references will both be rewritten to `a$$value` to ensure they
* match.
*
* @param {Module} mod
* @param {ast-types.NodePath} referencePath
* @return {ast-types.Expression}
*/
BundleFormatter.prototype.exportedReference = function(mod, referencePath) {
var specifier = mod.exports.findSpecifierForReference(referencePath);
if (specifier) {
specifier = specifier.terminalExportSpecifier;
return this.reference(specifier.module, specifier.name);
} else {
return null;
}
};

/**
* Get a reference to the original exported value referenced in `mod` at
* `referencePath`. This is very similar to {#exportedReference} in its
* approach.
*
* @param {Module} mod
* @param {ast-types.NodePath} referencePath
* @return {ast-types.Expression}
* @see {#exportedReference}
*/
BundleFormatter.prototype.importedReference = function(mod, referencePath) {
var specifier = mod.imports.findSpecifierForReference(referencePath);
if (specifier) {
specifier = specifier.terminalExportSpecifier;
return this.reference(specifier.module, specifier.name);
} else {
return null;
}
};

/**
* If the given reference has a local declaration at the top-level then we must
* rewrite that reference to have a module-scoped name.
*
* @param {Module} mod
* @param {ast-types.NodePath} referencePath
* @returns {?ast-types.Node}
*/
BundleFormatter.prototype.localReference = function(mod, referencePath) {
var scope = referencePath.scope.lookup(referencePath.node.name);
if (scope && scope.isGlobal) {
return this.reference(mod, referencePath.node);
} else {
return null;
}
};

/**
* Convert a list of ordered modules into a list of files.
*
* @param {Array.<Module>} modules Modules in execution order.
* @return {Array.<ast-types.File}
*/
BundleFormatter.prototype.build = function(modules) {
modules = sort(modules);
return [b.file(b.program([b.expressionStatement(IFFE(
b.expressionStatement(b.literal('use strict')),
modules.length === 1 ?
modules[0].ast.program.body :
modules.reduce(function(statements, mod) {
return statements.concat(mod.ast.program.body);
}, [])
))]))];
};

/**
* @param {Module} mod
* @param {ast-types.Expression} declaration
* @return {ast-types.Statement}
*/
BundleFormatter.prototype.defaultExport = function(mod, declaration) {
return b.variableDeclaration(
'var',
[b.variableDeclarator(
this.reference(mod, 'default'),
declaration
)]
);
};

/**
* Returns an expression which globally references the export named by
* `identifier` for the given module `mod`. For example:
*
* // rsvp/defer.js, export default
* rsvp$defer$$default
*
* // rsvp/utils.js, export function isFunction
* rsvp$utils$$isFunction
*
* @param {Module} mod
* @param {ast-types.Identifier|string} identifier
* @return {ast-types.Identifier}
*/
BundleFormatter.prototype.reference = function(mod, identifier) {
return b.identifier(
mod.id + (n.Identifier.check(identifier) ? identifier.name : identifier)
);
};

module.exports = BundleFormatter;
11 changes: 11 additions & 0 deletions lib/formatters/commonjs_formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ CommonJSFormatter.prototype.importedReference = function(mod, referencePath) {
}
};

/**
* We do not need to rewrite references to local declarations.
*
* @param {Module} mod
* @param {ast-types.NodePath} referencePath
* @returns {?ast-types.Node}
*/
CommonJSFormatter.prototype.localReference = function(mod, referencePath) {
return null;
};

/**
* @param {Module} mod
* @param {ast-types.Expression} declaration
Expand Down
Loading

0 comments on commit 16cb321

Please sign in to comment.