1

I know you can't conditionally require a module with browserify because they're bundled at compile time, not run time. How about conditionally stripping modules?

Let's say I have an app that allows you to create image galleries. The galleries can also be edited (reordering images etc.). But the rendering of the gallery and the editing are coupled and can't be fully separated. But for deploying the galleries I don't need the editing functionality and I know which modules are used. I want to create two different bundles, one with editing capabilities and one without by eliminating most of the editing code. What I did is use envify and uglify's dead code elimination to exclude my own code from the smaller bundle.

Before (thing.js)

//...some code that only the editor needs...
module.exports = thing;

After (thing.js)

if(process.env.INCLUDE_EDITOR === 'yes') {
    //...some code that only the editor needs...
    module.exports = thing;
}

This works great and the editor bundle is already smaller. And since I know that the other bundle will never use the functionality of thing it's OK to just export nothing and have an empty module.

Now here's the problem. If thing.js requires a module, say pica, it will still be bundled even though nobody uses it after dead code elimination.

Before (thing.js)

var pica = require('pica');
//...some code that uses pica...
module.exports = thing;

After (thing.js)

if(process.env.INCLUDE_EDITOR === 'yes') {
    var pica = require('pica');
    //...some code that uses pica...
    module.exports = thing;
}

To sum it up: my bundle now contains the pica library, but nobody requires it. The code that required it was dead code, but uglify can obviously not understand that it could remove pica completely.

Prinzhorn
  • 22,120
  • 7
  • 61
  • 65
  • Perhaps [`factor-bundle`](https://github.com/substack/factor-bundle) will be useful for you? – casr Oct 25 '16 at 04:06
  • @casr thanks! I don't think it can be applied here. Given the example in the docs what I want is to remove w and z from the bundle x, because I know that the `console.log(z(5) * w(2));` will never be executed. It will still be in the bundle though. I'm kind of doing stupid crazy things here. But hey, I removed 20% from the non-editor bundle! – Prinzhorn Oct 25 '16 at 09:58

2 Answers2

1

I think what you are after is a transform like uglifyify.

Uglifyify gives you the benefit of applying Uglify's "squeeze" transform on each file before it's included in the bundle, meaning you can remove dead code paths for conditional requires.

Take note that you will still want to run uglifyjs on the resulting output.

casr
  • 1,166
  • 2
  • 11
  • 17
  • You are absolutely correct! That does exactly what I originally wanted. I will stick with the solution I have now as it results in an even smaller bundle. But uglifyify is the less hacky version. – Prinzhorn Oct 25 '16 at 16:50
  • This should result in the same size bundles as your custom transform step with the added benefit of working automatically with changes to the project. If you're getting smaller bundles than this then it's likely your bundles have had code removed that it does in fact rely on – casr Oct 26 '16 at 08:36
0

I think I've found a solution that works well enough. As a bonus it doesn't require touching existing code (e.g. adding the process.env. check). I wrote a browserify transform which, given a list of modules/files, replaces their require call with {}. This way they're completely removed from the resulting bundle. It's just a hack anyway. I know.

Before:

var thing = require('./thing.js');
var pica = require('pica');

After:

var thing = {};
var pica = {};

Using https://www.npmjs.com/package/browserify-transform-tools this was only a few lines of code since it already offers a makeRequireTransform helper. I'm gonna dump the code here and recommend nobody should ever use it unless you know exactly what you're doing.

derequire.js

var path = require('path');
var resolve = require('resolve');
var transformTools = require('browserify-transform-tools');

var options = {
    evaluateArguments: true,
    jsFilesOnly: true
};

var cache = {};
var resolveDerequire = function(moduleName) {
    var fromCache = cache[moduleName];

    if(fromCache) {
        return fromCache;
    }

    return require.resolve(moduleName);
};

var transform = transformTools.makeRequireTransform('derequire', options, function(args, transformOptions, done) {
    var requiredModule = args[0];
    var basedir = path.dirname(transformOptions.file);

    var shouldDerequire = transformOptions.config.modules.some(function(moduleToRequire) {
        try {
            //The normal require which respects NODE_PATH.
            return require.resolve(requiredModule) === resolveDerequire(moduleToRequire);
        } catch(ex1) {
            try {
                //A local require relative to the current file.
                return resolve.sync(requiredModule, {basedir: basedir}) === resolveDerequire(moduleToRequire);
            } catch(ex2) {
                console.error(ex1, ex2);
                return false;
            }
        }
    });

    if(shouldDerequire) {
        done(null, '{}');
    } else {
        done();
    }
});


module.exports = transform;

The package.json config for the transform

"browserify": {
    "transform": [
        "babelify",
        [
            "./derequire.js",
            {
                "modules": [
                    "pica",
                    "exif-js",
                    "./src/thing.js",
                    "components/TextEditor.jsx",
                    "lib/FileUploader.js"
                ]
            }
        ],
        "browserify-css",
        "imgurify",
        "glslify"
    ]
},
Prinzhorn
  • 22,120
  • 7
  • 61
  • 65