Skip to content

Commit 40a67f6

Browse files
committed
Merge branch 'gajus-feature/use-json-schema-to-validate-webpack-config'
2 parents f9b81a7 + 8c3d97d commit 40a67f6

17 files changed

Lines changed: 1189 additions & 32 deletions

File tree

.editorconfig

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@ root = true
22

33
[*.js]
44
indent_style=tab
5-
trim_trailing_whitespace=true
5+
trim_trailing_whitespace=true
6+
7+
[*.json]
8+
indent_style = space
9+
indent_size = 2

bin/convert-argv.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ fs.existsSync = fs.existsSync || path.existsSync;
44
var resolve = require("enhanced-resolve");
55
var interpret = require("interpret");
66
var WebpackOptionsDefaulter = require("../lib/WebpackOptionsDefaulter");
7+
var validateWebpackOptions = require("../lib/validateWebpackOptions");
78

89
module.exports = function(optimist, argv, convertOptions) {
910

@@ -553,5 +554,11 @@ module.exports = function(optimist, argv, convertOptions) {
553554
console.error("Use --help to display the CLI options.");
554555
process.exit(-1); // eslint-disable-line
555556
}
557+
var webpackOptionsValidationErrors = validateWebpackOptions(options);
558+
559+
if(webpackOptionsValidationErrors.length) {
560+
console.error("Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. The following checks have failed.", webpackOptionsValidationErrors);
561+
process.exit(-1); // eslint-disable-line
562+
}
556563
}
557564
};

