WebpackOptionsValidationError.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  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/webpackOptionsSchema.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)
  20. schemaPart = inner;
  21. }
  22. return schemaPart;
  23. };
  24. const getSchemaPartText = (schemaPart, additionalPath) => {
  25. if(additionalPath) {
  26. for(let i = 0; i < additionalPath.length; i++) {
  27. const inner = schemaPart[additionalPath[i]];
  28. if(inner)
  29. schemaPart = inner;
  30. }
  31. }
  32. while(schemaPart.$ref) schemaPart = getSchemaPart(schemaPart.$ref);
  33. let schemaText = WebpackOptionsValidationError.formatSchema(schemaPart);
  34. if(schemaPart.description)
  35. schemaText += `\n-> ${schemaPart.description}`;
  36. return schemaText;
  37. };
  38. const getSchemaPartDescription = schemaPart => {
  39. while(schemaPart.$ref) schemaPart = getSchemaPart(schemaPart.$ref);
  40. if(schemaPart.description)
  41. return `\n-> ${schemaPart.description}`;
  42. return "";
  43. };
  44. const filterChildren = children => {
  45. return children.filter(err => err.keyword !== "anyOf" && err.keyword !== "allOf" && err.keyword !== "oneOf");
  46. };
  47. const indent = (str, prefix, firstLine) => {
  48. if(firstLine) {
  49. return prefix + str.replace(/\n(?!$)/g, "\n" + prefix);
  50. } else {
  51. return str.replace(/\n(?!$)/g, `\n${prefix}`);
  52. }
  53. };
  54. class WebpackOptionsValidationError extends WebpackError {
  55. constructor(validationErrors) {
  56. super();
  57. this.name = "WebpackOptionsValidationError";
  58. this.message = "Invalid configuration object. " +
  59. "Webpack has been initialised using a configuration object that does not match the API schema.\n" +
  60. validationErrors.map(err => " - " + indent(WebpackOptionsValidationError.formatValidationError(err), " ", false)).join("\n");
  61. this.validationErrors = validationErrors;
  62. Error.captureStackTrace(this, this.constructor);
  63. }
  64. static formatSchema(schema, prevSchemas) {
  65. prevSchemas = prevSchemas || [];
  66. const formatInnerSchema = (innerSchema, addSelf) => {
  67. if(!addSelf) return WebpackOptionsValidationError.formatSchema(innerSchema, prevSchemas);
  68. if(prevSchemas.indexOf(innerSchema) >= 0) return "(recursive)";
  69. return WebpackOptionsValidationError.formatSchema(innerSchema, prevSchemas.concat(schema));
  70. };
  71. if(schema.type === "string") {
  72. if(schema.minLength === 1)
  73. return "non-empty string";
  74. else if(schema.minLength > 1)
  75. return `string (min length ${schema.minLength})`;
  76. return "string";
  77. } else if(schema.type === "boolean") {
  78. return "boolean";
  79. } else if(schema.type === "number") {
  80. return "number";
  81. } else if(schema.type === "object") {
  82. if(schema.properties) {
  83. const required = schema.required || [];
  84. return `object { ${Object.keys(schema.properties).map(property => {
  85. if(required.indexOf(property) < 0) return property + "?";
  86. return property;
  87. }).concat(schema.additionalProperties ? ["..."] : []).join(", ")} }`;
  88. }
  89. if(schema.additionalProperties) {
  90. return `object { <key>: ${formatInnerSchema(schema.additionalProperties)} }`;
  91. }
  92. return "object";
  93. } else if(schema.type === "array") {
  94. return `[${formatInnerSchema(schema.items)}]`;
  95. }
  96. switch(schema.instanceof) {
  97. case "Function":
  98. return "function";
  99. case "RegExp":
  100. return "RegExp";
  101. }
  102. if(schema.$ref) return formatInnerSchema(getSchemaPart(schema.$ref), true);
  103. if(schema.allOf) return schema.allOf.map(formatInnerSchema).join(" & ");
  104. if(schema.oneOf) return schema.oneOf.map(formatInnerSchema).join(" | ");
  105. if(schema.anyOf) return schema.anyOf.map(formatInnerSchema).join(" | ");
  106. if(schema.enum) return schema.enum.map(item => JSON.stringify(item)).join(" | ");
  107. return JSON.stringify(schema, 0, 2);
  108. }
  109. static formatValidationError(err) {
  110. const dataPath = `configuration${err.dataPath}`;
  111. if(err.keyword === "additionalProperties") {
  112. const baseMessage = `${dataPath} has an unknown property '${err.params.additionalProperty}'. These properties are valid:\n${getSchemaPartText(err.parentSchema)}`;
  113. if(!err.dataPath) {
  114. switch(err.params.additionalProperty) {
  115. case "debug":
  116. return `${baseMessage}\n` +
  117. "The 'debug' property was removed in webpack 2.\n" +
  118. "Loaders should be updated to allow passing this option via loader options in module.rules.\n" +
  119. "Until loaders are updated one can use the LoaderOptionsPlugin to switch loaders into debug mode:\n" +
  120. "plugins: [\n" +
  121. " new webpack.LoaderOptionsPlugin({\n" +
  122. " debug: true\n" +
  123. " })\n" +
  124. "]";
  125. }
  126. return baseMessage + "\n" +
  127. "For typos: please correct them.\n" +
  128. "For loader options: webpack 2 no longer allows custom properties in configuration.\n" +
  129. " Loaders should be updated to allow passing options via loader options in module.rules.\n" +
  130. " Until loaders are updated one can use the LoaderOptionsPlugin to pass these options to the loader:\n" +
  131. " plugins: [\n" +
  132. " new webpack.LoaderOptionsPlugin({\n" +
  133. " // test: /\\.xxx$/, // may apply this only for some modules\n" +
  134. " options: {\n" +
  135. ` ${err.params.additionalProperty}: ...\n` +
  136. " }\n" +
  137. " })\n" +
  138. " ]";
  139. }
  140. return baseMessage;
  141. } else if(err.keyword === "oneOf" || err.keyword === "anyOf") {
  142. if(err.children && err.children.length > 0) {
  143. if(err.schema.length === 1) {
  144. const lastChild = err.children[err.children.length - 1];
  145. const remainingChildren = err.children.slice(0, err.children.length - 1);
  146. return WebpackOptionsValidationError.formatValidationError(Object.assign({}, lastChild, {
  147. children: remainingChildren,
  148. parentSchema: Object.assign({}, err.parentSchema, lastChild.parentSchema)
  149. }));
  150. }
  151. return `${dataPath} should be one of these:\n${getSchemaPartText(err.parentSchema)}\n` +
  152. `Details:\n${filterChildren(err.children).map(err => " * " + indent(WebpackOptionsValidationError.formatValidationError(err), " ", false)).join("\n")}`;
  153. }
  154. return `${dataPath} should be one of these:\n${getSchemaPartText(err.parentSchema)}`;
  155. } else if(err.keyword === "enum") {
  156. if(err.parentSchema && err.parentSchema.enum && err.parentSchema.enum.length === 1) {
  157. return `${dataPath} should be ${getSchemaPartText(err.parentSchema)}`;
  158. }
  159. return `${dataPath} should be one of these:\n${getSchemaPartText(err.parentSchema)}`;
  160. } else if(err.keyword === "allOf") {
  161. return `${dataPath} should be:\n${getSchemaPartText(err.parentSchema)}`;
  162. } else if(err.keyword === "type") {
  163. switch(err.params.type) {
  164. case "object":
  165. return `${dataPath} should be an object.${getSchemaPartDescription(err.parentSchema)}`;
  166. case "string":
  167. return `${dataPath} should be a string.${getSchemaPartDescription(err.parentSchema)}`;
  168. case "boolean":
  169. return `${dataPath} should be a boolean.${getSchemaPartDescription(err.parentSchema)}`;
  170. case "number":
  171. return `${dataPath} should be a number.${getSchemaPartDescription(err.parentSchema)}`;
  172. case "array":
  173. return `${dataPath} should be an array:\n${getSchemaPartText(err.parentSchema)}`;
  174. }
  175. return `${dataPath} should be ${err.params.type}:\n${getSchemaPartText(err.parentSchema)}`;
  176. } else if(err.keyword === "instanceof") {
  177. return `${dataPath} should be an instance of ${getSchemaPartText(err.parentSchema)}`;
  178. } else if(err.keyword === "required") {
  179. const missingProperty = err.params.missingProperty.replace(/^\./, "");
  180. return `${dataPath} misses the property '${missingProperty}'.\n${getSchemaPartText(err.parentSchema, ["properties", missingProperty])}`;
  181. } else if(err.keyword === "minimum") {
  182. return `${dataPath} ${err.message}.${getSchemaPartDescription(err.parentSchema)}`;
  183. } else if(err.keyword === "uniqueItems") {
  184. return `${dataPath} should not contain the item '${err.data[err.params.i]}' twice.${getSchemaPartDescription(err.parentSchema)}`;
  185. } else if(err.keyword === "minLength" || err.keyword === "minItems" || err.keyword === "minProperties") {
  186. if(err.params.limit === 1)
  187. return `${dataPath} should not be empty.${getSchemaPartDescription(err.parentSchema)}`;
  188. else
  189. return `${dataPath} ${err.message}${getSchemaPartDescription(err.parentSchema)}`;
  190. } else if(err.keyword === "absolutePath") {
  191. const baseMessage = `${dataPath}: ${err.message}${getSchemaPartDescription(err.parentSchema)}`;
  192. if(dataPath === "configuration.output.filename") {
  193. return `${baseMessage}\n` +
  194. "Please use output.path to specify absolute path and output.filename for the file name.";
  195. }
  196. return baseMessage;
  197. } else {
  198. // eslint-disable-line no-fallthrough
  199. return `${dataPath} ${err.message} (${JSON.stringify(err, 0, 2)}).\n${getSchemaPartText(err.parentSchema)}`;
  200. }
  201. }
  202. }
  203. module.exports = WebpackOptionsValidationError;