diff --git a/.changeset/slow-oranges-repair.md b/.changeset/slow-oranges-repair.md new file mode 100644 index 00000000000..a0c6e758a14 --- /dev/null +++ b/.changeset/slow-oranges-repair.md @@ -0,0 +1,5 @@ +--- +"webpack-cli": patch +--- + +The `file` protocol for configuration options (`--config`/`--extends`) is supported. diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index dd88847a165..edae3031ccb 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { type Readable as ReadableType } from "node:stream"; -import { pathToFileURL } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import util from "node:util"; import { type stringifyChunked as stringifyChunkedType } from "@discoveryjs/json-ext"; import { @@ -2163,12 +2163,14 @@ class WebpackCLI { ): Promise<{ options: Configuration | MultiConfiguration; path: string }> => { let options: LoadableWebpackConfiguration | undefined; + const isFileURL = configPath.startsWith("file://"); + try { let loadingError; try { - // eslint-disable-next-line no-eval - options = (await eval(`import("${pathToFileURL(configPath)}")`)).default; + options = // eslint-disable-next-line no-eval + (await eval(`import("${isFileURL ? configPath : pathToFileURL(configPath)}")`)).default; } catch (err) { if (this.isValidationError(err) || process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) { throw err; @@ -2209,7 +2211,7 @@ class WebpackCLI { } try { - options = require(configPath); + options = require(isFileURL ? fileURLToPath(configPath) : path.resolve(configPath)); } catch (err) { if (this.isValidationError(err)) { throw err; @@ -2301,9 +2303,7 @@ class WebpackCLI { if (options.config && options.config.length > 0) { const loadedConfigs = await Promise.all( - options.config.map((configPath: string) => - loadConfigByPath(path.resolve(configPath), options.argv), - ), + options.config.map((configPath: string) => loadConfigByPath(configPath, options.argv)), ); if (loadedConfigs.length === 1) { @@ -2406,9 +2406,7 @@ class WebpackCLI { delete config.extends; const loadedConfigs = await Promise.all( - extendsPaths.map((extendsPath) => - loadConfigByPath(path.resolve(extendsPath), options.argv), - ), + extendsPaths.map((extendsPath) => loadConfigByPath(extendsPath, options.argv)), ); const { merge } = await import("webpack-merge"); diff --git a/test/build/basic/basic.test.js b/test/build/basic/basic.test.js index 92033d98ba3..6dfebe00edc 100644 --- a/test/build/basic/basic.test.js +++ b/test/build/basic/basic.test.js @@ -1,6 +1,5 @@ "use strict"; -const { resolve } = require("node:path"); const { run } = require("../../utils/test-utils"); describe("bundle command", () => { @@ -181,11 +180,10 @@ describe("bundle command", () => { it("should log supplied config when logging level is log", async () => { const { exitCode, stderr, stdout } = await run(__dirname, ["--config", "./log.config.js"]); - const configPath = resolve(__dirname, "./log.config.js"); expect(exitCode).toBe(0); expect(stderr).toContain("Compiler starting..."); - expect(stderr).toContain(`Compiler is using config: '${configPath}'`); + expect(stderr).toContain("Compiler is using config: './log.config.js'"); expect(stderr).toContain("Compiler finished"); expect(stdout).toBeTruthy(); }); diff --git a/test/build/config-format/failure/failure.test.js b/test/build/config-format/failure/failure.test.js index 0f17f96b481..fbdcfe06b77 100644 --- a/test/build/config-format/failure/failure.test.js +++ b/test/build/config-format/failure/failure.test.js @@ -1,5 +1,3 @@ -const path = require("node:path"); - const { run } = require("../../../utils/test-utils"); describe("failure", () => { @@ -7,9 +5,7 @@ describe("failure", () => { const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "webpack.config.iced"]); expect(exitCode).toBe(2); - expect(stderr).toContain( - `Failed to load '${path.resolve(__dirname, "./webpack.config.iced")}'`, - ); + expect(stderr).toContain("Failed to load 'webpack.config.iced'"); expect(stdout).toBeFalsy(); }); }); diff --git a/test/build/config-format/failure/webpack.config.json5 b/test/build/config-format/failure/webpack.config.json5 deleted file mode 100644 index 43fdd4c1594..00000000000 --- a/test/build/config-format/failure/webpack.config.json5 +++ /dev/null @@ -1,4 +0,0 @@ -{ - mode: "development", - entry: "./main.js", -} diff --git a/test/build/config/basic/basic-config.test.js b/test/build/config/basic/basic-config.test.js index f971259aa1f..0bc76e25fd4 100644 --- a/test/build/config/basic/basic-config.test.js +++ b/test/build/config/basic/basic-config.test.js @@ -1,18 +1,53 @@ "use strict"; const { resolve } = require("node:path"); +const { pathToFileURL } = require("node:url"); const { run } = require("../../../utils/test-utils"); describe("basic config file", () => { it("should build and not throw error with a basic configuration file", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "webpack.config.js"]); + console.log(stdout); + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + }); + + it("should build and not throw error with a basic configuration file using relative path", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.js"]); + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + }); + + it("should build and not throw error with a basic configuration file using absolute path", async () => { const { exitCode, stderr, stdout } = await run(__dirname, [ "-c", resolve(__dirname, "webpack.config.js"), - "--output-path", - "./binary", ]); expect(exitCode).toBe(0); expect(stderr).toBeFalsy(); expect(stdout).toBeTruthy(); }); + + it("should build and not throw error with a basic configuration file using file protocol", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "-c", + pathToFileURL(resolve(__dirname, "webpack.config.js")).toString(), + ]); + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + }); + + it("should build and not throw error with a basic using weird path", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "-c", + "../basic/./webpack.config.js", + ]); + console.log(stdout); + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + }); }); diff --git a/test/build/config/error-array/__snapshots__/config-array-error.test.js.snap.webpack5 b/test/build/config/error-array/__snapshots__/config-array-error.test.js.snap.webpack5 index 837aa696909..3b3b72ef37d 100644 --- a/test/build/config/error-array/__snapshots__/config-array-error.test.js.snap.webpack5 +++ b/test/build/config/error-array/__snapshots__/config-array-error.test.js.snap.webpack5 @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`config with invalid array syntax should throw syntax error and exit with non-zero exit code when even 1 object has syntax error 1`] = ` -"[webpack-cli] Failed to load '/test/build/config/error-array/webpack.config.js' config +"[webpack-cli] Failed to load './webpack.config.js' config ▶ ESM (\`import\`) failed: Unexpected token ';' diff --git a/test/build/extends/extends.test.js b/test/build/extends/extends.test.js index 09ad57641d3..d1265ede8f2 100644 --- a/test/build/extends/extends.test.js +++ b/test/build/extends/extends.test.js @@ -1,6 +1,7 @@ "use strict"; const path = require("node:path"); +const { pathToFileURL } = require("node:url"); const { run } = require("../../utils/test-utils"); describe("extends property", () => { @@ -15,6 +16,38 @@ describe("extends property", () => { expect(stdout).toContain("mode: 'development'"); }); + it("extends a provided webpack config correctly using file protocol", async () => { + const { exitCode, stderr, stdout } = await run(path.resolve(__dirname, "./file-protocol")); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("base.webpack.config.js"); + expect(stdout).toContain("derived.webpack.config.js"); + expect(stdout).toContain("name: 'base_config'"); + expect(stdout).toContain("mode: 'development'"); + }); + + it("extends a provided webpack config correctly using `require.resolve`", async () => { + const { exitCode, stderr, stdout } = await run(path.resolve(__dirname, "./require-resolve")); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("base.webpack.config.js"); + expect(stdout).toContain("derived.webpack.config.js"); + expect(stdout).toContain("name: 'base_config'"); + expect(stdout).toContain("mode: 'development'"); + }); + + it("extends a provided webpack config correctly using `JSON` format", async () => { + const { exitCode, stderr, stdout } = await run(path.resolve(__dirname, "./json")); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("derived.webpack.config.js"); + expect(stdout).toContain("name: 'base_config'"); + expect(stdout).toContain("mode: 'development'"); + }); + it("extends a provided array of webpack configs correctly", async () => { const { exitCode, stderr, stdout } = await run(path.resolve(__dirname, "./multiple-extends")); @@ -89,7 +122,7 @@ describe("extends property", () => { "--extends", "./base.webpack.config.js", "--extends", - "./other.config.js", + path.resolve(__dirname, "./multiple-configs1/other.config.js"), ]); expect(exitCode).toBe(0); @@ -102,7 +135,7 @@ describe("extends property", () => { expect(stdout).toContain("topLevelAwait: true"); }); - it("cLI `extends` should override `extends` in a configuration", async () => { + it("`extends` should override `extends` in a configuration", async () => { const { exitCode, stderr, stdout } = await run(path.resolve(__dirname, "./simple-case"), [ "--extends", "./override.config.js", @@ -116,6 +149,20 @@ describe("extends property", () => { expect(stdout).toContain("mode: 'development'"); }); + it("`extends` should override `extends` in a configuration using file protocol", async () => { + const { exitCode, stderr, stdout } = await run(path.resolve(__dirname, "./simple-case"), [ + "--extends", + pathToFileURL(path.resolve(__dirname, "./simple-case/override.config.js")).toString(), + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("override.config.js"); + expect(stdout).toContain("derived.webpack.config.js"); + expect(stdout).toContain("name: 'override_config'"); + expect(stdout).toContain("mode: 'development'"); + }); + it("should throw an error on recursive", async () => { const { exitCode, stderr, stdout } = await run(path.resolve(__dirname, "./recursive-extends")); diff --git a/test/build/extends/file-protocol/base.webpack.config.mjs b/test/build/extends/file-protocol/base.webpack.config.mjs new file mode 100644 index 00000000000..9f7bf79aa30 --- /dev/null +++ b/test/build/extends/file-protocol/base.webpack.config.mjs @@ -0,0 +1,9 @@ +export default () => { + console.log("base.webpack.config.js"); + + return { + name: "base_config", + mode: "development", + entry: "./src/index.js", + }; +}; diff --git a/test/build/extends/file-protocol/override.config.mjs b/test/build/extends/file-protocol/override.config.mjs new file mode 100644 index 00000000000..b30dfb4ca99 --- /dev/null +++ b/test/build/extends/file-protocol/override.config.mjs @@ -0,0 +1,9 @@ +export default () => { + console.log("override.config.js"); + + return { + name: "override_config", + mode: "development", + entry: "./src/index.js", + }; +}; diff --git a/test/build/extends/file-protocol/package.json b/test/build/extends/file-protocol/package.json new file mode 100644 index 00000000000..e8fed3d1cf9 --- /dev/null +++ b/test/build/extends/file-protocol/package.json @@ -0,0 +1,5 @@ +{ + "name": "test", + "version": "1.0.0", + "type": "module" +} diff --git a/test/build/extends/file-protocol/src/index.js b/test/build/extends/file-protocol/src/index.js new file mode 100644 index 00000000000..7a21c1b9d7d --- /dev/null +++ b/test/build/extends/file-protocol/src/index.js @@ -0,0 +1 @@ +console.log("index.js") diff --git a/test/build/extends/file-protocol/webpack.config.mjs b/test/build/extends/file-protocol/webpack.config.mjs new file mode 100644 index 00000000000..fd628a12a39 --- /dev/null +++ b/test/build/extends/file-protocol/webpack.config.mjs @@ -0,0 +1,10 @@ +import WebpackCLITestPlugin from "../../../utils/webpack-cli-test-plugin.js"; + +export default () => { + console.log("derived.webpack.config.js"); + + return { + extends: [import.meta.resolve("./base.webpack.config.mjs")], + plugins: [new WebpackCLITestPlugin()], + }; +}; diff --git a/test/build/extends/json/base.webpack.config.json b/test/build/extends/json/base.webpack.config.json new file mode 100644 index 00000000000..86265f6ec64 --- /dev/null +++ b/test/build/extends/json/base.webpack.config.json @@ -0,0 +1,5 @@ +{ + "name": "base_config", + "mode": "development", + "entry": "./src/index.js" +} diff --git a/test/build/extends/json/override.config.mjs b/test/build/extends/json/override.config.mjs new file mode 100644 index 00000000000..b30dfb4ca99 --- /dev/null +++ b/test/build/extends/json/override.config.mjs @@ -0,0 +1,9 @@ +export default () => { + console.log("override.config.js"); + + return { + name: "override_config", + mode: "development", + entry: "./src/index.js", + }; +}; diff --git a/test/build/extends/json/package.json b/test/build/extends/json/package.json new file mode 100644 index 00000000000..e8fed3d1cf9 --- /dev/null +++ b/test/build/extends/json/package.json @@ -0,0 +1,5 @@ +{ + "name": "test", + "version": "1.0.0", + "type": "module" +} diff --git a/test/build/extends/json/src/index.js b/test/build/extends/json/src/index.js new file mode 100644 index 00000000000..7a21c1b9d7d --- /dev/null +++ b/test/build/extends/json/src/index.js @@ -0,0 +1 @@ +console.log("index.js") diff --git a/test/build/extends/json/webpack.config.mjs b/test/build/extends/json/webpack.config.mjs new file mode 100644 index 00000000000..1b54f52900b --- /dev/null +++ b/test/build/extends/json/webpack.config.mjs @@ -0,0 +1,10 @@ +import WebpackCLITestPlugin from "../../../utils/webpack-cli-test-plugin.js"; + +export default () => { + console.log("derived.webpack.config.js"); + + return { + extends: [import.meta.resolve("./base.webpack.config.json")], + plugins: [new WebpackCLITestPlugin()], + }; +}; diff --git a/test/build/extends/require-resolve/base.webpack.config.js b/test/build/extends/require-resolve/base.webpack.config.js new file mode 100644 index 00000000000..db299e40fa5 --- /dev/null +++ b/test/build/extends/require-resolve/base.webpack.config.js @@ -0,0 +1,9 @@ +module.exports = () => { + console.log("base.webpack.config.js"); + + return { + name: "base_config", + mode: "development", + entry: "./src/index.js", + }; +}; diff --git a/test/build/extends/require-resolve/override.config.js b/test/build/extends/require-resolve/override.config.js new file mode 100644 index 00000000000..bf79ce64650 --- /dev/null +++ b/test/build/extends/require-resolve/override.config.js @@ -0,0 +1,9 @@ +module.exports = () => { + console.log("override.config.js"); + + return { + name: "override_config", + mode: "development", + entry: "./src/index.js", + }; +}; diff --git a/test/build/extends/require-resolve/src/index.js b/test/build/extends/require-resolve/src/index.js new file mode 100644 index 00000000000..7a21c1b9d7d --- /dev/null +++ b/test/build/extends/require-resolve/src/index.js @@ -0,0 +1 @@ +console.log("index.js") diff --git a/test/build/extends/require-resolve/webpack.config.js b/test/build/extends/require-resolve/webpack.config.js new file mode 100644 index 00000000000..5e9974c4318 --- /dev/null +++ b/test/build/extends/require-resolve/webpack.config.js @@ -0,0 +1,10 @@ +const WebpackCLITestPlugin = require("../../../utils/webpack-cli-test-plugin"); + +module.exports = () => { + console.log("derived.webpack.config.js"); + + return { + extends: [require.resolve("./base.webpack.config.js")], + plugins: [new WebpackCLITestPlugin()], + }; +}; diff --git a/test/build/merge/config-absent/merge-config-absent.test.js b/test/build/merge/config-absent/merge-config-absent.test.js index a1ab65de323..506e715927d 100644 --- a/test/build/merge/config-absent/merge-config-absent.test.js +++ b/test/build/merge/config-absent/merge-config-absent.test.js @@ -1,7 +1,5 @@ "use strict"; -const path = require("node:path"); - const { run } = require("../../../utils/test-utils"); describe("merge flag configuration", () => { @@ -19,6 +17,6 @@ describe("merge flag configuration", () => { // Since the process will exit, nothing on stdout expect(stdout).toBeFalsy(); // Confirm that the user is notified - expect(stderr).toContain(`Failed to load '${path.resolve(__dirname, "./2.js")}' config`); + expect(stderr).toContain("Failed to load './2.js' config"); }); }); diff --git a/test/configtest/with-config-path/__snapshots__/with-config-path.test.js.snap.webpack5 b/test/configtest/with-config-path/__snapshots__/with-config-path.test.js.snap.webpack5 index 8f64293cc3c..f97b3daecd6 100644 --- a/test/configtest/with-config-path/__snapshots__/with-config-path.test.js.snap.webpack5 +++ b/test/configtest/with-config-path/__snapshots__/with-config-path.test.js.snap.webpack5 @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`'configtest' command with the configuration path option should throw error if configuration does not exist: stderr 1`] = `"[webpack-cli] Failed to load '/test/configtest/with-config-path/a.js' config"`; +exports[`'configtest' command with the configuration path option should throw error if configuration does not exist: stderr 1`] = `"[webpack-cli] Failed to load './a.js' config"`; exports[`'configtest' command with the configuration path option should throw error if configuration does not exist: stdout 1`] = `""`; exports[`'configtest' command with the configuration path option should throw syntax error: stderr 1`] = ` -"[webpack-cli] Failed to load '/test/configtest/with-config-path/syntax-error.config.js' config +"[webpack-cli] Failed to load './syntax-error.config.js' config ▶ ESM (\`import\`) failed: Unexpected token ';' @@ -22,7 +22,7 @@ exports[`'configtest' command with the configuration path option should throw va -> Enable production optimizations or development hints." `; -exports[`'configtest' command with the configuration path option should throw validation error: stdout 1`] = `"[webpack-cli] Validate '/test/configtest/with-config-path/error.config.js'."`; +exports[`'configtest' command with the configuration path option should throw validation error: stdout 1`] = `"[webpack-cli] Validate './error.config.js'."`; exports[`'configtest' command with the configuration path option should validate the config with alias 't': stderr 1`] = ` "[webpack-cli] Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema. @@ -31,11 +31,11 @@ exports[`'configtest' command with the configuration path option should validate -> Enable production optimizations or development hints." `; -exports[`'configtest' command with the configuration path option should validate the config with alias 't': stdout 1`] = `"[webpack-cli] Validate '/test/configtest/with-config-path/error.config.js'."`; +exports[`'configtest' command with the configuration path option should validate the config with alias 't': stdout 1`] = `"[webpack-cli] Validate './error.config.js'."`; exports[`'configtest' command with the configuration path option should validate webpack config successfully: stderr 1`] = `""`; exports[`'configtest' command with the configuration path option should validate webpack config successfully: stdout 1`] = ` -"[webpack-cli] Validate '/test/configtest/with-config-path/basic.config.js'. +"[webpack-cli] Validate './basic.config.js'. [webpack-cli] There are no validation errors in the given webpack configuration." `; diff --git a/test/watch/basic/basic.test.js b/test/watch/basic/basic.test.js index 72e74b30751..8ca22865a40 100644 --- a/test/watch/basic/basic.test.js +++ b/test/watch/basic/basic.test.js @@ -227,8 +227,6 @@ describe("basic", () => { }); it("should log supplied config with watch", async () => { - const configPath = resolve(__dirname, "./log.config.js"); - let stderr = ""; await runWatch(__dirname, ["watch", "--config", "log.config.js"], { @@ -240,7 +238,7 @@ describe("basic", () => { if (/Compiler finished/.test(data)) { expect(stderr).toContain("Compiler starting..."); - expect(stderr).toContain(`Compiler is using config: '${configPath}'`); + expect(stderr).toContain("Compiler is using config: 'log.config.js'"); expect(stderr).toContain("Compiler finished"); processKill(proc);