OptionsValidationError.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. 'use strict';
  2. /* eslint no-param-reassign: 'off' */
  3. const optionsSchema = require('./optionsSchema.json');
  4. const indent = (str, prefix, firstLine) => {
  5. if (firstLine) {
  6. return prefix + str.replace(/\n(?!$)/g, `\n${prefix}`);
  7. }
  8. return str.replace(/\n(?!$)/g, `\n${prefix}`);
  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 = optionsSchema;
  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. const getSchemaPartText = (schemaPart, additionalPath) => {
  26. if (additionalPath) {
  27. for (let i = 0; i < additionalPath.length; i++) {
  28. const inner = schemaPart[additionalPath[i]];
  29. if (inner) { schemaPart = inner; }
  30. }
  31. }
  32. while (schemaPart.$ref) schemaPart = getSchemaPart(schemaPart.$ref);
  33. let schemaText = OptionsValidationError.formatSchema(schemaPart); // eslint-disable-line
  34. if (schemaPart.description) { schemaText += `\n${schemaPart.description}`; }
  35. return schemaText;
  36. };
  37. class OptionsValidationError extends Error {
  38. constructor(validationErrors) {
  39. super();
  40. if (Error.hasOwnProperty('captureStackTrace')) { // eslint-disable-line
  41. Error.captureStackTrace(this, this.constructor);
  42. }
  43. this.name = 'WebpackDevServerOptionsValidationError';
  44. this.message = `${'Invalid configuration object. ' +
  45. 'webpack-dev-server has been initialised using a configuration object that does not match the API schema.\n'}${
  46. validationErrors.map(err => ` - ${indent(OptionsValidationError.formatValidationError(err), ' ', false)}`).join('\n')}`;
  47. this.validationErrors = validationErrors;
  48. }
  49. static formatSchema(schema, prevSchemas) {
  50. prevSchemas = prevSchemas || [];
  51. const formatInnerSchema = (innerSchema, addSelf) => {
  52. if (!addSelf) return OptionsValidationError.formatSchema(innerSchema, prevSchemas);
  53. if (prevSchemas.indexOf(innerSchema) >= 0) return '(recursive)';
  54. return OptionsValidationError.formatSchema(innerSchema, prevSchemas.concat(schema));
  55. };
  56. if (schema.type === 'string') {
  57. if (schema.minLength === 1) { return 'non-empty string'; } else if (schema.minLength > 1) { return `string (min length ${schema.minLength})`; }
  58. return 'string';
  59. } else if (schema.type === 'boolean') {
  60. return 'boolean';
  61. } else if (schema.type === 'number') {
  62. return 'number';
  63. } else if (schema.type === 'object') {
  64. if (schema.properties) {
  65. const required = schema.required || [];
  66. return `object { ${Object.keys(schema.properties).map((property) => {
  67. if (required.indexOf(property) < 0) return `${property}?`;
  68. return property;
  69. }).concat(schema.additionalProperties ? ['...'] : []).join(', ')} }`;
  70. }
  71. if (schema.additionalProperties) {
  72. return `object { <key>: ${formatInnerSchema(schema.additionalProperties)} }`;
  73. }
  74. return 'object';
  75. } else if (schema.type === 'array') {
  76. return `[${formatInnerSchema(schema.items)}]`;
  77. }
  78. switch (schema.instanceof) {
  79. case 'Function':
  80. return 'function';
  81. case 'RegExp':
  82. return 'RegExp';
  83. default:
  84. }
  85. if (schema.$ref) return formatInnerSchema(getSchemaPart(schema.$ref), true);
  86. if (schema.allOf) return schema.allOf.map(formatInnerSchema).join(' & ');
  87. if (schema.oneOf) return schema.oneOf.map(formatInnerSchema).join(' | ');
  88. if (schema.anyOf) return schema.anyOf.map(formatInnerSchema).join(' | ');
  89. if (schema.enum) return schema.enum.map(item => JSON.stringify(item)).join(' | ');
  90. return JSON.stringify(schema, 0, 2);
  91. }
  92. static formatValidationError(err) {
  93. const dataPath = `configuration${err.dataPath}`;
  94. if (err.keyword === 'additionalProperties') {
  95. return `${dataPath} has an unknown property '${err.params.additionalProperty}'. These properties are valid:\n${getSchemaPartText(err.parentSchema)}`;
  96. } else if (err.keyword === 'oneOf' || err.keyword === 'anyOf') {
  97. if (err.children && err.children.length > 0) {
  98. return `${dataPath} should be one of these:\n${getSchemaPartText(err.parentSchema)}\n` +
  99. `Details:\n${err.children.map(e => ` * ${indent(OptionsValidationError.formatValidationError(e), ' ', false)}`).join('\n')}`;
  100. }
  101. return `${dataPath} should be one of these:\n${getSchemaPartText(err.parentSchema)}`;
  102. } else if (err.keyword === 'enum') {
  103. if (err.parentSchema && err.parentSchema.enum && err.parentSchema.enum.length === 1) {
  104. return `${dataPath} should be ${getSchemaPartText(err.parentSchema)}`;
  105. }
  106. return `${dataPath} should be one of these:\n${getSchemaPartText(err.parentSchema)}`;
  107. } else if (err.keyword === 'allOf') {
  108. return `${dataPath} should be:\n${getSchemaPartText(err.parentSchema)}`;
  109. } else if (err.keyword === 'type') {
  110. switch (err.params.type) {
  111. case 'object':
  112. return `${dataPath} should be an object.`;
  113. case 'string':
  114. return `${dataPath} should be a string.`;
  115. case 'boolean':
  116. return `${dataPath} should be a boolean.`;
  117. case 'number':
  118. return `${dataPath} should be a number.`;
  119. case 'array':
  120. return `${dataPath} should be an array:\n${getSchemaPartText(err.parentSchema)}`;
  121. default:
  122. }
  123. return `${dataPath} should be ${err.params.type}:\n${getSchemaPartText(err.parentSchema)}`;
  124. } else if (err.keyword === 'instanceof') {
  125. return `${dataPath} should be an instance of ${getSchemaPartText(err.parentSchema)}.`;
  126. } else if (err.keyword === 'required') {
  127. const missingProperty = err.params.missingProperty.replace(/^\./, '');
  128. return `${dataPath} misses the property '${missingProperty}'.\n${getSchemaPartText(err.parentSchema, ['properties', missingProperty])}`;
  129. } else if (err.keyword === 'minLength' || err.keyword === 'minItems') {
  130. if (err.params.limit === 1) { return `${dataPath} should not be empty.`; }
  131. return `${dataPath} ${err.message}`;
  132. }
  133. // eslint-disable-line no-fallthrough
  134. return `${dataPath} ${err.message} (${JSON.stringify(err, 0, 2)}).\n${getSchemaPartText(err.parentSchema)}`;
  135. }
  136. }
  137. module.exports = OptionsValidationError;