Skip to content

Middleware

The basic unit of interacting with Neutrino is middleware. Middleware can modify configuration, listen for events, get build metadata, or augment Neutrino with custom functionality.

Formats

Neutrino middleware can be specified in several different formats depending on the complexity and level of customization needed. The Neutrino API and CLI operate on the concept that they are provided a collection of middleware. Specifically:

// The Neutrino API can use many middleware when it is run()
neutrino.run('command', [/* array of middleware */])

// When no middleware has been specified to run, it defaults
// to an array only containing the local .neutrinorc.js file
neutrino.run('command')
# The Neutrino CLI can use many middleware when running a command
❯ neutrino command --use middleware_a middleware_b

# When no middleware has been specified to the CLI, it defaults
# to an array only containing the local .neutrinorc.js file
❯ neutrino command

Each item in these collections can be provided in a number of different formats. For example, the CLI can only directly accept string formats, while the API and required middleware can be of any supported format.

The string and array formats are generally used from within the more powerful object and function formats.

String format

Providing a string as a middleware item denotes it as being a require-able module. Neutrino will attempt to require this string and then try to use its exports as another middleware format. For example, using the string "@neutrinojs/react" will be required by Neutrino, and then be re-processed through Neutrino using its exports as a different middleware type, depending on what that preset exported.

Using the string format is most commonly used to depend on other middleware. Referencing a string does not enable access to the Neutrino API, and options can only be passed to the string middleware if using the Neutrino API's use method.

# String format on the CLI
❯ neutrino start --use @neutrinojs/react
// String format exported from other middleware
// not very useful, but possible
module.exports = '@neutrinojs/react';
// String format being used within an object-format .neutrinorc.js
module.exports = {
  use: ['@neutrinojs/react']
};
// String format being used within function format
module.exports = neutrino => {
  neutrino.use('@neutrinojs/react');
  neutrino.use('@neutrinojs/react', {
    devServer: { port: 3000 }
  });
};

Array format

Providing an array as a middleware item denotes it as being a pair of middleware format and options, i.e. [middleware, options]. The first item in the pair can be any other middleware format, although it is typically specified as a string to require, and options to be passed to the required middleware.

Using the array format is most commonly used to depend on other middleware while being able to supply options to the required middleware. Referencing an array does not enable access to the Neutrino API.

// Array format exported from other middleware
// still not very useful, but possible
module.exports = ['@neutrinojs/react', { devServer: { port: 3000 } }];
// Array format being used within an object-format .neutrinorc.js
module.exports = {
  use: [
    ['@neutrinojs/react', {
      devServer: { port: 3000 }
    }]
  ]
};
// Array format being used within function format
module.exports = neutrino => {
  neutrino.use(['@neutrinojs/react']);
  neutrino.use(['@neutrinojs/react', {
    devServer: { port: 3000 }
  }]);
};

Object format

Providing an object as a middleware item denotes it as being a more complex middleware structure capable of overriding configuration and options. The object format is the recommended format when creating .neutrinorc.js files as it provides a good balance between easily setting options and using other middleware. The object format can be structured as follows:

module.exports = {
  // specify options to override in the Neutrino API
  options: {
    // for example, change the output directory to dist
    output: 'dist',
    // or set the default development port to 3000
    port: 3000
  },

  // Specify environment-specific changes. This is a key-value
  // mapping of environment variable names to their matching values
  // during which to override. The matching environment value can
  // be any middleware format
  env: {
    NODE_ENV: {
      // Require style-minify middleware during production
      production: '@neutrinojs/style-minify',

      // Use dev-server during development
      development: ['@neutrinojs/dev-server', { port: 3000 }],

      // Use Jest during test-only
      test: {
        use: ['@neutrinojs/jest']
      }
    },

    CSS_MODULES: {
      // Turn on CSS modules when the environment variable CSS_MODULES=enable
      enable: (neutrino) => {
        neutrino.config.module
          .rule('style')
            .use('css')
              .options({ modules: true });
      }
    }
  },

  // The "use" array defines another set of middleware for Neutrino to use.
  // Just like the CLI and API start with a collection of middleware to use,
  // providing a "use" array specifies another list of middleware formats to work with
  use: [
    // string format
    '@neutrinojs/airbnb-base',

    // array format
    ['@neutrinojs/react', html: { title: 'Epic React App' } }],

    // function format
    (neutrino) => {
      // Make whatever configuration overrides needed
    }
  ]
}

