From a77daf28f8a8ad96410a39d565f011f6bb14f6bb Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Sun, 4 Jun 2023 02:45:07 +0300 Subject: [PATCH] fix: improved support for `.cts` and `.mts` extensions --- packages/webpack-cli/src/webpack-cli.ts | 128 ++++++++++++++---- .../coffee/webpack.config.coffee | 2 +- .../commonjs-default/webpack.config.cjs | 2 +- .../config-format/commonjs/webpack.config.cjs | 2 +- .../disable-interpret/webpack.config.ts | 2 +- test/build/config-format/{mjs => esm}/main.js | 0 test/build/config-format/esm/mjs.test.js | 11 ++ .../{mjs => esm}/webpack.config.mjs | 2 +- .../config-format/failure/webpack.config.iced | 2 +- test/build/config-format/mjs/mjs.test.js | 18 --- .../config-format/typescript-commonjs/main.ts | 1 + .../typescript-commonjs/tsconfig.json | 5 + .../typescript-commonjs/typescript.test.js | 14 ++ .../typescript-commonjs/webpack.config.cts | 16 +++ .../typescript-esnext-mjs/main.ts | 1 + .../typescript-esnext-mjs/package.json | 6 + .../typescript-esnext-mjs/tsconfig.json | 6 + .../typescript-esnext-mjs/typescript.test.js | 20 +++ .../typescript-esnext-mjs/webpack.config.mts | 15 ++ .../typescript-esnext/typescript.test.js | 7 - .../typescript-esnext/webpack.config.ts | 4 +- .../typescript/typescript.test.js | 1 + .../typescript/webpack.config.ts | 4 +- 23 files changed, 206 insertions(+), 63 deletions(-) rename test/build/config-format/{mjs => esm}/main.js (100%) create mode 100644 test/build/config-format/esm/mjs.test.js rename test/build/config-format/{mjs => esm}/webpack.config.mjs (90%) delete mode 100644 test/build/config-format/mjs/mjs.test.js create mode 100644 test/build/config-format/typescript-commonjs/main.ts create mode 100644 test/build/config-format/typescript-commonjs/tsconfig.json create mode 100644 test/build/config-format/typescript-commonjs/typescript.test.js create mode 100644 test/build/config-format/typescript-commonjs/webpack.config.cts create mode 100644 test/build/config-format/typescript-esnext-mjs/main.ts create mode 100644 test/build/config-format/typescript-esnext-mjs/package.json create mode 100644 test/build/config-format/typescript-esnext-mjs/tsconfig.json create mode 100644 test/build/config-format/typescript-esnext-mjs/typescript.test.js create mode 100644 test/build/config-format/typescript-esnext-mjs/webpack.config.mts diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 6d190e65ad8..d225f47ad81 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -322,37 +322,83 @@ class WebpackCLI implements IWebpackCLI { process.exit(2); } - async tryRequireThenImport(module: ModuleName, handleError = true): Promise { + async tryRequireThenImport( + module: ModuleName, + handleError = true, + moduleType: "unknown" | "commonjs" | "esm" = "unknown", + ): Promise { let result; - try { - result = require(module); - } catch (error) { - const dynamicImportLoader: null | DynamicImport = - require("./utils/dynamic-import-loader")(); - if ( - ((error as ImportLoaderError).code === "ERR_REQUIRE_ESM" || - process.env.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) && - pathToFileURL && - dynamicImportLoader - ) { - const urlForConfig = pathToFileURL(module); - - result = await dynamicImportLoader(urlForConfig); - result = result.default; + switch (moduleType) { + case "unknown": { + try { + result = require(module); + } catch (error) { + const dynamicImportLoader: null | DynamicImport = + require("./utils/dynamic-import-loader")(); + if ( + ((error as ImportLoaderError).code === "ERR_REQUIRE_ESM" || + process.env.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) && + pathToFileURL && + dynamicImportLoader + ) { + const urlForConfig = pathToFileURL(module); + + result = await dynamicImportLoader(urlForConfig); + result = result.default; + + return result; + } - return result; + if (handleError) { + this.logger.error(error); + process.exit(2); + } else { + throw error; + } + } + break; } + case "commonjs": { + try { + result = require(module); + } catch (error) { + if (handleError) { + this.logger.error(error); + process.exit(2); + } else { + throw error; + } + } + break; + } + case "esm": { + try { + const dynamicImportLoader: null | DynamicImport = + require("./utils/dynamic-import-loader")(); - if (handleError) { - this.logger.error(error); - process.exit(2); - } else { - throw error; + if (pathToFileURL && dynamicImportLoader) { + const urlForConfig = pathToFileURL(module); + + result = await dynamicImportLoader(urlForConfig); + result = result.default; + + return result; + } + } catch (error) { + if (handleError) { + this.logger.error(error); + process.exit(2); + } else { + throw error; + } + } + + break; } } - // For babel/typescript + // For babel and other, only commonjs if (result && typeof result === "object" && "default" in result) { result = result.default || {}; } @@ -1749,8 +1795,15 @@ class WebpackCLI implements IWebpackCLI { const interpret = require("interpret"); const loadConfigByPath = async (configPath: string, argv: Argv = {}) => { - const ext = path.extname(configPath); - const interpreted = Object.keys(interpret.jsVariants).find((variant) => variant === ext); + const ext = path.extname(configPath).toLowerCase(); + let interpreted = interpret.jsVariants[ext]; + + // Fallback `.cts` to `.ts` + // TODO implement good `.mts` support after https://github.com/gulpjs/rechoir/issues/43 + // For ESM and `.mts` you need to use: 'NODE_OPTIONS="--loader ts-node/esm" webpack-cli --config ./webpack.config.mts' + if (!interpreted && /\.cts$/.test(ext)) { + interpreted = interpret.jsVariants[".ts"]; + } if (interpreted && !disableInterpret) { const rechoir: Rechoir = require("rechoir"); @@ -1777,10 +1830,24 @@ class WebpackCLI implements IWebpackCLI { type LoadConfigOption = PotentialPromise; + let moduleType: "unknown" | "commonjs" | "esm" = "unknown"; + + switch (ext) { + case ".cjs": + case ".cts": + moduleType = "commonjs"; + break; + case ".mjs": + case ".mts": + moduleType = "esm"; + break; + } + try { options = await this.tryRequireThenImport( configPath, false, + moduleType, ); // @ts-expect-error error type assertion } catch (error: Error) { @@ -1897,14 +1964,15 @@ class WebpackCLI implements IWebpackCLI { ".cjs", ".ts", ".cts", + ".mts", ...Object.keys(interpret.extensions), ]; // Order defines the priority, in decreasing order - const defaultConfigFiles = [ - "webpack.config", - ".webpack/webpack.config", - ".webpack/webpackfile", - ].flatMap((filename) => extensions.map((ext) => path.resolve(filename + ext))); + const defaultConfigFiles = new Set( + ["webpack.config", ".webpack/webpack.config", ".webpack/webpackfile"].flatMap((filename) => + extensions.map((ext) => path.resolve(filename + ext)), + ), + ); let foundDefaultConfigFile; diff --git a/test/build/config-format/coffee/webpack.config.coffee b/test/build/config-format/coffee/webpack.config.coffee index 15e1934891b..10cf2b84b46 100644 --- a/test/build/config-format/coffee/webpack.config.coffee +++ b/test/build/config-format/coffee/webpack.config.coffee @@ -1,7 +1,7 @@ path = require 'path' config = - mode: 'production' + mode: 'development' entry: './main.js' output: path: path.resolve(__dirname, 'dist') diff --git a/test/build/config-format/commonjs-default/webpack.config.cjs b/test/build/config-format/commonjs-default/webpack.config.cjs index 5ac5037995f..ce5f79fd2b6 100644 --- a/test/build/config-format/commonjs-default/webpack.config.cjs +++ b/test/build/config-format/commonjs-default/webpack.config.cjs @@ -1,7 +1,7 @@ const path = require("path"); const config = { - mode: "production", + mode: "development", entry: "./main.js", output: { path: path.resolve(__dirname, "dist"), diff --git a/test/build/config-format/commonjs/webpack.config.cjs b/test/build/config-format/commonjs/webpack.config.cjs index 931c439e129..434d0ea68b3 100644 --- a/test/build/config-format/commonjs/webpack.config.cjs +++ b/test/build/config-format/commonjs/webpack.config.cjs @@ -1,7 +1,7 @@ const path = require("path"); const config = { - mode: "production", + mode: "development", entry: "./main.js", output: { path: path.resolve(__dirname, "dist"), diff --git a/test/build/config-format/disable-interpret/webpack.config.ts b/test/build/config-format/disable-interpret/webpack.config.ts index c20e6fb7f63..5a6487b6f7d 100644 --- a/test/build/config-format/disable-interpret/webpack.config.ts +++ b/test/build/config-format/disable-interpret/webpack.config.ts @@ -2,7 +2,7 @@ import * as path from "path"; const config = { - mode: "production", + mode: "development", entry: "./main.ts", output: { path: path.resolve(__dirname, "dist"), diff --git a/test/build/config-format/mjs/main.js b/test/build/config-format/esm/main.js similarity index 100% rename from test/build/config-format/mjs/main.js rename to test/build/config-format/esm/main.js diff --git a/test/build/config-format/esm/mjs.test.js b/test/build/config-format/esm/mjs.test.js new file mode 100644 index 00000000000..f33ac10011b --- /dev/null +++ b/test/build/config-format/esm/mjs.test.js @@ -0,0 +1,11 @@ +const { run } = require("../../../utils/test-utils"); + +describe("webpack cli", () => { + it("should support mjs config format", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "webpack.config.mjs"]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + }); +}); diff --git a/test/build/config-format/mjs/webpack.config.mjs b/test/build/config-format/esm/webpack.config.mjs similarity index 90% rename from test/build/config-format/mjs/webpack.config.mjs rename to test/build/config-format/esm/webpack.config.mjs index b32e7d7be42..224ee8485a4 100644 --- a/test/build/config-format/mjs/webpack.config.mjs +++ b/test/build/config-format/esm/webpack.config.mjs @@ -2,7 +2,7 @@ import { fileURLToPath } from "url"; import path from "path"; export default { - mode: "production", + mode: "development", entry: "./main.js", output: { path: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "dist"), diff --git a/test/build/config-format/failure/webpack.config.iced b/test/build/config-format/failure/webpack.config.iced index 15e1934891b..10cf2b84b46 100644 --- a/test/build/config-format/failure/webpack.config.iced +++ b/test/build/config-format/failure/webpack.config.iced @@ -1,7 +1,7 @@ path = require 'path' config = - mode: 'production' + mode: 'development' entry: './main.js' output: path: path.resolve(__dirname, 'dist') diff --git a/test/build/config-format/mjs/mjs.test.js b/test/build/config-format/mjs/mjs.test.js deleted file mode 100644 index 590e8212847..00000000000 --- a/test/build/config-format/mjs/mjs.test.js +++ /dev/null @@ -1,18 +0,0 @@ -const { run } = require("../../../utils/test-utils"); - -describe("webpack cli", () => { - it("should support mjs config format", async () => { - const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "webpack.config.mjs"], { - env: { WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG: true }, - }); - - if (/Error: Not supported/.test(stderr)) { - expect(exitCode).toBe(2); - expect(stdout).toBeFalsy(); - } else { - expect(exitCode).toBe(0); - expect(stderr).toBeFalsy(); - expect(stdout).toBeTruthy(); - } - }); -}); diff --git a/test/build/config-format/typescript-commonjs/main.ts b/test/build/config-format/typescript-commonjs/main.ts new file mode 100644 index 00000000000..41d13d1a9a1 --- /dev/null +++ b/test/build/config-format/typescript-commonjs/main.ts @@ -0,0 +1 @@ +console.log("Main typescript file"); diff --git a/test/build/config-format/typescript-commonjs/tsconfig.json b/test/build/config-format/typescript-commonjs/tsconfig.json new file mode 100644 index 00000000000..391488ab17f --- /dev/null +++ b/test/build/config-format/typescript-commonjs/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "module": "commonjs" + } +} diff --git a/test/build/config-format/typescript-commonjs/typescript.test.js b/test/build/config-format/typescript-commonjs/typescript.test.js new file mode 100644 index 00000000000..5d00725cef8 --- /dev/null +++ b/test/build/config-format/typescript-commonjs/typescript.test.js @@ -0,0 +1,14 @@ +const { run } = require("../../../utils/test-utils"); +const { existsSync } = require("fs"); +const { resolve } = require("path"); + +describe("webpack cli", () => { + it("should support typescript file", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.cts"]); + + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + expect(exitCode).toBe(0); + expect(existsSync(resolve(__dirname, "dist/foo.bundle.js"))).toBeTruthy(); + }); +}); diff --git a/test/build/config-format/typescript-commonjs/webpack.config.cts b/test/build/config-format/typescript-commonjs/webpack.config.cts new file mode 100644 index 00000000000..2bdd9c19605 --- /dev/null +++ b/test/build/config-format/typescript-commonjs/webpack.config.cts @@ -0,0 +1,16 @@ +/* eslint-disable node/no-unsupported-features/es-syntax */ +/** eslint-disable **/ +import * as path from "path"; + +// cspell:ignore elopment +const mode: string = "dev" + "elopment"; +const config = { + mode, + entry: "./main.ts", + output: { + path: path.resolve(__dirname, "dist"), + filename: "foo.bundle.js", + }, +}; + +export = config; diff --git a/test/build/config-format/typescript-esnext-mjs/main.ts b/test/build/config-format/typescript-esnext-mjs/main.ts new file mode 100644 index 00000000000..dc6a7ea6788 --- /dev/null +++ b/test/build/config-format/typescript-esnext-mjs/main.ts @@ -0,0 +1 @@ +console.log("Rimuru Tempest"); diff --git a/test/build/config-format/typescript-esnext-mjs/package.json b/test/build/config-format/typescript-esnext-mjs/package.json new file mode 100644 index 00000000000..aa65b114f15 --- /dev/null +++ b/test/build/config-format/typescript-esnext-mjs/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "engines": { + "node": ">=14.15.0" + } +} diff --git a/test/build/config-format/typescript-esnext-mjs/tsconfig.json b/test/build/config-format/typescript-esnext-mjs/tsconfig.json new file mode 100644 index 00000000000..e0ba2dc7a46 --- /dev/null +++ b/test/build/config-format/typescript-esnext-mjs/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "module": "esnext", + "allowSyntheticDefaultImports": true + } +} diff --git a/test/build/config-format/typescript-esnext-mjs/typescript.test.js b/test/build/config-format/typescript-esnext-mjs/typescript.test.js new file mode 100644 index 00000000000..287a11f2862 --- /dev/null +++ b/test/build/config-format/typescript-esnext-mjs/typescript.test.js @@ -0,0 +1,20 @@ +// eslint-disable-next-line node/no-unpublished-require +const { run } = require("../../../utils/test-utils"); +const { existsSync } = require("fs"); +const { resolve } = require("path"); + +describe("webpack cli", () => { + it("should support typescript esnext file", async () => { + const env = { ...process.env }; + + const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.mts"], { + nodeOptions: ["--experimental-loader=ts-node/esm"], + env, + }); + + expect(stderr).not.toBeFalsy(); // Deprecation warning logs on stderr + expect(stdout).toBeTruthy(); + expect(exitCode).toBe(0); + expect(existsSync(resolve(__dirname, "dist/foo.bundle.js"))).toBeTruthy(); + }); +}); diff --git a/test/build/config-format/typescript-esnext-mjs/webpack.config.mts b/test/build/config-format/typescript-esnext-mjs/webpack.config.mts new file mode 100644 index 00000000000..8788aa61995 --- /dev/null +++ b/test/build/config-format/typescript-esnext-mjs/webpack.config.mts @@ -0,0 +1,15 @@ +/* eslint-disable node/no-unsupported-features/es-syntax */ +import * as path from "path"; + +// cspell:ignore elopment +const mode: string = "dev" + "elopment"; +const config = { + mode, + entry: "./main.ts", + output: { + path: path.resolve("dist"), + filename: "foo.bundle.js", + }, +}; + +export default config; diff --git a/test/build/config-format/typescript-esnext/typescript.test.js b/test/build/config-format/typescript-esnext/typescript.test.js index e1c0f0dd847..4a5bd0edef5 100644 --- a/test/build/config-format/typescript-esnext/typescript.test.js +++ b/test/build/config-format/typescript-esnext/typescript.test.js @@ -6,13 +6,6 @@ const { resolve } = require("path"); describe("webpack cli", () => { it("should support typescript esnext file", async () => { const majorNodeVersion = process.version.slice(1, 3); - - if (majorNodeVersion < 14) { - expect(true).toBe(true); - - return; - } - const env = { ...process.env }; if (majorNodeVersion >= 20) { diff --git a/test/build/config-format/typescript-esnext/webpack.config.ts b/test/build/config-format/typescript-esnext/webpack.config.ts index 4dcffcff3be..8788aa61995 100644 --- a/test/build/config-format/typescript-esnext/webpack.config.ts +++ b/test/build/config-format/typescript-esnext/webpack.config.ts @@ -1,8 +1,10 @@ /* eslint-disable node/no-unsupported-features/es-syntax */ import * as path from "path"; +// cspell:ignore elopment +const mode: string = "dev" + "elopment"; const config = { - mode: "production", + mode, entry: "./main.ts", output: { path: path.resolve("dist"), diff --git a/test/build/config-format/typescript/typescript.test.js b/test/build/config-format/typescript/typescript.test.js index 40ecd1cb094..6bb1ea5a8b0 100644 --- a/test/build/config-format/typescript/typescript.test.js +++ b/test/build/config-format/typescript/typescript.test.js @@ -5,6 +5,7 @@ const { resolve } = require("path"); describe("webpack cli", () => { it("should support typescript file", async () => { const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.ts"], { + // For `nyc`, remove it when we drop `nyc` usage nodeOptions: ["--require=ts-node/register"], }); diff --git a/test/build/config-format/typescript/webpack.config.ts b/test/build/config-format/typescript/webpack.config.ts index 22f8d9130fe..2bdd9c19605 100644 --- a/test/build/config-format/typescript/webpack.config.ts +++ b/test/build/config-format/typescript/webpack.config.ts @@ -2,8 +2,10 @@ /** eslint-disable **/ import * as path from "path"; +// cspell:ignore elopment +const mode: string = "dev" + "elopment"; const config = { - mode: "production", + mode, entry: "./main.ts", output: { path: path.resolve(__dirname, "dist"),