Skip to content

Commit bda7369

Browse files
authored
refactor: replace joi with superstruct (#188)
* Replace joi with superstruct Joi has a poor documentation and hard to find changelog in issues. In this diff I migrated to suggested by @shellscape "superstruct" which does not have dependencies and simpler to extend. * Remove log
1 parent 6a919ef commit bda7369

File tree

5 files changed

+117
-171
lines changed

5 files changed

+117
-171
lines changed

lib/validate.js

Lines changed: 42 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,73 +8,60 @@
88
The above copyright notice and this permission notice shall be
99
included in all copies or substantial portions of this Source Code Form.
1010
*/
11-
const Joi = require('@hapi/joi');
11+
const {
12+
refinement,
13+
any,
14+
nullable,
15+
type,
16+
partial,
17+
array,
18+
union,
19+
number,
20+
boolean,
21+
string,
22+
func,
23+
literal,
24+
never,
25+
validate
26+
} = require('superstruct');
1227
const isPromise = require('is-promise');
1328

14-
// until https://github.com/hapijs/joi/issues/1690 is fixed, we have to have a base of any(). once
15-
// they get that fixed, we can use object() here, which is what we should be using
16-
const joi = Joi.extend((base) => {
17-
return {
18-
base,
19-
language: {
20-
promise: 'must be a promise-like object'
21-
},
22-
name: 'any',
23-
rules: [
24-
{
25-
name: 'promise',
26-
validate(params, value, state, options) {
27-
if (!isPromise(value)) {
28-
return this.createError('any.promise', { v: value }, state, options);
29-
}
29+
const promise = refinement(any(), 'promise-like', (value) => isPromise(value));
3030

31-
return value;
32-
}
33-
}
34-
]
35-
};
36-
});
37-
38-
// until https://github.com/hapijs/joi/issues/1691 is fixed, we have to use `any().allow()`,
39-
// `any().forbidden()` and `any().promise()`
40-
const { any, array, boolean, func, number, object, string, validate } = joi.bind();
31+
const port = refinement(number(), 'port', (value) => Number.isInteger(value) && value <= 65535);
4132

4233
module.exports = {
4334
validate(options) {
44-
const keys = {
35+
const schema = partial({
4536
allowMany: boolean(),
46-
// get it together, Prettier! https://github.com/prettier/prettier/issues/3621
47-
// prettier-ignore
48-
client: [object().keys({ address: string(), retry: boolean(), silent: boolean() }).allow(null)],
49-
compress: [boolean().allow(null)],
50-
headers: object().allow(null),
51-
historyFallback: [boolean(), object()],
37+
client: partial({
38+
address: string(),
39+
retry: boolean(),
40+
silent: boolean()
41+
}),
42+
compress: nullable(boolean()),
43+
headers: nullable(type({})),
44+
historyFallback: union([boolean(), type({})]),
5245
hmr: boolean(),
53-
// prettier-ignore
54-
host: [any().promise().allow(null), string()],
55-
http2: [boolean(), object()],
56-
https: object().allow(null),
46+
host: nullable(union([promise, string()])),
47+
http2: union([boolean(), type({})]),
48+
https: nullable(type({})),
5749
liveReload: boolean(),
58-
log: object().keys({ level: string(), timestamp: boolean() }),
50+
log: partial({ level: string(), timestamp: boolean() }),
5951
middleware: func(),
60-
open: [boolean(), object()],
61-
// prettier-ignore
62-
port: [number().integer().max(65535), any().promise()],
63-
progress: [boolean(), string().valid('minimal')],
64-
ramdisk: [boolean(), object()],
65-
secure: any().forbidden(),
66-
// prettier-ignore
67-
static: [
68-
string().allow(null),
69-
array().items(string()),
70-
object().keys({ glob: array().items(string()), options: object() })
71-
],
52+
open: union([boolean(), type({})]),
53+
port: union([port, promise]),
54+
progress: union([boolean(), literal('minimal')]),
55+
ramdisk: union([boolean(), type({})]),
56+
secure: never(),
57+
static: nullable(
58+
union([string(), array(string()), partial({ glob: array(string()), options: type({}) })])
59+
),
7260
status: boolean(),
7361
waitForBuild: boolean()
74-
};
75-
const schema = object().keys(keys);
76-
const results = validate(options, schema);
62+
});
63+
const [error, value] = validate(options, schema);
7764

78-
return results;
65+
return { error, value };
7966
}
8067
};

package-lock.json

Lines changed: 38 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
"webpack": "^4.20.2"
3939
},
4040
"dependencies": {
41-
"@hapi/joi": "^15.1.0",
4241
"chalk": "^4.0.0",
4342
"connect-history-api-fallback": "^1.5.0",
4443
"globby": "^11.0.0",
@@ -58,6 +57,7 @@
5857
"read-pkg-up": "^7.0.1",
5958
"rimraf": "^3.0.2",
6059
"strip-ansi": "^6.0.0",
60+
"superstruct": "^0.10.12",
6161
"webpack-plugin-ramdisk": "^0.1.2",
6262
"ws": "^7.1.0"
6363
},

