1 |
|
2 |
|
3 |
|
4 |
|
5 | "use strict";
|
6 |
|
7 | const WebpackError = require("./WebpackError");
|
8 | const webpackOptionsSchema = require("../schemas/WebpackOptions.json");
|
9 |
|
10 | const 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 |
|
26 | const 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 |
|
43 | const 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 |
|
53 | const 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 |
|
70 | const 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 |
|
75 | const 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 |
|
83 | const 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 |
|
91 | class 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 |
|
386 | module.exports = WebpackOptionsValidationError;
|