pax_global_header00006660000000000000000000000064126154052660014521gustar00rootroot0000000000000052 comment=2f39efe2aa44b78105ecd516e190b392e3ba7eea smash-0.0.15/000077500000000000000000000000001261540526600127175ustar00rootroot00000000000000smash-0.0.15/.gitignore000066400000000000000000000000271261540526600147060ustar00rootroot00000000000000.DS_Store node_modules smash-0.0.15/LICENSE000066400000000000000000000026201261540526600137240ustar00rootroot00000000000000Copyright (c) 2013, Michael Bostock All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The name Michael Bostock may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. smash-0.0.15/README.md000066400000000000000000000010141261540526600141720ustar00rootroot00000000000000# SMASH SMASH TOGETHER FILES! PROBABLY JAVASCRIPT. SAY THIS foo.js: ```js import "bar"; function foo() { return "foo" + bar(); } ``` AND THIS bar.js: ```js function bar() { return "bar"; } ``` WHEN SMASH TOGETHER foo.js AND bar.js: ```js function bar() { return "bar"; } function foo() { return "foo" + bar(); } ``` SMASH HANDLE CIRCULAR AND REDUNDANT IMPORTS GOOD. SMASH GOOD. SMASH. SMASH LIKE MAKE, TOO. ```Makefile bundle.js: $(shell smash --list src/bundle.js) smash src/bundle.js > bundle.js ``` smash-0.0.15/index.js000066400000000000000000000005011261540526600143600ustar00rootroot00000000000000var smash = module.exports = require("./lib/smash/smash"); smash.version = require("./package").version; smash.load = require("./lib/smash/load"); smash.readGraph = require("./lib/smash/read-graph"); smash.readAllImports = require("./lib/smash/read-all-imports"); smash.readImports = require("./lib/smash/read-imports"); smash-0.0.15/lib/000077500000000000000000000000001261540526600134655ustar00rootroot00000000000000smash-0.0.15/lib/smash/000077500000000000000000000000001261540526600146005ustar00rootroot00000000000000smash-0.0.15/lib/smash/emit-imports.js000066400000000000000000000027261261540526600175760ustar00rootroot00000000000000var path = require("path"), events = require("events"), emitLines = require("./emit-lines"), expandFile = require("./expand-file"); // Returns an emitter for the specified file. The returned emitter emits // "import" events whenever an import statement is encountered, and "data" // events whenever normal text is encountered, in addition to the standard // "error" and "end" events. module.exports = function(file) { var emitter = new events.EventEmitter(), directory = path.dirname(file), extension = path.extname(file), lines = []; file = expandFile(file, extension); var lineEmitter = emitLines(file) .on("line", line) .on("end", end) .on("error", error); function line(line, number) { if (/^import\b/.test(line)) { flush(); var match = /^import\s+(["'])([^"']+)(\1)\s*;?\s*(?:\/\/.*)?$/.exec(line); if (match) { emitter.emit("import", path.join(directory, expandFile(match[2], extension))); } else { lineEmitter.removeAllListeners(); // ignore subsequent lines error(new Error("invalid import: " + file + ":" + number + ": " + line)); } } else { lines.push(line, newline); } } function flush() { if (lines.length) emitter.emit("data", Buffer.concat(lines)), lines = []; } function end() { flush(); emitter.emit("end"); } function error(e) { emitter.emit("error", e); } return emitter; }; var newline = new Buffer("\n"); smash-0.0.15/lib/smash/emit-lines.js000066400000000000000000000032001261540526600171770ustar00rootroot00000000000000var fs = require("fs"), events = require("events"); // Returns an emitter for the specified file. The returned emitter emits "line" // events for each line in the file, in addition to the standard "error" and // "end" events. module.exports = function(file) { var emitter = new events.EventEmitter(), lineFragments = [], lineNumber = -1; fs.createReadStream(file) .on("data", data) .on("end", end) .on("error", error); function data(chunk) { var i = 0, n = chunk.length; // Join queued line fragments, if any, with first line in new chunk. if (lineFragments.length) { var k = 0; while (i < n) { var c = chunk[i++]; if (c === 10) { ++k; break; } // \n if (c === 13) { ++k; if (chunk[i] === 10) ++i, ++k; break; } // \r or \r\n } lineFragments.push(chunk.slice(0, i - k)); if (k) emitter.emit("line", Buffer.concat(lineFragments), ++lineNumber), lineFragments = []; else return; } // Find subsequent lines in new chunk. while (true) { var i0 = i, k = 0; while (i < n) { var c = chunk[i++]; if (c === 10) { ++k; break; } // \n if (c === 13) { ++k; if (chunk[i] === 10) ++i, ++k; break; } // \r or \r\n } var line = chunk.slice(i0, i - k); if (k) emitter.emit("line", line, ++lineNumber); else { if (i0 !== i) lineFragments.push(line); break; } } } function end() { if (lineFragments.length) emitter.emit("line", Buffer.concat(lineFragments), ++lineNumber); emitter.emit("end"); } function error(e) { emitter.emit("error", e); } return emitter; }; smash-0.0.15/lib/smash/expand-file.js000066400000000000000000000004621261540526600173340ustar00rootroot00000000000000var path = require("path"); module.exports = function(file, extension) { if (extension == null) extension = defaultExtension; else extension += ""; if (/\/$/.test(file)) file += "index" + extension; else if (!path.extname(file)) file += extension; return file; }; var defaultExtension = ".js"; smash-0.0.15/lib/smash/load.js000066400000000000000000000012471261540526600160610ustar00rootroot00000000000000var vm = require("vm"), smash = require("./smash"); // Loads the specified files and their imports, then evaluates the specified // expression in the context of the concatenated code. module.exports = function(files, expression, sandbox, callback) { if (arguments.length < 4) callback = sandbox, sandbox = undefined; var chunks = []; smash(files) .on("error", callback) .on("data", function(chunk) { chunks.push(chunk); }) .on("end", function() { var error, result; try { result = vm.runInNewContext(chunks.join("") + ";(" + expression + ")", sandbox); } catch (e) { error = e; } callback(error, result); }); }; smash-0.0.15/lib/smash/read-all-imports.js000066400000000000000000000023441261540526600203150ustar00rootroot00000000000000var queue = require("queue-async"), expandFile = require("./expand-file"), readImports = require("./read-imports"); // Reads all the imports from the specified files, returning an array of files. // The returned array is in dependency order and only contains unique entries. // The returned arrays also includes any input files at the end. module.exports = function(files, options, callback) { if (typeof options === "function") callback = options, options = {}; var fileMap = {}, allFiles = []; function readRecursive(file, callback) { if (file in fileMap) return callback(null); fileMap[file] = true; readImports(file, function(error, files) { if (error) { if (options["ignore-missing"] && error.code === "ENOENT") files = []; else return void callback(error); } var q = queue(1); files.forEach(function(file) { q.defer(readRecursive, file); }); q.awaitAll(function(error) { if (!error) allFiles.push(file); callback(error); }); }); } var q = queue(1); files.forEach(function(file) { q.defer(readRecursive, expandFile(file)); }); q.awaitAll(function(error) { callback(error, error ? null : allFiles); }); }; smash-0.0.15/lib/smash/read-graph.js000066400000000000000000000022421261540526600171500ustar00rootroot00000000000000var queue = require("queue-async"), expandFile = require("./expand-file"), readImports = require("./read-imports"); // Returns the network of imports, starting with the specified input files. // For each file in the returned map, an array specifies the set of files // immediately imported by that file. This array is in order of import, and may // contain duplicate entries. module.exports = function(files, options, callback) { if (typeof options === "function") callback = options, options = {}; var fileMap = {}; function readRecursive(file, callback) { if (file in fileMap) return callback(null); readImports(file, function(error, files) { if (error) { if (options["ignore-missing"] && error.code === "ENOENT") files = []; else return void callback(error); } var q = queue(1); fileMap[file] = files; files.forEach(function(file) { q.defer(readRecursive, file); }); q.awaitAll(callback); }); } var q = queue(1); files.forEach(function(file) { q.defer(readRecursive, expandFile(file)); }); q.awaitAll(function(error) { callback(error, error ? null : fileMap); }); }; smash-0.0.15/lib/smash/read-imports.js000066400000000000000000000011671261540526600175510ustar00rootroot00000000000000var emitImports = require("./emit-imports"); // Reads the import statements from the specified file, returning an array of // files. Unlike readAllImports, this does not recursively traverse import // statements; it only returns import statements in the specified input file. // Also unlike readAllImports, this method returns every import statement, // including redundant imports and self-imports. module.exports = function(file, callback) { var files = []; emitImports(file) .on("import", function(file) { files.push(file); }) .on("error", callback) .on("end", function() { callback(null, files); }); }; smash-0.0.15/lib/smash/smash.js000066400000000000000000000040121261540526600162460ustar00rootroot00000000000000var stream = require("stream"), queue = require("queue-async"), expandFile = require("./expand-file"), emitImports = require("./emit-imports"); // Returns a readable stream for the specified files. // All imports are expanded the first time they are encountered. // Subsequent redundant imports are ignored. module.exports = function(files) { var s = new stream.PassThrough({encoding: "utf8", decodeStrings: false}), q = queue(1), fileMap = {}; // Streams the specified file and any imported files to the output stream. If // the specified file has already been streamed, does nothing and immediately // invokes the callback. Otherwise, the file is streamed in chunks, with // imports expanded and resolved as necessary. function streamRecursive(file, callback) { if (file in fileMap) return void callback(null); fileMap[file] = true; // Create a serialized queue with an initial guarding callback. This guard // ensures that the queue does not end prematurely; it only ends when the // entirety of the input file has been streamed, including all imports. var c, q = queue(1).defer(function(callback) { c = callback; }); // The "error" and "end" events can be sent immediately to the guard // callback, so that streaming terminates immediately on error or end. // Otherwise, imports are streamed recursively and chunks are sent serially. emitImports(file) .on("error", c) .on("import", function(file) { q.defer(streamRecursive, file); }) .on("data", function(data) { q.defer(function(callback) { s.write(data, callback); }); }) .on("end", c); // This last callback is only invoked when the file is fully streamed. q.awaitAll(callback); } // Stream each file serially. files.forEach(function(file) { q.defer(streamRecursive, expandFile(file)); }); // When all files are streamed, or an error occurs, we're done! q.awaitAll(function(error) { if (error) s.emit("error", error); else s.end(); }); return s; }; smash-0.0.15/package.json000066400000000000000000000014151261540526600152060ustar00rootroot00000000000000{ "name": "smash", "version": "0.0.15", "description": "Concatenate files together using import statements.", "keywords": [ "import", "module" ], "author": { "name": "Mike Bostock", "url": "http://bost.ocks.org/mike" }, "repository": { "type": "git", "url": "https://github.com/mbostock/smash.git" }, "main": "index.js", "bin": { "smash": "smash" }, "engines": { "node": ">=0.10.0" }, "engineStrict": true, "dependencies": { "optimist": "0.3.x", "queue-async": "1.0.x" }, "devDependencies": { "vows": "0.7.x" }, "scripts": { "test": "node_modules/.bin/vows; echo" }, "licenses": [ { "type": "BSD", "url": "https://github.com/mbostock/smash/blob/master/LICENSE" } ] } smash-0.0.15/smash000077500000000000000000000032721261540526600137640ustar00rootroot00000000000000#!/usr/bin/env node var smash = require("./"), optimist = require("optimist"); var argv = optimist .usage("Usage: \033[1msmash\033[0m [options] [file …]\n\n" + "Version: " + smash.version + "\n\n" + "Concatenates one or more input files, outputting a single merged file.\n" + "Any import statements in the input files are expanded in-place to the\n" + "contents of the imported file. If the same file is imported multiple\n" + "times, only the first instance of the file is included.") .options("list", { describe: "output a list of imported files", type: "boolean", default: false }) .options("ignore-missing", { describe: "ignore missing files instead of throwing an error; applies only to --list and --graph", type: "boolean", default: false }) .options("graph", { describe: "output the import network in Makefile format", type: "boolean", default: false }) .options("help", { describe: "display this helpful message", type: "boolean", default: false }) .check(function(argv) { if (argv.help) return; if (!argv._.length) throw new Error("input required"); if (argv.list && argv.graph) throw new Error("--list and --graph are exclusive"); }) .argv; if (argv.help) return optimist.showHelp(); if (argv.graph) return void smash.readGraph(argv._, argv, function(error, files) { if (error) throw error; for (var file in files) console.log(file + ": " + files[file].join(" ")); }); if (argv.list) return void smash.readAllImports(argv._, argv, function(error, files) { if (error) throw error; console.log(files.join("\n")); }); smash(argv._).pipe(process.stdout); smash-0.0.15/test/000077500000000000000000000000001261540526600136765ustar00rootroot00000000000000smash-0.0.15/test/data/000077500000000000000000000000001261540526600146075ustar00rootroot00000000000000smash-0.0.15/test/data/bar.js000066400000000000000000000000111261540526600157010ustar00rootroot00000000000000var bar; smash-0.0.15/test/data/baz.js000066400000000000000000000000111261540526600157110ustar00rootroot00000000000000var baz; smash-0.0.15/test/data/commented-import.js000066400000000000000000000000321261540526600204230ustar00rootroot00000000000000// import "foo"; var bar; smash-0.0.15/test/data/empty-lines.js000066400000000000000000000001451261540526600174130ustar00rootroot00000000000000// before an empty line // after an empty line // before two empty lines // after two empty lines smash-0.0.15/test/data/foo.js000066400000000000000000000000111261540526600157200ustar00rootroot00000000000000var foo; smash-0.0.15/test/data/forty-two.js000066400000000000000000000000431261540526600171140ustar00rootroot00000000000000import "foo"; foo = 42; bar = 41; smash-0.0.15/test/data/import-empty-lines.js000066400000000000000000000000261261540526600207210ustar00rootroot00000000000000import "empty-lines"; smash-0.0.15/test/data/imports-circular-bar.js000066400000000000000000000000501261540526600212010ustar00rootroot00000000000000import "imports-circular-foo"; var bar; smash-0.0.15/test/data/imports-circular-foo-expected.js000066400000000000000000000000221261540526600230160ustar00rootroot00000000000000var bar; var foo; smash-0.0.15/test/data/imports-circular-foo.js000066400000000000000000000000501261540526600212200ustar00rootroot00000000000000import "imports-circular-bar"; var foo; smash-0.0.15/test/data/imports-foo-bar-baz-expected.js000066400000000000000000000000331261540526600225320ustar00rootroot00000000000000var foo; var bar; var baz; smash-0.0.15/test/data/imports-foo-bar-baz.js000066400000000000000000000000521261540526600207340ustar00rootroot00000000000000import "foo"; import "bar"; import "baz"; smash-0.0.15/test/data/imports-foo-expected.js000066400000000000000000000000221261540526600212140ustar00rootroot00000000000000var foo; var bar; smash-0.0.15/test/data/imports-foo-foo-bar-foo-expected.js000066400000000000000000000000221261540526600233200ustar00rootroot00000000000000var foo; var bar; smash-0.0.15/test/data/imports-foo-foo-bar-foo.js000066400000000000000000000000701261540526600215240ustar00rootroot00000000000000import "foo"; import "foo"; import "bar"; import "foo"; smash-0.0.15/test/data/imports-foo.js000066400000000000000000000000271261540526600174220ustar00rootroot00000000000000import "foo"; var bar; smash-0.0.15/test/data/imports-imports-foo-expected.js000066400000000000000000000000331261540526600227110ustar00rootroot00000000000000var foo; var bar; var baz; smash-0.0.15/test/data/imports-imports-foo.js000066400000000000000000000000371261540526600211160ustar00rootroot00000000000000import "imports-foo"; var baz; smash-0.0.15/test/data/imports-index.js000066400000000000000000000000151261540526600177430ustar00rootroot00000000000000import "./"; smash-0.0.15/test/data/imports-not-found.js000066400000000000000000000000401261540526600205430ustar00rootroot00000000000000import "not-found"; var ruhroh; smash-0.0.15/test/data/imports-self.js000066400000000000000000000000401261540526600175630ustar00rootroot00000000000000import "imports-self"; var foo; smash-0.0.15/test/data/index.js000066400000000000000000000000131261540526600162460ustar00rootroot00000000000000var index; smash-0.0.15/test/data/invalid-import-syntax.js000066400000000000000000000000251261540526600214240ustar00rootroot00000000000000import foo; var bar; smash-0.0.15/test/data/mismatched-quotes.js000066400000000000000000000000271261540526600206000ustar00rootroot00000000000000import 'foo"; var bar; smash-0.0.15/test/data/not-commented-import-expected.js000066400000000000000000000000301261540526600230160ustar00rootroot00000000000000/* var foo; */ var bar; smash-0.0.15/test/data/not-commented-import.js000066400000000000000000000000351261540526600212240ustar00rootroot00000000000000/* import "foo"; */ var bar; smash-0.0.15/test/data/single-quote-import-expected.js000066400000000000000000000000221261540526600226620ustar00rootroot00000000000000var foo; var bar; smash-0.0.15/test/data/single-quote-import.js000066400000000000000000000000271261540526600210700ustar00rootroot00000000000000import 'foo'; var bar; smash-0.0.15/test/data/trailing-comment-import-expected.js000066400000000000000000000000331261540526600235210ustar00rootroot00000000000000var foo; var bar; var bar; smash-0.0.15/test/data/trailing-comment-import.js000066400000000000000000000001141261540526600217220ustar00rootroot00000000000000import "foo"; // This is a comment. import "bar"// And so is this. var bar; smash-0.0.15/test/expandFile-test.js000066400000000000000000000043171261540526600172750ustar00rootroot00000000000000var vows = require("vows"), assert = require("assert"), expandFile = require("../lib/smash/expand-file"); var suite = vows.describe("smash.expandFile"); suite.addBatch({ "expandFile": { "adds the specified file extension if necessary": function() { assert.equal("foo.js", expandFile("foo", ".js")); assert.equal("foo.coffee", expandFile("foo", ".coffee")); }, "adds index.extension if the file has a trailing slash": function() { assert.equal("foo/index.js", expandFile("foo/", ".js")); assert.equal("foo/index.coffee", expandFile("foo/", ".coffee")); }, "does nothing if the file already has an extension": function() { assert.equal("foo.js", expandFile("foo.js")); assert.equal("foo.js", expandFile("foo.js", ".coffee")); }, "uses the specified extension, even if it is the empty string": function() { assert.equal("foo", expandFile("foo", "")); assert.equal("foo/index", expandFile("foo/", "")); }, "coerces the specified extension to a string": function() { assert.equal("foo.1", expandFile("foo", {toString: function() { return ".1"; }})); assert.equal("foo/index1", expandFile("foo/", 1)); }, "does not require a \".\" in the file extension": function() { assert.equal("foo_bar", expandFile("foo", "_bar")); assert.equal("foo/index_bar", expandFile("foo/", "_bar")); }, "uses the specified extension, even if it is falsey": function() { assert.equal("foofalse", expandFile("foo", false)); assert.equal("foo/indexfalse", expandFile("foo/", false)); assert.equal("foo0", expandFile("foo", 0)); assert.equal("foo/index0", expandFile("foo/", 0)); }, "uses the default extension (.js), if not specified": function() { assert.equal("foo.js", expandFile("foo")); assert.equal("foo/index.js", expandFile("foo/")); }, "uses the default extension (.js), if null or undefined": function() { assert.equal("foo.js", expandFile("foo", null)); assert.equal("foo/index.js", expandFile("foo/", null)); assert.equal("foo.js", expandFile("foo", undefined)); assert.equal("foo/index.js", expandFile("foo/", undefined)); } } }); suite.export(module); smash-0.0.15/test/load-test.js000066400000000000000000000014561261540526600161360ustar00rootroot00000000000000var vows = require("vows"), assert = require("assert"), smash = require("../"); var suite = vows.describe("smash.load"); suite.addBatch({ "load": { "on a simple file": { topic: function() { smash.load(["test/data/forty-two"], "foo", this.callback); }, "returns the evaluated expression": function(foo) { assert.strictEqual(foo, 42); }, "does not pollute the global namespace": function(foo) { assert.equal(typeof bar, "undefined"); } }, "with an object literal expression": { topic: function() { smash.load(["test/data/forty-two"], "{foo: foo}", this.callback); }, "returns the evaluated expression": function(foo) { assert.deepEqual(foo, {foo: 42}); } } } }); suite.export(module); smash-0.0.15/test/readAllImports-test.js000066400000000000000000000120561261540526600201370ustar00rootroot00000000000000var vows = require("vows"), assert = require("assert"), smash = require("../"); var suite = vows.describe("smash.readAllImports"); suite.addBatch({ "readAllImports": { "on a file with no imports": { topic: function() { smash.readAllImports(["test/data/foo.js"], this.callback); }, "returns only the input file": function(imports) { assert.deepEqual(imports, ["test/data/foo.js"]); } }, "on a file with imports with trailing comments": { topic: function() { smash.readAllImports(["test/data/trailing-comment-import.js"], this.callback); }, "returns the empty array": function(imports) { assert.deepEqual(imports, ["test/data/foo.js", "test/data/bar.js", "test/data/trailing-comment-import.js"]); } }, "on a file with invalid import syntax": { topic: function() { var callback = this.callback; smash.readAllImports(["test/data/invalid-import-syntax.js"], function(error) { callback(null, error); }); }, "throws an error with the expected message": function(error) { assert.deepEqual(error.message, "invalid import: test/data/invalid-import-syntax.js:0: import foo;"); } }, "on a file with that imports a file that does not exist": { topic: function() { var callback = this.callback; smash.readAllImports(["test/data/imports-not-found.js"], function(error) { callback(null, error); }); }, "throws an error with the expected message": function(error) { assert.equal(error.code, "ENOENT"); assert.equal(error.path, "test/data/not-found.js"); } }, "on a file with that imports a file that does not exist with --ignore-missing": { topic: function() { smash.readAllImports(["test/data/imports-not-found.js"], {"ignore-missing": true}, this.callback); }, "returns the expected imports": function(imports) { assert.deepEqual(imports, ["test/data/not-found.js", "test/data/imports-not-found.js"]); } }, "on a file with a commented-out import": { topic: function() { smash.readAllImports(["test/data/commented-import.js"], this.callback); }, "ignores the commented-out input": function(imports) { assert.deepEqual(imports, ["test/data/commented-import.js"]); } }, "on a file with a not-commented-out import": { topic: function() { smash.readAllImports(["test/data/not-commented-import.js"], this.callback); }, "does not ignore the not-commented-out import": function(imports) { assert.deepEqual(imports, ["test/data/foo.js", "test/data/not-commented-import.js"]); } }, "on a file with one import": { topic: function() { smash.readAllImports(["test/data/imports-foo.js"], this.callback); }, "returns the expected import followed by the input file": function(imports) { assert.deepEqual(imports, ["test/data/foo.js", "test/data/imports-foo.js"]); } }, "on a file with multiple imports": { topic: function() { smash.readAllImports(["test/data/imports-foo-bar-baz.js"], this.callback); }, "returns the imports in order of declaration": function(imports) { assert.deepEqual(imports, ["test/data/foo.js", "test/data/bar.js", "test/data/baz.js", "test/data/imports-foo-bar-baz.js"]); } }, "on a file with nested imports": { topic: function() { smash.readAllImports(["test/data/imports-imports-foo.js"], this.callback); }, "returns the imports in order of dependency": function(imports) { assert.deepEqual(imports, ["test/data/foo.js", "test/data/imports-foo.js", "test/data/imports-imports-foo.js"]); } }, "on multiple input files": { topic: function() { smash.readAllImports(["test/data/foo.js", "test/data/bar.js", "test/data/baz.js"], this.callback); }, "returns the expected imports, in order": function(imports) { assert.deepEqual(imports, ["test/data/foo.js", "test/data/bar.js", "test/data/baz.js"]); } }, "with redundant input files": { topic: function() { smash.readAllImports(["test/data/foo.js", "test/data/foo.js"], this.callback); }, "ignores the redundant imports": function(imports) { assert.deepEqual(imports, ["test/data/foo.js"]); } }, "when a file that imports itself": { topic: function() { smash.readAllImports(["test/data/imports-self.js"], this.callback); }, "the self-import has no effect": function(imports) { assert.deepEqual(imports, ["test/data/imports-self.js"]); } }, "when circular imports are encountered": { topic: function() { smash.readAllImports(["test/data/imports-circular-foo.js"], this.callback); }, "imports are returned in arbtirary order": function(imports) { assert.deepEqual(imports, ["test/data/imports-circular-bar.js", "test/data/imports-circular-foo.js"]); } } } }); suite.export(module); smash-0.0.15/test/readGraph-test.js000066400000000000000000000132671261540526600171170ustar00rootroot00000000000000var vows = require("vows"), assert = require("assert"), smash = require("../"); var suite = vows.describe("smash.readGraph"); suite.addBatch({ "readGraph": { "on a file with no imports": { topic: function() { smash.readGraph(["test/data/foo.js"], this.callback); }, "returns only the input file": function(imports) { assert.deepEqual(imports, { "test/data/foo.js": [] }); } }, "on a file with imports with trailing comments": { topic: function() { smash.readGraph(["test/data/trailing-comment-import.js"], this.callback); }, "returns the empty array": function(imports) { assert.deepEqual(imports, { "test/data/trailing-comment-import.js": ["test/data/foo.js", "test/data/bar.js"], "test/data/foo.js": [], "test/data/bar.js": [] }); } }, "on a file with invalid import syntax": { topic: function() { var callback = this.callback; smash.readGraph(["test/data/invalid-import-syntax.js"], function(error) { callback(null, error); }); }, "throws an error with the expected message": function(error) { assert.deepEqual(error.message, "invalid import: test/data/invalid-import-syntax.js:0: import foo;"); } }, "on a file with that imports a file that does not exist": { topic: function() { var callback = this.callback; smash.readGraph(["test/data/imports-not-found.js"], function(error) { callback(null, error); }); }, "throws an error with the expected message": function(error) { assert.equal(error.code, "ENOENT"); assert.equal(error.path, "test/data/not-found.js"); } }, "on a file with that imports a file that does not exist with --ignore-missing": { topic: function() { smash.readGraph(["test/data/imports-not-found.js"], {"ignore-missing": true}, this.callback); }, "returns the empty array": function(imports) { assert.deepEqual(imports, { "test/data/imports-not-found.js": ["test/data/not-found.js"], "test/data/not-found.js": [] }); } }, "on a file with a commented-out import": { topic: function() { smash.readGraph(["test/data/commented-import.js"], this.callback); }, "ignores the commented-out input": function(imports) { assert.deepEqual(imports, { "test/data/commented-import.js": [] }); } }, "on a file with a not-commented-out import": { topic: function() { smash.readGraph(["test/data/not-commented-import.js"], this.callback); }, "does not ignore the not-commented-out import": function(imports) { assert.deepEqual(imports, { "test/data/not-commented-import.js": ["test/data/foo.js"], "test/data/foo.js": [] }); } }, "on a file with one import": { topic: function() { smash.readGraph(["test/data/imports-foo.js"], this.callback); }, "returns the expected import followed by the input file": function(imports) { assert.deepEqual(imports, { "test/data/imports-foo.js": ["test/data/foo.js"], "test/data/foo.js": [] }); } }, "on a file with multiple imports": { topic: function() { smash.readGraph(["test/data/imports-foo-bar-baz.js"], this.callback); }, "returns the imports in order of declaration": function(imports) { assert.deepEqual(imports, { "test/data/imports-foo-bar-baz.js": ["test/data/foo.js", "test/data/bar.js", "test/data/baz.js"], "test/data/foo.js": [], "test/data/bar.js": [], "test/data/baz.js": [] }); } }, "on a file with nested imports": { topic: function() { smash.readGraph(["test/data/imports-imports-foo.js"], this.callback); }, "returns the imports in order of dependency": function(imports) { assert.deepEqual(imports, { "test/data/imports-imports-foo.js": ["test/data/imports-foo.js"], "test/data/imports-foo.js": ["test/data/foo.js"], "test/data/foo.js": [] }); } }, "on multiple input files": { topic: function() { smash.readGraph(["test/data/foo.js", "test/data/bar.js", "test/data/baz.js"], this.callback); }, "returns the expected imports": function(imports) { assert.deepEqual(imports, { "test/data/foo.js": [], "test/data/bar.js": [], "test/data/baz.js": [] }); } }, "with redundant input files": { topic: function() { smash.readGraph(["test/data/foo.js", "test/data/foo.js"], this.callback); }, "ignores the redundant imports": function(imports) { assert.deepEqual(imports, { "test/data/foo.js": [] }); } }, "when a file that imports itself": { topic: function() { smash.readGraph(["test/data/imports-self.js"], this.callback); }, "returns a self-import": function(imports) { assert.deepEqual(imports, { "test/data/imports-self.js": ["test/data/imports-self.js"] }); } }, "when circular imports are encountered": { topic: function() { smash.readGraph(["test/data/imports-circular-foo.js"], this.callback); }, "returns circular imports": function(imports) { assert.deepEqual(imports, { "test/data/imports-circular-foo.js": ["test/data/imports-circular-bar.js"], "test/data/imports-circular-bar.js": ["test/data/imports-circular-foo.js"] }); } } } }); suite.export(module); smash-0.0.15/test/readImports-test.js000066400000000000000000000070661261540526600175130ustar00rootroot00000000000000var vows = require("vows"), assert = require("assert"), smash = require("../"); var suite = vows.describe("smash.readImports"); suite.addBatch({ "readImports": { "on a file with no imports": { topic: function() { smash.readImports("test/data/foo.js", this.callback); }, "returns the empty array": function(imports) { assert.deepEqual(imports, []); } }, "on a file with imports with trailing comments": { topic: function() { smash.readImports("test/data/trailing-comment-import.js", this.callback); }, "returns the empty array": function(imports) { assert.deepEqual(imports, ["test/data/foo.js", "test/data/bar.js"]); } }, "on a file with invalid import syntax": { topic: function() { var callback = this.callback; smash.readImports("test/data/invalid-import-syntax.js", function(error) { callback(null, error); }); }, "throws an error with the expected message": function(error) { assert.deepEqual(error.message, "invalid import: test/data/invalid-import-syntax.js:0: import foo;"); } }, "on a file with that imports a file that does not exist": { topic: function() { smash.readImports("test/data/imports-not-found.js", this.callback); }, "returns the expected import": function(imports) { assert.deepEqual(imports, ["test/data/not-found.js"]); } }, "on a file with a commented-out import": { topic: function() { smash.readImports("test/data/commented-import.js", this.callback); }, "ignores the commented-out input": function(imports) { assert.deepEqual(imports, []); } }, "on a file with a not-commented-out import": { topic: function() { smash.readImports("test/data/not-commented-import.js", this.callback); }, "does not ignore the not-commented-out import": function(imports) { assert.deepEqual(imports, ["test/data/foo.js"]); } }, "on a file with one import": { topic: function() { smash.readImports("test/data/imports-foo.js", this.callback); }, "returns the expected import": function(imports) { assert.deepEqual(imports, ["test/data/foo.js"]); } }, "on a file with multiple imports": { topic: function() { smash.readImports("test/data/imports-foo-bar-baz.js", this.callback); }, "returns the expected imports, in order": function(imports) { assert.deepEqual(imports, ["test/data/foo.js", "test/data/bar.js", "test/data/baz.js"]); } }, "on a file with multiple redundant imports": { topic: function() { smash.readImports("test/data/imports-foo-foo-bar-foo.js", this.callback); }, "returns all imports, in order": function(imports) { assert.deepEqual(imports, ["test/data/foo.js", "test/data/foo.js", "test/data/bar.js", "test/data/foo.js"]); } }, "on a file with nested imports": { topic: function() { smash.readImports("test/data/imports-imports-foo.js", this.callback); }, "returns the expected imports, in order": function(imports) { assert.deepEqual(imports, ["test/data/imports-foo.js"]); } }, "on a file that imports itself": { topic: function() { smash.readImports("test/data/imports-self.js", this.callback); }, "returns the expected import": function(imports) { assert.deepEqual(imports, ["test/data/imports-self.js"]); } } } }); suite.export(module); smash-0.0.15/test/smash-test.js000066400000000000000000000073441261540526600163340ustar00rootroot00000000000000var vows = require("vows"), assert = require("assert"), fs = require("fs"), stream = require("stream"), smash = require("../"); var suite = vows.describe("smash"); suite.addBatch({ "smash": { "on a file with no imports": testCase(["test/data/foo.js"], "test/data/foo.js"), "on a file with imports with trailing comments": testCase(["test/data/trailing-comment-import.js"], "test/data/trailing-comment-import-expected.js"), "on a file with single-quote import syntax": testCase(["test/data/single-quote-import.js"], "test/data/single-quote-import-expected.js"), "on a file with mismatched quote delimiters": testFailureCase(["test/data/mismatched-quotes.js"], {message: "invalid import: test/data/mismatched-quotes.js:0: import 'foo\";"}), "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;"}), "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"}), "on a file with a commented-out import": testCase(["test/data/commented-import.js"], "test/data/commented-import.js"), "on a file with a not-commented-out import": testCase(["test/data/not-commented-import.js"], "test/data/not-commented-import-expected.js"), "on a file with one import": testCase(["test/data/imports-foo.js"], "test/data/imports-foo-expected.js"), "on a file with multiple imports": testCase(["test/data/imports-foo-bar-baz.js"], "test/data/imports-foo-bar-baz-expected.js"), "on a file with nested imports": testCase(["test/data/imports-imports-foo.js"], "test/data/imports-imports-foo-expected.js"), "on a file with empty lines": testCase(["test/data/empty-lines.js"], "test/data/empty-lines.js"), "on a file which imports a file with empty lines": testCase(["test/data/import-empty-lines.js"], "test/data/empty-lines.js"), "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"), "with redundant input files": testCase(["test/data/foo.js", "test/data/foo.js"], "test/data/foo.js"), "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"), "when a file imports itself": testCase(["test/data/imports-self.js"], "test/data/foo.js"), "when circular imports are encountered": testCase(["test/data/imports-circular-foo.js"], "test/data/imports-circular-foo-expected.js"), "when the input is a directory": testCase(["test/data/"], "test/data/index.js"), "when the input is missing a file extension": testCase(["test/data/imports-index"], "test/data/index.js") } }); suite.export(module); function testCase(inputs, expected) { return { topic: function() { smash(inputs).pipe(testStream(this.callback)); }, "produces the expected output": function(actual) { assert.deepEqual(actual, fs.readFileSync(expected, "utf8")); } }; } function testFailureCase(inputs, expected) { return { topic: function() { var callback = this.callback; smash(inputs).on("error", function(error) { callback(null, error); }); }, "produces the expected error message": function(error) { for (var key in expected) { assert.equal(error[key], expected[key]); } } }; } function testStream(callback) { var s = new stream.Writable, chunks = []; s._write = function(chunk, encoding, callback) { chunks.push(chunk); callback(); }; s.on("error", callback); s.on("finish", function() { callback(null, Buffer.concat(chunks).toString("utf8")); }); return s; }