test/snapshots/validate.test.js.md

Lines changed: 36 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -8,76 +8,47 @@ Generated by [AVA](https://ava.li).
88

99
> Snapshot 1
1010
11-
ValidationError (Error) {
12-
_object: {
13-
foo: 'bar',
14-
},
15-
annotate: Function {},
16-
details: [
11+
TypeError (StructError) {
12+
branch: [
1713
{
18-
context: {
19-
child: 'foo',
20-
key: 'foo',
21-
label: 'foo',
22-
value: 'bar',
23-
},
24-
message: '"foo" is not allowed',
25-
path: [
26-
'foo',
27-
],
28-
type: 'object.allowUnknown',
14+
foo: 'bar',
2915
},
16+
'bar',
17+
],
18+
failures: GeneratorFunction failures {},
19+
path: [
20+
'foo',
3021
],
31-
isJoi: true,
32-
message: '"foo" is not allowed',
22+
type: 'never',
23+
value: 'bar',
24+
message: 'Expected a value of type `never` for `foo` but received `"bar"`.',
3325
}
3426

3527
## promise
3628

3729
> Snapshot 1
3830
39-
ValidationError (Error) {
40-
_object: {
41-
host: 0,
42-
port: '0',
43-
},
44-
annotate: Function {},
45-
details: [
46-
{
47-
context: {
48-
key: 'host',
49-
label: 'host',
50-
v: 0,
51-
},
52-
message: '"host" must be a promise-like object',
53-
path: [
54-
'host',
55-
],
56-
type: 'any.promise',
57-
},
31+
TypeError (StructError) {
32+
branch: [
5833
{
59-
context: {
60-
key: 'host',
61-
label: 'host',
62-
value: 0,
63-
},
64-
message: '"host" must be a string',
65-
path: [
66-
'host',
67-
],
68-
type: 'string.base',
34+
host: 0,
35+
port: '0',
6936
},
37+
0,
38+
],
39+
failures: GeneratorFunction failures {},
40+
path: [
41+
'host',
7042
],
71-
isJoi: true,
72-
message: 'child "host" fails because ["host" must be a promise-like object, "host" must be a string]',
43+
type: 'promise-like | string',
44+
value: 0,
45+
message: 'Expected a value of type `promise-like | string` for `host` but received `0`.',
7346
}
7447

7548
> Snapshot 2
7649
7750
{
78-
catch: Function catch {},
79-
error: null,
80-
then: Function then {},
51+
error: undefined,
8152
value: {
8253
host: Promise {},
8354
port: Promise {},
@@ -87,9 +58,7 @@ Generated by [AVA](https://ava.li).
8758
> Snapshot 3
8859
8960
{
90-
catch: Function catch {},
91-
error: null,
92-
then: Function then {},
61+
error: undefined,
9362
value: {
9463
host: {
9564
then: Function then {},
@@ -104,26 +73,18 @@ Generated by [AVA](https://ava.li).
10473

10574
> Snapshot 1
10675
107-
ValidationError (Error) {
108-
_object: {
109-
batman: 'nanananana',
110-
},
111-
annotate: Function {},
112-
details: [
76+
TypeError (StructError) {
77+
branch: [
11378
{
114-
context: {
115-
child: 'batman',
116-
key: 'batman',
117-
label: 'batman',
118-
value: 'nanananana',
119-
},
120-
message: '"batman" is not allowed',
121-
path: [
122-
'batman',
123-
],
124-
type: 'object.allowUnknown',
79+
batman: 'nanananana',
12580
},
81+
'nanananana',
82+
],
83+
failures: GeneratorFunction failures {},
84+
path: [
85+
'batman',
12686
],
127-
isJoi: true,
128-
message: '"batman" is not allowed',
87+
type: 'never',
88+
value: 'nanananana',
89+
message: 'Expected a value of type `never` for `batman` but received `"nanananana"`.',
12990
}

test/snapshots/validate.test.js.snap

-286 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)