Visitar URL original
fix(migrate): support non-interactive environments · rolldown/tsdown@51e4183 · GitHub
Skip to content

Commit 51e4183

Browse files
committed
fix(migrate): support non-interactive environments
closes #1047
1 parent be6bfd2 commit 51e4183

8 files changed

Lines changed: 277 additions & 11 deletions

File tree

‎docs/guide/migrate-from-tsup.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ The `migrate` command supports the following options to customize the migration
4040

4141
- `[...dirs]`: Specify directories to migrate. Supports glob patterns (e.g., `packages/*`). Defaults to the current directory if not specified.
4242
- `--dry-run` (or `-d`): Perform a dry run to preview the migration without making any changes.
43+
- `--yes` (or `-y`): Skip the migration confirmation prompt. This is required in non-interactive environments.
44+
- `--package-manager <name>`: Override package manager auto-detection for dependency installation.
45+
- `--no-install`: Skip dependency installation, for example when another migration tool will install dependencies afterward.
46+
47+
For non-interactive environments, explicitly confirm the migration:
48+
49+
```bash
50+
npx tsdown-migrate --yes --no-install
51+
```
52+
53+
When installation is enabled, the migration tool detects the package manager from the project's `packageManager` field or lockfile. If detection fails in a non-interactive environment, pass `--package-manager <name>` or `--no-install`.
4354

4455
With these options, you can easily tailor the migration process to fit your specific project setup.
4556

‎docs/zh-CN/guide/migrate-from-tsup.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ npx tsdown-migrate packages/foo packages/bar
4040

4141
- `[...dirs]`:指定要迁移的目录。支持通配符模式(如 `packages/*`)。如果未指定,默认为当前目录。
4242
- `--dry-run`(或 `-d`):执行预览迁移(dry run),不会进行任何实际更改。
43+
- `--yes`(或 `-y`):跳过迁移确认提示。在非交互环境中必须使用此选项。
44+
- `--package-manager <name>`:覆盖依赖安装所使用的包管理器自动检测结果。
45+
- `--no-install`:跳过依赖安装,例如由其他迁移工具在后续统一安装依赖时使用。
46+
47+
在非交互环境中,请显式确认迁移:
48+
49+
```bash
50+
npx tsdown-migrate --yes --no-install
51+
```
52+
53+
启用依赖安装时,迁移工具会从项目的 `packageManager` 字段或锁文件中检测包管理器。如果在非交互环境中检测失败,请传入 `--package-manager <name>` 或 `--no-install`。
4354

4455
通过这些选项,您可以轻松调整迁移过程以适应您的特定项目结构。
4556

‎packages/migrate/README.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,29 @@ npx tsdown-migrate
1515

1616
- `[...dirs]` - Directories to migrate (defaults to current directory)
1717
- `-d, --dry-run` - Preview changes without applying them
18+
- `-y, --yes` - Skip the migration confirmation prompt
19+
- `--package-manager <name>` - Override package manager auto-detection
20+
- `--no-install` - Skip dependency installation
21+
22+
For non-interactive environments, explicitly confirm the migration. Use
23+
`--no-install` when the caller will install dependencies after running other
24+
migrations:
25+
26+
```bash
27+
npx tsdown-migrate --yes --no-install
28+
```
29+
30+
When installation is enabled, `tsdown-migrate` detects the package manager from
31+
the project's `packageManager` field or lockfile. If detection fails in a
32+
non-interactive environment, pass `--package-manager <name>` or `--no-install`.
1833

1934
### What It Does
2035

2136
- Renames `tsup` to `tsdown` in dependencies and scripts
2237
- Transforms tsup config files to tsdown config format
2338
- Handles property renames, option transformations, and import path updates
2439
- Warns about unsupported options that need manual attention
25-
- Runs package manager install after successful migration
40+
- Runs package manager install after successful migration unless `--no-install`
2641

2742
## Documentation
2843