Hopefully it is clear that the object format is quite powerful, and although it doesn't give you direct access to the Neutrino API, providing a function format within does give you API access.

Function format

The most powerful middleware format. Using a function as middleware gives direct access to the Neutrino API, configuration, options, and events for modification, as well as being able to accept options for driving the behavior of the middleware.

function middleware(neutrino, options) {}

A middleware function can optionally accept an options argument which will be fed back into the middleware function when used. The options argument can be whatever value you accept.

If you're familiar with middleware from the Express/connect world, this works similarly. When using Express middleware, you provide a function to Express which receives arguments to modify a request or response along its lifecycle. There can be a number of middleware functions that Express can load, each one potentially modifying a request or response in succession. Neutrino will execute middleware functions similarly, where each function successively interacts with Neutrino along the lifecycle.

To use a concrete example, let's create middleware that adds an environment plugin:

const { Neutrino } = require('neutrino');
const { EnvironmentPlugin } = require('webpack');

module.exports = (neutrino, additionalVars = []) => {
  neutrino.config
    .plugin('env')
    .use(EnvironmentPlugin, ['NODE_ENV', ...additionalVars]);
};

// When this middleware gets used, options can be passed to it
// depending on the format:

module.exports = {
  use: [
    'your-env-middleware',

    ['your-env-middleware', ['CSS_MODULES']],

    (neutrino) => neutrino.use('your-env-middleware', ['CSS_MODULES'])
  ]
};

Loading middleware

Additional middleware can also be loaded from a middleware function. This makes their composition simpler for consumers.

// @neutrinojs/env
const { EnvironmentPlugin } = require('webpack');

module.exports = (neutrino, additionalVars = []) => neutrino.config
  .plugin('env')
  .use(EnvironmentPlugin, ['NODE_ENV', ...additionalVars]);
// env preset (which is also middleware)
const env = require('@neutrinojs/env');

module.exports = neutrino => {
  neutrino.use(env, ['SECRET_KEY']);
  neutrino.use(/* next middleware */);
  neutrino.use(/* next middleware */)
};

Configuring

If your middleware requires configuration outside of the options necessary for running the middleware, use a closure technique for simplifying this for your middleware consumers. In short, your module will provide a function to consumers which, when executed, will return a Neutrino middleware function. Describing this in code:

module.exports = function wrapper(...args) {
  return function middleware(neutrino, options) {
    // do something with neutrino, options, and args
  };
};

Let's create a contrived example using our env middleware. Let's use a closure to let the consumer provide an alternate plugin name when creating the middleware:

// @neutrinojs/env
const { EnvironmentPlugin } = require('webpack');

module.exports = (pluginName = 'env') => (neutrino, additionalVars = []) => {
    neutrino.config
      .plugin(pluginName)
      .use(EnvironmentPlugin, ['NODE_ENV', ...additionalVars]);
};
// react preset (which is also middleware)
const env = require('@neutrinojs/env');

module.exports = neutrino => {
  neutrino.use(env('ENV-PLUGIN'), ['SECRET_KEY']);
};

Distributing

If you would like your middleware to be used by others, feel free to publish and distribute! By putting your middleware on npm, GitHub, or another location, you can share the hard work put into abstracting away Neutrino and webpack interactions and save everyone in the community time and effort. As long as the Neutrino CLI, other middleware, or presets can require your middleware, it puts no restrictions on where you want to host it.

Core middleware

Neutrino maintains a number of core middleware packages which aid in creating the various preset packages we also distribute. Continue onward for documentation on these various middleware packages.