diff --git a/.changeset/tsx-config-loader.md b/.changeset/tsx-config-loader.md new file mode 100644 index 00000000000..5ca914305d2 --- /dev/null +++ b/.changeset/tsx-config-loader.md @@ -0,0 +1,5 @@ +--- +"webpack-cli": minor +--- + +Support `tsx` as a fallback loader for TypeScript and JSX configuration files (`.ts`, `.tsx`, `.cts`, `.mts` and `.jsx`), used when none of the loaders known to `interpret` (such as `ts-node`) are installed. diff --git a/eslint.config.mjs b/eslint.config.mjs index 960d43d6927..c4ad7725712 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -16,6 +16,7 @@ export default defineConfig([ "test/build/config-format/esm-require-await/webpack.config.js", "test/configtest/with-config-path/syntax-error.config.js", "test/build/config-format/auto/webpack.config.js", + "test/build/config-format/typescript-tsx/webpack.config.jsx", ]), { extends: [config], diff --git a/package-lock.json b/package-lock.json index 9bfd24927b7..196250dbd13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "style-loader": "^4.0.0", "ts-loader": "^9.6.2", "ts-node": "^10.9.2", + "tsx": "^4.23.0", "typescript": "^6.0.3", "webpack": "^5.107.2", "webpack-bundle-analyzer": "^5.3.0", @@ -4040,6 +4041,448 @@ "node": ">=10" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -8281,7 +8724,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, + "devOptional": true, "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { @@ -11210,6 +11653,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -16579,7 +17064,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.0.0.tgz", "integrity": "sha512-GSvaPUbk1U+FMZ7rJzF+F8e5YVtu7KnD40et/5rBXXRBv2jCO9L3qCewvIDDdudC0QycTFlf6EAA+h3kxBsuUw==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -16653,7 +17138,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -21952,6 +22437,25 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", + "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tsyringe": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", @@ -23435,7 +23939,7 @@ } }, "packages/webpack-cli": { - "version": "7.0.3", + "version": "7.1.0", "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^1.1.0", @@ -23461,11 +23965,23 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "js-yaml": "^4.0.0 || ^5.0.0", + "json5": "^2.2.3", + "toml": "^3.0.0 || ^4.0.0", "webpack": "^5.101.0", "webpack-bundle-analyzer": "^4.0.0 || ^5.0.0", "webpack-dev-server": "^5.0.0" }, "peerDependenciesMeta": { + "js-yaml": { + "optional": true + }, + "json5": { + "optional": true + }, + "toml": { + "optional": true + }, "webpack-bundle-analyzer": { "optional": true }, diff --git a/package.json b/package.json index 07b6ec8e095..44cf8d3c311 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "style-loader": "^4.0.0", "ts-loader": "^9.6.2", "ts-node": "^10.9.2", + "tsx": "^4.23.0", "typescript": "^6.0.3", "webpack": "^5.107.2", "webpack-bundle-analyzer": "^5.3.0", diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 9ecdbea15ac..6bc1f4a077b 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -61,6 +61,12 @@ const DATA_FORMAT_LOADERS: Record; @@ -2896,24 +2902,61 @@ class WebpackCLI { interpreted = jsVariants[".ts"] as string; } + // `tsx` is missing from `interpret`'s loader tables, and it cannot + // be added there: its `tsx/cjs` entry point exists only in the + // package's `exports` map, which `rechoir`'s resolver does not + // support. Register it manually instead, resolved from the + // configuration file's location so the user's copy is picked up. + // Requiring `tsx/cjs` installs its require hook as a side effect. + const registerTsxLoader = (): Error | undefined => { + try { + createRequire(configFilePath)("tsx/cjs"); + + return undefined; + } catch (err) { + // `throw` may raise non-`Error` values; normalize so the + // error-reporting path can rely on `.message`. + return err instanceof Error ? err : new Error(String(err)); + } + }; + if (interpreted && !disableInterpret) { const rechoir: Rechoir = (await import("rechoir")).default; try { rechoir.prepare(extensions, configPath); } catch (error) { - if ((error as RechoirError)?.failures) { - this.logger.error(`Unable load '${configPath}'`); - this.logger.error((error as RechoirError).message); - for (const failure of (error as RechoirError).failures) { - this.logger.error(failure.error.message); + // The loaders known to `interpret` take priority; `tsx` is + // only tried once all of them have failed. + const isTsxLoadable = TSX_LOADABLE_EXTENSIONS.has(ext); + const tsxFailure = isTsxLoadable ? registerTsxLoader() : undefined; + + if (!isTsxLoadable || tsxFailure) { + if ((error as RechoirError)?.failures) { + this.logger.error(`Unable to load '${configPath}'`); + this.logger.error((error as RechoirError).message); + for (const failure of (error as RechoirError).failures) { + this.logger.error(failure.error.message); + } + if (tsxFailure) { + // Only the first line — `require` errors append a noisy + // "Require stack" to the message. + const [tsxFailureReason] = tsxFailure.message.split("\n"); + this.logger.error(tsxFailureReason); + } + this.logger.error("Please install one of them"); + process.exit(2); } - this.logger.error("Please install one of them"); + this.logger.error(error); process.exit(2); } - this.logger.error(error); - process.exit(2); } + } else if (TSX_LOADABLE_EXTENSIONS.has(ext) && !disableInterpret) { + // `.mts` has no entry in `interpret`'s tables, so `tsx` is the + // only loader we can offer. Best effort: when it is missing, the + // `require()` below fails and the original errors are reported, + // exactly as before. + registerTsxLoader(); } try { diff --git a/test/build/config-format/typescript-tsx/main.ts b/test/build/config-format/typescript-tsx/main.ts new file mode 100644 index 00000000000..dc6a7ea6788 --- /dev/null +++ b/test/build/config-format/typescript-tsx/main.ts @@ -0,0 +1 @@ +console.log("Rimuru Tempest"); diff --git a/test/build/config-format/typescript-tsx/package.json b/test/build/config-format/typescript-tsx/package.json new file mode 100644 index 00000000000..449278eb4a4 --- /dev/null +++ b/test/build/config-format/typescript-tsx/package.json @@ -0,0 +1,6 @@ +{ + "type": "commonjs", + "engines": { + "node": ">=18.12.0" + } +} diff --git a/test/build/config-format/typescript-tsx/typescript.test.mjs b/test/build/config-format/typescript-tsx/typescript.test.mjs new file mode 100644 index 00000000000..dad922717f4 --- /dev/null +++ b/test/build/config-format/typescript-tsx/typescript.test.mjs @@ -0,0 +1,96 @@ +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join, 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); + +// The repository itself has working `interpret` loaders installed (`ts-node`, +// `@babel/register`, ...), so to prove the `tsx` fallback is really used this +// fixture shadows every loader `interpret` knows for TypeScript and JSX with +// a stub that throws. `tsx` itself is intentionally NOT stubbed: it must be resolved +// from the repository root, so the real package is exercised. +const loaderStubs = [ + "ts-node/register.js", + "sucrase/register/ts.js", + "sucrase/register/tsx.js", + "sucrase/register/jsx.js", + "@babel/register/index.js", + "esbuild-register/dist/node.js", + "@swc/register/index.js", +]; + +describe("typescript configuration with tsx", () => { + beforeAll(() => { + for (const stub of loaderStubs) { + const stubPath = join(__dirname, "node_modules", stub); + + mkdirSync(dirname(stubPath), { recursive: true }); + writeFileSync(stubPath, `throw new Error("stub: '${stub}' is disabled in this fixture");\n`); + } + }); + + it("should load a `.ts` configuration through the `tsx` fallback", async () => { + const [major] = process.versions.node.split(".").map(Number); + const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.ts"], { + nodeOptions: [ + // Disable built-in type stripping so the `interpret`/`tsx` fallback is exercised + ...(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(); + }); + + it("should load a `.mts` configuration through the `tsx` fallback", async () => { + const [major] = process.versions.node.split(".").map(Number); + const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.mts"], { + nodeOptions: [ + // Disable built-in type stripping so the `tsx` fallback is exercised + ...(major >= 22 ? ["--no-experimental-strip-types"] : []), + ], + }); + + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + expect(exitCode).toBe(0); + expect(existsSync(resolve(__dirname, "dist/bar.bundle.js"))).toBeTruthy(); + }); + + it("should load a `.cts` configuration through the `tsx` fallback", async () => { + const [major] = process.versions.node.split(".").map(Number); + const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.cts"], { + nodeOptions: [ + // Disable built-in type stripping so the `interpret`/`tsx` fallback is exercised + ...(major >= 22 ? ["--no-experimental-strip-types"] : []), + ], + }); + + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + expect(exitCode).toBe(0); + expect(existsSync(resolve(__dirname, "dist/qux.bundle.js"))).toBeTruthy(); + }); + + it("should load a `.tsx` configuration through the `tsx` fallback", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.tsx"]); + + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + expect(exitCode).toBe(0); + expect(existsSync(resolve(__dirname, "dist/quux.bundle.js"))).toBeTruthy(); + }); + + it("should load a `.jsx` configuration through the `tsx` fallback", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.jsx"]); + + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + expect(exitCode).toBe(0); + expect(existsSync(resolve(__dirname, "dist/baz.bundle.js"))).toBeTruthy(); + }); +}); diff --git a/test/build/config-format/typescript-tsx/webpack.config.cts b/test/build/config-format/typescript-tsx/webpack.config.cts new file mode 100644 index 00000000000..33d1d11d46b --- /dev/null +++ b/test/build/config-format/typescript-tsx/webpack.config.cts @@ -0,0 +1,15 @@ +const path = require("node:path"); + +/* eslint-disable no-useless-concat */ + +const filename: string = "qux" + ".bundle.js"; + +const config = { + entry: "./main.ts", + output: { + path: path.resolve("dist"), + filename, + }, +}; + +module.exports = config; diff --git a/test/build/config-format/typescript-tsx/webpack.config.jsx b/test/build/config-format/typescript-tsx/webpack.config.jsx new file mode 100644 index 00000000000..64a75e8277d --- /dev/null +++ b/test/build/config-format/typescript-tsx/webpack.config.jsx @@ -0,0 +1,16 @@ +/** @jsx createElement */ + +const path = require("node:path"); + +const createElement = (tag, props) => ({ tag, props }); +const banner =
; + +const config = { + entry: "./main.ts", + output: { + path: path.resolve("dist"), + filename: `${banner.props.title}.bundle.js`, + }, +}; + +module.exports = config; diff --git a/test/build/config-format/typescript-tsx/webpack.config.mts b/test/build/config-format/typescript-tsx/webpack.config.mts new file mode 100644 index 00000000000..c3ecbbf2ac7 --- /dev/null +++ b/test/build/config-format/typescript-tsx/webpack.config.mts @@ -0,0 +1,13 @@ +import path from "node:path"; + +/* eslint-disable no-useless-concat */ + +const filename: string = "bar" + ".bundle.js"; + +export default { + entry: "./main.ts", + output: { + path: path.resolve("dist"), + filename, + }, +}; diff --git a/test/build/config-format/typescript-tsx/webpack.config.ts b/test/build/config-format/typescript-tsx/webpack.config.ts new file mode 100644 index 00000000000..5059650f78b --- /dev/null +++ b/test/build/config-format/typescript-tsx/webpack.config.ts @@ -0,0 +1,15 @@ +const path = require("node:path"); + +/* eslint-disable no-useless-concat */ + +const filename: string = "foo" + ".bundle.js"; + +const config = { + entry: "./main.ts", + output: { + path: path.resolve("dist"), + filename, + }, +}; + +module.exports = config; diff --git a/test/build/config-format/typescript-tsx/webpack.config.tsx b/test/build/config-format/typescript-tsx/webpack.config.tsx new file mode 100644 index 00000000000..d79dcb1df4b --- /dev/null +++ b/test/build/config-format/typescript-tsx/webpack.config.tsx @@ -0,0 +1,23 @@ +/** @jsx createElement */ + +const path = require("node:path"); + +interface Banner { + tag: string; + props: { title: string }; +} + +// `createElement` is consumed through the `@jsx` pragma above +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const createElement = (tag: string, props: { title: string }): Banner => ({ tag, props }); +const banner: Banner =
; + +const config = { + entry: "./main.ts", + output: { + path: path.resolve("dist"), + filename: `${banner.props.title}.bundle.js`, + }, +}; + +module.exports = config;