├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── lib └── smash │ ├── emit-imports.js │ ├── emit-lines.js │ ├── expand-file.js │ ├── load.js │ ├── read-all-imports.js │ ├── read-graph.js │ ├── read-imports.js │ └── smash.js ├── package.json ├── smash └── test ├── data ├── bar.js ├── baz.js ├── commented-import.js ├── empty-lines.js ├── foo.js ├── forty-two.js ├── import-empty-lines.js ├── imports-circular-bar.js ├── imports-circular-foo-expected.js ├── imports-circular-foo.js ├── imports-foo-bar-baz-expected.js ├── imports-foo-bar-baz.js ├── imports-foo-expected.js ├── imports-foo-foo-bar-foo-expected.js ├── imports-foo-foo-bar-foo.js ├── imports-foo.js ├── imports-imports-foo-expected.js ├── imports-imports-foo.js ├── imports-index.js ├── imports-not-found.js ├── imports-self.js ├── index.js ├── invalid-import-syntax.js ├── mismatched-quotes.js ├── not-commented-import-expected.js ├── not-commented-import.js ├── single-quote-import-expected.js ├── single-quote-import.js ├── trailing-comment-import-expected.js └── trailing-comment-import.js ├── expandFile-test.js ├── load-test.js ├── readAllImports-test.js ├── readGraph-test.js ├── readImports-test.js └── smash-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Michael Bostock 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * The name Michael Bostock may not be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 24 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 26 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SMASH 2 | 3 | **SMASH DEPRECATED. [DO ROLLUP INSTEAD.](http://rollupjs.org/)** 4 | 5 | SMASH TOGETHER FILES! PROBABLY JAVASCRIPT. 6 | 7 | SAY THIS foo.js: 8 | 9 | ```js 10 | import "bar"; 11 | 12 | function foo() { 13 | return "foo" + bar(); 14 | } 15 | ``` 16 | 17 | AND THIS bar.js: 18 | 19 | ```js 20 | function bar() { 21 | return "bar"; 22 | } 23 | ``` 24 | 25 | WHEN SMASH TOGETHER foo.js AND bar.js: 26 | 27 | ```js 28 | function bar() { 29 | return "bar"; 30 | } 31 | 32 | function foo() { 33 | return "foo" + bar(); 34 | } 35 | ``` 36 | 37 | SMASH HANDLE CIRCULAR AND REDUNDANT IMPORTS GOOD. SMASH GOOD. SMASH. 38 | 39 | SMASH LIKE MAKE, TOO. 40 | 41 | ```Makefile 42 | bundle.js: $(shell smash --list src/bundle.js) 43 | smash src/bundle.js > bundle.js 44 | ``` 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var smash = module.exports = require("./lib/smash/smash"); 2 | smash.version = require("./package").version; 3 | smash.load = require("./lib/smash/load"); 4 | smash.readGraph = require("./lib/smash/read-graph"); 5 | smash.readAllImports = require("./lib/smash/read-all-imports"); 6 | smash.readImports = require("./lib/smash/read-imports"); 7 | -------------------------------------------------------------------------------- /lib/smash/emit-imports.js: -------------------------------------------------------------------------------- 1 | var path = require("path"), 2 | events = require("events"), 3 | emitLines = require("./emit-lines"), 4 | expandFile = require("./expand-file"); 5 | 6 | // Returns an emitter for the specified file. The returned emitter emits 7 | // "import" events whenever an import statement is encountered, and "data" 8 | // events whenever normal text is encountered, in addition to the standard 9 | // "error" and "end" events. 10 | module.exports = function(file) { 11 | var emitter = new events.EventEmitter(), 12 | directory = path.dirname(file), 13 | extension = path.extname(file), 14 | lines = []; 15 | 16 | file = expandFile(file, extension); 17 | 18 | var lineEmitter = emitLines(file) 19 | .on("line", line) 20 | .on("end", end) 21 | .on("error", error); 22 | 23 | function line(line, number) { 24 | if (/^import\b/.test(line)) { 25 | flush(); 26 | var match = /^import\s+(["'])([^"']+)(\1)\s*;?\s*(?:\/\/.*)?$/.exec(line); 27 | if (match) { 28 | emitter.emit("import", path.join(directory, expandFile(match[2], extension))); 29 | } else { 30 | lineEmitter.removeAllListeners(); // ignore subsequent lines 31 | error(new Error("invalid import: " + file + ":" + number + ": " + line)); 32 | } 33 | } else { 34 | lines.push(line, newline); 35 | } 36 | } 37 | 38 | function flush() { 39 | if (lines.length) emitter.emit("data", Buffer.concat(lines)), lines = []; 40 | } 41 | 42 | function end() { 43 | flush(); 44 | emitter.emit("end"); 45 | } 46 | 47 | function error(e) { 48 | emitter.emit("error", e); 49 | } 50 | 51 | return emitter; 52 | }; 53 | 54 | var newline = new Buffer("\n"); 55 | -------------------------------------------------------------------------------- /lib/smash/emit-lines.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"), 2 | events = require("events"); 3 | 4 | // Returns an emitter for the specified file. The returned emitter emits "line" 5 | // events for each line in the file, in addition to the standard "error" and 6 | // "end" events. 7 | module.exports = function(file) { 8 | var emitter = new events.EventEmitter(), 9 | lineFragments = [], 10 | lineNumber = -1; 11 | 12 | fs.createReadStream(file) 13 | .on("data", data) 14 | .on("end", end) 15 | .on("error", error); 16 | 17 | function data(chunk) { 18 | var i = 0, n = chunk.length; 19 | 20 | // Join queued line fragments, if any, with first line in new chunk. 21 | if (lineFragments.length) { 22 | var k = 0; 23 | while (i < n) { 24 | var c = chunk[i++]; 25 | if (c === 10) { ++k; break; } // \n 26 | if (c === 13) { ++k; if (chunk[i] === 10) ++i, ++k; break; } // \r or \r\n 27 | } 28 | lineFragments.push(chunk.slice(0, i - k)); 29 | if (k) emitter.emit("line", Buffer.concat(lineFragments), ++lineNumber), lineFragments = []; 30 | else return; 31 | } 32 | 33 | // Find subsequent lines in new chunk. 34 | while (true) { 35 | var i0 = i, k = 0; 36 | while (i < n) { 37 | var c = chunk[i++]; 38 | if (c === 10) { ++k; break; } // \n 39 | if (c === 13) { ++k; if (chunk[i] === 10) ++i, ++k; break; } // \r or \r\n 40 | } 41 | var line = chunk.slice(i0, i - k); 42 | if (k) emitter.emit("line", line, ++lineNumber); 43 | else { if (i0 !== i) lineFragments.push(line); break; } 44 | } 45 | } 46 | 47 | function end() { 48 | if (lineFragments.length) emitter.emit("line", Buffer.concat(lineFragments), ++lineNumber); 49 | emitter.emit("end"); 50 | } 51 | 52 | function error(e) { 53 | emitter.emit("error", e); 54 | } 55 | 56 | return emitter; 57 | }; 58 | -------------------------------------------------------------------------------- /lib/smash/expand-file.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | module.exports = function(file, extension) { 4 | if (extension == null) extension = defaultExtension; 5 | else extension += ""; 6 | if (/\/$/.test(file)) file += "index" + extension; 7 | else if (!path.extname(file)) file += extension; 8 | return file; 9 | }; 10 | 11 | var defaultExtension = ".js"; 12 | -------------------------------------------------------------------------------- /lib/smash/load.js: -------------------------------------------------------------------------------- 1 | var vm = require("vm"), 2 | smash = require("./smash"); 3 | 4 | // Loads the specified files and their imports, then evaluates the specified 5 | // expression in the context of the concatenated code. 6 | module.exports = function(files, expression, sandbox, callback) { 7 | if (arguments.length < 4) callback = sandbox, sandbox = undefined; 8 | var chunks = []; 9 | smash(files) 10 | .on("error", callback) 11 | .on("data", function(chunk) { chunks.push(chunk); }) 12 | .on("end", function() { 13 | var error, result; 14 | try { result = vm.runInNewContext(chunks.join("") + ";(" + expression + ")", sandbox); } 15 | catch (e) { error = e; } 16 | callback(error, result); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/smash/read-all-imports.js: -------------------------------------------------------------------------------- 1 | var queue = require("queue-async"), 2 | expandFile = require("./expand-file"), 3 | readImports = require("./read-imports"); 4 | 5 | // Reads all the imports from the specified files, returning an array of files. 6 | // The returned array is in dependency order and only contains unique entries. 7 | // The returned arrays also includes any input files at the end. 8 | module.exports = function(files, options, callback) { 9 | if (typeof options === "function") callback = options, options = {}; 10 | 11 | var fileMap = {}, 12 | allFiles = []; 13 | 14 | function readRecursive(file, callback) { 15 | if (file in fileMap) return callback(null); 16 | fileMap[file] = true; 17 | readImports(file, function(error, files) { 18 | if (error) { 19 | if (options["ignore-missing"] && error.code === "ENOENT") files = []; 20 | else return void callback(error); 21 | } 22 | var q = queue(1); 23 | files.forEach(function(file) { 24 | q.defer(readRecursive, file); 25 | }); 26 | q.awaitAll(function(error) { 27 | if (!error) allFiles.push(file); 28 | callback(error); 29 | }); 30 | }); 31 | } 32 | 33 | var q = queue(1); 34 | files.forEach(function(file) { 35 | q.defer(readRecursive, expandFile(file)); 36 | }); 37 | q.awaitAll(function(error) { 38 | callback(error, error ? null : allFiles); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /lib/smash/read-graph.js: -------------------------------------------------------------------------------- 1 | var queue = require("queue-async"), 2 | expandFile = require("./expand-file"), 3 | readImports = require("./read-imports"); 4 | 5 | // Returns the network of imports, starting with the specified input files. 6 | // For each file in the returned map, an array specifies the set of files 7 | // immediately imported by that file. This array is in order of import, and may 8 | // contain duplicate entries. 9 | module.exports = function(files, options, callback) { 10 | if (typeof options === "function") callback = options, options = {}; 11 | 12 | var fileMap = {}; 13 | 14 | function readRecursive(file, callback) { 15 | if (file in fileMap) return callback(null); 16 | readImports(file, function(error, files) { 17 | if (error) { 18 | if (options["ignore-missing"] && error.code === "ENOENT") files = []; 19 | else return void callback(error); 20 | } 21 | var q = queue(1); 22 | fileMap[file] = files; 23 | files.forEach(function(file) { 24 | q.defer(readRecursive, file); 25 | }); 26 | q.awaitAll(callback); 27 | }); 28 | } 29 | 30 | var q = queue(1); 31 | files.forEach(function(file) { 32 | q.defer(readRecursive, expandFile(file)); 33 | }); 34 | q.awaitAll(function(error) { 35 | callback(error, error ? null : fileMap); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/smash/read-imports.js: -------------------------------------------------------------------------------- 1 | var emitImports = require("./emit-imports"); 2 | 3 | // Reads the import statements from the specified file, returning an array of 4 | // files. Unlike readAllImports, this does not recursively traverse import 5 | // statements; it only returns import statements in the specified input file. 6 | // Also unlike readAllImports, this method returns every import statement, 7 | // including redundant imports and self-imports. 8 | module.exports = function(file, callback) { 9 | var files = []; 10 | 11 | emitImports(file) 12 | .on("import", function(file) { files.push(file); }) 13 | .on("error", callback) 14 | .on("end", function() { callback(null, files); }); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/smash/smash.js: -------------------------------------------------------------------------------- 1 | var stream = require("stream"), 2 | queue = require("queue-async"), 3 | expandFile = require("./expand-file"), 4 | emitImports = require("./emit-imports"); 5 | 6 | // Returns a readable stream for the specified files. 7 | // All imports are expanded the first time they are encountered. 8 | // Subsequent redundant imports are ignored. 9 | module.exports = function(files) { 10 | var s = new stream.PassThrough({encoding: "utf8", decodeStrings: false}), 11 | q = queue(1), 12 | fileMap = {}; 13 | 14 | // Streams the specified file and any imported files to the output stream. If 15 | // the specified file has already been streamed, does nothing and immediately 16 | // invokes the callback. Otherwise, the file is streamed in chunks, with 17 | // imports expanded and resolved as necessary. 18 | function streamRecursive(file, callback) { 19 | if (file in fileMap) return void callback(null); 20 | fileMap[file] = true; 21 | 22 | // Create a serialized queue with an initial guarding callback. This guard 23 | // ensures that the queue does not end prematurely; it only ends when the 24 | // entirety of the input file has been streamed, including all imports. 25 | var c, q = queue(1).defer(function(callback) { c = callback; }); 26 | 27 | // The "error" and "end" events can be sent immediately to the guard 28 | // callback, so that streaming terminates immediately on error or end. 29 | // Otherwise, imports are streamed recursively and chunks are sent serially. 30 | emitImports(file) 31 | .on("error", c) 32 | .on("import", function(file) { q.defer(streamRecursive, file); }) 33 | .on("data", function(data) { q.defer(function(callback) { s.write(data, callback); }); }) 34 | .on("end", c); 35 | 36 | // This last callback is only invoked when the file is fully streamed. 37 | q.awaitAll(callback); 38 | } 39 | 40 | // Stream each file serially. 41 | files.forEach(function(file) { 42 | q.defer(streamRecursive, expandFile(file)); 43 | }); 44 | 45 | // When all files are streamed, or an error occurs, we're done! 46 | q.awaitAll(function(error) { 47 | if (error) s.emit("error", error); 48 | else s.end(); 49 | }); 50 | 51 | return s; 52 | }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smash", 3 | "version": "0.0.15", 4 | "description": "Concatenate files together using import statements.", 5 | "keywords": [ 6 | "import", 7 | "module" 8 | ], 9 | "author": { 10 | "name": "Mike Bostock", 11 | "url": "http://bost.ocks.org/mike" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/mbostock/smash.git" 16 | }, 17 | "main": "index.js", 18 | "bin": { 19 | "smash": "smash" 20 | }, 21 | "engines": { 22 | "node": ">=0.10.0" 23 | }, 24 | "engineStrict": true, 25 | "dependencies": { 26 | "optimist": "0.3.x", 27 | "queue-async": "1.0.x" 28 | }, 29 | "devDependencies": { 30 | "vows": "0.7.x" 31 | }, 32 | "scripts": { 33 | "test": "node_modules/.bin/vows; echo" 34 | }, 35 | "licenses": [ 36 | { 37 | "type": "BSD", 38 | "url": "https://github.com/mbostock/smash/blob/master/LICENSE" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /smash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var smash = require("./"), 4 | optimist = require("optimist"); 5 | 6 | var argv = optimist 7 | .usage("Usage: \033[1msmash\033[0m [options] [file …]\n\n" 8 | 9 | + "Version: " + smash.version + "\n\n" 10 | 11 | + "Concatenates one or more input files, outputting a single merged file.\n" 12 | + "Any import statements in the input files are expanded in-place to the\n" 13 | + "contents of the imported file. If the same file is imported multiple\n" 14 | + "times, only the first instance of the file is included.") 15 | 16 | .options("list", { 17 | describe: "output a list of imported files", 18 | type: "boolean", 19 | default: false 20 | }) 21 | .options("ignore-missing", { 22 | describe: "ignore missing files instead of throwing an error; applies only to --list and --graph", 23 | type: "boolean", 24 | default: false 25 | }) 26 | .options("graph", { 27 | describe: "output the import network in Makefile format", 28 | type: "boolean", 29 | default: false 30 | }) 31 | .options("help", { 32 | describe: "display this helpful message", 33 | type: "boolean", 34 | default: false 35 | }) 36 | .check(function(argv) { 37 | if (argv.help) return; 38 | if (!argv._.length) throw new Error("input required"); 39 | if (argv.list && argv.graph) throw new Error("--list and --graph are exclusive"); 40 | }) 41 | .argv; 42 | 43 | if (argv.help) return optimist.showHelp(); 44 | 45 | if (argv.graph) return void smash.readGraph(argv._, argv, function(error, files) { 46 | if (error) throw error; 47 | for (var file in files) console.log(file + ": " + files[file].join(" ")); 48 | }); 49 | 50 | if (argv.list) return void smash.readAllImports(argv._, argv, function(error, files) { 51 | if (error) throw error; 52 | console.log(files.join("\n")); 53 | }); 54 | 55 | smash(argv._).pipe(process.stdout); 56 | -------------------------------------------------------------------------------- /test/data/bar.js: -------------------------------------------------------------------------------- 1 | var bar; 2 | -------------------------------------------------------------------------------- /test/data/baz.js: -------------------------------------------------------------------------------- 1 | var baz; 2 | -------------------------------------------------------------------------------- /test/data/commented-import.js: -------------------------------------------------------------------------------- 1 | // import "foo"; 2 | var bar; 3 | -------------------------------------------------------------------------------- /test/data/empty-lines.js: -------------------------------------------------------------------------------- 1 | // before an empty line 2 | 3 | // after an empty line 4 | // before two empty lines 5 | 6 | 7 | // after two empty lines 8 | -------------------------------------------------------------------------------- /test/data/foo.js: -------------------------------------------------------------------------------- 1 | var foo; 2 | -------------------------------------------------------------------------------- /test/data/forty-two.js: -------------------------------------------------------------------------------- 1 | import "foo"; 2 | 3 | foo = 42; 4 | bar = 41; 5 | -------------------------------------------------------------------------------- /test/data/import-empty-lines.js: -------------------------------------------------------------------------------- 1 | import "empty-lines"; 2 | -------------------------------------------------------------------------------- /test/data/imports-circular-bar.js: -------------------------------------------------------------------------------- 1 | import "imports-circular-foo"; 2 | var bar; 3 | -------------------------------------------------------------------------------- /test/data/imports-circular-foo-expected.js: -------------------------------------------------------------------------------- 1 | var bar; 2 | var foo; 3 | -------------------------------------------------------------------------------- /test/data/imports-circular-foo.js: -------------------------------------------------------------------------------- 1 | import "imports-circular-bar"; 2 | var foo; 3 | -------------------------------------------------------------------------------- /test/data/imports-foo-bar-baz-expected.js: -------------------------------------------------------------------------------- 1 | var foo; 2 | var bar; 3 | var baz; 4 | -------------------------------------------------------------------------------- /test/data/imports-foo-bar-baz.js: -------------------------------------------------------------------------------- 1 | import "foo"; 2 | import "bar"; 3 | import "baz"; 4 | -------------------------------------------------------------------------------- /test/data/imports-foo-expected.js: -------------------------------------------------------------------------------- 1 | var foo; 2 | var bar; 3 | -------------------------------------------------------------------------------- /test/data/imports-foo-foo-bar-foo-expected.js: -------------------------------------------------------------------------------- 1 | var foo; 2 | var bar; 3 | -------------------------------------------------------------------------------- /test/data/imports-foo-foo-bar-foo.js: -------------------------------------------------------------------------------- 1 | import "foo"; 2 | import "foo"; 3 | import "bar"; 4 | import "foo"; 5 | -------------------------------------------------------------------------------- /test/data/imports-foo.js: -------------------------------------------------------------------------------- 1 | import "foo"; 2 | var bar; 3 | -------------------------------------------------------------------------------- /test/data/imports-imports-foo-expected.js: -------------------------------------------------------------------------------- 1 | var foo; 2 | var bar; 3 | var baz; 4 | -------------------------------------------------------------------------------- /test/data/imports-imports-foo.js: -------------------------------------------------------------------------------- 1 | import "imports-foo"; 2 | var baz; 3 | -------------------------------------------------------------------------------- /test/data/imports-index.js: -------------------------------------------------------------------------------- 1 | import "./"; 2 | -------------------------------------------------------------------------------- /test/data/imports-not-found.js: -------------------------------------------------------------------------------- 1 | import "not-found"; 2 | var ruhroh; 3 | -------------------------------------------------------------------------------- /test/data/imports-self.js: -------------------------------------------------------------------------------- 1 | import "imports-self"; 2 | var foo; 3 | -------------------------------------------------------------------------------- /test/data/index.js: -------------------------------------------------------------------------------- 1 | var index; 2 | -------------------------------------------------------------------------------- /test/data/invalid-import-syntax.js: -------------------------------------------------------------------------------- 1 | import foo; 2 | var bar; 3 | -------------------------------------------------------------------------------- /test/data/mismatched-quotes.js: -------------------------------------------------------------------------------- 1 | import 'foo"; 2 | var bar; 3 | -------------------------------------------------------------------------------- /test/data/not-commented-import-expected.js: -------------------------------------------------------------------------------- 1 | /* 2 | var foo; 3 | */ 4 | var bar; 5 | -------------------------------------------------------------------------------- /test/data/not-commented-import.js: -------------------------------------------------------------------------------- 1 | /* 2 | import "foo"; 3 | */ 4 | var bar; 5 | -------------------------------------------------------------------------------- /test/data/single-quote-import-expected.js: -------------------------------------------------------------------------------- 1 | var foo; 2 | var bar; 3 | -------------------------------------------------------------------------------- /test/data/single-quote-import.js: -------------------------------------------------------------------------------- 1 | import 'foo'; 2 | var bar; 3 | -------------------------------------------------------------------------------- /test/data/trailing-comment-import-expected.js: -------------------------------------------------------------------------------- 1 | var foo; 2 | var bar; 3 | var bar; 4 | -------------------------------------------------------------------------------- /test/data/trailing-comment-import.js: -------------------------------------------------------------------------------- 1 | import "foo"; // This is a comment. 2 | import "bar"// And so is this. 3 | var bar; 4 | -------------------------------------------------------------------------------- /test/expandFile-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | expandFile = require("../lib/smash/expand-file"); 4 | 5 | var suite = vows.describe("smash.expandFile"); 6 | 7 | suite.addBatch({ 8 | "expandFile": { 9 | "adds the specified file extension if necessary": function() { 10 | assert.equal("foo.js", expandFile("foo", ".js")); 11 | assert.equal("foo.coffee", expandFile("foo", ".coffee")); 12 | }, 13 | "adds index.extension if the file has a trailing slash": function() { 14 | assert.equal("foo/index.js", expandFile("foo/", ".js")); 15 | assert.equal("foo/index.coffee", expandFile("foo/", ".coffee")); 16 | }, 17 | "does nothing if the file already has an extension": function() { 18 | assert.equal("foo.js", expandFile("foo.js")); 19 | assert.equal("foo.js", expandFile("foo.js", ".coffee")); 20 | }, 21 | "uses the specified extension, even if it is the empty string": function() { 22 | assert.equal("foo", expandFile("foo", "")); 23 | assert.equal("foo/index", expandFile("foo/", "")); 24 | }, 25 | "coerces the specified extension to a string": function() { 26 | assert.equal("foo.1", expandFile("foo", {toString: function() { return ".1"; }})); 27 | assert.equal("foo/index1", expandFile("foo/", 1)); 28 | }, 29 | "does not require a \".\" in the file extension": function() { 30 | assert.equal("foo_bar", expandFile("foo", "_bar")); 31 | assert.equal("foo/index_bar", expandFile("foo/", "_bar")); 32 | }, 33 | "uses the specified extension, even if it is falsey": function() { 34 | assert.equal("foofalse", expandFile("foo", false)); 35 | assert.equal("foo/indexfalse", expandFile("foo/", false)); 36 | assert.equal("foo0", expandFile("foo", 0)); 37 | assert.equal("foo/index0", expandFile("foo/", 0)); 38 | }, 39 | "uses the default extension (.js), if not specified": function() { 40 | assert.equal("foo.js", expandFile("foo")); 41 | assert.equal("foo/index.js", expandFile("foo/")); 42 | }, 43 | "uses the default extension (.js), if null or undefined": function() { 44 | assert.equal("foo.js", expandFile("foo", null)); 45 | assert.equal("foo/index.js", expandFile("foo/", null)); 46 | assert.equal("foo.js", expandFile("foo", undefined)); 47 | assert.equal("foo/index.js", expandFile("foo/", undefined)); 48 | } 49 | } 50 | }); 51 | 52 | suite.export(module); 53 | -------------------------------------------------------------------------------- /test/load-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | smash = require("../"); 4 | 5 | var suite = vows.describe("smash.load"); 6 | 7 | suite.addBatch({ 8 | "load": { 9 | "on a simple file": { 10 | topic: function() { 11 | smash.load(["test/data/forty-two"], "foo", this.callback); 12 | }, 13 | "returns the evaluated expression": function(foo) { 14 | assert.strictEqual(foo, 42); 15 | }, 16 | "does not pollute the global namespace": function(foo) { 17 | assert.equal(typeof bar, "undefined"); 18 | } 19 | }, 20 | "with an object literal expression": { 21 | topic: function() { 22 | smash.load(["test/data/forty-two"], "{foo: foo}", this.callback); 23 | }, 24 | "returns the evaluated expression": function(foo) { 25 | assert.deepEqual(foo, {foo: 42}); 26 | } 27 | } 28 | } 29 | }); 30 | 31 | suite.export(module); 32 | -------------------------------------------------------------------------------- /test/readAllImports-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | smash = require("../"); 4 | 5 | var suite = vows.describe("smash.readAllImports"); 6 | 7 | suite.addBatch({ 8 | "readAllImports": { 9 | "on a file with no imports": { 10 | topic: function() { 11 | smash.readAllImports(["test/data/foo.js"], this.callback); 12 | }, 13 | "returns only the input file": function(imports) { 14 | assert.deepEqual(imports, ["test/data/foo.js"]); 15 | } 16 | }, 17 | "on a file with imports with trailing comments": { 18 | topic: function() { 19 | smash.readAllImports(["test/data/trailing-comment-import.js"], this.callback); 20 | }, 21 | "returns the empty array": function(imports) { 22 | assert.deepEqual(imports, ["test/data/foo.js", "test/data/bar.js", "test/data/trailing-comment-import.js"]); 23 | } 24 | }, 25 | "on a file with invalid import syntax": { 26 | topic: function() { 27 | var callback = this.callback; 28 | smash.readAllImports(["test/data/invalid-import-syntax.js"], function(error) { 29 | callback(null, error); 30 | }); 31 | }, 32 | "throws an error with the expected message": function(error) { 33 | assert.deepEqual(error.message, "invalid import: test/data/invalid-import-syntax.js:0: import foo;"); 34 | } 35 | }, 36 | "on a file with that imports a file that does not exist": { 37 | topic: function() { 38 | var callback = this.callback; 39 | smash.readAllImports(["test/data/imports-not-found.js"], function(error) { 40 | callback(null, error); 41 | }); 42 | }, 43 | "throws an error with the expected message": function(error) { 44 | assert.equal(error.code, "ENOENT"); 45 | assert.equal(error.path, "test/data/not-found.js"); 46 | } 47 | }, 48 | "on a file with that imports a file that does not exist with --ignore-missing": { 49 | topic: function() { 50 | smash.readAllImports(["test/data/imports-not-found.js"], {"ignore-missing": true}, this.callback); 51 | }, 52 | "returns the expected imports": function(imports) { 53 | assert.deepEqual(imports, ["test/data/not-found.js", "test/data/imports-not-found.js"]); 54 | } 55 | }, 56 | "on a file with a commented-out import": { 57 | topic: function() { 58 | smash.readAllImports(["test/data/commented-import.js"], this.callback); 59 | }, 60 | "ignores the commented-out input": function(imports) { 61 | assert.deepEqual(imports, ["test/data/commented-import.js"]); 62 | } 63 | }, 64 | "on a file with a not-commented-out import": { 65 | topic: function() { 66 | smash.readAllImports(["test/data/not-commented-import.js"], this.callback); 67 | }, 68 | "does not ignore the not-commented-out import": function(imports) { 69 | assert.deepEqual(imports, ["test/data/foo.js", "test/data/not-commented-import.js"]); 70 | } 71 | }, 72 | "on a file with one import": { 73 | topic: function() { 74 | smash.readAllImports(["test/data/imports-foo.js"], this.callback); 75 | }, 76 | "returns the expected import followed by the input file": function(imports) { 77 | assert.deepEqual(imports, ["test/data/foo.js", "test/data/imports-foo.js"]); 78 | } 79 | }, 80 | "on a file with multiple imports": { 81 | topic: function() { 82 | smash.readAllImports(["test/data/imports-foo-bar-baz.js"], this.callback); 83 | }, 84 | "returns the imports in order of declaration": function(imports) { 85 | assert.deepEqual(imports, ["test/data/foo.js", "test/data/bar.js", "test/data/baz.js", "test/data/imports-foo-bar-baz.js"]); 86 | } 87 | }, 88 | "on a file with nested imports": { 89 | topic: function() { 90 | smash.readAllImports(["test/data/imports-imports-foo.js"], this.callback); 91 | }, 92 | "returns the imports in order of dependency": function(imports) { 93 | assert.deepEqual(imports, ["test/data/foo.js", "test/data/imports-foo.js", "test/data/imports-imports-foo.js"]); 94 | } 95 | }, 96 | "on multiple input files": { 97 | topic: function() { 98 | smash.readAllImports(["test/data/foo.js", "test/data/bar.js", "test/data/baz.js"], this.callback); 99 | }, 100 | "returns the expected imports, in order": function(imports) { 101 | assert.deepEqual(imports, ["test/data/foo.js", "test/data/bar.js", "test/data/baz.js"]); 102 | } 103 | }, 104 | "with redundant input files": { 105 | topic: function() { 106 | smash.readAllImports(["test/data/foo.js", "test/data/foo.js"], this.callback); 107 | }, 108 | "ignores the redundant imports": function(imports) { 109 | assert.deepEqual(imports, ["test/data/foo.js"]); 110 | } 111 | }, 112 | "when a file that imports itself": { 113 | topic: function() { 114 | smash.readAllImports(["test/data/imports-self.js"], this.callback); 115 | }, 116 | "the self-import has no effect": function(imports) { 117 | assert.deepEqual(imports, ["test/data/imports-self.js"]); 118 | } 119 | }, 120 | "when circular imports are encountered": { 121 | topic: function() { 122 | smash.readAllImports(["test/data/imports-circular-foo.js"], this.callback); 123 | }, 124 | "imports are returned in arbtirary order": function(imports) { 125 | assert.deepEqual(imports, ["test/data/imports-circular-bar.js", "test/data/imports-circular-foo.js"]); 126 | } 127 | } 128 | } 129 | }); 130 | 131 | suite.export(module); 132 | -------------------------------------------------------------------------------- /test/readGraph-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | smash = require("../"); 4 | 5 | var suite = vows.describe("smash.readGraph"); 6 | 7 | suite.addBatch({ 8 | "readGraph": { 9 | "on a file with no imports": { 10 | topic: function() { 11 | smash.readGraph(["test/data/foo.js"], this.callback); 12 | }, 13 | "returns only the input file": function(imports) { 14 | assert.deepEqual(imports, { 15 | "test/data/foo.js": [] 16 | }); 17 | } 18 | }, 19 | "on a file with imports with trailing comments": { 20 | topic: function() { 21 | smash.readGraph(["test/data/trailing-comment-import.js"], this.callback); 22 | }, 23 | "returns the empty array": function(imports) { 24 | assert.deepEqual(imports, { 25 | "test/data/trailing-comment-import.js": ["test/data/foo.js", "test/data/bar.js"], 26 | "test/data/foo.js": [], 27 | "test/data/bar.js": [] 28 | }); 29 | } 30 | }, 31 | "on a file with invalid import syntax": { 32 | topic: function() { 33 | var callback = this.callback; 34 | smash.readGraph(["test/data/invalid-import-syntax.js"], function(error) { 35 | callback(null, error); 36 | }); 37 | }, 38 | "throws an error with the expected message": function(error) { 39 | assert.deepEqual(error.message, "invalid import: test/data/invalid-import-syntax.js:0: import foo;"); 40 | } 41 | }, 42 | "on a file with that imports a file that does not exist": { 43 | topic: function() { 44 | var callback = this.callback; 45 | smash.readGraph(["test/data/imports-not-found.js"], function(error) { 46 | callback(null, error); 47 | }); 48 | }, 49 | "throws an error with the expected message": function(error) { 50 | assert.equal(error.code, "ENOENT"); 51 | assert.equal(error.path, "test/data/not-found.js"); 52 | } 53 | }, 54 | "on a file with that imports a file that does not exist with --ignore-missing": { 55 | topic: function() { 56 | smash.readGraph(["test/data/imports-not-found.js"], {"ignore-missing": true}, this.callback); 57 | }, 58 | "returns the empty array": function(imports) { 59 | assert.deepEqual(imports, { 60 | "test/data/imports-not-found.js": ["test/data/not-found.js"], 61 | "test/data/not-found.js": [] 62 | }); 63 | } 64 | }, 65 | "on a file with a commented-out import": { 66 | topic: function() { 67 | smash.readGraph(["test/data/commented-import.js"], this.callback); 68 | }, 69 | "ignores the commented-out input": function(imports) { 70 | assert.deepEqual(imports, { 71 | "test/data/commented-import.js": [] 72 | }); 73 | } 74 | }, 75 | "on a file with a not-commented-out import": { 76 | topic: function() { 77 | smash.readGraph(["test/data/not-commented-import.js"], this.callback); 78 | }, 79 | "does not ignore the not-commented-out import": function(imports) { 80 | assert.deepEqual(imports, { 81 | "test/data/not-commented-import.js": ["test/data/foo.js"], 82 | "test/data/foo.js": [] 83 | }); 84 | } 85 | }, 86 | "on a file with one import": { 87 | topic: function() { 88 | smash.readGraph(["test/data/imports-foo.js"], this.callback); 89 | }, 90 | "returns the expected import followed by the input file": function(imports) { 91 | assert.deepEqual(imports, { 92 | "test/data/imports-foo.js": ["test/data/foo.js"], 93 | "test/data/foo.js": [] 94 | }); 95 | } 96 | }, 97 | "on a file with multiple imports": { 98 | topic: function() { 99 | smash.readGraph(["test/data/imports-foo-bar-baz.js"], this.callback); 100 | }, 101 | "returns the imports in order of declaration": function(imports) { 102 | assert.deepEqual(imports, { 103 | "test/data/imports-foo-bar-baz.js": ["test/data/foo.js", "test/data/bar.js", "test/data/baz.js"], 104 | "test/data/foo.js": [], 105 | "test/data/bar.js": [], 106 | "test/data/baz.js": [] 107 | }); 108 | } 109 | }, 110 | "on a file with nested imports": { 111 | topic: function() { 112 | smash.readGraph(["test/data/imports-imports-foo.js"], this.callback); 113 | }, 114 | "returns the imports in order of dependency": function(imports) { 115 | assert.deepEqual(imports, { 116 | "test/data/imports-imports-foo.js": ["test/data/imports-foo.js"], 117 | "test/data/imports-foo.js": ["test/data/foo.js"], 118 | "test/data/foo.js": [] 119 | }); 120 | } 121 | }, 122 | "on multiple input files": { 123 | topic: function() { 124 | smash.readGraph(["test/data/foo.js", "test/data/bar.js", "test/data/baz.js"], this.callback); 125 | }, 126 | "returns the expected imports": function(imports) { 127 | assert.deepEqual(imports, { 128 | "test/data/foo.js": [], 129 | "test/data/bar.js": [], 130 | "test/data/baz.js": [] 131 | }); 132 | } 133 | }, 134 | "with redundant input files": { 135 | topic: function() { 136 | smash.readGraph(["test/data/foo.js", "test/data/foo.js"], this.callback); 137 | }, 138 | "ignores the redundant imports": function(imports) { 139 | assert.deepEqual(imports, { 140 | "test/data/foo.js": [] 141 | }); 142 | } 143 | }, 144 | "when a file that imports itself": { 145 | topic: function() { 146 | smash.readGraph(["test/data/imports-self.js"], this.callback); 147 | }, 148 | "returns a self-import": function(imports) { 149 | assert.deepEqual(imports, { 150 | "test/data/imports-self.js": ["test/data/imports-self.js"] 151 | }); 152 | } 153 | }, 154 | "when circular imports are encountered": { 155 | topic: function() { 156 | smash.readGraph(["test/data/imports-circular-foo.js"], this.callback); 157 | }, 158 | "returns circular imports": function(imports) { 159 | assert.deepEqual(imports, { 160 | "test/data/imports-circular-foo.js": ["test/data/imports-circular-bar.js"], 161 | "test/data/imports-circular-bar.js": ["test/data/imports-circular-foo.js"] 162 | }); 163 | } 164 | } 165 | } 166 | }); 167 | 168 | suite.export(module); 169 | -------------------------------------------------------------------------------- /test/readImports-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | smash = require("../"); 4 | 5 | var suite = vows.describe("smash.readImports"); 6 | 7 | suite.addBatch({ 8 | "readImports": { 9 | "on a file with no imports": { 10 | topic: function() { 11 | smash.readImports("test/data/foo.js", this.callback); 12 | }, 13 | "returns the empty array": function(imports) { 14 | assert.deepEqual(imports, []); 15 | } 16 | }, 17 | "on a file with imports with trailing comments": { 18 | topic: function() { 19 | smash.readImports("test/data/trailing-comment-import.js", this.callback); 20 | }, 21 | "returns the empty array": function(imports) { 22 | assert.deepEqual(imports, ["test/data/foo.js", "test/data/bar.js"]); 23 | } 24 | }, 25 | "on a file with invalid import syntax": { 26 | topic: function() { 27 | var callback = this.callback; 28 | smash.readImports("test/data/invalid-import-syntax.js", function(error) { 29 | callback(null, error); 30 | }); 31 | }, 32 | "throws an error with the expected message": function(error) { 33 | assert.deepEqual(error.message, "invalid import: test/data/invalid-import-syntax.js:0: import foo;"); 34 | } 35 | }, 36 | "on a file with that imports a file that does not exist": { 37 | topic: function() { 38 | smash.readImports("test/data/imports-not-found.js", this.callback); 39 | }, 40 | "returns the expected import": function(imports) { 41 | assert.deepEqual(imports, ["test/data/not-found.js"]); 42 | } 43 | }, 44 | "on a file with a commented-out import": { 45 | topic: function() { 46 | smash.readImports("test/data/commented-import.js", this.callback); 47 | }, 48 | "ignores the commented-out input": function(imports) { 49 | assert.deepEqual(imports, []); 50 | } 51 | }, 52 | "on a file with a not-commented-out import": { 53 | topic: function() { 54 | smash.readImports("test/data/not-commented-import.js", this.callback); 55 | }, 56 | "does not ignore the not-commented-out import": function(imports) { 57 | assert.deepEqual(imports, ["test/data/foo.js"]); 58 | } 59 | }, 60 | "on a file with one import": { 61 | topic: function() { 62 | smash.readImports("test/data/imports-foo.js", this.callback); 63 | }, 64 | "returns the expected import": function(imports) { 65 | assert.deepEqual(imports, ["test/data/foo.js"]); 66 | } 67 | }, 68 | "on a file with multiple imports": { 69 | topic: function() { 70 | smash.readImports("test/data/imports-foo-bar-baz.js", this.callback); 71 | }, 72 | "returns the expected imports, in order": function(imports) { 73 | assert.deepEqual(imports, ["test/data/foo.js", "test/data/bar.js", "test/data/baz.js"]); 74 | } 75 | }, 76 | "on a file with multiple redundant imports": { 77 | topic: function() { 78 | smash.readImports("test/data/imports-foo-foo-bar-foo.js", this.callback); 79 | }, 80 | "returns all imports, in order": function(imports) { 81 | assert.deepEqual(imports, ["test/data/foo.js", "test/data/foo.js", "test/data/bar.js", "test/data/foo.js"]); 82 | } 83 | }, 84 | "on a file with nested imports": { 85 | topic: function() { 86 | smash.readImports("test/data/imports-imports-foo.js", this.callback); 87 | }, 88 | "returns the expected imports, in order": function(imports) { 89 | assert.deepEqual(imports, ["test/data/imports-foo.js"]); 90 | } 91 | }, 92 | "on a file that imports itself": { 93 | topic: function() { 94 | smash.readImports("test/data/imports-self.js", this.callback); 95 | }, 96 | "returns the expected import": function(imports) { 97 | assert.deepEqual(imports, ["test/data/imports-self.js"]); 98 | } 99 | } 100 | } 101 | }); 102 | 103 | suite.export(module); 104 | -------------------------------------------------------------------------------- /test/smash-test.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | assert = require("assert"), 3 | fs = require("fs"), 4 | stream = require("stream"), 5 | smash = require("../"); 6 | 7 | var suite = vows.describe("smash"); 8 | 9 | suite.addBatch({ 10 | "smash": { 11 | "on a file with no imports": testCase(["test/data/foo.js"], "test/data/foo.js"), 12 | "on a file with imports with trailing comments": testCase(["test/data/trailing-comment-import.js"], "test/data/trailing-comment-import-expected.js"), 13 | "on a file with single-quote import syntax": testCase(["test/data/single-quote-import.js"], "test/data/single-quote-import-expected.js"), 14 | "on a file with mismatched quote delimiters": testFailureCase(["test/data/mismatched-quotes.js"], {message: "invalid import: test/data/mismatched-quotes.js:0: import 'foo\";"}), 15 | "on a file with invalid import syntax": testFailureCase(["test/data/invalid-import-syntax.js"], {message: "invalid import: test/data/invalid-import-syntax.js:0: import foo;"}), 16 | "on a file with that imports a file that does not exist": testFailureCase(["test/data/imports-not-found.js"], {code: "ENOENT", path: "test/data/not-found.js"}), 17 | "on a file with a commented-out import": testCase(["test/data/commented-import.js"], "test/data/commented-import.js"), 18 | "on a file with a not-commented-out import": testCase(["test/data/not-commented-import.js"], "test/data/not-commented-import-expected.js"), 19 | "on a file with one import": testCase(["test/data/imports-foo.js"], "test/data/imports-foo-expected.js"), 20 | "on a file with multiple imports": testCase(["test/data/imports-foo-bar-baz.js"], "test/data/imports-foo-bar-baz-expected.js"), 21 | "on a file with nested imports": testCase(["test/data/imports-imports-foo.js"], "test/data/imports-imports-foo-expected.js"), 22 | "on a file with empty lines": testCase(["test/data/empty-lines.js"], "test/data/empty-lines.js"), 23 | "on a file which imports a file with empty lines": testCase(["test/data/import-empty-lines.js"], "test/data/empty-lines.js"), 24 | "on multiple input files": testCase(["test/data/foo.js", "test/data/bar.js", "test/data/baz.js"], "test/data/imports-foo-bar-baz-expected.js"), 25 | "with redundant input files": testCase(["test/data/foo.js", "test/data/foo.js"], "test/data/foo.js"), 26 | "on a file with multiple redundant imports": testCase(["test/data/imports-foo-foo-bar-foo.js"], "test/data/imports-foo-foo-bar-foo-expected.js"), 27 | "when a file imports itself": testCase(["test/data/imports-self.js"], "test/data/foo.js"), 28 | "when circular imports are encountered": testCase(["test/data/imports-circular-foo.js"], "test/data/imports-circular-foo-expected.js"), 29 | "when the input is a directory": testCase(["test/data/"], "test/data/index.js"), 30 | "when the input is missing a file extension": testCase(["test/data/imports-index"], "test/data/index.js") 31 | } 32 | }); 33 | 34 | suite.export(module); 35 | 36 | function testCase(inputs, expected) { 37 | return { 38 | topic: function() { 39 | smash(inputs).pipe(testStream(this.callback)); 40 | }, 41 | "produces the expected output": function(actual) { 42 | assert.deepEqual(actual, fs.readFileSync(expected, "utf8")); 43 | } 44 | }; 45 | } 46 | 47 | function testFailureCase(inputs, expected) { 48 | return { 49 | topic: function() { 50 | var callback = this.callback; 51 | smash(inputs).on("error", function(error) { 52 | callback(null, error); 53 | }); 54 | }, 55 | "produces the expected error message": function(error) { 56 | for (var key in expected) { 57 | assert.equal(error[key], expected[key]); 58 | } 59 | } 60 | }; 61 | } 62 | 63 | function testStream(callback) { 64 | var s = new stream.Writable, chunks = []; 65 | 66 | s._write = function(chunk, encoding, callback) { 67 | chunks.push(chunk); 68 | callback(); 69 | }; 70 | 71 | s.on("error", callback); 72 | s.on("finish", function() { callback(null, Buffer.concat(chunks).toString("utf8")); }); 73 | return s; 74 | } 75 | --------------------------------------------------------------------------------