Skip to content

Commit a77daf2

Browse files
fix: improved support for .cts and .mts extensions
1 parent 73bd2cc commit a77daf2

File tree

23 files changed

+206
-63
lines changed

23 files changed

+206
-63
lines changed

packages/webpack-cli/src/webpack-cli.ts

+98-30
Original file line numberDiff line numberDiff line change
@@ -322,37 +322,83 @@ class WebpackCLI implements IWebpackCLI {
322322
process.exit(2);
323323
}
324324

325-
async tryRequireThenImport<T>(module: ModuleName, handleError = true): Promise<T> {
325+
async tryRequireThenImport<T>(
326+
module: ModuleName,
327+
handleError = true,
328+
moduleType: "unknown" | "commonjs" | "esm" = "unknown",
329+
): Promise<T> {
326330
let result;
327331

328-
try {
329-
result = require(module);
330-
} catch (error) {
331-
const dynamicImportLoader: null | DynamicImport<T> =
332-
require("./utils/dynamic-import-loader")();
333-
if (
334-
((error as ImportLoaderError).code === "ERR_REQUIRE_ESM" ||
335-
process.env.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) &&
336-
pathToFileURL &&
337-
dynamicImportLoader
338-
) {
339-
const urlForConfig = pathToFileURL(module);
340-
341-
result = await dynamicImportLoader(urlForConfig);
342-
result = result.default;
332+
switch (moduleType) {
333+
case "unknown": {
334+
try {
335+
result = require(module);
336+
} catch (error) {
337+
const dynamicImportLoader: null | DynamicImport<T> =
338+
require("./utils/dynamic-import-loader")();
339+
if (
340+
((error as ImportLoaderError).code === "ERR_REQUIRE_ESM" ||
341+
process.env.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) &&
342+
pathToFileURL &&
343+
dynamicImportLoader
344+
) {
345+
const urlForConfig = pathToFileURL(module);
346+
347+
result = await dynamicImportLoader(urlForConfig);
348+
result = result.default;
349+
350+
return result;
351+
}
343352

344-
return result;
353+
if (handleError) {
354+
this.logger.error(error);
355+
process.exit(2);
356+
} else {
357+
throw error;
358+
}
359+
}
360+
break;
345361
}
362+
case "commonjs": {
363+
try {
364+
result = require(module);
365+
} catch (error) {
366+
if (handleError) {
367+
this.logger.error(error);
368+
process.exit(2);
369+
} else {
370+
throw error;
371+
}
372+
}
373+
break;
374+
}
375+
case "esm": {
376+
try {
377+
const dynamicImportLoader: null | DynamicImport<T> =
378+
require("./utils/dynamic-import-loader")();
346379

347-
if (handleError) {
348-
this.logger.error(error);
349-
process.exit(2);
350-
} else {
351-
throw error;
380+
if (pathToFileURL && dynamicImportLoader) {
381+
const urlForConfig = pathToFileURL(module);
382+
383+
result = await dynamicImportLoader(urlForConfig);
384+
result = result.default;
385+
386+
return result;
387+
}
388+
} catch (error) {
389+
if (handleError) {
390+
this.logger.error(error);
391+
process.exit(2);
392+
} else {
393+
throw error;
394+
}
395+
}
396+
397+
break;
352398
}
353399
}
354400

355-
// For babel/typescript
401+
// For babel and other, only commonjs
356402
if (result && typeof result === "object" && "default" in result) {
357403
result = result.default || {};
358404
}
@@ -1749,8 +1795,15 @@ class WebpackCLI implements IWebpackCLI {
17491795

17501796
const interpret = require("interpret");
17511797
const loadConfigByPath = async (configPath: string, argv: Argv = {}) => {
1752-
const ext = path.extname(configPath);
1753-
const interpreted = Object.keys(interpret.jsVariants).find((variant) => variant === ext);
1798+
const ext = path.extname(configPath).toLowerCase();
1799+
let interpreted = interpret.jsVariants[ext];
1800+
1801+
// Fallback `.cts` to `.ts`
1802+
// TODO implement good `.mts` support after https://github.com/gulpjs/rechoir/issues/43
1803+
// For ESM and `.mts` you need to use: 'NODE_OPTIONS="--loader ts-node/esm" webpack-cli --config ./webpack.config.mts'
1804+
if (!interpreted && /\.cts$/.test(ext)) {
1805+
interpreted = interpret.jsVariants[".ts"];
1806+
}
17541807

17551808
if (interpreted && !disableInterpret) {
17561809
const rechoir: Rechoir = require("rechoir");
@@ -1777,10 +1830,24 @@ class WebpackCLI implements IWebpackCLI {
17771830

17781831
type LoadConfigOption = PotentialPromise<WebpackConfiguration>;
17791832

1833+
let moduleType: "unknown" | "commonjs" | "esm" = "unknown";
1834+
1835+
switch (ext) {
1836+
case ".cjs":
1837+
case ".cts":
1838+
moduleType = "commonjs";
1839+
break;
1840+
case ".mjs":
1841+
case ".mts":
1842+
moduleType = "esm";
1843+
break;
1844+
}
1845+
17801846
try {
17811847
options = await this.tryRequireThenImport<LoadConfigOption | LoadConfigOption[]>(
17821848
configPath,
17831849
false,
1850+
moduleType,
17841851
);
17851852
// @ts-expect-error error type assertion
17861853
} catch (error: Error) {
@@ -1897,14 +1964,15 @@ class WebpackCLI implements IWebpackCLI {
18971964
".cjs",
18981965
".ts",
18991966
".cts",
1967+
".mts",
19001968
...Object.keys(interpret.extensions),
19011969
];
19021970
// Order defines the priority, in decreasing order
1903-
const defaultConfigFiles = [
1904-
"webpack.config",
1905-
".webpack/webpack.config",
1906-
".webpack/webpackfile",
1907-
].flatMap((filename) => extensions.map((ext) => path.resolve(filename + ext)));
1971+
const defaultConfigFiles = new Set(
1972+
["webpack.config", ".webpack/webpack.config", ".webpack/webpackfile"].flatMap((filename) =>
1973+
extensions.map((ext) => path.resolve(filename + ext)),
1974+
),
1975+
);
19081976

19091977
let foundDefaultConfigFile;
19101978

test/build/config-format/coffee/webpack.config.coffee

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
path = require 'path'
22

33
config =
4-
mode: 'production'
4+
mode: 'development'
55
entry: './main.js'
66
output:
77
path: path.resolve(__dirname, 'dist')

test/build/config-format/commonjs-default/webpack.config.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const path = require("path");
22

33
const config = {
4-
mode: "production",
4+
mode: "development",
55
entry: "./main.js",
66
output: {
77
path: path.resolve(__dirname, "dist"),

test/build/config-format/commonjs/webpack.config.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const path = require("path");
22

33
const config = {
4-
mode: "production",
4+
mode: "development",
55
entry: "./main.js",
66
output: {
77
path: path.resolve(__dirname, "dist"),

test/build/config-format/disable-interpret/webpack.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import * as path from "path";
33

44
const config = {
5-
mode: "production",
5+
mode: "development",
66
entry: "./main.ts",
77
output: {
88
path: path.resolve(__dirname, "dist"),
File renamed without changes.
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const { run } = require("../../../utils/test-utils");
2+
3+
describe("webpack cli", () => {
4+
it("should support mjs config format", async () => {
5+
const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "webpack.config.mjs"]);
6+
7+
expect(exitCode).toBe(0);
8+
expect(stderr).toBeFalsy();
9+
expect(stdout).toBeTruthy();
10+
});
11+
});

test/build/config-format/mjs/webpack.config.mjs test/build/config-format/esm/webpack.config.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { fileURLToPath } from "url";
22
import path from "path";
33

44
export default {
5-
mode: "production",
5+
mode: "development",
66
entry: "./main.js",
77
output: {
88
path: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "dist"),

test/build/config-format/failure/webpack.config.iced

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
path = require 'path'
22

33
config =
4-
mode: 'production'
4+
mode: 'development'
55
entry: './main.js'
66
output:
77
path: path.resolve(__dirname, 'dist')

test/build/config-format/mjs/mjs.test.js

-18
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log("Main typescript file");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"compilerOptions": {
3+
"module": "commonjs"
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const { run } = require("../../../utils/test-utils");
2+
const { existsSync } = require("fs");
3+
const { resolve } = require("path");
4+
5+
describe("webpack cli", () => {
6+
it("should support typescript file", async () => {
7+
const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.cts"]);
8+
9+
expect(stderr).toBeFalsy();
10+
expect(stdout).toBeTruthy();
11+
expect(exitCode).toBe(0);
12+
expect(existsSync(resolve(__dirname, "dist/foo.bundle.js"))).toBeTruthy();
13+
});
14+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* eslint-disable node/no-unsupported-features/es-syntax */
2+
/** eslint-disable **/
3+
import * as path from "path";
4+
5+
// cspell:ignore elopment
6+
const mode: string = "dev" + "elopment";
7+
const config = {
8+
mode,
9+
entry: "./main.ts",
10+
output: {
11+
path: path.resolve(__dirname, "dist"),
12+
filename: "foo.bundle.js",
13+
},
14+
};
15+
16+
export = config;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log("Rimuru Tempest");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "module",
3+
"engines": {
4+
"node": ">=14.15.0"
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"compilerOptions": {
3+
"module": "esnext",
4+
"allowSyntheticDefaultImports": true
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// eslint-disable-next-line node/no-unpublished-require
2+
const { run } = require("../../../utils/test-utils");
3+
const { existsSync } = require("fs");
4+
const { resolve } = require("path");
5+
6+
describe("webpack cli", () => {
7+
it("should support typescript esnext file", async () => {
8+
const env = { ...process.env };
9+
10+
const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.mts"], {
11+
nodeOptions: ["--experimental-loader=ts-node/esm"],
12+
env,
13+
});
14+
15+
expect(stderr).not.toBeFalsy(); // Deprecation warning logs on stderr
16+
expect(stdout).toBeTruthy();
17+
expect(exitCode).toBe(0);
18+
expect(existsSync(resolve(__dirname, "dist/foo.bundle.js"))).toBeTruthy();
19+
});
20+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* eslint-disable node/no-unsupported-features/es-syntax */
2+
import * as path from "path";
3+
4+
// cspell:ignore elopment
5+
const mode: string = "dev" + "elopment";
6+
const config = {
7+
mode,
8+
entry: "./main.ts",
9+
output: {
10+
path: path.resolve("dist"),
11+
filename: "foo.bundle.js",
12+
},
13+
};
14+
15+
export default config;

test/build/config-format/typescript-esnext/typescript.test.js

-7
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,6 @@ const { resolve } = require("path");
66
describe("webpack cli", () => {
77
it("should support typescript esnext file", async () => {
88
const majorNodeVersion = process.version.slice(1, 3);
9-
10-
if (majorNodeVersion < 14) {
11-
expect(true).toBe(true);
12-
13-
return;
14-
}
15-
169
const env = { ...process.env };
1710

1811
if (majorNodeVersion >= 20) {

test/build/config-format/typescript-esnext/webpack.config.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
/* eslint-disable node/no-unsupported-features/es-syntax */
22
import * as path from "path";
33

4+
// cspell:ignore elopment
5+
const mode: string = "dev" + "elopment";
46
const config = {
5-
mode: "production",
7+
mode,
68
entry: "./main.ts",
79
output: {
810
path: path.resolve("dist"),

test/build/config-format/typescript/typescript.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { resolve } = require("path");
55
describe("webpack cli", () => {
66
it("should support typescript file", async () => {
77
const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.ts"], {
8+
// For `nyc`, remove it when we drop `nyc` usage
89
nodeOptions: ["--require=ts-node/register"],
910
});
1011

test/build/config-format/typescript/webpack.config.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
/** eslint-disable **/
33
import * as path from "path";
44

5+
// cspell:ignore elopment
6+
const mode: string = "dev" + "elopment";
57
const config = {
6-
mode: "production",
8+
mode,
79
entry: "./main.ts",
810
output: {
911
path: path.resolve(__dirname, "dist"),

0 commit comments

Comments
 (0)