examples/coffee-script/webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ module.exports = {
55
]
66
},
77
resolve: {
8-
extensions: ["", ".web.coffee", ".web.js", ".coffee", ".js"]
8+
extensions: [".web.coffee", ".web.js", ".coffee", ".js"]
99
}
1010
}
Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
var webpack = require("../../");
12
module.exports = {
2-
worker: {
3-
output: {
4-
filename: "hash.worker.js",
5-
chunkFilename: "[id].hash.worker.js"
6-
}
7-
}
8-
}
3+
plugins: [
4+
new webpack.LoaderOptionsPlugin({
5+
options: {
6+
worker: {
7+
output: {
8+
filename: "hash.worker.js",
9+
chunkFilename: "[id].hash.worker.js"
10+
}
11+
}
12+
}
13+
})
14+
]
15+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Gajus Kuizinas @gajus
4+
*/
5+
var webpackOptionsSchema = require("../schemas/webpackOptionsSchema.json");
6+
7+
function WebpackOptionsValidationError(validationErrors) {
8+
Error.call(this);
9+
Error.captureStackTrace(this, WebpackOptionsValidationError);
10+
this.name = "WebpackOptionsValidationError";
11+
this.message = "Invalid configuration object. " +
12+
"Webpack has been initialised using a configuration object that does not match the API schema.\n" +
13+
validationErrors.map(function(err) {
14+
return " - " + indent(WebpackOptionsValidationError.formatValidationError(err), " ", false);
15+
}).join("\n");
16+
this.validationErrors = validationErrors;
17+
}
18+
module.exports = WebpackOptionsValidationError;
19+
20+
WebpackOptionsValidationError.prototype = Object.create(Error.prototype);
21+
WebpackOptionsValidationError.prototype.constructor = WebpackOptionsValidationError;
22+
23+
WebpackOptionsValidationError.formatValidationError = function formatValidationError(err) {
24+
var dataPath = "configuration" + err.dataPath;
25+
switch(err.keyword) {
26+
case "additionalProperties":
27+
return dataPath + " has an unknown property '" + err.params.additionalProperty + "'. These properties are valid:\n" +
28+
getSchemaPartText(err.parentSchema);
29+
case "oneOf":
30+
case "anyOf":
31+
case "enum":
32+
return dataPath + " should be one of these:\n" +
33+
getSchemaPartText(err.parentSchema);
34+
case "allOf":
35+
return dataPath + " should be:\n" +
36+
getSchemaPartText(err.parentSchema);
37+
case "type":
38+
switch(err.params.type) {
39+
case "object":
40+
return dataPath + " should be an object.";
41+
case "string":
42+
return dataPath + " should be a string.";
43+
case "boolean":
44+
return dataPath + " should be a boolean.";
45+
case "number":
46+
return dataPath + " should be a number.";
47+
}
48+
return dataPath + " should be " + err.params.type + ":\n" +
49+
getSchemaPartText(err.parentSchema);
50+
case "required":
51+
var missingProperty = err.params.missingProperty.replace(/^\./, "");
52+
return dataPath + " misses the property '" + missingProperty + "'.\n" +
53+
getSchemaPartText(err.parentSchema, ["properties", missingProperty]);
54+
case "minLength":
55+
if(err.params.limit === 1)
56+
return dataPath + " should not be empty.";
57+
else
58+
return dataPath + " " + err.message;
59+
default:
60+
return dataPath + " " + err.message + " (" + JSON.stringify(err, 0, 2) + ").\n" +
61+
getSchemaPartText(err.parentSchema);
62+
}
63+
}
64+
65+
function getSchemaPart(path, parents, additionalPath) {
66+
parents = parents || 0;
67+
path = path.split("/");
68+
path = path.slice(0, path.length - parents);
69+
if(additionalPath) {
70+
additionalPath = additionalPath.split("/");
71+
path = path.concat(additionalPath);
72+
}
73+
var schemaPart = webpackOptionsSchema;
74+
for(var i = 1; i < path.length; i++) {
75+
var inner = schemaPart[path[i]];
76+
if(inner)
77+
schemaPart = inner;
78+
}
79+
return schemaPart;
80+
}
81+
82+
function getSchemaPartText2(path, parents, additionalPath) {
83+
var schemaPart = getSchemaPart(path, parents, additionalPath);
84+
while(schemaPart.$ref) schemaPart = getSchemaPart(schemaPart.$ref);
85+
var schemaText = WebpackOptionsValidationError.formatSchema(schemaPart);
86+
if(schemaPart.description)
87+
schemaText += "\n" + schemaPart.description;
88+
return schemaText;
89+
}
90+
91+
function getSchemaPartText(schemaPart, additionalPath) {
92+
if(additionalPath) {
93+
for(var i = 0; i < additionalPath.length; i++) {
94+
var inner = schemaPart[additionalPath[i]];
95+
if(inner)
96+
schemaPart = inner;
97+
}
98+
}
99+
while(schemaPart.$ref) schemaPart = getSchemaPart(schemaPart.$ref);
100+
var schemaText = WebpackOptionsValidationError.formatSchema(schemaPart);
101+
if(schemaPart.description)
102+
schemaText += "\n" + schemaPart.description;
103+
return schemaText;
104+
}
105+
106+
function formatSchema(schema, prevSchemas) {
107+
prevSchemas = prevSchemas || [];
108+
109+
function formatInnerSchema(innerSchema, addSelf) {
110+
if(!addSelf) return formatSchema(innerSchema, prevSchemas);
111+
if(prevSchemas.indexOf(innerSchema) >= 0) return "(recursive)";
112+
return formatSchema(innerSchema, prevSchemas.concat(schema));
113+
}
114+
switch(schema.type) {
115+
case "string":
116+
return "string";
117+
case "boolean":
118+
return "boolean";
119+
case "object":
120+
if(schema.properties) {
121+
var required = schema.required || [];
122+
return "object { " + Object.keys(schema.properties).map(function(property) {
123+
if(required.indexOf(property) < 0) return property + "?";
124+
return property;
125+
}).join(", ") + " }";
126+
}
127+
if(schema.additionalProperties) {
128+
return "object { <key>: " + formatInnerSchema(schema.additionalProperties) + " }";
129+
}
130+
return "object";
131+
case "array":
132+
return "[" + formatInnerSchema(schema.items) + "]";
133+
}
134+
switch(schema.instanceof) {
135+
case "Function":
136+
return "function";
137+
case "RegExp":
138+
return "RegExp";
139+
}
140+
if(schema.$ref) return formatInnerSchema(getSchemaPart(schema.$ref), true);
141+
if(schema.allOf) return schema.allOf.map(formatInnerSchema).join(" & ");
142+
if(schema.oneOf) return schema.oneOf.map(formatInnerSchema).join(" | ");
143+
if(schema.anyOf) return schema.anyOf.map(formatInnerSchema).join(" | ");
144+
if(schema.enum) return schema.enum.map(function(item) {
145+
return JSON.stringify(item);
146+
}).join(" | ");
147+
return JSON.stringify(schema, 0, 2);
148+
}
149+
150+
function indent(str, prefix, firstLine) {
151+
if(firstLine) {
152+
return prefix + str.replace(/\n(?!$)/g, "\n" + prefix);
153+
} else {
154+
return str.replace(/\n(?!$)/g, "\n" + prefix);
155+
}
156+
}
157+
158+
WebpackOptionsValidationError.formatSchema = formatSchema;

