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 +}