/* MIT License http://www.opensource.org/licenses/mit-license.php Author Gajus Kuizinas @gajus */ "use strict"; const WebpackError = require("./WebpackError"); const webpackOptionsSchema = require("../schemas/WebpackOptions.json"); 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 = webpackOptionsSchema; 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 = WebpackOptionsValidationError.formatSchema(schemaPart); if (schemaPart.description) { schemaText += `\n-> ${schemaPart.description}`; } return schemaText; }; const getSchemaPartDescription = schemaPart => { while (schemaPart.$ref) { schemaPart = getSchemaPart(schemaPart.$ref); } if (schemaPart.description) { return `\n-> ${schemaPart.description}`; } return ""; }; const SPECIFICITY = { type: 1, oneOf: 1, anyOf: 1, allOf: 1, additionalProperties: 2, enum: 1, instanceof: 1, required: 2, minimum: 2, uniqueItems: 2, minLength: 2, minItems: 2, minProperties: 2, absolutePath: 2 }; const filterMax = (array, fn) => { const max = array.reduce((max, item) => Math.max(max, fn(item)), 0); return array.filter(item => fn(item) === max); }; const filterChildren = children => { children = filterMax(children, err => err.dataPath ? err.dataPath.length : 0 ); children = filterMax(children, err => SPECIFICITY[err.keyword] || 2); return children; }; const indent = (str, prefix, firstLine) => { if (firstLine) { return prefix + str.replace(/\n(?!$)/g, "\n" + prefix); } else { return str.replace(/\n(?!$)/g, `\n${prefix}`); } }; class WebpackOptionsValidationError extends WebpackError { constructor(validationErrors) { super( "Invalid configuration object. " + "Webpack has been initialised using a configuration object that does not match the API schema.\n" + validationErrors .map( err => " - " + indent( WebpackOptionsValidationError.formatValidationError(err), " ", false ) ) .join("\n") ); this.name = "WebpackOptionsValidationError"; this.validationErrors = validationErrors; Error.captureStackTrace(this, this.constructor); } static formatSchema(schema, prevSchemas) { prevSchemas = prevSchemas || []; const formatInnerSchema = (innerSchema, addSelf) => { if (!addSelf) { return WebpackOptionsValidationError.formatSchema( innerSchema, prevSchemas ); } if (prevSchemas.includes(innerSchema)) { return "(recursive)"; } return WebpackOptionsValidationError.formatSchema( innerSchema, prevSchemas.concat(schema) ); }; if (schema.type === "string") { if (schema.minLength === 1) { return "non-empty string"; } if (schema.minLength > 1) { return `string (min length ${schema.minLength})`; } return "string"; } if (schema.type === "boolean") { return "boolean"; } if (schema.type === "number") { return "number"; } if (schema.type === "object") { if (schema.properties) { const required = schema.required || []; return `object { ${Object.keys(schema.properties) .map(property => { if (!required.includes(property)) return property + "?"; return property; }) .concat(schema.additionalProperties ? ["…"] : []) .join(", ")} }`; } if (schema.additionalProperties) { return `object { <key>: ${formatInnerSchema( schema.additionalProperties )} }`; } return "object"; } if (schema.type === "array") { return `[${formatInnerSchema(schema.items)}]`; } switch (schema.instanceof) { case "Function": return "function"; case "RegExp": return "RegExp"; } if (schema.enum) { return schema.enum.map(item => JSON.stringify(item)).join(" | "); } 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(" | "); } return JSON.stringify(schema, null, 2); } static formatValidationError(err) { const dataPath = `configuration${err.dataPath}`; if (err.keyword === "additionalProperties") { const baseMessage = `${dataPath} has an unknown property '${ err.params.additionalProperty }'. These properties are valid:\n${getSchemaPartText(err.parentSchema)}`; if (!err.dataPath) { switch (err.params.additionalProperty) { case "debug": return ( `${baseMessage}\n` + "The 'debug' property was removed in webpack 2.0.0.\n" + "Loaders should be updated to allow passing this option via loader options in module.rules.\n" + "Until loaders are updated one can use the LoaderOptionsPlugin to switch loaders into debug mode:\n" + "plugins: [\n" + " new webpack.LoaderOptionsPlugin({\n" + " debug: true\n" + " })\n" + "]" ); } return ( `${baseMessage}\n` + "For typos: please correct them.\n" + "For loader options: webpack >= v2.0.0 no longer allows custom properties in configuration.\n" + " Loaders should be updated to allow passing options via loader options in module.rules.\n" + " Until loaders are updated one can use the LoaderOptionsPlugin to pass these options to the loader:\n" + " plugins: [\n" + " new webpack.LoaderOptionsPlugin({\n" + " // test: /\\.xxx$/, // may apply this only for some modules\n" + " options: {\n" + ` ${err.params.additionalProperty}: …\n` + " }\n" + " })\n" + " ]" ); } return baseMessage; } else if (err.keyword === "oneOf" || err.keyword === "anyOf") { if (err.children && err.children.length > 0) { if (err.schema.length === 1) { const lastChild = err.children[err.children.length - 1]; const remainingChildren = err.children.slice( 0, err.children.length - 1 ); return WebpackOptionsValidationError.formatValidationError( Object.assign({}, lastChild, { children: remainingChildren, parentSchema: Object.assign( {}, err.parentSchema, lastChild.parentSchema ) }) ); } const children = filterChildren(err.children); if (children.length === 1) { return WebpackOptionsValidationError.formatValidationError( children[0] ); } return ( `${dataPath} should be one of these:\n${getSchemaPartText( err.parentSchema )}\n` + `Details:\n${children .map( err => " * " + indent( WebpackOptionsValidationError.formatValidationError(err), " ", 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.${getSchemaPartDescription( err.parentSchema )}`; case "string": return `${dataPath} should be a string.${getSchemaPartDescription( err.parentSchema )}`; case "boolean": return `${dataPath} should be a boolean.${getSchemaPartDescription( err.parentSchema )}`; case "number": return `${dataPath} should be a number.${getSchemaPartDescription( err.parentSchema )}`; case "array": return `${dataPath} should be an array:\n${getSchemaPartText( err.parentSchema )}`; } 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 === "minimum") { return `${dataPath} ${err.message}.${getSchemaPartDescription( err.parentSchema )}`; } else if (err.keyword === "uniqueItems") { return `${dataPath} should not contain the item '${ err.data[err.params.i] }' twice.${getSchemaPartDescription(err.parentSchema)}`; } else if ( err.keyword === "minLength" || err.keyword === "minItems" || err.keyword === "minProperties" ) { if (err.params.limit === 1) { switch (err.keyword) { case "minLength": return `${dataPath} should be an non-empty string.${getSchemaPartDescription( err.parentSchema )}`; case "minItems": return `${dataPath} should be an non-empty array.${getSchemaPartDescription( err.parentSchema )}`; case "minProperties": return `${dataPath} should be an non-empty object.${getSchemaPartDescription( err.parentSchema )}`; } return `${dataPath} should be not empty.${getSchemaPartDescription( err.parentSchema )}`; } else { return `${dataPath} ${err.message}${getSchemaPartDescription( err.parentSchema )}`; } } else if (err.keyword === "not") { return `${dataPath} should not be ${getSchemaPartText( err.schema )}\n${getSchemaPartText(err.parentSchema)}`; } else if (err.keyword === "absolutePath") { const baseMessage = `${dataPath}: ${ err.message }${getSchemaPartDescription(err.parentSchema)}`; if (dataPath === "configuration.output.filename") { return ( `${baseMessage}\n` + "Please use output.path to specify absolute path and output.filename for the file name." ); } return baseMessage; } else { return `${dataPath} ${err.message} (${JSON.stringify( err, null, 2 )}).\n${getSchemaPartText(err.parentSchema)}`; } } } module.exports = WebpackOptionsValidationError;