Skip to content

Commit 1c693c2

Browse files
committed
feat(create): auto-migrate ESLint/Prettier to oxlint/oxfmt (#1434)
Vite+ is opinionated about oxlint + oxfmt, but `vp create` previously scaffolded templates (e.g. `create-vite --template react-ts`) and left their ESLint flat config and Prettier configs untouched — forcing users to run `vp migrate` as a second step. This wires the existing migration helpers into `vp create` so freshly scaffolded projects are already on the unified toolchain, with no confirmation prompt. - Relocate the ESLint/Prettier prompt/warn/confirm helpers from migration/bin.ts to migration/migrator.ts so create/bin.ts can reuse them without triggering bin.ts's top-level `main()` side effect. - In create/bin.ts, run `promptEslintMigration` and `promptPrettierMigration` after `runViteInstall` (so `@oxlint/migrate` can resolve the template's ESLint plugin imports) and before `runViteFmt`. Always non-interactive — unlike `vp migrate`, the scaffold has no prior user preferences to respect. - Gate on `installSummary.status === 'installed'` so VP_SKIP_INSTALL snap-test runs skip migration cleanly. - Update rfcs/code-generator.md: flip the "does NOT migrate ESLint/Prettier" bullets, update flow diagrams and example output, and document why create skips the confirmation prompt. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes `vp create` post-scaffold flow to conditionally install-before-rewrite and run non-interactive ESLint/Prettier migrations, which can affect dependency installation order and generated configs across package managers (notably Yarn). CI coverage is added, but the behavior change is user-facing and tied to tooling resolution. > > **Overview** > `vp create` now detects when a scaffolded template includes ESLint (flat config) and/or Prettier and **automatically migrates** them to oxlint/oxfmt *before* the usual Vite+ rewrite, so the generated `.oxlintrc.json`/`.oxfmtrc.json` get merged into `vite.config.ts` and scripts/deps are updated. > > To enable reuse without CLI side effects, the ESLint/Prettier prompt/warn/confirm helpers are moved from `migration/bin.ts` into `migration/migrator.ts` (and `setPackageManager` is exported), and `create/bin.ts` adds an install-first path (including forcing Yarn `node-modules` linker) gated on successful install. > > CI and snapshot coverage are expanded with a remote `create-vite react-ts` case and assertions that `eslint.config.js` is removed, no loose `.oxlintrc.json` remains, `vite.config.ts` has a `lint` section, and `package.json` drops `eslint` while using `vp lint`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 0f8b49b. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent eafe577 commit 1c693c2

7 files changed

Lines changed: 436 additions & 146 deletions

File tree

