Describe the bug
When building with a monorepo where packages depend on each other, adding aliasExclude to make sure that the output d.ts files import the package by node_modules name is not respected. The output transforms the import statement to the local ts files instead of importing from the package.
This is a regression from v4, which worked properly.
Reproduction
https://github.com/anthony-bonta-gaf-energy/gafe-play-test
Steps to reproduce
git clone https://github.com/anthony-bonta-gaf-energy/gafe-play-test
cd gafe-play-test
yarn install
yarn build
Note the output of the dist src/index.d.ts files in package-b and package-c. Package C is correct, in that it outputted import { Hello } from '@gafe/package-a'. Package C depends on vite-plugin-dts version 4.
Now observe Package B:
import { Hello } from 'import { Hello } from '../../package-a/src/index.mts';
That is not correct. I had AI analyze this and it figured out that these options are not properly passed to the Runtime:
===============================================
Root Cause: Two bugs in unplugin-dts v1.0.0
Bug 1 — aliasesExclude is never forwarded to the Runtime
In buildStart, the aliasesExclude you pass to dts() is correctly used to filter the Vite resolve.alias list before Runtime.toInstance() is called. But it is never passed to Runtime.toInstance() itself:
// line 1758 — aliasesExclude is conspicuously absent
runtime = await Runtime.toInstance({
processor, root, outDirs, entryRoot,
tsconfigPath, compilerOptions,
pathsToAliases, // ✅ forwarded
aliases: options.aliases ?? aliases, // ✅ forwarded (pre-filtered)
...
// ❌ aliasesExclude is NOT forwarded
});
Inside Runtime, aliasesExclude therefore defaults to []:
// line 1020 — defaults to [] because the caller never passes it
aliasesExclude = [],
So when emitOutput calls transformCode({ ..., aliasesExclude }), aliasesExclude is always [], and the guard inside transformAlias (!aliasesExclude.some(...)) is always true, meaning every alias is transformed.
Bug 2 — tsconfig paths bypass the filter entirely
Even if Bug 1 were fixed, there's a second problem. The Runtime constructor first calls parseAliases(options.aliases, aliasesExclude) which does apply the filter — but then appends tsconfig paths (via parseTsAliases) after the filter, with no filtering applied:
// line 1031 — filter IS applied here, to the explicit aliases option
const aliases = parseAliases(options.aliases, aliasesExclude);
// line 1052 — tsconfig paths are pushed AFTER the filter, unconditionally
if (pathsToAliases && resolvedBaseUrl && paths) {
aliases.push(
...parseTsAliases(resolvedBaseUrl, paths) // ← no aliasesExclude check
);
}
With pathsToAliases: true (the default), all three land in the alias list unchecked. aliasesExclude: [/^@gafe//] should have removed the @gafe/ two, but because of both bugs it never does — so @gafe/package-a gets transformed from the import string into ../../../../package-a/src/index.mts.
The Fix — patch unplugin-dts
The minimal code change needed is two lines. Use yarn patch to make it permanent:
yarn patch unplugin-dts
Then make these two changes in the patched copy of dist/shared/unplugin-dts.BSDVFQ9C.cjs:
Fix 1 — forward aliasesExclude to Runtime.toInstance (line ~1772, inside buildStart):
pathsToAliases, include, exclude, resolvers, entries,
aliases: options.aliases ?? aliases,
libName, indexName, logger,
aliasesExclude,
});
Fix 2 — filter tsconfig paths through aliasesExclude before pushing them (line ~1052, inside the Runtime constructor):
if (pathsToAliases && resolvedBaseUrl && paths) {
const tsAliases = parseTsAliases(resolvedBaseUrl, paths);
aliases.push(
...parseTsAliases(resolvedBaseUrl, paths)
...(aliasesExclude.length > 0
? tsAliases.filter(({ find }) =>
!aliasesExclude.some((e) =>
isRegExp(find)
? find.toString() === e.toString()
: isRegExp(e) ? find.match(e)?.[0] : find === e
)
)
: tsAliases)
);
}
Then commit the patch:
yarn patch-commit /tmp/unplugin-dts-... # path yarn printed
Why this worked in vite-plugin-dts v4
vite-plugin-dts v4 had the same two-phase structure, but it passed aliasesExclude all the way through to the runtime correctly and also filtered the tsconfig path results before merging them. unplugin-dts is a rewrite/port and the forwarding was simply omitted.
System Info
System:
OS: macOS 15.7.5
CPU: (12) arm64 Apple M4 Pro
Memory: 2.11 GB / 48.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 24.15.0 - ~/.nvm/versions/node/v24.15.0/bin/node
Yarn: 4.14.1 - /opt/homebrew/bin/yarn
npm: 11.12.1 - ~/.nvm/versions/node/v24.15.0/bin/npm
Browsers:
Chrome: 146.0.7680.178
Safari: 26.3.1
Validations
Describe the bug
When building with a monorepo where packages depend on each other, adding aliasExclude to make sure that the output d.ts files import the package by node_modules name is not respected. The output transforms the import statement to the local ts files instead of importing from the package.
This is a regression from v4, which worked properly.
Reproduction
https://github.com/anthony-bonta-gaf-energy/gafe-play-test
Steps to reproduce
Note the output of the dist src/index.d.ts files in package-b and package-c. Package C is correct, in that it outputted import { Hello } from '@gafe/package-a'. Package C depends on vite-plugin-dts version 4.
Now observe Package B:
import { Hello } from 'import { Hello } from '../../package-a/src/index.mts';
That is not correct. I had AI analyze this and it figured out that these options are not properly passed to the Runtime:
===============================================
Root Cause: Two bugs in unplugin-dts v1.0.0
Bug 1 — aliasesExclude is never forwarded to the Runtime
In buildStart, the aliasesExclude you pass to dts() is correctly used to filter the Vite resolve.alias list before Runtime.toInstance() is called. But it is never passed to Runtime.toInstance() itself:
// line 1758 — aliasesExclude is conspicuously absent
runtime = await Runtime.toInstance({
processor, root, outDirs, entryRoot,
tsconfigPath, compilerOptions,
pathsToAliases, // ✅ forwarded
aliases: options.aliases ?? aliases, // ✅ forwarded (pre-filtered)
...
// ❌ aliasesExclude is NOT forwarded
});
Inside Runtime, aliasesExclude therefore defaults to []:
// line 1020 — defaults to [] because the caller never passes it
aliasesExclude = [],
So when emitOutput calls transformCode({ ..., aliasesExclude }), aliasesExclude is always [], and the guard inside transformAlias (!aliasesExclude.some(...)) is always true, meaning every alias is transformed.
Bug 2 — tsconfig paths bypass the filter entirely
Even if Bug 1 were fixed, there's a second problem. The Runtime constructor first calls parseAliases(options.aliases, aliasesExclude) which does apply the filter — but then appends tsconfig paths (via parseTsAliases) after the filter, with no filtering applied:
// line 1031 — filter IS applied here, to the explicit aliases option
const aliases = parseAliases(options.aliases, aliasesExclude);
// line 1052 — tsconfig paths are pushed AFTER the filter, unconditionally
if (pathsToAliases && resolvedBaseUrl && paths) {
aliases.push(
...parseTsAliases(resolvedBaseUrl, paths) // ← no aliasesExclude check
);
}
With pathsToAliases: true (the default), all three land in the alias list unchecked. aliasesExclude: [/^@gafe//] should have removed the @gafe/ two, but because of both bugs it never does — so @gafe/package-a gets transformed from the import string into ../../../../package-a/src/index.mts.
The Fix — patch unplugin-dts
The minimal code change needed is two lines. Use yarn patch to make it permanent:
yarn patch unplugin-dts
Then make these two changes in the patched copy of dist/shared/unplugin-dts.BSDVFQ9C.cjs:
Fix 1 — forward aliasesExclude to Runtime.toInstance (line ~1772, inside buildStart):
pathsToAliases, include, exclude, resolvers, entries,
aliases: options.aliases ?? aliases,
libName, indexName, logger,
aliasesExclude,
});
Fix 2 — filter tsconfig paths through aliasesExclude before pushing them (line ~1052, inside the Runtime constructor):
if (pathsToAliases && resolvedBaseUrl && paths) {
const tsAliases = parseTsAliases(resolvedBaseUrl, paths);
aliases.push(
...parseTsAliases(resolvedBaseUrl, paths)
...(aliasesExclude.length > 0
? tsAliases.filter(({ find }) =>
!aliasesExclude.some((e) =>
isRegExp(find)
? find.toString() === e.toString()
: isRegExp(e) ? find.match(e)?.[0] : find === e
)
)
: tsAliases)
);
}
Then commit the patch:
yarn patch-commit /tmp/unplugin-dts-... # path yarn printed
Why this worked in vite-plugin-dts v4
vite-plugin-dts v4 had the same two-phase structure, but it passed aliasesExclude all the way through to the runtime correctly and also filtered the tsconfig path results before merging them. unplugin-dts is a rewrite/port and the forwarding was simply omitted.
System Info
System: OS: macOS 15.7.5 CPU: (12) arm64 Apple M4 Pro Memory: 2.11 GB / 48.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 24.15.0 - ~/.nvm/versions/node/v24.15.0/bin/node Yarn: 4.14.1 - /opt/homebrew/bin/yarn npm: 11.12.1 - ~/.nvm/versions/node/v24.15.0/bin/npm Browsers: Chrome: 146.0.7680.178 Safari: 26.3.1Validations