‎packages/migrate/src/cli.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ const cli = cac(pkg.name).version(pkg.version).help()
1010
cli
1111
.command('[...dirs]', 'Migrate a project to tsdown')
1212
.option('-d, --dry-run', 'Perform a dry run without making changes')
13+
.option('-y, --yes', 'Skip the migration confirmation prompt')
14+
.option('--package-manager <name>', 'Package manager to use for installation')
15+
.option('--no-install', 'Skip dependency installation')
1316
.action((dirs: string[], options: Omit<MigrateOptions, 'dirs'>) =>
1417
migrate({ ...options, dirs }),
1518
)

‎packages/migrate/src/index.ts‎

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,89 @@
11
import process from 'node:process'
2-
import { getCliCommand, parseNi, run } from '@antfu/ni'
2+
import {
3+
AGENTS,
4+
detect,
5+
getCliCommand,
6+
parseNi,
7+
run,
8+
type ExtendedResolvedCommand,
9+
} from '@antfu/ni'
310
import consola from 'consola'
411
import { glob } from 'tinyglobby'
512
import { styleText } from '../../../src/utils/style.ts'
613
import { migratePackageJson } from './helpers/package-json.ts'
714
import { migrateTsupConfig } from './helpers/tsup-config.ts'
815

16+
export type PackageManager = (typeof AGENTS)[number]
17+
918
export interface MigrateOptions {
1019
dirs?: string[]
1120
dryRun?: boolean
21+
install?: boolean
22+
packageManager?: PackageManager
23+
yes?: boolean
24+
}
25+
26+
function isPackageManager(value: string): value is PackageManager {
27+
return AGENTS.includes(value as PackageManager)
28+
}
29+
30+
async function resolveInstallCommand(
31+
cwd: string,
32+
packageManager: PackageManager | undefined,
33+
interactive: boolean,
34+
): Promise<ExtendedResolvedCommand> {
35+
const agent = packageManager ?? (await detect({ cwd, programmatic: true }))
36+
37+
if (agent) {
38+
const command = await parseNi(agent, [], {
39+
cwd,
40+
hasLock: true,
41+
programmatic: true,
42+
})
43+
if (!command) {
44+
throw new Error(
45+
`Unable to resolve an install command for package manager "${agent}".`,
46+
)
47+
}
48+
return command
49+
}
50+
51+
if (interactive) {
52+
const command = await getCliCommand(parseNi, [], { cwd })
53+
if (command) return command
54+
throw new Error('Package manager selection cancelled.')
55+
}
56+
57+
throw new Error(
58+
'Unable to detect a package manager in a non-interactive environment. ' +
59+
'Add a packageManager field or lockfile, pass --package-manager <name>, or use --no-install.',
60+
)
1261
}
1362

