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 filterChildren = children => {
|
54 | return children.filter(
|
55 | err =>
|
56 | err.keyword !== "anyOf" &&
|
57 | err.keyword !== "allOf" &&
|
58 | err.keyword !== "oneOf"
|
59 | );
|
60 | };
|
61 |
|
62 | const indent = (str, prefix, firstLine) => {
|
63 | if (firstLine) {
|
64 | return prefix + str.replace(/\n(?!$)/g, "\n" + prefix);
|
65 | } else {
|
66 | return str.replace(/\n(?!$)/g, `\n${prefix}`);
|
67 | }
|
68 | };
|
69 |
|
70 | class WebpackOptionsValidationError extends WebpackError {
|
71 | constructor(validationErrors) {
|
72 | super(
|
73 | "Invalid configuration object. " +
|
74 | "Webpack has been initialised using a configuration object that does not match the API schema.\n" +
|
75 | validationErrors
|
76 | .map(
|
77 | err =>
|
78 | " - " +
|
79 | indent(
|
80 | WebpackOptionsValidationError.formatValidationError(err),
|
81 | " ",
|
82 | false
|
83 | )
|
84 | )
|
85 | .join("\n")
|
86 | );
|
87 |
|
88 | this.name = "WebpackOptionsValidationError";
|
89 | this.validationErrors = validationErrors;
|
90 |
|
91 | Error.captureStackTrace(this, this.constructor);
|
92 | }
|
93 |
|
94 | static formatSchema(schema, prevSchemas) {
|
95 | prevSchemas = prevSchemas || [];
|
96 |
|
97 | const formatInnerSchema = (innerSchema, addSelf) => {
|
98 | if (!addSelf) {
|
99 | return WebpackOptionsValidationError.formatSchema(
|
100 | innerSchema,
|
101 | prevSchemas
|
102 | );
|
103 | }
|
104 | if (prevSchemas.includes(innerSchema)) {
|
105 | return "(recursive)";
|
106 | }
|
107 | return WebpackOptionsValidationError.formatSchema(
|
108 | innerSchema,
|
109 | prevSchemas.concat(schema)
|
110 | );
|
111 | };
|
112 |
|
113 | if (schema.type === "string") {
|
114 | if (schema.minLength === 1) {
|
115 | return "non-empty string";
|
116 | }
|
117 | if (schema.minLength > 1) {
|
118 | return `string (min length ${schema.minLength})`;
|
119 | }
|
120 | return "string";
|
121 | }
|
122 | if (schema.type === "boolean") {
|
123 | return "boolean";
|
124 | }
|
125 | if (schema.type === "number") {
|
126 | return "number";
|
127 | }
|
128 | if (schema.type === "object") {
|
129 | if (schema.properties) {
|
130 | const required = schema.required || [];
|
131 | return `object { ${Object.keys(schema.properties)
|
132 | .map(property => {
|
133 | if (!required.includes(property)) return property + "?";
|
134 | return property;
|
135 | })
|
136 | .concat(schema.additionalProperties ? ["…"] : [])
|
137 | .join(", ")} }`;
|
138 | }
|
139 | if (schema.additionalProperties) {
|
140 | return `object { <key>: ${formatInnerSchema(
|
141 | schema.additionalProperties
|
142 | )} }`;
|
143 | }
|
144 | return "object";
|
145 | }
|
146 | if (schema.type === "array") {
|
147 | return `[${formatInnerSchema(schema.items)}]`;
|
148 | }
|
149 |
|
150 | switch (schema.instanceof) {
|
151 | case "Function":
|
152 | return "function";
|
153 | case "RegExp":
|
154 | return "RegExp";
|
155 | }
|
156 |
|
157 | if (schema.$ref) {
|
158 | return formatInnerSchema(getSchemaPart(schema.$ref), true);
|
159 | }
|
160 | if (schema.allOf) {
|
161 | return schema.allOf.map(formatInnerSchema).join(" & ");
|
162 | }
|
163 | if (schema.oneOf) {
|
164 | return schema.oneOf.map(formatInnerSchema).join(" | ");
|
165 | }
|
166 | if (schema.anyOf) {
|
167 | return schema.anyOf.map(formatInnerSchema).join(" | ");
|
168 | }
|
169 | if (schema.enum) {
|
170 | return schema.enum.map(item => JSON.stringify(item)).join(" | ");
|
171 | }
|
172 | return JSON.stringify(schema, null, 2);
|
173 | }
|
174 |
|
175 | static formatValidationError(err) {
|
176 | const dataPath = `configuration${err.dataPath}`;
|
177 | if (err.keyword === "additionalProperties") {
|
178 | const baseMessage = `${dataPath} has an unknown property '${
|
179 | err.params.additionalProperty
|
180 | }'. These properties are valid:\n${getSchemaPartText(err.parentSchema)}`;
|
181 | if (!err.dataPath) {
|
182 | switch (err.params.additionalProperty) {
|
183 | case "debug":
|
184 | return (
|
185 | `${baseMessage}\n` +
|
186 | "The 'debug' property was removed in webpack 2.0.0.\n" +
|
187 | "Loaders should be updated to allow passing this option via loader options in module.rules.\n" +
|
188 | "Until loaders are updated one can use the LoaderOptionsPlugin to switch loaders into debug mode:\n" +
|
189 | "plugins: [\n" +
|
190 | " new webpack.LoaderOptionsPlugin({\n" +
|
191 | " debug: true\n" +
|
192 | " })\n" +
|
193 | "]"
|
194 | );
|
195 | }
|
196 | return (
|
197 | `${baseMessage}\n` +
|
198 | "For typos: please correct them.\n" +
|
199 | "For loader options: webpack >= v2.0.0 no longer allows custom properties in configuration.\n" +
|
200 | " Loaders should be updated to allow passing options via loader options in module.rules.\n" +
|
201 | " Until loaders are updated one can use the LoaderOptionsPlugin to pass these options to the loader:\n" +
|
202 | " plugins: [\n" +
|
203 | " new webpack.LoaderOptionsPlugin({\n" +
|
204 | " // test: /\\.xxx$/, // may apply this only for some modules\n" +
|
205 | " options: {\n" +
|
206 | ` ${err.params.additionalProperty}: …\n` +
|
207 | " }\n" +
|
208 | " })\n" +
|
209 | " ]"
|
210 | );
|
211 | }
|
212 | return baseMessage;
|
213 | } else if (err.keyword === "oneOf" || err.keyword === "anyOf") {
|
214 | if (err.children && err.children.length > 0) {
|
215 | if (err.schema.length === 1) {
|
216 | const lastChild = err.children[err.children.length - 1];
|
217 | const remainingChildren = err.children.slice(
|
218 | 0,
|
219 | err.children.length - 1
|
220 | );
|
221 | return WebpackOptionsValidationError.formatValidationError(
|
222 | Object.assign({}, lastChild, {
|
223 | children: remainingChildren,
|
224 | parentSchema: Object.assign(
|
225 | {},
|
226 | err.parentSchema,
|
227 | lastChild.parentSchema
|
228 | )
|
229 | })
|
230 | );
|
231 | }
|
232 | return (
|
233 | `${dataPath} should be one of these:\n${getSchemaPartText(
|
234 | err.parentSchema
|
235 | )}\n` +
|
236 | `Details:\n${filterChildren(err.children)
|
237 | .map(
|
238 | err =>
|
239 | " * " +
|
240 | indent(
|
241 | WebpackOptionsValidationError.formatValidationError(err),
|
242 | " ",
|
243 | false
|
244 | )
|
245 | )
|
246 | .join("\n")}`
|
247 | );
|
248 | }
|
249 | return `${dataPath} should be one of these:\n${getSchemaPartText(
|
250 | err.parentSchema
|
251 | )}`;
|
252 | } else if (err.keyword === "enum") {
|
253 | if (
|
254 | err.parentSchema &&
|
255 | err.parentSchema.enum &&
|
256 | err.parentSchema.enum.length === 1
|
257 | ) {
|
258 | return `${dataPath} should be ${getSchemaPartText(err.parentSchema)}`;
|
259 | }
|
260 | return `${dataPath} should be one of these:\n${getSchemaPartText(
|
261 | err.parentSchema
|
262 | )}`;
|
263 | } else if (err.keyword === "allOf") {
|
264 | return `${dataPath} should be:\n${getSchemaPartText(err.parentSchema)}`;
|
265 | } else if (err.keyword === "type") {
|
266 | switch (err.params.type) {
|
267 | case "object":
|
268 | return `${dataPath} should be an object.${getSchemaPartDescription(
|
269 | err.parentSchema
|
270 | )}`;
|
271 | case "string":
|
272 | return `${dataPath} should be a string.${getSchemaPartDescription(
|
273 | err.parentSchema
|
274 | )}`;
|
275 | case "boolean":
|
276 | return `${dataPath} should be a boolean.${getSchemaPartDescription(
|
277 | err.parentSchema
|
278 | )}`;
|
279 | case "number":
|
280 | return `${dataPath} should be a number.${getSchemaPartDescription(
|
281 | err.parentSchema
|
282 | )}`;
|
283 | case "array":
|
284 | return `${dataPath} should be an array:\n${getSchemaPartText(
|
285 | err.parentSchema
|
286 | )}`;
|
287 | }
|
288 | return `${dataPath} should be ${err.params.type}:\n${getSchemaPartText(
|
289 | err.parentSchema
|
290 | )}`;
|
291 | } else if (err.keyword === "instanceof") {
|
292 | return `${dataPath} should be an instance of ${getSchemaPartText(
|
293 | err.parentSchema
|
294 | )}`;
|
295 | } else if (err.keyword === "required") {
|
296 | const missingProperty = err.params.missingProperty.replace(/^\./, "");
|
297 | return `${dataPath} misses the property '${missingProperty}'.\n${getSchemaPartText(
|
298 | err.parentSchema,
|
299 | ["properties", missingProperty]
|
300 | )}`;
|
301 | } else if (err.keyword === "minimum") {
|
302 | return `${dataPath} ${err.message}.${getSchemaPartDescription(
|
303 | err.parentSchema
|
304 | )}`;
|
305 | } else if (err.keyword === "uniqueItems") {
|
306 | return `${dataPath} should not contain the item '${
|
307 | err.data[err.params.i]
|
308 | }' twice.${getSchemaPartDescription(err.parentSchema)}`;
|
309 | } else if (
|
310 | err.keyword === "minLength" ||
|
311 | err.keyword === "minItems" ||
|
312 | err.keyword === "minProperties"
|
313 | ) {
|
314 | if (err.params.limit === 1) {
|
315 | return `${dataPath} should not be empty.${getSchemaPartDescription(
|
316 | err.parentSchema
|
317 | )}`;
|
318 | } else {
|
319 | return `${dataPath} ${err.message}${getSchemaPartDescription(
|
320 | err.parentSchema
|
321 | )}`;
|
322 | }
|
323 | } else if (err.keyword === "absolutePath") {
|
324 | const baseMessage = `${dataPath}: ${
|
325 | err.message
|
326 | }${getSchemaPartDescription(err.parentSchema)}`;
|
327 | if (dataPath === "configuration.output.filename") {
|
328 | return (
|
329 | `${baseMessage}\n` +
|
330 | "Please use output.path to specify absolute path and output.filename for the file name."
|
331 | );
|
332 | }
|
333 | return baseMessage;
|
334 | } else {
|
335 | return `${dataPath} ${err.message} (${JSON.stringify(
|
336 | err,
|
337 | null,
|
338 | 2
|
339 | )}).\n${getSchemaPartText(err.parentSchema)}`;
|
340 | }
|
341 | }
|
342 | }
|
343 |
|
344 | module.exports = WebpackOptionsValidationError;
|