diff --git a/packages/neutrino/bin/neutrino b/packages/neutrino/bin/neutrino index d5eaa6f..f2649a4 100755 --- a/packages/neutrino/bin/neutrino +++ b/packages/neutrino/bin/neutrino @@ -63,35 +63,10 @@ function run(command, presets) { const api = new Neutrino(Object.assign(options, { config })); // Grab all presets and merge them into a single webpack-chain config instance - presets.forEach(preset => { - const paths = [ - join(cwd, preset), - join(cwd, 'node_modules', preset), - preset - ]; - - for (let i = 0; i < paths.length; i += 1) { - try { - return api.use(require(paths[i])); - } catch (exception) { - if (/Cannot find module/.test(exception.message)) { - continue; - } - - exception.message = `Neutrino was unable to load the module '${preset}'. ` + - `Ensure this module exports a function and is free from errors.\n${exception.message}`; - throw exception; - } - } - - throw new Error(`Neutrino cannot find a module with the name or path '${preset}'. ` + - `Ensure this module can be found relative to the root of the project.`); - }); + api.import(presets); // Also grab any Neutrino config from package.json and merge it into the config at a higher precedence - if (Object.keys(config).length) { - api.config.merge(config); - } + api.use(() => api.config.merge(config)); if (args.inspect) { return inspect(api.getWebpackOptions()); diff --git a/packages/neutrino/package.json b/packages/neutrino/package.json index 6049f82..9a65625 100644 --- a/packages/neutrino/package.json +++ b/packages/neutrino/package.json @@ -31,5 +31,9 @@ "webpack-chain": "^3.0.0", "webpack-dev-server": "^2.4.1", "yargs": "^6.6.0" + }, + "devDependencies": { + "fs-extra": "^2.1.2", + "pify": "^2.3.0" } } diff --git a/packages/neutrino/src/neutrino.js b/packages/neutrino/src/neutrino.js index 75d510e..2da8f0c 100644 --- a/packages/neutrino/src/neutrino.js +++ b/packages/neutrino/src/neutrino.js @@ -5,6 +5,7 @@ const webpack = require('webpack'); const Config = require('webpack-chain'); const ora = require('ora'); const merge = require('deepmerge'); +const requireMiddleware = require('./requireMiddleware'); const normalizePath = (path, root) => (isAbsolute(path) ? path : join(root, path)); @@ -27,6 +28,14 @@ class Neutrino extends EventEmitter { preset(this, options); } + import(middleware) { + this.require(middleware).forEach(middleware => this.use(middleware)); + } + + require(middleware) { + return requireMiddleware(middleware, this.options); + } + /* eslint-disable no-console */ handleErrors(err, stats) { if (err) { diff --git a/packages/neutrino/src/requireMiddleware.js b/packages/neutrino/src/requireMiddleware.js new file mode 100644 index 0000000..cc1b777 --- /dev/null +++ b/packages/neutrino/src/requireMiddleware.js @@ -0,0 +1,36 @@ +/* eslint-disable global-require*/ +const { join } = require('path'); + +const castToArray = val => (Array.isArray(val) ? val : [val]); + +function requirePath(path, middleware) { + try { + return require(path); + } catch (exception) { + if (!/Cannot find module/.test(exception.message)) { + exception.message = `Neutrino was unable to load the module '${middleware}'. ` + + `Ensure this module exports a function and is free from errors.\n${exception.message}`; + throw exception; + } + + return undefined; + } +} + +module.exports = function requireMiddleware(middleware, options = {}) { + const root = options.root || process.cwd(); + + return castToArray(middleware).map((middleware) => { + const path = [ + join(root, middleware), + join(root, 'node_modules', middleware) + ].find(path => requirePath(path)); + + if (!path) { + throw new Error(`Neutrino cannot find a module with the name or path '${middleware}'. ` + + 'Ensure this module can be found relative to the root of the project.'); + } + + return require(path); + }); +}; diff --git a/packages/neutrino/test/api_test.js b/packages/neutrino/test/api_test.js index 0c7aee9..cabdae5 100644 --- a/packages/neutrino/test/api_test.js +++ b/packages/neutrino/test/api_test.js @@ -86,6 +86,14 @@ test('events handle multiple promise resolutions', async t => { t.deepEqual(values, ['alpha', 'beta', 'gamma']); }); +test('import middleware for use', t => { + const api = new Neutrino({ root: __dirname }); + + api.import('fixtures/middleware'); + + t.notDeepEqual(api.getWebpackOptions(), {}); +}); + test('creates a Webpack config', t => { const api = new Neutrino(); diff --git a/packages/neutrino/test/fixtures/middleware.js b/packages/neutrino/test/fixtures/middleware.js new file mode 100644 index 0000000..e4aae09 --- /dev/null +++ b/packages/neutrino/test/fixtures/middleware.js @@ -0,0 +1,3 @@ +module.exports = (api) => api.config.module + .rule('compile') + .test(/\.js$/); diff --git a/packages/neutrino/test/requireMiddleware_test.js b/packages/neutrino/test/requireMiddleware_test.js new file mode 100644 index 0000000..681b31c --- /dev/null +++ b/packages/neutrino/test/requireMiddleware_test.js @@ -0,0 +1,45 @@ +import { join } from 'path'; +import { outputFile as fsOutputFile, remove as fsRemove } from 'fs-extra'; +import pify from 'pify'; +import test from 'ava'; +import requireMiddleware from '../src/requireMiddleware'; + +const cwd = process.cwd(); +const outputFile = pify(fsOutputFile); +const remove = pify(fsRemove); + +const rootPath = join(__dirname, 'test-module'); +const rootMiddlewarePath = join(rootPath, 'middleware.js'); +const errorMiddlewarePath = join(rootPath, 'errorMiddleware.js'); +const modulePath = join(rootPath, 'node_modules', 'mymodule'); +const moduleMiddlewarePath = join(modulePath, 'index.js'); + +test.before(async (t) => { + await Promise.all([ + outputFile(rootMiddlewarePath, 'module.exports = "root"'), + outputFile(errorMiddlewarePath, '[;'), + outputFile(moduleMiddlewarePath, 'module.exports = "mymodule"') + ]) + process.chdir(rootPath); +}); + +test.after.always(async (t) => { + await remove(rootPath); + process.chdir(cwd); +}); + +test('requires middleware relative to root', t => { + t.is(requireMiddleware('middleware')[0], 'root'); +}); + +test('requires middleware from root/node_modules', t => { + t.is(requireMiddleware('mymodule')[0], 'mymodule'); +}); + +test('throws if middleware contains error', t => { + t.throws(() => requireMiddleware('errorMiddleware')); +}); + +test('throws if middleware cannot be found', t => { + t.throws(() => requireMiddleware('notExistent')); +});