From f1df013150585841dbee829cdbfe68a6ba34b4d5 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 21 May 2019 10:38:16 +0200 Subject: [PATCH 1/3] refactor Validation test for snapshots --- test/Validation.test.js | 770 ++++++++++++++++++++++------------------ 1 file changed, 415 insertions(+), 355 deletions(-) diff --git a/test/Validation.test.js b/test/Validation.test.js index 9d306bcbacf..df78b93c1d6 100644 --- a/test/Validation.test.js +++ b/test/Validation.test.js @@ -1,403 +1,463 @@ /* globals describe, it */ "use strict"; -const webpack = require("../lib/webpack"); +const webpack = require(".."); describe("Validation", () => { - const testCases = [ + const createTestCase = (name, config, fn) => { + it("should fail validation for " + name, () => { + try { + webpack(config); + } catch (err) { + if (err.name !== "WebpackOptionsValidationError") throw err; + + expect(err.message).toMatch(/^Invalid configuration object./); + fn(err.message); + + return; + } + + throw new Error("Validation didn't fail"); + }); + }; + + createTestCase("undefined configuration", undefined, msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration should be an object." +`) + ); + + createTestCase("null configuration", null, msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration should be an object." +`) + ); + + createTestCase( + "empty entry string", { - name: "undefined configuration", - config: undefined, - message: [" - configuration should be an object."] + entry: "" }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.entry should be one of these: + function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string] + -> The entry point(s) of the compilation. + Details: + * configuration.entry should be an instance of function + -> A Function returning an entry object, an entry string, an entry array or a promise to these things. + * configuration.entry should be an object. + -> Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array. + * configuration.entry should not be empty. + -> An entry point without name. The string is resolved to a module which is loaded upon startup. + * configuration.entry should be an array: + [non-empty string]" +`) + ); + + createTestCase( + "empty entry bundle array", { - name: "null configuration", - config: null, - message: [" - configuration should be an object."] + entry: { + bundle: [] + } }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.entry should be one of these: + function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string] + -> The entry point(s) of the compilation. + Details: + * configuration.entry should be an instance of function + -> A Function returning an entry object, an entry string, an entry array or a promise to these things. + * configuration.entry['bundle'] should be a string. + -> The string is resolved to a module which is loaded upon startup. + * configuration.entry['bundle'] should not be empty. + * configuration.entry should be a string. + -> An entry point without name. The string is resolved to a module which is loaded upon startup. + * configuration.entry should be an array: + [non-empty string]" +`) + ); + + createTestCase( + "invalid instanceof", { - name: "empty entry string", - config: { - entry: "" - }, - message: [ - " - configuration.entry should be one of these:", - " function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string]", - " -> The entry point(s) of the compilation.", - " Details:", - " * configuration.entry should be an instance of function", - " -> A Function returning an entry object, an entry string, an entry array or a promise to these things.", - " * configuration.entry should be an object.", - " -> Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array.", - " * configuration.entry should not be empty.", - " -> An entry point without name. The string is resolved to a module which is loaded upon startup.", - " * configuration.entry should be an array:", - " [non-empty string]" - ] + entry: "a", + module: { + wrappedContextRegExp: 1337 + } }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.module.wrappedContextRegExp should be an instance of RegExp + -> Set the inner regular expression for partial dynamic dependencies" +`) + ); + + createTestCase( + "invalid minimum", { - name: "empty entry bundle array", - config: { - entry: { - bundle: [] - } - }, - message: [ - " - configuration.entry should be one of these:", - " function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string]", - " -> The entry point(s) of the compilation.", - " Details:", - " * configuration.entry should be an instance of function", - " -> A Function returning an entry object, an entry string, an entry array or a promise to these things.", - " * configuration.entry['bundle'] should be a string.", - " -> The string is resolved to a module which is loaded upon startup.", - " * configuration.entry['bundle'] should not be empty.", - " * configuration.entry should be a string.", - " -> An entry point without name. The string is resolved to a module which is loaded upon startup.", - " * configuration.entry should be an array:", - " [non-empty string]" - ] + entry: "a", + parallelism: 0 }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.parallelism should be >= 1. + -> The number of parallel processed modules in the compilation." +`) + ); + + createTestCase( + "repeated value", { - name: "invalid instanceof", - config: { - entry: "a", - module: { - wrappedContextRegExp: 1337 - } - }, - message: [ - " - configuration.module.wrappedContextRegExp should be an instance of RegExp", - " -> Set the inner regular expression for partial dynamic dependencies" - ] + entry: ["abc", "def", "abc"] }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.entry should be one of these: + function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string] + -> The entry point(s) of the compilation. + Details: + * configuration.entry should be an instance of function + -> A Function returning an entry object, an entry string, an entry array or a promise to these things. + * configuration.entry should be an object. + -> Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array. + * configuration.entry should be a string. + -> An entry point without name. The string is resolved to a module which is loaded upon startup. + * configuration.entry should not contain the item 'abc' twice." +`) + ); + + createTestCase( + "multiple errors", { - name: "invalid minimum", - config: { - entry: "a", - parallelism: 0 - }, - message: [ - " - configuration.parallelism should be >= 1.", - " -> The number of parallel processed modules in the compilation." - ] + entry: [/a/], + output: { + filename: /a/ + } }, - { - name: "repeated value", - config: { - entry: ["abc", "def", "abc"] + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.entry should be one of these: + function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string] + -> The entry point(s) of the compilation. + Details: + * configuration.entry should be an instance of function + -> A Function returning an entry object, an entry string, an entry array or a promise to these things. + * configuration.entry should be an object. + -> Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array. + * configuration.entry should be a string. + -> An entry point without name. The string is resolved to a module which is loaded upon startup. + * configuration.entry[0] should be a string. + -> A non-empty string + - configuration.output.filename should be one of these: + string | function + -> Specifies the name of each output file on disk. You must **not** specify an absolute path here! The \`output.path\` option determines the location on disk the files are written to, filename is used solely for naming the individual files. + Details: + * configuration.output.filename should be a string. + * configuration.output.filename should be an instance of function" +`) + ); + + createTestCase( + "multiple configurations", + [ + { + entry: [/a/] }, - message: [ - " - configuration.entry should be one of these:", - " function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string]", - " -> The entry point(s) of the compilation.", - " Details:", - " * configuration.entry should be an instance of function", - " -> A Function returning an entry object, an entry string, an entry array or a promise to these things.", - " * configuration.entry should be an object.", - " -> Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array.", - " * configuration.entry should be a string.", - " -> An entry point without name. The string is resolved to a module which is loaded upon startup.", - " * configuration.entry should not contain the item 'abc' twice." - ] - }, - { - name: "multiple errors", - config: { - entry: [/a/], + { + entry: "a", output: { filename: /a/ } - }, - message: [ - " - configuration.entry should be one of these:", - " function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string]", - " -> The entry point(s) of the compilation.", - " Details:", - " * configuration.entry should be an instance of function", - " -> A Function returning an entry object, an entry string, an entry array or a promise to these things.", - " * configuration.entry should be an object.", - " -> Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array.", - " * configuration.entry should be a string.", - " -> An entry point without name. The string is resolved to a module which is loaded upon startup.", - " * configuration.entry[0] should be a string.", - " -> A non-empty string", - " - configuration.output.filename should be one of these:", - " string | function", - " -> Specifies the name of each output file on disk. You must **not** specify an absolute path here! The `output.path` option determines the location on disk the files are written to, filename is used solely for naming the individual files.", - " Details:", - " * configuration.output.filename should be a string.", - " * configuration.output.filename should be an instance of function" - ] - }, - { - name: "multiple configurations", - config: [ - { - entry: [/a/] - }, - { - entry: "a", - output: { - filename: /a/ - } - } - ], - message: [ - " - configuration[0].entry should be one of these:", - " function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string]", - " -> The entry point(s) of the compilation.", - " Details:", - " * configuration[0].entry should be an instance of function", - " -> A Function returning an entry object, an entry string, an entry array or a promise to these things.", - " * configuration[0].entry should be an object.", - " -> Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array.", - " * configuration[0].entry should be a string.", - " -> An entry point without name. The string is resolved to a module which is loaded upon startup.", - " * configuration[0].entry[0] should be a string.", - " -> A non-empty string", - " - configuration[1].output.filename should be one of these:", - " string | function", - " -> Specifies the name of each output file on disk. You must **not** specify an absolute path here! The `output.path` option determines the location on disk the files are written to, filename is used solely for naming the individual files.", - " Details:", - " * configuration[1].output.filename should be a string.", - " * configuration[1].output.filename should be an instance of function" - ] - }, + } + ], + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration[0].entry should be one of these: + function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string] + -> The entry point(s) of the compilation. + Details: + * configuration[0].entry should be an instance of function + -> A Function returning an entry object, an entry string, an entry array or a promise to these things. + * configuration[0].entry should be an object. + -> Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array. + * configuration[0].entry should be a string. + -> An entry point without name. The string is resolved to a module which is loaded upon startup. + * configuration[0].entry[0] should be a string. + -> A non-empty string + - configuration[1].output.filename should be one of these: + string | function + -> Specifies the name of each output file on disk. You must **not** specify an absolute path here! The \`output.path\` option determines the location on disk the files are written to, filename is used solely for naming the individual files. + Details: + * configuration[1].output.filename should be a string. + * configuration[1].output.filename should be an instance of function" +`) + ); + + createTestCase( + "deep error", { - name: "deep error", - config: { - entry: "a", - module: { - rules: [ - { - oneOf: [ - { - test: "/a", - passer: { - amd: false - } + entry: "a", + module: { + rules: [ + { + oneOf: [ + { + test: "/a", + passer: { + amd: false } - ] - } - ] - } - }, - message: [ - " - configuration.module.rules[0].oneOf[0] has an unknown property 'passer'. These properties are valid:", - " object { compiler?, enforce?, exclude?, include?, issuer?, loader?, loaders?, oneOf?, options?, parser?, query?, resolve?, resource?, resourceQuery?, rules?, sideEffects?, test?, type?, use? }", - " -> A rule" - ] + } + ] + } + ] + } }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.module.rules[0].oneOf[0] has an unknown property 'passer'. These properties are valid: + object { compiler?, enforce?, exclude?, include?, issuer?, loader?, loaders?, oneOf?, options?, parser?, query?, resolve?, resource?, resourceQuery?, rules?, sideEffects?, test?, type?, use? } + -> A rule" +`) + ); + + createTestCase( + "additional key on root", { - name: "additional key on root", - config: { - entry: "a", - postcss: () => {} - }, - message: [ - " - configuration has an unknown property 'postcss'. These properties are valid:", - " object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, externals?, loader?, mode?, module?, " + - "name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, " + - "recordsPath?, resolve?, resolveLoader?, serve?, stats?, target?, watch?, watchOptions? }", - " For typos: please correct them.", - " For loader options: webpack >= v2.0.0 no longer allows custom properties in configuration.", - " Loaders should be updated to allow passing options via loader options in module.rules.", - " Until loaders are updated one can use the LoaderOptionsPlugin to pass these options to the loader:", - " plugins: [", - " new webpack.LoaderOptionsPlugin({", - " // test: /\\.xxx$/, // may apply this only for some modules", - " options: {", - " postcss: …", - " }", - " })", - " ]" - ] + entry: "a", + postcss: () => {} }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration has an unknown property 'postcss'. These properties are valid: + object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, externals?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, serve?, stats?, target?, watch?, watchOptions? } + For typos: please correct them. + For loader options: webpack >= v2.0.0 no longer allows custom properties in configuration. + Loaders should be updated to allow passing options via loader options in module.rules. + Until loaders are updated one can use the LoaderOptionsPlugin to pass these options to the loader: + plugins: [ + new webpack.LoaderOptionsPlugin({ + // test: /\\\\.xxx$/, // may apply this only for some modules + options: { + postcss: … + } + }) + ]" +`) + ); + + createTestCase( + "enum", { - name: "enum", - config: { - entry: "a", - devtool: true - }, - message: [ - " - configuration.devtool should be one of these:", - " string | false", - " -> A developer tool to enhance debugging.", - " Details:", - " * configuration.devtool should be a string.", - " * configuration.devtool should be false" - ] + entry: "a", + devtool: true }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.devtool should be one of these: + string | false + -> A developer tool to enhance debugging. + Details: + * configuration.devtool should be a string. + * configuration.devtool should be false" +`) + ); + + createTestCase( + "! in path", { - name: "! in path", - config: { - entry: "foo.js", - output: { - path: "/somepath/!test", - filename: "bar" - } - }, - message: [ - ' - configuration.output.path: The provided value "/somepath/!test" contains exclamation mark (!) which is not allowed because it\'s reserved for loader syntax.', - " -> The output directory as **absolute path** (required)." - ] + entry: "foo.js", + output: { + path: "/somepath/!test", + filename: "bar" + } }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.output.path: The provided value \\"/somepath/!test\\" contains exclamation mark (!) which is not allowed because it's reserved for loader syntax. + -> The output directory as **absolute path** (required)." +`) + ); + + createTestCase( + "relative path", { - name: "relative path", - config: { - entry: "foo.js", - output: { - filename: "/bar" - } - }, - message: [ - " - configuration.output.filename should be one of these:", - " string | function", - " -> Specifies the name of each output file on disk. You must **not** specify an absolute path here! The `output.path` option determines the location on disk the files are written to, filename is used solely for naming the individual files.", - " Details:", - ' * configuration.output.filename: A relative path is expected. However, the provided value "/bar" is an absolute path!', - " Please use output.path to specify absolute path and output.filename for the file name.", - " * configuration.output.filename should be an instance of function" - ] + entry: "foo.js", + output: { + filename: "/bar" + } }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.output.filename should be one of these: + string | function + -> Specifies the name of each output file on disk. You must **not** specify an absolute path here! The \`output.path\` option determines the location on disk the files are written to, filename is used solely for naming the individual files. + Details: + * configuration.output.filename: A relative path is expected. However, the provided value \\"/bar\\" is an absolute path! + Please use output.path to specify absolute path and output.filename for the file name. + * configuration.output.filename should be an instance of function" +`) + ); + + createTestCase( + "absolute path", { - name: "absolute path", - config: { - entry: "foo.js", - output: { - filename: "bar" - }, - context: "baz" + entry: "foo.js", + output: { + filename: "bar" }, - message: [ - ' - configuration.context: The provided value "baz" is not an absolute path!', - " -> The base directory (absolute path!) for resolving the `entry` option. If `output.pathinfo` is set, the included pathinfo is shortened to this directory." - ] + context: "baz" }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.context: The provided value \\"baz\\" is not an absolute path! + -> The base directory (absolute path!) for resolving the \`entry\` option. If \`output.pathinfo\` is set, the included pathinfo is shortened to this directory." +`) + ); + + createTestCase( + "missing stats option", { - name: "missing stats option", - config: { - entry: "foo.js", - stats: { - foobar: true - } - }, - test(err) { - expect(err.message).toMatch(/^Invalid configuration object./); - expect(err.message.split("\n").slice(1)[0]).toBe( - " - configuration.stats should be one of these:" - ); + entry: "foo.js", + stats: { + foobar: true } }, + msg => { + expect( + msg + .replace(/object \{ .* \}/g, "object {...}") + .replace(/"none" \| .+/g, '"none" | ...') + ).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.stats should be one of these: + object {...} | boolean | \\"none\\" | ... + -> Used by the webpack CLI program to pass stats options. + Details: + * configuration.stats has an unknown property 'foobar'. These properties are valid: + object {...} + * configuration.stats should be a boolean. + * configuration.stats should be one of these: + \\"none\\" | ..." +`); + } + ); + + createTestCase( + "Invalid plugin provided: bool", { - name: "Invalid plugin provided: bool", - config: { - entry: "foo.js", - plugins: [false] - }, - message: [ - " - configuration.plugins[0] should be one of these:", - " object { apply, … } | function", - " -> Plugin of type object or instanceof Function", - " Details:", - " * configuration.plugins[0] should be an object.", - " -> Plugin instance", - " * configuration.plugins[0] should be an instance of function", - " -> Function acting as plugin" - ] + entry: "foo.js", + plugins: [false] }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.plugins[0] should be one of these: + object { apply, … } | function + -> Plugin of type object or instanceof Function + Details: + * configuration.plugins[0] should be an object. + -> Plugin instance + * configuration.plugins[0] should be an instance of function + -> Function acting as plugin" +`) + ); + + createTestCase( + "Invalid plugin provided: array", { - name: "Invalid plugin provided: array", - config: { - entry: "foo.js", - plugins: [[]] - }, - message: [ - " - configuration.plugins[0] should be one of these:", - " object { apply, … } | function", - " -> Plugin of type object or instanceof Function", - " Details:", - " * configuration.plugins[0] should be an object.", - " -> Plugin instance", - " * configuration.plugins[0] should be an instance of function", - " -> Function acting as plugin" - ] + entry: "foo.js", + plugins: [[]] }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.plugins[0] should be one of these: + object { apply, … } | function + -> Plugin of type object or instanceof Function + Details: + * configuration.plugins[0] should be an object. + -> Plugin instance + * configuration.plugins[0] should be an instance of function + -> Function acting as plugin" +`) + ); + + createTestCase( + "Invalid plugin provided: string", { - name: "Invalid plugin provided: string", - config: { - entry: "foo.js", - plugins: ["abc123"] - }, - message: [ - " - configuration.plugins[0] should be one of these:", - " object { apply, … } | function", - " -> Plugin of type object or instanceof Function", - " Details:", - " * configuration.plugins[0] should be an object.", - " -> Plugin instance", - " * configuration.plugins[0] should be an instance of function", - " -> Function acting as plugin" - ] + entry: "foo.js", + plugins: ["abc123"] }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.plugins[0] should be one of these: + object { apply, … } | function + -> Plugin of type object or instanceof Function + Details: + * configuration.plugins[0] should be an object. + -> Plugin instance + * configuration.plugins[0] should be an instance of function + -> Function acting as plugin" +`) + ); + + createTestCase( + "Invalid plugin provided: int", { - name: "Invalid plugin provided: int", - config: { - entry: "foo.js", - plugins: [12] - }, - message: [ - " - configuration.plugins[0] should be one of these:", - " object { apply, … } | function", - " -> Plugin of type object or instanceof Function", - " Details:", - " * configuration.plugins[0] should be an object.", - " -> Plugin instance", - " * configuration.plugins[0] should be an instance of function", - " -> Function acting as plugin" - ] + entry: "foo.js", + plugins: [12] }, - { - name: "Invalid plugin provided: object without apply function", - config: { - entry: "foo.js", - plugins: [{}] - }, - message: [ - " - configuration.plugins[0] should be one of these:", - " object { apply, … } | function", - " -> Plugin of type object or instanceof Function", - " Details:", - " * configuration.plugins[0] misses the property 'apply'.", - " function", - " -> The run point of the plugin, required method.", - " * configuration.plugins[0] should be an instance of function", - " -> Function acting as plugin" - ] - } - ]; - - testCases.forEach(testCase => { - it("should fail validation for " + testCase.name, () => { - try { - webpack(testCase.config); - } catch (err) { - if (err.name !== "WebpackOptionsValidationError") throw err; + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.plugins[0] should be one of these: + object { apply, … } | function + -> Plugin of type object or instanceof Function + Details: + * configuration.plugins[0] should be an object. + -> Plugin instance + * configuration.plugins[0] should be an instance of function + -> Function acting as plugin" +`) + ); - if (testCase.test) { - testCase.test(err); - - return; - } - - expect(err.message).toMatch(/^Invalid configuration object./); - expect(err.message.split("\n").slice(1)).toEqual(testCase.message); - - return; - } - - throw new Error("Validation didn't fail"); - }); - }); + createTestCase( + "Invalid plugin provided: object without apply function", + { + entry: "foo.js", + plugins: [{}] + }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.plugins[0] should be one of these: + object { apply, … } | function + -> Plugin of type object or instanceof Function + Details: + * configuration.plugins[0] misses the property 'apply'. + function + -> The run point of the plugin, required method. + * configuration.plugins[0] should be an instance of function + -> Function acting as plugin" +`) + ); }); From 0963d744e9bbfbec52e32d1663df03ff0b0ba339 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 21 May 2019 10:38:37 +0200 Subject: [PATCH 2/3] add test case for enum validation --- lib/WebpackOptionsValidationError.js | 8 +++++--- test/Validation.test.js | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/WebpackOptionsValidationError.js b/lib/WebpackOptionsValidationError.js index d66cb2ccca2..62f53f81f43 100644 --- a/lib/WebpackOptionsValidationError.js +++ b/lib/WebpackOptionsValidationError.js @@ -154,6 +154,10 @@ class WebpackOptionsValidationError extends WebpackError { return "RegExp"; } + if (schema.enum) { + return schema.enum.map(item => JSON.stringify(item)).join(" | "); + } + if (schema.$ref) { return formatInnerSchema(getSchemaPart(schema.$ref), true); } @@ -166,9 +170,6 @@ class WebpackOptionsValidationError extends WebpackError { 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, null, 2); } @@ -250,6 +251,7 @@ class WebpackOptionsValidationError extends WebpackError { err.parentSchema )}`; } else if (err.keyword === "enum") { + console.log(err.parentSchema); if ( err.parentSchema && err.parentSchema.enum && diff --git a/test/Validation.test.js b/test/Validation.test.js index df78b93c1d6..47314e32ac2 100644 --- a/test/Validation.test.js +++ b/test/Validation.test.js @@ -458,6 +458,20 @@ describe("Validation", () => { -> The run point of the plugin, required method. * configuration.plugins[0] should be an instance of function -> Function acting as plugin" +`) + ); + + createTestCase( + "invalid mode", + { + mode: "protuction" + }, + msg => + expect(msg).toMatchInlineSnapshot(` +"Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. + - configuration.mode should be one of these: + \\"development\\" | \\"production\\" | \\"none\\" + -> Enable production optimizations or development hints." `) ); }); From a4406ff187e09e701ff77c6b2ecfafab98e09f10 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 21 May 2019 11:27:45 +0200 Subject: [PATCH 3/3] improve validation errors --- declarations/WebpackOptions.d.ts | 2 + lib/WebpackOptionsValidationError.js | 56 ++++++++++++--- schemas/WebpackOptions.json | 1 + test/Validation.test.js | 102 +++++---------------------- 4 files changed, 68 insertions(+), 93 deletions(-) diff --git a/declarations/WebpackOptions.d.ts b/declarations/WebpackOptions.d.ts index d6b2c999610..7e45ddbd437 100644 --- a/declarations/WebpackOptions.d.ts +++ b/declarations/WebpackOptions.d.ts @@ -22,6 +22,8 @@ export type EntryDynamic = () => EntryStatic | Promise; */ export type EntryStatic = EntryObject | EntryItem; /** + * A non-empty array of non-empty strings + * * This interface was referenced by `WebpackOptions`'s JSON-Schema * via the `definition` "NonEmptyArrayOfUniqueStringValues". */ diff --git a/lib/WebpackOptionsValidationError.js b/lib/WebpackOptionsValidationError.js index 62f53f81f43..b3be3012df8 100644 --- a/lib/WebpackOptionsValidationError.js +++ b/lib/WebpackOptionsValidationError.js @@ -50,13 +50,34 @@ const getSchemaPartDescription = schemaPart => { 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 => { - return children.filter( - err => - err.keyword !== "anyOf" && - err.keyword !== "allOf" && - err.keyword !== "oneOf" + 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) => { @@ -230,11 +251,17 @@ class WebpackOptionsValidationError extends WebpackError { }) ); } + 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${filterChildren(err.children) + `Details:\n${children .map( err => " * " + @@ -251,7 +278,6 @@ class WebpackOptionsValidationError extends WebpackError { err.parentSchema )}`; } else if (err.keyword === "enum") { - console.log(err.parentSchema); if ( err.parentSchema && err.parentSchema.enum && @@ -314,7 +340,21 @@ class WebpackOptionsValidationError extends WebpackError { err.keyword === "minProperties" ) { if (err.params.limit === 1) { - return `${dataPath} should not be empty.${getSchemaPartDescription( + 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 { diff --git a/schemas/WebpackOptions.json b/schemas/WebpackOptions.json index 905c6e80480..53e0948f437 100644 --- a/schemas/WebpackOptions.json +++ b/schemas/WebpackOptions.json @@ -385,6 +385,7 @@ } }, "NonEmptyArrayOfUniqueStringValues": { + "description": "A non-empty array of non-empty strings", "type": "array", "items": { "description": "A non-empty string", diff --git a/test/Validation.test.js b/test/Validation.test.js index 47314e32ac2..e82f8de54fa 100644 --- a/test/Validation.test.js +++ b/test/Validation.test.js @@ -43,18 +43,8 @@ describe("Validation", () => { msg => expect(msg).toMatchInlineSnapshot(` "Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. - - configuration.entry should be one of these: - function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string] - -> The entry point(s) of the compilation. - Details: - * configuration.entry should be an instance of function - -> A Function returning an entry object, an entry string, an entry array or a promise to these things. - * configuration.entry should be an object. - -> Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array. - * configuration.entry should not be empty. - -> An entry point without name. The string is resolved to a module which is loaded upon startup. - * configuration.entry should be an array: - [non-empty string]" + - configuration.entry should be an non-empty string. + -> An entry point without name. The string is resolved to a module which is loaded upon startup." `) ); @@ -68,19 +58,8 @@ describe("Validation", () => { msg => expect(msg).toMatchInlineSnapshot(` "Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. - - configuration.entry should be one of these: - function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string] - -> The entry point(s) of the compilation. - Details: - * configuration.entry should be an instance of function - -> A Function returning an entry object, an entry string, an entry array or a promise to these things. - * configuration.entry['bundle'] should be a string. - -> The string is resolved to a module which is loaded upon startup. - * configuration.entry['bundle'] should not be empty. - * configuration.entry should be a string. - -> An entry point without name. The string is resolved to a module which is loaded upon startup. - * configuration.entry should be an array: - [non-empty string]" + - configuration.entry['bundle'] should be an non-empty array. + -> A non-empty array of non-empty strings" `) ); @@ -122,17 +101,8 @@ describe("Validation", () => { msg => expect(msg).toMatchInlineSnapshot(` "Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. - - configuration.entry should be one of these: - function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string] - -> The entry point(s) of the compilation. - Details: - * configuration.entry should be an instance of function - -> A Function returning an entry object, an entry string, an entry array or a promise to these things. - * configuration.entry should be an object. - -> Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array. - * configuration.entry should be a string. - -> An entry point without name. The string is resolved to a module which is loaded upon startup. - * configuration.entry should not contain the item 'abc' twice." + - configuration.entry should not contain the item 'abc' twice. + -> A non-empty array of non-empty strings" `) ); @@ -147,18 +117,8 @@ describe("Validation", () => { msg => expect(msg).toMatchInlineSnapshot(` "Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. - - configuration.entry should be one of these: - function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string] - -> The entry point(s) of the compilation. - Details: - * configuration.entry should be an instance of function - -> A Function returning an entry object, an entry string, an entry array or a promise to these things. - * configuration.entry should be an object. - -> Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array. - * configuration.entry should be a string. - -> An entry point without name. The string is resolved to a module which is loaded upon startup. - * configuration.entry[0] should be a string. - -> A non-empty string + - configuration.entry[0] should be a string. + -> A non-empty string - configuration.output.filename should be one of these: string | function -> Specifies the name of each output file on disk. You must **not** specify an absolute path here! The \`output.path\` option determines the location on disk the files are written to, filename is used solely for naming the individual files. @@ -184,18 +144,8 @@ describe("Validation", () => { msg => expect(msg).toMatchInlineSnapshot(` "Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. - - configuration[0].entry should be one of these: - function | object { : non-empty string | [non-empty string] } | non-empty string | [non-empty string] - -> The entry point(s) of the compilation. - Details: - * configuration[0].entry should be an instance of function - -> A Function returning an entry object, an entry string, an entry array or a promise to these things. - * configuration[0].entry should be an object. - -> Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array. - * configuration[0].entry should be a string. - -> An entry point without name. The string is resolved to a module which is loaded upon startup. - * configuration[0].entry[0] should be a string. - -> A non-empty string + - configuration[0].entry[0] should be a string. + -> A non-empty string - configuration[1].output.filename should be one of these: string | function -> Specifies the name of each output file on disk. You must **not** specify an absolute path here! The \`output.path\` option determines the location on disk the files are written to, filename is used solely for naming the individual files. @@ -305,13 +255,8 @@ describe("Validation", () => { msg => expect(msg).toMatchInlineSnapshot(` "Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. - - configuration.output.filename should be one of these: - string | function - -> Specifies the name of each output file on disk. You must **not** specify an absolute path here! The \`output.path\` option determines the location on disk the files are written to, filename is used solely for naming the individual files. - Details: - * configuration.output.filename: A relative path is expected. However, the provided value \\"/bar\\" is an absolute path! - Please use output.path to specify absolute path and output.filename for the file name. - * configuration.output.filename should be an instance of function" + - configuration.output.filename: A relative path is expected. However, the provided value \\"/bar\\" is an absolute path! + Please use output.path to specify absolute path and output.filename for the file name." `) ); @@ -347,15 +292,8 @@ describe("Validation", () => { .replace(/"none" \| .+/g, '"none" | ...') ).toMatchInlineSnapshot(` "Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. - - configuration.stats should be one of these: - object {...} | boolean | \\"none\\" | ... - -> Used by the webpack CLI program to pass stats options. - Details: - * configuration.stats has an unknown property 'foobar'. These properties are valid: - object {...} - * configuration.stats should be a boolean. - * configuration.stats should be one of these: - \\"none\\" | ..." + - configuration.stats has an unknown property 'foobar'. These properties are valid: + object {...}" `); } ); @@ -449,15 +387,9 @@ describe("Validation", () => { msg => expect(msg).toMatchInlineSnapshot(` "Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. - - configuration.plugins[0] should be one of these: - object { apply, … } | function - -> Plugin of type object or instanceof Function - Details: - * configuration.plugins[0] misses the property 'apply'. - function - -> The run point of the plugin, required method. - * configuration.plugins[0] should be an instance of function - -> Function acting as plugin" + - configuration.plugins[0] misses the property 'apply'. + function + -> The run point of the plugin, required method." `) );