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 __snapshots__/tsnapi/config.snapshot.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface UserConfig {
bundle?: boolean;
fixedExtension?: boolean;
outExtensions?: OutExtensionFactory;
outExtension?: OutExtensionFactory;
hash?: boolean;
cjsDefault?: boolean;
outputOptions?: OutputOptions | ((_: OutputOptions, _: NormalizedFormat, _: {
Expand Down
3 changes: 2 additions & 1 deletion __snapshots__/tsnapi/index.snapshot.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export interface UserConfig {
bundle?: boolean;
fixedExtension?: boolean;
outExtensions?: OutExtensionFactory;
outExtension?: OutExtensionFactory;
hash?: boolean;
cjsDefault?: boolean;
outputOptions?: OutputOptions | ((_: OutputOptions, _: NormalizedFormat, _: {
Expand Down Expand Up @@ -222,7 +223,7 @@ export type NoExternalFn = (_: string, _: string | undefined) => boolean | null
export type NormalizedFormat = InternalModuleFormat;
export type OutExtensionFactory = (_: OutExtensionContext) => OutExtensionObject | undefined;
export type PackageType = "module" | "commonjs" | undefined;
export type ResolvedConfig = Overwrite<MarkPartial<Omit<UserConfig, "workspace" | "fromVite" | "publicDir" | "bundle" | "injectStyle" | "removeNodeProtocol" | "external" | "noExternal" | "inlineOnly" | "skipNodeModulesBundle" | "logLevel" | "failOnWarn" | "customLogger" | "envFile" | "envPrefix">, "globalName" | "inputOptions" | "outputOptions" | "minify" | "define" | "alias" | "onSuccess" | "outExtensions" | "hooks" | "copy" | "loader" | "name" | "banner" | "footer" | "checks" | "css">, {
export type ResolvedConfig = Overwrite<MarkPartial<Omit<UserConfig, "workspace" | "fromVite" | "publicDir" | "bundle" | "injectStyle" | "removeNodeProtocol" | "outExtension" | "external" | "noExternal" | "inlineOnly" | "skipNodeModulesBundle" | "logLevel" | "failOnWarn" | "customLogger" | "envFile" | "envPrefix">, "globalName" | "inputOptions" | "outputOptions" | "minify" | "define" | "alias" | "onSuccess" | "outExtensions" | "hooks" | "copy" | "loader" | "name" | "banner" | "footer" | "checks" | "css">, {
entry: Record<string, string>;
rawEntry?: TsdownInputOption;
nameLabel: string | undefined;
Expand Down
22 changes: 18 additions & 4 deletions docs/guide/migrate-from-tsup.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ While `tsdown` aims to be highly compatible with `tsup`, there are some differen

Some options have been renamed for clarity:

| tsup | tsdown | Notes |
| ---------------- | ------------ | ---------------------------------- |
| `cjsInterop` | `cjsDefault` | CJS default export handling |
| `esbuildPlugins` | `plugins` | Now uses Rolldown/Unplugin plugins |
| tsup | tsdown | Notes |
| ---------------- | --------------- | ---------------------------------- |
| `cjsInterop` | `cjsDefault` | CJS default export handling |
| `esbuildPlugins` | `plugins` | Now uses Rolldown/Unplugin plugins |
| `outExtension` | `outExtensions` | Custom output extensions |

### Deprecated but Compatible Options

Expand All @@ -76,6 +77,19 @@ The following tsup options still work in tsdown for backward compatibility, but

tsdown also adds `deps.onlyBundle` for whitelisting allowed bundled packages.

### Output Filename Differences

For IIFE builds, `tsdown` emits names like `[name].iife.js`, while `tsup` commonly emitted `[name].global.js`. `outExtensions` customizes output extensions or suffixes, but it does not remove the built-in `.iife` or `.umd` segment. To preserve older full filename patterns, use Rolldown output options:

```ts
export default {
format: 'iife',
outputOptions: {
entryFileNames: '[name].global.js',
},
}
```

### Plugin System

tsdown uses [Rolldown](https://rolldown.rs/) plugins instead of esbuild plugins. If you use [unplugin](https://github.com/unjs/unplugin) plugins, update the import path:
Expand Down
3 changes: 3 additions & 0 deletions docs/options/output-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ tsdown --format iife
> [!NOTE]
> **CJS is in maintenance-only mode.** Since the ecosystem is transitioning to ESM and Node.js now supports `require(esm)`, `tsdown`'s CJS-specific features (such as [`cjsDefault`](./cjs-default.md)) are kept for compatibility but will not be further enhanced or optimized. New libraries are encouraged to publish ESM-only.

> [!NOTE]
> IIFE and UMD outputs include the format in their filenames by default, such as `index.iife.js` and `index.umd.js`. If you need a custom full filename pattern, set `outputOptions.entryFileNames`.

## Overriding Configuration by Format

You can override specific configuration options for each output format by setting `format` as an object in your config file. This allows you to tailor settings such as `target` or other options for each format individually.
Expand Down
22 changes: 18 additions & 4 deletions docs/zh-CN/guide/migrate-from-tsup.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ npx tsdown-migrate packages/foo packages/bar

部分选项已重命名以提高清晰度:

| tsup | tsdown | 说明 |
| ---------------- | ------------ | ----------------------------- |
| `cjsInterop` | `cjsDefault` | CJS 默认导出处理 |
| `esbuildPlugins` | `plugins` | 现使用 Rolldown/Unplugin 插件 |
| tsup | tsdown | 说明 |
| ---------------- | --------------- | ----------------------------- |
| `cjsInterop` | `cjsDefault` | CJS 默认导出处理 |
| `esbuildPlugins` | `plugins` | 现使用 Rolldown/Unplugin 插件 |
| `outExtension` | `outExtensions` | 自定义输出扩展名 |

### 已弃用但兼容的选项

Expand All @@ -76,6 +77,19 @@ npx tsdown-migrate packages/foo packages/bar

tsdown 还新增了 `deps.onlyBundle`,用于白名单指定允许打包的依赖。

### 输出文件名差异

对于 IIFE 构建,`tsdown` 会输出类似 `[name].iife.js` 的文件名,而 `tsup` 常见输出是 `[name].global.js`。`outExtensions` 用于自定义输出扩展名或后缀,但不会移除内置的 `.iife` 或 `.umd` 片段。如果需要保留旧的完整文件名模式,请使用 Rolldown 的输出选项:

```ts
export default {
format: 'iife',
outputOptions: {
entryFileNames: '[name].global.js',
},
}
```

### 插件系统

tsdown 使用 [Rolldown](https://rolldown.rs/) 插件代替 esbuild 插件。如果您使用 [unplugin](https://github.com/unjs/unplugin) 插件,需更新导入路径:
Expand Down
3 changes: 3 additions & 0 deletions docs/zh-CN/options/output-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ tsdown --format iife
> [!NOTE]
> **CJS 处于仅维护模式。** 由于生态系统正在向 ESM 迁移,且 Node.js 已支持 `require(esm)`,`tsdown` 中 CJS 专属的功能(例如 [`cjsDefault`](./cjs-default.md))仅保留以确保兼容性,不会再进一步增强或优化。建议新库直接发布为 ESM-only。

> [!NOTE]
> IIFE 和 UMD 输出默认会在文件名中包含格式片段,例如 `index.iife.js` 和 `index.umd.js`。如果需要自定义完整文件名模式,请设置 `outputOptions.entryFileNames`。

## 按格式覆盖配置

您可以在配置文件中将 `format` 设置为对象,从而为每种输出格式单独覆盖特定配置选项。这允许您为每个格式分别定制如 `target` 等设置。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,28 @@ exports[`option transformations > entryPoints should transform to entry > code 1
}"
`;

exports[`option transformations > outExtension method should transform to outExtensions > code 1`] = `
"export default {
format: 'esm',
outExtensions() {
return { js: '.mjs' }
},
clean: false,
dts: false,
target: false,
}"
`;

exports[`option transformations > outExtension property should transform to outExtensions > code 1`] = `
"export default {
format: 'esm',
outExtensions: () => ({ js: '.mjs' }),
clean: false,
dts: false,
target: false,
}"
`;

exports[`option transformations > publicDir should transform to copy > code 1`] = `
"export default {
copy: 'public',
Expand Down
28 changes: 28 additions & 0 deletions packages/migrate/src/helpers/tsup-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,34 @@ describe('option transformations', () => {
expect(code).not.toContain('publicDir')
})

test('outExtension property should transform to outExtensions', () => {
const input = `
export default {
format: 'esm',
outExtension: () => ({ js: '.mjs' }),
}
`
const { code } = transform(input, 'tsup.config.ts')
expect(code).toContain('outExtensions:')
expect(code).toContain("js: '.mjs'")
expect(code).not.toContain('outExtension:')
})

test('outExtension method should transform to outExtensions', () => {
const input = `
export default {
format: 'esm',
outExtension() {
return { js: '.mjs' }
},
}
`
const { code } = transform(input, 'tsup.config.ts')
expect(code).toContain('outExtensions()')
expect(code).toContain("js: '.mjs'")
expect(code).not.toContain('outExtension()')
})

test('removeNodeProtocol should transform to nodeProtocol: "strip"', () => {
const input = `
export default {
Expand Down
33 changes: 22 additions & 11 deletions packages/migrate/src/helpers/tsup-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const WARNING_MESSAGES: Record<string, string> = {
const PROPERTY_RENAMES: Record<string, string> = {
entryPoints: 'entry',
esbuildPlugins: 'plugins',
outExtension: 'outExtensions',
publicDir: 'copy',
cjsInterop: 'cjsDefault',
}
Expand Down Expand Up @@ -101,24 +102,34 @@ export function transformTsupConfig(
edits.push(node.replace(text.replace('/esbuild', '/rolldown')))
}

// Helper: Find property identifier (key only) by name using relational rule
const findPropertyIdentifier = (propName: string): SgNode | null => {
return root.find({
// Helper: Find property identifiers (keys only) by name using relational rules
const findPropertyIdentifiers = (propName: string): SgNode[] => {
return root.findAll({
rule: {
kind: 'property_identifier',
regex: `^${propName}$`,
inside: {
kind: 'pair',
field: 'key',
},
any: [
{
kind: 'property_identifier',
regex: `^${propName}$`,
inside: {
kind: 'pair',
field: 'key',
},
},
{
kind: 'property_identifier',
regex: `^${propName}$`,
inside: {
kind: 'method_definition',
},
},
],
},
})
}

// 3. Rename properties using AST - only replace the key identifier
for (const [oldName, newName] of Object.entries(PROPERTY_RENAMES)) {
const propIdentifier = findPropertyIdentifier(oldName)
if (propIdentifier) {
for (const propIdentifier of findPropertyIdentifiers(oldName)) {
edits.push(propIdentifier.replace(newName))
}
}
Expand Down
7 changes: 6 additions & 1 deletion skills/tsdown-migrate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Replace all identifiers: `tsup` → `tsdown`, `TSUP` → `TSDOWN`.
|------|--------|-------|
| `cjsInterop` | `cjsDefault` | CJS default export handling |
| `esbuildPlugins` | `plugins` | Now uses Rolldown/Unplugin plugins |
| `outExtension` | `outExtensions` | Custom output extensions |

### Deprecated but Compatible

Expand All @@ -87,6 +88,10 @@ These tsup options still work in tsdown for backward compatibility, but emit dep
| `noExternal: [...]` | `deps: { alwaysBundle: [...] }` | Moved to deps namespace |
| `skipNodeModulesBundle` | `deps: { skipNodeModulesBundle: true }` | Moved to deps namespace |

### Output Filename Differences

For IIFE builds, `tsdown` emits names like `[name].iife.js`, while `tsup` commonly emitted `[name].global.js`. `outExtensions` customizes extensions or suffixes, but it does not remove the built-in `.iife` or `.umd` segment. Use `outputOptions.entryFileNames: '[name].global.js'` to preserve old IIFE filenames.

### Dependency Namespace Moves

Dependencies config moved under `deps` namespace. If both `external` and `noExternal` exist, merge into a single `deps` object:
Expand Down Expand Up @@ -235,7 +240,7 @@ Use this checklist when performing a migration:
- [ ] Rename tsup.config.* → tsdown.config.*
- [ ] Update import from 'tsup' to 'tsdown'
- [ ] Replace tsup/TSUP identifiers with tsdown/TSDOWN
- [ ] Apply property renames (cjsInterop→cjsDefault, esbuildPlugins→plugins)
- [ ] Apply property renames (cjsInterop→cjsDefault, esbuildPlugins→plugins, outExtension→outExtensions)
- [ ] Migrate deprecated options (publicDir→copy, bundle→unbundle, removeNodeProtocol→nodeProtocol, injectStyle→css.inject)
- [ ] Move external/noExternal/skipNodeModulesBundle into deps namespace
- [ ] Update unplugin imports from /esbuild to /rolldown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Comprehensive comparison of tsdown and tsup for understanding migration impact a
|---------|------|--------|--------|
| CJS interop | `cjsInterop` | `cjsDefault` | Property rename |
| Plugins | `esbuildPlugins` | `plugins` | Different plugin format (Rolldown) |
| Output extensions | `outExtension` | `outExtensions` | Property rename |

### Deprecated but Compatible

Expand All @@ -61,6 +62,10 @@ These tsup options still work in tsdown but emit deprecation warnings. They will
| Inline deps | `noExternal` | `deps.alwaysBundle` | Moved to deps namespace |
| Skip node_modules | `skipNodeModulesBundle` | `deps.skipNodeModulesBundle` | Moved to deps namespace |

### Output Filename Differences

For IIFE builds, `tsdown` emits `[name].iife.js`; `tsup` commonly emitted `[name].global.js`. `outExtensions` customizes extensions or suffixes, but it does not remove `.iife` or `.umd`. Use `outputOptions.entryFileNames` for full filename patterns.

### Not Supported

| Feature | Reason | Alternative |
Expand Down
4 changes: 4 additions & 0 deletions skills/tsdown-migrate/references/guide-option-mappings.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export default defineConfig({

Note: All `unplugin-*/esbuild` imports must change to `unplugin-*/rolldown`.

### outExtension → outExtensions

`outExtension` was renamed to `outExtensions`.

## Deprecated but Compatible

These options still work in tsdown for backward compatibility but emit deprecation warnings. Migrate them immediately — they will be removed in a future version.
Expand Down
10 changes: 10 additions & 0 deletions skills/tsdown/references/guide-migrate-from-tsup.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ npx tsdown-migrate packages/foo packages/bar
| `dts` | `false` | Auto-enabled if `types`/`typings` in package.json |
| `target` | Manual | Auto-read from `engines.node` in package.json |

### Option Renames

| tsup | tsdown |
|------|--------|
| `outExtension` | `outExtensions` |

### Output Filename Differences

For IIFE builds, `tsdown` emits `[name].iife.js`; `tsup` commonly emitted `[name].global.js`. `outExtensions` customizes extensions or suffixes, but it does not remove `.iife` or `.umd`. Use `outputOptions.entryFileNames: '[name].global.js'` to preserve old IIFE filenames.

### New Features in tsdown

#### Node Protocol Control
Expand Down
4 changes: 3 additions & 1 deletion skills/tsdown/references/option-output-directory.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,11 @@ export default defineConfig({
|--------|-------------------|----------------------|
| `esm` | `.mjs` | `.js` |
| `cjs` | `.cjs` | `.js` |
| `iife` | `.global.js` | `.global.js` |
| `iife` | `.iife.js` | `.iife.js` |
| `umd` | `.umd.js` | `.umd.js` |

For IIFE/UMD builds, `outExtensions` customizes extensions or suffixes but does not remove the built-in `.iife` or `.umd` segment. Use `outputOptions.entryFileNames` for custom full filename patterns.

### ESM with .js Extension

```ts
Expand Down
6 changes: 4 additions & 2 deletions skills/tsdown/references/option-output-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export default defineConfig({
})
```

Output: `dist/index.global.js` (IIFE with global `MyLib`)
Output: `dist/index.iife.js` (IIFE with global `MyLib`)

### Universal Library (UMD)

Expand Down Expand Up @@ -147,9 +147,11 @@ export default defineConfig({
|--------|-----------|
| ESM | `.mjs` or `.js` (with `"type": "module"`) |
| CJS | `.cjs` or `.js` (without `"type": "module"`) |
| IIFE | `.global.js` |
| IIFE | `.iife.js` |
| UMD | `.umd.js` |

For custom IIFE filenames, set `outputOptions.entryFileNames`. `outExtensions` customizes extensions or suffixes but does not remove `.iife` or `.umd`.

### Customize Extensions

Use `outExtensions` to override:
Expand Down
15 changes: 15 additions & 0 deletions src/config/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ export async function resolveUserConfig(
globImport = true,
css,
injectStyle,
outExtension,
outExtensions,
fixedExtension = platform === 'node',
devtools = false,
write = true,
Expand Down Expand Up @@ -222,6 +224,18 @@ export async function resolveUserConfig(
}
}

if (outExtension) {
if (outExtensions) {
throw new TypeError(
'`outExtension` is deprecated. Cannot be used with `outExtensions`',
)
}
Comment thread
sxzz marked this conversation as resolved.
logger.warn(
`${blue`outExtension`} is deprecated. Use ${blue`outExtensions`} instead.`,
)
outExtensions = outExtension
}

envPrefix = toArray(envPrefix)
if (envPrefix.includes('')) {
logger.warn(
Expand Down Expand Up @@ -326,6 +340,7 @@ export async function resolveUserConfig(
nameLabel,
nodeProtocol,
outDir,
outExtensions,
pkg,
platform,
plugins,
Expand Down
Loading
Loading