.github/workflows/test-vp-create.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,29 @@ jobs:
123123
create-args: vite:monorepo --directory test-project
124124
template-args: ''
125125
verify-command: vp run ready
126+
verify-migration: 'false'
126127
- name: application
127128
create-args: vite:application --directory test-project
128129
template-args: '-- --template vanilla-ts'
129130
verify-command: vp run build
131+
verify-migration: 'false'
130132
- name: library
131133
create-args: vite:library --directory test-project
132134
template-args: ''
133135
verify-command: |
134136
vp run build
135137
vp run test
138+
verify-migration: 'false'
139+
# Remote template that ships ESLint (+ an eslint.config.js importing
140+
# @eslint/js etc.). Exercises the migrate-before-rewrite reorder in
141+
# `vp create`: after scaffold, ESLint → oxlint and Prettier → oxfmt
142+
# run before the vite-plus rewrite so `.oxlintrc` / `.oxfmtrc` get
143+
# merged into vite.config.ts.
144+
- name: remote-vite-react-ts
145+
create-args: vite@9.0.5
146+
template-args: '-- test-project --template react-ts'
147+
verify-command: vp run build
148+
verify-migration: 'true'
136149
package-manager:
137150
- pnpm
138151
- npm
@@ -253,6 +266,41 @@ jobs:
253266
console.log('✓ vite-plus@' + pkg.version + ' installed correctly');
254267
"
255268
269+
- name: Verify ESLint/Prettier auto-migration
270+
if: matrix.template.verify-migration == 'true'
271+
working-directory: ${{ runner.temp }}/test-project
272+
run: |
273+
# eslint.config.js must be gone (migration deleted it)
274+
test ! -f eslint.config.js
275+
echo "✓ eslint.config.js removed"
276+
277+
# .oxlintrc.json must NOT be loose on disk — it was merged into
278+
# vite.config.ts by the rewrite step that runs after migration.
279+
test ! -f .oxlintrc.json
280+
echo "✓ .oxlintrc.json merged into vite.config.ts"
281+
282+
# vite.config.ts must contain the merged oxlint config.
283+
grep -q '^[[:space:]]*lint:' vite.config.ts
284+
echo "✓ vite.config.ts has merged lint section"
285+
286+
# package.json: eslint devDep removed, vite-plus present, lint script rewritten.
287+
node -e "
288+
const pkg = require('./package.json');
289+
if (pkg.devDependencies && pkg.devDependencies.eslint) {
290+
console.error('✗ eslint devDependency should have been removed');
291+
process.exit(1);
292+
}
293+
if (!pkg.devDependencies || !pkg.devDependencies['vite-plus']) {
294+
console.error('✗ vite-plus devDependency missing');
295+
process.exit(1);
296+
}
297+
if (!pkg.scripts || !pkg.scripts.lint || !pkg.scripts.lint.includes('vp lint')) {
298+
console.error('✗ lint script should invoke vp lint, got: ' + (pkg.scripts && pkg.scripts.lint));
299+
process.exit(1);
300+
}
301+
console.log('✓ package.json migrated (eslint gone, vite-plus added, lint script rewritten)');
302+
"
303+
256304
- name: Run vp check
257305
working-directory: ${{ runner.temp }}/test-project
258306
run: vp check
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
> vp create vite@9.0.5 --no-interactive -- my-react-ts --template react-ts # create vite app with pinned version + react-ts template, should auto-migrate ESLint -> Oxlint and merge lint config into vite.config.ts
2+
> test ! -f my-react-ts/eslint.config.js && echo 'eslint.config.js removed' # eslint config deleted
3+
eslint.config.js removed
4+
5+
> test ! -f my-react-ts/.oxlintrc.json && echo '.oxlintrc.json merged into vite.config.ts' # migration output merged by rewrite step (matches vp migrate)
6+
.oxlintrc.json merged into vite.config.ts
7+
8+
> cat my-react-ts/vite.config.ts # merged vite config should contain lint and fmt sections
9+
import { defineConfig } from "vite-plus";
10+
import react from "@vitejs/plugin-react";
11+
12+
// https://vite.dev/config/
13+
export default defineConfig({
14+
staged: {
15+
"*": "vp check --fix",
16+
},
17+
fmt: {},
18+
lint: {
19+
plugins: ["oxc", "typescript", "unicorn", "react"],
20+
categories: {
21+
correctness: "warn",
22+
},
23+
env: {
24+
builtin: true,
25+
},
26+
ignorePatterns: ["dist"],
27+
overrides: [
28+
{
29+
files: ["**/*.{ts,tsx}"],
30+
rules: {
31+
"constructor-super": "error",
32+
"for-direction": "error",
33+
"getter-return": "error",
34+
"no-async-promise-executor": "error",
35+
"no-case-declarations": "error",
36+
"no-class-assign": "error",
37+
"no-compare-neg-zero": "error",
38+
"no-cond-assign": "error",
39+
"no-const-assign": "error",
40+
"no-constant-binary-expression": "error",
41+
"no-constant-condition": "error",
42+
"no-control-regex": "error",
43+
"no-debugger": "error",
44+
"no-delete-var": "error",
45+
"no-dupe-class-members": "error",
46+
"no-dupe-else-if": "error",
47+
"no-dupe-keys": "error",
48+
"no-duplicate-case": "error",
49+
"no-empty": "error",
50+
"no-empty-character-class": "error",
51+
"no-empty-pattern": "error",
52+
"no-empty-static-block": "error",
53+
"no-ex-assign": "error",
54+
"no-extra-boolean-cast": "error",
55+
"no-fallthrough": "error",
56+
"no-func-assign": "error",
57+
"no-global-assign": "error",
58+
"no-import-assign": "error",
59+
"no-invalid-regexp": "error",
60+
"no-irregular-whitespace": "error",
61+
"no-loss-of-precision": "error",
62+
"no-misleading-character-class": "error",
63+
"no-new-native-nonconstructor": "error",
64+
"no-nonoctal-decimal-escape": "error",
65+
"no-obj-calls": "error",
66+
"no-prototype-builtins": "error",
67+
"no-redeclare": "error",
68+
"no-regex-spaces": "error",
69+
"no-self-assign": "error",
70+
"no-setter-return": "error",
71+
"no-shadow-restricted-names": "error",
72+
"no-sparse-arrays": "error",
73+
"no-this-before-super": "error",
74+
"no-undef": "error",
75+
"no-unexpected-multiline": "error",
76+
"no-unreachable": "error",
77+
"no-unsafe-finally": "error",
78+
"no-unsafe-negation": "error",
79+
"no-unsafe-optional-chaining": "error",
80+
"no-unused-labels": "error",
81+
"no-unused-private-class-members": "error",
82+
"no-unused-vars": "error",
83+
"no-useless-backreference": "error",
84+
"no-useless-catch": "error",
85+
"no-useless-escape": "error",
86+
"no-with": "error",
87+
"require-yield": "error",
88+
"use-isnan": "error",
89+
"valid-typeof": "error",
90+
"no-array-constructor": "error",
91+
"no-unused-expressions": "error",
92+
"typescript/ban-ts-comment": "error",
93+
"typescript/no-duplicate-enum-values": "error",
94+
"typescript/no-empty-object-type": "error",
95+
"typescript/no-explicit-any": "error",
96+
"typescript/no-extra-non-null-assertion": "error",
97+
"typescript/no-misused-new": "error",
98+
"typescript/no-namespace": "error",
99+
"typescript/no-non-null-asserted-optional-chain": "error",
100+
"typescript/no-require-imports": "error",
101+
"typescript/no-this-alias": "error",
102+
"typescript/no-unnecessary-type-constraint": "error",
103+
"typescript/no-unsafe-declaration-merging": "error",
104+
"typescript/no-unsafe-function-type": "error",
105+
"typescript/no-wrapper-object-types": "error",
106+
"typescript/prefer-as-const": "error",
107+
"typescript/prefer-namespace-keyword": "error",
108+
"typescript/triple-slash-reference": "error",
109+
"react/rules-of-hooks": "error",
110+
"react/exhaustive-deps": "warn",
111+
"react/only-export-components": [
112+
"error",
113+
{
114+
allowConstantExport: true,
115+
},
116+
],
117+
},
118+
env: {
119+
es2020: true,
120+
browser: true,
121+
},
122+
},
123+
],
124+
options: {
125+
typeAware: true,
126+
typeCheck: true,
127+
},
128+
},
129+
plugins: [react()],
130+
});
131+
132+
> node -e "const p=require('./my-react-ts/package.json');console.log('lint:', p.scripts && p.scripts.lint);console.log('eslint dep:', !!(p.devDependencies && p.devDependencies.eslint));console.log('vite-plus dep:', !!(p.devDependencies && p.devDependencies['vite-plus']));" # scripts rewritten, eslint dep removed, vite-plus added
133+
lint: vp lint .
134+
eslint dep: false
135+
vite-plus dep: true
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"env": {
3+
"VP_SKIP_INSTALL": "",
4+
"CI": ""
5+
},
6+
"commands": [
7+
{
8+
"command": "vp create vite@9.0.5 --no-interactive -- my-react-ts --template react-ts # create vite app with pinned version + react-ts template, should auto-migrate ESLint -> Oxlint and merge lint config into vite.config.ts",
9+
"ignoreOutput": true
10+
},
11+
"test ! -f my-react-ts/eslint.config.js && echo 'eslint.config.js removed' # eslint config deleted",
12+
"test ! -f my-react-ts/.oxlintrc.json && echo '.oxlintrc.json merged into vite.config.ts' # migration output merged by rewrite step (matches vp migrate)",
13+
"cat my-react-ts/vite.config.ts # merged vite config should contain lint and fmt sections",
14+
"node -e \"const p=require('./my-react-ts/package.json');console.log('lint:', p.scripts && p.scripts.lint);console.log('eslint dep:', !!(p.devDependencies && p.devDependencies.eslint));console.log('vite-plus dep:', !!(p.devDependencies && p.devDependencies['vite-plus']));\" # scripts rewritten, eslint dep removed, vite-plus added"
15+
]
16+
}

packages/cli/src/create/bin.ts

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from 'node:fs';
12
import path from 'node:path';
23
import { styleText } from 'node:util';
34

@@ -7,12 +8,17 @@ import mri from 'mri';
78
import { vitePlusHeader } from '../../binding/index.js';
89
import {
910
addFrameworkShim,
11+
detectEslintProject,
1012
detectFramework,
13+
detectPrettierProject,
1114
hasFrameworkShim,
1215
installGitHooks,
16+
promptEslintMigration,
17+
promptPrettierMigration,
1318
rewriteMonorepo,
1419
rewriteMonorepoProject,
1520
rewriteStandaloneProject,
21+
setPackageManager,
1622
} from '../migration/migrator.ts';
1723
import { DependencyType, PackageManager, type WorkspaceInfo } from '../types/index.ts';
1824
import {
@@ -893,18 +899,50 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
893899
});
894900
resumeCreateProgress();
895901

902+
// The migrate-before-rewrite reorder is only needed when the template
903+
// actually ships ESLint or Prettier (e.g. `create-vite --template
904+
// react-ts`). Builtin templates (vite:library, vite:application,
905+
// vite:monorepo) don't — their package.json already references vite-plus
906+
// and relies on `rewrite*Project` to add tarball overrides BEFORE the
907+
// first install, so install-first would break CI's local-tarball resolve.
908+
const shouldMigrateLintFmtTools =
909+
detectEslintProject(fullPath).hasDependency || detectPrettierProject(fullPath).hasDependency;
910+
896911
let installSummary: CommandRunSummary | undefined;
912+
913+
// For templates that ship ESLint/Prettier, install template deps first so
914+
// `@oxlint/migrate` can resolve eslint.config.js's plugin imports, then
915+
// migrate before the vite-plus rewrite so the generated .oxlintrc/.oxfmtrc
916+
// get merged into vite.config.ts — matching `vp migrate`. Pin the
917+
// packageManager field (vite_install hardcodes pnpm in CI/non-TTY when no
918+
// signal is present) and force yarn's classic node_modules layout
919+
// (Plug'n'Play zip entries break @oxlint/migrate's fileURLToPath resolution).
920+
const installAndMigrate = async (installCwd: string) => {
921+
setPackageManager(fullPath, workspaceInfo.downloadPackageManager);
922+
if (workspaceInfo.packageManager === PackageManager.yarn) {
923+
const yarnrcPath = path.join(fullPath, '.yarnrc.yml');
924+
if (!fs.existsSync(yarnrcPath)) {
925+
fs.writeFileSync(yarnrcPath, 'nodeLinker: node-modules\n');
926+
}
927+
}
928+
updateCreateProgress('Installing dependencies');
929+
installSummary = await runViteInstall(installCwd, options.interactive, installArgs, {
930+
silent: compactOutput,
931+
});
932+
if (installSummary.status !== 'installed') {
933+
return;
934+
}
935+
updateCreateProgress('Migrating lint and format tools');
936+
pauseCreateProgress();
937+
await promptEslintMigration(fullPath, /* interactive */ false);
938+
await promptPrettierMigration(fullPath, /* interactive */ false);
939+
resumeCreateProgress();
940+
};
941+
897942
if (isMonorepo) {
898943
if (!compactOutput) {
899944
prompts.log.step('Monorepo integration...');
900945
}
901-
updateCreateProgress('Integrating into monorepo');
902-
rewriteMonorepoProject(fullPath, workspaceInfo.packageManager, undefined, compactOutput);
903-
for (const framework of detectFramework(fullPath)) {
904-
if (!hasFrameworkShim(fullPath, framework)) {
905-
addFrameworkShim(fullPath, framework);
906-
}
907-
}
908946

909947
if (workspaceInfo.packages.length > 0) {
910948
if (options.interactive) {
@@ -965,6 +1003,16 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
9651003
}
9661004

9671005
updateWorkspaceConfig(projectDir, workspaceInfo);
1006+
if (shouldMigrateLintFmtTools) {
1007+
await installAndMigrate(workspaceInfo.rootDir);
1008+
}
1009+
updateCreateProgress('Integrating into monorepo');
1010+
rewriteMonorepoProject(fullPath, workspaceInfo.packageManager, undefined, compactOutput);
1011+
for (const framework of detectFramework(fullPath)) {
1012+
if (!hasFrameworkShim(fullPath, framework)) {
1013+
addFrameworkShim(fullPath, framework);
1014+
}
1015+
}
9681016
updateCreateProgress('Installing dependencies');
9691017
installSummary = await runViteInstall(workspaceInfo.rootDir, options.interactive, installArgs, {
9701018
silent: compactOutput,
@@ -974,6 +1022,9 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
9741022
silent: compactOutput,
9751023
});
9761024
} else {
1025+
if (shouldMigrateLintFmtTools) {
1026+
await installAndMigrate(fullPath);
1027+
}
9771028
updateCreateProgress('Applying Vite+ project setup');
9781029
rewriteStandaloneProject(fullPath, workspaceInfo, undefined, compactOutput);
9791030
for (const framework of detectFramework(fullPath)) {

0 commit comments

Comments
 (0)