System Info
System:
OS: Linux 6.6 Debian GNU/Linux forky/sid (WSL2)
CPU: (28) x64 Intel(R) Core(TM) i7-14700T
Memory: 18.96 GB / 31.16 GB
Shell: 5.9.2 - /bin/zsh
Binaries:
Node: 24.21.0
npm: 11.19.0
pnpm: 12.4.2
npmPackages:
@rspack/cli: 2.2.4 => 2.2.4
@rspack/core: 2.2.4 => 2.2.4
Details
When resolve.tsConfig points at a tsconfig.json whose extends chain passes
through a package in node_modules, and that package is a symlink (the normal
pnpm layout), rspack fails to load the chain:
ERROR in ./entry.ts 1:1-31
× Tsconfig not found @tsconfig/node22/tsconfig.json
The chain is:
./tsconfig.json
extends "shared-config/tsconfig.json" <- node_modules/shared-config (symlink)
extends "@tsconfig/node22/tsconfig.json" <- only reachable from the symlink TARGET
@tsconfig/node22 is a dependency of shared-config, so under pnpm it lives at
node_modules/.pnpm/shared-config@.../node_modules/@tsconfig/node22, next to the
real shared-config directory. It is deliberately not a dependency of the root
project, so it is not present in the root node_modules.
It looks like the nested extends is resolved starting from the symlink path
(<root>/node_modules/shared-config/), which walks up to <root>/node_modules/
and finds nothing, instead of from the file's real path, which would walk up to
<root>/node_modules/.pnpm/shared-config@.../node_modules/ and find it.
Evidence that this is symlink-specific: if the same package tree is materialised
without a symlink (npm-style node_modules/shared-config/ as a real directory
with its own node_modules/@tsconfig/node22), the identical build succeeds.
tsc resolves the same chain without complaint — npx tsc --showConfig prints the
fully merged config — so this is an rspack/rspack-resolver divergence from
TypeScript's own extends resolution.
This is easy to hit unintentionally: any monorepo that ships a shared base
tsconfig as an npm package which itself extends @tsconfig/* will break under
pnpm.
Where I actually hit it
Via Rstest, which sets
resolve.tsConfig automatically. There the failure is much harder to diagnose,
because the resolve error is swallowed: autoExternalNodeModules treats any
resolver error as "externalize this request"
(autoExternalNodeModules in dist/rsbuild~0.js:
if (err) return callback(void 0, request, 'node-commonjs')), and
IgnoreModuleNotFoundErrorPlugin strips the build error. Every module in the
project is then externalized to Node, and the only visible symptom is a runtime
Cannot find module '.../foo.js' imported from '.../a.spec.ts'. Patching that
line to log the error yields the real cause:
RspackResolver(TsconfigNotFound("@tsconfig/node22/tsconfig.json"))
That part may deserve a separate issue on the Rstest side; filing here because
plain @rspack/cli reproduces it with no Rstest involved.
Reproduce Steps
Save as repro.sh and run it in an empty directory (needs npm, pnpm, network):
#!/bin/sh
set -eu
rm -rf rspack-tsconfig-extends-repro
mkdir -p rspack-tsconfig-extends-repro/pkg
cd rspack-tsconfig-extends-repro
# A package that ships a tsconfig which itself extends another package.
cat > pkg/package.json <<'EOF2'
{
"name": "shared-config",
"version": "1.0.0",
"files": ["tsconfig.json"],
"dependencies": { "@tsconfig/node22": "^22.0.0" }
}
EOF2
cat > pkg/tsconfig.json <<'EOF2'
{
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": { "strict": true }
}
EOF2
(cd pkg && npm pack --pack-destination .. >/dev/null)
rm -rf pkg
cat > package.json <<'EOF2'
{
"name": "rspack-tsconfig-extends-repro",
"private": true,
"dependencies": { "shared-config": "file:./shared-config-1.0.0.tgz" },
"devDependencies": { "@rspack/cli": "2.2.4", "@rspack/core": "2.2.4", "typescript": "^5" }
}
EOF2
cat > tsconfig.json <<'EOF2'
{
"extends": "shared-config/tsconfig.json",
"compilerOptions": { "baseUrl": ".", "paths": { "~/*": ["./*"] } }
}
EOF2
echo 'export const hello = 42;' > foo.ts
cat > entry.ts <<'EOF2'
import { hello } from '~/foo';
console.log(hello);
EOF2
cat > rspack.config.mjs <<'EOF2'
import path from 'node:path';
export default {
mode: 'development',
entry: './entry.ts',
devtool: false,
resolve: {
extensions: ['.ts', '.js'],
tsConfig: { configFile: path.resolve('tsconfig.json') }
},
module: {
rules: [{
test: /\.ts$/,
loader: 'builtin:swc-loader',
options: { jsc: { parser: { syntax: 'typescript' } } }
}]
},
output: { path: path.resolve('dist'), filename: 'main.js' }
};
EOF2
pnpm install --silent
echo; echo "--- node_modules layout (pnpm symlink) ---"
ls -l node_modules/shared-config
echo; echo "--- tsc (works) ---"
npx tsc --showConfig | head -5
echo; echo "--- rspack build (fails) ---"
npx rspack build || true
Observed output:
--- node_modules layout (pnpm symlink) ---
lrwxrwxrwx ... node_modules/shared-config -> .pnpm/shared-config@file+shared-config-1.0.0.tgz/node_modules/shared-config
--- tsc (works) ---
{
"compilerOptions": {
"lib": [
"es2024",
"esnext.array",
--- rspack build (fails) ---
ERROR in ./entry.ts 1:1-31
× Tsconfig not found @tsconfig/node22/tsconfig.json
Expected: the build succeeds, as it does with tsc and as it does when
node_modules/shared-config is a real directory rather than a symlink.
System Info
Details
When
resolve.tsConfigpoints at atsconfig.jsonwhoseextendschain passesthrough a package in
node_modules, and that package is a symlink (the normalpnpm layout), rspack fails to load the chain:
The chain is:
@tsconfig/node22is a dependency ofshared-config, so under pnpm it lives atnode_modules/.pnpm/shared-config@.../node_modules/@tsconfig/node22, next to thereal
shared-configdirectory. It is deliberately not a dependency of the rootproject, so it is not present in the root
node_modules.It looks like the nested
extendsis resolved starting from the symlink path(
<root>/node_modules/shared-config/), which walks up to<root>/node_modules/and finds nothing, instead of from the file's real path, which would walk up to
<root>/node_modules/.pnpm/shared-config@.../node_modules/and find it.Evidence that this is symlink-specific: if the same package tree is materialised
without a symlink (npm-style
node_modules/shared-config/as a real directorywith its own
node_modules/@tsconfig/node22), the identical build succeeds.tscresolves the same chain without complaint —npx tsc --showConfigprints thefully merged config — so this is an rspack/rspack-resolver divergence from
TypeScript's own
extendsresolution.This is easy to hit unintentionally: any monorepo that ships a shared base
tsconfig as an npm package which itself extends
@tsconfig/*will break underpnpm.
Where I actually hit it
Via Rstest, which sets
resolve.tsConfigautomatically. There the failure is much harder to diagnose,because the resolve error is swallowed:
autoExternalNodeModulestreats anyresolver error as "externalize this request"
(
autoExternalNodeModulesindist/rsbuild~0.js:if (err) return callback(void 0, request, 'node-commonjs')), andIgnoreModuleNotFoundErrorPluginstrips the build error. Every module in theproject is then externalized to Node, and the only visible symptom is a runtime
Cannot find module '.../foo.js' imported from '.../a.spec.ts'. Patching thatline to log the error yields the real cause:
That part may deserve a separate issue on the Rstest side; filing here because
plain
@rspack/clireproduces it with no Rstest involved.Reproduce Steps
Save as
repro.shand run it in an empty directory (needsnpm,pnpm, network):Observed output:
Expected: the build succeeds, as it does with
tscand as it does whennode_modules/shared-configis a real directory rather than a symlink.