OptionsValidationError.js 6.34 KB
'use strict';

/* eslint no-param-reassign: 'off' */

const optionsSchema = require('./optionsSchema.json');

const indent = (str, prefix, firstLine) => {
  if (firstLine) {
    return prefix + str.replace(/\n(?!$)/g, `\n${prefix}`);
  }
  return str.replace(/\n(?!$)/g, `\n${prefix}`);
};

const getSchemaPart = (path, parents, additionalPath) => {
  parents = parents || 0;
  path = path.split('/');
  path = path.slice(0, path.length - parents);
  if (additionalPath) {
    additionalPath = additionalPath.split('/');
    path = path.concat(additionalPath);
  }
  let schemaPart = optionsSchema;
  for (let i = 1; i < path.length; i++) {
    const inner = schemaPart[path[i]];
    if (inner) { schemaPart = inner; }
  }
  return schemaPart;
};

const getSchemaPartText = (schemaPart, additionalPath) => {
  if (additionalPath) {
    for (let i = 0; i < additionalPath.length; i++) {
      const inner = schemaPart[additionalPath[i]];
      if (inner) { schemaPart = inner; }
    }
  }
  while (schemaPart.$ref) schemaPart = getSchemaPart(schemaPart.$ref);
  let schemaText = OptionsValidationError.formatSchema(schemaPart); // eslint-disable-line
  if (schemaPart.description) { schemaText += `\n${schemaPart.description}`; }
  return schemaText;
};

class OptionsValidationError extends Error {
  constructor(validationErrors) {
    super();

    if (Error.hasOwnProperty('captureStackTrace')) { // eslint-disable-line
      Error.captureStackTrace(this, this.constructor);
    }
    this.name = 'WebpackDevServerOptionsValidationError';

    this.message = `${'Invalid configuration object. ' +
   'webpack-dev-server has been initialised using a configuration object that does not match the API schema.\n'}${
      validationErrors.map(err => ` - ${indent(OptionsValidationError.formatValidationError(err), '   ', false)}`).join('\n')}`;
    this.validationErrors = validationErrors;
  }

  static formatSchema(schema, prevSchemas) {
    prevSchemas = prevSchemas || [];

    const formatInnerSchema = (innerSchema, addSelf) => {
      if (!addSelf) return OptionsValidationError.formatSchema(innerSchema, prevSchemas);
      if (prevSchemas.indexOf(innerSchema) >= 0) return '(recursive)';
      return OptionsValidationError.formatSchema(innerSchema, prevSchemas.concat(schema));
    };

    if (schema.type === 'string') {
      if (schema.minLength === 1) { return 'non-empty string'; } else if (schema.minLength > 1) { return `string (min length ${schema.minLength})`; }
      return 'string';
    } else if (schema.type === 'boolean') {
      return 'boolean';
    } else if (schema.type === 'number') {
      return 'number';
    } else if (schema.type === 'object') {
      if (schema.properties) {
        const required = schema.required || [];
        return `object { ${Object.keys(schema.properties).map((property) => {
          if (required.indexOf(property) < 0) return `${property}?`;
          return property;
        }).concat(schema.additionalProperties ? ['...'] : []).join(', ')} }`;
      }
      if (schema.additionalProperties) {
        return `object { <key>: ${formatInnerSchema(schema.additionalProperties)} }`;
      }
      return 'object';
    } else if (schema.type === 'array') {
      return `[${formatInnerSchema(schema.items)}]`;
    }

    switch (schema.instanceof) {
      case 'Function':
        return 'function';
      case 'RegExp':
        return 'RegExp';
      default:
    }

    if (schema.$ref) return formatInnerSchema(getSchemaPart(schema.$ref), true);
    if (schema.allOf) return schema.allOf.map(formatInnerSchema).join(' & ');
    if (schema.oneOf) return schema.oneOf.map(formatInnerSchema).join(' | ');
    if (schema.anyOf) return schema.anyOf.map(formatInnerSchema).join(' | ');
    if (schema.enum) return schema.enum.map(item => JSON.stringify(item)).join(' | ');
    return JSON.stringify(schema, 0, 2);
  }

  static formatValidationError(err) {
    const dataPath = `configuration${err.dataPath}`;
    if (err.keyword === 'additionalProperties') {
      return `${dataPath} has an unknown property '${err.params.additionalProperty}'. These properties are valid:\n${getSchemaPartText(err.parentSchema)}`;
    } else if (err.keyword === 'oneOf' || err.keyword === 'anyOf') {
      if (err.children && err.children.length > 0) {
        return `${dataPath} should be one of these:\n${getSchemaPartText(err.parentSchema)}\n` +
     `Details:\n${err.children.map(e => ` * ${indent(OptionsValidationError.formatValidationError(e), '   ', false)}`).join('\n')}`;
      }
      return `${dataPath} should be one of these:\n${getSchemaPartText(err.parentSchema)}`;
    } else if (err.keyword === 'enum') {
      if (err.parentSchema && err.parentSchema.enum && err.parentSchema.enum.length === 1) {
        return `${dataPath} should be ${getSchemaPartText(err.parentSchema)}`;
      }
      return `${dataPath} should be one of these:\n${getSchemaPartText(err.parentSchema)}`;
    } else if (err.keyword === 'allOf') {
      return `${dataPath} should be:\n${getSchemaPartText(err.parentSchema)}`;
    } else if (err.keyword === 'type') {
      switch (err.params.type) {
        case 'object':
          return `${dataPath} should be an object.`;
        case 'string':
          return `${dataPath} should be a string.`;
        case 'boolean':
          return `${dataPath} should be a boolean.`;
        case 'number':
          return `${dataPath} should be a number.`;
        case 'array':
          return `${dataPath} should be an array:\n${getSchemaPartText(err.parentSchema)}`;
        default:
      }
      return `${dataPath} should be ${err.params.type}:\n${getSchemaPartText(err.parentSchema)}`;
    } else if (err.keyword === 'instanceof') {
      return `${dataPath} should be an instance of ${getSchemaPartText(err.parentSchema)}.`;
    } else if (err.keyword === 'required') {
      const missingProperty = err.params.missingProperty.replace(/^\./, '');
      return `${dataPath} misses the property '${missingProperty}'.\n${getSchemaPartText(err.parentSchema, ['properties', missingProperty])}`;
    } else if (err.keyword === 'minLength' || err.keyword === 'minItems') {
      if (err.params.limit === 1) { return `${dataPath} should not be empty.`; }
      return `${dataPath} ${err.message}`;
    }
    // eslint-disable-line no-fallthrough
    return `${dataPath} ${err.message} (${JSON.stringify(err, 0, 2)}).\n${getSchemaPartText(err.parentSchema)}`;
  }
}

module.exports = OptionsValidationError;