Skip to content

Commit 30e0240

Browse files
feat: add "rules" concept, resolve.root name-clash detection with node_modules packages, file path existence checks (#81)
BREAKING CHANGE: validate() now takes only two arguments. The schema which could be passed as second argument can now be supplied via property `schema` on the options object which is now the second argument. The now preferred way to supply extra properties to the schema is supplying a a Joi.object via the options property `schemaExtension`, that *just contains the properties you want to add* (see README under "Customizing"). The schema used by this library **is not exported anymore**, as we now parameterize it by supplying "rules" which toggle / modify parts of the schema (Have a look at `src/index.js` to learn more).
1 parent f01b917 commit 30e0240

34 files changed

+568
-226
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
node_modules/
22
dist/
33
test/passing-configs
4+
test/failing-configs
45
coverage/
56
.nyc_output/

.travis.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
sudo: false
22
language: node_js
33
node_js:
4-
- '5'
4+
- '4'
5+
before_install:
6+
- npm i -g npm@^3.0.0
57
cache:
68
directories:
79
- node_modules

README.md

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,27 +57,39 @@ Now run webpack. Either everything is green and the build continues or `joi` wil
5757
#### CLI
5858
For CLI usage you probably want to install the tool globally (`npm install -g webpack-validator`) first. Then just run `webpack-validator <your-config>`.
5959

60-
#### Customizing
60+
### Customizing
61+
#### Schema
6162
If you need to extend the schema, for example for custom top level properties or properties added by third party plugins like `eslint-loader` (which adds a toplevel `eslint` property), do it like this:
6263

6364
```js
6465
const validate = require('webpack-validator')
65-
const schema = require('webpack-validator').schema
66+
const Joi = require('webpack-validator').Joi
6667

67-
// joi is installed as dependency of this package and will be available in node_modules
68-
// if you use npm 3. Otherwise install it explicitly.
69-
const Joi = require('joi')
70-
71-
const yourSchema = schema.concat(Joi.object({
68+
// This joi schema will be `Joi.concat`-ed with the internal schema
69+
const yourSchemaExtension = Joi.object({
7270
// this would just allow the property and doesn't perform any additional validation
7371
eslint: Joi.any()
74-
}))
72+
})
7573

7674
const config = { /* ... your webpack config */ }
7775

78-
// Override default config by supplying your config as second parameter.
79-
module.exports = validate(config, yourSchema)
76+
module.exports = validate(config, { schemaExtension: yourSchemaExtension })
77+
```
78+
79+
#### Rules
80+
Some validations do more than just validating your data shape, they check for best practices and do "more" which you might want to opt out of / in to. This is an overview of the available rules (we just started with this, this list will grow :)):
81+
- **no-root-files-node-modules-nameclash** (default: true): this checks that files/folders that are found in directories specified via webpacks `resolve.root` option do not nameclash with `node_modules` packages. This prevents nasty path resolving bugs (for a motivating example, have a look at [this redux issue](https://github.com/reactjs/redux/issues/1681)).
82+
83+
You opt in/out of rules by using the `rules` option:
8084
```
85+
module.exports = validate(config, {
86+
rules: {
87+
'no-root-files-node-modules-nameclash': false,
88+
},
89+
)
90+
```
91+
92+
**Note**: This is not yet implemented via cli options, the default rules will apply in that case.
8193

8294
#### Quiet Mode
8395
If you want to mute console output apart from errors, set `--quiet` (`-q`) or `validate(config, yourSchema, {quiet: true})`. This is particularly useful if you are using webpack `--json` as you'll want to avoid writing additional text to the JSON output.

package.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,10 @@
1313
"build": "babel --ignore *.test.js -d dist src",
1414
"check-coverage": "nyc check-coverage --statements 100 --branches 100 --functions 100 --lines 100",
1515
"lint": "eslint .",
16-
"cover": "nyc --reporter=lcov --reporter=text --reporter=html mocha src/**/*.test.js",
16+
"cover": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text --reporter=html mocha \"src/**/*.test.js\"",
1717
"report-coverage": "cat ./coverage/lcov.info | node_modules/.bin/codecov",
18-
"test": "mocha \"src/**/*.test.js\"",
18+
"test": "cross-env NODE_ENV=test mocha \"src/**/*.test.js\"",
1919
"watch:test": "npm run test -- -w",
20-
"release": "npm run build && with-package git commit -am pkg.version && with-package git tag pkg.version && git push && npm publish && git push --tags",
21-
"release:beta": "npm run release && npm run tag:beta",
22-
"tag:beta": "with-package npm dist-tag add pkg.name@pkg.version beta",
2320
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
2421
},
2522
"repository": {
@@ -41,12 +38,16 @@
4138
]
4239
},
4340
"dependencies": {
41+
"basename": "0.1.2",
4442
"chalk": "1.1.3",
4543
"commander": "2.9.0",
46-
"joi": "8.0.5",
44+
"cross-env": "^1.0.7",
45+
"find-node-modules": "^1.0.1",
46+
"joi": "9.0.0-0",
4747
"lodash": "4.11.1",
4848
"npmlog": "2.0.3",
49-
"yargs": "4.7.1"
49+
"yargs": "4.7.1",
50+
"shelljs": "0.7.0"
5051
},
5152
"devDependencies": {
5253
"autoprefixer": "6.3.6",

src/index.js

Lines changed: 60 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,56 +7,79 @@ import devtoolSchema from './properties/devtool'
77
import externalsSchema from './properties/externals'
88
import nodeSchema from './properties/node'
99
import pluginsSchema from './properties/plugins'
10-
import resolveSchema from './properties/resolve'
10+
import resolveSchemaFn from './properties/resolve'
1111
import outputSchema from './properties/output'
1212
import watchOptionsSchema from './properties/watchOptions'
1313
import devServerSchema from './properties/devServer'
1414
import { absolutePath } from './types'
15+
import _merge from 'lodash/merge'
16+
import sh from 'shelljs'
1517

16-
const schema = Joi.object({
17-
amd: Joi.object(),
18-
bail: Joi.boolean(),
19-
cache: Joi.boolean(),
20-
context: contextSchema,
21-
debug: Joi.boolean(),
22-
devServer: devServerSchema,
23-
devtool: devtoolSchema,
24-
entry: entrySchema,
25-
externals: externalsSchema,
26-
loader: Joi.any(), // ?
27-
module: moduleSchema,
28-
node: nodeSchema,
29-
output: outputSchema,
30-
plugins: pluginsSchema,
31-
profile: Joi.boolean(),
32-
recordsInputPath: absolutePath,
33-
recordsOutputPath: absolutePath,
34-
recordsPath: absolutePath,
35-
resolve: resolveSchema,
36-
resolveLoader: resolveSchema.concat(Joi.object({
37-
moduleTemplates: Joi.array().items(Joi.string()),
38-
})),
39-
watchOptions: watchOptionsSchema,
40-
stats: Joi.any(), // TODO
41-
target: Joi.any(), // TODO
18+
sh.config.silent = true
4219

43-
// Plugins
44-
postcss: Joi.any(),
45-
eslint: Joi.any(),
46-
tslint: Joi.any(),
47-
metadata: Joi.any(),
48-
})//.unknown()
20+
const makeSchema = (schemaOptions, schemaExtension) => {
21+
const resolveSchema = resolveSchemaFn(schemaOptions)
4922

23+
const schema = Joi.object({
24+
amd: Joi.object(),
25+
bail: Joi.boolean(),
26+
cache: Joi.boolean(),
27+
context: contextSchema,
28+
debug: Joi.boolean(),
29+
devServer: devServerSchema,
30+
devtool: devtoolSchema,
31+
entry: entrySchema,
32+
externals: externalsSchema,
33+
loader: Joi.any(), // ?
34+
module: moduleSchema,
35+
node: nodeSchema,
36+
output: outputSchema,
37+
plugins: pluginsSchema,
38+
profile: Joi.boolean(),
39+
recordsInputPath: absolutePath,
40+
recordsOutputPath: absolutePath,
41+
recordsPath: absolutePath,
42+
resolve: resolveSchema,
43+
resolveLoader: resolveSchema.concat(Joi.object({
44+
moduleTemplates: Joi.array().items(Joi.string()),
45+
})),
46+
watchOptions: watchOptionsSchema,
47+
stats: Joi.any(), // TODO
48+
target: Joi.any(), // TODO
49+
50+
// Plugins
51+
postcss: Joi.any(),
52+
eslint: Joi.any(),
53+
tslint: Joi.any(),
54+
metadata: Joi.any(),
55+
})
56+
return schemaExtension ? schema.concat(schemaExtension) : schema
57+
}
58+
59+
const defaultSchemaOptions = {
60+
rules: {
61+
'no-root-files-node-modules-nameclash': true,
62+
},
63+
}
5064

5165
// Easier consumability for require (default use case for non-transpiled webpack configs)
52-
module.exports = function validate(config, schema_ = schema, options = {}) {
66+
module.exports = function validate(config, options = {}) {
5367
const {
5468
// Don't return the config object and throw on error, but just return the validation result
5569
returnValidation, // bool
5670
quiet, // bool
71+
schema: overrideSchema, // Don't take internal schema, but override with this one
72+
schemaExtension, // Internal schema will be `Joi.concat`-ted with this schema if supplied
73+
rules,
5774
} = options
5875

59-
const validationResult = Joi.validate(config, schema_, { abortEarly: false })
76+
const schemaOptions = _merge(defaultSchemaOptions, { rules })
77+
78+
const schema = overrideSchema || makeSchema(schemaOptions, schemaExtension)
79+
80+
const validationResult = Joi.validate(config, schema, { abortEarly: false })
81+
validationResult.schemaOptions = schemaOptions // Mainly for having sth to assert on right now
82+
6083
if (returnValidation) return validationResult
6184

6285
if (validationResult.error) {
@@ -70,4 +93,5 @@ module.exports = function validate(config, schema_ = schema, options = {}) {
7093

7194
return config
7295
}
73-
module.exports.schema = schema
96+
97+
module.exports.Joi = Joi

src/index.test.js

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sinon from 'sinon'
22
import configs from '../test/passing-configs'
3-
import validate from './'
3+
import failingConfigs from '../test/failing-configs'
4+
import validate, { Joi } from './'
45

56
describe('.', () => {
67
let sandbox
@@ -33,20 +34,19 @@ describe('.', () => {
3334
})
3435
})
3536

36-
it('for an invalid config the joi validation result is printed to console.error and ' +
37-
'the process exits with exit code 1', () => {
38-
const invalidConfig = { resolvee: 'bar' }
39-
validate(invalidConfig)
40-
41-
// The error message should have been printed
42-
assert(consoleErrorStub.callCount === 1)
37+
failingConfigs.forEach(({ config, name }) => {
38+
it(`throws for ${name}`, () => {
39+
validate(config)
40+
// The error message should have been printed
41+
assert(consoleErrorStub.callCount === 1)
4342

44-
// process.exit should have been called
45-
assert(processExitStub.callCount === 1)
43+
// process.exit should have been called
44+
assert(processExitStub.callCount === 1)
45+
})
4646
})
4747

4848
it('should allow console output to be muted', () => {
49-
validate({}, {}, { quiet: true })
49+
validate({}, { quiet: true })
5050

5151
// The success message should not have been printed
5252
assert(consoleInfoStub.callCount === 0)
@@ -58,4 +58,38 @@ describe('.', () => {
5858
// process.exit should not have been called
5959
assert(processExitStub.callCount === 0)
6060
})
61+
62+
const fooSchema = Joi.object({ foo: Joi.string() })
63+
64+
it('should allow the schema to be extended', () => {
65+
const result1 = validate({ foo: 'bar' }, { returnValidation: true })
66+
const result2 = validate({ foo: 'bar' }, {
67+
returnValidation: true,
68+
schemaExtension: fooSchema,
69+
})
70+
assert(result1.error)
71+
assert(!result2.error)
72+
})
73+
74+
it('should allow the schema to be overridden', () => {
75+
const result = validate({ foo: 'bar' }, {
76+
schema: fooSchema,
77+
returnValidation: true,
78+
})
79+
assert(!result.error)
80+
})
81+
82+
it('should allow overriding rules', () => {
83+
const result = validate({ foo: 'bar' }, {
84+
rules: {
85+
foo: true,
86+
'no-root-files-node-modules-nameclash': false,
87+
},
88+
returnValidation: true,
89+
})
90+
assert(result.schemaOptions.rules.foo)
91+
assert(result.schemaOptions.rules['no-root-files-node-modules-nameclash'] === false)
92+
// Will be merged with default rules, so length def greater then 1
93+
assert(Object.keys(result.schemaOptions.rules).length > 1)
94+
})
6195
})

src/properties/context/index.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { allValid, allInvalid } from '../../../test/utils'
33

44
const validModuleConfigs = [
55
// #0
6-
'/home/jwerner/foo/entry.js', // Absolute
6+
{ input: 'exists' }, // Absolute
77
]
88

99
const invalidModuleConfigs = [
1010
// #0
1111
// Relative
12-
{ input: './entry.js', error: { message: '"value" must be an absolute path' } },
12+
{ input: './entry.js', error: { type: 'path.absolute' } },
1313

1414
// #1
1515
{ input: 1, error: { message: '"value" must be a string' } },

0 commit comments

Comments
 (0)