UNPKG

10.9 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Gajus Kuizinas @gajus
4*/
5"use strict";
6
7const WebpackError = require("./WebpackError");
8const webpackOptionsSchema = require("../schemas/WebpackOptions.json");
9
10const getSchemaPart = (path, parents, additionalPath) => {
11 parents = parents || 0;
12 path = path.split("/");
13 path = path.slice(0, path.length - parents);
14 if (additionalPath) {
15 additionalPath = additionalPath.split("/");
16 path = path.concat(additionalPath);
17 }
18 let schemaPart = webpackOptionsSchema;
19 for (let i = 1; i < path.length; i++) {
20 const inner = schemaPart[path[i]];
21 if (inner) schemaPart = inner;
22 }
23 return schemaPart;
24};
25
26const getSchemaPartText = (schemaPart, additionalPath) => {
27 if (additionalPath) {
28 for (let i = 0; i < additionalPath.length; i++) {
29 const inner = schemaPart[additionalPath[i]];
30 if (inner) schemaPart = inner;
31 }
32 }
33 while (schemaPart.$ref) {
34 schemaPart = getSchemaPart(schemaPart.$ref);
35 }
36 let schemaText = WebpackOptionsValidationError.formatSchema(schemaPart);
37 if (schemaPart.description) {
38 schemaText += `\n-> ${schemaPart.description}`;
39 }
40 return schemaText;
41};
42
43const getSchemaPartDescription = schemaPart => {
44 while (schemaPart.$ref) {
45 schemaPart = getSchemaPart(schemaPart.$ref);
46 }
47 if (schemaPart.description) {
48 return `\n-> ${schemaPart.description}`;
49 }
50 return "";
51};
52
53const SPECIFICITY = {
54 type: 1,
55 oneOf: 1,
56 anyOf: 1,
57 allOf: 1,
58 additionalProperties: 2,
59 enum: 1,
60 instanceof: 1,
61 required: 2,
62 minimum: 2,
63 uniqueItems: 2,
64 minLength: 2,
65 minItems: 2,
66 minProperties: 2,
67 absolutePath: 2
68};
69
70const filterMax = (array, fn) => {
71 const max = array.reduce((max, item) => Math.max(max, fn(item)), 0);
72 return array.filter(item => fn(item) === max);
73};
74
75const filterChildren = children => {
76 children = filterMax(children, err =>
77 err.dataPath ? err.dataPath.length : 0
78 );
79 children = filterMax(children, err => SPECIFICITY[err.keyword] || 2);
80 return children;
81};
82
83const indent = (str, prefix, firstLine) => {
84 if (firstLine) {
85 return prefix + str.replace(/\n(?!$)/g, "\n" + prefix);
86 } else {
87 return str.replace(/\n(?!$)/g, `\n${prefix}`);
88 }
89};
90
91class WebpackOptionsValidationError extends WebpackError {
92 constructor(validationErrors) {
93 super(
94 "Invalid configuration object. " +
95 "Webpack has been initialised using a configuration object that does not match the API schema.\n" +
96 validationErrors
97 .map(
98 err =>
99 " - " +
100 indent(
101 WebpackOptionsValidationError.formatValidationError(err),
102 " ",
103 false
104 )
105 )
106 .join("\n")
107 );
108
109 this.name = "WebpackOptionsValidationError";
110 this.validationErrors = validationErrors;
111
112 Error.captureStackTrace(this, this.constructor);
113 }
114
115 static formatSchema(schema, prevSchemas) {
116 prevSchemas = prevSchemas || [];
117
118 const formatInnerSchema = (innerSchema, addSelf) => {
119 if (!addSelf) {
120 return WebpackOptionsValidationError.formatSchema(
121 innerSchema,
122 prevSchemas
123 );
124 }
125 if (prevSchemas.includes(innerSchema)) {
126 return "(recursive)";
127 }
128 return WebpackOptionsValidationError.formatSchema(
129 innerSchema,
130 prevSchemas.concat(schema)
131 );
132 };
133
134 if (schema.type === "string") {
135 if (schema.minLength === 1) {
136 return "non-empty string";
137 }
138 if (schema.minLength > 1) {
139 return `string (min length ${schema.minLength})`;
140 }
141 return "string";
142 }
143 if (schema.type === "boolean") {
144 return "boolean";
145 }
146 if (schema.type === "number") {
147 return "number";
148 }
149 if (schema.type === "object") {
150 if (schema.properties) {
151 const required = schema.required || [];
152 return `object { ${Object.keys(schema.properties)
153 .map(property => {
154 if (!required.includes(property)) return property + "?";
155 return property;
156 })
157 .concat(schema.additionalProperties ? ["…"] : [])
158 .join(", ")} }`;
159 }
160 if (schema.additionalProperties) {
161 return `object { <key>: ${formatInnerSchema(
162 schema.additionalProperties
163 )} }`;
164 }
165 return "object";
166 }
167 if (schema.type === "array") {
168 return `[${formatInnerSchema(schema.items)}]`;
169 }
170
171 switch (schema.instanceof) {
172 case "Function":
173 return "function";
174 case "RegExp":
175 return "RegExp";
176 }
177
178 if (schema.enum) {
179 return schema.enum.map(item => JSON.stringify(item)).join(" | ");
180 }
181
182 if (schema.$ref) {
183 return formatInnerSchema(getSchemaPart(schema.$ref), true);
184 }
185 if (schema.allOf) {
186 return schema.allOf.map(formatInnerSchema).join(" & ");
187 }
188 if (schema.oneOf) {
189 return schema.oneOf.map(formatInnerSchema).join(" | ");
190 }
191 if (schema.anyOf) {
192 return schema.anyOf.map(formatInnerSchema).join(" | ");
193 }
194 return JSON.stringify(schema, null, 2);
195 }
196
197 static formatValidationError(err) {
198 const dataPath = `configuration${err.dataPath}`;
199 if (err.keyword === "additionalProperties") {
200 const baseMessage = `${dataPath} has an unknown property '${
201 err.params.additionalProperty
202 }'. These properties are valid:\n${getSchemaPartText(err.parentSchema)}`;
203 if (!err.dataPath) {
204 switch (err.params.additionalProperty) {
205 case "debug":
206 return (
207 `${baseMessage}\n` +
208 "The 'debug' property was removed in webpack 2.0.0.\n" +
209 "Loaders should be updated to allow passing this option via loader options in module.rules.\n" +
210 "Until loaders are updated one can use the LoaderOptionsPlugin to switch loaders into debug mode:\n" +
211 "plugins: [\n" +
212 " new webpack.LoaderOptionsPlugin({\n" +
213 " debug: true\n" +
214 " })\n" +
215 "]"
216 );
217 }
218 return (
219 `${baseMessage}\n` +
220 "For typos: please correct them.\n" +
221 "For loader options: webpack >= v2.0.0 no longer allows custom properties in configuration.\n" +
222 " Loaders should be updated to allow passing options via loader options in module.rules.\n" +
223 " Until loaders are updated one can use the LoaderOptionsPlugin to pass these options to the loader:\n" +
224 " plugins: [\n" +
225 " new webpack.LoaderOptionsPlugin({\n" +
226 " // test: /\\.xxx$/, // may apply this only for some modules\n" +
227 " options: {\n" +
228 ` ${err.params.additionalProperty}: …\n` +
229 " }\n" +
230 " })\n" +
231 " ]"
232 );
233 }
234 return baseMessage;
235 } else if (err.keyword === "oneOf" || err.keyword === "anyOf") {
236 if (err.children && err.children.length > 0) {
237 if (err.schema.length === 1) {
238 const lastChild = err.children[err.children.length - 1];
239 const remainingChildren = err.children.slice(
240 0,
241 err.children.length - 1
242 );
243 return WebpackOptionsValidationError.formatValidationError(
244 Object.assign({}, lastChild, {
245 children: remainingChildren,
246 parentSchema: Object.assign(
247 {},
248 err.parentSchema,
249 lastChild.parentSchema
250 )
251 })
252 );
253 }
254 const children = filterChildren(err.children);
255 if (children.length === 1) {
256 return WebpackOptionsValidationError.formatValidationError(
257 children[0]
258 );
259 }
260 return (
261 `${dataPath} should be one of these:\n${getSchemaPartText(
262 err.parentSchema
263 )}\n` +
264 `Details:\n${children
265 .map(
266 err =>
267 " * " +
268 indent(
269 WebpackOptionsValidationError.formatValidationError(err),
270 " ",
271 false
272 )
273 )
274 .join("\n")}`
275 );
276 }
277 return `${dataPath} should be one of these:\n${getSchemaPartText(
278 err.parentSchema
279 )}`;
280 } else if (err.keyword === "enum") {
281 if (
282 err.parentSchema &&
283 err.parentSchema.enum &&
284 err.parentSchema.enum.length === 1
285 ) {
286 return `${dataPath} should be ${getSchemaPartText(err.parentSchema)}`;
287 }
288 return `${dataPath} should be one of these:\n${getSchemaPartText(
289 err.parentSchema
290 )}`;
291 } else if (err.keyword === "allOf") {
292 return `${dataPath} should be:\n${getSchemaPartText(err.parentSchema)}`;
293 } else if (err.keyword === "type") {
294 switch (err.params.type) {
295 case "object":
296 return `${dataPath} should be an object.${getSchemaPartDescription(
297 err.parentSchema
298 )}`;
299 case "string":
300 return `${dataPath} should be a string.${getSchemaPartDescription(
301 err.parentSchema
302 )}`;
303 case "boolean":
304 return `${dataPath} should be a boolean.${getSchemaPartDescription(
305 err.parentSchema
306 )}`;
307 case "number":
308 return `${dataPath} should be a number.${getSchemaPartDescription(
309 err.parentSchema
310 )}`;
311 case "array":
312 return `${dataPath} should be an array:\n${getSchemaPartText(
313 err.parentSchema
314 )}`;
315 }
316 return `${dataPath} should be ${err.params.type}:\n${getSchemaPartText(
317 err.parentSchema
318 )}`;
319 } else if (err.keyword === "instanceof") {
320 return `${dataPath} should be an instance of ${getSchemaPartText(
321 err.parentSchema
322 )}`;
323 } else if (err.keyword === "required") {
324 const missingProperty = err.params.missingProperty.replace(/^\./, "");
325 return `${dataPath} misses the property '${missingProperty}'.\n${getSchemaPartText(
326 err.parentSchema,
327 ["properties", missingProperty]
328 )}`;
329 } else if (err.keyword === "minimum") {
330 return `${dataPath} ${err.message}.${getSchemaPartDescription(
331 err.parentSchema
332 )}`;
333 } else if (err.keyword === "uniqueItems") {
334 return `${dataPath} should not contain the item '${
335 err.data[err.params.i]
336 }' twice.${getSchemaPartDescription(err.parentSchema)}`;
337 } else if (
338 err.keyword === "minLength" ||
339 err.keyword === "minItems" ||
340 err.keyword === "minProperties"
341 ) {
342 if (err.params.limit === 1) {
343 switch (err.keyword) {
344 case "minLength":
345 return `${dataPath} should be an non-empty string.${getSchemaPartDescription(
346 err.parentSchema
347 )}`;
348 case "minItems":
349 return `${dataPath} should be an non-empty array.${getSchemaPartDescription(
350 err.parentSchema
351 )}`;
352 case "minProperties":
353 return `${dataPath} should be an non-empty object.${getSchemaPartDescription(
354 err.parentSchema
355 )}`;
356 }
357 return `${dataPath} should be not empty.${getSchemaPartDescription(
358 err.parentSchema
359 )}`;
360 } else {
361 return `${dataPath} ${err.message}${getSchemaPartDescription(
362 err.parentSchema
363 )}`;
364 }
365 } else if (err.keyword === "absolutePath") {
366 const baseMessage = `${dataPath}: ${
367 err.message
368 }${getSchemaPartDescription(err.parentSchema)}`;
369 if (dataPath === "configuration.output.filename") {
370 return (
371 `${baseMessage}\n` +
372 "Please use output.path to specify absolute path and output.filename for the file name."
373 );
374 }
375 return baseMessage;
376 } else {
377 return `${dataPath} ${err.message} (${JSON.stringify(
378 err,
379 null,
380 2
381 )}).\n${getSchemaPartText(err.parentSchema)}`;
382 }
383 }
384}
385
386module.exports = WebpackOptionsValidationError;