diff --git a/docs/guide/migrate-from-unbuild.md b/docs/guide/migrate-from-unbuild.md
new file mode 100644
index 000000000..64e4cdd6c
--- /dev/null
+++ b/docs/guide/migrate-from-unbuild.md
@@ -0,0 +1,47 @@
+# Migrate from unbuild
+
+[unbuild](https://github.com/unjs/unbuild) is a popular unified JavaScript build system from the unjs ecosystem. If you're currently using `unbuild` and want to migrate to `tsdown`, the process is straightforward thanks to the dedicated `migrate` command with the `--from unbuild` option:
+
+```bash
+npx tsdown migrate --from unbuild
+```
+
+> [!WARNING]
+> Please save your changes before migration. The migration process may modify your configuration files, so it's important to ensure all your changes are committed or backed up beforehand.
+
+## What Gets Migrated
+
+The migration process handles:
+
+1. **Package.json** changes:
+ - Dependencies: Updates `unbuild` dependencies to `tsdown`
+ - Scripts: Updates any scripts using `unbuild` to use `tsdown` instead
+ - Configuration: Migrates any `unbuild` configuration in package.json to `tsdown` format
+
+2. **Config Files**:
+ - Converts `build.config.*` files to `tsdown.config.*` files
+ - Updates imports from `unbuild` to `tsdown`
+ - Transforms `defineBuildConfig` to `defineConfig`
+ - Adapts build configuration options to match tsdown equivalents
+
+## Migration Options
+
+The `migrate` command supports the following options to customize the migration process:
+
+- `--cwd
` (or `-c`): Specify the working directory for the migration.
+- `--dry-run` (or `-d`): Perform a dry run to preview the migration without making any changes.
+- `--from `: Specify the source bundler to migrate from (defaults to `tsup`, use `unbuild` for unbuild migration).
+
+With these options, you can easily tailor the migration process to fit your specific project setup.
+
+## Differences from unbuild
+
+While `tsdown` aims to provide a smooth migration experience from `unbuild`, there are some notable differences to be aware of:
+
+1. **Configuration Format**: tsdown uses a slightly different configuration format. The migration script handles most common options, but you may need to manually adjust some advanced configurations.
+
+2. **Builders**: unbuild's `mkdist` builder is mapped to tsdown's `dts` builder, but there might be slight differences in behavior.
+
+3. **Rollup vs Rolldown**: unbuild uses Rollup, whereas tsdown uses [Rolldown](https://rolldown.rs/), a Rust-based bundler that's faster and more efficient.
+
+After migration, it's recommended to review your configuration and build output to ensure everything is working as expected.
diff --git a/src/cli.ts b/src/cli.ts
index 72914817a..75a5e26c8 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -51,10 +51,13 @@ cli
})
cli
- .command('migrate', 'Migrate from tsup to tsdown')
+ .command('migrate', 'Migrate from tsup or unbuild to tsdown')
.option('-c, --cwd ', 'Working directory')
.option('-d, --dry-run', 'Dry run')
+ .option('--tsup', 'Migrate from tsup, default')
+ .option('--unbuild', 'Migrate from unbuild')
.action(async (args) => {
+ args.from = args.unbuild ? 'unbuild' : 'tsup'
const { migrate } = await import('./migrate')
await migrate(args)
})
diff --git a/src/migrate/index.ts b/src/migrate/index.ts
new file mode 100644
index 000000000..8ea6d3d2e
--- /dev/null
+++ b/src/migrate/index.ts
@@ -0,0 +1,57 @@
+import process from 'node:process'
+import { green, underline } from 'ansis'
+import consola from 'consola'
+import { migrateTsup } from './tsup'
+import { migrateUnbuild } from './unbuild'
+
+export async function migrate({
+ cwd,
+ dryRun,
+ from,
+}: {
+ cwd?: string
+ dryRun?: boolean
+ from?: 'tsup' | 'unbuild'
+}): Promise {
+ if (dryRun) {
+ consola.info('Dry run enabled. No changes were made.')
+ } else {
+ const confirm = await consola.prompt(
+ `Before proceeding, review the migration guide at ${underline`https://tsdown.dev/guide/migrate-from-${from}`}, as this process will modify your files.\n` +
+ `Uncommitted changes will be lost. Use the ${green`--dry-run`} flag to preview changes without applying them.\n\n` +
+ 'Continue?',
+ { type: 'confirm' },
+ )
+ if (!confirm) {
+ consola.error('Migration cancelled.')
+ process.exitCode = 1
+ return
+ }
+ }
+
+ if (cwd) process.chdir(cwd)
+
+ if (from === 'unbuild') {
+ return migrateUnbuild(dryRun)
+ }
+ // Default to tsup migration
+ return migrateTsup(dryRun)
+}
+
+// rename key but keep order
+export function renameKey(
+ obj: Record,
+ oldKey: string,
+ newKey: string,
+ newValue?: any,
+): Record {
+ const newObj: Record = {}
+ for (const key of Object.keys(obj)) {
+ if (key === oldKey) {
+ newObj[newKey] = newValue || obj[oldKey]
+ } else {
+ newObj[key] = obj[key]
+ }
+ }
+ return newObj
+}
diff --git a/src/migrate.ts b/src/migrate/tsup.ts
similarity index 75%
rename from src/migrate.ts
rename to src/migrate/tsup.ts
index 3cb248eaa..c00e1a644 100644
--- a/src/migrate.ts
+++ b/src/migrate/tsup.ts
@@ -1,35 +1,11 @@
import { existsSync } from 'node:fs'
import { readFile, unlink, writeFile } from 'node:fs/promises'
import process from 'node:process'
-import { green, underline } from 'ansis'
import consola from 'consola'
-import { version } from '../package.json'
-
-export async function migrate({
- cwd,
- dryRun,
-}: {
- cwd?: string
- dryRun?: boolean
-}): Promise {
- if (dryRun) {
- consola.info('Dry run enabled. No changes were made.')
- } else {
- const confirm = await consola.prompt(
- `Before proceeding, review the migration guide at ${underline`https://tsdown.dev/guide/migrate-from-tsup`}, as this process will modify your files.\n` +
- `Uncommitted changes will be lost. Use the ${green`--dry-run`} flag to preview changes without applying them.\n\n` +
- 'Continue?',
- { type: 'confirm' },
- )
- if (!confirm) {
- consola.error('Migration cancelled.')
- process.exitCode = 1
- return
- }
- }
-
- if (cwd) process.chdir(cwd)
+import { version } from '../../package.json'
+import { renameKey } from '.'
+export async function migrateTsup(dryRun?: boolean): Promise {
let migrated = await migratePackageJson(dryRun)
if (await migrateTsupConfig(dryRun)) {
migrated = true
@@ -123,6 +99,7 @@ const TSUP_FILES = [
'tsup.config.mjs',
'tsup.config.json',
]
+
async function migrateTsupConfig(dryRun?: boolean): Promise {
let found = false
@@ -156,21 +133,3 @@ async function migrateTsupConfig(dryRun?: boolean): Promise {
return found
}
-
-// rename key but keep order
-function renameKey(
- obj: Record,
- oldKey: string,
- newKey: string,
- newValue?: any,
-) {
- const newObj: Record = {}
- for (const key of Object.keys(obj)) {
- if (key === oldKey) {
- newObj[newKey] = newValue || obj[oldKey]
- } else {
- newObj[key] = obj[key]
- }
- }
- return newObj
-}
diff --git a/src/migrate/unbuild.ts b/src/migrate/unbuild.ts
new file mode 100644
index 000000000..deeced1cf
--- /dev/null
+++ b/src/migrate/unbuild.ts
@@ -0,0 +1,130 @@
+import { existsSync } from 'node:fs'
+import { readFile, unlink, writeFile } from 'node:fs/promises'
+import process from 'node:process'
+import consola from 'consola'
+import { version } from '../../package.json'
+import { renameKey } from '.'
+
+export async function migrateUnbuild(dryRun?: boolean): Promise {
+ let migrated = await migratePackageJson(dryRun)
+ if (await migrateUnbuildConfig(dryRun)) {
+ migrated = true
+ }
+ if (migrated) {
+ consola.success(
+ 'Migration completed. Remember to run install command with your package manager.',
+ )
+ } else {
+ consola.error('No migration performed.')
+ process.exitCode = 1
+ }
+}
+
+async function migratePackageJson(dryRun?: boolean): Promise {
+ if (!existsSync('package.json')) {
+ consola.error('No package.json found')
+ return false
+ }
+
+ const pkgRaw = await readFile('package.json', 'utf-8')
+ let pkg = JSON.parse(pkgRaw)
+ const semver = `^${version}`
+ let found = false
+ if (pkg.dependencies?.unbuild) {
+ consola.info('Migrating `dependencies` to tsdown.')
+ found = true
+ pkg.dependencies = renameKey(pkg.dependencies, 'unbuild', 'tsdown', semver)
+ }
+ if (pkg.devDependencies?.unbuild) {
+ consola.info('Migrating `devDependencies` to tsdown.')
+ found = true
+ pkg.devDependencies = renameKey(
+ pkg.devDependencies,
+ 'unbuild',
+ 'tsdown',
+ semver,
+ )
+ }
+ if (pkg.peerDependencies?.unbuild) {
+ consola.info('Migrating `peerDependencies` to tsdown.')
+ found = true
+ pkg.peerDependencies = renameKey(
+ pkg.peerDependencies,
+ 'unbuild',
+ 'tsdown',
+ '*',
+ )
+ }
+ if (pkg.unbuild) {
+ consola.info('Migrating `unbuild` field in package.json to `tsdown`.')
+ found = true
+ pkg = renameKey(pkg, 'unbuild', 'tsdown')
+ }
+
+ if (!found) {
+ consola.warn('No unbuild-related fields found in package.json')
+ return false
+ }
+
+ const pkgStr = `${JSON.stringify(pkg, null, 2)}\n`
+ if (dryRun) {
+ const { createPatch } = await import('diff')
+ consola.info('[dry-run] package.json:')
+ console.info(createPatch('package.json', pkgRaw, pkgStr))
+ } else {
+ await writeFile('package.json', pkgStr)
+ consola.success('Migrated `package.json`')
+ }
+ return true
+}
+
+const UNBUILD_CONFIG_FILES = [
+ 'build.config.ts',
+ 'build.config.cts',
+ 'build.config.mts',
+ 'build.config.js',
+ 'build.config.cjs',
+ 'build.config.mjs',
+ 'build.config.json',
+]
+
+async function migrateUnbuildConfig(dryRun?: boolean): Promise {
+ let found = false
+
+ for (const file of UNBUILD_CONFIG_FILES) {
+ if (!existsSync(file)) continue
+ consola.info(`Found \`${file}\``)
+ found = true
+
+ const unbuildConfigRaw = await readFile(file, 'utf-8')
+
+ let tsdownConfig = unbuildConfigRaw
+ .replaceAll(/\bunbuild\b/g, 'tsdown')
+ .replaceAll(/\bdefineBuildConfig\b/g, 'defineConfig')
+
+ // Replace unbuild specific options with tsdown equivalents
+ // This is a simplified approach - might need to be expanded based on actual options mapping
+ tsdownConfig = tsdownConfig
+ .replaceAll(/builder:\s*["']mkdist["']/g, 'builder: "dts"')
+ .replaceAll('rollup:', 'rolldown:')
+
+ const renamed = file.replace('build', 'tsdown')
+ if (dryRun) {
+ const { createTwoFilesPatch } = await import('diff')
+ consola.info(`[dry-run] ${file} -> ${renamed}:`)
+ console.info(
+ createTwoFilesPatch(file, renamed, unbuildConfigRaw, tsdownConfig),
+ )
+ } else {
+ await writeFile(renamed, tsdownConfig, 'utf8')
+ await unlink(file)
+ consola.success(`Migrated \`${file}\` to \`${renamed}\``)
+ }
+ }
+
+ if (!found) {
+ consola.warn('No unbuild config found')
+ }
+
+ return found
+}