Browse Source

Merge pull request #119 from jaridmargolin/neutrino-refactor

Neutrino refactor
v6-dev
Eli Perelman 8 years ago
committed by GitHub
parent
commit
69360d06b1
  1. 2
      .eslintrc.js
  2. 2
      package.json
  3. 79
      packages/neutrino/bin/neutrino
  4. 5
      packages/neutrino/package.json
  5. 49
      packages/neutrino/src/neutrino.js
  6. 36
      packages/neutrino/src/requireMiddleware.js
  7. 35
      packages/neutrino/test/api_test.js
  8. 3
      packages/neutrino/test/fixtures/middleware.js
  9. 45
      packages/neutrino/test/requireMiddleware_test.js

2
.eslintrc.js

@ -39,7 +39,7 @@ api.use(airbnb, {
}],
// Allow console during development, otherwise throw an error
'no-console': 'warn',
'no-console': 'off',
// Allow extra parentheses since multiline JSX being wrapped in parens is considered idiomatic
'no-extra-parens': 'off',

2
package.json

@ -8,7 +8,7 @@
"bugs": "https://github.com/mozilla-neutrino/neutrino-dev/issues",
"repository": "mozilla-neutrino/neutrino-dev",
"scripts": {
"lint": "eslint packages/*/*.js packages/*/src/**/*.js",
"lint": "eslint packages/*/*.js packages/*/src/**/*.js packages/neutrino/bin/*",
"bootstrap": "oao bootstrap",
"changelog": "changelog mozilla-neutrino/neutrino-dev all --markdown > CHANGELOG.md",
"deps:add": "oao add",

79
packages/neutrino/bin/neutrino

@ -10,17 +10,10 @@ const { pathOr, pipe, partialRight } = require('ramda');
const { join } = require('path');
const stringify = require('javascript-stringify');
const sort = require('deep-sort-object');
const optional = require('optional');
// eslint-disable-next-line no-console
const inspect = pipe(sort, partialRight(stringify, [null, 2]), console.log, process.exit);
const cwd = process.cwd();
let pkg = {};
try {
pkg = require(join(cwd, 'package.json'));
} catch (ex) {}
const pkgPresets = pathOr([], ['neutrino', 'presets'], pkg);
const environments = { build: 'production', start: 'development', test: 'test' };
const args = yargs
.option('inspect', {
description: 'Output a string representation of the configuration used by Neutrino and exit',
@ -28,12 +21,17 @@ const args = yargs
default: false,
global: true
})
.option('presets', {
description: 'A list of Neutrino presets used to configure the build',
.option('use', {
description: 'A list of Neutrino middleware used to configure the build',
array: true,
default: [],
global: true
})
.option('env', {
description: 'The value for the environment variable, NODE_ENV',
string: true,
global: true
})
.command('start', 'Build a project in development mode')
.command('build', 'Compile the source directory to a bundled build')
.command('test [files..]', 'Run all suites from the test directory or provided files', {
@ -55,57 +53,26 @@ const args = yargs
.help()
.argv;
function run(command, presets) {
process.env.NODE_ENV = environments[command];
function run(command, args) {
const pkg = optional(join(process.cwd(), 'package.json')) || {};
const pkgMiddleware = pathOr([], ['neutrino', 'use'], pkg);
const middleware = [...new Set(pkgMiddleware.concat(args.use))];
const options = pathOr({}, ['neutrino', 'options'], pkg);
const config = pathOr({}, ['neutrino', 'config'], pkg);
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.`);
});
// Grab all middleware and merge them into a single webpack-chain config instance
api.import(middleware);
// 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);
}
if (args.inspect) {
return inspect(api.getWebpackOptions());
}
api[command](args)
.then(() => process.exit(0))
.catch(err => {
if (err) {
console.error(err);
}
api.use(() => api.config.merge(config));
process.exit(1);
});
return args.inspect ?
inspect(api.getWebpackOptions()) :
api[command](args);
}
run(args._[0], [...new Set(pkgPresets.concat(args.presets))]);
run(args._[0], args).catch((err) => {
console.error(err || `Error during ${args._[0]}`);
process.exit(1);
});

5
packages/neutrino/package.json

@ -25,11 +25,16 @@
"deep-sort-object": "^1.0.1",
"deepmerge": "^1.3.2",
"javascript-stringify": "^1.6.0",
"optional": "^0.1.3",
"ora": "^1.1.0",
"ramda": "^0.23.0",
"webpack": "^2.2.1",
"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"
}
}

49
packages/neutrino/src/neutrino.js

@ -5,6 +5,8 @@ const webpack = require('webpack');
const Config = require('webpack-chain');
const ora = require('ora');
const merge = require('deepmerge');
const { defaultTo } = require('ramda');
const requireMiddleware = require('./requireMiddleware');
const normalizePath = (path, root) => (isAbsolute(path) ? path : join(root, path));
@ -23,11 +25,18 @@ class Neutrino extends EventEmitter {
this.options = merge(options, { root, source, output, tests, node_modules, entry });
}
use(preset, options = {}) {
preset(this, options);
use(middleware, options = {}) {
middleware(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) {
console.error(err.stack || err);
@ -48,7 +57,6 @@ class Neutrino extends EventEmitter {
return false;
}
/* eslint-enable no-console */
getWebpackOptions() {
return this.config.toConfig();
@ -59,31 +67,28 @@ class Neutrino extends EventEmitter {
}
build(args) {
return this
.emitForAll('prebuild', args)
.then(() => this.builder())
.then(() => this.emitForAll('build', args));
return this.runCommand('build', args, () => this.builder());
}
start(args) {
return this
.emitForAll('prestart', args)
.then(() => {
const config = this.getWebpackOptions();
if (config.devServer) {
return this.devServer();
}
return this.watcher();
})
.then(() => this.emitForAll('start', args));
return this.runCommand('start', args, () => (this.getWebpackOptions().devServer ? this.devServer() : this.watcher()));
}
test(args) {
return this.runCommand('test', args);
}
runCommand(command, args = {}, fn) {
process.env.NODE_ENV = defaultTo({
build: 'production',
start: 'development',
test: 'test'
}[command], args.env);
return this
.emitForAll('pretest', args)
.then(() => this.emitForAll('test', args));
.emitForAll(`pre${command}`, args)
.then(fn)
.then(() => this.emitForAll(command, args));
}
devServer() {

36
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);
});
};

35
packages/neutrino/test/api_test.js

@ -86,6 +86,41 @@ 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('command sets correct NODE_ENV', t => {
const api = new Neutrino();
api.runCommand('build');
t.is(process.env.NODE_ENV, 'production');
api.runCommand('start');
t.is(process.env.NODE_ENV, 'development');
api.runCommand('test');
t.is(process.env.NODE_ENV, 'test');
api.runCommand('build', { env: 'development' });
t.is(process.env.NODE_ENV, 'development');
});
test('command emits events around execution', async (t) => {
const api = new Neutrino();
const events = [];
api.on('prebuild', () => events.push('alpha'));
api.on('build', () => events.push('gamma'));
await api.runCommand('build', {}, () => events.push('beta'));
t.deepEqual(events, ['alpha', 'beta', 'gamma']);
});
test('creates a Webpack config', t => {
const api = new Neutrino();

3
packages/neutrino/test/fixtures/middleware.js

@ -0,0 +1,3 @@
module.exports = (api) => api.config.module
.rule('compile')
.test(/\.js$/);

45
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'));
});
Loading…
Cancel
Save