├── .gitattributes ├── globals.js ├── .gitignore ├── package.json ├── bin └── bundle-js.js ├── LICENSE ├── index.js ├── README.md └── bundler.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /globals.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | DEFAULT_OUTPUT_FILE: './bundlejs/output.js' 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | nocommit 40 | 41 | package-lock.json 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bundle-js", 3 | "version": "1.0.3", 4 | "description": "Bundle your inter-dependent Javascript files in the correct order", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/hugabor/bundle-js.git" 9 | }, 10 | "bin": { 11 | "bundle-js": "./bin/bundle-js.js" 12 | }, 13 | "keywords": [ 14 | "bundle", 15 | "js", 16 | "browser", 17 | "module", 18 | "package", 19 | "concat", 20 | "concatenate", 21 | "rollup", 22 | "roll", 23 | "up", 24 | "require", 25 | "include" 26 | ], 27 | "author": "hugabor", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/hugabor/bundle-js/issues" 31 | }, 32 | "homepage": "https://github.com/hugabor/bundle-js#readme", 33 | "engines": { 34 | "node": ">=6.8.1" 35 | }, 36 | "dependencies": { 37 | "js-beautify": "^1.6.4", 38 | "optimist": "^0.6.1", 39 | "resolve": "^1.4.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /bin/bundle-js.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const optimist = require('optimist') 4 | 5 | const globals = require('../globals.js') 6 | 7 | let argv = optimist 8 | .demand(1) 9 | .options('o', { alias : ['out', 'dest'], default: globals.DEFAULT_OUTPUT_FILE, describe: 'Output file' }) 10 | .options('p', { boolean: true, alias : ['print'], describe: 'Print the final bundled output to stdout' }) 11 | .options('disable-beautify', { boolean: true, describe: 'Leave the concatenated files as-is (might be ugly!)' }) 12 | .usage( 13 | '\n' + 14 | 'Usage: bundle-js ./path/to/entryfile.js [-o ./path/to/outputfile] [-p]\n' + // 80 character line width limit here 15 | ' [--disable-beautify]' 16 | ) 17 | .argv 18 | 19 | if (argv._[0] == 'help') { 20 | optimist.showHelp() 21 | process.exit() 22 | } 23 | 24 | let options = {} 25 | options.entry = argv._[0] 26 | options.dest = argv.o 27 | options.print = argv.p 28 | options.disablebeautify = argv['disable-beautify'] 29 | 30 | const bundle = require('../index.js') 31 | bundle(options) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Gábor Siffel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const globals = require('./globals.js') 5 | const bundler = require('./bundler.js') 6 | 7 | function bundle(options = {}) { 8 | 9 | if (!options.entry) { 10 | throw new Error('options.entryfilepath was not defined') 11 | } 12 | 13 | let entryfilepath = path.normalize(path.resolve(process.cwd(), options.entry)) 14 | let bundled = bundler.bundle(entryfilepath, options) 15 | 16 | if (options.dest) { 17 | let dest = path.normalize(path.resolve(process.cwd(), options.dest || globals.DEFAULT_OUTPUT_FILE)) 18 | if (!fs.existsSync(path.dirname(dest))) { 19 | fs.mkdirSync(path.dirname(dest)) 20 | } 21 | fs.writeFileSync(dest, bundled, { encoding: options.encoding || 'utf-8' }) 22 | } 23 | if (options.print) { 24 | process.stdout.write(bundled) 25 | } else { 26 | console.log('Bundled:', bundled.split('\n'), 'lines') 27 | if (options.dest) { 28 | console.log('Wrote to file:', options.dest) 29 | } 30 | console.log('Done.') 31 | } 32 | 33 | return bundled 34 | } 35 | 36 | module.exports = bundle 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bundle-js 2 | 3 | Bundle your inter-dependent Javascript files in the correct order 4 | 5 | Install: 6 | 7 | npm install -g bundle-js 8 | 9 | ## What It Does 10 | 11 | Concatenates your Javascript files. 12 | 13 | Just add require comments (`// require ./dependecy.js`) or (`// include ./inc/smallfile.js`) to your files and bundle-js will automatically concatenate every file that is needed by every file into one single bundled script. 14 | 15 | It uses [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting) to determine the order in which to concatenate so you don't have to worry about having anything undefined. However, as a result of this, bundle-js does **NOT support circular dependencies**. 16 | 17 | The output is a single JS script that can be written to an output file. 18 | 19 | ## Usage 20 | 21 | Within your javascript files you can use comments to indicate what external files are needed by the current file. 22 | 23 | + Using `// require ./path/to/file.js` ensures that the "required" file comes before the current file in the final concatenated output. Use this when developing multi-file Javascript without any module loaders. 24 | + Using `// include ./path/to/file.js` includes the entirety of the file directly at the location of the comment. Useful for including small snippets of code within other code. *Note: a file that is `require`-ed within a file that is `include`-ed, will still be placed at the top level of the bundled file. See `include_b` to avoid this behavior.* 25 | + Using `// include_b ./path/to/file.js` includes the entirety of the file **pre-bundled** directly at the location of the comment. This is useful for wrapping an entire project in something such as an [IIFE](http://benalman.com/news/2010/11/immediately-invoked-function-expression/) or to compile to a specific target such as for the browser or a node module. 26 | 27 | **Circular dependencies are not allowed (neither for requires or includes).** 28 | 29 | In order to require or include a file, you must begin the file path with `./`, `../`, or `/`. Otherwise, it will search for a node module. This is because file resolution is done using the [resolve module](https://www.npmjs.com/package/resolve), which implements the behavior of Node's `require.resolve()` ([more information here](https://nodejs.org/api/modules.html#modules_all_together)). 30 | 31 | *Note: These are not case sensitive (ie. you can freely use `REQUIRE`, `INCLUDE`, `INCLUDE_B`)* 32 | 33 | ### Options: 34 | 35 | + **entry**: (required) the "entry point" - a single file path to start finding the dependencies from recursively 36 | + **dest**: (optional) the output file path 37 | + **print**: (optional) prints the output file to stdout if set to true 38 | + **disable-beautify**: (optional) bundle-js by default runs the final output through beautify; set this to true to disable this behavior 39 | 40 | ### Command line: 41 | 42 | Usage: bundle-js ./path/to/entryfile.js [-o ./path/to/outputfile] [-p] 43 | [--disable-beautify] 44 | 45 | Options: 46 | -o, --out, --dest Output file [default: "./bundlejs/output.js"] 47 | -p, --print Print the final bundled output to stdout 48 | --disable-beautify Leave the concatenated files as-is (might be ugly!) 49 | 50 | ### Programmatic: 51 | 52 | const bundle = require('bundle-js') 53 | let output = bundle({ entry : './index.js' }) 54 | 55 | Configuration options: 56 | 57 | bundle({ 58 | entry : './index.js', 59 | dest : './bundle.js', 60 | print : false, 61 | disablebeautify : false 62 | }) 63 | 64 | ## Simple Example 65 | 66 | If in file `A.js` you have 67 | 68 | // require ./B.js 69 | console.log(b + ' world!'); 70 | 71 | and in file `B.js` 72 | 73 | var b = 'Hello'; 74 | 75 | The final output is going to look like this 76 | 77 | var b = 'Hello'; 78 | console.log(b + ' world!'); 79 | 80 | ## Wrapper Example 81 | 82 | In file `index.js` you have 83 | 84 | // require ./dep.js 85 | // some code 86 | 87 | in file `dep.js` 88 | 89 | // this is a dependency 90 | 91 | Using `wrapper1.js` 92 | 93 | (function() { 94 | // include ./index.js 95 | })(); 96 | 97 | Will result in 98 | 99 | // this is a dependency 100 | (function() { 101 | // some code 102 | })(); 103 | 104 | However, using `wrapper2.js` 105 | 106 | (function() { 107 | // include_b ./index.js 108 | })(); 109 | 110 | Will result in 111 | 112 | (function() { 113 | // this is a dependency 114 | // some code 115 | })(); 116 | 117 | ## License 118 | 119 | [MIT License](LICENSE) 120 | -------------------------------------------------------------------------------- /bundler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const resolve = require('resolve') 4 | const beautify = require('js-beautify').js_beautify 5 | 6 | 7 | const ENCODING = 'utf-8' 8 | 9 | function REQUIRE_REGEX() { 10 | return new RegExp(/\/\/+\s*REQUIRE\s+([^\s\n]+)/, "gi") 11 | } 12 | function INCLUDE_REGEX() { 13 | return new RegExp(/\/\/+\s*INCLUDE\s+([^\s\n]+)/, "gi") 14 | } 15 | function INCLUDEB_REGEX() { 16 | return new RegExp(/\/\/+\s*INCLUDE_?B\s+([^\s\n]+)/, "gi") 17 | } 18 | 19 | 20 | function concatFiles(files) { 21 | let concatenated = '' 22 | 23 | for (let file of files) { 24 | concatenated += file.getFinalContent() 25 | concatenated += '\n\n' 26 | } 27 | 28 | return concatenated 29 | } 30 | 31 | class File { 32 | constructor(filepath, basedir = process.cwd()) { 33 | this.absolutefilepath = File.resolve(filepath, basedir) 34 | this.dependentfiles = [] 35 | 36 | this._generated = false 37 | 38 | this.tempmark = false 39 | this.permmark = false 40 | 41 | this._contentwithincludes = null 42 | } 43 | 44 | readFile() { 45 | return fs.readFileSync(this.absolutefilepath, 'utf-8') 46 | } 47 | 48 | getContentWithIncludes(existingfiles = [], ancestorincludes = []) { 49 | if (this._contentwithincludes != null) { 50 | return this._contentwithincludes 51 | } 52 | 53 | // Check for cyclical inclusions 54 | ancestorincludes.forEach((file) => { 55 | if (this.absolutefilepath == file.absolutefilepath) { 56 | throw new Error('Ran into cyclical inclusion! Cannot INCLUDE file within itself: ' + this.absolutefilepath) 57 | } 58 | }) 59 | 60 | this._contentwithincludes = this.readFile(); 61 | 62 | // Return contents with INCLUDE statements replaced with the inclusions 63 | this._contentwithincludes = this._contentwithincludes.replace(INCLUDE_REGEX(), (m, p) => { 64 | let includedfile = this.getRelativeFile(p, existingfiles) 65 | return includedfile.getContentWithIncludes(existingfiles, ancestorincludes.concat([this])) 66 | }) 67 | this._contentwithincludes = this._contentwithincludes.replace(INCLUDEB_REGEX(), (m, p) => { 68 | let includedfile = this.getRelativeFile(p) 69 | return bundle(includedfile.absolutefilepath, { disablebeautify: true }) 70 | }) 71 | return this._contentwithincludes 72 | } 73 | 74 | getFinalContent() { 75 | if (this._contentwithincludes == null) { 76 | throw new Error('An unknown error occured (_contentwithincludes was null)') 77 | } 78 | 79 | return this._contentwithincludes.replace(REQUIRE_REGEX(), () => '') 80 | } 81 | 82 | addDependentFile(file) { 83 | for (let dependentfile of this.dependentfiles) { 84 | if (file.absolutefilepath == dependentfile.absolutefilepath) { 85 | return 86 | } 87 | } 88 | this.dependentfiles.push(file) 89 | this._generated = false 90 | } 91 | 92 | generateDependentFiles(existingfiles = []) { 93 | if (this._generated == true) { 94 | return 95 | } 96 | 97 | this.dependentfiles = [] 98 | 99 | let regex = REQUIRE_REGEX() 100 | 101 | let matches 102 | while (matches = regex.exec(this.getContentWithIncludes(existingfiles))) { 103 | this.addDependentFile(this.getRelativeFile(matches[1], existingfiles)) 104 | } 105 | this._generated = true 106 | } 107 | 108 | getRelativeFile(filepath, existingfiles = []) { 109 | let newfile = new File(filepath, this.dir()) 110 | for (let existingfile of existingfiles) { 111 | if (newfile.absolutefilepath == existingfile.absolutefilepath) { 112 | return existingfile 113 | } 114 | } 115 | existingfiles.push(newfile) 116 | return newfile 117 | } 118 | 119 | dir() { 120 | return path.dirname(this.absolutefilepath) 121 | } 122 | 123 | static resolve(filepath, basedir = process.cwd()) { 124 | return path.normalize( 125 | path.resolve( 126 | resolve.sync(filepath, { basedir }) 127 | ) 128 | ) 129 | } 130 | } 131 | 132 | function toposortDependencies(entryfilepath) { 133 | let existingfiles = [] 134 | 135 | let sortedlist = [] 136 | 137 | function DFSToposort(root) { 138 | if (root.permmark) 139 | return 140 | if (root.tempmark) 141 | throw new Error('Found a dependency cycle') 142 | 143 | root.tempmark = true 144 | 145 | // Generate dependencies on-the-fly while doing DFS 146 | root.generateDependentFiles(existingfiles) 147 | 148 | root.dependentfiles.forEach((vertex) => DFSToposort(vertex)) 149 | 150 | root.permmark = true 151 | 152 | sortedlist.push(root) 153 | } 154 | 155 | let root = new File(entryfilepath) 156 | existingfiles.push(root) 157 | DFSToposort(root) 158 | 159 | return sortedlist 160 | } 161 | 162 | 163 | function bundle(entryfilepath, options = {}) { 164 | 165 | let order = toposortDependencies(entryfilepath) 166 | 167 | let bundled = concatFiles(order) 168 | 169 | if (!(options.disablebeautify == true)) { 170 | bundled = beautify(bundled, { 171 | indent_size: 4, 172 | end_with_newline: true, 173 | preserve_newlines: false 174 | }) 175 | } 176 | 177 | return bundled 178 | } 179 | 180 | module.exports = { bundle: bundle } 181 | --------------------------------------------------------------------------------