diff --git a/Cargo.lock b/Cargo.lock index ba9aa79188..8c7bbdd531 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8569,11 +8569,13 @@ dependencies = [ "ast-grep-core", "ast-grep-language", "brush-parser", + "fast-glob", "ignore", "js-semver", "oxc", "rayon", "regex", + "serde", "serde_json", "tempfile", "vp_error", diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/app.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/app.test.ts new file mode 100644 index 0000000000..057a5af85f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/app.test.ts @@ -0,0 +1,2 @@ +import { test } from 'vitest'; +test('application', () => {}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/examples/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/examples/vite.config.ts new file mode 100644 index 0000000000..0b79ce8fee --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/examples/vite.config.ts @@ -0,0 +1,2 @@ +import { defineConfig } from 'vite'; +export default defineConfig({}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/package.json new file mode 100644 index 0000000000..16cf6cd188 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/package.json @@ -0,0 +1 @@ +{ "name": "lint-import-overrides", "private": true, "type": "module", "devDependencies": { "vite-plus": "latest" } } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/snapshots.toml new file mode 100644 index 0000000000..57c465b518 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/snapshots.toml @@ -0,0 +1,10 @@ +[[case]] +name = "lint_vite_plus_imports_overrides" +vp = "local" +comment = "Existing lint.overrides can disable only the import rule in templates and examples, with no migrate configuration. Other lint rules remain active." +steps = [ + { argv = ["vp", "lint", "--threads=1", "--fix", "app.test.ts", "templates/starter.test.ts", "examples/vite.config.ts"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "app.test.ts"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "templates/starter.test.ts"] }, + { argv = ["vpt", "print-file", "examples/vite.config.ts"] }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/snapshots/lint_vite_plus_imports_overrides.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/snapshots/lint_vite_plus_imports_overrides.md new file mode 100644 index 0000000000..fb05943e8c --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/snapshots/lint_vite_plus_imports_overrides.md @@ -0,0 +1,44 @@ +# lint_vite_plus_imports_overrides + +Existing lint.overrides can disable only the import rule in templates and examples, with no migrate configuration. Other lint rules remain active. + +## `vp lint --threads=1 --fix app.test.ts templates/starter.test.ts examples/vite.config.ts` + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[templates/starter.test.ts:2:1] + 1 │ import { test } from 'vitest'; + 2 │ debugger; + · ───────── + 3 │ test('template', () => {}); + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 3 files with rules using threads. +``` + +## `vpt print-file app.test.ts` + +``` +import { test } from 'vite-plus/test'; +test('application', () => {}); +``` + +## `vpt print-file templates/starter.test.ts` + +``` +import { test } from 'vitest'; +debugger; +test('template', () => {}); +``` + +## `vpt print-file examples/vite.config.ts` + +``` +import { defineConfig } from 'vite'; +export default defineConfig({}); +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/templates/starter.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/templates/starter.test.ts new file mode 100644 index 0000000000..a95b040e4a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/templates/starter.test.ts @@ -0,0 +1,3 @@ +import { test } from 'vitest'; +debugger; +test('template', () => {}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/vite.config.ts new file mode 100644 index 0000000000..40a3af6960 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite-plus'; +export default defineConfig({ + lint: { + jsPlugins: [{ name: 'vite-plus', specifier: 'vite-plus/oxlint-plugin' }], + rules: { 'vite-plus/prefer-vite-plus-imports': 'error', 'no-debugger': 'error' }, + overrides: [ + { + files: ['templates/**', 'examples/**'], + rules: { 'vite-plus/prefer-vite-plus-imports': 'off' }, + }, + ], + }, +}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/examples/standalone/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/examples/standalone/package.json new file mode 100644 index 0000000000..14a01f696b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/examples/standalone/package.json @@ -0,0 +1 @@ +{ "name": "standalone-template", "private": true, "type": "module", "devDependencies": { "vite": "^8.0.0", "vitest": "^4.1.11" } } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/examples/standalone/unit.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/examples/standalone/unit.test.ts new file mode 100644 index 0000000000..fee5fb1242 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/examples/standalone/unit.test.ts @@ -0,0 +1,2 @@ +import { expect, test } from 'vitest'; +test('outside-workspace', () => expect(1).toBe(1)); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/package.json new file mode 100644 index 0000000000..ab4f2d8a6b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/package.json @@ -0,0 +1,7 @@ +{ + "name": "migration-import-rule", + "private": true, + "type": "module", + "devDependencies": { "vite-plus": "catalog:" }, + "devEngines": { "packageManager": { "name": "pnpm", "version": "10.33.0", "onFail": "download" } } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/package.json new file mode 100644 index 0000000000..6c0d9255f6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/package.json @@ -0,0 +1,5 @@ +{ + "name": "app", "private": true, "type": "module", + "scripts": { "build": "vite build", "test": "vitest run" }, + "devDependencies": { "vite": "catalog:", "vitest": "catalog:legacy", "@vitest/coverage-v8": "catalog:legacy" } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/unit.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/unit.test.ts new file mode 100644 index 0000000000..43ccd6e1ae --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/unit.test.ts @@ -0,0 +1,2 @@ +import { expect, test } from 'vitest'; +test('application', () => expect(1).toBe(1)); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/vite.config.ts new file mode 100644 index 0000000000..0b79ce8fee --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/vite.config.ts @@ -0,0 +1,2 @@ +import { defineConfig } from 'vite'; +export default defineConfig({}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/pnpm-workspace.yaml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/pnpm-workspace.yaml new file mode 100644 index 0000000000..e7f5c119eb --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/pnpm-workspace.yaml @@ -0,0 +1,12 @@ +packages: + - packages/* + - templates/* +catalog: + vite-plus: latest + vite: ^8.0.0 +catalogs: + legacy: + vitest: ^4.1.11 + '@vitest/coverage-v8': ^4.1.11 +overrides: + unrelated-package: 1.0.0 diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots.toml new file mode 100644 index 0000000000..1cc88035e1 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots.toml @@ -0,0 +1,96 @@ +[[case]] +name = "migration_import_rule_upgrade" +vp = "local" +comment = "Existing Vite+ migration honors ordered lint overrides while still migrating scripts, dependencies, catalogs and overrides." +steps = [ + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, + { argv = ["vpt", "print-file", "templates/starter/vite.config.ts"] }, + { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, + { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/starter/package.json"] }, + { argv = ["vpt", "print-file", "pnpm-workspace.yaml"] }, + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, + { argv = ["vpt", "write-file", "packages/app/unit.test.ts", "import { test } from 'vitest';\ntest('autofix', () => {});\n"] }, + { argv = ["vp", "lint", "--threads=1", "--fix", "templates/starter/unit.test.ts", "packages/app/unit.test.ts"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, +] + +[[case]] +name = "migration_import_rule_fresh" +vp = "local" +comment = "Initial migration respects the same import rule configuration." +steps = [ + { argv = ["vpt", "json-edit", "package.json", "devDependencies", "{}"] }, + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, + { argv = ["vpt", "print-file", "templates/starter/vite.config.ts"] }, + { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, + { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/starter/package.json"] }, + { argv = ["vpt", "print-file", "pnpm-workspace.yaml"] }, +] + +[[case]] +name = "migration_import_rule_full" +vp = "local" +comment = "Full migration and check autofix share the import rule; other lint rules remain active." +steps = [ + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor", "--full"] }, + { argv = ["vpt", "print-file", "templates/starter/vite.config.ts"] }, + { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, + { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/starter/package.json"] }, + { argv = ["vpt", "print-file", "pnpm-workspace.yaml"] }, + { argv = ["vpt", "write-file", "packages/app/unit.test.ts", "import { test } from 'vitest';\ntest('autofix', () => {});\n"] }, + { argv = ["vp", "check", "--no-fmt", "--fix", "templates/starter/unit.test.ts", "packages/app/unit.test.ts"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, +] + +[[case]] +name = "migration_import_rule_global_off" +vp = "local" +comment = "A global numeric off setting applies unless a matching override re-enables imports." +steps = [ + { argv = ["vpt", "write-file", "vite.config.ts", "export default { lint: { rules: { 'vite-plus/prefer-vite-plus-imports': [0] }, overrides: [{ files: ['packages/**'], rules: { 'vite-plus/prefer-vite-plus-imports': [1] } }] } };\n"] }, + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, + { argv = ["vpt", "print-file", "templates/starter/vite.config.ts"] }, + { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, + { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, +] + +[[case]] +name = "migration_import_rule_runtime" +vp = "local" +comment = "Dynamic lint configuration uses normal config resolution." +steps = [ + { argv = ["vpt", "write-file", "vite.config.ts", "const files = ['templates/**', 'examples/**'];\nexport default () => ({ lint: { overrides: [{ files, excludeFiles: ['templates/managed/**'], rules: { 'vite-plus/prefer-vite-plus-imports': 'off' } }] } });\n"] }, + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, + { argv = ["vpt", "print-file", "templates/starter/vite.config.ts"] }, + { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, + { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, +] + +[[case]] +name = "migration_import_rule_extends" +vp = "local" +comment = "Inherited overrides apply before local overrides; bare patterns match at any depth." +steps = [ + { argv = ["vpt", "write-file", "vite.config.ts", "export default { lint: { extends: [{ overrides: [{ files: ['*.ts'], rules: { 'vite-plus/prefer-vite-plus-imports': 'off' } }] }], overrides: [{ files: ['packages/**', './templates/managed/**'], rules: { 'vite-plus/prefer-vite-plus-imports': ['error'] } }] } };\n"] }, + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, + { argv = ["vpt", "print-file", "templates/starter/vite.config.ts"] }, + { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, + { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_extends.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_extends.md new file mode 100644 index 0000000000..9bbe531555 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_extends.md @@ -0,0 +1,65 @@ +# migration_import_rule_extends + +Inherited overrides apply before local overrides; bare patterns match at any depth. + +## `vpt write-file vite.config.ts 'export default { lint: { extends: [{ overrides: [{ files: ['\''*.ts'\''], rules: { '\''vite-plus/prefer-vite-plus-imports'\'': '\''off'\'' } }] }], overrides: [{ files: ['\''packages/**'\'', '\''./templates/managed/**'\''], rules: { '\''vite-plus/prefer-vite-plus-imports'\'': ['\''error'\''] } }] } }; +'` + +``` +``` + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite-plus latest → + vite → +• 3 files had imports rewritten +• Package manager settings configured +``` + +## `vpt print-file templates/starter/vite.config.ts` + +``` +import { defineConfig } from 'vite'; +export default defineConfig({test: { + // Vitest v4 compatibility: preserve mock call history. + // Remove after tests no longer rely on calls from setup or earlier tests. + // https://viteplus.dev/guide/vitest-v5#remove-unneeded-compatibility-settings + // https://vitest.dev/guide/migration/#clearmocks-is-enabled-by-default + clearMocks: false, + poolOptions: {threads: {singleThread: true}}}}); +``` + +## `vpt print-file templates/starter/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +debugger; +test('template', () => expect(1).toBe(1)); +``` + +## `vpt print-file templates/managed/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('included', () => expect(1).toBe(1)); +``` + +## `vpt print-file examples/standalone/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +test('outside-workspace', () => expect(1).toBe(1)); +``` + +## `vpt print-file packages/app/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('application', () => expect(1).toBe(1)); +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_fresh.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_fresh.md new file mode 100644 index 0000000000..ec881ca2aa --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_fresh.md @@ -0,0 +1,110 @@ +# migration_import_rule_fresh + +Initial migration respects the same import rule configuration. + +## `vpt json-edit package.json devDependencies {}` + +``` +``` + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Migrated . to Vite+ +• Node pnpm +• 1 config update applied, 3 files had imports rewritten +! Warnings: + - Removed `"esModuleInterop": false` from templates/starter/tsconfig.json — this option has been deprecated. See https://github.com/oxc-project/tsgolint/issues/351, https://github.com/microsoft/TypeScript/issues/62529 +``` + +## `vpt print-file templates/starter/vite.config.ts` + +``` +import { defineConfig } from 'vite'; +export default defineConfig({test: { + // Vitest v4 compatibility: preserve mock call history. + // Remove after tests no longer rely on calls from setup or earlier tests. + // https://viteplus.dev/guide/vitest-v5#remove-unneeded-compatibility-settings + // https://vitest.dev/guide/migration/#clearmocks-is-enabled-by-default + clearMocks: false, + poolOptions: {threads: {singleThread: true}}}}); +``` + +## `vpt print-file templates/starter/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +debugger; +test('template', () => expect(1).toBe(1)); +``` + +## `vpt print-file templates/managed/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('included', () => expect(1).toBe(1)); +``` + +## `vpt print-file examples/standalone/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +test('outside-workspace', () => expect(1).toBe(1)); +``` + +## `vpt print-file packages/app/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('application', () => expect(1).toBe(1)); +``` + +## `vpt print-file templates/starter/package.json` + +``` +{ + "name": "starter", + "private": true, + "type": "module", + "scripts": { + "build": "vp build", + "test": "vp test run" + }, + "devDependencies": { + "vite": "catalog:", + "vitest": "catalog:legacy", + "@vitest/coverage-v8": "catalog:legacy", + "vite-plus": "catalog:" + } +} +``` + +## `vpt print-file pnpm-workspace.yaml` + +``` +packages: + - packages/* + - templates/* +catalog: + vite-plus: + vite: npm:@voidzero-dev/vite-plus-core@ + vitest: + '@vitest/coverage-v8': +catalogs: + legacy: + vitest: + '@vitest/coverage-v8': +overrides: + unrelated-package: 1.0.0 + vite@*: 'catalog:' + vitest@*: 'catalog:' +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: '*' + vitest: '*' +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_full.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_full.md new file mode 100644 index 0000000000..102ce5e374 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_full.md @@ -0,0 +1,146 @@ +# migration_import_rule_full + +Full migration and check autofix share the import rule; other lint rules remain active. + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor --full` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite-plus latest → + vite → +• 3 files had imports rewritten +• Package manager settings configured +``` + +## `vpt print-file templates/starter/vite.config.ts` + +``` +import { defineConfig } from 'vite'; +export default defineConfig({test: { + // Vitest v4 compatibility: preserve mock call history. + // Remove after tests no longer rely on calls from setup or earlier tests. + // https://viteplus.dev/guide/vitest-v5#remove-unneeded-compatibility-settings + // https://vitest.dev/guide/migration/#clearmocks-is-enabled-by-default + clearMocks: false, + poolOptions: {threads: {singleThread: true}}}}); +``` + +## `vpt print-file templates/starter/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +debugger; +test('template', () => expect(1).toBe(1)); +``` + +## `vpt print-file templates/managed/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('included', () => expect(1).toBe(1)); +``` + +## `vpt print-file examples/standalone/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +test('outside-workspace', () => expect(1).toBe(1)); +``` + +## `vpt print-file packages/app/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('application', () => expect(1).toBe(1)); +``` + +## `vpt print-file templates/starter/package.json` + +``` +{ + "name": "starter", + "private": true, + "type": "module", + "scripts": { + "build": "vp build", + "test": "vp test run" + }, + "devDependencies": { + "vite": "catalog:", + "vitest": "catalog:legacy", + "@vitest/coverage-v8": "catalog:legacy" + } +} +``` + +## `vpt print-file pnpm-workspace.yaml` + +``` +packages: + - packages/* + - templates/* +catalog: + vite-plus: + vite: npm:@voidzero-dev/vite-plus-core@ + vitest: + '@vitest/coverage-v8': +catalogs: + legacy: + vitest: + '@vitest/coverage-v8': +overrides: + unrelated-package: 1.0.0 + vite@*: 'catalog:' + vitest@*: 'catalog:' +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: '*' + vitest: '*' +``` + +## `vpt write-file packages/app/unit.test.ts 'import { test } from '\''vitest'\''; +test('\''autofix'\'', () => {}); +'` + +``` +``` + +## `vp check --no-fmt --fix templates/starter/unit.test.ts packages/app/unit.test.ts` + +**Exit code:** 1 + +``` +error: Lint issues found +× eslint(no-debugger): `debugger` statement is not allowed + ╭─[templates/starter/unit.test.ts:2:1] + 1 │ import { expect, test } from 'vitest'; + 2 │ debugger; + · ───────── + 3 │ test('template', () => expect(1).toBe(1)); + ╰──── + help: Remove the debugger statement + +Found 1 error and 0 warnings in 2 files (, threads) +``` + +## `vpt print-file templates/starter/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +debugger; +test('template', () => expect(1).toBe(1)); +``` + +## `vpt print-file packages/app/unit.test.ts` + +``` +import { test } from 'vite-plus/test'; +test('autofix', () => {}); +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_global_off.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_global_off.md new file mode 100644 index 0000000000..63097d475e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_global_off.md @@ -0,0 +1,65 @@ +# migration_import_rule_global_off + +A global numeric off setting applies unless a matching override re-enables imports. + +## `vpt write-file vite.config.ts 'export default { lint: { rules: { '\''vite-plus/prefer-vite-plus-imports'\'': [0] }, overrides: [{ files: ['\''packages/**'\''], rules: { '\''vite-plus/prefer-vite-plus-imports'\'': [1] } }] } }; +'` + +``` +``` + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite-plus latest → + vite → +• 2 files had imports rewritten +• Package manager settings configured +``` + +## `vpt print-file templates/starter/vite.config.ts` + +``` +import { defineConfig } from 'vite'; +export default defineConfig({test: { + // Vitest v4 compatibility: preserve mock call history. + // Remove after tests no longer rely on calls from setup or earlier tests. + // https://viteplus.dev/guide/vitest-v5#remove-unneeded-compatibility-settings + // https://vitest.dev/guide/migration/#clearmocks-is-enabled-by-default + clearMocks: false, + poolOptions: {threads: {singleThread: true}}}}); +``` + +## `vpt print-file templates/starter/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +debugger; +test('template', () => expect(1).toBe(1)); +``` + +## `vpt print-file templates/managed/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +test('included', () => expect(1).toBe(1)); +``` + +## `vpt print-file examples/standalone/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +test('outside-workspace', () => expect(1).toBe(1)); +``` + +## `vpt print-file packages/app/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('application', () => expect(1).toBe(1)); +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_runtime.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_runtime.md new file mode 100644 index 0000000000..55b659b359 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_runtime.md @@ -0,0 +1,66 @@ +# migration_import_rule_runtime + +Dynamic lint configuration uses normal config resolution. + +## `vpt write-file vite.config.ts 'const files = ['\''templates/**'\'', '\''examples/**'\'']; +export default () => ({ lint: { overrides: [{ files, excludeFiles: ['\''templates/managed/**'\''], rules: { '\''vite-plus/prefer-vite-plus-imports'\'': '\''off'\'' } }] } }); +'` + +``` +``` + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite-plus latest → + vite → +• 3 files had imports rewritten +• Package manager settings configured +``` + +## `vpt print-file templates/starter/vite.config.ts` + +``` +import { defineConfig } from 'vite'; +export default defineConfig({test: { + // Vitest v4 compatibility: preserve mock call history. + // Remove after tests no longer rely on calls from setup or earlier tests. + // https://viteplus.dev/guide/vitest-v5#remove-unneeded-compatibility-settings + // https://vitest.dev/guide/migration/#clearmocks-is-enabled-by-default + clearMocks: false, + poolOptions: {threads: {singleThread: true}}}}); +``` + +## `vpt print-file templates/starter/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +debugger; +test('template', () => expect(1).toBe(1)); +``` + +## `vpt print-file templates/managed/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('included', () => expect(1).toBe(1)); +``` + +## `vpt print-file examples/standalone/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +test('outside-workspace', () => expect(1).toBe(1)); +``` + +## `vpt print-file packages/app/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('application', () => expect(1).toBe(1)); +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_upgrade.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_upgrade.md new file mode 100644 index 0000000000..d3d8355f4e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_upgrade.md @@ -0,0 +1,155 @@ +# migration_import_rule_upgrade + +Existing Vite+ migration honors ordered lint overrides while still migrating scripts, dependencies, catalogs and overrides. + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite-plus latest → + vite → +• 3 files had imports rewritten +• Package manager settings configured +``` + +## `vpt print-file templates/starter/vite.config.ts` + +``` +import { defineConfig } from 'vite'; +export default defineConfig({test: { + // Vitest v4 compatibility: preserve mock call history. + // Remove after tests no longer rely on calls from setup or earlier tests. + // https://viteplus.dev/guide/vitest-v5#remove-unneeded-compatibility-settings + // https://vitest.dev/guide/migration/#clearmocks-is-enabled-by-default + clearMocks: false, + poolOptions: {threads: {singleThread: true}}}}); +``` + +## `vpt print-file templates/starter/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +debugger; +test('template', () => expect(1).toBe(1)); +``` + +## `vpt print-file templates/managed/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('included', () => expect(1).toBe(1)); +``` + +## `vpt print-file examples/standalone/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +test('outside-workspace', () => expect(1).toBe(1)); +``` + +## `vpt print-file packages/app/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('application', () => expect(1).toBe(1)); +``` + +## `vpt print-file templates/starter/package.json` + +``` +{ + "name": "starter", + "private": true, + "type": "module", + "scripts": { + "build": "vp build", + "test": "vp test run" + }, + "devDependencies": { + "vite": "catalog:", + "vitest": "catalog:legacy", + "@vitest/coverage-v8": "catalog:legacy" + } +} +``` + +## `vpt print-file pnpm-workspace.yaml` + +``` +packages: + - packages/* + - templates/* +catalog: + vite-plus: + vite: npm:@voidzero-dev/vite-plus-core@ + vitest: + '@vitest/coverage-v8': +catalogs: + legacy: + vitest: + '@vitest/coverage-v8': +overrides: + unrelated-package: 1.0.0 + vite@*: 'catalog:' + vitest@*: 'catalog:' +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: '*' + vitest: '*' +``` + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +``` +VITE+ - The Unified Toolchain for the Web + +This project is already using Vite+! Happy coding! +``` + +## `vpt write-file packages/app/unit.test.ts 'import { test } from '\''vitest'\''; +test('\''autofix'\'', () => {}); +'` + +``` +``` + +## `vp lint --threads=1 --fix templates/starter/unit.test.ts packages/app/unit.test.ts` + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[templates/starter/unit.test.ts:2:1] + 1 │ import { expect, test } from 'vitest'; + 2 │ debugger; + · ───────── + 3 │ test('template', () => expect(1).toBe(1)); + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 2 files with rules using threads. +``` + +## `vpt print-file templates/starter/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +debugger; +test('template', () => expect(1).toBe(1)); +``` + +## `vpt print-file packages/app/unit.test.ts` + +``` +import { test } from 'vite-plus/test'; +test('autofix', () => {}); +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/managed/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/managed/package.json new file mode 100644 index 0000000000..fb282db2e7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/managed/package.json @@ -0,0 +1 @@ +{ "name": "managed", "private": true, "type": "module", "devDependencies": { "vite": "^8.0.0", "vitest": "^4.1.11" } } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/managed/unit.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/managed/unit.test.ts new file mode 100644 index 0000000000..64ccb0c91d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/managed/unit.test.ts @@ -0,0 +1,2 @@ +import { expect, test } from 'vitest'; +test('included', () => expect(1).toBe(1)); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/package.json new file mode 100644 index 0000000000..dad97a5f9b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/package.json @@ -0,0 +1,5 @@ +{ + "name": "starter", "private": true, "type": "module", + "scripts": { "build": "vite build", "test": "vitest run" }, + "devDependencies": { "vite": "catalog:", "vitest": "catalog:legacy", "@vitest/coverage-v8": "catalog:legacy" } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/tsconfig.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/tsconfig.json new file mode 100644 index 0000000000..dc98a30fcd --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "types": ["vite/client", "vitest/globals"], "esModuleInterop": false } } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/unit.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/unit.test.ts new file mode 100644 index 0000000000..a8415c55f6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/unit.test.ts @@ -0,0 +1,3 @@ +import { expect, test } from 'vitest'; +debugger; +test('template', () => expect(1).toBe(1)); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/vite.config.ts new file mode 100644 index 0000000000..2df3caf172 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/vite.config.ts @@ -0,0 +1,2 @@ +import { defineConfig } from 'vite'; +export default defineConfig({test: {poolOptions: {threads: {singleThread: true}}}}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/vite.config.ts new file mode 100644 index 0000000000..48f48b642f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite-plus'; +export default defineConfig({ + lint: { + jsPlugins: ['vite-plus/oxlint-plugin'], + rules: { 'vite-plus/prefer-vite-plus-imports': 'error', 'no-debugger': 'error' }, + overrides: [ + { files: ['templates/**', 'examples/**'], rules: { 'vite-plus/prefer-vite-plus-imports': 'off' } }, + { files: ['templates/managed/**'], rules: { 'vite-plus/prefer-vite-plus-imports': 'warn' } }, + ], + }, +}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/package.json new file mode 100644 index 0000000000..772d7f1641 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/package.json @@ -0,0 +1,14 @@ +{ + "name": "vite-starter", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "test": "vitest run" + }, + "devDependencies": { + "vite": "^8.0.0", + "vitest": "^4.1.11" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/src/example.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/src/example.test.ts new file mode 100644 index 0000000000..be9a49686b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/src/example.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest'; + +test('starter', () => { + expect(1 + 1).toBe(2); +}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/tsconfig.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/tsconfig.json new file mode 100644 index 0000000000..9b63a9c44f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "types": ["vite/client", "vitest/globals"] + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/vite.config.ts new file mode 100644 index 0000000000..c049f46e10 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/package.json new file mode 100644 index 0000000000..0b5804846a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/package.json @@ -0,0 +1,15 @@ +{ + "name": "migration-workspace-vite-template", + "private": true, + "type": "module", + "devDependencies": { + "vite-plus": "catalog:" + }, + "devEngines": { + "packageManager": { + "name": "pnpm", + "version": "10.33.0", + "onFail": "download" + } + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/packages/app/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/packages/app/package.json new file mode 100644 index 0000000000..44e2883301 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/packages/app/package.json @@ -0,0 +1,11 @@ +{ + "name": "app", + "private": true, + "type": "module", + "scripts": { + "build": "vp build" + }, + "devDependencies": { + "vite-plus": "catalog:" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/packages/app/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/packages/app/vite.config.ts new file mode 100644 index 0000000000..3210accbf9 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/packages/app/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite-plus'; + +export default defineConfig({}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/pnpm-workspace.yaml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/pnpm-workspace.yaml new file mode 100644 index 0000000000..0f1b01f634 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +packages: + - packages/* + - examples/* +catalog: + vite-plus: latest + vite: npm:@voidzero-dev/vite-plus-core@latest +overrides: + vite: 'catalog:' diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/snapshots.toml new file mode 100644 index 0000000000..a34266b28d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/snapshots.toml @@ -0,0 +1,14 @@ +[[case]] +name = "migration_workspace_vite_template" +vp = "local" +comment = "Without a lint override, migration rewrites template imports along with other workspace files." +steps = [ + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "an ordinary upgrade rewrites template imports by default" }, + { argv = ["vpt", "print-file", "examples/starter/vite.config.ts"], comment = "the template's Vite import is converted to vite-plus" }, + { argv = ["vpt", "print-file", "examples/starter/src/example.test.ts"], comment = "the template's Vitest import is converted to vite-plus/test" }, + { argv = ["vpt", "print-file", "examples/starter/package.json"], comment = "record dependency and script changes that also affect standalone use" }, + { argv = ["vpt", "print-file", "examples/starter/tsconfig.json"], comment = "record related type-reference changes" }, + { argv = ["vpt", "print-file", "pnpm-workspace.yaml"], comment = "the template remains a workspace member and shares toolchain overrides" }, + { argv = ["vpt", "print-file", "packages/app/package.json"], comment = "control: the application continues to use Vite+" }, + { argv = ["vpt", "print-file", "packages/app/vite.config.ts"] }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/snapshots/migration_workspace_vite_template.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/snapshots/migration_workspace_vite_template.md new file mode 100644 index 0000000000..dc86eb27d5 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/snapshots/migration_workspace_vite_template.md @@ -0,0 +1,132 @@ +# migration_workspace_vite_template + +Without a lint override, migration rewrites template imports along with other workspace files. + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +an ordinary upgrade rewrites template imports by default + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite-plus latest → + vite → +• 2 files had imports rewritten +• Package manager settings configured +``` + +## `vpt print-file examples/starter/vite.config.ts` + +the template's Vite import is converted to vite-plus + +``` +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + test: { + // Vitest v4 compatibility: preserve mock call history. + // Remove after tests no longer rely on calls from setup or earlier tests. + // https://viteplus.dev/guide/vitest-v5#remove-unneeded-compatibility-settings + // https://vitest.dev/guide/migration/#clearmocks-is-enabled-by-default + clearMocks: false + } +}); +``` + +## `vpt print-file examples/starter/src/example.test.ts` + +the template's Vitest import is converted to vite-plus/test + +``` +import { expect, test } from 'vite-plus/test'; + +test('starter', () => { + expect(1 + 1).toBe(2); +}); +``` + +## `vpt print-file examples/starter/package.json` + +record dependency and script changes that also affect standalone use + +``` +{ + "name": "vite-starter", + "private": true, + "type": "module", + "scripts": { + "dev": "vp dev", + "build": "vp build", + "test": "vp test run" + }, + "devDependencies": { + "vite": "catalog:", + "vitest": "catalog:" + } +} +``` + +## `vpt print-file examples/starter/tsconfig.json` + +record related type-reference changes + +``` +{ + "compilerOptions": { + "types": ["vite/client", "vitest/globals"] + } +} +``` + +## `vpt print-file pnpm-workspace.yaml` + +the template remains a workspace member and shares toolchain overrides + +``` +packages: + - packages/* + - examples/* +catalog: + vite-plus: + vite: npm:@voidzero-dev/vite-plus-core@ + vitest: +overrides: + vite@*: 'catalog:' + vitest@*: 'catalog:' +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: '*' + vitest: '*' +``` + +## `vpt print-file packages/app/package.json` + +control: the application continues to use Vite+ + +``` +{ + "name": "app", + "private": true, + "type": "module", + "scripts": { + "build": "vp build" + }, + "devDependencies": { + "vite-plus": "catalog:" + } +} +``` + +## `vpt print-file packages/app/vite.config.ts` + +``` +import { defineConfig } from 'vite-plus'; + +export default defineConfig({}); +``` diff --git a/crates/vp_migration/Cargo.toml b/crates/vp_migration/Cargo.toml index 7422188685..41f4970246 100644 --- a/crates/vp_migration/Cargo.toml +++ b/crates/vp_migration/Cargo.toml @@ -12,11 +12,13 @@ ast-grep-config = { workspace = true } ast-grep-core = { workspace = true } ast-grep-language = { workspace = true } brush-parser = { workspace = true } +fast-glob = { workspace = true } ignore = { workspace = true } js-semver = { workspace = true } oxc = { workspace = true } rayon = { workspace = true } regex = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true, features = ["preserve_order"] } vp_error = { workspace = true, features = ["migration"] } vp_toolchain = { workspace = true } diff --git a/crates/vp_migration/src/import_rewriter.rs b/crates/vp_migration/src/import_rewriter.rs index da8643be9f..6be27b5b1f 100644 --- a/crates/vp_migration/src/import_rewriter.rs +++ b/crates/vp_migration/src/import_rewriter.rs @@ -10,7 +10,7 @@ use rayon::prelude::*; use regex::Regex; use vp_error::Error; -use crate::{ast_grep, file_walker, import_merge, pack_config}; +use crate::{ImportRuleConfig, ast_grep, file_walker, import_merge, pack_config}; /// ast-grep rules for rewriting vite imports and declare module statements /// @@ -2062,6 +2062,8 @@ struct PackageRewriteContext { /// Options controlling directory-wide import rewriting. #[derive(Debug, Clone, Default)] pub struct RewriteImportsOptions { + /// Effective settings for `vite-plus/prefer-vite-plus-imports` from the root lint config. + pub import_rule: ImportRuleConfig, /// Preserve `vitest` and `vitest/*` module specifiers throughout packages /// whose nearest package.json declares `@nuxt/test-utils`. pub preserve_vitest_in_nuxt_packages: bool, @@ -2326,6 +2328,7 @@ pub fn rewrite_imports_in_directory_with_options( &file_path, root, &skip_packages, + options.import_rule.is_enabled(file_path.strip_prefix(root).unwrap_or(&file_path)), options.preserve_vitest_in_nuxt_packages && package_context.uses_nuxt_test_utils, ) { Ok(rewrite_result) => { @@ -2400,6 +2403,7 @@ fn rewrite_import( file_path: &Path, root: &Path, skip_packages: &SkipPackages, + rewrite_imports: bool, preserve_vitest_in_nuxt_package: bool, ) -> Result { let content = std::fs::read_to_string(file_path)?; @@ -2419,13 +2423,17 @@ fn rewrite_import( // whereas `vite-plus` re-exports no `UserConfig` symbol for a rewritten // augmentation to merge with. This is layered ON TOP of the package-level // skip (a skipped package stays skipped). - let mut result = rewrite_import_content_full( - &content, - skip_packages, - preserve_vitest_in_nuxt_package, - is_vite_config, - file_path.to_str(), - )?; + let mut result = if rewrite_imports { + rewrite_import_content_full( + &content, + skip_packages, + preserve_vitest_in_nuxt_package, + is_vite_config, + file_path.to_str(), + )? + } else { + RewriteResult { content, updated: false, preserved_vitest: false, warnings: Vec::new() } + }; if let Some(preserve_defaults) = preserve_defaults { let rewritten = pack_config::rewrite_pack_config(&result.content, is_tsdown_config, preserve_defaults); @@ -3029,9 +3037,14 @@ export default defineConfig({{ .unwrap(); // Run the rewrite - let result = - rewrite_import(&vite_config_path, temp_dir.path(), &SkipPackages::default(), false) - .unwrap(); + let result = rewrite_import( + &vite_config_path, + temp_dir.path(), + &SkipPackages::default(), + true, + false, + ) + .unwrap(); assert!(result.updated); assert_eq!( diff --git a/crates/vp_migration/src/import_rule.rs b/crates/vp_migration/src/import_rule.rs new file mode 100644 index 0000000000..cdae2acb62 --- /dev/null +++ b/crates/vp_migration/src/import_rule.rs @@ -0,0 +1,101 @@ +use std::path::Path; + +use serde::Deserialize; +use vp_error::Error; + +/// The import rule's settings, with inherited configs flattened by the JS caller. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ImportRuleConfig { + enabled: bool, + overrides: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ImportRuleOverride { + files: Vec, + exclude_files: Vec, + enabled: bool, +} + +impl Default for ImportRuleConfig { + fn default() -> Self { + Self { enabled: true, overrides: Vec::new() } + } +} + +impl ImportRuleConfig { + pub fn from_json(json: &str) -> Result { + let mut config: Self = serde_json::from_str(json)?; + for entry in &mut config.overrides { + for pattern in entry.files.iter_mut().chain(&mut entry.exclude_files) { + fast_glob::validate(&*pattern).map_err(|error| { + Error::InvalidArgument(format!("Invalid lint glob {pattern:?}: {error}").into()) + })?; + // Match Oxlint's GlobSet: bare names match at every depth, while + // ./ anchors a pattern to the config directory. These are glob + // patterns, not gitignore rules. + if let Some(relative) = pattern.strip_prefix("./") { + *pattern = relative.to_string(); + } else if !pattern.contains('/') { + *pattern = format!("**/{pattern}"); + } + } + } + Ok(config) + } + + pub(crate) fn is_enabled(&self, relative_path: &Path) -> bool { + let path = relative_path.to_string_lossy().replace('\\', "/"); + self.overrides + .iter() + .filter(|entry| { + entry.files.iter().any(|pattern| fast_glob::glob_match(pattern, &path)) + && !entry + .exclude_files + .iter() + .any(|pattern| fast_glob::glob_match(pattern, &path)) + }) + .fold(self.enabled, |_, entry| entry.enabled) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn matches_oxlint_globs_and_applies_overrides_in_order() { + let config = ImportRuleConfig::from_json(r#"{ + "enabled": true, + "overrides": [ + { "files": ["templates/**", "*.test.ts"], "excludeFiles": ["templates/keep/**"], "enabled": false }, + { "files": ["./root.test.ts", "templates/managed/**/*.{ts,tsx}"], "excludeFiles": [], "enabled": true } + ] + }"#).unwrap(); + for (path, enabled) in [ + ("app.ts", true), + ("templates/starter/vite.config.ts", false), + ("templates/keep/vite.config.ts", true), + ("templates/managed/test.tsx", true), + ("deep/unit.test.ts", false), + ("root.test.ts", true), + ("deep/root.test.ts", false), + (r"templates\starter\vite.config.ts", false), + ] { + assert_eq!(config.is_enabled(Path::new(path)), enabled, "{path}"); + } + } + + #[test] + fn rejects_invalid_patterns() { + let result = ImportRuleConfig::from_json( + r#"{ + "enabled": true, + "overrides": [{ "files": ["src/*.{ts"], "excludeFiles": [], "enabled": false }] + }"#, + ); + assert!(result.unwrap_err().to_string().contains("Invalid lint glob")); + } +} diff --git a/crates/vp_migration/src/lib.rs b/crates/vp_migration/src/lib.rs index 00ced33720..0731edc7e9 100644 --- a/crates/vp_migration/src/lib.rs +++ b/crates/vp_migration/src/lib.rs @@ -11,6 +11,7 @@ mod eslint; mod file_walker; mod import_merge; mod import_rewriter; +mod import_rule; mod pack_config; mod package; mod prettier; @@ -25,6 +26,7 @@ pub use import_rewriter::{ BatchRewriteResult, RewriteImportsOptions, rewrite_imports_in_directory, rewrite_imports_in_directory_with_options, }; +pub use import_rule::ImportRuleConfig; pub use package::{rewrite_eslint, rewrite_prettier, rewrite_scripts}; pub use shell_command::{LiteralCommandToken, parse_literal_commands}; pub use source_analysis::analyze_migration_source; diff --git a/crates/vp_static_config/src/lib.rs b/crates/vp_static_config/src/lib.rs index d79c2d1a06..12e34c2c4c 100644 --- a/crates/vp_static_config/src/lib.rs +++ b/crates/vp_static_config/src/lib.rs @@ -18,7 +18,8 @@ use rustc_hash::FxHashMap; use vt_path::AbsolutePath; /// Packages whose `defineConfig` helpers preserve top-level config fields. -const TRUSTED_DEFINE_CONFIG_PACKAGES: &[&str] = &["vite-plus", "vite"]; +const TRUSTED_DEFINE_CONFIG_PACKAGES: &[&str] = + &["vite-plus", "vite", "vitest/config", "vite-plus/test/config"]; /// The name of the config helper static extraction trusts. const DEFINE_CONFIG: &str = "defineConfig"; @@ -174,6 +175,13 @@ fn extract_config_fields(program: &Program<'_>) -> FieldMap { // ESM: export default ... if let Statement::ExportDefaultDeclaration(decl) = stmt { if let Some(expr) = decl.declaration.as_expression() { + if let Expression::Identifier(id) = expr.get_inner_expression() { + return extract_exported_const( + program, + id.name.as_str(), + has_trusted_define_config_binding, + ); + } return extract_config_from_expr(expr, has_trusted_define_config_binding); } // export default class/function — not analyzable @@ -195,6 +203,36 @@ fn extract_config_fields(program: &Program<'_>) -> FieldMap { FieldMap::unanalyzable() } +// A config can be assigned to a const before exporting it. Only accept the +// imports/declaration/export shape: other statements could mutate that object +// or pass it to code that does, so those require normal runtime resolution. +fn extract_exported_const( + program: &Program<'_>, + name: &str, + has_trusted_define_config_binding: bool, +) -> FieldMap { + let mut initializer = None; + for stmt in &program.body { + match stmt { + Statement::ImportDeclaration(_) | Statement::ExportDefaultDeclaration(_) => {} + Statement::VariableDeclaration(decl) + if decl.kind == VariableDeclarationKind::Const && decl.declarations.len() == 1 => + { + let declarator = &decl.declarations[0]; + if !matches!(&declarator.id, BindingPattern::BindingIdentifier(id) if id.name == name) + { + return FieldMap::unanalyzable(); + } + initializer = declarator.init.as_ref(); + } + _ => return FieldMap::unanalyzable(), + } + } + initializer.map_or_else(FieldMap::unanalyzable, |expr| { + extract_config_from_expr(expr, has_trusted_define_config_binding) + }) +} + /// Extract the config object from an expression that is either: /// - `defineConfig({ ... })` → extract the object argument /// - `defineConfig(() => ({ ... }))` → extract from arrow function expression body @@ -752,6 +790,52 @@ mod tests { assert_non_static(&result, "run"); } + #[test] + fn vitest_config_preserves_lint_fields_without_evaluating_plugins() { + for import in [ + "import { defineConfig } from 'vitest/config';", + "import { defineConfig } from 'vite-plus/test/config';", + ] { + let source = [ + import, + "export default defineConfig({ + plugins: [plugin()], + lint: { rules: { 'vite-plus/prefer-vite-plus-imports': 'off' } }, + });", + ] + .join("\n"); + let result = parse(&source); + assert_json( + &result, + "lint", + serde_json::json!({ "rules": { "vite-plus/prefer-vite-plus-imports": "off" } }), + ); + assert!(result.get("run").is_none()); + } + } + + #[test] + fn exported_const_config_is_static_only_without_possible_mutations() { + let source = "import { defineConfig } from 'vitest/config'; + const config = defineConfig({ lint: { rules: { 'vite-plus/prefer-vite-plus-imports': 'off' } } });"; + let result = parse(&[source, "export default config;"].join("\n")); + assert_json( + &result, + "lint", + serde_json::json!({ "rules": { "vite-plus/prefer-vite-plus-imports": "off" } }), + ); + assert!(result.get("run").is_none()); + for mutation in [ + "config.lint = {};", + "Object.assign(config, other);", + "const changed = mutate(config);", + ] { + let result = parse(&[source, mutation, "export default config;"].join("\n")); + assert_non_static(&result, "lint"); + } + assert_non_static(&parse("let config = {}; export default config;"), "lint"); + } + #[test] fn define_config_type_import_from_vite_plus_is_non_static() { let result = parse( diff --git a/docs/guide/migrate.md b/docs/guide/migrate.md index 9c8911ce6c..eb762a6a77 100644 --- a/docs/guide/migrate.md +++ b/docs/guide/migrate.md @@ -61,6 +61,37 @@ Most projects will require further manual adjustments after running `vp migrate` For the Vitest v5 upgrade, read the [compatibility settings and review checklist](./vitest-v5.md) and the [upstream Vitest migration guide](https://vitest.dev/guide/migration/). The preflight checks your original runner version and Node runtime before dependency updates. Keep the original lockfile available and resolve blocking findings before retrying. +### Keeping upstream imports in templates and examples + +Disable `vite-plus/prefer-vite-plus-imports` for selected files in the root +`vite.config.ts`: + +```ts +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + lint: { + overrides: [ + { + files: ['templates/**', 'examples/**'], + rules: { + 'vite-plus/prefer-vite-plus-imports': 'off', + }, + }, + ], + }, +}); +``` + +`vp migrate` uses the same rule settings for its import rewrite step. The override +also applies to `vp lint --fix` and `vp check --fix`. Patterns use Oxlint's glob +syntax, relative to the root config directory. Later matching overrides can +re-enable the rule. + +This controls import rewrites only. Migration still updates dependencies, scripts, +configs, shared catalogs, and package-manager overrides. Other lint rules remain +enabled. There is no separate migration exclusion setting. + ## Recommended Workflow Before running the migration: diff --git a/packages/cli/binding/index.cjs b/packages/cli/binding/index.cjs index dbdf014927..472be475d9 100644 --- a/packages/cli/binding/index.cjs +++ b/packages/cli/binding/index.cjs @@ -1121,6 +1121,7 @@ module.exports.parseHooksArgs = nativeBinding.parseHooksArgs; module.exports.parseLiteralCommands = nativeBinding.parseLiteralCommands; module.exports.parseMigrateArgs = nativeBinding.parseMigrateArgs; module.exports.parseStagedArgs = nativeBinding.parseStagedArgs; +module.exports.readStaticLintConfig = nativeBinding.readStaticLintConfig; module.exports.removeConfigKey = nativeBinding.removeConfigKey; module.exports.resolveNpmAuthorization = nativeBinding.resolveNpmAuthorization; module.exports.resolveNpmRegistry = nativeBinding.resolveNpmRegistry; diff --git a/packages/cli/binding/index.d.cts b/packages/cli/binding/index.d.cts index 42d608349c..870ead46db 100644 --- a/packages/cli/binding/index.d.cts +++ b/packages/cli/binding/index.d.cts @@ -3983,6 +3983,9 @@ export interface PathAccess { readDir: boolean; } +/** JSON for a static lint block; undefined requests normal config resolution. */ +export declare function readStaticLintConfig(root: string): string | null; + /** Remove a top-level key from a recognized Vite config object. */ export declare function removeConfigKey( viteConfigPath: string, @@ -4044,6 +4047,7 @@ export declare function rewriteImportsInDirectory( root: string, preserveVitestInNuxtPackages?: boolean | undefined | null, oxlintOwnerDirs?: Array | undefined | null, + importRuleConfig?: string | undefined | null, ): BatchRewriteResult; /** diff --git a/packages/cli/binding/src/migration.rs b/packages/cli/binding/src/migration.rs index aa0901e775..c9ba983a25 100644 --- a/packages/cli/binding/src/migration.rs +++ b/packages/cli/binding/src/migration.rs @@ -3,6 +3,18 @@ use std::path::Path; use napi::{anyhow, bindgen_prelude::*}; use napi_derive::napi; +/// JSON for a static lint block; undefined requests normal config resolution. +#[napi] +pub fn read_static_lint_config(root: String) -> Result> { + let root = vt_path::AbsolutePathBuf::new(root.into()) + .ok_or_else(|| Error::from_reason("Migration root must be absolute"))?; + Ok(match vp_static_config::resolve_static_config(&root).get("lint") { + Some(vp_static_config::FieldValue::Json(value)) => Some(value.to_string()), + Some(vp_static_config::FieldValue::NonStatic) => None, + None => Some("null".to_string()), + }) +} + /// A literal argument with its original JavaScript UTF-16 source range. #[napi(object)] pub struct LiteralCommandToken { @@ -367,10 +379,16 @@ pub fn rewrite_imports_in_directory( root: String, preserve_vitest_in_nuxt_packages: Option, oxlint_owner_dirs: Option>, + import_rule_config: Option, ) -> Result { let result = vp_migration::rewrite_imports_in_directory_with_options( Path::new(&root), vp_migration::RewriteImportsOptions { + import_rule: import_rule_config + .map(|json| vp_migration::ImportRuleConfig::from_json(&json)) + .transpose() + .map_err(anyhow::Error::from)? + .unwrap_or_default(), preserve_vitest_in_nuxt_packages: preserve_vitest_in_nuxt_packages.unwrap_or(false), oxlint_owner_dirs: oxlint_owner_dirs .unwrap_or_default() diff --git a/packages/cli/src/migration/__tests__/import-rule.spec.ts b/packages/cli/src/migration/__tests__/import-rule.spec.ts new file mode 100644 index 0000000000..64cbab382d --- /dev/null +++ b/packages/cli/src/migration/__tests__/import-rule.spec.ts @@ -0,0 +1,101 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { resolveViteConfig } from '../../resolve-vite-config.ts'; +import { resolveImportRuleConfig, rewriteAllImports } from '../migrator.ts'; + +vi.mock('../../resolve-vite-config.ts', () => ({ resolveViteConfig: vi.fn() })); + +describe('migration import rule settings', () => { + let root: string; + const rule = 'vite-plus/prefer-vite-plus-imports'; + const write = (filename: string, content: string) => { + const file = path.join(root, filename); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, content); + }; + const read = (filename: string) => fs.readFileSync(path.join(root, filename), 'utf8'); + const rewrite = async () => + rewriteAllImports(root, true, undefined, true, [], await resolveImportRuleConfig(root)); + const config = (lint: unknown) => + write('vite.config.ts', `export default ${JSON.stringify({ lint })};`); + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-import-rule-')); + write('package.json', '{"private":true}'); + write('unit.test.ts', "import { test } from 'vitest';"); + }); + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }); + vi.resetAllMocks(); + }); + + it.each(['off', 0, ['off'], [0], 'allow'].map((severity) => [severity]))( + 'honors disabled severity %j', + async (severity) => { + config({ rules: { [rule]: severity } }); + expect(await rewrite()).toBe(false); + expect(read('unit.test.ts')).toContain("from 'vitest'"); + expect(resolveViteConfig).not.toHaveBeenCalled(); + }, + ); + + it.each([undefined, 'warn', 'error', 1, 2, ['warn'], [2], 'deny'].map((severity) => [severity]))( + 'keeps the default rewrite for severity %j', + async (severity) => { + config({ rules: { [rule]: severity } }); + expect(await rewrite()).toBe(true); + expect(read('unit.test.ts')).toContain("from 'vite-plus/test'"); + }, + ); + + it('merges inherited rules before applying ordered overrides', async () => { + config({ + extends: [ + { + rules: { [rule]: 'off' }, + overrides: [{ files: ['templates/**'], rules: { [rule]: 0 } }], + }, + { rules: { [rule]: 'error' } }, + ], + overrides: [ + { files: ['templates/managed/**'], rules: { [rule]: ['warn'] } }, + { files: ['templates/**'], rules: { 'no-debugger': 'error' } }, + ], + }); + for (const file of ['templates/starter/test.ts', 'templates/managed/test.ts']) { + write(file, "export { test } from 'vitest';"); + } + expect(await rewrite()).toBe(true); + expect(read('unit.test.ts')).toContain("from 'vite-plus/test'"); + expect(read('templates/starter/test.ts')).toContain("from 'vitest'"); + expect(read('templates/managed/test.ts')).toContain("from 'vite-plus/test'"); + expect(await rewrite()).toBe(false); + }); + + it('resolves dynamic lint config when static extraction cannot read it', async () => { + write('vite.config.ts', 'export default () => getConfig();'); + vi.mocked(resolveViteConfig, { partial: true }).mockResolvedValue({ + lint: { rules: { [rule]: 'off' } }, + }); + expect(await rewrite()).toBe(false); + expect(resolveViteConfig).toHaveBeenCalledWith(root); + expect(read('unit.test.ts')).toContain("from 'vitest'"); + }); + + it('reads JSON config inheritance before the config migration removes it', async () => { + write('base.json', JSON.stringify({ rules: { [rule]: 'off' } })); + write('.oxlintrc.json', '{"extends":["./base.json"]}'); + expect(await rewrite()).toBe(false); + expect(read('unit.test.ts')).toContain("from 'vitest'"); + }); + + it('fails on an invalid explicit rule setting', async () => { + config({ rules: { [rule]: 'disabled' } }); + await expect(rewrite()).rejects.toThrow('Invalid severity'); + expect(read('unit.test.ts')).toContain("from 'vitest'"); + }); +}); diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index de19f1893e..444339740e 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -85,6 +85,7 @@ import { configureYarnNodeModulesMode, rewriteMonorepo, rewriteStandaloneProject, + resolveImportRuleConfig, planVitestV5Migration, refreshVitestV5Migration, retryPreservedOxlintConfigs, @@ -827,6 +828,8 @@ async function executeMigrationPlan( }, ); + const importRuleConfig = await resolveImportRuleConfig(workspaceInfo.rootDir); + // 4. Check vite and vitest version is supported by migration updateMigrationProgress('Validating toolchain'); const isViteSupported = checkViteVersion(workspaceInfo.rootDir); @@ -920,7 +923,7 @@ async function executeMigrationPlan( // 7. Rewrite configs updateMigrationProgress('Rewriting configs'); if (workspaceInfo.isMonorepo) { - rewriteMonorepo(workspaceInfo, skipStagedMigration, true, report); + rewriteMonorepo(workspaceInfo, skipStagedMigration, true, report, importRuleConfig); } else { rewriteStandaloneProject( workspaceInfo.rootDir, @@ -928,6 +931,7 @@ async function executeMigrationPlan( skipStagedMigration, true, report, + importRuleConfig, ); } @@ -1213,11 +1217,13 @@ async function main() { // so surface it first instead of leaving the spinner silent. The spinner paints // this message synchronously on start(), before the walk begins. updateMigrationProgress('Rewriting toolchain imports across the workspace'); + const importRuleConfig = await resolveImportRuleConfig(workspaceInfoOptional.rootDir); const coreMigrationResult = finalizeCoreMigrationForExistingVitePlus( workspaceInfoOptional, true, report, pendingCoreMigration, + importRuleConfig, ); if ( coreMigrationResult.dependencies || diff --git a/packages/cli/src/migration/migrator/core-finalization.ts b/packages/cli/src/migration/migrator/core-finalization.ts index d57f2b556a..a42053ccde 100644 --- a/packages/cli/src/migration/migrator/core-finalization.ts +++ b/packages/cli/src/migration/migrator/core-finalization.ts @@ -147,6 +147,7 @@ export function finalizeCoreMigrationForExistingVitePlus( silent = false, report?: MigrationReport, pending = detectPendingCoreMigration(workspaceInfo), + importRuleConfig?: string, ): CoreMigrationFinalizationResult { const projectPaths = getCoreMigrationProjectPaths(workspaceInfo); const result: CoreMigrationFinalizationResult = { @@ -172,7 +173,14 @@ export function finalizeCoreMigrationForExistingVitePlus( } } - result.imports = rewriteAllImports(workspaceInfo.rootDir, silent, report, true); + result.imports = rewriteAllImports( + workspaceInfo.rootDir, + silent, + report, + true, + [], + importRuleConfig, + ); result.dependencies = dropDeadOxlintPluginsDependency( workspaceInfo.rootDir, workspaceInfo.packages, diff --git a/packages/cli/src/migration/migrator/orchestrators.ts b/packages/cli/src/migration/migrator/orchestrators.ts index 3e515708e5..bf4aed2879 100644 --- a/packages/cli/src/migration/migrator/orchestrators.ts +++ b/packages/cli/src/migration/migrator/orchestrators.ts @@ -76,6 +76,7 @@ export function rewriteStandaloneProject( skipStagedMigration?: boolean, silent = false, report?: MigrationReport, + importRuleConfig?: string, ): void { const packageJsonPath = path.join(projectPath, 'package.json'); if (!fs.existsSync(packageJsonPath)) { @@ -349,7 +350,7 @@ export function rewriteStandaloneProject( injectFmtDefaults(projectPath, silent, report); mergeTsdownConfigFile(projectPath, silent, report); // rewrite imports in all TypeScript/JavaScript files before lazy plugin import merging - rewriteAllImports(projectPath, silent, report, true, oxlintOwnerDirs); + rewriteAllImports(projectPath, silent, report, true, oxlintOwnerDirs, importRuleConfig); dropDeadOxlintPluginsDependency(projectPath, workspaceInfo.packages, originalOxlintDependencies); wrapLazyPluginsInViteConfig(projectPath, silent, report); migrateTaskCacheConfigInViteConfig(projectPath, silent, report); @@ -364,6 +365,7 @@ export function rewriteMonorepo( skipStagedMigration?: boolean, silent = false, report?: MigrationReport, + importRuleConfig?: string, ): void { const catalogDependencyResolver = createCatalogDependencyResolver( workspaceInfo.rootDir, @@ -514,7 +516,7 @@ export function rewriteMonorepo( injectFmtDefaults(workspaceInfo.rootDir, silent, report); mergeTsdownConfigFile(workspaceInfo.rootDir, silent, report); // rewrite imports in all TypeScript/JavaScript files before lazy plugin import merging - rewriteAllImports(workspaceInfo.rootDir, silent, report, true, oxlintOwnerDirs); + rewriteAllImports(workspaceInfo.rootDir, silent, report, true, oxlintOwnerDirs, importRuleConfig); dropDeadOxlintPluginsDependency( workspaceInfo.rootDir, workspaceInfo.packages, diff --git a/packages/cli/src/migration/migrator/vite-config.ts b/packages/cli/src/migration/migrator/vite-config.ts index 96e6e6d46f..df21c40394 100644 --- a/packages/cli/src/migration/migrator/vite-config.ts +++ b/packages/cli/src/migration/migrator/vite-config.ts @@ -12,6 +12,7 @@ import { mergeJsonConfig, mergeTsdownConfig, migrateTaskCacheConfig, + readStaticLintConfig, rewriteImportsInDirectory, rewriteScripts, wrapLazyPlugins, @@ -886,6 +887,65 @@ export function migrateTaskCacheConfigInViteConfig( return true; } +function importRuleEnabled(rule: unknown): boolean | undefined { + const severity = Array.isArray(rule) ? rule[0] : rule; + if (severity === undefined) { + return undefined; + } + if (severity === 'off' || severity === 'allow' || severity === 0) { + return false; + } + if ( + severity === 1 || + severity === 2 || + severity === 'warn' || + severity === 'error' || + severity === 'deny' + ) { + return true; + } + throw new Error(`Invalid severity for ${PREFER_VITE_PLUS_IMPORTS_RULE}: ${JSON.stringify(rule)}`); +} + +/** Read the import rule before migration merges or removes lint configs. */ +export async function resolveImportRuleConfig(projectPath: string): Promise { + const extracted = readStaticLintConfig(projectPath); + let lint: OxlintConfig | undefined; + if (extracted == null) { + const { resolveViteConfig } = await import('../../resolve-vite-config.ts'); + lint = (await resolveViteConfig(projectPath)).lint; + } else { + lint = JSON.parse(extracted); + } + if (!lint) { + const filename = detectConfigs(projectPath).oxlintConfig; + if (filename) { + try { + lint = resolveOxlintConfigExtends(path.join(projectPath, filename)).config; + } catch { + // The config migration reports and preserves unresolved JSON configs. + // Keep its existing error handling instead of aborting unrelated steps. + } + } + } + + const layers = lint ? oxlintConfigLayers(lint) : []; + const enabled = layers.reduce( + (previous, layer) => + importRuleEnabled(layer.rules?.[PREFER_VITE_PLUS_IMPORTS_RULE]) ?? previous, + true, + ); + const overrides = layers.flatMap((layer) => + (layer.overrides ?? []).flatMap((override) => { + const enabled = importRuleEnabled(override.rules?.[PREFER_VITE_PLUS_IMPORTS_RULE]); + return enabled === undefined + ? [] + : [{ files: override.files, excludeFiles: override.excludeFiles ?? [], enabled }]; + }), + ); + return JSON.stringify({ enabled, overrides }); +} + /** * Rewrite imports in all TypeScript/JavaScript files under a directory * This rewrites vite/vitest imports to @voidzero-dev/vite-plus @@ -900,8 +960,14 @@ export function rewriteAllImports( // `rewritePackageJson` stripped `oxlint` from their manifests. See // `collectOxlintOwnerDirs`. oxlintOwnerDirs: string[] = [], + importRuleConfig?: string, ): boolean { - const result = rewriteImportsInDirectory(projectPath, preserveNuxtVitestImports, oxlintOwnerDirs); + const result = rewriteImportsInDirectory( + projectPath, + preserveNuxtVitestImports, + oxlintOwnerDirs, + importRuleConfig, + ); const modified = result.modifiedFiles.length; const preserved = result.preservedVitestFiles.length; const errors = result.errors.length;