lib/validateWebpackOptions.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Gajus Kuizinas @gajus
4+
*/
5+
var webpackOptionsSchema = require("../schemas/webpackOptionsSchema.json");
6+
var Ajv = require("ajv");
7+
var ajv = new Ajv({
8+
errorDataPath: "configuration",
9+
allErrors: true,
10+
verbose: true
11+
});
12+
var validate = ajv.compile(webpackOptionsSchema);
13+
14+
function validateWebpackOptions(options) {
15+
if(Array.isArray(options)) {
16+
var errors = options.map(validateObject);
17+
errors.forEach(function(list, idx) {
18+
list.forEach(function applyPrefix(err) {
19+
err.dataPath = "[" + idx + "]" + err.dataPath;
20+
if(err.children) {
21+
err.children.forEach(applyPrefix);
22+
}
23+
});
24+
});
25+
return errors.reduce(function(arr, items) {
26+
return arr.concat(items);
27+
}, []);
28+
} else {
29+
return validateObject(options);
30+
}
31+
}
32+
33+
function validateObject(options) {
34+
var valid = validate(options);
35+
return valid ? [] : filterErrors(validate.errors);
36+
}
37+
38+
function filterErrors(errors) {
39+
var errorsByDataPath = {};
40+
var newErrors = [];
41+
errors.forEach(function(err) {
42+
var dataPath = err.dataPath;
43+
var key = "$" + dataPath;
44+
if(errorsByDataPath[key]) {
45+
var oldError = errorsByDataPath[key];
46+
var idx = newErrors.indexOf(oldError);
47+
newErrors.splice(idx, 1);
48+
if(oldError.children) {
49+
var children = oldError.children;
50+
delete oldError.children;
51+
children.push(oldError);
52+
err.children = children;
53+
} else {
54+
err.children = [oldError];
55+
}
56+
}
57+
errorsByDataPath[key] = err;
58+
newErrors.push(err);
59+
});
60+
return newErrors;
61+
}
62+
63+
module.exports = validateWebpackOptions;

lib/webpack.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,20 @@ var MultiCompiler = require("./MultiCompiler");
77
var NodeEnvironmentPlugin = require("./node/NodeEnvironmentPlugin");
88
var WebpackOptionsApply = require("./WebpackOptionsApply");
99
var WebpackOptionsDefaulter = require("./WebpackOptionsDefaulter");
10+
var validateWebpackOptions = require("./validateWebpackOptions");
11+
var WebpackOptionsValidationError = require("./WebpackOptionsValidationError");
1012

1113
function webpack(options, callback) {
14+
var webpackOptionsValidationErrors = validateWebpackOptions(options);
15+
if(webpackOptionsValidationErrors.length) {
16+
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
17+
}
1218
var compiler;
1319
if(Array.isArray(options)) {
1420
compiler = new MultiCompiler(options.map(function(options) {
1521
return webpack(options);
1622
}));
1723
} else if(typeof options === "object") {
18-
if(!options.entry && !options.plugins) {
19-
throw new Error("Passed 'options' object does not look like a valid webpack configuration");
20-
}
2124
new WebpackOptionsDefaulter().process(options);
2225

2326
compiler = new Compiler();
@@ -49,6 +52,7 @@ webpack.WebpackOptionsApply = WebpackOptionsApply;
4952
webpack.Compiler = Compiler;
5053
webpack.MultiCompiler = MultiCompiler;
5154
webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin;
55+
webpack.validate = validateWebpackOptions;
5256

5357
function exportPlugins(exports, path, plugins) {
5458
plugins.forEach(function(name) {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"description": "Packs CommonJs/AMD modules for the browser. Allows to split your codebase into multiple bundles, which can be loaded on demand. Support loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.",
66
"dependencies": {
77
"acorn": "^3.2.0",
8+
"ajv": "^4.7.0",
89
"async": "^1.3.0",
910
"clone": "^1.0.2",
1011
"enhanced-resolve": "^2.2.0",

0 commit comments

Comments
 (0)