14-
export async function migrate({ dirs, dryRun }: MigrateOptions): Promise<void> {
63+
export async function migrate({
64+
dirs,
65+
dryRun,
66+
install = true,
67+
packageManager,
68+
yes,
69+
}: MigrateOptions): Promise<void> {
70+
if (packageManager && !isPackageManager(packageManager)) {
71+
throw new Error(
72+
`Unsupported package manager "${packageManager}". Expected one of: ${AGENTS.join(', ')}.`,
73+
)
74+
}
75+
76+
const interactive = process.stdin.isTTY && process.stdout.isTTY
77+
1578
if (dryRun) {
1679
consola.info('Dry run enabled. No changes were made.')
17-
} else {
80+
} else if (!yes) {
81+
if (!interactive) {
82+
throw new Error(
83+
'Non-interactive migration requires explicit confirmation. Re-run with --yes.',
84+
)
85+
}
86+
1887
const confirm = await consola.prompt(
1988
`Before proceeding, review the migration guide at ${styleText.underline(`https://tsdown.dev/guide/migrate-from-tsup`)}, as this process will modify your files.\n` +
2089
`Uncommitted changes will be lost. Use the ${styleText.green(`--dry-run`)} flag to preview changes without applying them.\n\n` +
@@ -46,6 +115,9 @@ export async function migrate({ dirs, dryRun }: MigrateOptions): Promise<void> {
46115
cwds = [baseCwd]
47116
}
48117

118+
const installCommand = install
119+
? await resolveInstallCommand(baseCwd, packageManager, interactive)
120+
: undefined
49121
let migratedAny = false
50122

51123
try {
@@ -77,13 +149,21 @@ export async function migrate({ dirs, dryRun }: MigrateOptions): Promise<void> {
77149
return
78150
}
79151

80-
consola.info('Migration completed. Installing dependencies...')
152+
if (installCommand) {
153+
consola.info('Migration completed. Installing dependencies...')
81154

82-
if (dryRun) {
83-
consola.info('[dry-run] would run:', await getCliCommand(parseNi, []))
155+
if (dryRun) {
156+
consola.info('[dry-run] would run:', installCommand)
157+
} else {
158+
await run(
159+
() => ({ ...installCommand, args: [...installCommand.args] }),
160+
[],
161+
{ cwd: baseCwd, programmatic: true },
162+
)
163+
consola.success('Dependencies installed.')
164+
}
84165
} else {
85-
await run(parseNi, [], { cwd: baseCwd })
86-
consola.success('Dependencies installed.')
166+
consola.info('Migration completed. Dependency installation skipped.')
87167
}
88168

89169
consola.box(

‎packages/migrate/tests/cli.test.ts‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { spawnSync } from 'node:child_process'
2+
import { access, cp, mkdtemp, readFile, rm } from 'node:fs/promises'
3+
import { tmpdir } from 'node:os'
4+
import path from 'node:path'
5+
import process from 'node:process'
6+
import { afterEach, describe, expect, test } from 'vitest'
7+
8+
const cliPath = path.resolve(import.meta.dirname, '../src/run.ts')
9+
const fixturesPath = path.resolve(import.meta.dirname, 'fixtures/monorepo')
10+
const temporaryDirectories: string[] = []
11+
12+
async function createFixture(relativePath: string): Promise<string> {
13+
const directory = await mkdtemp(path.join(tmpdir(), 'tsdown-migrate-'))
14+
temporaryDirectories.push(directory)
15+
await cp(path.join(fixturesPath, relativePath), directory, {
16+
recursive: true,
17+
})
18+
return directory
19+
}
20+
21+
function runMigration(cwd: string, args: string[]) {
22+
return spawnSync(process.execPath, [cliPath, ...args], {
23+
cwd,
24+
encoding: 'utf8',
25+
env: { ...process.env, NO_COLOR: '1' },
26+
stdio: ['ignore', 'pipe', 'pipe'],
27+
timeout: 5_000,
28+
})
29+
}
30+
31+
afterEach(async () => {
32+
await Promise.all(
33+
temporaryDirectories
34+
.splice(0)
35+
.map((directory) => rm(directory, { force: true, recursive: true })),
36+
)
37+
})
38+
39+
describe('non-interactive CLI', () => {
40+
test('requires explicit confirmation before changing files', async () => {
41+
const directory = await createFixture('packages/pkg1')
42+
const packageJsonPath = path.join(directory, 'package.json')
43+
const packageJson = await readFile(packageJsonPath, 'utf8')
44+
45+
const result = runMigration(directory, [])
46+
47+
expect(result.status).toBe(1)
48+
expect(result.stderr).toContain(
49+
'Non-interactive migration requires explicit confirmation. Re-run with --yes.',
50+
)
51+
expect(result.stderr).not.toContain('TTY initialization failed')
52+
expect(await readFile(packageJsonPath, 'utf8')).toBe(packageJson)
53+
await expect(
54+
access(path.join(directory, 'tsdown.config.ts')),
55+
).rejects.toThrow()
56+
})
57+
58+
test('migrates without prompts when installation is skipped', async () => {
59+
const directory = await createFixture('packages/pkg1')
60+
61+
const result = runMigration(directory, ['--yes', '--no-install'])
62+
63+
expect(result.status).toBe(0)
64+
65+
const packageJson = JSON.parse(
66+
await readFile(path.join(directory, 'package.json'), 'utf8'),
67+
)
68+
expect(packageJson.devDependencies).toHaveProperty('tsdown')
69+
expect(packageJson.devDependencies).not.toHaveProperty('tsup')
70+
await expect(
71+
access(path.join(directory, 'tsdown.config.ts')),
72+
).resolves.toBeUndefined()
73+
await expect(
74+
access(path.join(directory, 'tsup.config.ts')),
75+
).rejects.toThrow()
76+
})
77+
78+
test('fails before changing files when no package manager is detected', async () => {
79+
const directory = await createFixture('packages/pkg1')
80+
const packageJsonPath = path.join(directory, 'package.json')
81+
const packageJson = await readFile(packageJsonPath, 'utf8')
82+
83+
const result = runMigration(directory, ['--yes'])
84+
85+
expect(result.status).toBe(1)
86+
expect(result.stderr).toContain(
87+
'Unable to detect a package manager in a non-interactive environment.',
88+
)
89+
expect(await readFile(packageJsonPath, 'utf8')).toBe(packageJson)
90+
await expect(
91+
access(path.join(directory, 'tsdown.config.ts')),
92+
).rejects.toThrow()
93+
})
94+
95+
test('uses an explicitly selected package manager', async () => {
96+
const directory = await createFixture('packages/pkg1')
97+
98+
const result = runMigration(directory, [
99+
'--dry-run',
100+
'--package-manager',
101+
'pnpm',
102+
])
103+
const output = `${result.stdout}${result.stderr}`
104+
105+
expect(result.status).toBe(0)
106+
expect(output).not.toContain('Choose the agent')
107+
expect(output).not.toContain('TTY initialization failed')
108+
})
109+
110+
test('auto-detects the package manager', async () => {
111+
const directory = await createFixture('apps/pkg3')
112+
113+
const result = runMigration(directory, ['--dry-run'])
114+
const output = `${result.stdout}${result.stderr}`
115+
116+
expect(result.status).toBe(0)
117+
expect(output).not.toContain('Choose the agent')
118+
expect(output).not.toContain('TTY initialization failed')
119+
})
120+
121+
test('rejects an unsupported package manager', async () => {
122+
const directory = await createFixture('packages/pkg1')
123+
124+
const result = runMigration(directory, [
125+
'--dry-run',
126+
'--package-manager',
127+
'invalid',
128+
])
129+
130+
expect(result.status).toBe(1)
131+
expect(result.stderr).toContain('Unsupported package manager "invalid".')
132+
})
133+
})

‎skills/tsdown/references/guide-migrate-from-tsup.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ npx tsdown-migrate packages/foo packages/bar
2828

2929
- `[...dirs]` - Directories to migrate (supports globs)
3030
- `--dry-run` or `-d` - Preview changes without modifying files
31+
- `--yes` or `-y` - Skip confirmation (required in non-interactive environments)
32+
- `--package-manager <name>` - Override package manager auto-detection
33+
- `--no-install` - Skip dependency installation
34+
35+
For non-interactive callers that install dependencies separately:
36+
37+
```bash
38+
npx tsdown-migrate --yes --no-install
39+
```
40+
41+
When installation is enabled, the tool detects the package manager from the
42+
project's `packageManager` field or lockfile. If detection fails without a TTY,
43+
pass `--package-manager <name>` or `--no-install`.
3144

3245
**Important:** Commit your changes before running migration.
3346

‎src/utils/style.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ const factory = (...formats: InspectColor[]) =>
3030
}
3131
return factory(...formats, format)
3232
},
33-
})
33+
}) as StyleText
3434

3535
type Formats = InspectColor | `#${string}`
3636
export type StyleText = {
3737
(text: string, options?: StyleTextOptions): string
3838
} & { [key in Formats]: StyleText }
3939

40-
export const styleText = factory() as StyleText
40+
export const styleText: StyleText = factory()

0 commit comments

Comments
 (0)