Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul Baumgart committed Mar 27, 2012
0 parents commit b9d6ecb
Show file tree
Hide file tree
Showing 23 changed files with 782 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
node_modules/
8 changes: 8 additions & 0 deletions LICENSE
@@ -0,0 +1,8 @@
Copyright (C) 2012 ProxV, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

100 changes: 100 additions & 0 deletions README.md
@@ -0,0 +1,100 @@
# jsbundle -- Node.JS modules for the browser

**jsbundle** takes your Node modules and makes them work in the browser.

It finds all *require* calls in your code and includes the necessary module files. Then, it wraps all these modules using the Node variant of CommonJS module headers.

It handles *node\_modules* directories and *package.json* files just like Node does.

It comes with a "Dev CDN" that will serve the latest *jsbundled* version of your code via local HTTP.

## Usage

### jsbundle

jsbundle <entry_file> [config_file]

Bundle up *entry\_file* and all its dependencies, optionally using configuration specified in *config\_file*, and write it to *stdout*.

This command is basically equivalent to running:

node <entry_file>

in the sense that when the resulting script is executed in the browser, the module *entry\_file* will be the first module to begin executing.

For production deployment, you'll probably want to pipe the resulting output to the JavaScript minifier of your choice.

### devcdn

devcdn [config_file] [--port tcp_port]

Start a "Dev CDN", serving on *tcp\_port* (default 8081), optionally using configuration specified in *config\_file*.
The *entry\_file* passed to *jsbundle* is determined by the request URL, which will be resolved relative to the current working directory of *devcdn*.

## Tests

Test coverage is currently mediocre. You can run tests with:

npm test

## Caveats

* All values passed to *require* in your code must be string literals. Otherwise, *jsbundle* wouldn't be able to reliably find all the modules to include in the bundled output.

* The special variable *\_\_filename* is equal to the *module.id*. If you specify the *mangleNames* option (see below), then the *\_\_filename* will be the mangled numeric id of the module.

* The special variable *\_\_dirname* doesn't really make sense in the context of browser modules, so while the variable exists, its value is *undefined*.

## Config

### Example

{
"outputFilters": [
"./ext/logger-filter"
],
"extraRequires": {
"_": "underscore",
"Logger": "./ext/logger"
},
"beforeModuleBody": [
"var logger = new Logger(__filename);"
],
"afterModuleBody": [],
"mangleNames": true,
"logLevel": "off"
}

All configuration is optional. If you want to configure *jsbundle* operation, create a JSON file with one or more of the following key/value pairs:

### mangleNames
By default, *jsbundle* uses the absolute path of a file as its module id. This is useful for development, but in production it's wasteful and potentially reveals information you want to keep private. If you enable the **mangleNames** option, module ids will be numeric instead.

### extraRequires
You can specify additional requires that *jsbundle* will automatically add to all of your modules. This is useful for e.g. ensuring you always have underscore available without having to pollute the global namespace or remember to manually require it every time. The value for this configuration option must be an object literal, with keys the variable name for the required module and values the path to the module. **Relative paths will be resolved relative to the config file location.**

### beforeModuleBody
An array of arbitrary JavaScript statements to insert **before** every module body.

### afterModuleBody
An array of arbitrary JavaScript statements to insert **after** every module body.

### outputFilters
An array of output filters module files, resolved relative to the config file path.

Output filters allow you to specify additional ways to transform your module code. They are regular Node modules that must export an *init* function. This function takes in the *jsbundle* configuration object as a parameter and returns a second function. The returned function must accept a string containing the source code of a module and return the transformed source code, also as a string.

Example:

exports.init = function(config) {
return function(sourceCode) {
return sourceCode + '; alert("this is a silly output filter");';
};
};

