Browse Source

Functional refactor

v6-dev
Eli Perelman 8 years ago
parent
commit
ea5c0e72e5
  1. 13
      .eslintrc.js
  2. 1
      .travis.yml
  3. 60
      docs/middleware/neutrino-middleware-progress/README.md
  4. 2
      packages/neutrino-middleware-clean/index.js
  5. 60
      packages/neutrino-middleware-progress/README.md
  6. 3
      packages/neutrino-middleware-progress/index.js
  7. 26
      packages/neutrino-middleware-progress/package.json
  8. 1765
      packages/neutrino-middleware-progress/yarn.lock
  9. 2
      packages/neutrino-preset-karma/index.js
  10. 16
      packages/neutrino-preset-node/index.js
  11. 1
      packages/neutrino-preset-node/package.json
  12. 8
      packages/neutrino-preset-node/test/node_test.js
  13. 8
      packages/neutrino-preset-react/test/react_test.js
  14. 32
      packages/neutrino-preset-web/index.js
  15. 1
      packages/neutrino-preset-web/package.json
  16. 8
      packages/neutrino-preset-web/test/web_test.js
  17. 23
      packages/neutrino/bin/build.js
  18. 7
      packages/neutrino/bin/inspect.js
  19. 52
      packages/neutrino/bin/neutrino
  20. 35
      packages/neutrino/bin/start.js
  21. 7
      packages/neutrino/bin/test.js
  22. 7
      packages/neutrino/package.json
  23. 71
      packages/neutrino/src/api.js
  24. 16
      packages/neutrino/src/build.js
  25. 19
      packages/neutrino/src/devServer.js
  26. 46
      packages/neutrino/src/index.js
  27. 9
      packages/neutrino/src/inspect.js
  28. 72
      packages/neutrino/src/neutrino.js
  29. 36
      packages/neutrino/src/requireMiddleware.js
  30. 6
      packages/neutrino/src/start.js
  31. 5
      packages/neutrino/src/test.js
  32. 64
      packages/neutrino/src/utils.js
  33. 16
      packages/neutrino/src/watch.js
  34. 10
      packages/neutrino/src/webpack/build.js
  35. 28
      packages/neutrino/src/webpack/develop.js
  36. 22
      packages/neutrino/src/webpack/utils.js
  37. 15
      packages/neutrino/src/webpack/watch.js
  38. 63
      packages/neutrino/test/api_test.js
  39. 45
      packages/neutrino/test/requireMiddleware_test.js
  40. 52
      packages/neutrino/test/require_test.js
  41. 32
      packages/neutrino/test/run_test.js
  42. 50
      packages/neutrino/yarn.lock

13
.eslintrc.js

