Skip to content

Commit ac1675c

Browse files
committed
Refactor config validation
this makes it match more with the validation in webpack/webpack. Also refactored it to ES6.
1 parent d725fd9 commit ac1675c

File tree

2 files changed

+113
-102
lines changed

2 files changed

+113
-102
lines changed

lib/OptionsValidationError.js

Lines changed: 107 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -2,69 +2,7 @@
22

33
const optionsSchema = require("./optionsSchema.json");
44

5-
function OptionsValidationError(validationErrors) {
6-
Error.call(this);
7-
Error.captureStackTrace(this, OptionsValidationError);
8-
this.name = "WebpackDevServerOptionsValidationError";
9-
this.message = `${"Invalid configuration object. " +
10-
"webpack-dev-server has been initialised using a configuration object that does not match the API schema.\n"}${
11-
validationErrors.map(function(err) {
12-
return ` - ${indent(OptionsValidationError.formatValidationError(err), " ", false)}`;
13-
}).join("\n")}`;
14-
this.validationErrors = validationErrors;
15-
}
16-
module.exports = OptionsValidationError;
17-
18-
OptionsValidationError.prototype = Object.create(Error.prototype);
19-
OptionsValidationError.prototype.constructor = OptionsValidationError;
20-
21-
OptionsValidationError.formatValidationError = function formatValidationError(err) {
22-
const dataPath = `configuration${err.dataPath}`;
23-
switch(err.keyword) {
24-
case "additionalProperties":
25-
return `${dataPath} has an unknown property '${err.params.additionalProperty}'. These properties are valid:\n${
26-
getSchemaPartText(err.parentSchema)}`;
27-
case "oneOf":
28-
case "anyOf":
29-
case "enum":
30-
return `${dataPath} should be one of these:\n${
31-
getSchemaPartText(err.parentSchema)}`;
32-
case "allOf":
33-
return `${dataPath} should be:\n${
34-
getSchemaPartText(err.parentSchema)}`;
35-
case "type":
36-
switch(err.params.type) {
37-
case "object":
38-
return `${dataPath} should be an object.`;
39-
case "array":
40-
return `${dataPath} should be an array.`;
41-
case "string":
42-
return `${dataPath} should be a string.`;
43-
case "boolean":
44-
return `${dataPath} should be a boolean.`;
45-
case "number":
46-
return `${dataPath} should be a number.`;
47-
}
48-
return `${dataPath} should be ${err.params.type}:\n${
49-
getSchemaPartText(err.parentSchema)}`;
50-
case "instanceof":
51-
return `${dataPath} should be an instance of ${getSchemaPartText(err.parentSchema)}.`;
52-
case "required": // eslint-disable-line no-case-declarations
53-
const missingProperty = err.params.missingProperty.replace(/^\./, "");
54-
return `${dataPath} misses the property '${missingProperty}'.\n${
55-
getSchemaPartText(err.parentSchema, ["properties", missingProperty])}`;
56-
case "minLength":
57-
if(err.params.limit === 1)
58-
return `${dataPath} should not be empty.`;
59-
else
60-
return `${dataPath} ${err.message}`;
61-
default:
62-
return `${dataPath} ${err.message} (${JSON.stringify(err, 0, 2)}).\n${
63-
getSchemaPartText(err.parentSchema)}`;
64-
}
65-
}
66-
67-
function getSchemaPart(path, parents, additionalPath) {
5+
const getSchemaPart = (path, parents, additionalPath) => {
686
parents = parents || 0;
697
path = path.split("/");
708
path = path.slice(0, path.length - parents);
@@ -79,9 +17,9 @@ function getSchemaPart(path, parents, additionalPath) {
7917
schemaPart = inner;
8018
}
8119
return schemaPart;
82-
}
20+
};
8321

