Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
node_modules
test/**/node_modules
!test/external-command/node_modules
!test/build/config-format/typescript-tsx/node_modules

# Lock files
yarn.lock
Expand Down
53 changes: 47 additions & 6 deletions packages/webpack-cli/src/webpack-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
program,
} from "commander";
import { type Config as EnvinfoConfig, type Options as EnvinfoOptions } from "envinfo";
import { type Extensions as InterpretExtensions } from "interpret";
import { type prepare } from "rechoir";
import {
type Argument as WebpackArgument,
Expand Down Expand Up @@ -60,6 +61,8 @@ const DATA_FORMAT_LOADERS: Record<string, { package: string; method: "parse" | "
".yml": { package: "js-yaml", method: "load" },
".toml": { package: "toml", method: "parse" },
};
const TSX_COMMONJS_LOADER = "tsx/cjs";
const TSX_COMMONJS_EXTENSIONS = [".ts", ".tsx", ".cts", ".mts"] as const;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type RecordAny = Record<string, any>;
Expand Down Expand Up @@ -580,6 +583,45 @@ class WebpackCLI {
return distance(first, second);
}

static #getInterpretExtensionsWithTsx(extensions: InterpretExtensions): InterpretExtensions {
const enhancedExtensions = { ...extensions };

// `interpret` owns the default loader table; add tsx locally until it ships
// there so projects with only `tsx` installed can still load TS configs.
for (const extension of TSX_COMMONJS_EXTENSIONS) {
if (!Object.prototype.hasOwnProperty.call(enhancedExtensions, extension)) {
enhancedExtensions[extension] = TSX_COMMONJS_LOADER;
continue;
}

const extensionLoaders = enhancedExtensions[extension];

if (extensionLoaders === null) {
enhancedExtensions[extension] = TSX_COMMONJS_LOADER;
continue;
}

const loaders = Array.isArray(extensionLoaders) ? extensionLoaders : [extensionLoaders];
const hasTsxLoader = loaders.some((loader) => {
if (typeof loader === "string") {
return loader === TSX_COMMONJS_LOADER;
}

return (
loader !== null &&
typeof loader === "object" &&
loader.module === TSX_COMMONJS_LOADER
);
});

if (!hasTsxLoader) {
enhancedExtensions[extension] = [...loaders, TSX_COMMONJS_LOADER];
}
}

return enhancedExtensions;
}

getLogger(): Logger {
// Status icons brand every webpack-cli message (`✔`/`✖`/`⚠`/`ℹ`). They sit
// between the `[webpack-cli]` prefix and the unchanged message text, so
Expand Down Expand Up @@ -2889,18 +2931,17 @@ class WebpackCLI {
if (loadingError) {
const configFilePath = isFileURL ? fileURLToPath(configPath) : path.resolve(configPath);
const { jsVariants, extensions } = await import("interpret");
const interpretExtensions = WebpackCLI.#getInterpretExtensionsWithTsx(extensions);

let interpreted = Object.keys(jsVariants).find((variant) => variant === ext);

if (!interpreted && ext.endsWith(".cts")) {
interpreted = jsVariants[".ts"] as string;
}
const interpreted =
Object.prototype.hasOwnProperty.call(jsVariants, ext) ||
Object.prototype.hasOwnProperty.call(interpretExtensions, ext);

if (interpreted && !disableInterpret) {
const rechoir: Rechoir = (await import("rechoir")).default;

try {
rechoir.prepare(extensions, configPath);
rechoir.prepare(interpretExtensions, configPath);
} catch (error) {
if ((error as RechoirError)?.failures) {
this.logger.error(`Unable load '${configPath}'`);
Expand Down
1 change: 1 addition & 0 deletions test/build/config-format/typescript-tsx/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("Rimuru Tempest");

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions test/build/config-format/typescript-tsx/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "commonjs",
"engines": {
"node": ">=18.12.0"
}
}
24 changes: 24 additions & 0 deletions test/build/config-format/typescript-tsx/typescript.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { existsSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { run } from "../../../utils/test-utils.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

describe("typescript configuration with tsx", () => {
it("should support `webpack --mode=production` with only `tsx` installed", async () => {
const [major] = process.versions.node.split(".").map(Number);
const { exitCode, stderr, stdout } = await run(__dirname, ["--mode=production"], {
nodeOptions: [
// Disable TypeScript strip types so this test exercises the interpret fallback.
...(major >= 22 ? ["--no-experimental-strip-types"] : []),
],
});

expect(stderr).toBeFalsy();
expect(stdout).toBeTruthy();
expect(exitCode).toBe(0);
expect(existsSync(resolve(__dirname, "dist/foo.bundle.js"))).toBeTruthy();
});
});
13 changes: 13 additions & 0 deletions test/build/config-format/typescript-tsx/webpack.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const path = require("node:path");

const filename: string = "foo.bundle.js";

const config = {
entry: "./main.ts",
output: {
path: path.resolve("dist"),
filename,
},
};

module.exports = config;