Freezing your config

Will Becker
Lexical Labs Engineering
4 min readAug 4, 2015

I just migrated off RequireJS to Webpack, having last week moved from ES5 to ES6. One more migration to go and we’re running a totally 2015 compliant webapp (React+Backbone -> something Flux-ish)!

In migrating to Webpack, I found that I was using (mostly) the same configuration in 3 places:

  1. The production build
  2. The test build
  3. The development build

It’s the “mostly” bit that is troublesome though. In a production environment, you want everything minified with sourcemaps. When you are testing, you want code coverage tools running over the top of things, so you don’t want that — but you do want the code coverage plugin. When you are developing you want hot module replacement, because it is totally awesome.

How do you manage all 3 environments at once? Well ideally you would just write the common stuff in one module and require it in and modify it, eg:

webpack_common.config.js

module.exports = {
module: {
loaders: [...]
},
resolve: {
root: [...]
}
plugins: [...]
};

Gruntfile.js

module.exports = function (grunt) {
grunt.initConfig({
webpack: {
release: (function () {
var config = require("./webpack_common.config");
config.entry = "src/main.js";
config.plugins.push(
new webpack.optimize.UglifyJsPlugin({})
);
return config;
});
},
karma: {
test: {
//your karma file has a ref to webpack_common.config too
configFile: "karma.conf.js"
}
}
});
grunt.loadNpmTasks("grunt-webpack");
grunt.registerTask("release", ["webpack"]);
}

However, if you do this through your single build tool that lives in node.js (eg Grunt or Gulp), you might find that those changes that you are applying get modified on top of each other — since you are in the same environment and directly modifying the module that you are requiring in, the time the next one comes to it, it will see those changes. D’oh.

Enter Object.freeze(). It’s one of those new things that have been around in Internet Explorer for 4 years, that you’ve probably not really had an excuse to use, but it’s 2015 — let’s embrace the API! With freeze(), changes to your Objects are verboten.

So why do we use it here? Not to prevent us from making changes ever, because we want to be able to customise our config. We just don’t want to ever change the core configuration that is shared.

So we want to do something like:

webpack_common.config.js

module.exports = Object.freeze({
...
});

Obviously if we do this, this bit here will screw up:

var config = require("./webpack_common.config");
config.entry = "src/main.js"

But this is what we want. We shouldn’t be allowing ourselves to be lazy and just making arbitrary changes to things that persevere. Immutability is your friend, and is the enemy of state, lest the state of your program becomes akin to the state of your bedroom when the more-responsible-adult-than-you in your life divests themselves from you on a short-to-medium-term basis. Or maybe I’m projecting.

Aaaanyway. Let’s extend our config instead:

var config = _.extend(require("./webpack_common.config"), {
entry: "src/main.js"
});
config.plugins.push(new webpack.optimize.UglifyJsPlugin({}));

Again, nope. _.extend() doesn’t magically copy everything into a new object, it adds to the object in the first argument. Instead, let’s flip the order around:

var config = _.extend({
entry: "src/main.js"
}, require("./webpack_common.config"));
config.plugins.push(new webpack.optimize.UglifyJsPlugin({}));

This will run. But again it’s not what we want.

Why not? It’s that plugins.push(). Unfortunately when we said freeze(), what we said was don’t allow properties to be reassigned — not that you can’t modify the contents of them. So if we add that plugin, we actually add it everywhere where webpack_common.config is imported.

So what do we do? In theory we want a recursive freeze, and libraries exist to do that for you. However, in practice, not everyone is as cool about the whole stateless thing as we are. Webpack wants to get into the innards of the object we are passing and start adding defaults to things that we didn’t add, and when it finds it can’t will get into a tizz.

Instead, let’s be a bit pragmatic and handle it ourself:

module.exports = Object.freeze({
module: {
loaders: Object.freeze([...])
},
resolve: {
root: Object.freeze([...])
}
plugins: Object.freeze([...])
};

So Webpack can still add things to module and resolve, but it can’t (and more importantly we can’t) add extra loaders or root locations.

So we can’t do the config.plugins() call any more either, we have to do something like:

config.plugins = config.plugins.concat([
new webpack.optimize.UglifyJsPlugin({})
]);

But now, the config that we are using is no longer bound to the config we are requiring in!

Morals of the story

So this is just a single, small example; the big picture to take out of this is that:

  1. Don’t Repeat Yourself
  2. When you aren’t repeating yourself, Keep Code Immutable, because…
  3. State tends to make your life painful
  4. It’s easy to fall into statefulness
  5. Use tools to stop yourself from doing things you don’t mean to do.

The trend in Javascript is moving towards towards stateless programming, and as LISPers will tell you, it’s about time. Things like Flux and ImmutableJS are driving us towards that and hopefully this idiom will reduce the amount of avenues through which we can screw up, so you can spend more of your time being awesome.

--

--