Home Reference Source

Webpack Node Utils

Build Status Coverage Status Documentation Status Dependencies status Dev dependencies status

A set of utility methods that for a better experience building Node applications using Webpack.

The motivation

The moment we started building universal applications we found a few issues that complicated the process a little bit:

So we built workarounds, but those workarounds were attached to the project where they were created, so moving them involved a lot of copy&paste, and for something this generic, having them on a module made more sense.

Information

- -
Package webpack-node-utils
Description A set of utility methods to help you build Node apps with Webpack.
Node Version >= v6.0.0

Usage

Handling multiple Webpack configurations for multiple apps

Let's start by making clear that when you build an app with Webpack, you usually have two configurations: One that may have development plugins, like loggers, hot reload, and such; and one for production, where you optimize your code, uglify it and maybe even compress it.

Now, let's say you have a universal app that has both backend and frontend code, you probably have four different files or maybe just two where you validate the NODE_ENV and export the configuration for the environment you need. Based on what you have, this may lead you to one of these problems:

Ok, this module tries to simplify all the above... kind of using both approaches:

We'll review this with an example/tutorial:

First, create a directory to store your Webpack configuration, we usually use .webpack. Then, we are going to create a base configuration that all the other can extend. This will allow you to have all the shared code in one place:

// File: ./.webpack/base.js
module.exports = () => ({
    module: {
        loaders: [...],
    },
    resolve: ['js', 'jsx', 'json'],
});

Take a look, we only took things that weren't really specific to the configurations themselves. You would want to just move the shared code, not remove all repeated code, is not the same: The loaders are usually always the same, so that's ok, but if you put in here output.filename, that wouldn't be ok, because it takes clarity of the output configuration, and you would have to know that part of that configuration is on another file.

Moving along, you are probably wondering why the file is not exporting an Object, but instead is exporting a Function? Well, that will get explained when you see how the configuration is invoked.

Now that we have the base configuration, let's add the ones for the frontend, for both production and development:

// File: ./.webpack/frontend.dev.js
module.exports = () => ({
    extends: 'base',
    entry: {...},
    output: {...},
    plugins: {...},
});

// File: ./.webpack/frontend.prod.js
module.exports = () => ({
    extends: 'base',
    entry: {...},
    output: {...},
    plugins: {...},
});

They look the same, right? That's the idea, they only change where they need to change, and the shared settings are on the base configuration, which they both extend.

For the backend, it's the same, but for this example, let's say you are using Express for your server and you want to use supertest for integration tests. The thing is that Webpack, by default, generates a bundle that auto executes itself, and that can't be accessed from the outside, and for Supertest, you need to be able to start and stop the server between suites.

You probably know this, but in case you don't, you can specify on your Webpack configuration that the bundle is a commonjs library, but wait... does that means that if I want to test both the production and the development server I would need an extra configuration for each one? Yes, and No. Webpack Node Utils allows you to have use variations of the same configuration on a very simple way:

// File: ./.webpack/server.dev.js
const defaultSetup = module.exports = () => ({
    extends: 'base',
    entry: {...},
    output: {...},
    plugins: {...},
});

module.exports.library = (params) => {
    const config = defaultSetup(params);
    config.output.libraryTarget = 'commonjs2';
    return config;
};

// And yes, it would be the same for `server.prod.js`.

You define your configuration, export it and at the same time you save it on variable, then you export another function, which would be your variation, in this case, a commonjs library. The variation gets the configuration you export by default, makes a small change on the output and return it like nothing happened.

We know, you are wondering what the params argument is, right? We are almost there.

So, five files, four build types and two extra variations, but... how the hell do you use it?

In this example, we are going to use only one webpack.config.js file, but you can use one per target if you want.

// File: ./webpack.config.js
const webpackNodeUtils = require('webpack-node-utils');

// The directory where the configuration files are.
const directory = '.webpack';
// Use a environment variable to detect the target
const target = process.env.BUILD_TARGET || 'frontend';
// Use the `NODE_ENV` environment variable to detect the build type.
const type = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';
// And another environment variable to detect the variation
const variation = process.env.BUILD_AS_LIB === 'true' ? 'library' : '';
// Should the module add a timestamp hash on the parameters so I can use when creating the files?
const createHash = type === 'prod';

// Define some parameters you would need access on your configurations
const params = {
    HTMLTitle: 'Hello world!',
    outputDir: './dist/',
};

// Finally, get and export the configuration
module.exports = webpackNodeUtils.config(directory, target, type, createHash, params, variation);

One entry point, multiple apps, configurations and variations:

# Build the frontend on development mode
webpack

# Build the frontend on production mode
NODE_ENV=production webpack

# Build the backend on development mode
BUILD_TARGET=backend webpack

# Build the backend on production mode
NODE_ENV=production BUILD_TARGET=backend webpack

# Build the backend on development mode, as a commonjs library
BUILD_AS_LIB=true BUILD_TARGET=backend webpack

# Build the backend on production mode, as a commonjs library
BUILD_AS_LIB=true NODE_ENV=production BUILD_TARGET=backend webpack

Simple to read and simple to maintain.

Now, the params: There are certain things on the Webpack configurations that are there because a plugin or a setting require them to be there, but in your app structure, they should be on a higher level, like those two on the example:

Those are kind of edge cases, but they exist. What we usually do is create a sort of config.json with these higher level settings and then feed them to the configuration objects using the params argument.

That's all for Handling multiple Webpack configurations for multiple apps, for more information, check the technical documentation.

Dynamic readFileSync and require on runtime

There are two problems here:

The workaround for this are two proxy methods for require and for fs.readFileSync, they both run from inside this module, and since this module is not inside the bundle, there's no problem with Webpack. Also, these methods use a path relative to your project root path, so you don't have to worry about that either:

