Skip to content

Commit 8849dca

Browse files
committed
fix(migrate): reject unsupported tsup config inputs
1 parent b58805b commit 8849dca

5 files changed

Lines changed: 170 additions & 14 deletions

File tree

crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsup/snapshots.toml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,27 @@ steps = [
4646
{ argv = ["vpt", "print-file", "package.json"], comment = "both inline configs are unchanged" },
4747
]
4848

49+
[[case]]
50+
name = "migration_from_tsup_inline_config"
51+
vp = "local"
52+
steps = [
53+
{ argv = ["vpt", "rm", "tsup.config.ts"], snapshot = false },
54+
{ argv = ["vpt", "json-edit", "package.json", "scripts.build", "tsup"], snapshot = false },
55+
{ argv = ["vpt", "json-edit", "package.json", "tsup", '{"entry":["src/index.ts"]}'], snapshot = false },
56+
{ argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "an inline tsup config should stop automatic migration", continue-on-failure = true },
57+
{ argv = ["vpt", "print-file", "package.json"], comment = "the inline tsup config is unchanged" },
58+
]
59+
4960
[[case]]
5061
name = "migration_from_tsup_custom_config"
5162
vp = "local"
5263
steps = [
5364
{ argv = ["vpt", "mkdir", "-p", "configs"], snapshot = false },
5465
{ argv = ["vpt", "write-file", "configs/legacy.ts", "export default {};\n"], snapshot = false },
5566
{ argv = ["vpt", "json-edit", "package.json", "scripts.build", "tsup --config configs/legacy.ts"], snapshot = false },
56-
{ argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "a custom config should stop automatic migration before any files change", continue-on-failure = true },
67+
{ argv = ["vpt", "json-edit", "package.json", "scripts.irregular", "tsup --config ././tsup.config.ts"], snapshot = false },
68+
{ argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "unsupported config paths should stop automatic migration before any files change", continue-on-failure = true },
5769
{ argv = ["vpt", "print-file", "tsup.config.ts"], comment = "the standard config is unchanged" },
5870
{ argv = ["vpt", "print-file", "configs/legacy.ts"], comment = "the custom config is unchanged" },
59-
{ argv = ["vpt", "print-file", "package.json"], comment = "the custom-config script and tsup dependency are unchanged" },
71+
{ argv = ["vpt", "print-file", "package.json"], comment = "the unsupported scripts and tsup dependency are unchanged" },
6072
]

crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsup/snapshots/migration_from_tsup_custom_config.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
## `vpt json-edit package.json scripts.build 'tsup --config configs/legacy.ts'`
1111

1212

13+
## `vpt json-edit package.json scripts.irregular 'tsup --config ././tsup.config.ts'`
14+
15+
1316
## `vp migrate --no-interactive --no-hooks --no-agent --no-editor`
1417

15-
a custom config should stop automatic migration before any files change
18+
unsupported config paths should stop automatic migration before any files change
1619

1720
**Exit code:** 1
1821

@@ -23,6 +26,7 @@ tsup configuration detected. Auto-migrating to tsdown...
2326
2427
Automatic tsup migration was skipped because these scripts use configs that cannot be migrated automatically:
2528
package.json#build -> configs/legacy.ts
29+
package.json#irregular -> ././tsup.config.ts
2630
2731
Choose one of these manual migration methods:
2832
1. Run `vp dlx tsdown-migrate` in the project root.
@@ -56,7 +60,7 @@ export default {};
5660

5761
## `vpt print-file package.json`
5862

59-
the custom-config script and tsup dependency are unchanged
63+
the unsupported scripts and tsup dependency are unchanged
6064

6165
```
6266
{
@@ -66,7 +70,8 @@ the custom-config script and tsup dependency are unchanged
6670
},
6771
"name": "migration-from-tsup",
6872
"scripts": {
69-
"build": "tsup --config configs/legacy.ts"
73+
"build": "tsup --config configs/legacy.ts",
74+
"irregular": "tsup --config ././tsup.config.ts"
7075
}
7176
}
7277
```
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# migration_from_tsup_inline_config
2+
3+
## `vpt rm tsup.config.ts`
4+
5+
6+
## `vpt json-edit package.json scripts.build tsup`
7+
8+
9+
## `vpt json-edit package.json tsup '{"entry":["src/index.ts"]}'`
10+
11+
12+
## `vp migrate --no-interactive --no-hooks --no-agent --no-editor`
13+
14+
an inline tsup config should stop automatic migration
15+
16+
**Exit code:** 1
17+
18+
```
19+
VITE+ - The Unified Toolchain for the Web
20+
21+
tsup configuration detected. Auto-migrating to tsdown...
22+
23+
Automatic tsup migration was skipped because these inline tsup configs cannot be migrated automatically:
24+
package.json#tsup
25+
26+
Choose one of these manual migration methods:
27+
1. Run `vp dlx tsdown-migrate` in the project root.
28+
2. Use the tsdown migration skill:
29+
https://github.com/rolldown/tsdown/blob/main/skills/tsdown-migrate/SKILL.md
30+
Complete the tsup migration manually, then re-run `vp migrate`.
31+
```
32+
33+
## `vpt print-file package.json`
34+
35+
the inline tsup config is unchanged
36+
37+
```
38+
{
39+
"devDependencies": {
40+
"tsup": "^8.5.0",
41+
"vite": "^7.0.0"
42+
},
43+
"name": "migration-from-tsup",
44+
"scripts": {
45+
"build": "tsup"
46+
},
47+
"tsup": {
48+
"entry": [
49+
"src/index.ts"
50+
]
51+
}
52+
}
53+
```

