WebpackOptionsValidationError.js 9.6 KB

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