Require a file with a dynamic name on runtime: .require()

On this example, we'll assume you have a set of configuration files on a config directory, located on your root path; and we'll create a function to require those configurations based on a given environment.

const webpackNodeUtils = require('webpack-node-utils');

// Set the function that does the dynamic require.
const getConfigForEnvironment = env => webpackNodeUtils.require('./config/config.' + env);

// require('<root>/config/config.dev')
getConfigForEnvironment('dev');

// require('<root>/config/config.prod')
getConfigForEnvironment('prod');

We aware that we are moving in the direction of import instead of require, and that imports can't be dynamic, but dynamic requires are still a useful thing on Node apps.

Read a file with a dynamic name on runtime: .read()

On this example, we'll assume you have a list of .csv files on a sales directory, located on your root path; and we'll create a function to read those files based on the name of a month:

const webpackNodeUtils = require('webpack-node-utils');

// Set the function that reads the files
const getCSVByMonth = month => webpackNodeUtils.read('./sales/sales.' + month + '.csv');

// fs.readFileSync('./sales/sales.july.csv', 'utf-8');
getCSVByMonth('july');

// fs.readFileSync('./sales/sales.september.csv', 'utf-8');
getCSVByMonth('september');

Generating external dependencies for your configuration

By default, Webpack reads all the requires on your code and tries to put them inside the bundle, but on a Node app, there are some dependencies that you don't want in there, and others that don't even work when inside a bundle.

There are a lot of tutorials out there that show you how to read your package.json, get all your dependencies and define them as externals on your Webpack configuration. We'll, we decided to wrap that logic inside Webpack Node Utils and add a few other options for you to play with:

Let's see all of this with a few examples based on this package.json:

{
    "dependencies": {
        "jest-cli": "1.0.0",
        "node-fetch": "1.0.0"
    },
    "devDependencies": {
        "webpack": "1.0.0",
        "webpack-middleware": "1.0.0"
    }
}
const webpackNodeUtils = require('webpack-node-utils');

// Get all the production dependencies as externals
webpackNodeUtils.externals();
/**
 *  {
 *      'jest-cli': 'commonjs jest-cli',
 *      'node-fetch': 'commonjs node-fetch',
 *  }
 */

// Add a custom dependency
webpackNodeUtils.externals({
    'my-custom-module': 'modules/custom.js'
});
/**
 *  {
 *      'jest-cli': 'commonjs jest-cli',
 *      'node-fetch': 'commonjs node-fetch',
 *      'my-custom-module': 'commonjs <rootDir>/modules/custom.js'
 *  }
 */

// Include the `devDependencies`
webpackNodeUtils.externals({}, true);
/**
 *  {
 *      'jest-cli': 'commonjs jest-cli',
 *      'node-fetch': 'commonjs node-fetch',
 *      'webpack': 'commonjs webpack',
 *      'webpack-middleware': 'commonjs webpack-middleware',
 *  }
 */

// Add `colors/safe` as default
webpackNodeUtils.externals({}, false, ['colors/safe']);
/**
 *  {
 *      'colors/safe': 'commonjs colors/safe',
 *      'jest-cli': 'commonjs jest-cli',
 *      'node-fetch': 'commonjs node-fetch',
 *  }
 */

// Let's ignore `node-fetch` and include it on the bundle
webpackNodeUtils.externals({}, false, [], ['node-fetch']);
/**
 *  {
 *      'jest-cli': 'commonjs jest-cli',
 *  }
 */

Running the backend build with the watch flag

One of the issues we had while building both backend and frontend with Webpack was that we couldn't use the --watch flag for the backend without having to open another terminal, because Webpack stops on the watch and whatever comes next doesn't get executed. One of the solutions we tried was to use nodemon to watch the backend and restart the necessary task when the files change, but that also means that Webpack needs to be restarted too, which may take a few seconds (more if the task you use is hooked to other things, like cleaning the build folder for example). Now, the magic of Webpack watching the files is that it doesn't need to be restarted and the change happens almost immediately (in most cases :P).

We did some research and we found start-server-webpack-plugin, which uses the cluster module to start a server when Webpack finishes loading; which is great, but not entirely what we wanted, so we built a small plugin based on that:

WebpackNodeUtilsRunner receives an entry name and it takes care of executing the build once Webpack finishes, and if Webpack needs to rebuild, it stops the build process, waits for Webpack to finish again and restart the build.

All you have to do is to include it on your Webpack configuration:

// File: ./.webpack/backend.dev.js
const WebpackNodeUtilsRunner = require('webpack-node-utils').WebpackNodeUtilsRunner;

module.exports = () => ({
    extends: 'base',
    entry: {...},
    output: {...},
    plugins: [
        new WebpackNodeUtilsRunner('your-backend-asset-name'),
    ],
});

Yes, the example uses the syntax we use for handling multiple configurations, but that's not required.

For more information, check the technical documentation.

Development

Before doing anything, install the repository hooks:

npm run install-hooks

NPM Tasks

Task Description
npm run install-hooks Install the GIT repository hooks.
npm test Run the project unit tests.
npm run lint Lint the project code.
npm run docs Generate the project documentation.

Testing

We use Jest as test runner. The configuration file is on ./.jestrc, the tests and mocks are on ./tests and the script that runs it is on ./utils/scripts/test.

Linting

We use ESlint to validate all our JS code. The configuration file is on ./.eslintrc, there's also an ./.eslintignore to ignore some files on the process, and the script that runs it is on ./utils/scripts/lint.

Documentation

We use ESDoc to generate HTML documentation for the project. The configuration file ion ./.esdocrc and the script that runs it is on ./utils/scripts/docs.