Skip to content

Instantly share code, notes, and snippets.

@Aeon
Created June 3, 2015 19:19
Show Gist options
  • Save Aeon/2063cbad046fa53749c7 to your computer and use it in GitHub Desktop.
Save Aeon/2063cbad046fa53749c7 to your computer and use it in GitHub Desktop.
gulp + browserify...
/*
* 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