@ -1,13 +1,16 @@
const Neutrino = require('./packages/neutrino');
const { Neutrino } = require('./packages/neutrino');
const airbnb = require('./packages/neutrino-preset-airbnb-base');
const api = new Neutrino();
const api = Neutrino();
api.use(airbnb, {
eslint: {
node: true,
plugins: ['eslint-plugin-prettier'],
rules: {
// Algebraic and functional types should allow capital constructors without new
'babel/new-cap': 'off',
// Disable necessitating return after a callback
'callback-return': 'off',
@ -63,9 +66,15 @@ api.use(airbnb, {
// Restrict usage of specified node modules
'no-restricted-modules': 'off',
// Allow return assign
'no-return-assign': 'off',
// Allowing shadowing variable that share the same context as the outer scope
'no-shadow': 'off',
// It makes sense to have unused expressions to avoid imperative conditionals
'no-unused-expressions': 'off',
// Allow use of synchronous methods (off by default)
'no-sync': 'off',

1
.travis.yml

@ -22,7 +22,6 @@ cache:
- packages/neutrino-middleware-loader-merge/node_modules
- packages/neutrino-middleware-minify/node_modules
- packages/neutrino-middleware-named-modules/node_modules
- packages/neutrino-middleware-progress/node_modules
- packages/neutrino-middleware-start-server/node_modules
- packages/neutrino-middleware-style-loader/node_modules
- packages/neutrino-preset-airbnb-base/node_modules

60
docs/middleware/neutrino-middleware-progress/README.md

@ -1,60 +0,0 @@
# Neutrino Progress Middleware
[![NPM version][npm-image]][npm-url] [![NPM downloads][npm-downloads]][npm-url] [![Join Slack][slack-image]][slack-url]
`neutrino-middleware-progress` is Neutrino middleware for displaying a progress bar showing the progress of a build.
## Requirements
- Node.js v6.9+
- Yarn or npm client
- Neutrino v5
## Installation
`neutrino-middleware-progress` can be installed via the Yarn or npm clients.
#### Yarn
```bash
❯ yarn add neutrino-middleware-progress
```
#### npm
```bash
❯ npm install --save neutrino-middleware-progress
```
## Usage
`neutrino-middleware-progress` can be consumed from the Neutrino API, middleware, or presets. Require this package
and plug it into Neutrino:
```js
const progress = require('neutrino-middleware-progress');
neutrino.use(progress);
```
## Customization
`neutrino-middleware-progress` creates some conventions to make overriding the configuration easier once you are ready to
make changes.
### Plugins
The following is a list of plugins and their identifiers which can be overridden:
- `progress`: Displays a bar showing progression of build.
## Contributing
This preset is part of the [neutrino-dev](https://github.com/mozilla-neutrino/neutrino-dev) repository, a monorepo
containing all resources for developing Neutrino and its core presets. Follow the
[contributing guide](../../contributing/README.md) for details.
[npm-image]: https://img.shields.io/npm/v/neutrino-middleware-progress.svg
[npm-downloads]: https://img.shields.io/npm/dt/neutrino-middleware-progress.svg
[npm-url]: https://npmjs.org/package/neutrino-middleware-progress
[slack-image]: https://neutrino-slack.herokuapp.com/badge.svg
[slack-url]: https://neutrino-slack.herokuapp.com/

2
packages/neutrino-middleware-clean/index.js

@ -6,5 +6,5 @@ module.exports = (neutrino, options) => {
neutrino.config
.plugin('clean')
.use(CleanPlugin, [paths, { root }]);
.use(CleanPlugin, [paths, { root, verbose: false }]);
};

60
packages/neutrino-middleware-progress/README.md

@ -1,60 +0,0 @@
# Neutrino Progress Middleware
[![NPM version][npm-image]][npm-url] [![NPM downloads][npm-downloads]][npm-url] [![Join Slack][slack-image]][slack-url]
`neutrino-middleware-progress` is Neutrino middleware for displaying a progress bar showing the progress of a build.
## Requirements
- Node.js v6.9+
- Yarn or npm client
- Neutrino v5
## Installation
`neutrino-middleware-progress` can be installed via the Yarn or npm clients.
#### Yarn
```bash
❯ yarn add neutrino-middleware-progress
```
#### npm
```bash
❯ npm install --save neutrino-middleware-progress
```
## Usage
`neutrino-middleware-progress` can be consumed from the Neutrino API, middleware, or presets. Require this package
and plug it into Neutrino:
```js
const progress = require('neutrino-middleware-progress');
neutrino.use(progress);
```
## Customization
`neutrino-middleware-progress` creates some conventions to make overriding the configuration easier once you are ready to
make changes.
### Plugins
The following is a list of plugins and their identifiers which can be overridden:
- `progress`: Displays a bar showing progression of build.
## Contributing
This preset is part of the [neutrino-dev](https://github.com/mozilla-neutrino/neutrino-dev) repository, a monorepo
containing all resources for developing Neutrino and its core presets. Follow the
[contributing guide](https://neutrino.js.org/contributing) for details.
[npm-image]: https://img.shields.io/npm/v/neutrino-middleware-progress.svg
[npm-downloads]: https://img.shields.io/npm/dt/neutrino-middleware-progress.svg
[npm-url]: https://npmjs.org/package/neutrino-middleware-progress
[slack-image]: https://neutrino-slack.herokuapp.com/badge.svg
[slack-url]: https://neutrino-slack.herokuapp.com/

3
packages/neutrino-middleware-progress/index.js

@ -1,3 +0,0 @@
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
module.exports = ({ config }) => config.plugin('progress').use(ProgressBarPlugin);

26
packages/neutrino-middleware-progress/package.json

@ -1,26 +0,0 @@
{
"name": "neutrino-middleware-progress",
"version": "5.0.0",
"description": "Neutrino middleware for displaying a progress bar during compilation",
"main": "index.js",
"keywords": [
"neutrino",
"neutrino-middleware",
"progress",
"bar"
],
"author": "Eli Perelman <eli@eliperelman.com>",
"license": "MPL-2.0",
"repository": "https://github.com/mozilla-neutrino/neutrino-dev/tree/master/packages/neutrino-middleware-progress",
"homepage": "https://neutrino.js.org",
"bugs": "https://github.com/mozilla-neutrino/neutrino-dev/issues",
"dependencies": {
"progress-bar-webpack-plugin": "^1.9.3"
},
"devDependencies": {
"webpack": "^2.2.1"
},
"peerDependencies": {
"neutrino": "^5.0.0"
}
}

1765
packages/neutrino-middleware-progress/yarn.lock

File diff suppressed because it is too large

2
packages/neutrino-preset-karma/index.js

@ -44,7 +44,7 @@ module.exports = (neutrino) => {
{
singleRun: !watch,
autoWatch: watch,
webpack: neutrino.getWebpackConfig()
webpack: neutrino.config.toConfig()
}
]);

16
packages/neutrino-preset-node/index.js

@ -1,7 +1,6 @@
const banner = require('neutrino-middleware-banner');
const compile = require('neutrino-middleware-compile-loader');
const copy = require('neutrino-middleware-copy');
const progress = require('neutrino-middleware-progress');
const clean = require('neutrino-middleware-clean');
const loaderMerge = require('neutrino-middleware-loader-merge');
const startServer = require('neutrino-middleware-start-server');
@ -9,7 +8,7 @@ const hot = require('neutrino-middleware-hot');
const namedModules = require('neutrino-middleware-named-modules');
const nodeExternals = require('webpack-node-externals');
const { join } = require('path');
const { pathOr } = require('ramda');
const { path } = require('ramda');
const MODULES = join(__dirname, 'node_modules');
@ -23,6 +22,16 @@ module.exports = (neutrino) => {
} catch (ex) {}
/* eslint-enable global-require no-empty */
if (!path(['options', 'compile', 'targets', 'browsers'], neutrino)) {
Object.assign(neutrino.options, {
compile: {
targets: {
node: 6.9
}
}
});
}
neutrino.use(namedModules);
neutrino.use(compile, {
include: [neutrino.options.source, neutrino.options.tests],
@ -31,7 +40,7 @@ module.exports = (neutrino) => {
presets: [
[require.resolve('babel-preset-env'), {
modules: false,
targets: pathOr({ node: 6.9 }, ['options', 'compile', 'targets'], neutrino)
targets: neutrino.options.compile.targets
}]
]
}
@ -81,7 +90,6 @@ module.exports = (neutrino) => {
if (process.env.NODE_ENV !== 'development') {
neutrino.use(clean, { paths: [neutrino.options.output] });
neutrino.use(progress);
neutrino.use(copy, {
patterns: [{ context: neutrino.options.source, from: '**/*' }],
options: { ignore: ['*.js*'] }

1
packages/neutrino-preset-node/package.json

@ -29,7 +29,6 @@
"neutrino-middleware-hot": "^5.0.0",
"neutrino-middleware-loader-merge": "^5.0.0",
"neutrino-middleware-named-modules": "^5.0.0",
"neutrino-middleware-progress": "^5.0.0",
"neutrino-middleware-start-server": "^5.0.0"
},
"peerDependencies": {

8
packages/neutrino-preset-node/test/web_test.js → packages/neutrino-preset-node/test/node_test.js

@ -1,23 +1,23 @@
import test from 'ava';
import { validate } from 'webpack';
import Neutrino from 'neutrino';
import { Neutrino } from 'neutrino';
test('loads preset', t => {
t.notThrows(() => require('..'));
});
test('uses preset', t => {
const api = new Neutrino();
const api = Neutrino();
t.notThrows(() => api.use(require('..')));
});
test('valid preset', t => {
const api = new Neutrino();
const api = Neutrino();
api.use(require('..'));
const errors = validate(api.getWebpackConfig());
const errors = validate(api.config.toConfig());
t.is(errors.length, 0);
});

8
packages/neutrino-preset-react/test/web_test.js → packages/neutrino-preset-react/test/react_test.js

@ -1,23 +1,23 @@
import test from 'ava';
import { validate } from 'webpack';
import Neutrino from 'neutrino';
import { Neutrino } from 'neutrino';
test('loads preset', t => {
t.notThrows(() => require('..'));
});
test('uses preset', t => {
const api = new Neutrino();
const api = Neutrino();
t.notThrows(() => api.use(require('..')));
});
test('valid preset', t => {
const api = new Neutrino();
const api = Neutrino();
api.use(require('..'));
const errors = validate(api.getWebpackConfig());
const errors = validate(api.config.toConfig());
t.is(errors.length, 0);
});

32
packages/neutrino-preset-web/index.js

@ -8,13 +8,12 @@ const htmlTemplate = require('neutrino-middleware-html-template');
const chunk = require('neutrino-middleware-chunk');
const hot = require('neutrino-middleware-hot');
const copy = require('neutrino-middleware-copy');
const progress = require('neutrino-middleware-progress');
const clean = require('neutrino-middleware-clean');
const minify = require('neutrino-middleware-minify');
const loaderMerge = require('neutrino-middleware-loader-merge');
const namedModules = require('neutrino-middleware-named-modules');
const { join } = require('path');
const { pathOr } = require('ramda');
const { path, pathOr } = require('ramda');
const MODULES = join(__dirname, 'node_modules');
@ -45,6 +44,23 @@ function devServer({ config }, options) {
module.exports = (neutrino) => {
const { config } = neutrino;
if (!path(['options', 'compile', 'targets', 'browsers'], neutrino)) {
Object.assign(neutrino.options, {
compile: {
targets: {
browsers: [
'last 2 Chrome versions',
'last 2 Firefox versions',
'last 2 Edge versions',
'last 2 Opera versions',
'last 2 Safari versions',
'last 2 iOS versions'
]
}
}
});
}
neutrino.use(env);
neutrino.use(htmlLoader);
neutrino.use(styleLoader);
@ -61,16 +77,7 @@ module.exports = (neutrino) => {
modules: false,
useBuiltIns: true,
include: ['transform-regenerator'],
targets: pathOr({
browsers: [
'last 2 Chrome versions',
'last 2 Firefox versions',
'last 2 Edge versions',
'last 2 Opera versions',
'last 2 Safari versions',
'last 2 iOS versions'
]
}, ['options', 'compile', 'targets'], neutrino)
targets: neutrino.options.compile.targets
}]
]
}
@ -135,7 +142,6 @@ module.exports = (neutrino) => {
.add('webpack/hot/dev-server');
} else {
neutrino.use(clean, { paths: [neutrino.options.output] });
neutrino.use(progress);
neutrino.use(minify);
neutrino.use(copy, {
patterns: [{ context: neutrino.options.source, from: '**/*' }],

1
packages/neutrino-preset-web/package.json

@ -36,7 +36,6 @@
"neutrino-middleware-loader-merge": "^5.0.0",
"neutrino-middleware-image-loader": "^5.0.0",
"neutrino-middleware-named-modules": "^5.0.0",
"neutrino-middleware-progress": "^5.0.0",
"neutrino-middleware-minify": "^5.0.0",
"neutrino-middleware-style-loader": "^5.0.0"
},

8
packages/neutrino-preset-web/test/web_test.js

@ -1,23 +1,23 @@
import test from 'ava';
import { validate } from 'webpack';
import Neutrino from 'neutrino';
import { Neutrino } from 'neutrino';
test('loads preset', t => {
t.notThrows(() => require('..'));
});
test('uses preset', t => {
const api = new Neutrino();
const api = Neutrino();
t.notThrows(() => api.use(require('..')));
});
test('valid preset', t => {
const api = new Neutrino();
const api = Neutrino();
api.use(require('..'));
const errors = validate(api.getWebpackConfig());
const errors = validate(api.config.toConfig());
t.is(errors.length, 0);
});

23
packages/neutrino/bin/build.js

@ -0,0 +1,23 @@
const { build } = require('../src');
const ora = require('ora');
module.exports = (middleware, options) => {
const spinner = ora('Building project').start();
return build(middleware, options)
.fork((errors) => {
spinner.fail('Building project failed');
errors.forEach((err) => {
console.error(err.stack || err);
err.details && console.error(err.details);
});
process.exit(1);
}, (stats) => {
spinner.succeed('Building project completed');
console.log(stats.toString({
colors: true,
chunks: false,
children: false
}));
});
};

7
packages/neutrino/bin/inspect.js

@ -0,0 +1,7 @@
const { inspect } = require('../src');
module.exports = (middleware, options) => inspect(middleware, options)
.fork((err) => {
console.error(err.stack || err);
process.exit(1);
}, console.log);

52
packages/neutrino/bin/neutrino

@ -4,16 +4,14 @@
// https://github.com/babel/babel-loader/pull/391
process.noDeprecation = true;
const Neutrino = require('../src/neutrino');
const yargs = require('yargs');
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');
const { pathOr } = require('ramda');
const build = require('./build');
const inspect = require('./inspect');
const start = require('./start');
const test = require('./test');
const { getNodeEnv, getPackageJson } = require('../src/utils');
// eslint-disable-next-line no-console
const inspect = pipe(sort, partialRight(stringify, [null, 2]), console.log, process.exit);
const args = yargs
.option('inspect', {
description: 'Output a string representation of the configuration used by Neutrino and exit',
@ -53,26 +51,28 @@ const args = yargs
.help()
.argv;
function run(command, args) {
const pkg = optional(join(process.cwd(), 'package.json')) || {};
// foldMiddleware :: Object -> Array
const foldMiddleware = (args) => {
const pkg = getPackageJson();
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 middleware and merge them into a single webpack-chain config instance
api.import(middleware);
return [...new Set(pkgMiddleware.concat(args.use))];
};
// normalizeOptions :: Object -> Object
const normalizeOptions = (args) => {
const pkg = getPackageJson();
const options = pathOr({}, ['neutrino', 'options'], pkg);
const env = args.inspect ? getNodeEnv(args._[0], args.env) : args.env;
// Also grab any Neutrino config from package.json and merge it into the config at a higher precedence
api.use(() => api.config.merge(config));
return Object.assign(options, {
config: pathOr({}, ['neutrino', 'config'], pkg),
args: Object.assign(args, { env })
});
};
return args.inspect ?
inspect(api.getWebpackConfig()) :
api[command](args);
}
const cmd = args.inspect ? 'inspect' : args._[0];
const middleware = foldMiddleware(args);
const options = normalizeOptions(args);
run(args._[0], args).catch((err) => {
console.error(err || `Error during ${args._[0]}`);
process.exit(1);
});
({ build, inspect, start, test })[cmd](middleware, options);

35
packages/neutrino/bin/start.js

@ -0,0 +1,35 @@
const { start } = require('../src');
const ora = require('ora');
module.exports = (middleware, options) => {
const spinner = ora('Building project').start();
return start(middleware, options)
.fork((errors) => {
spinner.fail('Building project failed');
errors.forEach((err) => {
console.error(err.stack || err);
err.details && console.error(err.details);
});
process.exit(1);
}, (compiler) => { // eslint-disable-line consistent-return
if (!compiler.options.devServer) {
return spinner.succeed('Build completed');
}
const { devServer } = compiler.options;
const protocol = devServer.https ? 'https' : 'http';
const { host, port } = devServer;
spinner.succeed(`Development server running on: ${protocol}://${host}:${port}`);
const building = ora('Waiting for initial build to finish').start();
compiler.plugin('done', () => building.succeed('Build completed'));
compiler.plugin('compile', () => {
building.text = 'Source changed, re-compiling';
building.start();
});
});
};

7
packages/neutrino/bin/test.js

@ -0,0 +1,7 @@
const { test } = require('../src');
module.exports = (middleware, options) => test(middleware, options)
.fork((err) => {
console.error(err.stack || err);
process.exit(1);
}, Function.prototype);

7
packages/neutrino/package.json

@ -2,7 +2,7 @@
"name": "neutrino",
"version": "5.0.0",
"description": "Create and build JS applications with managed configurations",
"main": "src/neutrino.js",
"main": "src/index.js",
"bin": {
"neutrino": "./bin/neutrino"
},
@ -24,10 +24,15 @@
"dependencies": {
"deep-sort-object": "^1.0.1",
"deepmerge": "^1.3.2",
"fluture": "^5.0.0",
"immutable": "^3.8.1",
"immutable-ext": "^1.0.8",
"javascript-stringify": "^1.6.0",
"mitt": "^1.1.0",
"optional": "^0.1.3",
"ora": "^1.1.0",
"ramda": "^0.23.0",
"ramda-fantasy": "^0.7.0",
"webpack": "^2.2.1",
"webpack-chain": "^3.0.0",
"webpack-dev-server": "^2.4.1",

71
packages/neutrino/src/api.js

@ -0,0 +1,71 @@
const Config = require('webpack-chain');
const merge = require('deepmerge');
const { List } = require('immutable-ext');
const Future = require('fluture');
const mitt = require('mitt');
const { chain, defaultTo, pipe } = require('ramda');
const { createPaths, normalizePath, resolveAny, requireSafe } = require('./utils');
const build = require('./build');
const inspect = require('./inspect');
const start = require('./start');
const test = require('./test');
const getOptions = (options = {}) => {
const root = normalizePath(process.cwd(), defaultTo('', options.root));
const base = normalizePath(root);
const source = base(defaultTo('src', options.source));
const output = base(defaultTo('build', options.build));
const tests = base(defaultTo('test', options.tests));
const node_modules = base(defaultTo('node_modules', options.node_modules)); // eslint-disable-line camelcase
const entry = normalizePath(source, defaultTo('index.js', options.entry));
return merge(options, { root, source, output, tests, node_modules, entry });
};
// Api :: () -> Object
const Api = pipe(getOptions, (options) => {
const listeners = {};
const api = merge(mitt(listeners), {
listeners,
options,
commands: {},
config: new Config(),
// emitForAll :: String -> payload -> Promise
emitForAll: (eventName, payload) => Promise
.all((api.listeners[eventName] || []).map(f => f(payload))),
// register :: String command -> Future handler -> ()
register: (command, handler) => api.commands[command] = handler,
// requireMiddleware :: (Array String middleware) -> Future Error a
requires: middleware => List(middleware).map(pipe(
createPaths(api.options.root),
resolveAny,
chain(requireSafe)
)),
// requiresAndUses :: Future Error a -> Future Error a
requiresAndUses: middleware => api.useRequires(api.requires(middleware)),
// use :: Function middleware -> Object options -> IO ()
use: (middleware, options = {}) => middleware(api, options),
// useRequires :: Future Error a -> Future Error a
useRequires: requires => requires
// For all middleware, pass it to api.use()
.map(chain(Future.encase(api.use)))
// Fold the middleware down to a single Task/Future status
.reduce((f, current) => f.chain(() => current), Future.of())
});
api.register('build', build);
api.register('inspect', inspect);
api.register('start', start);
api.register('test', test);
return api;
});
module.exports = Api;

16
packages/neutrino/src/build.js

@ -0,0 +1,16 @@
const webpack = require('webpack');
const Future = require('fluture');
const { webpackErrors } = require('./utils');
// build :: Object config -> Future (Array Error) Function
const build = config => Future((reject, resolve) => {
const compiler = webpack(config);
compiler.run((err, stats) => {
const errors = webpackErrors(err, stats);
errors.length ? reject(errors) : resolve(stats);
});
});
module.exports = build;

19
packages/neutrino/src/devServer.js

@ -0,0 +1,19 @@
const merge = require('deepmerge');
const webpack = require('webpack');
const DevServer = require('webpack-dev-server');
const Future = require('fluture');
// devServer :: Object webpackConfig -> Future () Function
const devServer = webpackConfig => new Future((reject, resolve) => {
const config = merge({
devServer: { host: 'localhost', port: 5000, noInfo: true }
}, webpackConfig);
const { host, port } = config.devServer;
const compiler = webpack(config);
const server = new DevServer(compiler, config.devServer);
server.listen(port, host, () => resolve(compiler));
});
module.exports = devServer;

46
packages/neutrino/src/index.js

@ -0,0 +1,46 @@
const Api = require('./api');
const { partial } = require('ramda');
const Future = require('fluture');
const { getNodeEnv, toArray } = require('./utils');
// run :: (String command -> Array middleware -> Object options) -> Future Error a
const run = (command, middleware, options) => {
const api = Api(options);
process.env.NODE_ENV = getNodeEnv(command, api.options.args && api.options.args.env);
// Require and use all middleware
return api.requiresAndUses(middleware)
// Also grab any config overrides and merge it into the config at a higher precedence
.map(() => api.config.merge(options.config))
// Trigger all pre-events for the current command
.chain(() => Future.fromPromise2(api.emitForAll, `pre${command}`, api.options.args))
// Execute the command
.chain(() => api.commands[command](api.config.toConfig()))
// Trigger all post-command events, resolving with the value of the command execution
.chain(value => Future
.fromPromise2(api.emitForAll, command, api.options.args)
.chain(() => Future.of(value)))
.mapRej(toArray);
};
// build :: (Array middleware -> Object options) -> Future Error a
const build = partial(run, ['build']);
// inspect :: (Array middleware -> Object options) -> Future Error a
const inspect = partial(run, ['inspect']);
// start :: (Array middleware -> Object options) -> Future Error a
const start = partial(run, ['start']);
// test :: (Array middleware -> Object options) -> Future Error a
const test = partial(run, ['test']);
module.exports = {
Neutrino: Api,
run,
build,
inspect,
start,
test
};

9
packages/neutrino/src/inspect.js

@ -0,0 +1,9 @@
const Future = require('fluture');
const stringify = require('javascript-stringify');
const sort = require('deep-sort-object');
const { partialRight, pipe } = require('ramda');
// inspect :: Object config -> Future () String
const inspect = pipe(sort, partialRight(stringify, [null, 2]), Future.of);
module.exports = inspect;

72
packages/neutrino/src/neutrino.js

@ -1,72 +0,0 @@
const { join, isAbsolute } = require('path');
const { EventEmitter } = require('events');
const Config = require('webpack-chain');
const merge = require('deepmerge');
const { defaultTo, identity } = require('ramda');
const requireMiddleware = require('./requireMiddleware');
const build = require('./webpack/build');
const develop = require('./webpack/develop');
const watch = require('./webpack/watch');
const normalizePath = (path, root) => (isAbsolute(path) ? path : join(root, path));
module.exports = class Neutrino extends EventEmitter {
constructor(options = {}) {
super();
const root = normalizePath(options.root || '', process.cwd());
const source = normalizePath(options.source || 'src', root);
const output = normalizePath(options.output || 'build', root);
const tests = normalizePath(options.tests || 'test', root);
const node_modules = normalizePath(options.node_modules || 'node_modules', root); // eslint-disable-line camelcase
const entry = normalizePath(options.entry || 'index.js', source);
this.config = new Config();
this.options = merge(options, { root, source, output, tests, node_modules, entry });
}
use(middleware, options = {}) {
middleware(this, options);
}
import(middleware) {
this.require(middleware).forEach(middleware => this.use(middleware));
}
require(middleware) {
return requireMiddleware(middleware, this.options);
}
getWebpackConfig() {
return this.config.toConfig();
}
emitForAll(eventName, payload) {
return Promise.all(this.listeners(eventName).map(fn => fn(payload)));
}
build(args) {
return this.runCommand('build', args, build);
}
start(args) {
return this.runCommand('start', args, this.getWebpackConfig().devServer ? develop : watch);
}
test(args) {
return this.runCommand('test', args);
}
runCommand(command, args = {}, fn = identity) {
process.env.NODE_ENV = defaultTo({
build: 'production',
start: 'development',
test: 'test'
}[command], args.env);
return this
.emitForAll(`pre${command}`, args)
.then(() => fn(this.getWebpackConfig()))
.then(() => this.emitForAll(command, args));
}
}

36
packages/neutrino/src/requireMiddleware.js

@ -1,36 +0,0 @@
/* 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);
});
};

6
packages/neutrino/src/start.js

@ -0,0 +1,6 @@
const { ifElse, propSatisfies } = require('ramda');
const devServer = require('./devServer');
const watch = require('./watch');
// start :: Object options -> Future (Array Error) Function
module.exports = ifElse(propSatisfies(Boolean, 'devServer'), devServer, watch);

5
packages/neutrino/src/test.js

@ -0,0 +1,5 @@
const Future = require('fluture');
const { identity } = require('ramda');
// test :: Object options -> Future () Function
module.exports = () => Future.of(identity);

64
packages/neutrino/src/utils.js

@ -0,0 +1,64 @@
const Future = require('fluture');
const { cond, curry, defaultTo, identity, map, memoize, of, partialRight, pipe, reduce, T } = require('ramda');
const { List } = require('immutable-ext');
const { isAbsolute, join } = require('path');
const optional = require('optional');
// any :: List -> Future a b
const any = reduce(Future.or, Future.reject('empty list'));
// createPaths :: (String -> String) -> List String
const createPaths = curry((base, middleware) => List.of(
join(base, middleware),
join(base, 'node_modules', middleware),
middleware
));
// getNodeEnv :: String command -> String? env -> String
const getNodeEnv = (command = 'start', env) => defaultTo({
build: 'production',
start: 'development',
test: 'test'
}[command] || 'development', env);
// getPackageJson :: () -> Object
const getPackageJson = memoize(pipe(
process.cwd,
partialRight(join, ['package.json']),
optional,
defaultTo({})
));
// normalize :: String base -> String path -> String
const normalizePath = curry((base, path) => (isAbsolute(path) ? path : join(base, path)));
// resolveSafe :: String -> Future Error String
const resolveSafe = Future.encase(require.resolve);
// requireSafe :: String -> Future Error a
const requireSafe = Future.encase(require);
// resolveAny :: List -> Future Error String
const resolveAny = pipe(map(resolveSafe), any);
// toArray :: a -> Array
const toArray = cond([
[Array.isArray, identity],
[T, of]
]);
// webpackErrors :: (Error|Array Error err -> Object stats) -> Array Error
const webpackErrors = (err, stats) => (err ? toArray(err) : stats.toJson().errors);
module.exports = {
any,
createPaths,
getNodeEnv,
getPackageJson,
normalizePath,
requireSafe,
resolveAny,
resolveSafe,
toArray,
webpackErrors
};

16
packages/neutrino/src/watch.js

@ -0,0 +1,16 @@
const webpack = require('webpack');
const Future = require('fluture');
const { webpackErrors } = require('./utils');
// watch :: Object config -> Future (Array Error) ()
const watch = config => new Future((reject, resolve) => {
const compiler = webpack(config);
compiler.watch(config.watchOptions || {}, (err, stats) => {
const errors = webpackErrors(err, stats);
errors.length ? reject(errors) : resolve(compiler);
});
});
module.exports = watch;

10
packages/neutrino/src/webpack/build.js

@ -1,10 +0,0 @@
const webpack = require('webpack');
const { handle, logErrors, logStats } = require('./utils');
module.exports = config => new Promise((resolve, reject) => {
const compiler = webpack(config);
compiler.run(handle((errors, stats) => (
errors.length ? reject(logErrors(errors)) : resolve(logStats(stats))
)));
});

28
packages/neutrino/src/webpack/develop.js

@ -1,28 +0,0 @@
const ora = require('ora');
const merge = require('deepmerge');
const webpack = require('webpack');
const DevServer = require('webpack-dev-server');
module.exports = _config => new Promise((resolve) => {
const defaultDevServer = { host: 'localhost', port: 5000, noInfo: true };
const config = merge({ devServer: defaultDevServer }, _config);
const protocol = config.devServer.https ? 'https' : 'http';
const { host, port } = config.devServer;
const starting = ora('Starting development server').start();
const compiler = webpack(config);
const server = new DevServer(compiler, config.devServer);
const building = ora('Waiting for initial build to finish').start();
server.listen(port, host, () => {
starting.succeed(`Development server running on: ${protocol}://${host}:${port}`);
compiler.plugin('done', () => building.succeed('Build completed'));
compiler.plugin('compile', () => {
building.text = 'Source changed, re-compiling';
building.start();
});
});
process.on('SIGINT', resolve);
});

22
packages/neutrino/src/webpack/utils.js

@ -1,22 +0,0 @@
module.exports.handle = fn => (err, stats) => fn(err ? [err] : stats.toJson().errors, stats);
module.exports.logStats = (stats) => {
console.log(stats.toString({
colors: true,
chunks: false,
children: false
}));
return stats;
};
module.exports.logErrors = (errors) => {
errors.forEach((err) => {
console.error(err.stack || err);
if (err.details) {
console.error(err.details);
}
});
return errors;
};

15
packages/neutrino/src/webpack/watch.js

@ -1,15 +0,0 @@
const webpack = require('webpack');
const ora = require('ora');
const { handle, logErrors } = require('./utils');
module.exports = config => new Promise((resolve) => {
const compiler = webpack(config);
const building = ora('Waiting for initial build to finish').start();
const watcher = compiler.watch(config.watchOptions || {}, handle((errors) => {
building.succeed('Build completed');
logErrors(errors);
}));
process.on('SIGINT', () => watcher.close(resolve));
});

63
packages/neutrino/test/api_test.js

@ -1,17 +1,17 @@
import test from 'ava';
import Neutrino from '../src/neutrino';
import { Neutrino } from '../src';
test('initializes with no arguments', t => {
t.notThrows(() => new Neutrino());
t.notThrows(() => Neutrino());
});
test('initializes with options', t => {
t.notThrows(() => new Neutrino({ testing: true }));
t.notThrows(() => Neutrino({ testing: true }));
});
test('initialization stores options', t => {
const options = { alpha: 'a', beta: 'b', gamma: 'c' };
const api = new Neutrino(options);
const api = Neutrino(options);
t.is(api.options.alpha, options.alpha);
t.is(api.options.beta, options.beta);
@ -19,19 +19,19 @@ test('initialization stores options', t => {
});
test('creates an instance of webpack-chain', t => {
const api = new Neutrino();
const api = Neutrino();
t.is(typeof api.config.toConfig, 'function');
});
test('middleware receives API instance', t => {
const api = new Neutrino();
const api = Neutrino();
api.use(n => t.is(n, api));
});
test('middleware receives default options', t => {
const api = new Neutrino();
const api = Neutrino();
api.use((api, options) => {
t.deepEqual(options, {});
@ -39,7 +39,7 @@ test('middleware receives default options', t => {
});
test('middleware receives options parameter', t => {
const api = new Neutrino();
const api = Neutrino();
const defaults = { alpha: 'a', beta: 'b', gamma: 'c' };
api.use((api, options) => {
@ -48,14 +48,14 @@ test('middleware receives options parameter', t => {
});
test('triggers promisified event handlers', t => {
const api = new Neutrino();
const api = Neutrino();
api.on('test', () => t.pass('test event triggered'));
api.emitForAll('test');
});
test('events handle promise resolution', async t => {
const api = new Neutrino();
const api = Neutrino();
api.on('test', () => Promise.resolve('alpha'));
@ -65,7 +65,7 @@ test('events handle promise resolution', async t => {
});
test('events handle promise rejection', async t => {
const api = new Neutrino();
const api = Neutrino();
api.on('test', () => Promise.reject(new Error('beta')));
@ -75,7 +75,7 @@ test('events handle promise rejection', async t => {
});
test('events handle multiple promise resolutions', async t => {
const api = new Neutrino();
const api = Neutrino();
api.on('test', () => Promise.resolve('alpha'));
api.on('test', () => Promise.resolve('beta'));
@ -86,47 +86,32 @@ 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 });
test('import middleware for use', async (t) => {
const api = Neutrino({ root: __dirname });
api.import('fixtures/middleware');
t.notDeepEqual(api.getWebpackConfig(), {});
});
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');
await api.requiresAndUses(['fixtures/middleware']).promise();
t.notDeepEqual(api.config.toConfig(), {});
});
test('command emits events around execution', async (t) => {
const api = new Neutrino();
const api = Neutrino();
const events = [];
api.on('prebuild', () => events.push('alpha'));
api.on('build', () => events.push('gamma'));
api.on('build', () => events.push('beta'));
await api.emitForAll('prebuild');
await api.emitForAll('build');
await api.runCommand('build', {}, () => events.push('beta'));
t.deepEqual(events, ['alpha', 'beta', 'gamma']);
t.deepEqual(events, ['alpha', 'beta']);
});
test('creates a Webpack config', t => {
const api = new Neutrino();
const api = Neutrino();
api.use(api => api.config.module
.rule('compile')
.test(/\.js$/));
t.notDeepEqual(api.getWebpackConfig(), {});
t.notDeepEqual(api.config.toConfig(), {});
});

45
packages/neutrino/test/requireMiddleware_test.js

@ -1,45 +0,0 @@
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'));
});

52
packages/neutrino/test/require_test.js

@ -0,0 +1,52 @@
import { join } from 'path';
import { outputFile as fsOutputFile, remove as fsRemove } from 'fs-extra';
import pify from 'pify';
import test from 'ava';
import { Neutrino } from '../src';
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', 'alpha');
const moduleMiddlewarePath = join(modulePath, 'index.js');
test.before(async () => {
await Promise.all([
outputFile(rootMiddlewarePath, 'module.exports = () => "root"'),
outputFile(errorMiddlewarePath, '[;'),
outputFile(moduleMiddlewarePath, 'module.exports = () => "alpha"')
]);
process.chdir(rootPath);
});
test.after.always(async () => {
await remove(rootPath);
process.chdir(cwd);
});
test('requires middleware relative to root', t => {
const api = Neutrino();
api.requiresAndUses(['middleware']).value(v => t.is(v, 'root'));
});
test('requires middleware from root/node_modules', t => {
const api = Neutrino();
api.requiresAndUses(['alpha']).value(v => t.is(v, 'alpha'));
});
test('forks with error middleware contains error', async (t) => {
const api = Neutrino();
await t.throws(api.requiresAndUses(['errorMiddleware']).promise());
});
test('throws if middleware cannot be found', async (t) => {
const api = Neutrino();
await t.throws(api.requiresAndUses(['nonExistent']).promise());
});

32
packages/neutrino/test/run_test.js

@ -0,0 +1,32 @@
import test from 'ava';
import commands from '../src';
test('run::build uses "production" NODE_ENV', t => {
commands.run('build');
t.is(process.env.NODE_ENV, 'production');
});
test('build uses "production" NODE_ENV', t => {
commands.build();
t.is(process.env.NODE_ENV, 'production');
});
test('start uses "development" NODE_ENV', t => {
commands.start();
t.is(process.env.NODE_ENV, 'development');
});
test('test uses "test" NODE_ENV', t => {
commands.test();
t.is(process.env.NODE_ENV, 'test');
});
test('inspect uses "development" NODE_ENV by default', t => {
commands.inspect();
t.is(process.env.NODE_ENV, 'development');
});
test('inspect uses overridden NODE_ENV', t => {
commands.inspect([], { args: { env: 'production' } });
t.is(process.env.NODE_ENV, 'production');
});

50
packages/neutrino/yarn.lock

@ -407,6 +407,13 @@ concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
concurrify@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/concurrify/-/concurrify-0.1.1.tgz#ae8ae2cd5bded30aee315b3bd5322ae4de38ee85"
dependencies:
sanctuary-type-classes "^3.0.0"
sanctuary-type-identifiers "^1.0.0"
connect-history-api-fallback@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169"
@ -738,6 +745,15 @@ find-up@^1.0.0:
path-exists "^2.0.0"
pinkie-promise "^2.0.0"
fluture@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/fluture/-/fluture-5.0.0.tgz#5abd07756a12edc54907b7a6d40e61e717cd9b0f"
dependencies:
concurrify "^0.1.0"
inspect-f "^1.2.0"
sanctuary-type-classes "^3.0.0"
sanctuary-type-identifiers "^1.0.0"
for-in@^0.1.5:
version "0.1.6"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8"
@ -975,6 +991,14 @@ ieee754@^1.1.4:
version "1.1.8"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
immutable-ext@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/immutable-ext/-/immutable-ext-1.0.8.tgz#be49852c009b9c06cbceaf166c873a1d109a4ddc"
immutable@^3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2"
indexof@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
@ -998,6 +1022,10 @@ ini@~1.3.0:
version "1.3.4"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
inspect-f@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/inspect-f/-/inspect-f-1.2.1.tgz#117573b5df57a509de6fe401131d426bac1592b5"
interpret@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
@ -1324,6 +1352,10 @@ minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
mitt@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.1.0.tgz#22f0d57e2fedd39620a62bb41b7cdd93667d3c41"
mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@~0.5.0, mkdirp@~0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
@ -1645,7 +1677,13 @@ querystringify@0.0.x:
version "0.0.4"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c"
ramda@^0.23.0:
ramda-fantasy@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/ramda-fantasy/-/ramda-fantasy-0.7.0.tgz#ac77a7a5d55dfc1ddc224ac418e9be8900c65d17"
dependencies:
ramda ">=0.15.0"
ramda@>=0.15.0, ramda@^0.23.0:
version "0.23.0"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.23.0.tgz#ccd13fff73497a93974e3e86327bfd87bd6e8e2b"
@ -1796,6 +1834,16 @@ ripemd160@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e"
sanctuary-type-classes@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/sanctuary-type-classes/-/sanctuary-type-classes-3.0.1.tgz#65163f8e1dfdfdc05cf842b83b608fff921cda15"
dependencies:
sanctuary-type-identifiers "1.0.x"
sanctuary-type-identifiers@1.0.x, sanctuary-type-identifiers@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/sanctuary-type-identifiers/-/sanctuary-type-identifiers-1.0.0.tgz#e8f359f006cb5e624cfb8464603fc114608bde9f"
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"

Loading…
Cancel
Save