gulp-browserify-watchify-glob

Create a Browserify bundler with multiple entries based on a glob pattern. Rebuild incrementally watching for changes, including additions and deletions of files that match the glob pattern.


Keywords
gulp, browserify, watchify, glob
License
BSD-3-Clause
Install
npm install gulp-browserify-watchify-glob@1.0.1

Documentation

pipeline status coverage report libraries.io dependency status latest version on npm inspect the package.json code hosted on GitLab issue tracker on GitLab author: Julian Gonggrijp license text made with CoffeeScript contributing changelog

gulp-browserify-watchify-glob

Create a Browserify bundler with multiple entries based on a glob pattern. Rebuild incrementally watching for changes, including additions and deletions of files that match the glob pattern.

Motivating example

With just browserify, you can define a bundle based on a fixed set of entries.

const testBundler = browserify({
    entries: ['test1.js', 'test2.js'],
    debug: true,
});

You can build the bundle as often as you want from this definition, but only manually.

testBundler.bundle();

If you combine browserify with gulp, your setup might look like this.

const vinylStream = require('vinyl-source-stream');

const testEntries = ['test1.js', 'test2.js'];

const testBundler = browserify({
    entries: testEntries,
    debug: true,
});

function bundleTests() {
    testBundler.bundle()
        .pipe(vinylStream('testBundle.js'))
        .pipe(gulp.dest('test-directory'));
}

You can watch for changes in the entries and their dependencies by using watchify. This gives you efficient, automatic, incremental rebuilds.

const testEntries = ['test1.js', 'test2.js'];

const testBundler = browserify({
    entries: testEntries,
    debug: true,
    cache: {},
    packageCache: {},
}).plugin('watchify');

function bundleTests() {
    return testBundler.bundle()
        .pipe(vinylStream('testBundle.js'))
        .pipe(gulp.dest('test-directory'));
}

function watch(done) {
    testBundler.on('update', bundleTests);
    // You don't often see the following lines. They ensure a clean
    // exit when you stop the watch task with a keyboard interrupt.
    const finish = () => done(null, testBundler.close());
    process.once('SIGINT', finish);
    process.once('SIGTERM', finish);
}

You might have lots of entries, for example when you have lots of test modules. If this is the case, your entries are best enumerated with a glob.

const glob = require('glob');

const testEntriesGlob = 'src/**/*-test.js';
const testEntries = glob.sync(testEntriesGlob);
// ['src/apple-test.js', 'src/banana-test.js', 'src/cherry/drop-test.js', ...]

Alas, the set of entries becomes fixed again directly after the line above. If you add a test module while the watch task is running, watchify will not include the new module in the bundle. Even worse, if you delete an entry, the build will break.

You could try to work around this by using gulp.watch instead of watchify.

function bundleTests() {
    return browserify({
        entries: glob.sync(testEntriesGlob),
        debug: true,
    }).bundle()
        .pipe(vinylStream('testBundle.js'))
        .pipe(gulp.dest('test-directory'));
}

function watch(done) {
    const watcher = gulp.watch(testEntriesGlob, bundleTests);
    const finish = () => done(null, watcher.close());
    process.once('SIGINT', finish);
    process.once('SIGTERM', finish);
}

Unfortunately, this only watches the entries; it doesn't detect changes in the dependencies. To make matters worse, the rebuild is not incremental anymore; bundleTests redefines and rebuilds the bundle entirely from scratch every time.

gulp-browserify-watchify-glob lets you have the best of both worlds.

const globbedBrowserify = require('gulp-browserify-watchify-glob');

const testBundler = globbedBrowserify({
    entries: testEntriesGlob,
    debug: true,
});

function bundleTests() {
    return testBundler.bundle()
        .pipe(vinylStream('testBundle.js'))
        .pipe(gulp.dest('test-directory'));
}

function watch(done) {
    testBundler.watch(bundleTests)();
    const finish = () => done(null, testBundler.close());
    process.once('SIGINT', finish);
    process.once('SIGTERM', finish);
}

Finally, you can have a dynamic set of entries and rebuild incrementally, whenever an entry or any of its dependencies is modified and whenever an entry is added or deleted.

Usage

Install:

yarn add gulp-browserify-watchify-glob -D
# or
npm i gulp-browserify-watchify-glob -D

Import:

const globbedBrowserify = require('gulp-browserify-watchify-glob');
// or
import globbedBrowserify from 'gulp-browserify-watchify-glob';

globbedBrowserify is a drop-in replacement for the browserify constructor. It accepts the same options and it returns a browserify instance that you can invoke the same methods on.

const bundler = globbedBrowserify({
    entries: 'src/**/*-test.js',
    debug: true,
    // cache and packageCache default to {}
}).transform(/* whatever */);

There are two main differences:

  • You must set the entries option, which may be a glob pattern or an array of glob patterns.
  • The returned bundler has a watch method that you should use instead of other watching mechanisms, such as watchify and gulp.watch.

The watch method causes the bundler to start watching all files that match the glob pattern(s) in the entries and all of their dependencies. It accepts two arguments, an update task and a kickoff task.

const wrappedKickoff = bundler.watch(updateTask, kickoffTask);

The kickoff task is optional. If omitted, it is assumed to be the same as the update task. Both tasks should be functions that meet the requirements of a gulp task and both should call bundler.bundle() internally. The kickoff task should contain the necessary steps for the first build while the update task will be invoked on every update.

function updateTask() {
    // main option one: return a stream that starts with the build output
    return bundler.bundle()
        .pipe(...);
}

function kickoffTask(done) {
    // main option two: handle the complete build output with a callback
    bundler.bundle((error, buffer) => {
        if (error) return done(error);
        doSomethingAsyncWith(buffer, done);
    });
}

The return value wrappedKickoff is a wrapper of the kickoff task that still needs to be invoked in order to trigger the first build. This is not done automatically to facilitate async workflows. If you want to trigger the first build immediately, simply invoke the return value:

bundler.watch(updateTask, kickoffTask)();

Once the watch method has been invoked, the bundler also has a close method which you can invoke to stop watching the entries glob(s) and the dependencies.

bundler.close();

gulp-browserify-watchify-glob wraps the tasks to prevent race conditions. If you have another (third, fourth...) task that also invokes bundler.bundle(), wrap it so that builds will not be triggered while a previous build is still in progress.

const wrappedOtherTask = bundler.wrap(otherTask);

Caveat

Browserify was not designed with this use case in mind. I rely on browserify implementation details in order to make the magic work.