[Here is a more useful example.](https://github.com/proxv/jsbundle/blob/master/ext/logger-filter.js)

## Thanks To

* [substack](https://github.com/substack) for his [browserify](https://github.com/substack/node-browserify) package, which served as inspiration for *jsbundle*.

7 changes: 7 additions & 0 deletions bin/devcdn
@@ -0,0 +1,7 @@
#!/usr/bin/env node

var argv = require('optimist').argv;
var jsbundle = require('../jsbundle');

jsbundle.devCdn(argv.port || 8081, argv._[0] || argv.config);

11 changes: 11 additions & 0 deletions bin/jsbundle
@@ -0,0 +1,11 @@
#!/usr/bin/env node

var argv = require('optimist').argv;
var jsbundle = require('../jsbundle');

if (argv._.length > 1) {
throw new Error('only one entry file can be specified at a time');
}

require('util').puts(jsbundle.bundle(argv._[0], argv.config));

15 changes: 15 additions & 0 deletions config.json
@@ -0,0 +1,15 @@
{
"outputFilters": [
"./ext/logger-filter"
],
"extraRequires": {
"_": "underscore",
"Logger": "./ext/logger"
},
"beforeModuleBody": [
"var logger = new Logger(__filename);"
],
"afterModuleBody": [],
"mangleNames": true,
"logLevel": "off"
}
60 changes: 60 additions & 0 deletions ext/logger-filter.js
@@ -0,0 +1,60 @@
var parser = require('uglify-js').parser;
var uglify = require('uglify-js').uglify;
var _ = require('underscore');

var LOG_LEVELS = {
'off': 0,
'error': 1,
'warn': 2,
'info': 3,
'debug': 4,
'trace': 5
};

function init(options) {
var logLevel = options.logLevel;
if (typeof logLevel !== 'string' || typeof LOG_LEVELS[logLevel] === 'undefined') {
throw new Error('invalid log level: ' + logLevel);
}

var allowedLogLevelsSet = {};

_(LOG_LEVELS).each(function(rank, name) {
if (rank > 0 && rank <= LOG_LEVELS[logLevel]) {
allowedLogLevelsSet[name] = true;
}
});

function loggerFilter(src) {
var replacementOffset = 0;
var walker = uglify.ast_walker();

walker.with_walkers({
'call': function() {
if (this[1][0] === 'dot' &&
this[1][1][0] === 'name' &&
this[1][1][1] === 'logger') {
var logLevel = this[1][2];
if (allowedLogLevelsSet[logLevel] !== true) {
var token = this[0];
var startPos = token.start.pos + replacementOffset;
var endPos = token.end.endpos + replacementOffset;
src = src.substring(0, startPos) + '/* ' +
src.substring(startPos, endPos) + ' */' +
src.substring(endPos);
replacementOffset += '/* */'.length;
}
}
}
}, function() {
return walker.walk(parser.parse(src, false, true));
});

return src;
}

return loggerFilter;
}

exports.init = init;

62 changes: 62 additions & 0 deletions ext/logger.js
@@ -0,0 +1,62 @@
function Logger(name) {
this._name = name;
}

var logMethods = ['error', 'warn', 'info', 'debug', 'trace'];

for (var index = 0, len = logMethods.length; index < len; ++index) {
(function(index, funcName) {
Logger.prototype[funcName] = function() {
var con = self.console;
var message = [funcName[0], '[', this._name, ']'].join('');
var args = Array.prototype.slice.call(arguments);
var stack_traces = [];

// logger.trace -> console.log
funcName = funcName === 'trace' ? 'log' : funcName;

if (!con) {
return;
}

if (typeof args[0] === 'string') {
message += ': ' + args.shift();
}

for (var i = 0, len = args.length; i < len; ++i) {
if (args[i] && args[i].stack) {
stack_traces.push(args[i].stack);
}
}

if (con.firebug) {
args.unshift(message);
con[funcName].apply(self, args);
} else {
if (args.length <= 0) {
con[funcName] ? con[funcName](message) :
con.log(message);
} else if (args.length === 1) {
con[funcName] ? con[funcName](message, args[0]) :
con.log(message, args[0]);
} else {
con[funcName] ? con[funcName](message, args) :
con.log(message, args);
}
}

var len = stack_traces.length;

if (len > 0) {
con.log('Listing exception stack traces individually:');
for (var i = 0; i < len; ++i) {
con.log(stack_traces[i]); // why? because in Google Chrome,
// this will make clickable links
}
}
};
})(index, logMethods[index]);
}

module.exports = Logger;

6 changes: 6 additions & 0 deletions jsbundle.js
@@ -0,0 +1,6 @@
var bundle = require('./lib/bundle');
var devCdn = require('./lib/dev-cdn');

exports.bundle = bundle;
exports.devCdn = devCdn;

120 changes: 120 additions & 0 deletions lib/bundle.js
@@ -0,0 +1,120 @@
var crypto = require('crypto');
var fs = require('fs');
var path = require('path');
var _ = require('underscore');
var template = require('./template');
var Module = require('./module');

function _expandExtraRequires(extraRequires, relativeToDir) {
var expandedRequires = {};
_(extraRequires).each(function (value, key) {
if (typeof value === 'string' && /^\.\/|^\.\./.test(value)) {
value = path.resolve(relativeToDir, value);
}
expandedRequires[key] = value;
});
return expandedRequires;
}

function _compileStatementArray(statementArray) {
var compiledStatements = [];
_(statementArray).each(function (value) {
compiledStatements.push(/;\s*$/.test(value) ? value : value + ';');
});
return compiledStatements.join('\n');
}

function _buildModuleOptions(options, relativeToDir) {
var moduleOptions = {};

moduleOptions.extraRequires = _expandExtraRequires(options.extraRequires, relativeToDir);
moduleOptions.beforeModuleBody = _compileStatementArray(options.beforeModuleBody);
moduleOptions.afterModuleBody = _compileStatementArray(options.afterModuleBody);

return moduleOptions;
}

function _initOutputFilters(options, relativeToDir) {
var outputFilters = [];
_(options.outputFilters).each(function(outputFilterPath) {
outputFilterPath = path.resolve(relativeToDir, outputFilterPath);
var outputFilter = require(outputFilterPath).init(options);
outputFilters.push(outputFilter);
});
return outputFilters;
}

function bundle(entryFile, configFile) {
entryFile = path.resolve(entryFile);

var configDir;
var options = {};
var counter = 0;
var files = [ entryFile ];

var modules = {};
var moduleHashToIdMap = {};
var moduleFileToIdMap = {};

// for ensuring that extra requires don't also require themselves
var moduleFilesWithExtraRequires = {};
moduleFilesWithExtraRequires[entryFile] = true;
var moduleIdsWithExtraRequires = {};

if (configFile) {
options = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
configDir = path.dirname(configFile);
}
var moduleOptions = _buildModuleOptions(options, configDir);
moduleOptions.outputFilters = _initOutputFilters(options, configDir);

while (files.length > 0) {
var file = path.resolve(files.shift());
var id = moduleFileToIdMap[file];
if (!id) {
var mod = new Module(file, moduleOptions);

// resolve duplicates: identical files with different paths
// this comes up a lot because of how npm copies modules everywhere
var hash = mod.sha1();
id = moduleHashToIdMap[hash];
if (!id) {
id = moduleHashToIdMap[hash] = (options.mangleNames ? counter++ : file);
}

modules[id] = mod;
moduleFileToIdMap[file] = id;

var deps = mod.dependencies();

// only "real" dependencies, not "extra" dependencies, get registered
// as needing extra requires
if (moduleFilesWithExtraRequires[file] === true) {
_(deps).each(function(dep) {
moduleFilesWithExtraRequires[dep] = true;
});
}

files = files.concat(mod.extraDependencies().concat(deps));
}

if (moduleFilesWithExtraRequires[file] === true) {
moduleIdsWithExtraRequires[id] = true;
}
}

var moduleDefs = [];
_(modules).each(function(module, id) {
module.updateRequires(moduleFileToIdMap, moduleIdsWithExtraRequires[id] !== true);
module.setId(id);
moduleDefs.push(module.compile());
});

return template.compile('bundle', {
moduleDefs: moduleDefs.join('\n\n'),
mainModuleId: JSON.stringify(moduleFileToIdMap[path.resolve(entryFile)])
});
}

module.exports = bundle;

0 comments on commit b9d6ecb

Please sign in to comment.