diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..b16d3d7 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,85 @@ +{ + "maxerr" : 50, // {int} Maximum error before stopping + + // Enforcing + "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) + "camelcase" : true, // true: Identifiers must be in camelCase + "curly" : false, // true: Require {} for every new block or scope + "eqeqeq" : false, // true: Require triple equals (===) for comparison + "forin" : false, // true: Require filtering for..in loops with obj.hasOwnProperty() + "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` + "indent" : false, // {int} Number of spaces to use for indentation + "latedef" : false, // true: Require variables/functions to be defined before being used + "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` + "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` + "noempty" : true, // true: Prohibit use of empty blocks + "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) + "plusplus" : false, // true: Prohibit use of `++` & `--` + "quotmark" : false, // Quotation mark consistency: + // false : do nothing (default) + // true : ensure whatever is used is consistent + // "single" : require single quotes + // "double" : require double quotes + "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) + "unused" : "vars", // true: Require all defined variables be used + "strict" : false, // true: Requires all functions run in ES5 Strict Mode + "trailing" : false, // true: Prohibit trailing whitespaces + "maxparams" : false, // {int} Max number of formal params allowed per function + "maxdepth" : false, // {int} Max depth of nested blocks (within functions) + "maxstatements" : false, // {int} Max number statements per function + "maxcomplexity" : false, // {int} Max cyclomatic complexity per function + "maxlen" : false, // {int} Max number of characters per line + + // Relaxing + "asi" : true, // true: Tolerate Automatic Semicolon Insertion (no semicolons) + "boss" : true, // true: Tolerate assignments where comparisons would be expected + "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. + "eqnull" : true, // true: Tolerate use of `== null` + "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) + "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) + // (ex: `for each`, multiple try/catch, function expression…) + "evil" : true, // true: Tolerate use of `eval` and `new Function()` + "expr" : false, // true: Tolerate `ExpressionStatement` as Programs + "funcscope" : false, // true: Tolerate defining variables inside control statements" + "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') + "iterator" : false, // true: Tolerate using the `__iterator__` property + "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block + "laxbreak" : false, // true: Tolerate possibly unsafe line breakings + "laxcomma" : false, // true: Tolerate comma-first style coding + "loopfunc" : true, // true: Tolerate functions being defined in loops + "multistr" : false, // true: Tolerate multi-line strings + "proto" : false, // true: Tolerate using the `__proto__` property + "scripturl" : false, // true: Tolerate script-targeted URLs + "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment + "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` + "sub" : true, // true: Tolerate using `[]` notation when it can still be expressed in dot notation + "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` + "validthis" : false, // true: Tolerate using this in a non-constructor function + + // Environments + "browser" : false, // Web Browser (window, document, etc) + "couch" : false, // CouchDB + "devel" : false, // Development/debugging (alert, confirm, etc) + "dojo" : false, // Dojo Toolkit + "jquery" : false, // jQuery + "mootools" : false, // MooTools + "node" : true, // Node.js + "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) + "prototypejs" : false, // Prototype and Scriptaculous + "rhino" : false, // Rhino + "worker" : false, // Web Workers + "wsh" : false, // Windows Scripting Host + "yui" : false, // Yahoo User Interface + + // Legacy + "nomen" : false, // true: Prohibit dangling `_` in variables + "onevar" : false, // true: Allow only one `var` statement per function + "passfail" : false, // true: Stop on first error + "white" : false, // true: Check against strict whitespace and indentation rules + + // Custom Globals + "globals" : {}, // additional predefined global variables + + // Suppress specific warnings + "-W058": true // Missing '()' invoking a constructor. +} \ No newline at end of file diff --git a/index.js b/index.js index 3bdf689..daec505 100644 --- a/index.js +++ b/index.js @@ -2,22 +2,22 @@ var _ = require('lodash'); var fs = require('fs'); var path = require('path'); var RSVP = require('rsvp'); -var quickTemp = require('quick-temp'); var mkdirp = require('mkdirp'); var browserify = require('browserify'); -var walkSync = require('walk-sync'); var glob = require('glob'); var through = require('through2'); var xtend = require('xtend'); var hashTree = require('broccoli-kitchen-sink-helpers').hashTree; +var BrocPlugin = require('broccoli-plugin'); -function FastBrowserify(inputTree, options) { - if (!(this instanceof FastBrowserify)) { return new FastBrowserify(inputTree, options); } +function FastBrowserify(inputNode, options) { + if (!(this instanceof FastBrowserify)) { return new FastBrowserify(inputNode, options); } this.options = this.getOptions(options || {}); - this.destDir = quickTemp.makeOrRemake(this, 'tmpDestDir'); - this.inputTree = inputTree; + inputNode = Array.isArray(inputNode) ? inputNode : [inputNode]; + + BrocPlugin.call(this, inputNode, options); this.bundles = {}; this.cache = {}; @@ -25,10 +25,11 @@ function FastBrowserify(inputTree, options) { this.packageCache = {}; } +FastBrowserify.prototype = Object.create(BrocPlugin.prototype); + FastBrowserify.prototype.getOptions = function(options) { var bundleExtension = options.bundleExtension || '.browserify'; var outputExtension = options.outputExtension || '.js'; - var self = this; if (bundleExtension[0] != '.') { bundleExtension = '.' + bundleExtension; } if (outputExtension[0] != '.') { outputExtension = '.' + outputExtension; } @@ -77,194 +78,192 @@ FastBrowserify.prototype.getOptions = function(options) { }, options); }; -FastBrowserify.prototype.cleanup = function() { - quickTemp.remove(this, 'tmpDestDir'); -}; - -FastBrowserify.prototype.read = function(readTree) { +FastBrowserify.prototype.build = function() { var self = this; var promises = []; - return readTree(this.inputTree).then(function(srcDir) { - // remove output files that don't have a corrisponding input file anymore - self.cleanupBundles(); - self.invalidateCache(); + this.destDir = this.outputPath; - for (var bundleNameOrGlob in self.options.bundles) { - var bundleTemplate = self.options.bundles[bundleNameOrGlob]; - var bundleFiles = []; + var srcDir = this.inputPaths[0]; - // If we're dealing with multiple bundle files in a single declaration - // then the bundle key is a glob to the files that serve as the basis to - // determine which bundles to create (they are often the entrypoints to the bundle) - if (bundleTemplate.glob) { - bundleFiles = glob.sync(bundleNameOrGlob, { cwd: srcDir, mark: true }); - } else { - // If we're dealing with a single bundle declaration, then the bundle key - // is the path to the output bundle, and the user must specify a list of - // entryPoints - bundleFiles = [bundleNameOrGlob]; - } + // remove output files that don't have a corrisponding input file anymore + this.cleanupBundles(); + this.invalidateCache(); - bundleFiles.forEach(function(relativePath) { - var bundle = self.bundles[relativePath]; + for (var bundleNameOrGlob in this.options.bundles) { + var bundleTemplate = this.options.bundles[bundleNameOrGlob]; + var bundleFiles = []; - if (! bundle) { - var outputBasename; - var outputRelativePath; - var outputAbsolutePath; - var entryPoints; + // If we're dealing with multiple bundle files in a single declaration + // then the bundle key is a glob to the files that serve as the basis to + // determine which bundles to create (they are often the entrypoints to the bundle) + if (bundleTemplate.glob) { + bundleFiles = glob.sync(bundleNameOrGlob, { cwd: srcDir, mark: true }); + } else { + // If we're dealing with a single bundle declaration, then the bundle key + // is the path to the output bundle, and the user must specify a list of + // entryPoints + bundleFiles = [bundleNameOrGlob]; + } - if (bundleTemplate.glob) { - if (! _.isFunction(bundleTemplate.outputPath)) { - throw "When glob == true, outputPath must be a function that returns the output bundle filename"; - } - outputRelativePath = bundleTemplate.outputPath.call(self, relativePath); - } else { - if (bundleTemplate.outputPath) { - throw "outputPath is only valid for glob bundle specifications, specify the output bundle filename in the key of the bundle specification"; - } - outputRelativePath = relativePath; - } + bundleFiles.forEach(function(relativePath) { + var bundle = self.bundles[relativePath]; + + if (! bundle) { + var outputBasename; + var outputRelativePath; + var outputAbsolutePath; + var entryPoints; - // Add on the globally specified output directory if specified - if (self.options.outputDirectory && _.isString(self.options.outputDirectory)) { - outputRelativePath = path.join(self.options.outputDirectory, outputRelativePath); + if (bundleTemplate.glob) { + if (! _.isFunction(bundleTemplate.outputPath)) { + throw "When glob == true, outputPath must be a function that returns the output bundle filename"; } + outputRelativePath = bundleTemplate.outputPath.call(self, relativePath); + } else { + if (bundleTemplate.outputPath) { + throw "outputPath is only valid for glob bundle specifications, specify the output bundle filename in the key of the bundle specification"; + } + outputRelativePath = relativePath; + } - entryPoints = self.readEntryPoints(srcDir, relativePath, bundleTemplate); + // Add on the globally specified output directory if specified + if (self.options.outputDirectory && _.isString(self.options.outputDirectory)) { + outputRelativePath = path.join(self.options.outputDirectory, outputRelativePath); + } - if (entryPoints.absolute.length === 0 && !bundleTemplate.require) { - console.log("Bundle specified by \"", relativePath, "\" does not have any entry files nor required modules."); - } else { - // hash the entryPoints so we can tell if they change so we can update - // the browerify options with the new files - var entryPointsHashes = []; - for (var i = 0; i < entryPoints.absolute.length; ++i) { - entryPointsHashes.push(hashTree(entryPoints.absolute[i])); - } + entryPoints = self.readEntryPoints(srcDir, relativePath, bundleTemplate); - outputBasename = path.basename(outputRelativePath); - outputAbsolutePath = path.resolve(self.destDir, outputRelativePath); - - bundle = { - key: relativePath, - srcDir: srcDir, - template: bundleTemplate, - browserify: null, - entryPoints: entryPoints.absolute, - entryPointsHashes: entryPointsHashes, - outputBasename: outputBasename, - outputFileName: outputAbsolutePath, - browserifyOptions: _.clone(self.options.browserify), - dependentFileNames: {} - }; - - bundle.browserifyOptions = _.extend(bundle.browserifyOptions, { - basedir: srcDir, - cache: self.cache, - packageCache: self.packageCache, - extensions: ['.js', self.options.bundleExtension].concat(self.options.browserify.extensions || []), - entries: entryPoints.relative - }); + if (entryPoints.absolute.length === 0 && !bundleTemplate.require) { + console.log("Bundle specified by \"", relativePath, "\" does not have any entry files nor required modules."); + } else { + // hash the entryPoints so we can tell if they change so we can update + // the browerify options with the new files + var entryPointsHashes = []; + for (var i = 0; i < entryPoints.absolute.length; ++i) { + entryPointsHashes.push(hashTree(entryPoints.absolute[i])); + } - bundle.browserify = browserify(bundle.browserifyOptions); + outputBasename = path.basename(outputRelativePath); + outputAbsolutePath = path.resolve(self.destDir, outputRelativePath); + + bundle = { + key: relativePath, + srcDir: srcDir, + template: bundleTemplate, + browserify: null, + entryPoints: entryPoints.absolute, + entryPointsHashes: entryPointsHashes, + outputBasename: outputBasename, + outputFileName: outputAbsolutePath, + browserifyOptions: _.clone(self.options.browserify), + dependentFileNames: {} + }; + + bundle.browserifyOptions = _.extend(bundle.browserifyOptions, { + basedir: srcDir, + cache: self.cache, + packageCache: self.packageCache, + extensions: ['.js', self.options.bundleExtension].concat(self.options.browserify.extensions || []), + entries: entryPoints.relative + }); + + bundle.browserify = browserify(bundle.browserifyOptions); + + // Set up the external files + [].concat(self.options.externals).concat(bundleTemplate.externals).filter(Boolean).forEach(function(external) { + var externalFile; + var externalSplit = external.split(/:/); + // var externalOptions = { basedir: srcDir }; + var externalOptions = { }; + + if (externalSplit.length === 2) { + externalFile = externalSplit[0]; + var externalExpose = externalSplit[1]; + + externalOptions = xtend({ + expose: externalExpose + }, externalOptions); + } else { + externalFile = external; + } - // Set up the external files - [].concat(self.options.externals).concat(bundleTemplate.externals).filter(Boolean).forEach(function(external) { - var externalFile; - var externalSplit = external.split(/:/); - // var externalOptions = { basedir: srcDir }; - var externalOptions = { }; + if (/^[\/.]/.test(externalFile)) { + // externalFile = path.resolve(srcDir, externalFile); + externalFile = path.resolve(externalFile); + } - if (externalSplit.length === 2) { - externalFile = externalSplit[0]; - var externalExpose = externalSplit[1]; + bundle.browserify.external(externalFile, externalOptions); + }); - externalOptions = xtend({ - expose: externalExpose - }, externalOptions); + if (bundleTemplate.transform) { + // Set up the transforms + bundleTemplate.transform = [].concat(bundleTemplate.transform); + bundleTemplate.transform.forEach(function (transform) { + if (_.isPlainObject(transform)) { + bundle.browserify.transform(transform.tr, transform.options || {}); } else { - externalFile = external; - } - - if (/^[\/.]/.test(externalFile)) { - // externalFile = path.resolve(srcDir, externalFile); - externalFile = path.resolve(externalFile); + bundle.browserify.transform.apply(bundle.browserify, Array.prototype.concat(transform)); } - - bundle.browserify.external(externalFile, externalOptions); }); + } - if (bundleTemplate.transform) { - // Set up the transforms - bundleTemplate.transform = [].concat(bundleTemplate.transform); - bundleTemplate.transform.forEach(function (transform) { - if (_.isPlainObject(transform)) { - bundle.browserify.transform(transform.tr, transform.options || {}); - } else { - bundle.browserify.transform.apply(bundle.browserify, Array.prototype.concat(transform)); - } - }); - } - - if (bundleTemplate.require) { - // Set up the requires - bundleTemplate.require = Array.prototype.concat(bundleTemplate.require); - bundleTemplate.require.forEach(function (require) { - browserify.prototype.require.apply(bundle.browserify, Array.prototype.concat(require)) - }); - } - - // Watch dependencies for changes and invalidate the cache when needed - var collect = function() { - bundle.browserify.pipeline.get('deps').push(through.obj(function(row, enc, next) { - var file = row.expose ? bundle.browserify._expose[row.id] : row.file; - - if (self.cache) { - bundle.browserifyOptions.cache[file] = { - source: row.source, - deps: xtend({}, row.deps) - }; - } - - this.push(row); - next(); - })); - }; - - // Cache the dependencies and re-run the cache when we re-bundle - bundle.browserify.on('reset', collect); - collect(); - - bundle.browserify.on('file', function(file) { - self.watchFiles[file] = hashTree(file); - bundle.dependentFileNames[file] = file; + if (bundleTemplate.require) { + // Set up the requires + bundleTemplate.require = Array.prototype.concat(bundleTemplate.require); + bundleTemplate.require.forEach(function (require) { + browserify.prototype.require.apply(bundle.browserify, Array.prototype.concat(require)) }); + } - bundle.browserify.on('package', function(pkg) { - var packageFile = path.join(pkg.__dirname, 'package.json'); - if (fs.existsSync(packageFile)) { - self.watchFiles[packageFile] = hashTree(packageFile); - bundle.dependentFileNames[packageFile] = packageFile; + // Watch dependencies for changes and invalidate the cache when needed + var collect = function() { + bundle.browserify.pipeline.get('deps').push(through.obj(function(row, enc, next) { + var file = row.expose ? bundle.browserify._expose[row.id] : row.file; + + if (self.cache) { + bundle.browserifyOptions.cache[file] = { + source: row.source, + deps: xtend({}, row.deps) + }; } - }); - self.bundles[relativePath] = bundle; + this.push(row); + next(); + })); + }; + + // Cache the dependencies and re-run the cache when we re-bundle + bundle.browserify.on('reset', collect); + collect(); + + bundle.browserify.on('file', function(file) { + self.watchFiles[file] = hashTree(file); + bundle.dependentFileNames[file] = file; + }); + + bundle.browserify.on('package', function(pkg) { + var packageFile = path.join(pkg.__dirname, 'package.json'); + if (fs.existsSync(packageFile)) { + self.watchFiles[packageFile] = hashTree(packageFile); + bundle.dependentFileNames[packageFile] = packageFile; + } + }); - // Create the target directory in the destination - mkdirp.sync(path.dirname(bundle.outputFileName)); + self.bundles[relativePath] = bundle; - promise = self.bundle(bundle); - promises.push(promise); - } - } - }); - } + // Create the target directory in the destination + mkdirp.sync(path.dirname(bundle.outputFileName)); - return RSVP.all(promises).then(function(outputFiles) { - return self.destDir; + var promise = self.bundle(bundle); + promises.push(promise); + } + } }); + } + + return RSVP.all(promises).then(function(outputFiles) { + return self.destDir; }); }; @@ -325,7 +324,6 @@ FastBrowserify.prototype.invalidateCache = function() { var file; var i; var time; - var fileMTime; for (bundleKey in this.bundles) { bundle = this.bundles[bundleKey]; diff --git a/package.json b/package.json index 891f7a9..8af0c42 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A caching, flexible Browserify filter for Broccoli", "main": "index.js", "scripts": { - "test": "faucet test/*.js" + "test": "tape test/*.js | faucet" }, "repository": { "type": "git", @@ -28,14 +28,13 @@ "homepage": "https://github.com/caleb/broccoli-fast-browserify", "dependencies": { "broccoli-kitchen-sink-helpers": "^0.2.5", + "broccoli-plugin": "^1.2.1", "browserify": "10.2.4", "glob": "^4.3.1", "lodash": "^2.4.1", "mkdirp": "^0.5.0", - "quick-temp": "^0.1.2", "rsvp": "^3.0.14", "through2": "^0.6.5", - "walk-sync": "^0.1.3", "xtend": "^4.0.0" }, "devDependencies": { @@ -45,7 +44,9 @@ "copy-dereference": "^1.0.0", "faucet": "0.0.1", "jshint": "^2.4.4", + "quick-temp": "^0.1.5", "tape": "^3.0.3", - "through": "^2.3.6" + "through": "^2.3.6", + "walk-sync": "^0.2.6" } }