diff --git a/lib/NormalModule.js b/lib/NormalModule.js index 4fafd5c53..1f8ea1818 100644 --- a/lib/NormalModule.js +++ b/lib/NormalModule.js @@ -5,7 +5,10 @@ "use strict"; +const parseJson = require("json-parse-better-errors"); const { getContext, runLoaders } = require("loader-runner"); +const querystring = require("querystring"); +const validateOptions = require("schema-utils"); const { SyncHook } = require("tapable"); const { CachedSource, @@ -350,6 +353,50 @@ class NormalModule extends Module { }; const loaderContext = { version: 2, + getOptions: (loaderName, schema) => { + const loader = loaderContext.loaders[loaderContext.loaderIndex]; + let options = {}; + + if (loader.options && typeof loader.options === "object") { + ({ options } = loader); + } else if (loader.query) { + let { query } = loader; + + if (query.substr(0, 1) !== "?") { + throw new WebpackError( + "A valid query string should begin with '?'" + ); + } + + query = query.substr(1); + + // Allow to use `?foo=bar` in `options` + if (query.substr(0, 1) === "?") { + query = query.substr(1); + } + + if (query.substr(0, 1) === "{" && query.substr(-1) === "}") { + try { + options = parseJson(query); + } catch (e) { + throw new WebpackError(`Cannot parse query string: ${e.message}`); + } + } else { + options = querystring.parse(query); + } + } + + if (!schema) { + return options; + } + + validateOptions(schema, options, { + name: loaderName || "Unknown Loader", + baseDataPath: "options" + }); + + return options; + }, emitWarning: warning => { if (!(warning instanceof Error)) { warning = new NonErrorEmittedError(warning); diff --git a/test/configCases/loaders/options/a.js b/test/configCases/loaders/options/a.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/loaders/options/b.js b/test/configCases/loaders/options/b.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/loaders/options/c.js b/test/configCases/loaders/options/c.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/loaders/options/d.js b/test/configCases/loaders/options/d.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/loaders/options/e.js b/test/configCases/loaders/options/e.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/loaders/options/f.js b/test/configCases/loaders/options/f.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/loaders/options/g.js b/test/configCases/loaders/options/g.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/loaders/options/h.js b/test/configCases/loaders/options/h.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/loaders/options/i.js b/test/configCases/loaders/options/i.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/loaders/options/index.js b/test/configCases/loaders/options/index.js new file mode 100644 index 000000000..57ddc5110 --- /dev/null +++ b/test/configCases/loaders/options/index.js @@ -0,0 +1,45 @@ +it("should get options", function() { + expect(require("./a")).toStrictEqual({ + arg: true, + arg1: null, + arg3: 1234567890, + arg4: "string", + arg5: [1, 2, 3], + arg6: { foo: "value", bar: { baz: "other-value" } } + }); + expect(require("./b")).toStrictEqual({ + arg: true, + arg1: null, + arg3: 1234567890, + arg4: "string", + arg5: [1, 2, 3], + arg6: { foo: "value", bar: { baz: "other-value" } } + }); + expect(require("./c")).toStrictEqual({ + arg: true, + arg1: null, + arg3: 1234567890, + arg4: "string", + arg5: [1, 2, 3], + arg6: { foo: "value", bar: { baz: "other-value" } } + }); + expect(require("./d")).toStrictEqual({ + arg4: "text" + }); + expect(require("./e")).toStrictEqual({}); + expect(require("./f")).toStrictEqual({ + delicious: "", + name: "cheesecake", + slices: "8", + warm: "false" + }); + expect(require("./g")).toStrictEqual({ + "=": "=" + }); + expect(require("./h")).toStrictEqual({ + "foo": "bar" + }); + expect(require("./i")).toStrictEqual({ + "foo": "bar" + }); +}); diff --git a/test/configCases/loaders/options/loader-1.js b/test/configCases/loaders/options/loader-1.js new file mode 100644 index 000000000..5c8b11efa --- /dev/null +++ b/test/configCases/loaders/options/loader-1.js @@ -0,0 +1,12 @@ +const schema = require('./loader-1.options'); + +module.exports = function() { + const options = this.getOptions('Loader Name', schema); + + const json = JSON.stringify(options) + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); + + + return `module.exports = ${json}`; +}; diff --git a/test/configCases/loaders/options/loader-1.options.json b/test/configCases/loaders/options/loader-1.options.json new file mode 100644 index 000000000..3c86ba010 --- /dev/null +++ b/test/configCases/loaders/options/loader-1.options.json @@ -0,0 +1,43 @@ +{ + "additionalProperties": false, + "properties": { + "arg": { + "type": "boolean" + }, + "arg1": { + "type": "null" + }, + "arg2": {}, + "arg3": { + "type": "number" + }, + "arg4": { + "type": "string" + }, + "arg5": { + "type": "array", + "items": { + "type": "number" + } + }, + "arg6": { + "type": "object", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "object", + "properties": { + "baz": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "type": "object" +} diff --git a/test/configCases/loaders/options/loader.js b/test/configCases/loaders/options/loader.js new file mode 100644 index 000000000..5b9386651 --- /dev/null +++ b/test/configCases/loaders/options/loader.js @@ -0,0 +1,9 @@ +module.exports = function() { + const options = this.getOptions(); + + const json = JSON.stringify(options) + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); + + return `module.exports = ${json}`; +}; diff --git a/test/configCases/loaders/options/webpack.config.js b/test/configCases/loaders/options/webpack.config.js new file mode 100644 index 000000000..0f343bd7b --- /dev/null +++ b/test/configCases/loaders/options/webpack.config.js @@ -0,0 +1,78 @@ +module.exports = { + mode: "none", + module: { + rules: [ + { + test: /a\.js$/, + loader: "./loader", + options: { + arg: true, + arg1: null, + arg2: undefined, + arg3: 1234567890, + arg4: "string", + arg5: [1, 2, 3], + arg6: { foo: "value", bar: { baz: "other-value" } } + } + }, + { + test: /b\.js$/, + loader: "./loader-1", + options: { + arg: true, + arg1: null, + arg2: undefined, + arg3: 1234567890, + arg4: "string", + arg5: [1, 2, 3], + arg6: { foo: "value", bar: { baz: "other-value" } } + } + }, + { + test: /c\.js$/, + loader: "./loader-1", + options: JSON.stringify({ + arg: true, + arg1: null, + arg2: undefined, + arg3: 1234567890, + arg4: "string", + arg5: [1, 2, 3], + arg6: { foo: "value", bar: { baz: "other-value" } } + }) + }, + { + test: /d\.js$/, + loader: "./loader-1", + options: "arg4=text" + }, + { + test: /d\.js$/, + loader: "./loader", + options: "?" + }, + { + test: /f\.js$/, + loader: "./loader", + options: "name=cheesecake&slices=8&delicious&warm=false" + }, + { + test: /g\.js$/, + loader: "./loader", + options: "%3d=%3D" + }, + { + test: /h\.js$/, + loader: "./loader", + options: "?foo=bar" + }, + { + test: /i\.js$/, + loader: "./loader", + options: `?${JSON.stringify({ + foo: "bar" + })}` + } + ] + } +};