diff --git a/docs/guide/migrate-rules.md b/docs/guide/migrate-rules.md index 561cbacf73..adeecfdcd3 100644 --- a/docs/guide/migrate-rules.md +++ b/docs/guide/migrate-rules.md @@ -228,6 +228,7 @@ scripts while preserving their arguments: | `lint-staged` | `vp staged` | | `eslint` | `vp lint`, when its optional migration runs | | `prettier` | `vp fmt`, when its optional migration runs | +| `tsup` | `vp pack`, when its optional migration runs | For commands launched through `bunx`, migration preserves `bunx` and its `--bun` flag (keeping the user's chosen runtime) and rewrites only the managed diff --git a/docs/guide/migrate.md b/docs/guide/migrate.md index a9243c27d7..d841330d75 100644 --- a/docs/guide/migrate.md +++ b/docs/guide/migrate.md @@ -4,7 +4,7 @@ ## Overview -This command is the starting point for consolidating separate Vite, Vitest, Oxlint, Oxfmt, ESLint, and Prettier setups into Vite+. +This command is the starting point for consolidating separate Vite, Vitest, Oxlint, Oxfmt, ESLint, Prettier, and tsup setups into Vite+. Use it when you want to take an existing project and move it onto the Vite+ defaults instead of wiring each tool by hand. @@ -70,7 +70,7 @@ After running the migration: - Run `vp install` - Run `vp check` - Run `vp test` -- Run `vp build` +- Run `vp build` (or `vp pack` if you are building a library) ## Manual Installation & Migration diff --git a/packages/cli/rules/vite-tools.yml b/packages/cli/rules/vite-tools.yml index 7e438987fc..2b02711e7a 100644 --- a/packages/cli/rules/vite-tools.yml +++ b/packages/cli/rules/vite-tools.yml @@ -84,3 +84,12 @@ rule: kind: command_name regex: '^tsdown$' fix: vp pack + +# tsup => vp pack +--- +id: replace-tsup +language: bash +rule: + kind: command_name + regex: '^tsup$' +fix: vp pack diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index c21f793146..3a2255ba85 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -12,11 +12,13 @@ import { detectEslintProject, detectFramework, detectPrettierProject, + detectTsupProject, hasFrameworkShim, injectCreateDefaultTemplate, installGitHooks, promptEslintMigration, promptPrettierMigration, + promptTsupMigration, rewriteMonorepo, rewriteMonorepoProject, rewriteStandaloneProject, @@ -1285,7 +1287,9 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h // and relies on `rewrite*Project` to add tarball overrides BEFORE the // first install, so install-first would break CI's local-tarball resolve. const shouldMigrateLintFmtTools = - detectEslintProject(fullPath).hasDependency || detectPrettierProject(fullPath).hasDependency; + detectEslintProject(fullPath).hasDependency || + detectPrettierProject(fullPath).hasDependency || + detectTsupProject(fullPath).hasDependency; let installSummary: CommandRunSummary | undefined; @@ -1325,10 +1329,11 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h if (installSummary.pendingBuilds && installSummary.pendingBuilds.length > 0) { migratePendingBuilds = installSummary.pendingBuilds; } - updateCreateProgress('Migrating lint and format tools'); + updateCreateProgress('Migrating lint, format & pack tools'); pauseCreateProgress(); await promptEslintMigration(fullPath, /* interactive */ false); await promptPrettierMigration(fullPath, /* interactive */ false); + await promptTsupMigration(fullPath, /* interactive */ false, packageManager); resumeCreateProgress(); }; diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index 75a1b5148c..7418f7bfca 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -47,6 +47,7 @@ import { detectNodeVersionManagerFile, detectPendingCoreMigration, detectPrettierProject, + detectTsupProject, detectVitePlusBootstrapPending, detectYarnPnpMode, ensureVitePlusBootstrap, @@ -55,10 +56,12 @@ import { detectLegacyGitHooksMigrationCandidate, injectLintTypeCheckDefaults, installGitHooks, + mergeTsdownConfigFile, mergeViteConfigFiles, migrateEslintToOxlint, migrateNodeVersionManagerFile, migratePrettierToOxfmt, + migrateTsupToTsdown, configureYarnNodeModulesMode, rewriteMonorepo, rewriteStandaloneProject, @@ -70,7 +73,11 @@ import { import { prepareNpmViteAliasReinstall } from './npm-reinstall.ts'; import type { MigrationOptions } from './options.ts'; import { addMigrationWarning, createMigrationReport, type MigrationReport } from './report.ts'; -import { collectMigrationSetupPlan, type MigrationSetupPlan } from './setup-plan.ts'; +import { + collectMigrationSetupPlan, + collectTsupMigrationDecision, + type MigrationSetupPlan, +} from './setup-plan.ts'; async function confirmNodeVersionFileMigration( interactive: boolean, @@ -379,6 +386,8 @@ interface MigrationPlan extends MigrationSetupPlan { migratePrettier: boolean; hasPrettierDependency: boolean; prettierConfigFile?: string; + migrateTsup: boolean; + tsupConfigFile?: string; fixBaseUrl: boolean; migrateNodeVersionFile: boolean; nodeVersionDetection?: NodeVersionManagerDetection; @@ -467,12 +476,14 @@ function hasExistingVitePlusMigrationCandidates( ): boolean { const eslintProject = detectEslintProject(workspaceInfo.rootDir, workspaceInfo.packages); const prettierProject = detectPrettierProject(workspaceInfo.rootDir, workspaceInfo.packages); + const tsupProject = detectTsupProject(workspaceInfo.rootDir, workspaceInfo.packages); return ( hasExplicitExistingVitePlusSetupRequest(options) || detectLegacyGitHooksMigrationCandidate(workspaceInfo.rootDir) || hasBaseUrlInWorkspace(workspaceInfo) || eslintProject.hasDependency || prettierProject.hasDependency || + tsupProject.hasDependency || detectNodeVersionManagerFile(workspaceInfo.rootDir) !== undefined || getFrameworkShimCandidates(workspaceInfo.rootDir, workspaceInfo.packages).length > 0 ); @@ -525,6 +536,13 @@ async function collectMigrationPlan( warnPackageLevelPrettier(); } + // 3b. tsup detection + prompt (after Prettier so Prettier -> Oxfmt is checked first) + const { migrateTsup, tsupConfigFile } = await collectTsupMigrationDecision( + rootDir, + options, + packages, + ); + // 9. tsconfig baseUrl prompt const fixBaseUrl = hasBaseUrlInWorkspace({ rootDir, packages }) ? await confirmBaseUrlFix(options.interactive) @@ -550,6 +568,8 @@ async function collectMigrationPlan( migratePrettier, hasPrettierDependency: prettierProject.hasDependency, prettierConfigFile: prettierProject.configFile, + migrateTsup, + tsupConfigFile, fixBaseUrl, migrateNodeVersionFile, nodeVersionDetection, @@ -691,6 +711,9 @@ function showMigrationSummary(options: { if (report.prettierMigrated) { log(`${styleText('gray', '•')} Prettier migrated to Oxfmt`); } + if (report.tsupMigrated) { + log(`${styleText('gray', '•')} tsup config migrated to tsdown (\`vp pack\`)`); + } if (report.nodeVersionFileMigrated) { log(`${styleText('gray', '•')} Node version manager file migrated to .node-version`); } @@ -905,6 +928,23 @@ async function executeMigrationPlan( } } + // 6b. tsup → tsdown migration (before main rewrite so tsdown.config.* gets picked up) + if (plan.migrateTsup) { + updateMigrationProgress('Migrating tsup'); + const tsupOk = await migrateTsupToTsdown( + workspaceInfo.rootDir, + interactive, + plan.packageManager, + plan.tsupConfigFile, + workspaceInfo.packages, + { silent: true, report }, + ); + if (!tsupOk) { + failMigrationProgress('Migration failed'); + cancelAndExit('tsup migration failed. Fix the issue and re-run `vp migrate`.', 1); + } + } + // Preserve lint-staged whenever hook setup is disabled/unsafe or existing // project-owned hooks remain authoritative. const skipStagedMigration = shouldSkipStagedMigrationForHooks( @@ -1345,6 +1385,37 @@ async function main() { } } + let tsupMigrated = false; + if (fullSetup) { + // Interactive only: stop any active spinner (e.g. "Migrating Prettier") so + // it does not animate beneath the confirm prompt. + if (options.interactive) { + clearMigrationProgress(); + } + const { migrateTsup, tsupConfigFile } = await collectTsupMigrationDecision( + workspaceInfoOptional.rootDir, + setupOptions, + workspaceInfoOptional.packages, + ); + if (migrateTsup) { + await ensureExistingPackageManager(); + updateMigrationProgress('Migrating tsup'); + const tsupOk = await migrateTsupToTsdown( + workspaceInfoOptional.rootDir, + options.interactive, + packageManager!, // is it safe to do this? + tsupConfigFile, + workspaceInfoOptional.packages, + { silent: true, report }, + ); + if (!tsupOk) { + clearMigrationProgress(); + cancelAndExit('tsup migration failed. Fix the issue and re-run `vp migrate`.', 1); + } + tsupMigrated = true; + } + } + // Check if node version manager file migration is needed (full setup only) if (fullSetup) { const nodeVersionDetection = detectNodeVersionManagerFile(workspaceInfoOptional.rootDir); @@ -1391,7 +1462,7 @@ async function main() { } // Merge configs and reinstall once if any tool or bootstrap migration happened - if (eslintMigrated || prettierMigrated) { + if (eslintMigrated || prettierMigrated || tsupMigrated) { updateMigrationProgress('Rewriting configs'); mergeViteConfigFiles( workspaceInfoOptional.rootDir, @@ -1399,10 +1470,17 @@ async function main() { report, workspaceInfoOptional.packages, ); + if (tsupMigrated) { + mergeTsdownConfigFile(workspaceInfoOptional.rootDir, true, report); + for (const pkg of workspaceInfoOptional.packages ?? []) { + mergeTsdownConfigFile(path.join(workspaceInfoOptional.rootDir, pkg.path), true, report); + } + } needsInstall = true; didMigrate = true; report.eslintMigrated = eslintMigrated; report.prettierMigrated = prettierMigrated; + report.tsupMigrated = tsupMigrated; } if (plan.shouldSetupHooks) { diff --git a/packages/cli/src/migration/detector.ts b/packages/cli/src/migration/detector.ts index fd4065cd86..aa8bcf4513 100644 --- a/packages/cli/src/migration/detector.ts +++ b/packages/cli/src/migration/detector.ts @@ -7,6 +7,7 @@ export interface ConfigFiles { viteConfig?: string; vitestConfig?: string; tsdownConfig?: string; + tsupConfig?: string; oxlintConfig?: string; oxfmtConfig?: string; eslintConfig?: string; @@ -20,6 +21,21 @@ export interface ConfigFiles { // Sentinel value indicating Prettier config lives inside package.json "prettier" key. export const PRETTIER_PACKAGE_JSON_CONFIG = 'package.json#prettier'; +// Sentinel value indicating tsup config lives inside package.json "tsup" key. +export const TSUP_PACKAGE_JSON_CONFIG = 'package.json#tsup'; + +// All known tsup config file names (standalone files only). +// https://tsup.egoist.dev/#using-a-config-file +export const TSUP_CONFIG_FILES = [ + 'tsup.config.ts', + 'tsup.config.mts', + 'tsup.config.cts', + 'tsup.config.js', + 'tsup.config.mjs', + 'tsup.config.cjs', + 'tsup.config.json', +] as const; + // All known Prettier config file names (standalone files only). // https://prettier.io/docs/configuration export const PRETTIER_CONFIG_FILES = [ @@ -90,6 +106,17 @@ export function detectConfigs(projectPath: string): ConfigFiles { } } + // Check for tsup.config.* (still detected even though tsdown supersedes it — + // `promptTsupMigration` / `detectTsupProject` use this to offer the tsup → + // tsdown migration). + // https://tsup.egoist.dev/#using-a-config-file + for (const config of TSUP_CONFIG_FILES) { + if (fs.existsSync(path.join(projectPath, config))) { + configs.tsupConfig = config; + break; + } + } + // Check for oxlint configs // https://oxc.rs/docs/guide/usage/linter/config.html#configuration-file-format const oxlintConfigs = ['.oxlintrc.json', '.oxlintrc.jsonc']; @@ -172,6 +199,10 @@ export function detectConfigs(projectPath: string): ConfigFiles { configs.prettierConfig = PRETTIER_PACKAGE_JSON_CONFIG; } + if (!configs.tsupConfig && pkg.tsup) { + configs.tsupConfig = TSUP_PACKAGE_JSON_CONFIG; + } + const voltaNode = pkg.volta?.node; if (typeof voltaNode === 'string') { configs.voltaNode = voltaNode; diff --git a/packages/cli/src/migration/migrator.ts b/packages/cli/src/migration/migrator.ts index fa4c23f13d..d5d5f122b7 100644 --- a/packages/cli/src/migration/migrator.ts +++ b/packages/cli/src/migration/migrator.ts @@ -1,6 +1,7 @@ export * from './migrator/shared.ts'; export * from './migrator/eslint.ts'; export * from './migrator/prettier.ts'; +export * from './migrator/tsup.ts'; export * from './migrator/tsconfig.ts'; export * from './migrator/framework-shim.ts'; export * from './migrator/vitest-ecosystem.ts'; diff --git a/packages/cli/src/migration/migrator/README.md b/packages/cli/src/migration/migrator/README.md index 9b4cd71638..0c3aad0389 100644 --- a/packages/cli/src/migration/migrator/README.md +++ b/packages/cli/src/migration/migrator/README.md @@ -31,6 +31,7 @@ Pick the file by what a function _does_, not by where it happens to be called. | `vite-config.ts` | `vite.config.ts` merging, default-config injection, staged-config merge, lazy-plugin wrapping, import rewriting (`rewriteAllImports`), migrated-oxlint-config sanitization, lint-staged removal. | | `eslint.ts` | ESLint → Oxlint migration, oxlint JS-plugin namespace handling, ESLint prompts/warnings. | | `prettier.ts` | Prettier → Oxfmt migration and its prompts/warnings. | +| `tsup.ts` | tsup → tsdown (`vp pack`) migration via `tsdown-migrate` and its prompts/warnings. | | `tsconfig.ts` | `tsconfig.json` cleanup and `types` rewriting. | | `framework-shim.ts` | Framework (Vue/Astro) shim detection and injection. | | `git-hooks.ts` | Vite+ hook defaults, project-owned hook preservation, and conservative detection of existing hook tools. | diff --git a/packages/cli/src/migration/migrator/tsup.ts b/packages/cli/src/migration/migrator/tsup.ts new file mode 100644 index 0000000000..c58ce11e09 --- /dev/null +++ b/packages/cli/src/migration/migrator/tsup.ts @@ -0,0 +1,272 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { styleText } from 'node:util'; + +import * as prompts from '@voidzero-dev/vite-plus-prompts'; + +import { PackageManager, type WorkspacePackage } from '../../types/index.ts'; +import { runCommandSilently } from '../../utils/command.ts'; +import { editJsonFile, readJsonFile } from '../../utils/json.ts'; +import { displayRelative } from '../../utils/path.ts'; +import { cancelAndExit } from '../../utils/prompts.ts'; +import { getSilentSpinner, getSpinner } from '../../utils/spinner.ts'; +import { detectConfigs, TSUP_CONFIG_FILES, TSUP_PACKAGE_JSON_CONFIG } from '../detector.ts'; +import { type MigrationReport } from '../report.ts'; + +export function detectTsupProject( + projectPath: string, + packages?: WorkspacePackage[], +): { + hasDependency: boolean; + configFile?: string; +} { + const packageJsonPath = path.join(projectPath, 'package.json'); + if (!fs.existsSync(packageJsonPath)) { + return { hasDependency: false }; + } + const pkg = readJsonFile(packageJsonPath) as { + devDependencies?: Record; + dependencies?: Record; + }; + let hasDependency = !!(pkg.devDependencies?.tsup || pkg.dependencies?.tsup); + const configs = detectConfigs(projectPath); + const configFile = configs.tsupConfig; + + // If root doesn't have tsup dependency, check workspace packages + if (!hasDependency && packages) { + for (const wp of packages) { + const pkgJsonPath = path.join(projectPath, wp.path, 'package.json'); + if (!fs.existsSync(pkgJsonPath)) { + continue; + } + const wpPkg = readJsonFile(pkgJsonPath) as { + devDependencies?: Record; + dependencies?: Record; + }; + if (wpPkg.devDependencies?.tsup || wpPkg.dependencies?.tsup) { + hasDependency = true; + break; + } + } + } + + return { hasDependency, configFile }; +} + +/** + * Run `vp dlx tsdown-migrate` in `cwd` with graceful error handling. + * Returns true on success, false on failure (spawn error or non-zero exit). + */ +async function runTsdownMigrateStep( + vpBin: string, + cwd: string, + spinner: ReturnType, + failMessage: string, + manualHint: string, + packageManager: PackageManager, +): Promise { + try { + const result = await runCommandSilently({ + command: vpBin, + args: ['dlx', 'tsdown-migrate@rc', '--yes', `--package-manager ${packageManager}`], // remove pin to rc tag once it graduates to main version + cwd, + envs: process.env, + }); + if (result.exitCode !== 0) { + spinner.stop(failMessage); + const stderr = result.stderr.toString().trim(); + if (stderr) { + prompts.log.warn(`⚠ ${stderr}`); + } + prompts.log.info(manualHint); + return false; + } + return true; + } catch { + spinner.stop(failMessage); + prompts.log.info(manualHint); + return false; + } +} + +export async function migrateTsupToTsdown( + projectPath: string, + interactive: boolean, + packageManager: PackageManager, + tsupConfigFile?: string, + packages?: WorkspacePackage[], + options?: { silent?: boolean; report?: MigrationReport }, +): Promise { + const vpBin = process.env.VP_CLI_BIN ?? 'vp'; + const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive); + + // A tsup config isn't necessarily workspace-wide the way an ESLint flat + // config usually is — a monorepo commonly builds each package + // independently with its own `tsup.config.*`. Run `tsdown-migrate` in + // every directory that has one (root and/or workspace packages) so each + // gets its own `tsdown.config.*`, which `mergeTsdownConfigFile` then picks + // up per-project — mirroring how `tsdown.config.*` itself is merged. + const targets = [projectPath, ...(packages ?? []).map((p) => path.join(projectPath, p.path))]; + const tsupTargets = targets.filter((target) => + target === projectPath ? !!tsupConfigFile : !!detectConfigs(target).tsupConfig, + ); + + if (tsupTargets.length > 0) { + spinner.start('Migrating tsup config to tsdown...'); + for (const target of tsupTargets) { + const migrateOk = await runTsdownMigrateStep( + vpBin, + target, + spinner, + 'tsup migration failed', + `You can run \`vp dlx tsdown-migrate\` manually later in ${displayRelative(target)}`, + packageManager, + ); + if (!migrateOk) { + return false; + } + } + spinner.stop('tsup config migrated to tsdown.config'); + } + + if (options?.report) { + options.report.tsupMigrated = true; + } + + // Cleanup runs uniformly across the root and every workspace package — + // delete tsup config files and remove the `tsup` dependency from + // package.json. Mirrors the eslint/prettier cleanup pass. + for (const target of targets) { + if (!fs.existsSync(path.join(target, 'package.json'))) { + continue; + } + deleteTsupConfigFiles(target, options?.report, options?.silent); + rewriteTsupPackageJson(path.join(target, 'package.json')); + } + + return true; +} + +function deleteTsupConfigFiles(basePath: string, report?: MigrationReport, silent = false): void { + const configs = detectConfigs(basePath); + if (configs.tsupConfig && configs.tsupConfig !== TSUP_PACKAGE_JSON_CONFIG) { + const configPath = path.join(basePath, configs.tsupConfig); + if (fs.existsSync(configPath)) { + fs.unlinkSync(configPath); + if (report) { + report.removedConfigCount++; + } + if (!silent) { + prompts.log.success(`✔ Removed ${displayRelative(configPath)}`); + } + } + } + // Also clean up any stale tsup config files that detectConfigs didn't pick + // (tsup only uses one config, but users may have leftover files). + for (const file of TSUP_CONFIG_FILES) { + if (file === configs.tsupConfig) { + continue; // already handled above + } + const configPath = path.join(basePath, file); + if (fs.existsSync(configPath)) { + fs.unlinkSync(configPath); + if (report) { + report.removedConfigCount++; + } + if (!silent) { + prompts.log.success(`✔ Removed ${displayRelative(configPath)}`); + } + } + } + // Remove "tsup" key from package.json if present — `tsdown-migrate` + // reads it as a config source but never deletes it. + editJsonFile<{ tsup?: unknown }>(path.join(basePath, 'package.json'), (pkg) => { + if (pkg.tsup) { + delete pkg.tsup; + return pkg; + } + return undefined; + }); +} + +function rewriteTsupPackageJson(packageJsonPath: string): void { + if (!fs.existsSync(packageJsonPath)) { + return; + } + editJsonFile<{ + devDependencies?: Record; + dependencies?: Record; + }>(packageJsonPath, (pkg) => { + let changed = false; + // Remove the tsup dependency itself. Scripts (`"build": "tsup"`) are + // already rewritten to `vp pack` generically by `rewriteScripts` (see + // `replace-tsup` in rules/vite-tools.yml), and `tsdown` is a managed + // vite-plus-bundled dependency (see `REMOVE_PACKAGES`), so neither needs + // handling here. + for (const field of ['devDependencies', 'dependencies'] as const) { + if (pkg[field]?.tsup) { + delete pkg[field].tsup; + changed = true; + } + } + return changed ? pkg : undefined; + }); +} + +export function warnPackageLevelTsup() { + prompts.log.warn( + 'tsup detected in workspace packages but no root config found. Package-level tsup must be migrated manually.', + ); +} + +export async function confirmTsupMigration(interactive: boolean): Promise { + if (interactive) { + const confirmed = await prompts.confirm({ + message: + 'Migrate tsup config to tsdown using tsdown-migrate?\n ' + + styleText( + 'gray', + "tsdown is Vite+'s built-in bundler (exposed via `vp pack`) — a mostly drop-in tsup replacement powered by Rolldown. tsdown-migrate converts your existing config automatically.", + ), + initialValue: true, + }); + if (prompts.isCancel(confirmed)) { + cancelAndExit(); + } + return confirmed; + } + prompts.log.info('tsup configuration detected. Auto-migrating to tsdown...'); + return true; +} + +export async function promptTsupMigration( + projectPath: string, + interactive: boolean, + packageManager: PackageManager, + packages?: WorkspacePackage[], +): Promise { + const tsupProject = detectTsupProject(projectPath, packages); + if (!tsupProject.hasDependency) { + return false; + } + if (!tsupProject.configFile) { + // Packages have tsup but no root config → warn and skip + warnPackageLevelTsup(); + return false; + } + const confirmed = await confirmTsupMigration(interactive); + if (!confirmed) { + return false; + } + const ok = await migrateTsupToTsdown( + projectPath, + interactive, + packageManager, + tsupProject.configFile, + packages, + ); + if (!ok) { + cancelAndExit('tsup migration failed.', 1); + } + return true; +} diff --git a/packages/cli/src/migration/report.ts b/packages/cli/src/migration/report.ts index 78c7ef2461..4de0bc25a2 100644 --- a/packages/cli/src/migration/report.ts +++ b/packages/cli/src/migration/report.ts @@ -23,6 +23,7 @@ export interface MigrationReport { rewrittenImportErrors: Array<{ path: string; message: string }>; eslintMigrated: boolean; prettierMigrated: boolean; + tsupMigrated: boolean; nodeVersionFileMigrated: boolean; gitHooksConfigured: boolean; frameworkShimAdded: boolean; @@ -45,6 +46,7 @@ export function createMigrationReport(): MigrationReport { preservedUpstreamVitestImportFileCount: 0, rewrittenImportErrors: [], eslintMigrated: false, + tsupMigrated: false, prettierMigrated: false, nodeVersionFileMigrated: false, gitHooksConfigured: false, diff --git a/packages/cli/src/migration/setup-plan.ts b/packages/cli/src/migration/setup-plan.ts index d6eb7a2645..d0b9d7ac1e 100644 --- a/packages/cli/src/migration/setup-plan.ts +++ b/packages/cli/src/migration/setup-plan.ts @@ -17,12 +17,15 @@ import { import { cancelAndExit, promptGitHooks } from '../utils/prompts.ts'; import { confirmEslintMigration, + confirmTsupMigration, detectEslintProject, detectIncompatibleEslintIntegration, + detectTsupProject, preflightGitHooksSetup, warnIncompatibleEslintIntegration, warnLegacyEslintConfig, warnPackageLevelEslint, + warnPackageLevelTsup, } from './migrator.ts'; import type { MigrationOptions } from './options.ts'; @@ -185,6 +188,25 @@ async function collectEslintMigrationDecision( return { migrateEslint, eslintConfigFile: eslintProject.configFile }; } +// Collected separately from collectMigrationSetupPlan so callers can prompt +// for it after the Prettier -> Oxfmt decision (tsup/tsdown is checked last +// among the tool migrations). +export async function collectTsupMigrationDecision( + rootDir: string, + options: MigrationOptions, + packages?: WorkspacePackage[], +): Promise<{ migrateTsup: boolean; tsupConfigFile?: string }> { + const tsupProject = detectTsupProject(rootDir, packages); + let migrateTsup = false; + if (tsupProject.hasDependency && tsupProject.configFile) { + migrateTsup = await confirmTsupMigration(options.interactive); + } else if (tsupProject.hasDependency) { + warnPackageLevelTsup(); + } + + return { migrateTsup, tsupConfigFile: tsupProject.configFile }; +} + export async function collectMigrationSetupPlan( rootDir: string, packageManager: PackageManager | undefined,