WebpackOptionsValidationError.js 11 KB

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