Clear and concise description of the problem
When using unbundle: true with the alias option, path aliases are correctly resolved in the JS output (.mjs / .js) but remain unresolved in the DTS output (.d.ts / .d.mts).
This breaks consumers of the library because they do not have the same tsconfig path aliases configured, so TypeScript cannot resolve the imports from the declaration files.
Reproduction
tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~src/*": ["src/*"],
"~utils/*": ["src/utils/*"]
}
}
}
tsdown.config.ts
import path from 'node:path';
import { defineConfig } from 'tsdown';
export default defineConfig({
entry: ['src/**/*.ts'],
format: ['esm', 'cjs'],
dts: true,
unbundle: true,
alias: {
'~src': path.resolve('src'),
'~utils': path.resolve('src/utils'),
},
deps: {
neverBundle: (id) => !id.startsWith('.') && !id.startsWith('~') && !id.startsWith('\0'),
},
});
src/apis/getFoo.ts
import type { DefaultHeaders } from '~src/types';
export const getFoo = (params: DefaultHeaders) => fetch('/foo');
Expected output (dist/apis/getFoo.d.ts)
import { DefaultHeaders } from "../types"; // resolved relative path ✅
Actual output (dist/apis/getFoo.d.ts)
import { DefaultHeaders } from "~src/types"; // unresolved alias ❌
Root cause
The alias option feeds into Rolldown's JS resolution pipeline. However, the TypeScript DTS emitter runs as a separate step and is not aware of the alias config — it preserves the original import specifiers as written in the source.
This means the two outputs (JS and DTS) are inconsistent: JS imports work correctly at runtime, but DTS imports break TypeScript consumers of the published package.
Current workaround
A post-build script that walks dist/ and rewrites alias specifiers to relative paths:
// scripts/fix-dts-aliases.mjs
import { readdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, relative, resolve } from 'node:path';
const DIST_DIR = resolve(process.cwd(), 'dist');
const ALIASES = [
['~utils', 'utils'],
['~src', ''],
];
async function* walkDts(dir) {
for (const entry of await readdir(dir, { withFileTypes: true })) {
const full = `${dir}/${entry.name}`;
if (entry.isDirectory()) yield* walkDts(full);
else if (entry.name.endsWith('.d.ts') || entry.name.endsWith('.d.mts')) yield full;
}
}
async function fixFile(filePath) {
const original = await readFile(filePath, 'utf8');
const fileDir = dirname(filePath);
const fixed = ALIASES.reduce((content, [alias, targetBase]) => {
return content.replace(new RegExp(`"${alias}(/[^"]*)?"`,'g'), (_, sub = '') => {
const subPath = sub.startsWith('/') ? sub.slice(1) : sub;
const absTarget = targetBase ? resolve(DIST_DIR, targetBase, subPath) : resolve(DIST_DIR, subPath);
let rel = relative(fileDir, absTarget);
if (!rel.startsWith('.')) rel = `./${rel}`;
return `"${rel}"`;
});
}, original);
if (fixed !== original) await writeFile(filePath, fixed);
}
for await (const file of walkDts(DIST_DIR)) await fixFile(file);
Suggested solution
The alias config (or at minimum tsconfig paths) should be applied to the DTS generation step as well, so aliases are resolved to relative paths in the declaration output. This would be consistent with how tools like tsc-alias or typescript-transform-paths handle this for the TypeScript compiler.
Environment
tsdown: 0.21.2
typescript: 5.9.3
node: v24.14.0
Clear and concise description of the problem
When using
unbundle: truewith thealiasoption, path aliases are correctly resolved in the JS output (.mjs/.js) but remain unresolved in the DTS output (.d.ts/.d.mts).This breaks consumers of the library because they do not have the same
tsconfigpath aliases configured, so TypeScript cannot resolve the imports from the declaration files.Reproduction
tsconfig.json{ "compilerOptions": { "baseUrl": ".", "paths": { "~src/*": ["src/*"], "~utils/*": ["src/utils/*"] } } }tsdown.config.tssrc/apis/getFoo.tsExpected output (
dist/apis/getFoo.d.ts)Actual output (
dist/apis/getFoo.d.ts)Root cause
The
aliasoption feeds into Rolldown's JS resolution pipeline. However, the TypeScript DTS emitter runs as a separate step and is not aware of the alias config — it preserves the original import specifiers as written in the source.This means the two outputs (JS and DTS) are inconsistent: JS imports work correctly at runtime, but DTS imports break TypeScript consumers of the published package.
Current workaround
A post-build script that walks
dist/and rewrites alias specifiers to relative paths:Suggested solution
The
aliasconfig (or at minimumtsconfigpaths) should be applied to the DTS generation step as well, so aliases are resolved to relative paths in the declaration output. This would be consistent with how tools liketsc-aliasortypescript-transform-pathshandle this for the TypeScript compiler.Environment
tsdown: 0.21.2typescript: 5.9.3node: v24.14.0