Created
June 3, 2015 19:19
-
-
Save Aeon/2063cbad046fa53749c7 to your computer and use it in GitHub Desktop.
gulp + browserify...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* gulp and site config based on | |
* http://justinjohnson.org/javascript/getting-started-with-gulp-and-browserify/ | |
* https://gist.github.com/Sigmus/9253068 | |
* | |
*/ | |
try { | |
var gulp = require('gulp'), | |
gutil = require('gulp-util'), | |
concat = require("gulp-concat"), | |
forEach = require("gulp-foreach"), | |
jshint = require("gulp-jshint"), | |
rename = require("gulp-rename"), | |
size = require("gulp-size"), | |
sourcemaps = require("gulp-sourcemaps"), | |
uglify = require("gulp-uglify"), | |
util = require("gulp-util"), | |
notify = require("gulp-notify"), | |
file = require("read-file"), | |
log = require("npmlog"), | |
runSequence = require("run-sequence"), | |
shell = require("shelljs"), | |
source = require('vinyl-source-stream'), | |
buffer = require('vinyl-buffer'), | |
browserify = require('browserify'), | |
watchify = require('watchify'), | |
babelify = require('babelify'), | |
reactify = require('reactify'), | |
lrload = require('livereactload'), | |
_ = require("lodash"); | |
} catch (e) { | |
// Unknown error, rethrow it. | |
if (e.code !== "MODULE_NOT_FOUND") { | |
throw e; | |
} | |
// Otherwise, we have a missing dependency. If the module is in the dependency list, the user just needs to run `npm install`. | |
// Otherwise, they need to install and save it. | |
var dependencies = require("./package.json").devDependencies; | |
var module = e.toString().match(/'(.*?)'/)[1]; | |
var command = "npm install"; | |
if (typeof dependencies[module] === "undefined") { | |
command += " --save-dev " + module; | |
} | |
console.error(e.toString() + ". Fix this by executing:\n\n" + command + "\n"); | |
process.exit(1); | |
} | |
/* | |
* Configuration | |
*/ | |
const JS_BASE_DIR = "./client/"; | |
const APPS_GLOB = JS_BASE_DIR + "/**/*.js*"; | |
const APPS_DIST_DIR = "./js/apps/"; | |
const TESTS_GLOB = "./tests/client/**/*.js"; | |
// TODO: add bower for frontend library dependencies? | |
const EXTERNAL_LIBS = { | |
//"jquery": "./node_modules/jquery/dist/jquery.min.js", | |
//"jquery-ui": "./node_modules/jquery-ui/jquery-ui.js", | |
//"highcharts": "./node_modules/highcharts/scripts/highcharts.js", | |
//"highcharts-more": "./node_modules/highcharts/scripts/highcharts-more.js", | |
//"datatables": "./node_modules/datatables/media/js/jquery.dataTables.js", | |
//"select2": "./node_modules/select2/select2.min.js", | |
"react": "./node_modules/react/dist/react-with-addons.js", | |
//"react-jsx": "./node_modules/react/dist/JSXTransformer.js", | |
"marty": "./node_modules/marty/marty.js" | |
}; | |
const BROWSERIFY_DEV_TRANSFORMS = [reactify, babelify, lrload, "brfs"]; | |
const BROWSERIFY_TRANSFORMS = [reactify, babelify, "brfs"]; | |
const LAST_DEPENDENCY_UPDATE_ID_FILE = ".npmDependenciesLastCommitId"; | |
const SIZE_OPTS = { | |
showFiles: true, | |
gzip: true | |
}; | |
const LINT_OPTS = { | |
unused: true, | |
eqnull: true, | |
jquery: true | |
}; | |
const ALLOW_NPM_MODULE_MANAGEMENT = true; | |
process.env.NODE_PATH = JS_BASE_DIR + ":" + (process.env.NODE_PATH || ""); | |
gulp.task("build-common-lib", function() { | |
var paths = []; | |
// Get just the path to each externalizable lib. | |
_.forEach(EXTERNAL_LIBS, function(path) { | |
paths.push(path); | |
}); | |
return gulp.src(paths) | |
// Log each file that will be concatenated into the common.js file. | |
.pipe(size(SIZE_OPTS)) | |
// Concatenate all files. | |
.pipe(concat("common.min.js")) | |
// Minify the result. | |
.pipe(uglify()) | |
// Log the new file size. | |
.pipe(size(SIZE_OPTS)) | |
// Save that file to the appropriate location. | |
.pipe(gulp.dest(APPS_DIST_DIR + "../lib/")); | |
}); | |
/** | |
* Browserify and minify each individual application found with APPS_GLOB. | |
* Each file therein represents a separate application and should have its own resultant bundle. | |
*/ | |
gulp.task("build", function() { | |
var stream = gulp.src(APPS_GLOB) | |
.pipe(forEach(function(stream, file) { | |
bundle(file, getBundler(file)); | |
})); | |
return stream; | |
}); | |
/** | |
* Get a properly configured bundler for manual (browserify) and automatic (watchify) builds. | |
* | |
* @param {object} file The file to bundle (a Vinyl file object). | |
* @param {object|null} options Options passed to browserify. | |
*/ | |
function getBundler(file, options) { | |
var dev = process.env.NODE_ENV != 'production'; | |
options = _.extend(options || {}, { | |
// Enable source maps. | |
debug: dev, | |
// Configure transforms. | |
transform: dev ? BROWSERIFY_DEV_TRANSFORMS : BROWSERIFY_TRANSFORMS | |
}); | |
// Initialize browserify with the file and options provided. | |
var bundler = browserify(file.path, options); | |
// Exclude externalized libs (those from build-common-lib). | |
Object.keys(EXTERNAL_LIBS).forEach(function(lib) { | |
bundler.external(lib); | |
}); | |
return bundler; | |
} | |
/** | |
* Build a single application with browserify creating two differnt versions: one normal and one minified. | |
* | |
* @param {object} file The file to bundle (a Vinyl file object). | |
* @param {browserify|watchify} bundler The bundler to use. The "build" task will use browserify, the "autobuild" task will use watchify. | |
*/ | |
function bundle(file, bundler) { | |
// Remove file.base from file.path to create a relative path. For example, if file looks like | |
// file.base === "/Users/johnsonj/dev/web/super-project/applications/client/<i>apps</i>/" | |
// file.path === "/Users/johnsonj/dev/web/super-project/applications/client/<i>apps</i>/login/reset-password/confirm.js" | |
// then result is "login/reset-password/confirm.js" | |
var relativeFilename = file.path.replace(file.base, ""); | |
return bundler | |
// Log browserify errors | |
.on("error", notifyErrors) | |
// Bundle the application | |
.bundle() | |
// Rename the bundled file to relativeFilename | |
.pipe(source(relativeFilename)) | |
// Convert stream to a buffer | |
.pipe(buffer()) | |
// Save the source map for later (uglify will remove it since it is a comment) | |
.pipe(sourcemaps.init({loadMaps: true})) | |
// Save normal source (useful for debugging) | |
.pipe(gulp.dest(APPS_DIST_DIR)) | |
// Minify source for production | |
.pipe(uglify()) | |
// Restore the sourceMap | |
.pipe(sourcemaps.write()) | |
// Add the .min suffix before the extension | |
.pipe(rename({suffix: ".min"})) | |
// Debuging output | |
.pipe(size(SIZE_OPTS)) | |
// Write the minified file. | |
.pipe(gulp.dest(APPS_DIST_DIR)); | |
} | |
/** | |
* Watch applications and their dependencies for changes and automatically rebuild them. This will keep build times small since | |
* we don't have to manually rebuild all applications everytime we make even the smallest/most isolated of changes. | |
*/ | |
gulp.task("autobuild", function() { | |
return gulp.src(APPS_GLOB) | |
.pipe(forEach(function(stream, file) { | |
var relativeFilename = file.path.replace(file.base, ""); | |
log.info("autobuild", "monitoring " + APPS_DIST_DIR + relativeFilename + " for changes"); | |
// monitor the output file for livereload | |
lrload.monitor(APPS_DIST_DIR + relativeFilename, {displayNotification: true}); | |
// Get our bundler just like in the "build" task, but wrap it with watchify and use the watchify default args (options). | |
var bundler = watchify(getBundler(file, _.extend(watchify.args, { | |
cache: {}, // required for watchify | |
packageCache: {}, // required for watchify | |
fullPaths: true // required to be true only for watchify | |
}))); | |
function rebundle() { | |
return bundle(file, bundler); | |
} | |
// Whenever the application or its dependencies are modified, automatically rebundle the application. | |
bundler.on("update", rebundle); | |
// Rebundle this application now. | |
return rebundle(); | |
})); | |
}); | |
/** | |
* Run tests with tape and cleanup the output with faucet. | |
*/ | |
gulp.task("test", function() { | |
shell.exec("tape " + TESTS_GLOB + " | faucet"); | |
}); | |
/** | |
* Automatically run tests anytime anything is changed (tests or test subjects). | |
*/ | |
gulp.task("autotest", function() { | |
gulp.watch( | |
[JS_BASE_DIR + "**/*.js", TESTS_GLOB], | |
["test"] | |
); | |
}); | |
/** | |
* Linter for the most basic of quality assurance. | |
*/ | |
gulp.task("lint", function() { | |
return gulp.src(JS_BASE_DIR + "**/*.js") | |
.pipe(jshint(LINT_OPTS)) | |
.pipe(jshint.reporter("default")); | |
}); | |
/** | |
* Ensure that our project is always setup correctly. So far this includes two things: | |
* 1. Make sure git hooks are installed | |
* 2. Make sure npm dependencies are current (optional) | |
* | |
* #2 is achieved by keeping track of the last commit ID in which we updated dependencies. If | |
* the current state of the repo does not have that commit ID, then we will update dependencies | |
* and the ID in that file. It's a naive approach, but it works for now. | |
*/ | |
gulp.task("housekeeping", function() { | |
// Ensure that the git client-side hooks are installed. | |
gulp.src("assets/git/hooks/*") | |
.pipe(forEach(function(stream, file) { | |
// The link source must be relative to .git/hooks | |
var src = "../../" + file.path.replace(process.cwd() + "/", ""), | |
dest = ".git/hooks/" + file.path.replace(file.base, ""); | |
// Make sure the hook is executable. | |
shell.chmod("ug+x", file.path); | |
// Don't use `shell.ln("-sf", src, dest);` This will create the symlink with an absolute path | |
// which will break if you ever move this repo. | |
shell.exec("ln -sf " + src + " " + dest); | |
})); | |
// If we are not allowed to manage npm modules, there is nothing else to do. | |
if (!ALLOW_NPM_MODULE_MANAGEMENT) { | |
return; | |
} | |
// Get the current repo ID. | |
var currentId = shell.exec("git rev-parse HEAD", {silent: true}).output, | |
lastId = null; | |
// Get the last repo ID at which we updated the npm dependencies. | |
try { | |
lastId = file.readFileSync(LAST_DEPENDENCY_UPDATE_ID_FILE); | |
} catch (e) { } | |
// IDs match, nothing to do. | |
if (lastId != null && lastId == currentId) { | |
log.info("housekeeping", "npm dependencies are current since the last commit"); | |
return; | |
} | |
// IDs do not match, make sure everything is installed and up to date | |
log.info("housekeeping", "Executing `npm install`"); | |
shell.exec("npm install"); | |
log.info("housekeeping", "Executing `npm-check-updates -u`"); | |
shell.exec("npm-check-updates -u"); | |
// Update our ID tracking file. | |
shell.exec("git rev-parse HEAD > " + LAST_DEPENDENCY_UPDATE_ID_FILE, {async: true, silent: true}); | |
}); | |
/** | |
* Run all automatic tasks. | |
*/ | |
gulp.task("auto", ["autobuild", "autotest"]); | |
/** | |
* The same as the default task, but done serially so that the output doesn't get all jumbled. | |
*/ | |
gulp.task("serial", function() { | |
runSequence( | |
"housekeeping", | |
"build-common-lib", | |
"lint", | |
"build", | |
"test" | |
); | |
}); | |
function notifyErrors() { | |
var args = Array.prototype.slice.call(arguments); | |
notify.onError({ | |
title: "Compile Error", | |
message: "<%= error.message %>" | |
}).apply(this, args); | |
this.emit('end'); // Keep gulp from hanging on this task | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment