Skip to content

Commit e3bd9db

Browse files
authored
fix(migrate): finish leftover Oxc config migration (#2654)
When an interrupted migration has already added `vite-plus`, running `vp migrate` again can leave `.oxfmtrc.json` behind and report that the project already uses Vite+. Formatting then uses defaults instead of the unmerged options. The existing-project finalization path skips standalone Oxc configs. Run the existing config merger for the root and workspace packages, and count completed merges as migration work. This covers Oxlint and Oxfmt JSON/JSONC configs, preserves existing inline config precedence, and retains configs that cannot be merged with a warning. A subsequent migration remains a no-op. The branch incorporates upstream main at `b1c41b2a`, including the leftover tsdown config fix from #2646. Both finalization paths and result flags are preserved. A regression test covers simultaneous Oxfmt and tsdown config migration and an unchanged retry.
1 parent 12e5eaf commit e3bd9db

10 files changed

Lines changed: 377 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"singleQuote": true,
3+
"semi": false
4+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "migration-existing-oxc-configs",
3+
"private": true,
4+
"type": "module",
5+
"packageManager": "pnpm@12.3.4",
6+
"devDependencies": {
7+
"vite-plus": "latest"
8+
}
9+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[[case]]
2+
name = "migration_existing_oxc_configs"
3+
vp = "global"
4+
steps = [
5+
{ argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "finish a leftover Oxfmt config even when Vite+ is already installed" },
6+
["vpt", "print-file", "vite.config.ts"],
7+
["vpt", "stat-file", ".oxfmtrc.json", "--assert-not", "file"],
8+
{ argv = ["vp", "fmt", "src/index.ts"], comment = "the migrated options must affect formatting" },
9+
["vpt", "print-file", "src/index.ts"],
10+
{ argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "a completed migration should be a no-op on retry" },
11+
["vpt", "print-file", "vite.config.ts"],
12+
["vpt", "stat-file", "AGENTS.md", "--assert-not", "file"],
13+
["vpt", "stat-file", ".vite-hooks", "--assert-not", "dir"],
14+
["vpt", "stat-file", ".vscode", "--assert-not", "dir"],
15+
]
16+
17+
[[case]]
18+
name = "migration_existing_oxc_configs_inline_fmt"
19+
vp = "global"
20+
steps = [
21+
{ argv = ["vpt", "write-file", "vite.config.ts", "export default { fmt: { singleQuote: false, semi: false } };\n"], snapshot = false },
22+
{ argv = ["vp", "fmt", "src/index.ts"], comment = "the existing inline fmt config takes precedence over the standalone config" },
23+
["vpt", "print-file", "src/index.ts"],
24+
{ argv = ["vpt", "write-file", "src/index.ts", "export const message = \"preserved\";\n"], snapshot = false },
25+
{ argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "remove the leftover standalone config without changing the existing fmt config" },
26+
["vpt", "print-file", "vite.config.ts"],
27+
["vpt", "stat-file", ".oxfmtrc.json", "--assert-not", "file"],
28+
{ argv = ["vp", "fmt", "src/index.ts"], comment = "formatting must still use the existing inline options after migration" },
29+
["vpt", "print-file", "src/index.ts"],
30+
{ argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "retrying the completed migration should be a no-op" },
31+
["vpt", "print-file", "vite.config.ts"],
32+
]
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# migration_existing_oxc_configs
2+
3+
## `vp migrate --no-interactive --no-hooks --no-agent --no-editor`
4+
5+
finish a leftover Oxfmt config even when Vite+ is already installed
6+
7+
```
8+
VITE+ - The Unified Toolchain for the Web
9+
10+
◇ Updated . to Vite+ <version>
11+
• Node <version> pnpm <version>
12+
• Dependencies:
13+
vite-plus latest → <version>
14+
vite → <version>
15+
• 1 config update applied
16+
• Package manager settings configured
17+
```
18+
19+
## `vpt print-file vite.config.ts`
20+
21+
```
22+
export default {
23+
fmt: {
24+
"singleQuote": true,
25+
"semi": false
26+
},
27+
28+
}
29+
```
30+
31+
## `vpt stat-file .oxfmtrc.json --assert-not file`
32+
33+
```
34+
.oxfmtrc.json: missing
35+
```
36+
37+
## `vp fmt src/index.ts`
38+
39+
the migrated options must affect formatting
40+
41+
```
42+
VITE+ - The Unified Toolchain for the Web
43+
44+
Finished in <duration> on 1 files using <n> threads.
45+
```
46+
47+
## `vpt print-file src/index.ts`
48+
49+
```
50+
export const message = 'preserved'
51+
```
52+
53+
## `vp migrate --no-interactive --no-hooks --no-agent --no-editor`
54+
55+
a completed migration should be a no-op on retry
56+
57+
```
58+
VITE+ - The Unified Toolchain for the Web
59+
60+
This project is already using Vite+! Happy coding!
61+
```
62+
63+
## `vpt print-file vite.config.ts`
64+
65+
```
66+
export default {
67+
fmt: {
68+
"singleQuote": true,
69+
"semi": false
70+
},
71+
72+
}
73+
```
74+
75+
## `vpt stat-file AGENTS.md --assert-not file`
76+
77+
```
78+
AGENTS.md: missing
79+
```
80+
81+
## `vpt stat-file .vite-hooks --assert-not dir`
82+
83+
```
84+
.vite-hooks: missing
85+
```
86+
87+
## `vpt stat-file .vscode --assert-not dir`
88+
89+
```
90+
.vscode: missing
91+
```
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# migration_existing_oxc_configs_inline_fmt
2+
3+
## `vpt write-file vite.config.ts 'export default { fmt: { singleQuote: false, semi: false } };
4+
'`
5+
6+
7+
## `vp fmt src/index.ts`
8+
9+
the existing inline fmt config takes precedence over the standalone config
10+
11+
```
12+
VITE+ - The Unified Toolchain for the Web
13+
14+
Finished in <duration> on 1 files using <n> threads.
15+
```
16+
17+
## `vpt print-file src/index.ts`
18+
19+
```
20+
export const message = "preserved"
21+
```
22+
23+
## `vpt write-file src/index.ts 'export const message = "preserved";
24+
'`
25+
26+
27+
## `vp migrate --no-interactive --no-hooks --no-agent --no-editor`
28+
29+
remove the leftover standalone config without changing the existing fmt config
30+
31+
```
32+
VITE+ - The Unified Toolchain for the Web
33+
34+
◇ Updated . to Vite+ <version>
35+
• Node <version> pnpm <version>
36+
• Dependencies:
37+
vite-plus latest → <version>
38+
vite → <version>
39+
• Package manager settings configured
40+
```
41+
42+
## `vpt print-file vite.config.ts`
43+
44+
```
45+
export default { fmt: { singleQuote: false, semi: false } };
46+
```
47+
48+
## `vpt stat-file .oxfmtrc.json --assert-not file`
49+
50+
```
51+
.oxfmtrc.json: missing
52+
```
53+
54+
## `vp fmt src/index.ts`
55+
56+
formatting must still use the existing inline options after migration
57+
58+
```
59+
VITE+ - The Unified Toolchain for the Web
60+
61+
Finished in <duration> on 1 files using <n> threads.
62+
```
63+
64+
## `vpt print-file src/index.ts`
65+
66+
```
67+
export const message = "preserved"
68+
```
69+
70+
## `vp migrate --no-interactive --no-hooks --no-agent --no-editor`
71+
72+
retrying the completed migration should be a no-op
73+
74+
```
75+
VITE+ - The Unified Toolchain for the Web
76+
77+
This project is already using Vite+! Happy coding!
78+
```
79+
80+
## `vpt print-file vite.config.ts`
81+
82+
```
83+
export default { fmt: { singleQuote: false, semi: false } };
84+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const message = "preserved";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {};

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

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8796,6 +8796,7 @@ describe('existing Vite+ core migration finalization', () => {
87968796
scripts: true,
87978797
tsconfigTypes: true,
87988798
imports: true,
8799+
oxcConfigs: false,
87998800
tsdownConfig: false,
88008801
});
88018802

@@ -8827,6 +8828,96 @@ describe('existing Vite+ core migration finalization', () => {
88278828
});
88288829
});
88298830

8831+
it.each(['.oxfmtrc.json', '.oxfmtrc.jsonc'])(
8832+
'finishes a leftover %s config and remains idempotent',
8833+
(configFile) => {
8834+
fs.writeFileSync(
8835+
path.join(tmpDir, 'package.json'),
8836+
JSON.stringify({ name: 'test', devDependencies: { 'vite-plus': 'latest' } }),
8837+
);
8838+
fs.writeFileSync(path.join(tmpDir, 'vite.config.ts'), 'export default {};\n');
8839+
fs.writeFileSync(path.join(tmpDir, configFile), '{"singleQuote":true,"semi":false}\n');
8840+
const workspaceInfo = makeWorkspaceInfo(tmpDir, PackageManager.npm);
8841+
const report = createMigrationReport();
8842+
8843+
expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true, report).oxcConfigs).toBe(
8844+
true,
8845+
);
8846+
const config = fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8');
8847+
expect(config).toContain('fmt:');
8848+
expect(config).toContain('"singleQuote":true');
8849+
expect(config).toContain('"semi":false');
8850+
expect(fs.existsSync(path.join(tmpDir, configFile))).toBe(false);
8851+
expect(report.mergedConfigCount).toBe(1);
8852+
expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true).oxcConfigs).toBe(false);
8853+
expect(fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8')).toBe(config);
8854+
},
8855+
);
8856+
8857+
it('finishes leftover lint and format configs in workspace packages', () => {
8858+
const appDir = path.join(tmpDir, 'packages', 'app');
8859+
fs.mkdirSync(appDir, { recursive: true });
8860+
fs.writeFileSync(
8861+
path.join(tmpDir, 'package.json'),
8862+
JSON.stringify({ name: 'root', devDependencies: { 'vite-plus': 'latest' } }),
8863+
);
8864+
fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({ name: 'app' }));
8865+
fs.writeFileSync(path.join(appDir, '.oxfmtrc.json'), '{"singleQuote":true}\n');
8866+
fs.writeFileSync(path.join(appDir, '.oxlintrc.json'), '{"rules":{"no-console":"error"}}\n');
8867+
const workspaceInfo = {
8868+
...makeWorkspaceInfo(tmpDir, PackageManager.pnpm),
8869+
isMonorepo: true,
8870+
packages: [{ name: 'app', path: 'packages/app' }],
8871+
};
8872+
8873+
expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true).oxcConfigs).toBe(true);
8874+
const config = fs.readFileSync(path.join(appDir, 'vite.config.ts'), 'utf8');
8875+
expect(config).toContain('fmt:');
8876+
expect(config).toContain('lint:');
8877+
expect(config).toMatch(/"no-console":\s*"error"/);
8878+
expect(fs.existsSync(path.join(appDir, '.oxfmtrc.json'))).toBe(false);
8879+
expect(fs.existsSync(path.join(appDir, '.oxlintrc.json'))).toBe(false);
8880+
expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true).oxcConfigs).toBe(false);
8881+
});
8882+
8883+
it('preserves existing inline config when removing a redundant standalone config', () => {
8884+
fs.writeFileSync(
8885+
path.join(tmpDir, 'package.json'),
8886+
JSON.stringify({ name: 'test', devDependencies: { 'vite-plus': 'latest' } }),
8887+
);
8888+
const config = 'export default { fmt: { singleQuote: false } };\n';
8889+
fs.writeFileSync(path.join(tmpDir, 'vite.config.ts'), config);
8890+
fs.writeFileSync(path.join(tmpDir, '.oxfmtrc.json'), '{"singleQuote":true}\n');
8891+
const workspaceInfo = makeWorkspaceInfo(tmpDir, PackageManager.npm);
8892+
8893+
expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true).oxcConfigs).toBe(true);
8894+
expect(fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8')).toBe(config);
8895+
expect(fs.existsSync(path.join(tmpDir, '.oxfmtrc.json'))).toBe(false);
8896+
expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true).oxcConfigs).toBe(false);
8897+
});
8898+
8899+
it('keeps an unmergeable config and reports the incomplete migration', () => {
8900+
fs.writeFileSync(
8901+
path.join(tmpDir, 'package.json'),
8902+
JSON.stringify({ name: 'test', devDependencies: { 'vite-plus': 'latest' } }),
8903+
);
8904+
fs.writeFileSync(
8905+
path.join(tmpDir, 'vite.config.ts'),
8906+
'const config = {}; export default config;\n',
8907+
);
8908+
fs.writeFileSync(path.join(tmpDir, '.oxfmtrc.json'), '{"singleQuote":true}\n');
8909+
const report = createMigrationReport();
8910+
8911+
const result = finalizeCoreMigrationForExistingVitePlus(
8912+
makeWorkspaceInfo(tmpDir, PackageManager.npm),
8913+
true,
8914+
report,
8915+
);
8916+
expect(result.oxcConfigs).toBe(false);
8917+
expect(fs.existsSync(path.join(tmpDir, '.oxfmtrc.json'))).toBe(true);
8918+
expect(report.warnings.some((warning) => warning.includes('Failed to merge'))).toBe(true);
8919+
});
8920+
88308921
it('detects package-level legacy signals in workspaces', () => {
88318922
const appDir = path.join(tmpDir, 'packages', 'app');
88328923
fs.mkdirSync(appDir, { recursive: true });
@@ -8852,6 +8943,40 @@ describe('existing Vite+ core migration finalization', () => {
88528943
expect(appPkg.scripts.dev).toBe('vp dev');
88538944
});
88548945

8946+
it('finishes leftover Oxc and tsdown configs together and remains idempotent', () => {
8947+
fs.writeFileSync(
8948+
path.join(tmpDir, 'package.json'),
8949+
JSON.stringify({ name: 'test', devDependencies: { 'vite-plus': 'latest' } }),
8950+
);
8951+
fs.writeFileSync(path.join(tmpDir, 'vite.config.ts'), 'export default {};\n');
8952+
fs.writeFileSync(path.join(tmpDir, '.oxfmtrc.json'), '{"singleQuote":true,"semi":false}\n');
8953+
fs.writeFileSync(
8954+
path.join(tmpDir, 'tsdown.config.ts'),
8955+
"import { defineConfig } from 'tsdown'; export default defineConfig({ entry: 'src/index.ts' });\n",
8956+
);
8957+
const workspaceInfo = makeWorkspaceInfo(tmpDir, PackageManager.pnpm);
8958+
8959+
const result = finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true);
8960+
expect(result.oxcConfigs).toBe(true);
8961+
expect(result.tsdownConfig).toBe(true);
8962+
const config = fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8');
8963+
expect(config).toContain('pack: tsdownConfig');
8964+
expect(config).toContain('"singleQuote":true');
8965+
expect(config).toContain('"semi":false');
8966+
expect(fs.existsSync(path.join(tmpDir, '.oxfmtrc.json'))).toBe(false);
8967+
expect(fs.readFileSync(path.join(tmpDir, 'tsdown.config.ts'), 'utf8')).toContain(
8968+
"from 'vite-plus/pack'",
8969+
);
8970+
expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true)).toEqual({
8971+
scripts: false,
8972+
tsconfigTypes: false,
8973+
imports: false,
8974+
oxcConfigs: false,
8975+
tsdownConfig: false,
8976+
});
8977+
expect(fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8')).toBe(config);
8978+
});
8979+
88558980
it('makes a leftover tsdown config discoverable in an existing Vite+ project', () => {
88568981
fs.writeFileSync(
88578982
path.join(tmpDir, 'package.json'),
@@ -8873,6 +8998,7 @@ export default defineConfig({
88738998
tsconfigTypes: false,
88748999
imports: true,
88759000
tsdownConfig: true,
9001+
oxcConfigs: false,
88769002
});
88779003
expect(fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8')).toContain(
88789004
"import tsdownConfig from './tsdown.config.js';",
@@ -8889,6 +9015,7 @@ export default defineConfig({
88899015
tsconfigTypes: false,
88909016
imports: false,
88919017
tsdownConfig: false,
9018+
oxcConfigs: false,
88929019
});
88939020
});
88949021

@@ -8925,6 +9052,7 @@ export default defineConfig({ entry: 'src/index.ts' });
89259052
tsconfigTypes: false,
89269053
imports: true,
89279054
tsdownConfig: false,
9055+
oxcConfigs: false,
89289056
});
89299057
expect(fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8')).toBe(originalViteConfig);
89309058
expect(report.tsdownImportCount).toBe(0);

0 commit comments

Comments
 (0)