I'm working on multiple browser extension / add-ons that usually need to work at least in Chrome and Firefox (sometimes in Safari as well).
The biggest issue is staying DRY and on the other hand keeping the source clean. Conceptually the project usually has following parts:
To reduce code duplication I have a single content script for both browser and preprocess it (removing other-browser-specific parts) during the build process.
Unfortunately this makes content scripts really long and ugly (and hard to lint).
I would like to use Browserify basically for the whole JS code in my projects. Still to do it I need a solution to handle this kind of flow:
Browser specific entry script -> cross-browser code -> browser specific low-level code.
I would imagine this kind of hierarchy:
- Entry scripts
- Browser A
- Browser B
- ...
- Common code
- Low-level code
- Browser A
- Browser B
- ...
So, for example, during the build process I would like Browserify to take an entry script for browser A, then bundle it together with the common code and with low-level code for browser A only. This is to be done without this kind of switching in the common code:
if(isBrowserA()) {
var lowLevelModule = require("../lowLevel/browserA/module");
} else {
var lowLevelModule = require("../lowLevel/browserB/module");
}
I would like the build process with Browserify to do exactly that for me -- replace the "root path" of low level code depending on the target.
Hacking it around with package.json wouldn't work because I need flexible number of targets (and possibly even deeper dependency tree).
Try using the factor-bundle or partition-bundle Browserify plugins. They both help split code into different entry files and a common modules file. partition-bundle also includes scripts that enable asynchronous loading of your different bundles.
One possible way to do this would be to drop the if
/else
require()
calls from your code, using a fixed path instead.
var lowLevelModule = require("../lowLevel/module")
Then run a separate build for each browser, using browserify to change what the path resolves to for each build using expose
.
So in a gulpfile.js
(just for example, the browserify API is the important bit - you could do the same from a shell using -r
and -x
flags to browserify, using :
to separate the require/expose values), run the build once for each browser, passing in a different --browser=
arg each time.
var browserify = require('browserify')
var gulp = require('gulp')
var gutil = require('gulp-util')
var source = require('vinyl-source-stream')
var browser = gutil.env.browser // browserA, or browserB, or...
// You might want to configure paths up-front separately, just
// hardcoding below for brevity.
gulp.task('bundle-app', function() {
var b = browserify('./entry/' + browser + '/module', {detectGlobals: false})
b.require('./path/to/lowLevel/' + browser + '/module', {expose: '../lowLevel/module'})
return b.bundle()
.pipe(source('app.js'))
.pipe(gulp.dest('./build'))
})
For common dependencies, you could bundle them into an external file and add external
calls to your browser-specific bundles:
var commonModules = ['module1', 'module2']
gulp.task('bundle-common', function() {
var b = browserify({detectGlobals: false})
commonModules.forEach(function(module) {
b.require(module)
})
return b.bundle()
.pipe(source('common.js'))
.pipe(gulp.dest('./build'))
})
gulp.task('bundle-app', function() {
var b = browserify('./entry/' + browser + '/module', {detectGlobals: false})
commonModules.forEach(function(module) {
b.external(module)
})
b.require('./path/to/lowLevel/' + browser + '/module', {expose: '../lowLevel/module'})
return b.bundle()
.pipe(source('app.js'))
.pipe(gulp.dest('./build'))
})
I usually put chains of builds in a package.json script for convenience:
"scripts": {
"build": "gulp bundle-common && gulp bundle-app --browser=browserA && gulp bundle-app --browser=browserB"
}
Finally:
npm run build
This is not something Browserify supports out-of-the-box unless, but you could in theory achieve what you want if you write a custom source code transform.
As an alternative, the RaptorJS Optimizer provides the exact same feature that you are looking for out-of-the-box. (disclaimer: I am the author of this tool and it is the tool we use at eBay for all our Node.js applications) The RaptorJS Optimizer allows you to remap one module to another based on a set of arbitrary flags that are enabled during optimization. We use this feature at eBay to conditionally send down different code for different web browsers, devices, experimentation groups, etc. For more details on that feature, please see:
FYI, the RaptorJS Optimizer supports all of the features of Browserify, plus it provides support for non-JS dependencies, async loading, conditional dependencies, dynamic requires, etc. It is still very modular like Browserify and can be extended via plugins to teach it how to handle new dependency types. Unlike Webpack, the RaptorJS Optimizer does not overload the CommonJS module loading system so that code will still be allowed run under Node.js and in the web browser. We have had a lot of success with the RaptorJS Optimizer at eBay (and other companies) so I encourage you to check it out.