diff --git a/CHANGES.md b/CHANGES.md
index b986fe81..59b6ce7f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,8 +1,24 @@
# Unreleased (in `master`)
+## Breaking Changes
+
+- Validation of the configuration object provided by `nwb.config.js` files has been expanded, so previously valid config files may now be invalid:
+ - Unexpected properties in top-level configuration or in `babel`, `karma`, `npm` and `webpack` configuration (i.e. anything that's not documented in the [Configuration docs](https://github.com/insin/nwb/blob/master/docs/Configuration.md)) are now treated as errors.
+ - Basic type checking is now performed for all documented configuration properties.
+ - After upgrading, run `nwb check-config` to check your configuration file.
+
+## `nwb.config.js` Config Changes
+
+- Deprecated the `webpack.compat.sinon` flag for Sinon 1.x compatibility settings, as subsequent major versions since July 2017 support Webpack out of the box.
+
## Added
-- You can now provide a [`karma.config()` function](https://github.com/insin/nwb/blob/master/docs/Configuration.md#config-function-1) which will be given the generated Karma config to do whatever it wants with [[#408](https://github.com/insin/nwb/issues/408)]
+- You can now provide a [`babel.config()` function](https://github.com/insin/nwb/blob/master/docs/Configuration.md#config-function) which will be given the generated Babel config to do whatever it wants with.
+- You can now provide a [`karma.config()` function](https://github.com/insin/nwb/blob/master/docs/Configuration.md#config-function-2) which will be given the generated Karma config to do whatever it wants with [[#408](https://github.com/insin/nwb/issues/408)]
+
+## Changed
+
+- Simplified configuration of locales in [`webpack.compat` config](https://github.com/insin/nwb/blob/master/docs/Configuration.md#compat-object)
## Dependencies
@@ -17,7 +33,8 @@
## Docs
-- Added more headings to the [Commands docs]() to make them easier to browse, and to make feature flags such as `--copy-files` for component builds more visible [[#407](https://github.com/insin/nwb/issues/407)]
+- Added missing docs for [`webpack.copy` config](https://github.com/insin/nwb/blob/master/docs/Configuration.md#copy-array--object)
+- Added more headings to the [Commands docs](https://github.com/insin/nwb/blob/master/docs/Commands.md#commands) to make them easier to browse, and to make feature flags such as `--copy-files` for component builds more visible [[#407](https://github.com/insin/nwb/issues/407)]
# 0.20.0 / 2017-11-18
diff --git a/docs/Configuration.md b/docs/Configuration.md
index d9621648..3deb3842 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -38,11 +38,11 @@ If a function is exported, it will be passed an object with the following proper
- `args`: a parsed version of arguments passed to the `nwb` command
- `command`: the name of the command currently being executed, e.g. `'build'` or `'test'`
-- `webpack`: nwb's version of the `webpack` module, giving you access to the other plugins webpack provides.
+- `webpack`: nwb's version of the `webpack` module, giving you access to plugins bundled with Webpack.
#### Example Configuration Files
-- [react-hn's `nwb.config.js`](https://github.com/insin/react-hn/blob/concat/nwb.config.js) is a simple configuration file with minor tweaks to Babel and Webpack config.
+- [bootstrap-4-nwb's `nwb.config.js`](https://github.com/insin/bootstrap-4-nwb/blob/master/nwb.config.js) is a simple configuration file with minor tweaks to Babel and Webpack config.
- [React Yelp Clone's `nwb.config.js`](https://github.com/insin/react-yelp-clone/blob/nwb/nwb.config.js) configures Babel, Karma and Webpack to allow nwb to be dropped into an existing app to handle its development tooling, [reducing the amount of `devDependencies` and configuration](https://github.com/insin/react-yelp-clone/compare/master...nwb) which need to be managed.
### Configuration Via Arguments
@@ -80,26 +80,28 @@ The configuration object can include the following properties:
- [`babel.reactConstantElements`](#reactconstantelements-false) - disable use of React constant element hoisting in production builds
- [`babel.runtime`](#runtime-string--boolean) - enable the `transform-runtime` plugin with different configurations
- [`babel.stage`](#stage-number--false) - control which experimental and upcoming JavaScript features can be used
+ - [`babel.config`](#config-function) - an escape hatch for manually editing the generated Babel config
- [Webpack Configuration](#webpack-configuration)
- [`webpack`](#webpack-object)
- [`webpack.aliases`](#aliases-object) - rewrite certain import paths
- [`webpack.autoprefixer`](#autoprefixer-string--object) - options for Autoprefixer
- [`webpack.compat`](#compat-object) - enable Webpack compatibility tweaks for commonly-used modules
+ - [`webpack.copy`](#copy-array--object) - patterns and options for `CopyWebpackPlugin`
- [`webpack.debug`](#debug-boolean) - create a more debuggable production build
- [`webpack.define`](#define-object) - options for `DefinePlugin`, for replacing certain expressions with values
- [`webpack.extractText`](#extracttext-object--boolean) - options for `ExtractTextPlugin`
- [`webpack.hoisting`](#hoisting-boolean) - enable partial scope hoisting with Webpack 3's `ModuleConcatenationPlugin`
- [`webpack.html`](#html-object) - options for `HtmlPlugin`
- [`webpack.install`](#install-object) - options for `NpmInstallPlugin`
+ - [`webpack.publicPath`](#publicpath-string) - path to static resources
- [`webpack.rules`](#rules-object) - tweak the configuration of generated Webpack rules and their loaders
- [Default Rules](#default-rules)
- [Customising loaders](#customising-loaders)
- [Disabling default rules](#disabling-default-rules)
- - [`webpack.publicPath`](#publicpath-string) - path to static resources
- [`webpack.styles`](#styles-object--false--old) - customise creation of Webpack rules for stylesheets
- [`webpack.uglify`](#uglify-object--false) - configure use of Webpack's `UglifyJsPlugin`
- [`webpack.extra`](#extra-object) - an escape hatch for extra Webpack config, which will be merged into the generated config
- - [`webpack.config`](#config-function) - an escape hatch for manually editing the generated Webpack config
+ - [`webpack.config`](#config-function-1) - an escape hatch for manually editing the generated Webpack config
- [Dev Server Configuration](#dev-server-configuration)
- [`devServer`](#devserver-object) - configure Webpack Dev Server
- [Karma Configuration](#karma-configuration)
@@ -112,13 +114,13 @@ The configuration object can include the following properties:
- [`karma.testContext`](#testcontext-string) - point to a Webpack context module for your tests
- [`karma.testFiles`](#testfiles-string--arraystring) - patterns for test files
- [`karma.extra`](#extra-object-1) - an escape hatch for extra Karma config, which will be merged into the generated config
- - [`karma.config`](#config-function-1) - an escape hatch for manually editing the generated Karma config
+ - [`karma.config`](#config-function-2) - an escape hatch for manually editing the generated Karma config
- [npm Build Configuration](#npm-build-configuration)
- [`npm`](#npm-object)
- [`npm.cjs`](#esmodules-boolean) - toggle creation of a CommonJS build
- [`npm.esModules`](#esmodules-boolean) - toggle creation of an ES modules build
- UMD build
- - [`npm.umd`](#umd-string--object) - enable a UMD build which exports a global variable
+ - [`npm.umd`](#umd-string--object--false) - configure a UMD build which exports a global variable
- [`umd.global`](#global-string) - global variable name exported by UMD build
- [`umd.externals`](#externals-object) - dependencies to use via global variables in UMD build
- [`package.json` fields](#packagejson-umd-banner-configuration)
@@ -328,6 +330,25 @@ module.exports = {
}
```
+##### `config`: `Function`
+
+Finally, if you need *complete* control, provide a `babel.config()` function which will be given the generated config.
+
+> **Note:** you *must* return a config object from this function.
+
+```js
+module.exports = {
+ webpack: {
+ config(config) {
+ // Change config as you wish
+
+ // You MUST return the edited config object
+ return config
+ }
+ }
+}
+```
+
### Webpack Configuration
#### `webpack`: `Object`
@@ -395,28 +416,24 @@ The following libraries are supported:
###### `enzyme`: `Boolean`
-Set to `true` for [Enzyme](http://airbnb.io/enzyme/) v2 compatibility - this also assumes you're using React v15.5 or v15.6.
-
-###### `intl`: `Object`
-
-If you use [intl](https://www.npmjs.com/package/intl) in a Webpack build, all the locales it supports will be imported by default and your build will be larger than you were expecting!
+*deprecated in v0.19.1+*
-Provide an object with a `locales` Array specifying language codes for the locales you want to load, or a String if you only need one locale.
-
-###### `moment`: `Object`
+Set to `true` for [Enzyme](http://airbnb.io/enzyme/) v2 compatibility - this also assumes you're using React v15.5 or v15.6.
-If you use [Moment.js](http://momentjs.com/) in a Webpack build, all the locales it supports will be imported by default and your build will be larger than you were expecting!
+###### `intl` / `moment` / `react-intl`: `String | Array | Object`
-Provide an object with a `locales` Array specifying language codes for the locales you want to load, or a String if you only need one locale.
+*changed in v0.21.0*
-###### `react-intl`: `Object`
+If you use [intl](https://www.npmjs.com/package/intl), [Moment.js](http://momentjs.com/) or [react-intl](https://github.com/yahoo/react-intl) in a Webpack build, all the locales they support will be imported by default and your build will be larger than you were expecting!
-If you use [react-intl](https://github.com/yahoo/react-intl) in a Webpack build, all the locales it supports will be imported by default and your build will be larger than you were expecting!
+Provide an Array specifying language codes for the locales you want to load, or a String if you only need one locale.
-Provide an object with a `locales` Array specifying language codes for the locales you want to load, or a String if you only need one locale.
+> For backwards-compatibility with older versions of nwb, locales can also be provided as an Object with a `locales` property.
###### `sinon`: `Boolean`
+*deprecated in v0.21.0+*
+
Set to `true` for [Sinon.js](http://sinonjs.org/) 1.x compatibility.
---
@@ -427,16 +444,49 @@ Example config showing the use of multiple `compat` settings:
module.exports = {
webpack: {
compat: {
- enzyme: true,
- moment: {
- locales: ['de', 'en-gb', 'es', 'fr', 'it']
- },
+ moment: ['de', 'en-gb', 'es', 'fr', 'it'],
sinon: true
}
}
}
```
+##### `copy`: `Array | Object`
+
+Configures [`CopyWebpackPlugin` patterns and options](https://github.com/webpack-contrib/extract-text-webpack-plugin#options).
+
+By default, nwb uses [`CopyWebpackPlugin`](https://github.com/webpack-contrib/extract-text-webpack-plugin#options) to copy any contents in an app's `public/` directory to the output directory.
+
+To add extra [copy patterns](https://github.com/webpack-contrib/copy-webpack-plugin#pattern-properties), you can provide an Array of them:
+
+```js
+module.exports = {
+ webpack: {
+ copy: [
+ // Copy directory contents to output
+ {from: 'path/to/mystuff'}
+ ]
+ }
+}
+```
+
+To configure [plugin options](https://github.com/webpack-contrib/copy-webpack-plugin#available-options), provide an object with `options` or `patterns` properties:
+
+```js
+module.exports = {
+ webpack: {
+ copy: {
+ options: {
+ debug: true
+ },
+ patterns: [
+ {from: 'path/to/mystuff'}
+ ]
+ }
+ }
+}
+```
+
##### `debug`: `boolean`
Enables a more debuggable production build:
@@ -563,6 +613,42 @@ module.exports = {
Configures [options for `NpmInstallPlugin`](https://github.com/webpack-contrib/npm-install-webpack-plugin#usage), which will be used if you pass an `--install` flag to nwb commands which run a development server.
+##### `publicPath`: `String`
+
+> This is just Webpack's [`output.publicPath` config](https://webpack.js.org/configuration/output/#output-publicpath) pulled up a level to make it more convenient to configure.
+
+`publicPath` defines the URL static resources will be referenced by in build output, such as `` and `` tags in generated HTML, `url()` in stylesheets and paths to any static resources you `require()` into your modules.
+
+The default `publicPath` configured for most app builds is `/`, which assumes you will be serving your app's static resources from the root of whatever URL it's hosted at:
+
+```html
+
+```
+
+If you're serving static resources from a different path, or from an external URL such as a CDN, set it as the `publicPath`:
+
+```js
+module.exports = {
+ webpack: {
+ publicPath: 'https://cdn.example.com/myapp/'
+ }
+}
+```
+
+The exception is the React component demo app, which doesn't set a `publicPath`, generating a build without any root URL paths to static resources. This allows you to serve it at any path without configuration (e.g. on GitHub Project Pages), or open the generated `index.html` file directly in a browser, which is ideal for distributing app builds which don't require a server to run.
+
+If you want to create a path-independent build, set `publicPath` to blank or `null`:
+
+```js
+module.exports = {
+ webpack: {
+ publicPath: ''
+ }
+}
+```
+
+The trade-off for path-independence is HTML5 History routing won't work, as serving up `index.html` at anything but its real path will mean its static resource URLs won't resolve. You will have to fall back on hash-based routing if you need it.
+
##### `rules`: `Object`
Each [Webpack rule](https://webpack.js.org/configuration/module/#module-rules) generated by nwb has a unique id you can use to customise it:
@@ -663,42 +749,6 @@ module.exports = {
}
```
-##### `publicPath`: `String`
-
-> This is just Webpack's [`output.publicPath` config](https://webpack.js.org/configuration/output/#output-publicpath) pulled up a level to make it more convenient to configure.
-
-`publicPath` defines the URL static resources will be referenced by in build output, such as `` and `` tags in generated HTML, `url()` in stylesheets and paths to any static resources you `require()` into your modules.
-
-The default `publicPath` configured for most app builds is `/`, which assumes you will be serving your app's static resources from the root of whatever URL it's hosted at:
-
-```html
-
-```
-
-If you're serving static resources from a different path, or from an external URL such as a CDN, set it as the `publicPath`:
-
-```js
-module.exports = {
- webpack: {
- publicPath: 'https://cdn.example.com/myapp/'
- }
-}
-```
-
-The exception is the React component demo app, which doesn't set a `publicPath`, generating a build without any root URL paths to static resources. This allows you to serve it at any path without configuration (e.g. on GitHub Project Pages), or open the generated `index.html` file directly in a browser, which is ideal for distributing app builds which don't require a server to run.
-
-If you want to create a path-independent build, set `publicPath` to blank or `null`:
-
-```js
-module.exports = {
- webpack: {
- publicPath: ''
- }
-}
-```
-
-The trade-off for path-independence is HTML5 History routing won't work, as serving up `index.html` at anything but its real path will mean its static resource URLs won't resolve. You will have to fall back on hash-based routing if you need it.
-
##### `styles`: `Object | false | 'old'`
Configures how nwb creates Webpack config for importing stylesheets.
@@ -957,24 +1007,6 @@ module.exports = {
> **Note:** If you're configuring frameworks and you want to use the Mocha framework plugin managed by nwb, just pass its name as in the above example.
-##### `testContext`: `String`
-
-Use this configuration to point to a [Webpack context module](/docs/Testing.md#using-a-test-context-module) for your tests if you need to run code prior to any tests being run, such as customising the assertion library you're using, or global before and after hooks.
-
-If you provide a context module, it is responsible for including tests via Webpack's `require.context()` - see the [example in the Testing docs](/docs/Testing.md#using-a-test-context-module).
-
-If the default [`testFiles`](#testfiles-string--arraystring) config wouldn't have picked up your tests, you must also configure it so they can be excluded from code coverage.
-
-##### `testFiles`: `String | Array`
-
-> Default: `.spec.js`, `.test.js` or `-test.js` files anywhere under `src/`, `test/` or `tests/`
-
-[Minimatch glob patterns](https://github.com/isaacs/minimatch) for test files.
-
-If [`karma.testContext`](#testcontext-string) is not being used, this controls which files Karma will run tests from.
-
-This can also be used to exclude tests from code coverage if you're using [`karma.testContext`](#testcontext-string) - if the default `testFiles` patterns wouldn't have picked up your tests, configure this as well to exclude then from code coverage.
-
##### `plugins`: `Array`
A list of plugins to be loaded by Karma - this should be used in combination with [`browsers`](#browsers-arraystring--plugin), [`frameworks`](#frameworks-arraystring--plugin) and [`reporters`](#reporters-arraystring--plugin) config as necessary.
@@ -1010,6 +1042,24 @@ module.exports = {
}
```
+##### `testContext`: `String`
+
+Use this configuration to point to a [Webpack context module](/docs/Testing.md#using-a-test-context-module) for your tests if you need to run code prior to any tests being run, such as customising the assertion library you're using, or global before and after hooks.
+
+If you provide a context module, it is responsible for including tests via Webpack's `require.context()` - see the [example in the Testing docs](/docs/Testing.md#using-a-test-context-module).
+
+If the default [`testFiles`](#testfiles-string--arraystring) config wouldn't have picked up your tests, you must also configure it so they can be excluded from code coverage.
+
+##### `testFiles`: `String | Array`
+
+> Default: `.spec.js`, `.test.js` or `-test.js` files anywhere under `src/`, `test/` or `tests/`
+
+[Minimatch glob patterns](https://github.com/isaacs/minimatch) for test files.
+
+If [`karma.testContext`](#testcontext-string) is not being used, this controls which files Karma will run tests from.
+
+This can also be used to exclude tests from code coverage if you're using [`karma.testContext`](#testcontext-string) - if the default `testFiles` patterns wouldn't have picked up your tests, configure this as well to exclude then from code coverage.
+
##### `extra`: `Object`
Extra configuration to be merged into the generated Karma configuration using [webpack-merge](https://github.com/survivejs/webpack-merge#webpack-merge---merge-designed-for-webpack).
@@ -1088,9 +1138,9 @@ When providing an ES6 modules build, you should also provide the following in `p
These are included automatically if you create a project with an ES6 modules build enabled.
-##### `umd`: `String | Object`
+##### `umd`: `String | Object | false`
-Configures creation of a UMD build when you run `nwb build` for a React component/library or web module.
+Configures a UMD build when you run `nwb build` for a React component/library or web module.
If you just need to configure the global variable the UMD build will export, you can use a String:
@@ -1128,6 +1178,8 @@ module.exports = {
}
```
+The UMD build can also be explicitly disabled by configuring `umd = false`.
+
#### `package.json` UMD Banner Configuration
A banner comment added to UMD builds will use as many of the following `package.json` fields as are present:
diff --git a/docs/guides/QuickDevelopment.md b/docs/guides/QuickDevelopment.md
index 85ade6e6..54cf6bfa 100644
--- a/docs/guides/QuickDevelopment.md
+++ b/docs/guides/QuickDevelopment.md
@@ -183,7 +183,7 @@ Create a build of a React project which uses Inferno or Preact as the runtime vi
- [`do` expressions](http://babeljs.io/docs/plugins/transform-do-expressions/#detail)
- [`::` function binding operator](http://babeljs.io/docs/plugins/transform-function-bind/#detail)
-- Polyfills for `Promise`, `fetch()` and `Object.assign()`, which can be disabled with a `--no-polyfill` flag if you don’t need them or want to provide your own.
+- Polyfills for `Promise`, `fetch()` and `Object.assign()`, which can be disabled with a `--no-polyfill` flag if you don’t need them or want to provide your own.
- Import images and stylesheets into JavaScript like any other module, to be handled by Webpack as part of its build.
```js
diff --git a/src/commands/build.js b/src/commands/build.js
index f9bd3aca..018a2dd3 100644
--- a/src/commands/build.js
+++ b/src/commands/build.js
@@ -1,5 +1,5 @@
+import {getProjectType} from '../config'
import {INFERNO_APP, PREACT_APP, REACT_APP, REACT_COMPONENT, WEB_APP, WEB_MODULE} from '../constants'
-import {getProjectType} from '../getUserConfig'
import buildInfernoApp from './build-inferno-app'
import buildPreactApp from './build-preact-app'
import buildReactApp from './build-react-app'
diff --git a/src/commands/check-config.js b/src/commands/check-config.js
index b6976001..ed71f43c 100644
--- a/src/commands/check-config.js
+++ b/src/commands/check-config.js
@@ -1,5 +1,4 @@
-import getUserConfig, {UserConfigReport} from '../getUserConfig'
-import getPluginConfig from '../getPluginConfig'
+import {getPluginConfig, getUserConfig, UserConfigReport} from '../config'
function getFullEnv(env) {
if (env === 'dev') return 'development'
diff --git a/src/commands/clean.js b/src/commands/clean.js
index 70b09be9..33d5af9c 100644
--- a/src/commands/clean.js
+++ b/src/commands/clean.js
@@ -1,5 +1,5 @@
+import {getProjectType} from '../config'
import {INFERNO_APP, PREACT_APP, REACT_APP, REACT_COMPONENT, WEB_APP, WEB_MODULE} from '../constants'
-import {getProjectType} from '../getUserConfig'
import cleanApp from './clean-app'
import cleanModule from './clean-module'
diff --git a/src/commands/serve.js b/src/commands/serve.js
index eba42853..b35ca0f0 100644
--- a/src/commands/serve.js
+++ b/src/commands/serve.js
@@ -1,7 +1,7 @@
// @flow
+import {getProjectType} from '../config'
import {INFERNO_APP, PREACT_APP, REACT_APP, REACT_COMPONENT, WEB_APP} from '../constants'
import {UserError} from '../errors'
-import {getProjectType} from '../getUserConfig'
import serveInfernoApp from './serve-inferno-app'
import servePreactApp from './serve-preact-app'
import serveReactApp from './serve-react-app'
diff --git a/src/commands/test.js b/src/commands/test.js
index 5007bfa5..37f16188 100644
--- a/src/commands/test.js
+++ b/src/commands/test.js
@@ -1,6 +1,6 @@
// @flow
+import {getProjectType} from '../config'
import {INFERNO_APP, PREACT_APP, REACT_APP, REACT_COMPONENT} from '../constants'
-import {getProjectType} from '../getUserConfig'
import karmaServer from '../karmaServer'
import testInferno from './test-inferno'
import testPreact from './test-preact'
diff --git a/src/config/UserConfigReport.js b/src/config/UserConfigReport.js
new file mode 100644
index 00000000..3cfd39b0
--- /dev/null
+++ b/src/config/UserConfigReport.js
@@ -0,0 +1,106 @@
+import util from 'util'
+
+import chalk from 'chalk'
+import figures from 'figures'
+
+import {pluralise as s} from '../utils'
+
+export default class UserConfigReport {
+ constructor({configFileExists, configPath} = {}) {
+ this.configFileExists = configFileExists
+ this.configPath = configPath
+ this.deprecations = []
+ this.errors = []
+ this.hints = []
+ this.hasArgumentOverrides = false
+ }
+
+ deprecated(path, ...messages) {
+ this.deprecations.push({path, messages})
+ }
+
+ error(path, value, message) {
+ this.errors.push({path, value, message})
+ }
+
+ hasErrors() {
+ return this.errors.length > 0
+ }
+
+ hasSomethingToReport() {
+ return this.errors.length + this.deprecations.length + this.hints.length > 0
+ }
+
+ hint(path, ...messages) {
+ this.hints.push({path, messages})
+ }
+
+ getConfigSource() {
+ if (this.configFileExists) {
+ let description = this.configPath
+ if (this.hasArgumentOverrides) {
+ description += ' (with CLI argument overrides)'
+ }
+ return description
+ }
+ else if (this.hasArgumentOverrides) {
+ return 'config via CLI arguments'
+ }
+ return 'funsies'
+ }
+
+ getReport() {
+ let report = []
+
+ report.push(chalk.underline(`nwb config report for ${this.getConfigSource()}`))
+ report.push('')
+
+ if (!this.hasSomethingToReport()) {
+ report.push(chalk.green(`${figures.tick} Nothing to report!`))
+ return report.join('\n')
+ }
+
+ if (this.errors.length) {
+ let count = this.errors.length > 1 ? `${this.errors.length} ` : ''
+ report.push(chalk.red.underline(`${count}Error${s(this.errors.length)}`))
+ report.push('')
+ }
+ this.errors.forEach(({path, value, message}) => {
+ report.push(`${chalk.red(`${figures.cross} ${path}`)} ${chalk.cyan('=')} ${util.inspect(value)}`)
+ report.push(` ${message}`)
+ report.push('')
+ })
+
+ if (this.deprecations.length) {
+ let count = this.deprecations.length > 1 ? `${this.deprecations.length} ` : ''
+ report.push(chalk.yellow.underline(`${count}Deprecation Warning${s(this.deprecations.length)}`))
+ report.push('')
+ }
+ this.deprecations.forEach(({path, messages}) => {
+ report.push(chalk.yellow(`${figures.warning} ${path}`))
+ messages.forEach(message => {
+ report.push(` ${message}`)
+ })
+ report.push('')
+ })
+
+ if (this.hints.length) {
+ let count = this.hints.length > 1 ? `${this.hints.length} ` : ''
+ report.push(chalk.cyan.underline(`${count}Hint${s(this.hints.length)}`))
+ report.push('')
+ }
+ this.hints.forEach(({path, messages}) => {
+ report.push(chalk.cyan(`${figures.info} ${path}`))
+ messages.forEach(message => {
+ report.push(` ${message}`)
+ })
+ report.push('')
+ })
+
+ return report.join('\n')
+ }
+
+ log() {
+ console.log(this.getReport())
+ }
+}
diff --git a/src/config/babel.js b/src/config/babel.js
new file mode 100644
index 00000000..8ce99c47
--- /dev/null
+++ b/src/config/babel.js
@@ -0,0 +1,156 @@
+import chalk from 'chalk'
+
+import {pluralise as s, typeOf} from '../utils'
+
+const BABEL_RUNTIME_OPTIONS = new Set(['helpers', 'polyfill'])
+
+export function processBabelConfig({report, userConfig}) {
+ let {
+ cherryPick,
+ env,
+ loose,
+ plugins,
+ presets,
+ removePropTypes,
+ reactConstantElements,
+ runtime,
+ stage,
+ config,
+ ...unexpectedConfig
+ } = userConfig.babel
+
+ let unexpectedProps = Object.keys(unexpectedConfig)
+ if (unexpectedProps.length > 0) {
+ report.error(
+ 'babel',
+ unexpectedProps.join(', '),
+ `Unexpected prop${s(unexpectedProps.length)} in ${chalk.cyan('babel')} config - ` +
+ 'see https://github.com/insin/nwb/blob/master/docs/Configuration.md#babel-configuration for supported config'
+ )
+ }
+
+ // cherryPick
+ if ('cherryPick' in userConfig.babel) {
+ if (typeOf(cherryPick) !== 'string' && typeOf(cherryPick) !== 'array') {
+ report.error(
+ 'babel.cherryPick',
+ cherryPick,
+ `Must be a ${chalk.cyan('String')} or an ${chalk.cyan('Array')}`
+ )
+ }
+ }
+
+ // env
+ if ('env' in userConfig.babel) {
+ if (typeOf(env) !== 'Object') {
+ report.error(
+ 'babel.env',
+ env,
+ `Must be an ${chalk.cyan('Object')}`
+ )
+ }
+ }
+
+ // loose
+ if ('loose' in userConfig.babel) {
+ if (typeOf(loose) !== 'boolean') {
+ report.error(
+ 'babel.loose',
+ loose,
+ `Must be ${chalk.cyan('Boolean')}`
+ )
+ }
+ }
+
+ // plugins
+ if ('plugins' in userConfig.babel) {
+ if (typeOf(plugins) === 'string') {
+ userConfig.babel.plugins = [plugins]
+ }
+ else if (typeOf(userConfig.babel.plugins) !== 'array') {
+ report.error(
+ 'babel.plugins',
+ plugins,
+ `Must be a ${chalk.cyan('String')} or an ${chalk.cyan('Array')}`
+ )
+ }
+ }
+
+ // presets
+ if ('presets' in userConfig.babel) {
+ if (typeOf(presets) === 'string') {
+ userConfig.babel.presets = [presets]
+ }
+ else if (typeOf(presets) !== 'array') {
+ report.error(
+ 'babel.presets',
+ presets,
+ `Must be a ${chalk.cyan('String')} or an ${chalk.cyan('Array')}`
+ )
+ }
+ }
+
+ // removePropTypes
+ if ('removePropTypes' in userConfig.babel) {
+ if (removePropTypes !== false && typeOf(removePropTypes) !== 'object') {
+ report.error(
+ `babel.removePropTypes`,
+ removePropTypes,
+ `Must be ${chalk.cyan('false')} (to disable removal of PropTypes) ` +
+ `or an ${chalk.cyan('Object')} (to configure react-remove-prop-types)`
+ )
+ }
+ }
+
+ // reactConstantElements
+ if ('reactConstantElements' in userConfig.babel) {
+ if (typeOf(reactConstantElements) !== 'boolean') {
+ report.error(
+ 'babel.reactConstantElements',
+ reactConstantElements,
+ `Must be ${chalk.cyan('Boolean')}`
+ )
+ }
+ }
+
+ // runtime
+ if ('runtime' in userConfig.babel &&
+ typeOf(runtime) !== 'boolean' &&
+ !BABEL_RUNTIME_OPTIONS.has(runtime)) {
+ report.error(
+ 'babel.runtime',
+ runtime,
+ `Must be ${chalk.cyan('Boolean')}, ${chalk.cyan("'helpers'")} or ${chalk.cyan("'polyfill'")}`
+ )
+ }
+
+ // stage
+ if ('stage' in userConfig.babel) {
+ if (typeOf(stage) === 'number') {
+ if (stage < 0 || stage > 3) {
+ report.error(
+ 'babel.stage',
+ stage,
+ `Must be between ${chalk.cyan(0)} and ${chalk.cyan(3)}`
+ )
+ }
+ }
+ else if (stage !== false) {
+ report.error(
+ 'babel.stage',
+ stage,
+ `Must be a ${chalk.cyan('Number')} between ${chalk.cyan('0')} and ${chalk.cyan('3')} (to choose a stage preset), ` +
+ `or ${chalk.cyan('false')} (to disable use of a stage preset)`
+ )
+ }
+ }
+
+ // config
+ if ('config' in userConfig.babel && typeOf(config) !== 'function') {
+ report.error(
+ `babel.config`,
+ `type: ${typeOf(config)}`,
+ `Must be a ${chalk.cyan('Function')}`
+ )
+ }
+}
diff --git a/src/config/index.js b/src/config/index.js
new file mode 100644
index 00000000..764cdaad
--- /dev/null
+++ b/src/config/index.js
@@ -0,0 +1,3 @@
+export {getPluginConfig} from './plugin'
+export {getProjectType, getUserConfig} from './user'
+export {default as UserConfigReport} from './UserConfigReport'
diff --git a/src/config/karma.js b/src/config/karma.js
new file mode 100644
index 00000000..db7f1ab4
--- /dev/null
+++ b/src/config/karma.js
@@ -0,0 +1,140 @@
+import fs from 'fs'
+
+import chalk from 'chalk'
+
+import {pluralise as s, typeOf} from '../utils'
+
+export function processKarmaConfig({report, userConfig}) {
+ let {
+ browsers,
+ excludeFromCoverage,
+ frameworks,
+ plugins,
+ reporters,
+ testContext,
+ testFiles,
+ extra,
+ config,
+ ...unexpectedConfig
+ } = userConfig.karma
+
+ let unexpectedProps = Object.keys(unexpectedConfig)
+ if (unexpectedProps.length > 0) {
+ report.error(
+ 'karma',
+ unexpectedProps.join(', '),
+ `Unexpected prop${s(unexpectedProps.length)} in ${chalk.cyan('karma')} config - ` +
+ 'see https://github.com/insin/nwb/blob/master/docs/Configuration.md#karma-configuration for supported config'
+ )
+ }
+
+ // browsers
+ if ('browsers' in userConfig.karma) {
+ if (typeOf(browsers) !== 'array') {
+ report.error(
+ 'karma.browsers',
+ browsers,
+ `Must be an ${chalk.cyan('Array')}`
+ )
+ }
+ }
+
+ // excludeFromCoverage
+ if ('excludeFromCoverage' in userConfig.karma) {
+ if (typeOf(excludeFromCoverage) === 'string') {
+ userConfig.karma.excludeFromCoverage = [excludeFromCoverage]
+ }
+ else if (typeOf(excludeFromCoverage) !== 'array') {
+ report.error(
+ 'karma.excludeFromCoverage',
+ excludeFromCoverage,
+ `Must be a ${chalk.cyan('String')} or an ${chalk.cyan('Array')}`
+ )
+ }
+ }
+
+ // frameworks
+ if ('frameworks' in userConfig.karma) {
+ if (typeOf(frameworks) !== 'array') {
+ report.error(
+ 'karma.frameworks',
+ frameworks,
+ `Must be an ${chalk.cyan('Array')}`
+ )
+ }
+ }
+
+ // plugins
+ if ('plugins' in userConfig.karma) {
+ if (typeOf(plugins) !== 'array') {
+ report.error(
+ 'karma.plugins',
+ plugins,
+ `Must be an ${chalk.cyan('Array')}`
+ )
+ }
+ }
+
+ // reporters
+ if ('reporters' in userConfig.karma) {
+ if (typeOf(reporters) !== 'array') {
+ report.error(
+ 'karma.reporters',
+ reporters,
+ `Must be an ${chalk.cyan('Array')}`
+ )
+ }
+ }
+
+ // testContext
+ if ('testContext' in userConfig.karma) {
+ if (typeOf(testContext) !== 'string') {
+ report.error(
+ 'karma.testContext',
+ testContext,
+ `Must be a ${chalk.cyan('String')}`
+ )
+ }
+ else if (!fs.existsSync(testContext)) {
+ report.error(
+ 'karma.testContext',
+ testContext,
+ `The specified test context module does not exist`
+ )
+ }
+ }
+
+ // testFiles
+ if ('testFiles' in userConfig.karma) {
+ if (typeOf(testFiles) === 'string') {
+ userConfig.karma.testFiles = [testFiles]
+ }
+ else if (typeOf(testFiles) !== 'array') {
+ report.error(
+ 'karma.testFiles',
+ testFiles,
+ `Must be a ${chalk.cyan('String')} or an ${chalk.cyan('Array')}`
+ )
+ }
+ }
+
+ // extra
+ if ('extra' in userConfig.karma) {
+ if (typeOf(extra) !== 'object') {
+ report.error(
+ 'karma.extra',
+ `type: ${typeOf(extra)}`,
+ `Must be an ${chalk.cyan('Object')}`
+ )
+ }
+ }
+
+ // config
+ if ('config' in userConfig.karma && typeOf(config) !== 'function') {
+ report.error(
+ `karma.config`,
+ `type: ${typeOf(config)}`,
+ `Must be a ${chalk.cyan('Function')}`
+ )
+ }
+}
diff --git a/src/config/npm.js b/src/config/npm.js
new file mode 100644
index 00000000..5477beae
--- /dev/null
+++ b/src/config/npm.js
@@ -0,0 +1,96 @@
+import chalk from 'chalk'
+
+import {pluralise as s, typeOf} from '../utils'
+
+export function processNpmBuildConfig({report, userConfig}) {
+ let {
+ cjs,
+ esModules,
+ umd,
+ ...unexpectedConfig
+ } = userConfig.npm
+
+ let unexpectedProps = Object.keys(unexpectedConfig)
+ if (unexpectedProps.length > 0) {
+ report.error(
+ 'npm',
+ unexpectedProps.join(', '),
+ `Unexpected prop${s(unexpectedProps.length)} in ${chalk.cyan('babel')} config - ` +
+ 'see https://github.com/insin/nwb/blob/master/docs/Configuration.md#npm-build-configuration for supported config'
+ )
+ }
+
+ // cjs
+ if ('cjs' in userConfig.npm) {
+ if (typeOf(cjs) !== 'boolean') {
+ report.error(
+ 'npm.cjs',
+ cjs,
+ `Must be ${chalk.cyan('Boolean')}`
+ )
+ }
+ }
+
+ // esModules
+ if ('esModules' in userConfig.npm) {
+ if (typeOf(esModules) !== 'boolean') {
+ report.error(
+ 'npm.esModules',
+ esModules,
+ `Must be ${chalk.cyan('Boolean')}`
+ )
+ }
+ }
+
+ // umd
+ if ('umd' in userConfig.npm) {
+ if (umd === false) {
+ // ok
+ }
+ else if (typeOf(umd) === 'string') {
+ userConfig.npm.umd = {global: umd}
+ }
+ else if (typeOf(umd) !== 'object') {
+ report.error(
+ 'npm.umd',
+ umd,
+ `Must be a ${chalk.cyan('String')} (for ${chalk.cyan('global')} ` +
+ `config only)}, an ${chalk.cyan('Object')} (for any UMD build options) ` +
+ `or ${chalk.cyan('false')} (to explicitly disable the UMD build)`
+ )
+ }
+ else {
+ let {
+ global: umdGlobal,
+ externals,
+ ...unexpectedConfig
+ } = umd
+
+ let unexpectedProps = Object.keys(unexpectedConfig)
+ if (unexpectedProps.length > 0) {
+ report.error(
+ 'npm.umd',
+ unexpectedProps.join(', '),
+ `Unexpected prop${s(unexpectedProps.length)} in ${chalk.cyan('npm.umd')} config - ` +
+ 'see https://github.com/insin/nwb/blob/master/docs/Configuration.md#umd-string--object for supported config'
+ )
+ }
+
+ if ('global' in umd && typeOf(umdGlobal) !== 'string') {
+ report.error(
+ 'npm.umd.global',
+ umdGlobal,
+ `Must be a ${chalk.cyan('String')}`
+ )
+ }
+
+ if ('externals' in umd && typeOf(externals) !== 'object') {
+ report.error(
+ 'npm.umd.externals',
+ externals,
+ `Must be an ${chalk.cyan('Object')}`
+ )
+ }
+ }
+ }
+}
diff --git a/src/getPluginConfig.js b/src/config/plugin.js
similarity index 88%
rename from src/getPluginConfig.js
rename to src/config/plugin.js
index f76a9fd8..a40f21c0 100644
--- a/src/getPluginConfig.js
+++ b/src/config/plugin.js
@@ -3,8 +3,8 @@ import path from 'path'
import resolve from 'resolve'
import merge from 'webpack-merge'
-import debug from './debug'
-import {deepToString, getArgsPlugins, unique} from './utils'
+import debug from '../debug'
+import {deepToString, getArgsPlugins, unique} from '../utils'
function getPackagePlugins(cwd) {
let pkg = require(path.join(cwd, 'package.json'))
@@ -19,7 +19,7 @@ function getPackagePlugins(cwd) {
* arguments when supported, import them and merge the plugin config objects
* they export.
*/
-export default function getPluginConfig(args = {}, {cwd = process.cwd()} = {}) {
+export function getPluginConfig(args = {}, {cwd = process.cwd()} = {}) {
let plugins = []
try {
diff --git a/src/config/user.js b/src/config/user.js
new file mode 100644
index 00000000..5077dea7
--- /dev/null
+++ b/src/config/user.js
@@ -0,0 +1,217 @@
+import fs from 'fs'
+import path from 'path'
+
+import chalk from 'chalk'
+import webpack from 'webpack'
+
+import {CONFIG_FILE_NAME, PROJECT_TYPES} from '../constants'
+import debug from '../debug'
+import {ConfigValidationError} from '../errors'
+import {deepToString, joinAnd, pluralise as s, replaceArrayMerge, typeOf} from '../utils'
+import {processBabelConfig} from './babel'
+import {processKarmaConfig} from './karma'
+import {processNpmBuildConfig} from './npm'
+import UserConfigReport from './UserConfigReport'
+import {processWebpackConfig} from './webpack'
+
+const DEFAULT_REQUIRED = false
+
+/**
+ * Load a user config file and process it.
+ */
+export function getUserConfig(args = {}, options = {}) {
+ let {
+ check = false,
+ pluginConfig = {}, // eslint-disable-line no-unused-vars
+ required = DEFAULT_REQUIRED,
+ } = options
+
+ // Try to load default user config, or use a config file path we were given
+ let userConfig = {}
+ let configPath = path.resolve(args.config || CONFIG_FILE_NAME)
+
+ // Bail early if a config file is required by the current command, or if the
+ // user specified a custom location, and it doesn't exist.
+ let configFileExists = fs.existsSync(configPath)
+ if ((args.config || required) && !configFileExists) {
+ throw new Error(`Couldn't find a config file at ${configPath}`)
+ }
+
+ // If a config file exists, it should be a valid module regardless of whether
+ // or not it's required.
+ if (configFileExists) {
+ try {
+ userConfig = require(configPath)
+ debug('imported config module from %s', configPath)
+ // Delete the config file from the require cache as some builds need to
+ // import it multiple times with a different NODE_ENV in place.
+ delete require.cache[configPath]
+ }
+ catch (e) {
+ throw new Error(`Couldn't import the config file at ${configPath}: ${e.message}\n${e.stack}`)
+ }
+ }
+
+ userConfig = processUserConfig({
+ args,
+ check,
+ configFileExists,
+ configPath,
+ pluginConfig,
+ required,
+ userConfig,
+ })
+
+ if (configFileExists) {
+ userConfig.path = configPath
+ }
+
+ return userConfig
+}
+
+/**
+ * Load a user config file to get its project type. If we need to check the
+ * project type, a config file must exist.
+ */
+export function getProjectType(args = {}) {
+ // Try to load default user config, or use a config file path we were given
+ let userConfig = {}
+ let configPath = path.resolve(args.config || CONFIG_FILE_NAME)
+
+ // Bail early if a config file doesn't exist
+ let configFileExists = fs.existsSync(configPath)
+ if (!configFileExists) {
+ throw new Error(`Couldn't find a config file at ${configPath} to determine project type.`)
+ }
+
+ try {
+ userConfig = require(configPath)
+ // Delete the file from the require cache as it may be imported multiple
+ // times with a different NODE_ENV in place depending on the command.
+ delete require.cache[configPath]
+ }
+ catch (e) {
+ throw new Error(`Couldn't import the config file at ${configPath}: ${e.message}\n${e.stack}`)
+ }
+
+ // Config modules can export a function if they need to access the current
+ // command or the webpack dependency nwb manages for them.
+ if (typeOf(userConfig) === 'function') {
+ userConfig = userConfig({
+ args,
+ command: args._[0],
+ webpack,
+ })
+ }
+
+ let report = new UserConfigReport({configFileExists, configPath})
+
+ if (!PROJECT_TYPES.has(userConfig.type)) {
+ report.error('type', userConfig.type, `Must be one of: ${joinAnd(Array.from(PROJECT_TYPES), 'or')}`)
+ }
+ if (report.hasErrors()) {
+ throw new ConfigValidationError(report)
+ }
+
+ return userConfig.type
+}
+
+/**
+ * Validate user config and perform any supported transformations to it.
+ */
+export function processUserConfig({
+ args = {},
+ check = false,
+ configFileExists,
+ configPath,
+ pluginConfig = {},
+ required = DEFAULT_REQUIRED,
+ userConfig,
+ }) {
+ // Config modules can export a function if they need to access the current
+ // command or the webpack dependency nwb manages for them.
+ if (typeOf(userConfig) === 'function') {
+ userConfig = userConfig({
+ args,
+ command: args._[0],
+ webpack,
+ })
+ }
+
+ let report = new UserConfigReport({configFileExists, configPath})
+
+ let {
+ type, polyfill,
+ babel, devServer, karma, npm, webpack: _webpack, // eslint-disable-line no-unused-vars
+ ...unexpectedConfig
+ } = userConfig
+
+ let unexpectedProps = Object.keys(unexpectedConfig)
+ if (unexpectedProps.length > 0) {
+ report.error(
+ 'nwb config',
+ unexpectedProps.join(', '),
+ `Unexpected top-level prop${s(unexpectedProps.length)} in nwb config - ` +
+ 'see https://github.com/insin/nwb/blob/master/docs/Configuration.md for supported config'
+ )
+ }
+
+ if ((required || 'type' in userConfig) && !PROJECT_TYPES.has(type)) {
+ report.error('type', userConfig.type, `Must be one of: ${joinAnd(Array.from(PROJECT_TYPES), 'or')}`)
+ }
+
+ if ('polyfill' in userConfig && typeOf(polyfill) !== 'boolean') {
+ report.error(
+ 'polyfill',
+ `type: ${typeOf(polyfill)}`,
+ `Must be ${chalk.cyan('Boolean')}`
+ )
+ }
+
+ let argumentOverrides = {}
+ void ['babel', 'devServer', 'karma', 'npm', 'webpack'].forEach(prop => {
+ // Set defaults for top-level config objects so we don't have to
+ // existence-check them everywhere.
+ if (!(prop in userConfig)) {
+ userConfig[prop] = {}
+ }
+ else if (typeOf(userConfig[prop]) !== 'object') {
+ report.error(
+ prop,
+ `type: ${typeOf(userConfig[prop])}`,
+ `${chalk.cyan(prop)} config must be an ${chalk.cyan('Object')} `
+ )
+ // Set a valid default so further checks can continue
+ userConfig[prop] = {}
+ }
+ // Allow config overrides via --path.to.config in args
+ if (typeOf(args[prop]) === 'object') {
+ argumentOverrides[prop] = args[prop]
+ }
+ })
+
+ if (Object.keys(argumentOverrides).length > 0) {
+ debug('user config arguments: %s', deepToString(argumentOverrides))
+ userConfig = replaceArrayMerge(userConfig, argumentOverrides)
+ report.hasArgumentOverrides = true
+ }
+
+ processBabelConfig({report, userConfig})
+ processKarmaConfig({report, userConfig})
+ processNpmBuildConfig({report, userConfig})
+ processWebpackConfig({pluginConfig, report, userConfig})
+
+ if (report.hasErrors()) {
+ throw new ConfigValidationError(report)
+ }
+ if (check) {
+ throw report
+ }
+ if (report.hasSomethingToReport()) {
+ report.log()
+ }
+
+ debug('user config: %s', deepToString(userConfig))
+
+ return userConfig
+}
diff --git a/src/config/webpack.js b/src/config/webpack.js
new file mode 100644
index 00000000..81d01314
--- /dev/null
+++ b/src/config/webpack.js
@@ -0,0 +1,485 @@
+import chalk from 'chalk'
+
+import {INFERNO_APP, PREACT_APP} from '../constants'
+import {COMPAT_CONFIGS} from '../createWebpackConfig'
+import {joinAnd, pluralise as s, typeOf} from '../utils'
+
+const DEFAULT_STYLE_LOADERS = new Set(['css', 'postcss'])
+
+// TODO Remove in a future version
+let warnedAboutEnzymeCompat = false
+let warnedAboutOldStyleRules = false
+let warnedAboutSinonCompat = false
+
+export function processWebpackConfig({pluginConfig, report, userConfig}) {
+ let {
+ aliases,
+ autoprefixer,
+ compat,
+ copy,
+ debug,
+ define,
+ extractText,
+ hoisting,
+ html,
+ install,
+ publicPath,
+ rules,
+ styles,
+ uglify,
+ extra,
+ config,
+ ...unexpectedConfig
+ } = userConfig.webpack
+
+ let unexpectedProps = Object.keys(unexpectedConfig)
+ if (unexpectedProps.length > 0) {
+ report.error(
+ 'webpack',
+ unexpectedProps.join(', '),
+ `Unexpected prop${s(unexpectedProps.length)} in ${chalk.cyan('webpack')} config - ` +
+ 'see https://github.com/insin/nwb/blob/master/docs/Configuration.md#webpack-configuration for supported config'
+ )
+ }
+
+ // aliases
+ if ('aliases' in userConfig.webpack) {
+ if (typeOf(aliases) !== 'object') {
+ report.error(
+ 'webpack.aliases',
+ `type: ${typeOf(aliases)}`,
+ `Must be an ${chalk.cyan('Object')}`
+ )
+ }
+ else {
+ checkForRedundantCompatAliases(
+ userConfig.type,
+ aliases,
+ 'webpack.aliases',
+ report
+ )
+ }
+ }
+
+ // autoprefixer
+ if ('autoprefixer' in userConfig.webpack) {
+ // Convenience: allow Autoprefixer browsers config to be configured as a String
+ if (typeOf(autoprefixer) === 'string') {
+ userConfig.webpack.autoprefixer = {browsers: autoprefixer}
+ }
+ else if (typeOf(autoprefixer) !== 'object') {
+ report.error(
+ 'webpack.autoprefixer',
+ `type: ${typeOf(autoprefixer)}`,
+ `Must be a ${chalk.cyan('String')} (for ${chalk.cyan('browsers')} config only) ` +
+ `or an ${chalk.cyan('Object')} (for any Autoprefixer options)`
+ )
+ }
+ }
+
+ // compat
+ if ('compat' in userConfig.webpack) {
+ if (typeOf(compat) !== 'object') {
+ report.error(
+ 'webpack.compat',
+ `type: ${typeOf(compat)}`,
+ `Must be an ${chalk.cyan('Object')}`
+ )
+ }
+ else {
+ // Validate compat props
+ let compatProps = Object.keys(compat)
+ let unexpectedCompatProps = compatProps.filter(prop => !(prop in COMPAT_CONFIGS))
+ if (unexpectedCompatProps.length > 0) {
+ report.error(
+ 'webpack.compat',
+ unexpectedCompatProps.join(', '),
+ `Unexpected prop${s(unexpectedCompatProps.length)} in ${chalk.cyan('webpack.compat')}. ` +
+ `Valid props are: ${joinAnd(Object.keys(COMPAT_CONFIGS).map(p => chalk.cyan(p)), 'or')}`)
+ }
+
+ if (compat.enzyme) {
+ if (!warnedAboutEnzymeCompat) {
+ report.deprecated(
+ 'webpack.compat.enzyme',
+ `The ${chalk.cyan('enzyme')} compat flag applies to Enzyme 2.x, which is no longer the current major version`,
+ 'Consider upgrading Enzyme if you can, which now requires configuring Enzyme itself rather than Webpack config'
+ )
+ warnedAboutEnzymeCompat = true
+ }
+ }
+
+ if (compat.sinon) {
+ if (!warnedAboutSinonCompat) {
+ report.deprecated(
+ 'webpack.compat.sinon',
+ `The ${chalk.cyan('sinon')} compat flag applies to Sinon 1.x, which is no longer the current major version`,
+ 'Consider upgrading Sinon if you can, which now works with Webpack out of the box'
+ )
+ warnedAboutSinonCompat = true
+ }
+ }
+
+ void ['intl', 'moment', 'react-intl'].forEach(compatProp => {
+ let config = compat[compatProp]
+ let configType = typeOf(config)
+ if (configType === 'string') {
+ compat[compatProp] = {locales: [config]}
+ }
+ else if (configType === 'array') {
+ compat[compatProp] = {locales: config}
+ }
+ else if (configType === 'object') {
+ if (typeOf(config.locales) === 'string') {
+ config.locales = [config.locales]
+ }
+ else if (typeOf(config.locales) !== 'array') {
+ report.error(
+ `webpack.compat.${compatProp}.locales`,
+ config.locales,
+ `Must be a ${chalk.cyan('String')} (single locale name) or an ${chalk.cyan('Array')} of locales`
+ )
+ }
+ }
+ else {
+ report.error(
+ `webpack.compat.${compatProp}`,
+ `type: ${configType}`,
+ `Must be a ${chalk.cyan('String')} (single locale name), an ${chalk.cyan('Array')} ` +
+ `of locales or an ${chalk.cyan('Object')} with a ${chalk.cyan('locales')} property - ` +
+ 'see https://github.com/insin/nwb/blob/master/docs/Configuration.md#compat-object '
+ )
+ }
+ })
+ }
+ }
+
+ // copy
+ if ('copy' in userConfig.webpack) {
+ if (typeOf(copy) === 'array') {
+ userConfig.webpack.copy = {patterns: copy}
+ }
+ else if (typeOf(copy) === 'object') {
+ if (!copy.patterns && !copy.options) {
+ report.error(
+ 'webpack.copy',
+ copy,
+ `Must include ${chalk.cyan('patterns')} or ${chalk.cyan('options')}`
+ )
+ }
+ if (copy.patterns && typeOf(copy.patterns) !== 'array') {
+ report.error(
+ 'webpack.copy.patterns',
+ copy.patterns,
+ `Must be an ${chalk.cyan('Array')}`
+ )
+ }
+ if (copy.options && typeOf(copy.options) !== 'object') {
+ report.error(
+ 'webpack.copy.options',
+ copy.options,
+ `Must be an ${chalk.cyan('Object')}`
+ )
+ }
+ }
+ else {
+ report.error(
+ 'webpack.copy',
+ copy,
+ `Must be an ${chalk.cyan('Array')} or an ${chalk.cyan('Object')}`
+ )
+ }
+ }
+
+ // debug
+ if (debug) {
+ // Make it harder for the user to forget to disable the production debug build
+ // if they've enabled it in the config file.
+ report.hint('webpack.debug',
+ "Don't forget to disable the debug build before building for production"
+ )
+ }
+
+ // define
+ if ('define' in userConfig.webpack) {
+ if (typeOf(define) !== 'object') {
+ report.error(
+ 'webpack.define',
+ `type: ${typeOf(define)}`,
+ `Must be an ${chalk.cyan('Object')}`
+ )
+ }
+ }
+
+ // extractText
+ if ('extractText' in userConfig.webpack) {
+ if (typeOf(extractText) !== 'object') {
+ report.error(
+ 'webpack.extractText',
+ `type: ${typeOf(extractText)}`,
+ `Must be an ${chalk.cyan('Object')}`
+ )
+ }
+ }
+
+ // hoisting
+ if ('hoisting' in userConfig.webpack) {
+ if (typeOf(hoisting) !== 'boolean') {
+ report.error(
+ 'webpack.hoisting',
+ `type: ${typeOf(hoisting)}`,
+ `Must be a ${chalk.cyan('Boolean')}`
+ )
+ }
+ }
+
+ // html
+ if ('html' in userConfig.webpack) {
+ if (typeOf(html) !== 'object') {
+ report.error(
+ 'webpack.html',
+ `type: ${typeOf(html)}`,
+ `Must be an ${chalk.cyan('Object')}`
+ )
+ }
+ }
+
+ // install
+ if ('install' in userConfig.webpack) {
+ if (typeOf(install) !== 'object') {
+ report.error(
+ 'webpack.install',
+ `type: ${typeOf(install)}`,
+ `Must be an ${chalk.cyan('Object')}`
+ )
+ }
+ }
+
+ // publicPath
+ if ('publicPath' in userConfig.webpack) {
+ if (typeOf(publicPath) !== 'string') {
+ report.error(
+ 'webpack.publicPath',
+ `type: ${typeOf(publicPath)}`,
+ `Must be a ${chalk.cyan('String')}`
+ )
+ }
+ }
+
+ // rules
+ if ('rules' in userConfig.webpack) {
+ if (typeOf(rules) !== 'object') {
+ report.error(
+ 'webpack.rules',
+ `type: ${typeOf(rules)}`,
+ `Must be an ${chalk.cyan('Object')}`
+ )
+ }
+ else {
+ let error = false
+ Object.keys(rules).forEach(ruleId => {
+ let rule = rules[ruleId]
+ if (rule.use && typeOf(rule.use) !== 'array') {
+ report.error(
+ `webpack.rules.${ruleId}.use`,
+ `type: ${typeOf(rule.use)}`,
+ `Must be an ${chalk.cyan('Array')}`
+ )
+ error = true
+ }
+ })
+ if (!error) {
+ prepareWebpackRuleConfig(rules)
+ }
+ }
+ }
+
+ // styles
+ if ('styles' in userConfig.webpack) {
+ let configType = typeOf(styles)
+ let help = `Must be ${chalk.cyan("'old'")} (to use default style rules from nwb <= v0.15), ` +
+ `${chalk.cyan('false')} (to disable default style rules) or ` +
+ `an ${chalk.cyan('Object')} (to configure custom style rules)`
+ if (configType === 'string' && styles !== 'old') {
+ report.error('webpack.styles', styles, help)
+ if (!warnedAboutOldStyleRules) {
+ report.deprecated('webpack.styles', 'Support for default style rules from nwb <= v0.15 will be removed in a future release')
+ warnedAboutOldStyleRules = true
+ }
+ }
+ else if (configType === 'boolean' && styles !== false) {
+ report.error('webpack.styles', styles, help)
+ }
+ else if (configType !== 'object' && configType !== 'boolean') {
+ report.error('webpack.styles', `type: ${configType}`, help)
+ }
+ else {
+ let styleTypeIds = ['css']
+ if (pluginConfig.cssPreprocessors) {
+ styleTypeIds = styleTypeIds.concat(Object.keys(pluginConfig.cssPreprocessors))
+ }
+ let error = false
+ Object.keys(styles).forEach(styleType => {
+ if (styleTypeIds.indexOf(styleType) === -1) {
+ report.error(
+ 'webpack.styles',
+ `property: ${styleType}`,
+ `Unknown style type - must be ${joinAnd(styleTypeIds.map(s => chalk.cyan(s)), 'or')}`
+ )
+ error = true
+ }
+ else if (typeOf(styles[styleType]) !== 'array') {
+ report.error(
+ `webpack.styles.${styleType}`,
+ `type: ${typeOf(styles[styleType])}`,
+ `Must be an ${chalk.cyan('Array')} - if you don't need multiple custom rules, ` +
+ `configure the defaults via ${chalk.cyan('webpack.rules')} instead`
+ )
+ error = true
+ }
+ else {
+ styles[styleType].forEach((styleConfig, index) => {
+ let {
+ test, include, exclude, // eslint-disable-line no-unused-vars
+ ...loaderConfig
+ } = styleConfig
+ Object.keys(loaderConfig).forEach(loaderId => {
+ if (!DEFAULT_STYLE_LOADERS.has(loaderId) && loaderId !== styleType) {
+ // XXX Assumption: preprocessors provide a single loader which
+ // is configured with the same id as the style type id.
+ // XXX Using Array.from() manually as babel-preset-env with a
+ // Node 4 target is tranpiling Array spreads to concat()
+ // calls without ensuring Sets are converted to Arrays.
+ let loaderIds = Array.from(new Set([
+ ...Array.from(DEFAULT_STYLE_LOADERS),
+ styleType
+ ])).map(id => chalk.cyan(id))
+ report.error(
+ `webpack.styles.${styleType}[${index}]`,
+ `property: ${loaderId}`,
+ `Must be ${chalk.cyan('include')}, ${chalk.cyan('exclude')} or a loader id: ${joinAnd(loaderIds, 'or')}`
+ )
+ error = true
+ }
+ })
+ })
+ }
+ })
+ if (!error) {
+ prepareWebpackStyleConfig(styles)
+ }
+ }
+ }
+
+ // uglify
+ if ('uglify' in userConfig.webpack) {
+ if (uglify !== false && typeOf(uglify) !== 'object') {
+ report.error(
+ `webpack.uglify`,
+ uglify,
+ `Must be ${chalk.cyan('false')} (to disable UglifyJsPlugin) or ` +
+ `an ${chalk.cyan('Object')} (to configure UglifyJsPlugin)`
+ )
+ }
+ }
+
+ // extra
+ if ('extra' in userConfig.webpack) {
+ if (typeOf(extra) !== 'object') {
+ report.error(
+ 'webpack.extra',
+ `type: ${typeOf(extra)}`,
+ `Must be an ${chalk.cyan('Object')}`
+ )
+ }
+ else {
+ if (typeOf(extra.output) === 'object' && extra.output.publicPath) {
+ report.hint('webpack.extra.output.publicPath',
+ `You can use the more convenient ${chalk.cyan('webpack.publicPath')} config instead`
+ )
+ }
+ if (typeOf(extra.resolve) === 'object' && extra.resolve.alias) {
+ report.hint('webpack.extra.resolve.alias',
+ `You can use the more convenient ${chalk.cyan('webpack.aliases')} config instead`
+ )
+ checkForRedundantCompatAliases(
+ userConfig.type,
+ extra.resolve.alias,
+ 'webpack.extra.resolve.alias',
+ report
+ )
+ }
+ }
+ }
+
+ // config
+ if ('config' in userConfig.webpack && typeOf(config) !== 'function') {
+ report.error(
+ `webpack.config`,
+ `type: ${typeOf(config)}`,
+ `Must be a ${chalk.cyan('Function')}`
+ )
+ }
+}
+
+/**
+ * Tell the user if they've manually set up the same React compatibility aliases
+ * nwb configured by default.
+ */
+function checkForRedundantCompatAliases(projectType, aliases, configPath, report) {
+ if (!new Set([INFERNO_APP, PREACT_APP]).has(projectType)) return
+
+ let compatModule = `${projectType.split('-')[0]}-compat`
+ if (aliases.react && aliases.react.includes(compatModule)) {
+ report.hint(`${configPath}.react`,
+ `nwb aliases ${chalk.yellow('react')} to ${chalk.cyan(compatModule)} by default, so you can remove this config`
+ )
+ }
+ if (aliases['react-dom'] && aliases['react-dom'].includes(compatModule)) {
+ report.hint(`${configPath}.react-dom`,
+ `nwb aliases ${chalk.yellow('react-dom')} to ${chalk.cyan(compatModule)} by default, so you can remove this config`
+ )
+ }
+}
+
+/**
+ * Move loader options into an options object, allowing users to provide flatter
+ * config.
+ */
+export function prepareWebpackRuleConfig(rules) {
+ Object.keys(rules).forEach(ruleId => {
+ let rule = rules[ruleId]
+ // XXX Special case for stylus-loader, which uses a 'use' option for plugins
+ if ((rule.use && !/stylus$/.test(ruleId)) || rule.options) return
+ let {
+ exclude, include, test, loader, // eslint-disable-line no-unused-vars
+ ...options
+ } = rule
+ if (Object.keys(options).length > 0) {
+ rule.options = options
+ Object.keys(options).forEach(prop => delete rule[prop])
+ }
+ })
+}
+
+/**
+ * Move loader options into a loaders object, allowing users to provide flatter
+ * config.
+ */
+export function prepareWebpackStyleConfig(styles) {
+ Object.keys(styles).forEach(type => {
+ styles[type].forEach(styleConfig => {
+ let {
+ exclude, include, // eslint-disable-line no-unused-vars
+ ...loaderConfig
+ } = styleConfig
+ if (Object.keys(loaderConfig).length > 0) {
+ styleConfig.loaders = {}
+ Object.keys(loaderConfig).forEach(loader => {
+ styleConfig.loaders[loader] = {options: styleConfig[loader]}
+ delete styleConfig[loader]
+ })
+ }
+ })
+ })
+}
diff --git a/src/createBabelConfig.js b/src/createBabelConfig.js
index 17d28279..47462a5b 100644
--- a/src/createBabelConfig.js
+++ b/src/createBabelConfig.js
@@ -1,6 +1,7 @@
// @flow
import path from 'path'
+import {UserError} from './errors'
import {typeOf} from './utils'
type BabelPluginConfig = string | [string, Object];
@@ -24,6 +25,7 @@ type BuildOptions = {
type UserOptions = {
cherryPick?: string | string[],
+ config?: (BabelConfig) => BabelConfig,
env?: Object,
loose?: boolean,
plugins?: BabelPluginConfig[],
@@ -39,7 +41,8 @@ const RUNTIME_PATH = path.dirname(require.resolve('babel-runtime/package'))
export default function createBabelConfig(
buildConfig: BuildOptions = {},
- userConfig: UserOptions = {}
+ userConfig: UserOptions = {},
+ userConfigPath: string = ''
): BabelConfig {
let {
commonJSInterop,
@@ -54,6 +57,7 @@ export default function createBabelConfig(
let {
cherryPick,
+ config: userConfigFunction,
env = {},
loose,
plugins: userPlugins = [],
@@ -180,5 +184,14 @@ export default function createBabelConfig(
config.plugins = plugins
}
+ // Finally, give the user a chance to do whatever they want with the generated
+ // config.
+ if (typeof userConfigFunction === 'function') {
+ config = userConfigFunction(config)
+ if (!config) {
+ throw new UserError(`babel.config() in ${userConfigPath} didn't return anything - it must return the Babel config object.`)
+ }
+ }
+
return config
}
diff --git a/src/createKarmaConfig.js b/src/createKarmaConfig.js
index 3773f842..e3b88921 100644
--- a/src/createKarmaConfig.js
+++ b/src/createKarmaConfig.js
@@ -132,9 +132,7 @@ export default function createKarmaConfig(args, buildConfig, pluginConfig, userC
})
let {excludeFromCoverage = DEFAULT_EXCLUDE_FROM_COVERAGE} = userKarma
- if (typeOf(excludeFromCoverage) === 'string') excludeFromCoverage = [excludeFromCoverage]
let testFiles = userKarma.testFiles || DEFAULT_TEST_FILES
- if (typeOf(testFiles) === 'string') testFiles = [testFiles]
// Polyfill by default for browsers which lack features (hello PhantomJS)
let files = [require.resolve('babel-polyfill/dist/polyfill.js')]
@@ -217,7 +215,7 @@ export default function createKarmaConfig(args, buildConfig, pluginConfig, userC
karmaConfig = merge(karmaConfig, userKarma.extra)
}
- // Finally, give them a chance to do whatever they want with the generated
+ // Finally, give the user a chance to do whatever they want with the generated
// config.
if (typeOf(userKarma.config) === 'function') {
karmaConfig = userKarma.config(karmaConfig)
diff --git a/src/createServerWebpackConfig.js b/src/createServerWebpackConfig.js
index 893b2cb8..0b3f3983 100644
--- a/src/createServerWebpackConfig.js
+++ b/src/createServerWebpackConfig.js
@@ -1,7 +1,6 @@
// @flow
+import {getPluginConfig, getUserConfig} from './config'
import createWebpackConfig from './createWebpackConfig'
-import getPluginConfig from './getPluginConfig'
-import getUserConfig from './getUserConfig'
import type {ServerConfig} from './types'
diff --git a/src/createWebpackConfig.js b/src/createWebpackConfig.js
index e50758c1..5e2d6b23 100644
--- a/src/createWebpackConfig.js
+++ b/src/createWebpackConfig.js
@@ -786,7 +786,7 @@ export default function createWebpackConfig(
}
// Generate config for babel-loader and set it as loader config for the build
- buildRulesConfig.babel = {options: createBabelConfig(buildBabelConfig, userConfig.babel)}
+ buildRulesConfig.babel = {options: createBabelConfig(buildBabelConfig, userConfig.babel, userConfig.path)}
let webpackConfig = {
module: {
diff --git a/src/expressMiddleware.js b/src/expressMiddleware.js
index 7b864813..61968f16 100644
--- a/src/expressMiddleware.js
+++ b/src/expressMiddleware.js
@@ -4,10 +4,10 @@ import assert from 'assert'
import webpack from 'webpack'
import {createServeConfig} from './appCommands'
+import {getProjectType} from './config'
import {INFERNO_APP, PREACT_APP, REACT_APP, WEB_APP} from './constants'
import createServerWebpackConfig from './createServerWebpackConfig'
import debug from './debug'
-import {getProjectType} from './getUserConfig'
import {deepToString, joinAnd} from './utils'
const APP_TYPE_CONFIG = {
diff --git a/src/getUserConfig.js b/src/getUserConfig.js
deleted file mode 100644
index 3883350e..00000000
--- a/src/getUserConfig.js
+++ /dev/null
@@ -1,643 +0,0 @@
-import fs from 'fs'
-import path from 'path'
-import util from 'util'
-
-import chalk from 'chalk'
-import figures from 'figures'
-import webpack from 'webpack'
-
-import {CONFIG_FILE_NAME, INFERNO_APP, PREACT_APP, PROJECT_TYPES} from './constants'
-import {COMPAT_CONFIGS} from './createWebpackConfig'
-import debug from './debug'
-import {ConfigValidationError} from './errors'
-import {deepToString, joinAnd, replaceArrayMerge, typeOf} from './utils'
-
-const DEFAULT_REQUIRED = false
-
-const BABEL_RUNTIME_OPTIONS = new Set(['helpers', 'polyfill'])
-const DEFAULT_STYLE_LOADERS = new Set(['css', 'postcss'])
-
-let s = (n, w = ',s') => w.split(',')[n === 1 ? 0 : 1]
-
-export class UserConfigReport {
- constructor(configPath) {
- this.configPath = configPath
- this.deprecations = []
- this.errors = []
- this.hints = []
- }
-
- deprecated(path, ...messages) {
- this.deprecations.push({path, messages})
- }
-
- error(path, value, message) {
- this.errors.push({path, value, message})
- }
-
- hasErrors() {
- return this.errors.length > 0
- }
-
- hasSomethingToReport() {
- return this.errors.length + this.deprecations.length + this.hints.length > 0
- }
-
- hint(path, ...messages) {
- this.hints.push({path, messages})
- }
-
- log() {
- console.log(chalk.underline(`nwb config report for ${this.configPath}`))
- console.log()
- if (!this.hasSomethingToReport()) {
- console.log(chalk.green(`${figures.tick} Nothing to report!`))
- return
- }
-
- if (this.errors.length) {
- let count = this.errors.length > 1 ? `${this.errors.length} ` : ''
- console.log(chalk.red.underline(`${count}Error${s(this.errors.length)}`))
- console.log()
- }
- this.errors.forEach(({path, value, message}) => {
- console.log(`${chalk.red(`${figures.cross} ${path}`)} ${chalk.cyan('=')} ${util.inspect(value)}`)
- console.log(` ${message}`)
- console.log()
- })
- if (this.deprecations.length) {
- let count = this.deprecations.length > 1 ? `${this.deprecations.length} ` : ''
- console.log(chalk.yellow.underline(`${count}Deprecation Warning${s(this.deprecations.length)}`))
- console.log()
- }
- this.deprecations.forEach(({path, messages}) => {
- console.log(chalk.yellow(`${figures.warning} ${path}`))
- messages.forEach(message => {
- console.log(` ${message}`)
- })
- console.log()
- })
- if (this.hints.length) {
- let count = this.hints.length > 1 ? `${this.hints.length} ` : ''
- console.log(chalk.cyan.underline(`${count}Hint${s(this.hints.length)}`))
- console.log()
- }
- this.hints.forEach(({path, messages}) => {
- console.log(chalk.cyan(`${figures.info} ${path}`))
- messages.forEach(message => {
- console.log(` ${message}`)
- })
- console.log()
- })
- }
-}
-
-function checkForRedundantCompatAliases(projectType, aliases, configPath, report) {
- if (!new Set([INFERNO_APP, PREACT_APP]).has(projectType)) return
- if (!aliases) return
-
- let compatModule = `${projectType.split('-')[0]}-compat`
- if (aliases.react && aliases.react.includes(compatModule)) {
- report.hint(`${configPath}.react`,
- `nwb aliases ${chalk.yellow('react')} to ${chalk.green(compatModule)} by default, so you can remove this config.`
- )
- }
- if (aliases['react-dom'] && aliases['react-dom'].includes(compatModule)) {
- report.hint(`${configPath}.react-dom`,
- `nwb aliases ${chalk.yellow('react-dom')} to ${chalk.green(compatModule)} by default, so you can remove this config.`
- )
- }
-}
-
-/**
- * Load a user config file to get its project type. If we need to check the
- * project type, a config file must exist.
- */
-export function getProjectType(args = {}) {
- // Try to load default user config, or use a config file path we were given
- let userConfig = {}
- let userConfigPath = path.resolve(args.config || CONFIG_FILE_NAME)
-
- // Bail early if a config file doesn't exist
- let configFileExists = fs.existsSync(userConfigPath)
- if (!configFileExists) {
- throw new Error(`Couldn't find a config file at ${userConfigPath} to determine project type.`)
- }
-
- try {
- userConfig = require(userConfigPath)
- // Delete the file from the require cache as it may be imported multiple
- // times with a different NODE_ENV in place depending on the command.
- delete require.cache[userConfigPath]
- }
- catch (e) {
- throw new Error(`Couldn't import the config file at ${userConfigPath}: ${e.message}\n${e.stack}`)
- }
-
- // Config modules can export a function if they need to access the current
- // command or the webpack dependency nwb manages for them.
- if (typeOf(userConfig) === 'function') {
- userConfig = userConfig({
- args,
- command: args._[0],
- webpack,
- })
- }
-
- let report = new UserConfigReport(userConfigPath)
-
- if (!PROJECT_TYPES.has(userConfig.type)) {
- report.error('type', userConfig.type, `Must be one of: ${[...PROJECT_TYPES].join(', ')}`)
- }
- if (report.hasErrors()) {
- throw new ConfigValidationError(report)
- }
-
- return userConfig.type
-}
-
-/**
- * Move loader options into an options object, allowing users to provide flatter
- * config.
- */
-export function prepareWebpackRuleConfig(rules) {
- Object.keys(rules).forEach(ruleId => {
- let rule = rules[ruleId]
- // XXX Special case for stylus-loader, which uses a 'use' option for plugins
- if ((rule.use && !/stylus$/.test(ruleId)) || rule.options) return
- let {
- exclude, include, test, loader, // eslint-disable-line no-unused-vars
- ...options
- } = rule
- if (Object.keys(options).length > 0) {
- rule.options = options
- Object.keys(options).forEach(prop => delete rule[prop])
- }
- })
-}
-
-/**
- * Move loader options into a loaders object, allowing users to provide flatter
- * config.
- */
-export function prepareWebpackStyleConfig(styles) {
- Object.keys(styles).forEach(type => {
- styles[type].forEach(styleConfig => {
- let {
- exclude, include, // eslint-disable-line no-unused-vars
- ...loaderConfig
- } = styleConfig
- if (Object.keys(loaderConfig).length > 0) {
- styleConfig.loaders = {}
- Object.keys(loaderConfig).forEach(loader => {
- styleConfig.loaders[loader] = {options: styleConfig[loader]}
- delete styleConfig[loader]
- })
- }
- })
- })
-}
-
-// TODO Remove in a future version
-let warnedAboutEnzymeCompat = false
-let warnedAboutOldStyleRules = false
-
-/**
- * Validate user config and perform any necessary validation and transformation
- * to it.
- */
-export function processUserConfig({
- args = {},
- check = false,
- pluginConfig = {},
- required = DEFAULT_REQUIRED,
- userConfig,
- userConfigPath,
- }) {
- // Config modules can export a function if they need to access the current
- // command or the webpack dependency nwb manages for them.
- if (typeOf(userConfig) === 'function') {
- userConfig = userConfig({
- args,
- command: args._[0],
- webpack,
- })
- }
-
- let report = new UserConfigReport(userConfigPath)
-
- if ((required || 'type' in userConfig) && !PROJECT_TYPES.has(userConfig.type)) {
- report.error('type', userConfig.type, `Must be one of: ${[...PROJECT_TYPES].join(', ')}`)
- }
-
- let argumentOverrides = {}
- void ['babel', 'devServer', 'karma', 'npm', 'webpack'].forEach(prop => {
- // Set defaults for top-level config objects so we don't have to
- // existence-check them everywhere.
- if (!(prop in userConfig)) {
- userConfig[prop] = {}
- }
- // Allow config overrides via --path.to.config in args
- if (typeOf(args[prop]) === 'object') {
- argumentOverrides[prop] = args[prop]
- }
- })
-
- // Make it harder for the user to forget to disable the production debug build
- // if they've enabled it in the config file.
- if (userConfig.webpack.debug) {
- report.hint('webpack.debug',
- "Don't forget to disable the debug build before building for production"
- )
- }
-
- if (Object.keys(argumentOverrides).length > 0) {
- debug('user config arguments: %s', deepToString(argumentOverrides))
- userConfig = replaceArrayMerge(userConfig, argumentOverrides)
- }
-
- // Babel config
- if (!!userConfig.babel.stage || userConfig.babel.stage === 0) {
- if (typeOf(userConfig.babel.stage) !== 'number') {
- report.error(
- 'babel.stage',
- userConfig.babel.stage,
- `Must be a ${chalk.cyan('Number')} between ${chalk.cyan('0')} and ${chalk.cyan('3')}, ` +
- `or ${chalk.cyan('false')} to disable use of a stage preset.`
- )
- }
- else if (userConfig.babel.stage < 0 || userConfig.babel.stage > 3) {
- report.error(
- 'babel.stage',
- userConfig.babel.stage,
- `Must be between ${chalk.cyan(0)} and ${chalk.cyan(3)}`
- )
- }
- }
- if (userConfig.babel.presets) {
- if (typeOf(userConfig.babel.presets) === 'string') {
- userConfig.babel.presets = [userConfig.babel.presets]
- }
- else if (typeOf(userConfig.babel.presets) !== 'array') {
- report.error('babel.presets', userConfig.babel.presets, `Must be a string or an ${chalk.cyan('Array')}`)
- }
- }
- if (userConfig.babel.plugins) {
- if (typeOf(userConfig.babel.plugins) === 'string') {
- userConfig.babel.plugins = [userConfig.babel.plugins]
- }
- else if (typeOf(userConfig.babel.plugins) !== 'array') {
- report.error('babel.plugins', userConfig.babel.plugins, `Must be a string or an ${chalk.cyan('Array')}`)
- }
- }
- if ('runtime' in userConfig.babel &&
- typeOf(userConfig.babel.runtime) !== 'boolean' &&
- !BABEL_RUNTIME_OPTIONS.has(userConfig.babel.runtime)) {
- report.error(
- 'babel.runtime',
- userConfig.babel.runtime,
- `Must be ${chalk.cyan('boolean')}, ${chalk.cyan("'helpers'")} or ${chalk.cyan("'polyfill'")})`
- )
- }
-
- if ('loose' in userConfig.babel) {
- if (typeOf(userConfig.babel.loose) !== 'boolean') {
- report.error(
- 'babel.loose',
- userConfig.babel.loose,
- `Must be ${chalk.cyan('boolean')}`
- )
- }
- else if (userConfig.babel.loose === true) {
- report.hint('babel.loose',
- 'Loose mode is enabled by default, so you can remove this config.'
- )
- }
- }
-
- // Karma config
- // noop
-
- // npm build config
- if (typeOf(userConfig.npm.umd) === 'string') {
- userConfig.npm.umd = {global: userConfig.npm.umd}
- }
-
- // Webpack config
- if (typeOf(userConfig.webpack.autoprefixer) === 'string') {
- userConfig.webpack.autoprefixer = {browsers: userConfig.webpack.autoprefixer}
- }
-
- if ('copy' in userConfig.webpack) {
- if (typeOf(userConfig.webpack.copy) === 'array') {
- userConfig.webpack.copy = {patterns: userConfig.webpack.copy}
- }
- else if (typeOf(userConfig.webpack.copy) === 'object') {
- if (!userConfig.webpack.copy.patterns &&
- !userConfig.webpack.copy.options) {
- report.error(
- 'webpack.copy',
- userConfig.webpack.copy,
- `Must include ${chalk.cyan('patterns')} or ${chalk.cyan('options')} when given as an ${chalk.cyan('Object')}`
- )
- }
- if (userConfig.webpack.copy.patterns &&
- typeOf(userConfig.webpack.copy.patterns) !== 'array') {
- report.error(
- 'webpack.copy.patterns',
- userConfig.webpack.copy.patterns,
- `Must be an ${chalk.cyan('Array')} when provided`
- )
- }
- if (userConfig.webpack.copy.options &&
- typeOf(userConfig.webpack.copy.options) !== 'object') {
- report.error(
- 'webpack.copy.options',
- userConfig.webpack.copy.options,
- `Must be an ${chalk.cyan('Object')} when provided.`
- )
- }
- }
- else {
- report.error(
- 'webpack.copy',
- userConfig.webpack.copy,
- `Must be an ${chalk.cyan('Array')} or an ${chalk.cyan('Object')}.`
- )
- }
- }
-
- if (userConfig.webpack.compat) {
- if (userConfig.webpack.compat.enzyme) {
- if (!warnedAboutEnzymeCompat) {
- report.deprecated(
- 'webpack.compat.enzyme',
- `The ${chalk.cyan('enzyme')} compat flag applies to Enzyme v2, which has been superseded by Enzyme v3.`,
- 'Consider upgrading to Enzyme v3 if you can, which requires configuring Enzyme rather than Webpack.'
- )
- warnedAboutEnzymeCompat = true
- }
- }
- let compatProps = Object.keys(userConfig.webpack.compat)
- let unknownCompatProps = compatProps.filter(prop => !(prop in COMPAT_CONFIGS))
- if (unknownCompatProps.length !== 0) {
- report.error(
- 'userConfig.webpack.compat',
- compatProps,
- `Unknown propert${unknownCompatProps.length === 1 ? 'y' : 'ies'} present.` +
- `Valid properties are: ${Object.keys(COMPAT_CONFIGS).join(', ')}.`)
- }
-
- if (userConfig.webpack.compat.intl) {
- if (typeOf(userConfig.webpack.compat.intl.locales) === 'string') {
- userConfig.webpack.compat.intl.locales = [userConfig.webpack.compat.intl.locales]
- }
- else if (typeOf(userConfig.webpack.compat.intl.locales) !== 'array') {
- report.error(
- 'webpack.compat.intl.locales',
- webpack.compat.intl.locales,
- 'Must be a string or an Array.'
- )
- }
- }
-
- if (userConfig.webpack.compat.moment) {
- if (typeOf(userConfig.webpack.compat.moment.locales) === 'string') {
- userConfig.webpack.compat.moment.locales = [userConfig.webpack.compat.moment.locales]
- }
- else if (typeOf(userConfig.webpack.compat.moment.locales) !== 'array') {
- report.error(
- 'webpack.compat.moment.locales',
- webpack.compat.moment.locales,
- 'Must be a string or an Array.'
- )
- }
- }
-
- if (userConfig.webpack.compat['react-intl']) {
- if (typeOf(userConfig.webpack.compat['react-intl'].locales) === 'string') {
- userConfig.webpack.compat['react-intl'].locales =
- [userConfig.webpack.compat['react-intl'].locales]
- }
- else if (typeOf(userConfig.webpack.compat['react-intl'].locales) !== 'array') {
- report.error(
- 'webpack.compat[\'react-intl\'].locales',
- webpack.compat['react-intl'].locales,
- 'Must be a string or an Array.'
- )
- }
- }
- }
-
- if (userConfig.webpack.vendorBundle === false) {
- report.error(
- 'webpack.vendorBundle',
- webpack.vendorBundle,
- 'No longer supported - add a --no-vendor flag to your build command instead.'
- )
- }
-
- if ('rules' in userConfig.webpack) {
- if (typeOf(userConfig.webpack.rules) !== 'object') {
- report.error(
- 'webpack.rules',
- `type: ${typeOf(userConfig.webpack.rules)}`,
- 'Must be an Object.'
- )
- }
- else {
- let error = false
- Object.keys(userConfig.webpack.rules).forEach(ruleId => {
- let rule = userConfig.webpack.rules[ruleId]
- if (rule.use && typeOf(rule.use) !== 'array') {
- report.error(
- `webpack.rules.${ruleId}.use`,
- `type: ${typeOf(rule.use)}`,
- 'Must be an Array.'
- )
- error = true
- }
- })
- if (!error) {
- prepareWebpackRuleConfig(userConfig.webpack.rules)
- }
- }
- }
-
- if ('styles' in userConfig.webpack) {
- let configType = typeOf(userConfig.webpack.styles)
- if (configType === 'string' && userConfig.webpack.styles !== 'old') {
- report.error(
- 'webpack.styles',
- userConfig.webpack.styles,
- `Must be ${chalk.green("'old'")} (to use default style rules from nwb <= v0.15), ${chalk.green('false')} or an Object.`
- )
- if (!warnedAboutOldStyleRules) {
- report.deprecated('webpack.styles', 'Support for default style rules from nwb <= v0.15 will be removed in a future release.')
- warnedAboutOldStyleRules = true
- }
- }
- else if (configType === 'boolean' && userConfig.webpack.styles !== false) {
- report.error(
- 'webpack.styles',
- userConfig.webpack.styles,
- `Must be ${chalk.green("'old'")}, ${chalk.green('false')} (to disable default style rules) or an Object.`
- )
- }
- else if (configType !== 'object' && configType !== 'boolean') {
- report.error(
- 'webpack.styles',
- `type: ${configType}`,
- `Must be ${chalk.green("'old'")}, ${chalk.green('false')} or an Object (to configure custom style rules).`
- )
- }
- else {
- let styleTypeIds = ['css']
- if (pluginConfig.cssPreprocessors) {
- styleTypeIds = styleTypeIds.concat(Object.keys(pluginConfig.cssPreprocessors))
- }
- let error = false
- Object.keys(userConfig.webpack.styles).forEach(styleType => {
- if (styleTypeIds.indexOf(styleType) === -1) {
- report.error(
- 'webpack.styles',
- `property: ${styleType}`,
- `Unknown style type - must be ${joinAnd(styleTypeIds.map(chalk.green), 'or')}`
- )
- error = true
- }
- else if (typeOf(userConfig.webpack.styles[styleType]) !== 'array') {
- report.error(
- `webpack.styles.${styleType}`,
- `type: ${typeOf(userConfig.webpack.styles[styleType])}`,
- `Must be an Array - if you don't need multiple custom rules, configure the defaults via ${chalk.green('webpack.rules')} instead.`
- )
- error = true
- }
- else {
- userConfig.webpack.styles[styleType].forEach((styleConfig, index) => {
- let {
- test, include, exclude, // eslint-disable-line no-unused-vars
- ...loaderConfig
- } = styleConfig
- Object.keys(loaderConfig).forEach(loaderId => {
- if (!DEFAULT_STYLE_LOADERS.has(loaderId) && loaderId !== styleType) {
- // XXX Assumption: preprocessors provide a single loader which
- // is configured with the same id as the style type id.
- // XXX Using Array.from() manually as babel-preset-env with a
- // Node 4 target is tranpiling Array spreads to concat()
- // calls without ensuring Sets are converted to Arrays.
- let loaderIds = Array.from(new Set([
- ...Array.from(DEFAULT_STYLE_LOADERS),
- styleType
- ])).map(id => chalk.green(id))
- report.error(
- `webpack.styles.${styleType}[${index}]`,
- `property: ${loaderId}`,
- `Must be ${chalk.green('include')}, ${chalk.green('exclude')} or a loader id: ${joinAnd(loaderIds, 'or')}`
- )
- error = true
- }
- })
- })
- }
- })
- if (!error) {
- prepareWebpackStyleConfig(userConfig.webpack.styles, pluginConfig)
- }
- }
- }
-
- checkForRedundantCompatAliases(
- userConfig.type,
- userConfig.webpack.aliases,
- 'webpack.aliases',
- report
- )
-
- if (userConfig.webpack.extra) {
- if (userConfig.webpack.extra.output &&
- userConfig.webpack.extra.output.publicPath) {
- report.hint('webpack.extra.output.publicPath',
- `You can use the more convenient ${chalk.green('webpack.publicPath')} instead.`
- )
- }
- if (userConfig.webpack.extra.resolve &&
- userConfig.webpack.extra.resolve.alias) {
- report.hint('webpack.extra.resolve.alias',
- `You can use the more convenient ${chalk.green('webpack.aliases')} instead.`
- )
- checkForRedundantCompatAliases(
- userConfig.type,
- userConfig.webpack.extra.resolve.alias,
- 'webpack.extra.resolve.alias',
- report
- )
- }
- }
-
- if ('config' in userConfig.webpack && typeOf(userConfig.webpack.config) !== 'function') {
- report.error(
- `webpack.config`,
- `type: ${typeOf(userConfig.webpack.config)}`,
- 'Must be a Function.'
- )
- }
-
- if (report.hasErrors()) {
- throw new ConfigValidationError(report)
- }
- if (check) {
- throw report
- }
- if (report.hasSomethingToReport()) {
- report.log()
- }
-
- debug('user config: %s', deepToString(userConfig))
-
- return userConfig
-}
-
-/**
- * Load a user config file and process it.
- */
-export default function getUserConfig(args = {}, options = {}) {
- let {
- check = false,
- pluginConfig = {}, // eslint-disable-line no-unused-vars
- required = DEFAULT_REQUIRED,
- } = options
- // Try to load default user config, or use a config file path we were given
- let userConfig = {}
- let userConfigPath = path.resolve(args.config || CONFIG_FILE_NAME)
-
- // Bail early if a config file is required and doesn't exist
- let configFileExists = fs.existsSync(userConfigPath)
- if ((args.config || required) && !configFileExists) {
- throw new Error(`Couldn't find a config file at ${userConfigPath}`)
- }
-
- // If a config file exists, it should be a valid module regardless of whether
- // or not it's required.
- if (configFileExists) {
- try {
- userConfig = require(userConfigPath)
- debug('imported config module from %s', userConfigPath)
- // Delete the file from the require cache as some builds need to import
- // it multiple times with a different NODE_ENV in place.
- delete require.cache[userConfigPath]
- }
- catch (e) {
- throw new Error(`Couldn't import the config file at ${userConfigPath}: ${e.message}\n${e.stack}`)
- }
- }
-
- userConfig = processUserConfig({args, check, pluginConfig, required, userConfig, userConfigPath})
-
- if (configFileExists) {
- userConfig.path = userConfigPath
- }
-
- return userConfig
-}
diff --git a/src/karmaServer.js b/src/karmaServer.js
index 61095bcf..ba651fe8 100644
--- a/src/karmaServer.js
+++ b/src/karmaServer.js
@@ -1,9 +1,8 @@
import {Server} from 'karma'
+import {getPluginConfig, getUserConfig} from './config'
import createKarmaConfig from './createKarmaConfig'
import {KarmaExitCodeError} from './errors'
-import getPluginConfig from './getPluginConfig'
-import getUserConfig from './getUserConfig'
export default function karmaServer(args, buildConfig, cb) {
// Force the environment to test
diff --git a/src/moduleBuild.js b/src/moduleBuild.js
index d88b49af..7e0a5a82 100644
--- a/src/moduleBuild.js
+++ b/src/moduleBuild.js
@@ -7,11 +7,10 @@ import runSeries from 'run-series'
import merge from 'webpack-merge'
import cleanModule from './commands/clean-module'
+import {getPluginConfig, getUserConfig} from './config'
import createBabelConfig from './createBabelConfig'
import debug from './debug'
import {UserError} from './errors'
-import getPluginConfig from './getPluginConfig'
-import getUserConfig from './getUserConfig'
import {deepToString} from './utils'
import webpackBuild from './webpackBuild'
import {createBanner, createExternals, logGzippedFileSizes} from './webpackUtils'
@@ -29,8 +28,8 @@ const DEFAULT_BABEL_IGNORE_CONFIG = [
/**
* Run Babel with generated config written to a temporary .babelrc.
*/
-function runBabel(name, {copyFiles, outDir, src}, buildBabelConfig, userBabelConfig, cb) {
- let babelConfig = createBabelConfig(buildBabelConfig, userBabelConfig)
+function runBabel(name, {copyFiles, outDir, src}, buildBabelConfig, userConfig, cb) {
+ let babelConfig = createBabelConfig(buildBabelConfig, userConfig.babel, userConfig.path)
babelConfig.ignore = DEFAULT_BABEL_IGNORE_CONFIG
debug('babel config: %s', deepToString(babelConfig))
@@ -150,7 +149,7 @@ export default function moduleBuild(args, buildConfig = {}, cb) {
// Don't enable webpack-specific plugins
webpack: false,
}),
- userConfig.babel,
+ userConfig,
cb
))
}
@@ -169,7 +168,7 @@ export default function moduleBuild(args, buildConfig = {}, cb) {
// Don't enable webpack-specific plugins
webpack: false,
}),
- userConfig.babel,
+ userConfig,
cb
))
}
diff --git a/src/utils.js b/src/utils.js
index cff11fd8..58908310 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -199,6 +199,10 @@ export function modulePath(module: string, basedir: string = process.cwd()): str
return path.dirname(resolve.sync(`${module}/package.json`, {basedir}))
}
+export function pluralise(count: number, suffixes : string = ',s'): string {
+ return suffixes.split(',')[count === 1 ? 0 : 1]
+}
+
/**
* Custom merge which replaces arrays instead of concatenating them.
*/
diff --git a/src/webpackBuild.js b/src/webpackBuild.js
index 3c37748f..7c5a85af 100644
--- a/src/webpackBuild.js
+++ b/src/webpackBuild.js
@@ -1,10 +1,9 @@
import ora from 'ora'
import webpack from 'webpack'
+import {getPluginConfig, getUserConfig} from './config'
import createWebpackConfig from './createWebpackConfig'
import debug from './debug'
-import getPluginConfig from './getPluginConfig'
-import getUserConfig from './getUserConfig'
import {deepToString} from './utils'
import {logBuildResults} from './webpackUtils'
diff --git a/src/webpackServer.js b/src/webpackServer.js
index df2fa633..fc26aaca 100644
--- a/src/webpackServer.js
+++ b/src/webpackServer.js
@@ -2,12 +2,11 @@ import {yellow} from 'chalk'
import detect from 'detect-port'
import inquirer from 'inquirer'
+import {getPluginConfig, getUserConfig} from './config'
import {DEFAULT_PORT} from './constants'
import createServerWebpackConfig from './createServerWebpackConfig'
import debug from './debug'
import devServer from './devServer'
-import getPluginConfig from './getPluginConfig'
-import getUserConfig from './getUserConfig'
import {clearConsole, deepToString} from './utils'
/**
diff --git a/tests/getUserConfig-test.js b/tests/config-test.js
similarity index 88%
rename from tests/getUserConfig-test.js
rename to tests/config-test.js
index 689edda8..025c4d84 100644
--- a/tests/getUserConfig-test.js
+++ b/tests/config-test.js
@@ -1,8 +1,12 @@
+import path from 'path'
+
import expect from 'expect'
import webpack from 'webpack'
import {ConfigValidationError} from '../src/errors'
-import getUserConfig, {getProjectType, prepareWebpackRuleConfig, prepareWebpackStyleConfig, processUserConfig} from '../src/getUserConfig'
+import {getPluginConfig, getUserConfig, getProjectType} from '../src/config'
+import {processUserConfig} from '../src/config/user'
+import {prepareWebpackRuleConfig, prepareWebpackStyleConfig} from '../src/config/webpack'
describe('getProjectType()', () => {
it("throws an error when a config file can't be found", () => {
@@ -144,19 +148,24 @@ describe('processUserConfig()', () => {
it('passes command and webpack arguments when a config function is provided', () => {
let args = {_: ['abc123']}
+ // Using the webpack.extra escape hatch to pass arguments back out
let config = processUserConfig({
args,
userConfig(args) {
return {
- args: args.args,
- command: args.command,
- webpack: args.webpack,
+ webpack: {
+ extra: {
+ args: args.args,
+ command: args.command,
+ webpack: args.webpack,
+ }
+ }
}
}
})
- expect(config.args).toEqual(args)
- expect(config.command).toEqual('abc123')
- expect(config.webpack).toEqual(webpack)
+ expect(config.webpack.extra.args).toEqual(args)
+ expect(config.webpack.extra.command).toEqual('abc123')
+ expect(config.webpack.extra.webpack).toEqual(webpack)
})
it('defaults top-level config when none is provided', () => {
@@ -260,3 +269,17 @@ describe('prepareWebpackStyleConfig()', () => {
})
})
})
+
+describe('getPluginConfig()', () => {
+ it('scans package.json for nwb-* dependencies and imports them', () => {
+ let config = getPluginConfig({}, {cwd: path.join(__dirname, 'fixtures/plugins')})
+ expect(config).toEqual({
+ cssPreprocessors: {
+ fake: {
+ loader: 'path/to/fake.js',
+ test: /\.fake$/,
+ }
+ }
+ })
+ })
+})
diff --git a/tests/fixtures/bad-config.js b/tests/fixtures/bad-config.js
new file mode 100644
index 00000000..97ffa632
--- /dev/null
+++ b/tests/fixtures/bad-config.js
@@ -0,0 +1,8 @@
+// A config file in which everything is wrong
+module.exports = {
+ babel: 45,
+ devServer: 45,
+ karma: 45,
+ npm: 45,
+ webpack: 45
+}
diff --git a/tests/fixtures/worst-config.js b/tests/fixtures/worst-config.js
new file mode 100644
index 00000000..edfe1629
--- /dev/null
+++ b/tests/fixtures/worst-config.js
@@ -0,0 +1,68 @@
+// A config file in which everything is wrong
+module.exports = {
+ type: 'invalid',
+ polyfill: 45,
+ invalidTopLevelConfig: true,
+ moreInvalidTopLevelConfig: true,
+ babel: {
+ invalidBabelConfig: true,
+ cherryPick: 45,
+ env: 45,
+ loose: 45,
+ plugins: 45,
+ presets: 45,
+ removePropTypes: 45,
+ reactConstantElements: 45,
+ runtime: 45,
+ stage: 45,
+ config: 45
+ },
+ devServer: 45,
+ karma: {
+ invalidKarmaConfig: true,
+ browsers: 45,
+ excludeFromCoverage: 45,
+ frameworks: 45,
+ plugins: 45,
+ reporters: 45,
+ testContext: 45,
+ testFiles: 45,
+ extra: 45,
+ config: 45
+ },
+ npm: {
+ invalidNpmBuildConfig: true,
+ cjs: 45,
+ esModules: 45,
+ umd: {
+ invalidUMDConfig: true,
+ global: 45,
+ externals: 45,
+ }
+ },
+ webpack: {
+ invalidWebpackConfig: true,
+ moreInvalidWebpackConfig: true,
+ aliases: 45,
+ autoprefixer: 45,
+ compat: {
+ invalidCompatConfig: true,
+ intl: 45,
+ moment: 45,
+ 'react-intl': 45,
+ },
+ copy: 45,
+ debug: 45,
+ define: 45,
+ extractText: 45,
+ hoisting: 45,
+ html: 45,
+ install: 45,
+ publicPath: 45,
+ rules: 45,
+ styles: 45,
+ uglify: 45,
+ extra: 45,
+ config: 45,
+ }
+}
diff --git a/tests/getPluginConfig-test.js b/tests/getPluginConfig-test.js
deleted file mode 100644
index 65c467a2..00000000
--- a/tests/getPluginConfig-test.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import path from 'path'
-
-import expect from 'expect'
-
-import getPluginConfig from '../src/getPluginConfig'
-
-describe('getPluginConfig()', () => {
- it('scans package.json for nwb-* dependencies and imports them', () => {
- let config = getPluginConfig({}, {cwd: path.join(__dirname, 'fixtures/plugins')})
- expect(config).toEqual({
- cssPreprocessors: {
- fake: {
- loader: 'path/to/fake.js',
- test: /\.fake$/,
- }
- }
- })
- })
-})