84-
function getSchemaPartText(schemaPart, additionalPath) {
22+
const getSchemaPartText = (schemaPart, additionalPath) => {
8523
if(additionalPath) {
8624
for(let i = 0; i < additionalPath.length; i++) {
8725
const inner = schemaPart[additionalPath[i]];
@@ -94,60 +32,128 @@ function getSchemaPartText(schemaPart, additionalPath) {
9432
if(schemaPart.description)
9533
schemaText += `\n${schemaPart.description}`;
9634
return schemaText;
97-
}
35+
};
9836

99-
function formatSchema(schema, prevSchemas) {
100-
prevSchemas = prevSchemas || [];
37+
const indent = (str, prefix, firstLine) => {
38+
if(firstLine) {
39+
return prefix + str.replace(/\n(?!$)/g, "\n" + prefix);
40+
} else {
41+
return str.replace(/\n(?!$)/g, `\n${prefix}`);
42+
}
43+
};
44+
45+
class OptionsValidationError extends Error {
46+
47+
constructor(validationErrors) {
48+
super();
49+
50+
if(Error.hasOwnProperty("captureStackTrace")) {
51+
Error.captureStackTrace(this, this.constructor);
52+
}
53+
this.name = "WebpackDevServerOptionsValidationError";
10154

102-
function formatInnerSchema(innerSchema, addSelf) {
103-
if(!addSelf) return formatSchema(innerSchema, prevSchemas);
104-
if(prevSchemas.indexOf(innerSchema) >= 0) return "(recursive)";
105-
return formatSchema(innerSchema, prevSchemas.concat(schema));
55+
this.message = "Invalid configuration object. " +
56+
"webpack-dev-server has been initialised using a configuration object that does not match the API schema.\n" +
57+
validationErrors.map(err => " - " + indent(OptionsValidationError.formatValidationError(err), " ", false)).join("\n");
58+
this.validationErrors = validationErrors;
10659
}
107-
switch(schema.type) {
108-
case "string":
60+
61+
static formatSchema(schema, prevSchemas) {
62+
prevSchemas = prevSchemas || [];
63+
64+
const formatInnerSchema = (innerSchema, addSelf) => {
65+
if(!addSelf) return OptionsValidationError.formatSchema(innerSchema, prevSchemas);
66+
if(prevSchemas.indexOf(innerSchema) >= 0) return "(recursive)";
67+
return OptionsValidationError.formatSchema(innerSchema, prevSchemas.concat(schema));
68+
};
69+
70+
if(schema.type === "string") {
71+
if(schema.minLength === 1)
72+
return "non-empty string";
73+
else if(schema.minLength > 1)
74+
return `string (min length ${schema.minLength})`;
10975
return "string";
110-
case "boolean":
76+
} else if(schema.type === "boolean") {
11177
return "boolean";
112-
case "number":
78+
} else if(schema.type === "number") {
11379
return "number";
114-
case "object":
80+
} else if(schema.type === "object") {
11581
if(schema.properties) {
11682
const required = schema.required || [];
117-
return `object { ${Object.keys(schema.properties).map(function(property) {
118-
if(required.indexOf(property) < 0) return `${property}?`;
83+
return `object { ${Object.keys(schema.properties).map(property => {
84+
if(required.indexOf(property) < 0) return property + "?";
11985
return property;
12086
}).concat(schema.additionalProperties ? ["..."] : []).join(", ")} }`;
12187
}
12288
if(schema.additionalProperties) {
12389
return `object { <key>: ${formatInnerSchema(schema.additionalProperties)} }`;
12490
}
12591
return "object";
126-
case "array":
92+
} else if(schema.type === "array") {
12793
return `[${formatInnerSchema(schema.items)}]`;
94+
}
95+
96+
switch(schema.instanceof) {
97+
case "Function":
98+
return "function";
99+
case "RegExp":
100+
return "RegExp";
101+
}
102+
if(schema.$ref) return formatInnerSchema(getSchemaPart(schema.$ref), true);
103+
if(schema.allOf) return schema.allOf.map(formatInnerSchema).join(" & ");
104+
if(schema.oneOf) return schema.oneOf.map(formatInnerSchema).join(" | ");
105+
if(schema.anyOf) return schema.anyOf.map(formatInnerSchema).join(" | ");
106+
if(schema.enum) return schema.enum.map(item => JSON.stringify(item)).join(" | ");
107+
return JSON.stringify(schema, 0, 2);
128108
}
129-
switch(schema.instanceof) {
130-
case "Function":
131-
return "function";
132-
case "RegExp":
133-
return "RegExp";
134-
}
135-
if(schema.$ref) return formatInnerSchema(getSchemaPart(schema.$ref), true);
136-
if(schema.allOf) return schema.allOf.map(formatInnerSchema).join(" & ");
137-
if(schema.oneOf) return schema.oneOf.map(formatInnerSchema).join(" | ");
138-
if(schema.anyOf) return schema.anyOf.map(formatInnerSchema).join(" | ");
139-
if(schema.enum) return schema.enum.map(function(item) {
140-
return JSON.stringify(item);
141-
}).join(" | ");
142-
return JSON.stringify(schema, 0, 2);
143-
}
144109

145-
function indent(str, prefix, firstLine) {
146-
if(firstLine) {
147-
return prefix + str.replace(/\n(?!$)/g, `\n${prefix}`);
148-
} else {
149-
return str.replace(/\n(?!$)/g, `\n${prefix}`);
110+
static formatValidationError(err) {
111+
const dataPath = `configuration${err.dataPath}`;
112+
if(err.keyword === "additionalProperties") {
113+
return `${dataPath} has an unknown property '${err.params.additionalProperty}'. These properties are valid:\n${getSchemaPartText(err.parentSchema)}`;
114+
} else if(err.keyword === "oneOf" || err.keyword === "anyOf") {
115+
if(err.children && err.children.length > 0) {
116+
return `${dataPath} should be one of these:\n${getSchemaPartText(err.parentSchema)}\n` +
117+
`Details:\n${err.children.map(err => " * " + indent(OptionsValidationError.formatValidationError(err), " ", false)).join("\n")}`;
118+
}
119+
return `${dataPath} should be one of these:\n${getSchemaPartText(err.parentSchema)}`;
120+
121+
} else if(err.keyword === "enum") {
122+
if(err.parentSchema && err.parentSchema.enum && err.parentSchema.enum.length === 1) {
123+
return `${dataPath} should be ${getSchemaPartText(err.parentSchema)}`;
124+
}
125+
return `${dataPath} should be one of these:\n${getSchemaPartText(err.parentSchema)}`;
126+
} else if(err.keyword === "allOf") {
127+
return `${dataPath} should be:\n${getSchemaPartText(err.parentSchema)}`;
128+
} else if(err.keyword === "type") {
129+
switch(err.params.type) {
130+
case "object":
131+
return `${dataPath} should be an object.`;
132+
case "string":
133+
return `${dataPath} should be a string.`;
134+
case "boolean":
135+
return `${dataPath} should be a boolean.`;
136+
case "number":
137+
return `${dataPath} should be a number.`;
138+
case "array":
139+
return `${dataPath} should be an array:\n${getSchemaPartText(err.parentSchema)}`;
140+
}
141+
return `${dataPath} should be ${err.params.type}:\n${getSchemaPartText(err.parentSchema)}`;
142+
} else if(err.keyword === "instanceof") {
143+
return `${dataPath} should be an instance of ${getSchemaPartText(err.parentSchema)}.`;
144+
} else if(err.keyword === "required") {
145+
const missingProperty = err.params.missingProperty.replace(/^\./, "");
146+
return `${dataPath} misses the property '${missingProperty}'.\n${getSchemaPartText(err.parentSchema, ["properties", missingProperty])}`;
147+
} else if(err.keyword === "minLength" || err.keyword === "minItems") {
148+
if(err.params.limit === 1)
149+
return `${dataPath} should not be empty.`;
150+
else
151+
return `${dataPath} ${err.message}`;
152+
} else {
153+
// eslint-disable-line no-fallthrough
154+
return `${dataPath} ${err.message} (${JSON.stringify(err, 0, 2)}).\n${getSchemaPartText(err.parentSchema)}`;
155+
}
150156
}
151157
}
152158

153-
OptionsValidationError.formatSchema = formatSchema;
159+
module.exports = OptionsValidationError;

test/Validation.test.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ describe("Validation", function() {
2828
message: [
2929
" - configuration.contentBase should be one of these:",
3030
" [string] | false | number | string",
31-
" A directory to serve files non-webpack files from."
31+
" A directory to serve files non-webpack files from.",
32+
" Details:",
33+
" * configuration.contentBase[0] should be a string.",
34+
" * configuration.contentBase should be false",
35+
" * configuration.contentBase should be a number.",
36+
" * configuration.contentBase should be a string."
3237
]
3338
}, {
3439
name: "non-existing key configuration",

0 commit comments

Comments
 (0)