packages/cli/src/migration/__tests__/tsup.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,31 @@ describe('tsup migration', () => {
143143
expect(mockInfo).toHaveBeenCalledWith(manualMigrationOptions());
144144
});
145145

146+
it('refuses to migrate an inline tsup config', async () => {
147+
fs.unlinkSync(path.join(projectPath, 'tsup.config.ts'));
148+
const packageJsonPath = path.join(projectPath, 'package.json');
149+
const originalPackageJson = {
150+
name: 'fixture',
151+
scripts: { build: 'tsup' },
152+
devDependencies: { tsup: '^8.5.0' },
153+
tsup: { entry: ['src/index.ts'] },
154+
};
155+
fs.writeFileSync(packageJsonPath, `${JSON.stringify(originalPackageJson, null, 2)}\n`);
156+
157+
await expect(
158+
migrateTsupToTsdown(projectPath, false, PackageManager.npm, 'package.json#tsup', undefined, {
159+
silent: true,
160+
}),
161+
).resolves.toBe(false);
162+
163+
expect(mockRunCommandSilently).not.toHaveBeenCalled();
164+
expect(readJsonFile(packageJsonPath)).toEqual(originalPackageJson);
165+
expect(mockWarn).toHaveBeenCalledWith(
166+
'Automatic tsup migration was skipped because these inline tsup configs cannot be migrated automatically:\n package.json#tsup',
167+
);
168+
expect(mockInfo).toHaveBeenCalledWith(manualMigrationOptions());
169+
});
170+
146171
it('refuses to migrate a script that uses a custom tsup config', async () => {
147172
const packageJsonPath = path.join(projectPath, 'package.json');
148173
const originalPackageJson = {
@@ -169,6 +194,30 @@ describe('tsup migration', () => {
169194
expect(mockInfo).toHaveBeenCalledWith(manualMigrationOptions());
170195
});
171196

197+
it('refuses a default config path that cleanup cannot remove', async () => {
198+
const packageJsonPath = path.join(projectPath, 'package.json');
199+
const originalPackageJson = {
200+
name: 'fixture',
201+
scripts: { build: 'tsup --config ././tsup.config.ts' },
202+
devDependencies: { tsup: '^8.5.0' },
203+
};
204+
fs.writeFileSync(packageJsonPath, `${JSON.stringify(originalPackageJson, null, 2)}\n`);
205+
206+
await expect(
207+
migrateTsupToTsdown(projectPath, false, PackageManager.npm, 'tsup.config.ts', undefined, {
208+
silent: true,
209+
}),
210+
).resolves.toBe(false);
211+
212+
expect(mockRunCommandSilently).not.toHaveBeenCalled();
213+
expect(readJsonFile(packageJsonPath)).toEqual(originalPackageJson);
214+
expect(fs.existsSync(path.join(projectPath, 'tsup.config.ts'))).toBe(true);
215+
expect(mockWarn).toHaveBeenCalledWith(
216+
'Automatic tsup migration was skipped because these scripts use configs that cannot be migrated automatically:\n package.json#build -> ././tsup.config.ts',
217+
);
218+
expect(mockInfo).toHaveBeenCalledWith(manualMigrationOptions());
219+
});
220+
172221
it('refuses to remove selectors for multiple standard tsup configs', async () => {
173222
fs.writeFileSync(path.join(projectPath, 'tsup.config.js'), 'export default {};\n');
174223
const packageJsonPath = path.join(projectPath, 'package.json');

packages/cli/src/migration/migrator/tsup.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,13 @@ interface SharedTsupConfig {
106106
consumers: Map<string, SharedTsupConsumer>;
107107
}
108108

109-
interface TsdownConfigCollision {
109+
interface TsupConfigLocation {
110110
configPath: string;
111111
targetPath: string;
112112
}
113113

114114
interface UnsupportedTsupConfigConsumer {
115-
configPath: string;
115+
configArgument: string;
116116
packagePath: string;
117117
scriptName: string;
118118
}
@@ -175,8 +175,8 @@ function collectSharedTsupConfigs(
175175
return sharedConfigs;
176176
}
177177

178-
function collectTsupConfigCollisions(tsupTargets: string[]): TsdownConfigCollision[] {
179-
const collisions: TsdownConfigCollision[] = [];
178+
function collectTsupConfigCollisions(tsupTargets: string[]): TsupConfigLocation[] {
179+
const collisions: TsupConfigLocation[] = [];
180180
for (const target of tsupTargets) {
181181
const tsdownConfig = detectConfigs(target).tsdownConfig;
182182
if (tsdownConfig) {
@@ -193,6 +193,25 @@ function collectTsupConfigCollisions(tsupTargets: string[]): TsdownConfigCollisi
193193
return collisions;
194194
}
195195

196+
function collectInlineTsupConfigs(tsupTargets: string[]): TsupConfigLocation[] {
197+
return tsupTargets.flatMap((target) => {
198+
const packageJsonPath = path.join(target, 'package.json');
199+
if (!fs.existsSync(packageJsonPath) || !Object.hasOwn(readJsonFile(packageJsonPath), 'tsup')) {
200+
return [];
201+
}
202+
return [{ configPath: `${packageJsonPath}#tsup`, targetPath: target }];
203+
});
204+
}
205+
206+
function isRemovableDefaultConfigArgument(configArgument: string, defaultConfig?: string): boolean {
207+
return (
208+
!!defaultConfig &&
209+
(configArgument === defaultConfig ||
210+
configArgument === `./${defaultConfig}` ||
211+
configArgument === `.\\${defaultConfig}`)
212+
);
213+
}
214+
196215
function collectUnsupportedTsupConfigConsumers(
197216
tsupTargets: string[],
198217
): UnsupportedTsupConfigConsumer[] {
@@ -219,12 +238,15 @@ function collectUnsupportedTsupConfigConsumers(
219238

220239
for (const [scriptName, script] of Object.entries(packageJson.scripts ?? {})) {
221240
for (const match of script.matchAll(TSUP_CONFIG_OPTION_RE)) {
222-
const configPath = resolveScriptConfigPath(target, match[1] ?? match[2] ?? match[3]);
223-
const isDefaultConfig = configPath === defaultConfigPath;
241+
const configArgument = match[1] ?? match[2] ?? match[3];
242+
const configPath = resolveScriptConfigPath(target, configArgument);
243+
const isDefaultConfig =
244+
configPath === defaultConfigPath &&
245+
isRemovableDefaultConfigArgument(configArgument, detectedConfig);
224246
const isSharedMigratedConfig =
225247
migratedConfigPaths.has(configPath) && path.dirname(configPath) !== target;
226248
if (!isDefaultConfig && !isSharedMigratedConfig) {
227-
unsupported.push({ configPath, packagePath: target, scriptName });
249+
unsupported.push({ configArgument, packagePath: target, scriptName });
228250
}
229251
}
230252
}
@@ -413,13 +435,28 @@ export async function migrateTsupToTsdown(
413435
showTsdownMigrationOptions(targetLabel);
414436
return false;
415437
}
438+
const inlineTsupConfigs = collectInlineTsupConfigs(tsupTargets);
439+
if (inlineTsupConfigs.length > 0) {
440+
prompts.log.warn(
441+
`Automatic tsup migration was skipped because these inline tsup configs cannot be migrated automatically:\n${inlineTsupConfigs
442+
.map(({ configPath }) => ` ${displayRelative(configPath, projectPath)}`)
443+
.join('\n')}`,
444+
);
445+
const firstInlineTarget = inlineTsupConfigs[0].targetPath;
446+
const targetLabel =
447+
firstInlineTarget === rootPath
448+
? 'the project root'
449+
: displayRelative(firstInlineTarget, projectPath);
450+
showTsdownMigrationOptions(targetLabel);
451+
return false;
452+
}
416453
const unsupportedConfigConsumers = collectUnsupportedTsupConfigConsumers(tsupTargets);
417454
if (unsupportedConfigConsumers.length > 0) {
418455
prompts.log.warn(
419456
`Automatic tsup migration was skipped because these scripts use configs that cannot be migrated automatically:\n${unsupportedConfigConsumers
420457
.map(
421-
({ configPath, packagePath, scriptName }) =>
422-
` ${displayRelative(path.join(packagePath, 'package.json'), projectPath)}#${scriptName} -> ${displayRelative(configPath, projectPath)}`,
458+
({ configArgument, packagePath, scriptName }) =>
459+
` ${displayRelative(path.join(packagePath, 'package.json'), projectPath)}#${scriptName} -> ${configArgument}`,
423460
)
424461
.join('\n')}`,
425462
);

0 commit comments

Comments
 (0)