├── .gitignore ├── LICENSE ├── README.md ├── cli.js ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (Expat) 2 | 3 | Copyright (c) 2014 Andrew Kelley 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation files 7 | (the "Software"), to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of the Software, 10 | and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 20 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 21 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # browserify-lite 2 | 3 | browserify, minus some of the advanced features and heavy dependencies. 4 | 5 | * No builtin Node.js shims. 6 | * Naive AST tokenization for require instead of true AST parsing. 7 | - All require statements are found regardless of if they are in an `if` 8 | statement or a function body that is never called. 9 | * Only supports a single entry file and the `--outfile` parameter, 10 | nothing else. 11 | * No source maps. 12 | * Minimal dependencies. 13 | 14 | ## Usage 15 | 16 | ``` 17 | browserify-lite ./entry-file.js --outfile bundle.js 18 | 19 | Standard Options: 20 | 21 | --outfile Write the browserify bundle to this file. 22 | --standalone xyz Export as a window global. 23 | ``` 24 | 25 | ./src/app.js can depend on any modules using Node.js's 26 | [module resolution system](http://nodejs.org/docs/latest/api/modules.html#modules_all_together) 27 | and they will be bundled as well. 28 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var browserifyLite = require('./'); 5 | 6 | var options = {}; 7 | for (var i = 2; i < process.argv.length; i += 1) { 8 | var arg = process.argv[i]; 9 | if (arg === '--help') { 10 | usage(); 11 | } else if (arg === '--outfile') { 12 | if (++i >= process.argv.length) usage(); 13 | options.outBundlePath = process.argv[i]; 14 | } else if (arg === '--standalone') { 15 | if (++i >= process.argv.length) usage(); 16 | options.standalone = process.argv[i]; 17 | } else if (!options.entrySourcePath) { 18 | options.entrySourcePath = arg; 19 | } else { 20 | usage(); 21 | } 22 | } 23 | 24 | if (!options.outBundlePath || !options.entrySourcePath) { 25 | usage(); 26 | } 27 | 28 | browserifyLite.createBundle(options, function(err) { 29 | if (err) throw err; 30 | }); 31 | 32 | 33 | function usage() { 34 | console.error( 35 | "Usage: browserify-lite ./entry-file.js --outfile bundle.js\n" + 36 | "\n" + 37 | "Standard Options:\n" + 38 | "\n" + 39 | " --outfile Write the browserify bundle to this file\n" + 40 | " --standalone xyz Export as window.xyz"); 41 | process.exit(1); 42 | } 43 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var Pend = require('pend'); 4 | 5 | exports.extractRequires = extractRequires; 6 | exports.createBundle = createBundle; 7 | exports.renderBundle = renderBundle; 8 | 9 | function createBundle(options, cb) { 10 | var outBundlePath = options.outBundlePath; 11 | renderBundle(options, function(err, output) { 12 | if (err) return cb(err); 13 | fs.writeFile(outBundlePath, output, cb); 14 | }); 15 | } 16 | 17 | function renderBundle(options, cb) { 18 | var entrySourcePath = options.entrySourcePath; 19 | var standalone = options.standalone; 20 | 21 | // data structure that is filled up with canonical source path as the key, 22 | // source code as the value. 23 | var sources = {}; 24 | 25 | // describes the dependency graph. key is canonical source path, value is 26 | // array of canonical source path dependencies 27 | var deps = {}; 28 | 29 | // each file has its own map of how require statements resolve. 30 | var depMap = {}; 31 | 32 | var sourceQueue = []; 33 | 34 | requireResolve(entrySourcePath, process.cwd(), function(err, resolvedPath) { 35 | if (err) return cb(err); 36 | sourceQueue.push(resolvedPath); 37 | collectDependencies(function(err) { 38 | if (err) return cb(err); 39 | render(resolvedPath, function(err, output) { 40 | if (err) return cb(err); 41 | cb(null, output); 42 | }); 43 | }); 44 | }); 45 | 46 | function collectDependencies(cb) { 47 | var canonicalSourcePath = sourceQueue.shift(); 48 | if (!canonicalSourcePath) return cb(); 49 | if (sources[canonicalSourcePath]) return collectDependencies(cb); 50 | 51 | fs.readFile(canonicalSourcePath, {encoding: 'utf8'}, function(err, source) { 52 | if (err) return cb(err); 53 | sources[canonicalSourcePath] = source; 54 | deps[canonicalSourcePath] = {}; 55 | depMap[canonicalSourcePath] = {}; 56 | 57 | var pend = new Pend(); 58 | extractRequires(source, function(err, requireList) { 59 | if (err) return cb(err); 60 | requireList.forEach(function(requireItem) { 61 | pend.go(function(cb) { 62 | requireResolve(requireItem, path.dirname(canonicalSourcePath), function(err, canonicalDepPath) { 63 | if (err) return cb(err); 64 | deps[canonicalSourcePath][canonicalDepPath] = true; 65 | depMap[canonicalSourcePath][requireItem] = canonicalDepPath; 66 | sourceQueue.push(canonicalDepPath); 67 | cb(); 68 | }); 69 | }); 70 | }); 71 | pend.wait(function(err) { 72 | if (err) return cb(err); 73 | collectDependencies(cb); 74 | }); 75 | }); 76 | 77 | }); 78 | } 79 | 80 | function render(entrySourcePath, cb) { 81 | var modules = Object.keys(sources); 82 | var aliases = {}; 83 | modules.forEach(function(canonicalSourcePath, index) { 84 | aliases[canonicalSourcePath] = index; 85 | }); 86 | modules.forEach(function(canonicalSourcePath, index) { 87 | var thisDepMap = depMap[canonicalSourcePath] 88 | for (var depSourcePath in thisDepMap) { 89 | thisDepMap[depSourcePath] = aliases[thisDepMap[depSourcePath]]; 90 | } 91 | }); 92 | 93 | var standaloneLine = standalone ? 94 | " window." + standalone + " = req(entry);\n" : 95 | " req(entry);\n"; 96 | 97 | var out = 98 | "(function(modules, cache, entry) {\n" + 99 | standaloneLine + 100 | " function req(name) {\n" + 101 | " if (cache[name]) return cache[name].exports;\n" + 102 | " var m = cache[name] = {exports: {}};\n" + 103 | " modules[name][0].call(m.exports, modRequire, m, m.exports, window);\n" + 104 | " return m.exports;\n" + 105 | " function modRequire(alias) {\n" + 106 | " var id = modules[name][1][alias];\n" + 107 | " if (!id) throw new Error(\"Cannot find module \" + alias);\n" + 108 | " return req(id);\n" + 109 | " }\n" + 110 | " }\n" + 111 | "})({"; 112 | 113 | modules.forEach(function(canonicalSourcePath) { 114 | var thisDepMap = depMap[canonicalSourcePath]; 115 | out += aliases[canonicalSourcePath] + ": [function(require,module,exports,global){\n"; 116 | if (canonicalSourcePath.match(/\.json$/)) { 117 | out += "module.exports = "; 118 | } 119 | out += sources[canonicalSourcePath]; 120 | out += "\n}, " + JSON.stringify(thisDepMap, Object.keys(thisDepMap).sort()) + "],"; 121 | }); 122 | 123 | out += "}, {}, " + aliases[entrySourcePath] + ");\n"; 124 | 125 | cb(null, out); 126 | } 127 | } 128 | 129 | function tokenizeSource(source) { 130 | var tokens = []; 131 | var inQuote = false; 132 | var quoteType; 133 | var qEscape = false; 134 | var token = ""; 135 | var inLineComment = false; 136 | var inMultiLineComment = false; 137 | var startComment = false; 138 | var endComment = false; 139 | for (var i = 0; i < source.length; i += 1) { 140 | var c = source[i]; 141 | if (inQuote) { 142 | if (qEscape) { 143 | token += c; 144 | qEscape = false; 145 | } else if (c === "\\") { 146 | qEscape = true; 147 | } else if (c === quoteType) { 148 | inQuote = false; 149 | tokens.push(token); 150 | token = ""; 151 | } else { 152 | token += c; 153 | } 154 | } else if (inLineComment) { 155 | if (c === "\n") { 156 | inLineComment = false; 157 | } 158 | } else if (inMultiLineComment) { 159 | if (c === '*') { 160 | endComment = true; 161 | } else if (c === '/') { 162 | if (endComment) { 163 | inMultiLineComment = false; 164 | endComment = false; 165 | } 166 | } else { 167 | endComment = false; 168 | } 169 | } else if (c === "\"" || c === "'") { 170 | startComment = false; 171 | if (token) tokens.push(token); 172 | token = ""; 173 | inQuote = true; 174 | quoteType = c; 175 | qEscape = false; 176 | } else if (c === '{') { 177 | startComment = false; 178 | if (token) tokens.push(token); 179 | token = ""; 180 | } else if (c === '}') { 181 | startComment = false; 182 | } else if (c === '/') { 183 | if (startComment) { 184 | if (token) tokens.push(token); 185 | token = ""; 186 | inLineComment = true; 187 | startComment = false; 188 | } else { 189 | startComment = true; 190 | } 191 | } else if (c === '*' && startComment) { 192 | if (token) tokens.push(token); 193 | token = ""; 194 | inMultiLineComment = true; 195 | startComment = false; 196 | } else { 197 | if (/\W/.test(c)) { 198 | if (token) tokens.push(token); 199 | token = ""; 200 | } 201 | if (/\S/.test(c)) { 202 | token += c; 203 | } 204 | } 205 | } 206 | if (token) tokens.push(token); 207 | return tokens; 208 | } 209 | 210 | var stateCount = 0; 211 | var STATE_WANT_REQUIRE = stateCount++; 212 | var STATE_WANT_LPAREN = stateCount++; 213 | var STATE_WANT_STR = stateCount++; 214 | var STATE_WANT_RPAREN = stateCount++; 215 | function extractRequires(source, cb) { 216 | var tokens = tokenizeSource(source); 217 | 218 | var requiresList = []; 219 | var state = STATE_WANT_REQUIRE; 220 | var requireName; 221 | 222 | for (var i = 0; i < tokens.length; i += 1) { 223 | var token = tokens[i]; 224 | if (state === STATE_WANT_REQUIRE && token === 'require') { 225 | state = STATE_WANT_LPAREN; 226 | } else if (state === STATE_WANT_LPAREN && token === '(') { 227 | state = STATE_WANT_STR; 228 | } else if (state === STATE_WANT_STR) { 229 | requireName = token; 230 | state = STATE_WANT_RPAREN; 231 | } else if (state === STATE_WANT_RPAREN && token === ')') { 232 | state = STATE_WANT_REQUIRE; 233 | requiresList.push(requireName); 234 | } else if (state === STATE_WANT_RPAREN && token !== ')') { 235 | state = STATE_WANT_REQUIRE; 236 | } 237 | } 238 | cb(null, requiresList); 239 | } 240 | 241 | function requireResolve(pkg, basedir, cb) { 242 | if (/^[.\/]/.test(pkg)) { 243 | requireResolvePath(path.resolve(basedir, pkg), cb); 244 | } else { 245 | requireResolveModule(pkg, basedir, cb); 246 | } 247 | } 248 | 249 | function requireResolveModule(pkg, basedir, cb) { 250 | var globalSearchPaths = process.env.NODE_PATH ? process.env.NODE_PATH.split(path.delimiter) : []; 251 | 252 | var localSearchPaths = []; 253 | var parts = basedir.split(path.sep); 254 | var it = "/"; 255 | for (var i = 0; i < parts.length; i += 1) { 256 | it = path.join(it, parts[i]); 257 | localSearchPaths.unshift(path.join(it, "node_modules")); 258 | } 259 | 260 | var searchPaths = localSearchPaths.concat(globalSearchPaths); 261 | var index = 0; 262 | 263 | trySearchPath(); 264 | 265 | function trySearchPath() { 266 | var searchPath = searchPaths[index]; 267 | if (!searchPath) return cb(new Error("module " + pkg + " in " + basedir + " not found")); 268 | 269 | requireResolvePath(path.resolve(searchPath, pkg), function(err, resolvedFilename) { 270 | if (!err) return cb(null, resolvedFilename); 271 | index += 1; 272 | trySearchPath(); 273 | }); 274 | } 275 | } 276 | 277 | function requireResolvePath(filename, cb) { 278 | resolveFile(filename, function(err, resolvedFilename) { 279 | if (!err) return cb(null, resolvedFilename); 280 | resolveFile(filename + '.js', function(err, resolvedFilename) { 281 | if (!err) return cb(null, resolvedFilename); 282 | resolveFile(filename + '.json', function(err, resolvedFilename) { 283 | if (!err) return cb(null, resolvedFilename); 284 | resolveDirectory(filename, cb); 285 | }); 286 | }); 287 | }); 288 | } 289 | 290 | function resolveFile(filename, cb) { 291 | fs.stat(filename, function(err, stat) { 292 | if (err) return cb(err); 293 | if (stat.isDirectory()) return cb(new Error("directory")); 294 | cb(null, filename); 295 | }); 296 | } 297 | 298 | function resolveDirectory(dirname, cb) { 299 | var packageJsonPath = path.resolve(dirname, "package.json"); 300 | fs.readFile(packageJsonPath, {encoding: 'utf8'}, function(err, packageJsonStr) { 301 | if (!err) { 302 | var packageJson; 303 | try { 304 | packageJson = JSON.parse(packageJsonStr); 305 | } catch (err) { 306 | cb(new Error("Invalid package.json: " + packageJsonPath + ": "+ err.message)); 307 | return; 308 | } 309 | var filename; 310 | var browserObject = packageJson.browserify || packageJson.browser; 311 | if (typeof browserObject === 'string') { 312 | filename = path.resolve(dirname, browserObject); 313 | requireResolvePath(filename, tryIndex); 314 | } else if (packageJson.main) { 315 | filename = path.resolve(dirname, packageJson.main); 316 | requireResolvePath(filename, tryIndex); 317 | } else { 318 | tryIndex(new Error("no main found in package.json")); 319 | } 320 | } else { 321 | tryIndex(new Error("no package.json found")); 322 | } 323 | 324 | 325 | function tryIndex(err, filename) { 326 | if (!err) return cb(null, filename); 327 | filename = path.resolve(dirname, "index.js"); 328 | resolveFile(filename, function(err) { 329 | if (!err) return cb(null, filename); 330 | filename = path.resolve(dirname, "index.json"); 331 | resolveFile(filename, cb); 332 | }); 333 | } 334 | }); 335 | } 336 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserify-lite", 3 | "version": "0.5.1", 4 | "description": "browserify, minus some of the advanced features and heavy dependencies.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "bin": { 10 | "browserify-lite": "./cli.js" 11 | }, 12 | "author": "Andrew Kelley ", 13 | "license": "MIT", 14 | "dependencies": { 15 | "pend": "~1.2.0" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/andrewrk/browserify-lite.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/andrewrk/browserify-lite/issues" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var browserifyLite = require('./'); 3 | 4 | var extractRequiresTests = [ 5 | { 6 | name: "basic", 7 | source: "require('./code')", 8 | output: [ 9 | "./code" 10 | ], 11 | }, 12 | { 13 | name: "multiple", 14 | source: 15 | "var EventEmitter = require('./event_emitter');\n" + 16 | "var inherits = require('./inherits');\n" + 17 | "var uuid = require('./uuid');\n" + 18 | "var MusicLibraryIndex = require('music-library-index');\n" + 19 | "var keese = require(\"keese\");\n" + 20 | "var curlydiff = require('curlydiff');\n", 21 | output: [ 22 | "./event_emitter", 23 | "./inherits", 24 | "./uuid", 25 | "music-library-index", 26 | "keese", 27 | "curlydiff", 28 | ], 29 | }, 30 | { 31 | name: "trick", 32 | source: "require('./code');\nvar a = \"require('foo');\";\nrequire(\"../morecode\");", 33 | output: [ 34 | "./code", 35 | "../morecode", 36 | ], 37 | }, 38 | { 39 | name: "unescape", 40 | source: "require('./code');\nvar a = \"require(\\\"foo\\\");\";\nrequire(\"../morecode\");", 41 | output: [ 42 | "./code", 43 | "../morecode", 44 | ], 45 | }, 46 | { 47 | name: "spaces", 48 | source: "var foo = require ( 'derp ' ) ;\n", 49 | output: [ 50 | "derp ", 51 | ], 52 | }, 53 | { 54 | name: "ignore braces", 55 | source: "var foo = require('derp'); { require('dont-ignore-this'); } require('this-ok')\n", 56 | output: [ 57 | "derp", 58 | "dont-ignore-this", 59 | "this-ok", 60 | ], 61 | }, 62 | { 63 | name: "ignore comments", 64 | source: "/* var foo = require('derp');*/ { require('dont-ignore-this'); } require('this-ok') // require('also-ignore-this'); \n require('this-also-ok')", 65 | output: [ 66 | "dont-ignore-this", 67 | "this-ok", 68 | "this-also-ok", 69 | ], 70 | } 71 | ]; 72 | 73 | process.stderr.write("extract requires tests:\n"); 74 | extractRequiresTests.forEach(function(extractRequiresTest) { 75 | process.stderr.write(extractRequiresTest.name + "..."); 76 | browserifyLite.extractRequires(extractRequiresTest.source, function(err, requiresList) { 77 | if (err) throw err; 78 | assert.deepEqual(extractRequiresTest.output, requiresList); 79 | process.stderr.write("OK\n"); 80 | }); 81 | }); 82 | --------------------------------------------------------------------------------