From e1bc4b782d4ac85fd1f210654a5f8097acede8f1 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 10 Nov 2026 13:36:24 +0800 Subject: [PATCH 1/4] feat(migrate): add migrate.ignorePatterns --- .../app.test.ts | 2 + .../examples/vite.config.ts | 2 + .../package.json | 1 + .../snapshots.toml | 10 + .../lint_vite_plus_imports_overrides.md | 44 ++++ .../templates/starter.test.ts | 3 + .../vite.config.ts | 13 ++ .../examples/standalone/package.json | 1 + .../examples/standalone/unit.test.ts | 2 + .../migration_ignore_patterns/package.json | 7 + .../packages/app/package.json | 5 + .../packages/app/unit.test.ts | 2 + .../packages/app/vite.config.ts | 2 + .../pnpm-workspace.yaml | 12 + .../migration_ignore_patterns/snapshots.toml | 126 ++++++++++ ...tion_ignore_patterns_baseurl_references.md | 43 ++++ ...gration_ignore_patterns_explicit_target.md | 27 +++ .../migration_ignore_patterns_fresh.md | 114 +++++++++ .../migration_ignore_patterns_full.md | 164 +++++++++++++ .../migration_ignore_patterns_invalid.md | 28 +++ ...ation_ignore_patterns_override_conflict.md | 47 ++++ ...gration_ignore_patterns_parent_negation.md | 35 +++ ...igration_ignore_patterns_runtime_config.md | 39 ++++ .../migration_ignore_patterns_upgrade.md | 165 +++++++++++++ .../templates/managed/package.json | 1 + .../templates/managed/unit.test.ts | 2 + .../templates/starter/package.json | 5 + .../templates/starter/tsconfig.json | 1 + .../templates/starter/unit.test.ts | 3 + .../templates/starter/vite.config.ts | 2 + .../migration_ignore_patterns/vite.config.ts | 8 + .../examples/starter/package.json | 14 ++ .../examples/starter/src/example.test.ts | 5 + .../examples/starter/tsconfig.json | 5 + .../examples/starter/vite.config.ts | 3 + .../package.json | 15 ++ .../packages/app/package.json | 11 + .../packages/app/vite.config.ts | 3 + .../pnpm-workspace.yaml | 8 + .../snapshots.toml | 14 ++ .../migration_workspace_vite_template.md | 132 +++++++++++ crates/vp_migration/src/file_walker.rs | 68 ++++++ crates/vp_migration/src/import_rewriter.rs | 9 +- crates/vp_migration/src/lib.rs | 2 +- crates/vp_static_config/src/lib.rs | 82 ++++++- docs/guide/migrate.md | 61 +++++ packages/cli/binding/index.cjs | 2 + packages/cli/binding/index.d.cts | 10 + packages/cli/binding/src/migration.rs | 35 +++ packages/cli/src/define-config.ts | 25 +- packages/cli/src/index.ts | 2 + packages/cli/src/migrate-config.ts | 21 ++ packages/cli/src/migration-ignore.ts | 7 + .../__tests__/ignore-patterns.spec.ts | 162 +++++++++++++ packages/cli/src/migration/bin.ts | 35 +++ packages/cli/src/migration/files.ts | 113 +++++++++ packages/cli/src/migration/format.ts | 25 +- packages/cli/src/migration/migrator/README.md | 7 + .../cli/src/migration/migrator/catalog.ts | 18 +- .../migration/migrator/core-finalization.ts | 2 +- packages/cli/src/migration/migrator/eslint.ts | 45 ++-- .../src/migration/migrator/framework-shim.ts | 8 +- .../cli/src/migration/migrator/git-hooks.ts | 13 +- .../src/migration/migrator/orchestrators.ts | 2 +- .../src/migration/migrator/oxlint-config.ts | 2 +- .../cli/src/migration/migrator/prettier.ts | 12 +- .../src/migration/migrator/script-runner.ts | 4 +- packages/cli/src/migration/migrator/setup.ts | 4 +- .../cli/src/migration/migrator/source-scan.ts | 9 +- .../cli/src/migration/migrator/tsconfig.ts | 9 +- packages/cli/src/migration/migrator/tsup.ts | 17 +- .../cli/src/migration/migrator/vite-config.ts | 35 ++- .../migration/migrator/vite-plus-bootstrap.ts | 13 +- .../migration/migrator/vitest-ecosystem.ts | 74 +----- .../cli/src/migration/migrator/vitest-v5.ts | 6 +- packages/cli/src/migration/migrator/yarn.ts | 6 +- packages/cli/src/migration/scope.ts | 218 ++++++++++++++++++ packages/cli/src/oxlint-plugin.ts | 40 ++++ packages/cli/src/utils/agent.ts | 5 + packages/cli/src/utils/editor.ts | 10 +- packages/cli/src/utils/package-overrides.ts | 68 ++++++ 81 files changed, 2285 insertions(+), 127 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/app.test.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/examples/vite.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/snapshots/lint_vite_plus_imports_overrides.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/templates/starter.test.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/lint_vite_plus_imports_overrides/vite.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/examples/standalone/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/examples/standalone/unit.test.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/packages/app/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/packages/app/unit.test.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/packages/app/vite.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/pnpm-workspace.yaml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_baseurl_references.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_explicit_target.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_fresh.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_full.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_invalid.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_override_conflict.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_parent_negation.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_runtime_config.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_upgrade.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/managed/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/managed/unit.test.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/tsconfig.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/unit.test.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/vite.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/vite.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/src/example.test.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/tsconfig.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/examples/starter/vite.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/packages/app/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/packages/app/vite.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/pnpm-workspace.yaml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/snapshots/migration_workspace_vite_template.md create mode 100644 packages/cli/src/migrate-config.ts create mode 100644 packages/cli/src/migration-ignore.ts create mode 100644 packages/cli/src/migration/__tests__/ignore-patterns.spec.ts create mode 100644 packages/cli/src/migration/files.ts create mode 100644 packages/cli/src/migration/scope.ts 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_ignore_patterns/examples/standalone/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/examples/standalone/package.json new file mode 100644 index 0000000000..14a01f696b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/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_ignore_patterns/examples/standalone/unit.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/examples/standalone/unit.test.ts new file mode 100644 index 0000000000..fee5fb1242 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/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_ignore_patterns/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/package.json new file mode 100644 index 0000000000..74a3099faf --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/package.json @@ -0,0 +1,7 @@ +{ + "name": "migration-ignore-patterns", + "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_ignore_patterns/packages/app/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/packages/app/package.json new file mode 100644 index 0000000000..6c0d9255f6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/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_ignore_patterns/packages/app/unit.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/packages/app/unit.test.ts new file mode 100644 index 0000000000..43ccd6e1ae --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/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_ignore_patterns/packages/app/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/packages/app/vite.config.ts new file mode 100644 index 0000000000..0b79ce8fee --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/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_ignore_patterns/pnpm-workspace.yaml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/pnpm-workspace.yaml new file mode 100644 index 0000000000..e7f5c119eb --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/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_ignore_patterns/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots.toml new file mode 100644 index 0000000000..e5aaeafc16 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots.toml @@ -0,0 +1,126 @@ +[[case]] +name = "migration_ignore_patterns_upgrade" +vp = "local" +comment = "Preserve templates and their shared catalogs and overrides, re-include one template, and leave non-workspace examples alone." +steps = [ + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, + { argv = ["vpt", "print-file", "templates/starter/package.json"] }, + { argv = ["vpt", "print-file", "templates/starter/vite.config.ts"] }, + { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "templates/starter/tsconfig.json"] }, + { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, + { argv = ["vpt", "print-file", "packages/app/package.json"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, + { argv = ["vpt", "print-file", "pnpm-workspace.yaml"] }, + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "Rerunning uses the same policy and explicit managed dependency specs." }, + { 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, comment = "Ignored imports remain upstream; other rules still report the template's debugger statement." }, + { 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_ignore_patterns_fresh" +vp = "local" +comment = "Initial migration honors the same exclusions." +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/package.json"] }, + { argv = ["vpt", "print-file", "templates/starter/vite.config.ts"] }, + { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "templates/starter/tsconfig.json"] }, + { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, + { argv = ["vpt", "print-file", "packages/app/package.json"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, + { argv = ["vpt", "print-file", "pnpm-workspace.yaml"] }, +] + +[[case]] +name = "migration_ignore_patterns_override_conflict" +vp = "local" +comment = "A conflicting global override is reported before project files change." +steps = [ + { argv = ["vpt", "write-file", "pnpm-workspace.yaml", "packages: ['packages/*', 'templates/*']\noverrides:\n vite: ^8.0.0\n"] }, + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "packages/app/package.json"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, + { argv = ["vpt", "print-file", "pnpm-workspace.yaml"] }, +] + + +[[case]] +name = "migration_ignore_patterns_full" +vp = "local" +comment = "Full setup honors exclusions, and vp check --fix keeps the import policy." +steps = [ + { argv = ["vp", "migrate", "--full", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, + { argv = ["vpt", "print-file", "templates/starter/package.json"] }, + { argv = ["vpt", "print-file", "templates/starter/vite.config.ts"] }, + { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "templates/starter/tsconfig.json"] }, + { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, + { argv = ["vpt", "print-file", "packages/app/package.json"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, + { argv = ["vpt", "print-file", "pnpm-workspace.yaml"] }, + { argv = ["vp", "migrate", "--full", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "Rerunning uses the same policy and explicit managed dependency specs." }, + { 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, comment = "Ignored imports remain upstream; other rules still report the template's debugger statement." }, + { 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_ignore_patterns_parent_negation" +vp = "local" +steps = [ + { argv = ["vpt", "write-file", "vite.config.ts", "export default { migrate: { ignorePatterns: ['templates/', '!templates/managed/unit.test.ts'] } };\n"] }, + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, + { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, + { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, +] + +[[case]] +name = "migration_ignore_patterns_invalid" +vp = "local" +steps = [ + { argv = ["vpt", "write-file", "vite.config.ts", "export default { migrate: { ignorePatterns: 'templates/' } };\n"] }, + { argv = ["vp", "migrate", "--no-interactive"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "packages/app/package.json"], continue-on-failure = true }, +] + +[[case]] +name = "migration_ignore_patterns_runtime_config" +vp = "local" +comment = "A runtime config export retains the policy, and the root config can be a plain object." +steps = [ + { argv = ["vpt", "write-file", "vite.config.ts", "const excluded = ['templates/*', '!templates/managed/'];\nexport default () => ({ migrate: { ignorePatterns: excluded } });\n"] }, + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, + { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"] }, + { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, +] + +[[case]] +name = "migration_ignore_patterns_explicit_target" +vp = "local" +comment = "An explicit non-workspace template target must not bypass the workspace policy." +steps = [ + { argv = ["vp", "migrate", "examples/standalone", "--full", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "examples/standalone/package.json"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, +] + +[[case]] +name = "migration_ignore_patterns_baseurl_references" +vp = "local" +comment = "The external baseUrl fixer follows references and extends; skip it so excluded configs stay unchanged." +steps = [ + { argv = ["vpt", "write-file", "tsconfig.json", "{ \"compilerOptions\": { \"baseUrl\": \".\" }, \"references\": [{ \"path\": \"./templates/starter\" }] }\n"] }, + { argv = ["vpt", "write-file", "templates/starter/tsconfig.json", "{ \"extends\": \"../../tsconfig.json\", \"compilerOptions\": { \"baseUrl\": \".\", \"paths\": { \"@/*\": [\"src/*\"] } } }\n"] }, + { argv = ["vp", "migrate", "--full", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, + { argv = ["vpt", "print-file", "templates/starter/tsconfig.json"] }, + { argv = ["vpt", "print-file", "tsconfig.json"] }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_baseurl_references.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_baseurl_references.md new file mode 100644 index 0000000000..30d48e1f1d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_baseurl_references.md @@ -0,0 +1,43 @@ +# migration_ignore_patterns_baseurl_references + +The external baseUrl fixer follows references and extends; skip it so excluded configs stay unchanged. + +## `vpt write-file tsconfig.json '{ "compilerOptions": { "baseUrl": "." }, "references": [{ "path": "./templates/starter" }] } +'` + +``` +``` + +## `vpt write-file templates/starter/tsconfig.json '{ "extends": "../../tsconfig.json", "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } } +'` + +``` +``` + +## `vp migrate --full --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 +! Warnings: + - Skipped automatic tsconfig baseUrl removal because its external fixer cannot honor migrate.ignorePatterns. Remove baseUrl manually in migrated projects to enable type-aware linting. +``` + +## `vpt print-file templates/starter/tsconfig.json` + +``` +{ "extends": "../../tsconfig.json", "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } } +``` + +## `vpt print-file tsconfig.json` + +``` +{ "compilerOptions": { "baseUrl": "." }, "references": [{ "path": "./templates/starter" }] } +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_explicit_target.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_explicit_target.md new file mode 100644 index 0000000000..902581add3 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_explicit_target.md @@ -0,0 +1,27 @@ +# migration_ignore_patterns_explicit_target + +An explicit non-workspace template target must not bypass the workspace policy. + +## `vp migrate examples/standalone --full --no-interactive --no-hooks --no-agent --no-editor` + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +■ Cannot migrate examples/standalone: excluded by migrate.ignorePatterns in /vite.config.ts. No project files were changed. +Error: Cannot migrate examples/standalone: excluded by migrate.ignorePatterns in /vite.config.ts. No project files were changed. +``` + +## `vpt print-file examples/standalone/package.json` + +``` +{ "name": "standalone-template", "private": true, "type": "module", "devDependencies": { "vite": "^8.0.0", "vitest": "^4.1.11" } } +``` + +## `vpt print-file examples/standalone/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +test('outside-workspace', () => expect(1).toBe(1)); +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_fresh.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_fresh.md new file mode 100644 index 0000000000..fc01414323 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_fresh.md @@ -0,0 +1,114 @@ +# migration_ignore_patterns_fresh + +Initial migration honors the same exclusions. + +## `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 +• 3 files had imports rewritten +``` + +## `vpt print-file templates/starter/package.json` + +``` +{ + "name": "starter", "private": true, "type": "module", + "scripts": { "build": "vite build", "test": "vitest run" }, + "devDependencies": { "vite": "catalog:", "vitest": "catalog:legacy", "@vitest/coverage-v8": "catalog:legacy" } +} +``` + +## `vpt print-file templates/starter/vite.config.ts` + +``` +import { defineConfig } from 'vite'; +export default defineConfig({test: {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/starter/tsconfig.json` + +``` +{ "compilerOptions": { "types": ["vite/client", "vitest/globals"], "esModuleInterop": false } } +``` + +## `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/package.json` + +``` +{ + "name": "app", + "private": true, + "type": "module", + "scripts": { + "build": "vp build", + "test": "vp test run" + }, + "devDependencies": { + "vite": "npm:@voidzero-dev/vite-plus-core@", + "vitest": "", + "@vitest/coverage-v8": "", + "vite-plus": "" + } +} +``` + +## `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/managed/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('included', () => expect(1).toBe(1)); +``` + +## `vpt print-file pnpm-workspace.yaml` + +``` +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 +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: '*' + vitest: '*' +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_full.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_full.md new file mode 100644 index 0000000000..12c5eddb3d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_full.md @@ -0,0 +1,164 @@ +# migration_ignore_patterns_full + +Full setup honors exclusions, and vp check --fix keeps the import policy. + +## `vp migrate --full --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/package.json` + +``` +{ + "name": "starter", "private": true, "type": "module", + "scripts": { "build": "vite build", "test": "vitest run" }, + "devDependencies": { "vite": "catalog:", "vitest": "catalog:legacy", "@vitest/coverage-v8": "catalog:legacy" } +} +``` + +## `vpt print-file templates/starter/vite.config.ts` + +``` +import { defineConfig } from 'vite'; +export default defineConfig({test: {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/starter/tsconfig.json` + +``` +{ "compilerOptions": { "types": ["vite/client", "vitest/globals"], "esModuleInterop": false } } +``` + +## `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/package.json` + +``` +{ + "name": "app", + "private": true, + "type": "module", + "scripts": { + "build": "vp build", + "test": "vp test run" + }, + "devDependencies": { + "vite": "npm:@voidzero-dev/vite-plus-core@", + "vitest": "", + "@vitest/coverage-v8": "" + } +} +``` + +## `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/managed/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('included', () => expect(1).toBe(1)); +``` + +## `vpt print-file pnpm-workspace.yaml` + +``` +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 +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: '*' + vitest: '*' +``` + +## `vp migrate --full --no-interactive --no-hooks --no-agent --no-editor` + +Rerunning uses the same policy and explicit managed dependency specs. + +``` +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 check --no-fmt --fix templates/starter/unit.test.ts packages/app/unit.test.ts` + +Ignored imports remain upstream; other rules still report the template's debugger statement. + +**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_ignore_patterns/snapshots/migration_ignore_patterns_invalid.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_invalid.md new file mode 100644 index 0000000000..87c5f49b0a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_invalid.md @@ -0,0 +1,28 @@ +# migration_ignore_patterns_invalid + +## `vpt write-file vite.config.ts 'export default { migrate: { ignorePatterns: '\''templates/'\'' } }; +'` + +``` +``` + +## `vp migrate --no-interactive` + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +■ migrate.ignorePatterns must be an array of strings. +Error: migrate.ignorePatterns must be an array of strings. +``` + +## `vpt print-file packages/app/package.json` + +``` +{ + "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_ignore_patterns/snapshots/migration_ignore_patterns_override_conflict.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_override_conflict.md new file mode 100644 index 0000000000..935a3ee49a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_override_conflict.md @@ -0,0 +1,47 @@ +# migration_ignore_patterns_override_conflict + +A conflicting global override is reported before project files change. + +## `vpt write-file pnpm-workspace.yaml 'packages: ['\''packages/*'\'', '\''templates/*'\''] +overrides: + vite: ^8.0.0 +'` + +``` +``` + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +■ Cannot migrate while preserving excluded packages: override "vite" ("^8.0.0") conflicts with "npm:@voidzero-dev/vite-plus-core@". Review the root override or migrate the excluded packages first. No project files were changed. +Error: Cannot migrate while preserving excluded packages: override "vite" ("^8.0.0") conflicts with "npm:@voidzero-dev/vite-plus-core@". Review the root override or migrate the excluded packages first. No project files were changed. +``` + +## `vpt print-file packages/app/package.json` + +``` +{ + "name": "app", "private": true, "type": "module", + "scripts": { "build": "vite build", "test": "vitest run" }, + "devDependencies": { "vite": "catalog:", "vitest": "catalog:legacy", "@vitest/coverage-v8": "catalog:legacy" } +} +``` + +## `vpt print-file packages/app/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +test('application', () => expect(1).toBe(1)); +``` + +## `vpt print-file pnpm-workspace.yaml` + +``` +packages: ['packages/*', 'templates/*'] +overrides: + vite: ^8.0.0 +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_parent_negation.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_parent_negation.md new file mode 100644 index 0000000000..c06a67cd3b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_parent_negation.md @@ -0,0 +1,35 @@ +# migration_ignore_patterns_parent_negation + +## `vpt write-file vite.config.ts 'export default { migrate: { ignorePatterns: ['\''templates/'\'', '\'''\!'templates/managed/unit.test.ts'\''] } }; +'` + +``` +``` + +## `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/managed/unit.test.ts` + +``` +import { expect, test } from 'vitest'; +test('included', () => 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_ignore_patterns/snapshots/migration_ignore_patterns_runtime_config.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_runtime_config.md new file mode 100644 index 0000000000..4cbf7d2eda --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_runtime_config.md @@ -0,0 +1,39 @@ +# migration_ignore_patterns_runtime_config + +A runtime config export retains the policy, and the root config can be a plain object. + +## `vpt write-file vite.config.ts 'const excluded = ['\''templates/*'\'', '\'''\!'templates/managed/'\'']; +export default () => ({ migrate: { ignorePatterns: excluded } }); +'` + +``` +``` + +## `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 → +• 4 files had imports rewritten +• Package manager settings configured +``` + +## `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)); +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_upgrade.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_upgrade.md new file mode 100644 index 0000000000..3e6a52f834 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_upgrade.md @@ -0,0 +1,165 @@ +# migration_ignore_patterns_upgrade + +Preserve templates and their shared catalogs and overrides, re-include one template, and leave non-workspace examples alone. + +## `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/package.json` + +``` +{ + "name": "starter", "private": true, "type": "module", + "scripts": { "build": "vite build", "test": "vitest run" }, + "devDependencies": { "vite": "catalog:", "vitest": "catalog:legacy", "@vitest/coverage-v8": "catalog:legacy" } +} +``` + +## `vpt print-file templates/starter/vite.config.ts` + +``` +import { defineConfig } from 'vite'; +export default defineConfig({test: {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/starter/tsconfig.json` + +``` +{ "compilerOptions": { "types": ["vite/client", "vitest/globals"], "esModuleInterop": false } } +``` + +## `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/package.json` + +``` +{ + "name": "app", + "private": true, + "type": "module", + "scripts": { + "build": "vp build", + "test": "vp test run" + }, + "devDependencies": { + "vite": "npm:@voidzero-dev/vite-plus-core@", + "vitest": "", + "@vitest/coverage-v8": "" + } +} +``` + +## `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/managed/unit.test.ts` + +``` +import { expect, test } from 'vite-plus/test'; +test('included', () => expect(1).toBe(1)); +``` + +## `vpt print-file pnpm-workspace.yaml` + +``` +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 +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: '*' + vitest: '*' +``` + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +Rerunning uses the same policy and explicit managed dependency specs. + +``` +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` + +Ignored imports remain upstream; other rules still report the template's debugger statement. + +**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_ignore_patterns/templates/managed/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/managed/package.json new file mode 100644 index 0000000000..fb282db2e7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/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_ignore_patterns/templates/managed/unit.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/managed/unit.test.ts new file mode 100644 index 0000000000..64ccb0c91d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/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_ignore_patterns/templates/starter/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/package.json new file mode 100644 index 0000000000..dad97a5f9b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/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_ignore_patterns/templates/starter/tsconfig.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/tsconfig.json new file mode 100644 index 0000000000..dc98a30fcd --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/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_ignore_patterns/templates/starter/unit.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/unit.test.ts new file mode 100644 index 0000000000..a8415c55f6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/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_ignore_patterns/templates/starter/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/vite.config.ts new file mode 100644 index 0000000000..2df3caf172 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/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_ignore_patterns/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/vite.config.ts new file mode 100644 index 0000000000..e27914cdc7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite-plus'; +export default defineConfig({ + migrate: { ignorePatterns: ['/templates/*', '!/templates/managed/', '/examples/'] }, + lint: { + jsPlugins: ['vite-plus/oxlint-plugin'], + rules: { 'vite-plus/prefer-vite-plus-imports': 'error', 'no-debugger': 'error' }, + }, +}); 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..2a81fc7335 --- /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 = "Reproduce #3406: an existing Vite+ workspace converts an independently reusable Vite/Vitest template. This records the current behavior, not the desired exclusion behavior." +steps = [ + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "ordinary upgrade without --full also migrates the template" }, + { 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..a90c3b5806 --- /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 + +Reproduce #3406: an existing Vite+ workspace converts an independently reusable Vite/Vitest template. This records the current behavior, not the desired exclusion behavior. + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +ordinary upgrade without --full also migrates the template + +``` +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/src/file_walker.rs b/crates/vp_migration/src/file_walker.rs index 91585964f9..636d3d273d 100644 --- a/crates/vp_migration/src/file_walker.rs +++ b/crates/vp_migration/src/file_walker.rs @@ -3,6 +3,37 @@ use std::path::{Path, PathBuf}; use ignore::{WalkBuilder, gitignore::GitignoreBuilder}; use vp_error::Error; +/// Config-relative gitignore rules shared by migration and its lint autofix. +#[derive(Clone)] +pub struct MigrationIgnoreMatcher { + root: PathBuf, + matcher: ignore::gitignore::Gitignore, +} + +impl MigrationIgnoreMatcher { + pub fn new(root: &Path, patterns: &[String]) -> Result { + let mut builder = GitignoreBuilder::new(root); + for pattern in patterns { + builder.add_line(None, pattern)?; + } + Ok(Self { root: root.to_path_buf(), matcher: builder.build()? }) + } + + pub fn is_ignored(&self, path: &Path, is_dir: bool) -> bool { + let Ok(relative) = path.strip_prefix(&self.root) else { return false }; + let mut current = self.root.clone(); + let mut components = relative.components().peekable(); + while let Some(component) = components.next() { + current.push(component); + // Git cannot re-include a child of an excluded directory. + if self.matcher.matched(¤t, components.peek().is_some() || is_dir).is_ignore() { + return true; + } + } + false + } +} + /// File extensions to process for import rewriting. /// /// Includes CommonJS (`.cjs`/`.cts`) variants in addition to ESM, because @@ -82,6 +113,13 @@ pub fn is_directory_gitignored(root: &Path, directory: &Path) -> Result Result { + find_ts_files_with_ignore(root, None) +} + +pub fn find_ts_files_with_ignore( + root: &Path, + ignore: Option, +) -> Result { let mut files = Vec::new(); let walker = WalkBuilder::new(root) @@ -90,6 +128,12 @@ pub fn find_ts_files(root: &Path) -> Result { .git_global(true) // Respect global gitignore .git_exclude(true) // Respect .git/info/exclude .require_git(false) // Work even if not a git repo + .filter_entry(move |entry| { + !ignore.as_ref().is_some_and(|matcher| { + matcher + .is_ignored(entry.path(), entry.file_type().is_some_and(|kind| kind.is_dir())) + }) + }) .build(); for entry in walker { @@ -120,6 +164,30 @@ mod tests { use super::*; + #[test] + fn migration_patterns_follow_gitignore_semantics() { + let temp = tempdir().unwrap(); + for (patterns, file, is_dir, expected) in [ + (vec!["/templates/"], "templates/app/vite.config.ts", false, true), + (vec!["/templates/"], "nested/templates/a.ts", false, false), + (vec!["templates/"], "nested/templates/a.ts", false, true), + (vec!["templates/*", "!templates/app/"], "templates/app/a.ts", false, false), + (vec!["templates/", "!templates/app/a.ts"], "templates/app/a.ts", false, true), + (vec!["**/*.test.ts", "!app.test.ts"], "app.test.ts", false, false), + (vec!["# comment", "\\!literal/"], "!literal/a.ts", false, true), + (vec!["templates/"], "templates", false, false), + (vec!["templates/"], "templates", true, true), + ] { + let patterns = patterns.into_iter().map(String::from).collect::>(); + let matcher = MigrationIgnoreMatcher::new(temp.path(), &patterns).unwrap(); + assert_eq!( + matcher.is_ignored(&temp.path().join(file), is_dir), + expected, + "{patterns:?}: {file}" + ); + } + } + #[test] fn test_inherited_directory_gitignore() { for (root_rules, package_rules, expected) in [ diff --git a/crates/vp_migration/src/import_rewriter.rs b/crates/vp_migration/src/import_rewriter.rs index da8643be9f..d99dfe8f21 100644 --- a/crates/vp_migration/src/import_rewriter.rs +++ b/crates/vp_migration/src/import_rewriter.rs @@ -2062,6 +2062,9 @@ struct PackageRewriteContext { /// Options controlling directory-wide import rewriting. #[derive(Debug, Clone, Default)] pub struct RewriteImportsOptions { + /// Config-relative exclusions, also applied when the scan starts below the config root. + pub ignore_root: Option, + pub ignore_patterns: Vec, /// Preserve `vitest` and `vitest/*` module specifiers throughout packages /// whose nearest package.json declares `@nuxt/test-utils`. pub preserve_vitest_in_nuxt_packages: bool, @@ -2285,7 +2288,11 @@ pub fn rewrite_imports_in_directory_with_options( root: &Path, options: RewriteImportsOptions, ) -> Result { - let walk_result = file_walker::find_ts_files(root)?; + let ignore = file_walker::MigrationIgnoreMatcher::new( + options.ignore_root.as_deref().unwrap_or(root), + &options.ignore_patterns, + )?; + let walk_result = file_walker::find_ts_files_with_ignore(root, Some(ignore))?; // Pre-compute package context for each file (requires mutable cache, done sequentially). let mut package_context_cache: HashMap = HashMap::new(); diff --git a/crates/vp_migration/src/lib.rs b/crates/vp_migration/src/lib.rs index 00ced33720..d6832db050 100644 --- a/crates/vp_migration/src/lib.rs +++ b/crates/vp_migration/src/lib.rs @@ -20,7 +20,7 @@ mod source_analysis; mod task_cache; mod vite_config; -pub use file_walker::{WalkResult, find_ts_files, is_directory_gitignored}; +pub use file_walker::{MigrationIgnoreMatcher, WalkResult, find_ts_files, is_directory_gitignored}; pub use import_rewriter::{ BatchRewriteResult, RewriteImportsOptions, rewrite_imports_in_directory, rewrite_imports_in_directory_with_options, diff --git a/crates/vp_static_config/src/lib.rs b/crates/vp_static_config/src/lib.rs index d79c2d1a06..26f488a98c 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,48 @@ mod tests { assert_non_static(&result, "run"); } + #[test] + fn vitest_config_preserves_migration_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()], + migrate: { ignorePatterns: ['templates/'] }, + });", + ] + .join("\n"); + let result = parse(&source); + assert_json( + &result, + "migrate", + serde_json::json!({ "ignorePatterns": ["templates/"] }), + ); + 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({ migrate: { ignorePatterns: ['templates/'] } });"; + let result = parse(&[source, "export default config;"].join("\n")); + assert_json(&result, "migrate", serde_json::json!({ "ignorePatterns": ["templates/"] })); + assert!(result.get("run").is_none()); + for mutation in [ + "config.migrate = { ignorePatterns: [] };", + "Object.assign(config, other);", + "const changed = mutate(config);", + ] { + let result = parse(&[source, mutation, "export default config;"].join("\n")); + assert_non_static(&result, "migrate"); + } + assert_non_static(&parse("let config = {}; export default config;"), "migrate"); + } + #[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..2ccd47aefa 100644 --- a/docs/guide/migrate.md +++ b/docs/guide/migrate.md @@ -45,6 +45,67 @@ vp migrate my-app ## Migration Flow +### Excluding templates and examples + +Use `migrate.ignorePatterns` in the workspace-root `vite.config.ts` to keep +independently reusable projects on Vite or Vitest: + +```ts +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + migrate: { + ignorePatterns: ['templates/', 'examples/'], + }, +}); +``` + +Patterns use `.gitignore` syntax and are relative to the root config directory. +They support comments, anchored paths, directory patterns, and ordered `!` +negation. To include one template, use `['templates/*', '!templates/managed/']`. +As with Git, a negation cannot include a file whose parent directory is excluded. + +The policy is read before migration makes edits. Literal config blocks can be +read without loading plugins. Dynamic configs require the dependencies needed +to load the config to be installed first. + +Excluded files keep their dependencies, scripts, configs, and imports, and are +omitted from automatic formatting. The same policy applies to initial migration, +upgrades, and `--full`. Workspace membership stays unchanged. Migration reads +excluded package manifests to identify shared dependency settings. + +Automatic ESLint comment renaming and `baseUrl` removal are skipped when +exclusions are configured because those external tools cannot honor the patterns. +ESLint disable comments continue to work. Remove `baseUrl` manually in migrated +projects to enable type-aware linting. + +When excluded packages are present, migration preserves existing catalogs and +overrides and uses explicit dependency versions in migrated packages. It does not +add global toolchain overrides. Existing overrides still apply during installation; +an incompatible toolchain override must be resolved before migration can proceed. +Catalog references in an excluded template still require its original workspace. + +The `vite-plus/prefer-vite-plus-imports` lint rule also respects these exclusions, +including during `vp lint --fix` and `vp check --fix`. Other lint rules and normal +formatting remain enabled. To disable only the import rule with existing lint +configuration, use: + +```ts +lint: { + overrides: [ + { + files: ['templates/**', 'examples/**'], + rules: { 'vite-plus/prefer-vite-plus-imports': 'off' }, + }, + ], +}, +``` + +This lint override disables both diagnostics and autofix for that rule. It does +not exclude files from `vp migrate`. + +### Steps + The `migrate` command is designed to move existing projects onto Vite+ quickly. Here is what the command does: - Updates project dependencies diff --git a/packages/cli/binding/index.cjs b/packages/cli/binding/index.cjs index dbdf014927..bb2b26c8d1 100644 --- a/packages/cli/binding/index.cjs +++ b/packages/cli/binding/index.cjs @@ -1102,6 +1102,7 @@ module.exports.resetNativeMemoryStats = nativeBinding.resetNativeMemoryStats; module.exports.resolveTsconfig = nativeBinding.resolveTsconfig; module.exports.shutdownAsyncRuntime = nativeBinding.shutdownAsyncRuntime; module.exports.startAsyncRuntime = nativeBinding.startAsyncRuntime; +module.exports.MigrationIgnoreMatcher = nativeBinding.MigrationIgnoreMatcher; module.exports.analyzeMigrationSource = nativeBinding.analyzeMigrationSource; module.exports.detectWorkspace = nativeBinding.detectWorkspace; module.exports.documentationUrl = nativeBinding.documentationUrl; @@ -1121,6 +1122,7 @@ module.exports.parseHooksArgs = nativeBinding.parseHooksArgs; module.exports.parseLiteralCommands = nativeBinding.parseLiteralCommands; module.exports.parseMigrateArgs = nativeBinding.parseMigrateArgs; module.exports.parseStagedArgs = nativeBinding.parseStagedArgs; +module.exports.readStaticMigrationConfig = nativeBinding.readStaticMigrationConfig; 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..4e219f82dc 100644 --- a/packages/cli/binding/index.d.cts +++ b/packages/cli/binding/index.d.cts @@ -3610,6 +3610,11 @@ export interface ViteImportGlobMeta { isSubImportsPattern?: boolean; } +export declare class MigrationIgnoreMatcher { + constructor(root: string, patterns: Array); + isIgnored(path: string, isDir: boolean): boolean; +} + /** Parse source and resolve lexical bindings for the TypeScript migration rules. */ export declare function analyzeMigrationSource(filename: string, source: string): string; @@ -3983,6 +3988,9 @@ export interface PathAccess { readDir: boolean; } +/** JSON for a static migrate block; undefined requests normal config resolution. */ +export declare function readStaticMigrationConfig(root: string): string | null; + /** Remove a top-level key from a recognized Vite config object. */ export declare function removeConfigKey( viteConfigPath: string, @@ -4044,6 +4052,8 @@ export declare function rewriteImportsInDirectory( root: string, preserveVitestInNuxtPackages?: boolean | undefined | null, oxlintOwnerDirs?: Array | undefined | null, + ignoreRoot?: string | undefined | null, + ignorePatterns?: Array | undefined | null, ): BatchRewriteResult; /** diff --git a/packages/cli/binding/src/migration.rs b/packages/cli/binding/src/migration.rs index aa0901e775..b4f9cdf156 100644 --- a/packages/cli/binding/src/migration.rs +++ b/packages/cli/binding/src/migration.rs @@ -3,6 +3,37 @@ use std::path::Path; use napi::{anyhow, bindgen_prelude::*}; use napi_derive::napi; +#[napi] +pub struct MigrationIgnoreMatcher(vp_migration::MigrationIgnoreMatcher); + +#[napi] +impl MigrationIgnoreMatcher { + #[napi(constructor)] + pub fn new(root: String, patterns: Vec) -> Result { + Ok(Self( + vp_migration::MigrationIgnoreMatcher::new(Path::new(&root), &patterns) + .map_err(anyhow::Error::from)?, + )) + } + + #[napi] + pub fn is_ignored(&self, path: String, is_dir: bool) -> bool { + self.0.is_ignored(Path::new(&path), is_dir) + } +} + +/// JSON for a static migrate block; undefined requests normal config resolution. +#[napi] +pub fn read_static_migration_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("migrate") { + Some(vp_static_config::FieldValue::Json(value)) => Some(value.to_string()), + Some(vp_static_config::FieldValue::NonStatic) => None, + None => Some("{}".to_string()), + }) +} + /// A literal argument with its original JavaScript UTF-16 source range. #[napi(object)] pub struct LiteralCommandToken { @@ -367,10 +398,14 @@ pub fn rewrite_imports_in_directory( root: String, preserve_vitest_in_nuxt_packages: Option, oxlint_owner_dirs: Option>, + ignore_root: Option, + ignore_patterns: Option>, ) -> Result { let result = vp_migration::rewrite_imports_in_directory_with_options( Path::new(&root), vp_migration::RewriteImportsOptions { + ignore_root: ignore_root.map(std::path::PathBuf::from), + ignore_patterns: ignore_patterns.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/define-config.ts b/packages/cli/src/define-config.ts index ae22427c4f..33ca2d4263 100644 --- a/packages/cli/src/define-config.ts +++ b/packages/cli/src/define-config.ts @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'; import { ignoreInput, ignoreOutput } from '@voidzero-dev/vite-task-client'; import type { OxfmtConfig } from 'oxfmt'; import type { OxlintConfig } from 'oxlint'; -import { resolveConfig, searchForWorkspaceRoot } from 'vite'; +import { resolveConfig as viteResolveConfig, searchForWorkspaceRoot } from 'vite'; import type { Alias, InlineConfig as ViteInlineConfig, @@ -27,6 +27,7 @@ import { import type { InlineConfig as VitestInlineConfig } from 'vitest/node'; import type { CreateTemplateEntry } from './create/org-manifest.ts'; +import { migrationIgnorePatterns, type MigrateConfig } from './migrate-config.ts'; import type { PackUserConfig } from './pack.ts'; import type { RunConfig } from './run-config.ts'; import type { StagedConfig } from './staged-config.ts'; @@ -42,6 +43,8 @@ declare module 'vite' { fmt?: OxfmtConfig; + migrate?: MigrateConfig; + /** * Defaults for the `vp check` composite command. Each flag mirrors the * matching CLI option (`--no-fmt` / `--no-lint`) and only affects @@ -112,6 +115,26 @@ declare module 'vite' { } } +// Oxlint loads the lint block through this public resolver, including configs +// which export a plain object instead of calling our defineConfig helper. +export const resolveConfig: typeof viteResolveConfig = async (...args) => { + const config = await viteResolveConfig(...args); + const patterns = migrationIgnorePatterns(config.migrate); + if (config.lint && patterns.length > 0) { + Object.assign(config.lint, { + ...config.lint, + settings: { + ...config.lint.settings, + 'vite-plus/migrate': { + root: config.configFile ? dirname(config.configFile) : config.root, + ignorePatterns: patterns, + }, + }, + }); + } + return config; +}; + type ViteUserConfigFnObject = (env: ConfigEnv) => UserConfig; type ViteUserConfigFnPromise = (env: ConfigEnv) => Promise; type ViteUserConfigFn = (env: ConfigEnv) => UserConfig | Promise; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index d8f7b7fff8..5a449ea7f1 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -2,9 +2,11 @@ export { defineConfig, defineProject, lazyPlugins, + resolveConfig, resolveConfigFromFile, } from './define-config.ts'; export type { ResolveConfigFromFileOptions } from './define-config.ts'; +export type { MigrateConfig } from './migrate-config.ts'; export * from 'vite'; diff --git a/packages/cli/src/migrate-config.ts b/packages/cli/src/migrate-config.ts new file mode 100644 index 0000000000..8986f081b5 --- /dev/null +++ b/packages/cli/src/migrate-config.ts @@ -0,0 +1,21 @@ +export interface MigrateConfig { + /** Paths excluded from migration, using .gitignore syntax relative to this config. */ + ignorePatterns?: string[]; +} + +export function migrationIgnorePatterns(config: unknown): string[] { + if (config === undefined) { + return []; + } + if (config === null || typeof config !== 'object' || Array.isArray(config)) { + throw new Error('migrate must be an object.'); + } + const patterns = (config as MigrateConfig).ignorePatterns; + if (patterns === undefined) { + return []; + } + if (!Array.isArray(patterns) || patterns.some((pattern) => typeof pattern !== 'string')) { + throw new Error('migrate.ignorePatterns must be an array of strings.'); + } + return patterns; +} diff --git a/packages/cli/src/migration-ignore.ts b/packages/cli/src/migration-ignore.ts new file mode 100644 index 0000000000..79ac324465 --- /dev/null +++ b/packages/cli/src/migration-ignore.ts @@ -0,0 +1,7 @@ +import path from 'node:path'; + +import { MigrationIgnoreMatcher } from '../binding/index.js'; + +export function createMigrationIgnoreMatcher(root: string, patterns: string[]) { + return new MigrationIgnoreMatcher(path.resolve(root), patterns); +} diff --git a/packages/cli/src/migration/__tests__/ignore-patterns.spec.ts b/packages/cli/src/migration/__tests__/ignore-patterns.spec.ts new file mode 100644 index 0000000000..699b5a346d --- /dev/null +++ b/packages/cli/src/migration/__tests__/ignore-patterns.spec.ts @@ -0,0 +1,162 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { VITEST_VERSION, VITE_PLUS_OVERRIDE_PACKAGES } from '../../utils/constants.ts'; +import { readJsonFile } from '../../utils/json.ts'; +import { readYamlFile } from '../../utils/yaml.ts'; +import { editJsonFile, editYamlFile } from '../files.ts'; +import { formatMigratedProject } from '../format.ts'; +import { mergeTsdownConfigFile, rewriteAllImports } from '../migrator.ts'; +import { createMigrationReport } from '../report.ts'; +import { checkEnclosingMigrationPolicy, loadMigrationScope, withMigrationScope } from '../scope.ts'; + +describe('migration exclusions', () => { + let root: string; + const write = (file: string, content: string) => { + const filename = path.join(root, file); + fs.mkdirSync(path.dirname(filename), { recursive: true }); + fs.writeFileSync(filename, content); + return filename; + }; + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-migration-ignore-')); + write('package.json', '{"private":true}'); + write( + 'vite.config.ts', + `export default { migrate: { ignorePatterns: ['templates/', 'keep.config.ts'] } };`, + ); + write('templates/starter/package.json', '{"name":"starter"}'); + }); + afterEach(() => fs.rmSync(root, { recursive: true, force: true })); + + it('loads a static policy without executing uninstalled plugins', async () => { + write( + 'vite.config.ts', + `import { defineConfig } from 'vite'; +import missing from 'uninstalled-plugin'; +export default defineConfig({ plugins: [missing()], migrate: { ignorePatterns: ['templates/'] } });`, + ); + const scope = await loadMigrationScope(root); + expect(scope.ignores(path.join(root, 'templates/starter/vite.config.ts'))).toBe(true); + }); + + it('rejects explicit excluded targets while allowing an independent clone', async () => { + const target = path.join(root, 'templates/starter'); + await expect(checkEnclosingMigrationPolicy(target)).rejects.toThrow( + 'excluded by migrate.ignorePatterns', + ); + fs.mkdirSync(path.join(target, '.git')); + await expect(checkEnclosingMigrationPolicy(target)).resolves.toBeUndefined(); + }); + + it('preserves excluded source and rewrites included source, including a scan below the root', async () => { + const excluded = write('templates/starter/test.ts', `import { test } from 'vitest';`); + const included = write('app/test.ts', `import { test } from 'vitest';`); + withMigrationScope(await loadMigrationScope(root), () => { + expect(rewriteAllImports(root, true)).toBe(true); + expect(rewriteAllImports(path.dirname(excluded), true)).toBe(false); + }); + expect(fs.readFileSync(excluded, 'utf8')).toBe(`import { test } from 'vitest';`); + expect(fs.readFileSync(included, 'utf8')).toBe(`import { test } from 'vite-plus/test';`); + }); + + it('preserves shared catalogs and overrides in YAML and JSON, including nested Bun catalogs', async () => { + const workspace = write( + 'pnpm-workspace.yaml', + 'catalog:\n vite: ^8.0.0\ncatalogs:\n old:\n vitest: ^4.0.0\noverrides:\n unrelated: 1.0.0\n', + ); + const manifest = write( + 'package.json', + JSON.stringify({ + private: true, + workspaces: { packages: ['templates/*'], catalogs: { old: { vitest: '^4.0.0' } } }, + pnpm: { overrides: { unrelated: '1.0.0' } }, + }), + ); + const before = readYamlFile(workspace); + withMigrationScope(await loadMigrationScope(root), () => { + editYamlFile(workspace, (doc) => { + doc.setIn(['catalog', 'vite'], VITE_PLUS_OVERRIDE_PACKAGES.vite); + doc.delete('catalogs'); + doc.setIn(['overrides', 'vite'], VITE_PLUS_OVERRIDE_PACKAGES.vite); + doc.set('unrelatedSetting', true); + }); + editJsonFile(manifest, (pkg) => { + pkg.workspaces = { packages: ['templates/*'] }; + delete pkg.pnpm; + pkg.overrides = { vite: VITE_PLUS_OVERRIDE_PACKAGES.vite }; + pkg.devDependencies = { 'vite-plus': 'latest' }; + return pkg; + }); + }); + expect(readYamlFile(workspace)).toEqual({ ...before, unrelatedSetting: true }); + expect(readJsonFile(manifest)).toMatchObject({ + workspaces: { catalogs: { old: { vitest: '^4.0.0' } } }, + pnpm: { overrides: { unrelated: '1.0.0' } }, + devDependencies: { 'vite-plus': 'latest' }, + }); + expect(readJsonFile(manifest)).not.toHaveProperty('overrides'); + }); + + it.each([ + { overrides: { 'vite@*': '^8.0.0' } }, + { resolutions: { '**/vitest': '^4.0.0' } }, + { overrides: { 'vite-plus': { vitest: '^4.0.0' } } }, + { overrides: { vitest: { '.': '^4.0.0' } } }, + ])('rejects conflicting shared overrides before any writes: %j', async (settings) => { + const file = write('package.json', JSON.stringify(settings)); + const before = fs.readFileSync(file, 'utf8'); + await expect(loadMigrationScope(root)).rejects.toThrow('No project files were changed.'); + expect(fs.readFileSync(file, 'utf8')).toBe(before); + }); + + it('accepts compatible global overrides and preserves overrides scoped to an excluded package', async () => { + write( + 'package.json', + JSON.stringify({ + overrides: { + vitest: VITEST_VERSION, + starter: { vitest: '^4.0.0' }, + }, + }), + ); + await expect(loadMigrationScope(root)).resolves.toBeDefined(); + }); + + it('does not consume a source config when its destination is ignored', async () => { + write('vite.config.ts', `export default { migrate: { ignorePatterns: ['vite.config.ts'] } };`); + const source = write('tsdown.config.json', '{"entry":["src/index.ts"]}'); + withMigrationScope(await loadMigrationScope(root), () => { + expect(mergeTsdownConfigFile(root, true)).toBe(false); + }); + expect(fs.readFileSync(source, 'utf8')).toBe('{"entry":["src/index.ts"]}'); + }); + + it('filters automatic formatting outside Git and when explicit changed paths are supplied', async () => { + write('app.ts', 'export const app=1;'); + write('templates/starter/app.ts', 'export const template=1;'); + const scope = await loadMigrationScope(root); + for (const changed of [undefined, ['app.ts', 'templates/starter/app.ts']]) { + const formatted: string[] = []; + await withMigrationScope(scope, () => + formatMigratedProject(root, false, createMigrationReport(), { + collectPaths: async () => changed, + format: async (_cwd, _interactive, paths) => { + expect(paths).toBeDefined(); + formatted.push(...paths!); + return { status: 'formatted', durationMs: 0 }; + }, + }), + ); + expect(formatted).toContain('app.ts'); + expect( + formatted.some( + (file) => file.startsWith(`templates${path.sep}`) || file.startsWith('templates/'), + ), + ).toBe(false); + } + }); +}); diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index de19f1893e..b17f630ba4 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -100,6 +100,13 @@ import { import { prepareNpmViteAliasReinstall } from './npm-reinstall.ts'; import type { MigrationOptions } from './options.ts'; import { addMigrationWarning, createMigrationReport, type MigrationReport } from './report.ts'; +import { + checkEnclosingMigrationPolicy, + isMigrationIgnored, + loadMigrationScope, + migrationScope, + withMigrationScope, +} from './scope.ts'; import { collectMigrationSetupPlan, collectTsupMigrationDecision, @@ -197,6 +204,15 @@ async function fixBaseUrlForWorkspace( if (!fixBaseUrl) { return []; } + // ts5to6 follows references and extends outside its input config, but does + // not accept exclusions. Filtering input paths alone cannot protect them. + if (migrationScope()?.patterns.length) { + addMigrationWarning( + report, + 'Skipped automatic tsconfig baseUrl removal because its external fixer cannot honor migrate.ignorePatterns. Remove baseUrl manually in migrated projects to enable type-aware linting.', + ); + return []; + } const fixedProjectPaths: string[] = []; for (const projectPath of getWorkspaceProjectPaths(workspaceInfo)) { @@ -949,6 +965,7 @@ async function executeMigrationPlan( // 9. Write agent instructions (using pre-resolved decisions) updateMigrationProgress('Writing agent instructions'); await writeAgentInstructions({ + shouldWriteFile: (file) => !isMigrationIgnored(file), projectRoot: workspaceInfo.rootDir, targetPaths: plan.selectedAgentTargetPaths, interactive, @@ -959,6 +976,7 @@ async function executeMigrationPlan( // 10. Write editor configs (using pre-resolved decisions) updateMigrationProgress('Writing editor configs'); await writeEditorConfigs({ + shouldWriteFile: (file) => !isMigrationIgnored(file), projectRoot: workspaceInfo.rootDir, editorId: plan.selectedEditor, interactive, @@ -1061,6 +1079,21 @@ async function main() { 1, ); } + await checkEnclosingMigrationPolicy(workspaceInfoOptional.rootDir); + const scope = await loadMigrationScope( + workspaceInfoOptional.rootDir, + workspaceInfoOptional.packageManager, + ); + workspaceInfoOptional.packages = workspaceInfoOptional.packages?.filter( + (pkg) => !scope.ignores(path.join(workspaceInfoOptional.rootDir, pkg.path), true), + ); + return withMigrationScope(scope, () => migrateWorkspace(workspaceInfoOptional, options)); +} + +async function migrateWorkspace( + workspaceInfoOptional: WorkspaceInfoOptional, + options: MigrationOptions, +) { const initialChangedPaths = await collectChangedFormatPaths(workspaceInfoOptional.rootDir); const preExistingChangedPaths = initialChangedPaths ? new Set(initialChangedPaths) : undefined; const resolvedPackageManager = workspaceInfoOptional.packageManager ?? 'unknown'; @@ -1654,6 +1687,7 @@ async function main() { if (plan.selectedAgentTargetPaths && plan.selectedAgentTargetPaths.length > 0) { updateMigrationProgress('Writing agent instructions'); await writeAgentInstructions({ + shouldWriteFile: (file) => !isMigrationIgnored(file), projectRoot: workspaceInfoOptional.rootDir, targetPaths: plan.selectedAgentTargetPaths, interactive: options.interactive, @@ -1666,6 +1700,7 @@ async function main() { if (plan.selectedEditor) { updateMigrationProgress('Writing editor configs'); await writeEditorConfigs({ + shouldWriteFile: (file) => !isMigrationIgnored(file), projectRoot: workspaceInfoOptional.rootDir, editorId: plan.selectedEditor, interactive: options.interactive, diff --git a/packages/cli/src/migration/files.ts b/packages/cli/src/migration/files.ts new file mode 100644 index 0000000000..d539b13cdb --- /dev/null +++ b/packages/cli/src/migration/files.ts @@ -0,0 +1,113 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { parseDocument } from 'yaml'; + +import { readJsonFile, writeJsonFile as writeJson } from '../utils/json.ts'; +import { type YamlDocument } from '../utils/yaml.ts'; +import { isMigrationIgnored, migrationScope, sharedDependencyFields } from './scope.ts'; + +export { readJsonFile, isJsonFile } from '../utils/json.ts'; +export { readYamlFile, scalarString, type YamlDocument } from '../utils/yaml.ts'; + +function ignored(file: fs.PathLike | number): boolean { + return ( + typeof file !== 'number' && + isMigrationIgnored(file instanceof URL ? fileURLToPath(file) : file.toString()) + ); +} + +// Keep migration writes behind one boundary. Reads still see excluded files: +// shared dependency ownership must be determined from the complete project. +const migrationFs: typeof fs = Object.assign(Object.create(fs) as typeof fs, { + writeFileSync(file, ...args) { + if (!ignored(file)) { + fs.writeFileSync(file, ...args); + } + }, + appendFileSync(file, ...args) { + if (!ignored(file)) { + fs.appendFileSync(file, ...args); + } + }, + unlinkSync(file) { + if (!ignored(file)) { + fs.unlinkSync(file); + } + }, + rmSync(file, ...args) { + if (!ignored(file)) { + fs.rmSync(file, ...args); + } + }, + renameSync(from, to) { + if (!ignored(from) && !ignored(to)) { + fs.renameSync(from, to); + } + }, +} satisfies Pick< + typeof fs, + 'writeFileSync' | 'appendFileSync' | 'unlinkSync' | 'rmSync' | 'renameSync' +>); +export default migrationFs; + +function preserveShared(file: string): boolean { + const scope = migrationScope(); + return ( + !!scope?.preserveSharedDependencies && + path.dirname(file) === scope.root && + ['package.json', 'pnpm-workspace.yaml', '.yarnrc.yml'].includes(path.basename(file)) + ); +} + +export function writeJsonFile(file: string, data: Record): void { + if (isMigrationIgnored(file)) { + return; + } + if (preserveShared(file) && fs.existsSync(file)) { + const before = parseDocument(fs.readFileSync(file, 'utf8')); + const after = parseDocument(JSON.stringify(data)); + for (const keys of sharedDependencyFields) { + if (before.hasIn(keys)) { + after.setIn(keys, before.getIn(keys)); + } else if (after.hasIn(keys)) { + after.deleteIn(keys); + } + } + data = after.toJS() as Record; + } + writeJson(file, data); +} + +export function editJsonFile = Record>( + file: string, + callback: (content: T) => T | undefined, +): void { + if (isMigrationIgnored(file)) { + return; + } + const updated = callback(readJsonFile(file) as T); + if (updated) { + writeJsonFile(file, updated); + } +} + +export function editYamlFile(file: string, callback: (doc: YamlDocument) => void): void { + if (isMigrationIgnored(file)) { + return; + } + const before = parseDocument(fs.readFileSync(file, 'utf8')); + const after = parseDocument(fs.readFileSync(file, 'utf8')); + callback(after); + if (preserveShared(file)) { + for (const keys of sharedDependencyFields) { + if (before.hasIn(keys)) { + after.setIn(keys, before.getIn(keys)); + } else if (after.hasIn(keys)) { + after.deleteIn(keys); + } + } + } + fs.writeFileSync(file, after.toString({ singleQuote: true }), 'utf8'); +} diff --git a/packages/cli/src/migration/format.ts b/packages/cli/src/migration/format.ts index 81ac5b1203..5c9e6484a4 100644 --- a/packages/cli/src/migration/format.ts +++ b/packages/cli/src/migration/format.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import { runCommandSilently } from '../utils/command.ts'; import { type CommandRunSummary, runViteFmt } from '../utils/prompts.ts'; import { addMigrationWarning, type MigrationReport } from './report.ts'; +import { isMigrationIgnored, migrationScope } from './scope.ts'; type FormatRunner = ( cwd: string, @@ -163,7 +164,29 @@ export async function formatMigratedProject( ): Promise { const { format = runViteFmt, collectPaths = collectChangedFormatPaths, excludedPaths } = options; try { - const paths = await collectPaths(projectRoot, excludedPaths); + let paths = await collectPaths(projectRoot, excludedPaths); + if (migrationScope()?.patterns.length) { + // Outside Git, enumerate files rather than handing an excluded directory + // to the formatter. Do not follow dependency trees or symlinks. + if (paths === undefined) { + paths = []; + const walk = (dir: string) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const file = path.join(dir, entry.name); + if (isMigrationIgnored(file, entry.isDirectory())) { + continue; + } + if (entry.isDirectory() && !['node_modules', '.git', '.yarn'].includes(entry.name)) { + walk(file); + } else if (entry.isFile()) { + paths!.push(path.relative(projectRoot, file)); + } + } + }; + walk(projectRoot); + } + paths = paths.filter((file) => !isMigrationIgnored(path.resolve(projectRoot, file))); + } if (paths?.length === 0) { return true; } diff --git a/packages/cli/src/migration/migrator/README.md b/packages/cli/src/migration/migrator/README.md index acd19f4eb8..acc7cc7601 100644 --- a/packages/cli/src/migration/migrator/README.md +++ b/packages/cli/src/migration/migrator/README.md @@ -48,6 +48,13 @@ Pick the file by what a function _does_, not by where it happens to be called. ## Rules for adding / changing code +Migration file writes use `../files.ts`, which applies the active exclusion +scope from `../scope.ts`. Keep reads available for shared dependency analysis. +For operations that consume one file to create another, check both paths before +the operation. External tools must receive filtered targets or skip a step whose +writes cannot respect the scope. `vp create` and callers without a migration +scope retain the normal write behavior. + 1. **Add a function to the module that matches its category.** Keep it `export`ed so the barrel surfaces it. If nothing fits, prefer extending an existing module over creating a new one; if you do add a module, add a line diff --git a/packages/cli/src/migration/migrator/catalog.ts b/packages/cli/src/migration/migrator/catalog.ts index 76c5e8b932..64a90241aa 100644 --- a/packages/cli/src/migration/migrator/catalog.ts +++ b/packages/cli/src/migration/migrator/catalog.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs'; import path from 'node:path'; import { coerce, isGreaterThanOrEqual, isLessThan } from 'verkit'; @@ -13,9 +12,10 @@ import { VITE_PLUS_VERSION, isForceOverrideMode, } from '../../utils/constants.ts'; -import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { type NpmWorkspaces } from '../../utils/workspace.ts'; -import { editYamlFile, readYamlFile, scalarString, type YamlDocument } from '../../utils/yaml.ts'; +import fs from '../files.ts'; +import { editJsonFile, readJsonFile } from '../files.ts'; +import { editYamlFile, readYamlFile, scalarString, type YamlDocument } from '../files.ts'; import { dropRemovePackageOverrideKeys, ensurePnpmExoticSubdepsSetting, @@ -31,6 +31,7 @@ import { shouldDropProviderOverrideKey, workspaceUsesWebdriverioProvider, } from '../migrator.ts'; +import { preserveSharedDependencies } from '../scope.ts'; import { type PreparedOxlintConfigs, LEGACY_WRAPPER_FALLBACK_VERSIONS, @@ -166,6 +167,9 @@ export function supportsCatalog( version: string, isBunWorkspace = false, ): boolean { + if (preserveSharedDependencies()) { + return false; + } switch (packageManager) { case PackageManager.pnpm: return pnpmSupportsCatalog(version); @@ -619,7 +623,7 @@ export function getCatalogDependencySpec( ) { return version; } - if (!supportCatalog || version.startsWith('file:')) { + if (!supportCatalog || preserveSharedDependencies() || version.startsWith('file:')) { return version; } return currentValue?.startsWith('catalog:') @@ -901,6 +905,9 @@ export function rewriteCatalog( catalogs?: Record>; } | null; const preferredCatalogSpec = selectPreferredCatalogSpec(parsed?.catalog, parsed?.catalogs); + if (preserveSharedDependencies()) { + return preferredCatalogSpec; + } const preferredCatalogName = preferredCatalogSpec.slice('catalog:'.length); const targetPath: readonly string[] = preferredCatalogName && preferredCatalogName !== 'default' @@ -1090,6 +1097,9 @@ export function rewriteBunCatalog( vitestEcosystemPackages: ReadonlySet, usesWebdriverioProvider = false, ): void { + if (preserveSharedDependencies()) { + return; + } const packageJsonPath = path.join(projectPath, 'package.json'); if (!fs.existsSync(packageJsonPath)) { return; diff --git a/packages/cli/src/migration/migrator/core-finalization.ts b/packages/cli/src/migration/migrator/core-finalization.ts index d57f2b556a..eadcd558c7 100644 --- a/packages/cli/src/migration/migrator/core-finalization.ts +++ b/packages/cli/src/migration/migrator/core-finalization.ts @@ -3,8 +3,8 @@ import path from 'node:path'; import { rewriteScripts } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; -import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { rulesDir } from '../../utils/path.ts'; +import { editJsonFile, readJsonFile } from '../files.ts'; import { dropDeadOxlintPluginsDependency, hasTsconfigTypesToRewrite, diff --git a/packages/cli/src/migration/migrator/eslint.ts b/packages/cli/src/migration/migrator/eslint.ts index bf9231d0e1..72f8feb60b 100644 --- a/packages/cli/src/migration/migrator/eslint.ts +++ b/packages/cli/src/migration/migrator/eslint.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { styleText } from 'node:util'; @@ -10,14 +9,16 @@ import { compareReversed, isLessThanOrEqual, normalize } from 'verkit'; import { ResolverFactory, rewriteEslint } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; import { getVpCommand, runCommandSilently } from '../../utils/command.ts'; -import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts'; import { fetchNpmResource, getNpmRegistry } from '../../utils/npm-config.ts'; import { displayRelative } from '../../utils/path.ts'; import { cancelAndExit } from '../../utils/prompts.ts'; import { getSilentSpinner, getSpinner } from '../../utils/spinner.ts'; import { hasBaseUrlInTsconfig } from '../../utils/tsconfig.ts'; import { detectConfigs } from '../detector.ts'; +import fs from '../files.ts'; +import { editJsonFile, isJsonFile, readJsonFile } from '../files.ts'; import { type MigrationReport } from '../report.ts'; +import { isMigrationIgnored, migrationScope } from '../scope.ts'; import { LINT_STAGED_JSON_CONFIG_FILES, LINT_STAGED_OTHER_CONFIG_FILES, @@ -217,6 +218,13 @@ export async function migrateEslintToOxlint( packages?: WorkspacePackage[], options?: { silent?: boolean; report?: MigrationReport }, ): Promise { + if ( + [eslintConfigFile, '.oxlintrc.json', '.oxlintrc.json.bak'].some( + (file) => file && isMigrationIgnored(path.join(projectPath, file)), + ) + ) { + return false; + } const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive); // Steps 1-2: Only run @oxlint/migrate if there's an eslint config at root @@ -252,18 +260,22 @@ export async function migrateEslintToOxlint( warnMigration(migrateResult.skippedRules, options?.report); } - // Step 2: Replace eslint-disable comments with oxlint-disable - spinner.start('Replacing ESLint comments with Oxlint equivalents...'); - const replaceResult = await runOxlintMigrateStep( - projectPath, - migratePackage, - ['--replace-eslint-comments'], - spinner, - 'ESLint comment replacement failed', - `You can run \`vp dlx ${migratePackage} --replace-eslint-comments\` manually later`, - ); - if (replaceResult.ok) { - spinner.stop('ESLint comments replaced'); + // The upstream comment renamer scans the whole tree and cannot consume our + // exclusions. Existing ESLint disable comments already work with Oxlint. + if (!migrationScope()?.patterns.length) { + // Step 2: Replace eslint-disable comments with oxlint-disable + spinner.start('Replacing ESLint comments with Oxlint equivalents...'); + const replaceResult = await runOxlintMigrateStep( + projectPath, + migratePackage, + ['--replace-eslint-comments'], + spinner, + 'ESLint comment replacement failed', + `You can run \`vp dlx ${migratePackage} --replace-eslint-comments\` manually later`, + ); + if (replaceResult.ok) { + spinner.stop('ESLint comments replaced'); + } } // Continue with cleanup regardless — .oxlintrc.json was generated successfully } @@ -285,10 +297,7 @@ export async function migrateEslintToOxlint( // package — delete eslint config files, scrub ESLint-ecosystem deps from // package.json, and rewrite eslint references in any local lint-staged // config. A monorepo running `vp migrate` is treated as adopted as a - // whole; there's no per-package opt-out today. If a workspace package - // publishes a shared ESLint preset that you want to keep intact, exclude - // it from your `pnpm-workspace.yaml` / `workspaces` before running - // `vp migrate`, then add it back afterwards. + // whole, with migration-ignored packages removed from this target list. const cleanupTargets = [ projectPath, ...(packages ?? []).map((p) => path.join(projectPath, p.path)), diff --git a/packages/cli/src/migration/migrator/framework-shim.ts b/packages/cli/src/migration/migrator/framework-shim.ts index 9d2333a517..57acd7b936 100644 --- a/packages/cli/src/migration/migrator/framework-shim.ts +++ b/packages/cli/src/migration/migrator/framework-shim.ts @@ -1,8 +1,9 @@ -import fs from 'node:fs'; import path from 'node:path'; -import { readJsonFile } from '../../utils/json.ts'; +import fs from '../files.ts'; +import { readJsonFile } from '../files.ts'; import { type MigrationReport } from '../report.ts'; +import { isMigrationIgnored } from '../scope.ts'; // .svelte files are handled by @sveltejs/vite-plugin-svelte (transpilation) // and svelte-check / Svelte Language Server (type checking). @@ -87,6 +88,9 @@ export function addFrameworkShim( report?: MigrationReport, ): void { const envDtsPath = getEnvDtsPath(projectPath); + if (isMigrationIgnored(envDtsPath)) { + return; + } const shim = FRAMEWORK_SHIMS[framework]; if (fs.existsSync(envDtsPath)) { const existing = fs.readFileSync(envDtsPath, 'utf-8'); diff --git a/packages/cli/src/migration/migrator/git-hooks.ts b/packages/cli/src/migration/migrator/git-hooks.ts index 7273b40d4c..b82e67b4f9 100644 --- a/packages/cli/src/migration/migrator/git-hooks.ts +++ b/packages/cli/src/migration/migrator/git-hooks.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs'; import path from 'node:path'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; @@ -20,7 +19,8 @@ import type { PackageManager, WorkspacePackage } from '../../types/index.ts'; import { getVpCommand } from '../../utils/command.ts'; import { documentationUrl } from '../../utils/documentation.ts'; import { findGitRoot } from '../../utils/git.ts'; -import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts'; +import fs from '../files.ts'; +import { editJsonFile, isJsonFile, readJsonFile } from '../files.ts'; import { hasStagedConfigInViteConfig, mergeStagedConfigToViteConfig, @@ -28,6 +28,7 @@ import { removeLintStagedFromPackageJson, } from '../migrator.ts'; import { type MigrationReport } from '../report.ts'; +import { isMigrationIgnored } from '../scope.ts'; import { getCommandStartIndex, LINT_STAGED_ALL_CONFIG_FILES, @@ -462,6 +463,9 @@ export function installGitHooks( } // Install git hooks via vp config (--no-agent to skip agent setup, handled by migration) + if (isMigrationIgnored(path.join(projectPath, hooksDir, 'pre-commit'))) { + return false; + } const { command, args } = getVpCommand(['config', '--no-agent']); const configResult = spawn.sync(command, args, { cwd: projectPath, @@ -521,8 +525,11 @@ const DEFAULT_STAGED_CONFIG: Record = { '*': 'vp check --fix' }; export function createPreCommitHook(projectPath: string, dir = '.vite-hooks'): void { const huskyDir = path.join(projectPath, dir); - fs.mkdirSync(huskyDir, { recursive: true }); const hookPath = path.join(huskyDir, 'pre-commit'); + if (isMigrationIgnored(hookPath)) { + return; + } + fs.mkdirSync(huskyDir, { recursive: true }); if (fs.existsSync(hookPath)) { return; } diff --git a/packages/cli/src/migration/migrator/orchestrators.ts b/packages/cli/src/migration/migrator/orchestrators.ts index 3e515708e5..89cdc07a85 100644 --- a/packages/cli/src/migration/migrator/orchestrators.ts +++ b/packages/cli/src/migration/migrator/orchestrators.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { PackageManager, type WorkspaceInfo, type WorkspacePackage } from '../../types/index.ts'; import { VITE_PLUS_NAME, VITE_PLUS_VERSION, isForceOverrideMode } from '../../utils/constants.ts'; -import { editJsonFile, readJsonFile } from '../../utils/json.ts'; +import { editJsonFile, readJsonFile } from '../files.ts'; import { applyBuildAllowanceToPackageJsonPnpm, applyYarnWorkspaceHoistingFix, diff --git a/packages/cli/src/migration/migrator/oxlint-config.ts b/packages/cli/src/migration/migrator/oxlint-config.ts index b971d3fd49..7d2ebb1a7f 100644 --- a/packages/cli/src/migration/migrator/oxlint-config.ts +++ b/packages/cli/src/migration/migrator/oxlint-config.ts @@ -1,5 +1,4 @@ import { createHash } from 'node:crypto'; -import fs from 'node:fs'; import path from 'node:path'; import type { Program } from '@oxc-project/types'; @@ -8,6 +7,7 @@ import type { OxlintConfig } from 'oxlint'; import { analyzeMigrationSource, ResolverFactory } from '../../../binding/index.js'; import { detectConfigs } from '../detector.ts'; +import fs from '../files.ts'; import { ensureSvelteRuneGlobals } from '../migrator.ts'; import { OXLINT_OWNER_CONFIG_KEYS, diff --git a/packages/cli/src/migration/migrator/prettier.ts b/packages/cli/src/migration/migrator/prettier.ts index f837621375..a096f01978 100644 --- a/packages/cli/src/migration/migrator/prettier.ts +++ b/packages/cli/src/migration/migrator/prettier.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs'; import path from 'node:path'; import { styleText } from 'node:util'; @@ -7,13 +6,15 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts'; import { rewritePrettier } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; import { getVpCommand, runCommandSilently } from '../../utils/command.ts'; -import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { displayRelative } from '../../utils/path.ts'; import { cancelAndExit } from '../../utils/prompts.ts'; import { getSilentSpinner, getSpinner } from '../../utils/spinner.ts'; import { PRETTIER_CONFIG_FILES, PRETTIER_PACKAGE_JSON_CONFIG, detectConfigs } from '../detector.ts'; +import fs from '../files.ts'; +import { editJsonFile, readJsonFile } from '../files.ts'; import { rewriteToolLintStagedConfigFiles } from '../migrator.ts'; import { type MigrationReport } from '../report.ts'; +import { isMigrationIgnored } from '../scope.ts'; import { warnMigration } from './shared.ts'; export function detectPrettierProject( @@ -107,6 +108,13 @@ export async function migratePrettierToOxfmt( packages?: WorkspacePackage[], options?: { silent?: boolean; report?: MigrationReport }, ): Promise<'migrated' | 'skipped' | 'failed'> { + if ( + [prettierConfigFile, '.oxfmtrc.json'].some( + (file) => file && isMigrationIgnored(path.join(projectPath, file)), + ) + ) { + return 'skipped'; + } const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive); // Step 1: Generate .oxfmtrc.json from Prettier config diff --git a/packages/cli/src/migration/migrator/script-runner.ts b/packages/cli/src/migration/migrator/script-runner.ts index 8d68ee0894..351fc693a0 100644 --- a/packages/cli/src/migration/migrator/script-runner.ts +++ b/packages/cli/src/migration/migrator/script-runner.ts @@ -1,14 +1,14 @@ -import fs from 'node:fs'; import path from 'node:path'; import { parse as parseJsonc } from 'jsonc-parser'; import { type LiteralCommandToken, parseLiteralCommands } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; -import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts'; import { readNearestPackageJson } from '../../utils/package.ts'; import { displayRelative } from '../../utils/path.ts'; import { NODE_OPTIONS_WITH_VALUE, SCRIPT_EXTENSIONS } from '../../vpx-script.ts'; +import fs from '../files.ts'; +import { editJsonFile, isJsonFile, readJsonFile } from '../files.ts'; import { sourceTreeMatches } from '../migrator.ts'; import { type MigrationReport } from '../report.ts'; import { diff --git a/packages/cli/src/migration/migrator/setup.ts b/packages/cli/src/migration/migrator/setup.ts index a124ba8f25..1183c89b37 100644 --- a/packages/cli/src/migration/migrator/setup.ts +++ b/packages/cli/src/migration/migrator/setup.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs'; import path from 'node:path'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; @@ -8,8 +7,9 @@ import { parse as parseYaml } from 'yaml'; import { type DownloadPackageManagerResult } from '../../../binding/index.js'; import { SETUP_VP_VERSION } from '../../utils/constants.ts'; -import { editJsonFile } from '../../utils/json.ts'; import { detectConfigs } from '../detector.ts'; +import fs from '../files.ts'; +import { editJsonFile } from '../files.ts'; import { type MigrationReport } from '../report.ts'; import { isPlainRecord, warnMigration } from './shared.ts'; diff --git a/packages/cli/src/migration/migrator/source-scan.ts b/packages/cli/src/migration/migrator/source-scan.ts index 5f56f0731b..df44b567e5 100644 --- a/packages/cli/src/migration/migrator/source-scan.ts +++ b/packages/cli/src/migration/migrator/source-scan.ts @@ -3,10 +3,11 @@ import path from 'node:path'; import { type WorkspacePackage } from '../../types/index.ts'; import { VITE_PLUS_NAME } from '../../utils/constants.ts'; -import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { detectPackageMetadata } from '../../utils/package.ts'; import { hasVitestTypesInTsconfig } from '../../utils/tsconfig.ts'; +import { editJsonFile, readJsonFile } from '../files.ts'; import { projectUsesVitestDirectly } from '../migrator.ts'; +import { isMigrationIgnored } from '../scope.ts'; import { OPT_IN_BROWSER_PROVIDERS, OXLINT_PLUGINS_PACKAGE, @@ -229,6 +230,9 @@ export function sourceTreeMatches( } for (const entry of entries) { const entryPath = path.join(dir, entry.name); + if (isMigrationIgnored(entryPath, entry.isDirectory())) { + continue; + } if (entry.isDirectory()) { if (skipDirs.has(entry.name)) { continue; @@ -290,6 +294,9 @@ function findPackageTsconfigFiles(projectPath: string): string[] { } for (const entry of entries) { const entryPath = path.join(dir, entry.name); + if (isMigrationIgnored(entryPath, entry.isDirectory())) { + continue; + } if (entry.isDirectory()) { if (!VITEST_SCAN_SKIP_DIRS.has(entry.name)) { scanDir(entryPath, false); diff --git a/packages/cli/src/migration/migrator/tsconfig.ts b/packages/cli/src/migration/migrator/tsconfig.ts index 58782601f8..c1db8452c3 100644 --- a/packages/cli/src/migration/migrator/tsconfig.ts +++ b/packages/cli/src/migration/migrator/tsconfig.ts @@ -8,6 +8,7 @@ import { rewriteTypesInTsconfig, } from '../../utils/tsconfig.ts'; import { type MigrationReport } from '../report.ts'; +import { isMigrationIgnored } from '../scope.ts'; import { warnMigration } from './shared.ts'; export function cleanupDeprecatedTsconfigOptions( @@ -16,7 +17,7 @@ export function cleanupDeprecatedTsconfigOptions( report?: MigrationReport, ): void { const deprecatedOptions = ['esModuleInterop', 'allowSyntheticDefaultImports']; - const files = findTsconfigFiles(projectPath); + const files = findTsconfigFiles(projectPath).filter((file) => !isMigrationIgnored(file)); for (const filePath of files) { for (const name of deprecatedOptions) { if (removeDeprecatedTsconfigFalseOption(filePath, name)) { @@ -41,7 +42,7 @@ export function rewriteTsconfigTypes( report?: MigrationReport, ): boolean { let changed = false; - const files = findTsconfigFiles(projectPath); + const files = findTsconfigFiles(projectPath).filter((file) => !isMigrationIgnored(file)); for (const filePath of files) { if (rewriteTypesInTsconfig(filePath)) { changed = true; @@ -57,5 +58,7 @@ export function rewriteTsconfigTypes( } export function hasTsconfigTypesToRewrite(projectPath: string): boolean { - return findTsconfigFiles(projectPath).some((filePath) => hasTypesToRewriteInTsconfig(filePath)); + return findTsconfigFiles(projectPath) + .filter((file) => !isMigrationIgnored(file)) + .some((filePath) => hasTypesToRewriteInTsconfig(filePath)); } diff --git a/packages/cli/src/migration/migrator/tsup.ts b/packages/cli/src/migration/migrator/tsup.ts index d9e5846fe9..44f68b4479 100644 --- a/packages/cli/src/migration/migrator/tsup.ts +++ b/packages/cli/src/migration/migrator/tsup.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs'; import path from 'node:path'; import { styleText } from 'node:util'; @@ -8,12 +7,15 @@ import corePkg from 'vite/package.json' with { type: 'json' }; import { PackageManager, type WorkspacePackage } from '../../types/index.ts'; import { getVpCommand, runCommandSilently } from '../../utils/command.ts'; import { TSDOWN_MIGRATION_SKILL_URL } from '../../utils/constants.ts'; -import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { displayRelative } from '../../utils/path.ts'; import { cancelAndExit } from '../../utils/prompts.ts'; import { getSilentSpinner, getSpinner } from '../../utils/spinner.ts'; import { detectConfigs, TSUP_CONFIG_FILES, TSUP_PACKAGE_JSON_CONFIG } from '../detector.ts'; +import fs from '../files.ts'; +import { editJsonFile, readJsonFile } from '../files.ts'; import { addMigrationWarning, type MigrationReport } from '../report.ts'; +import { isMigrationIgnored } from '../scope.ts'; +import { warnMigration } from './shared.ts'; function showTsdownMigrationOptions( targetLabel = 'the project root', @@ -428,6 +430,17 @@ export async function migrateTsupToTsdown( const tsupTargets = targets.filter((target) => target === rootPath ? !!tsupConfigFile : !!detectConfigs(target).tsupConfig, ); + if ( + tsupTargets.some((target) => + TSDOWN_MIGRATION_FILES.some((file) => isMigrationIgnored(path.join(target, file))), + ) + ) { + warnMigration( + 'Skipped tsup migration because migrate.ignorePatterns excludes one of its input or output files.', + options?.report, + ); + return false; + } const configCollisions = collectTsupConfigCollisions(tsupTargets); if (configCollisions.length > 0) { prompts.log.warn( diff --git a/packages/cli/src/migration/migrator/vite-config.ts b/packages/cli/src/migration/migrator/vite-config.ts index 96e6e6d46f..2eaf6b5b94 100644 --- a/packages/cli/src/migration/migrator/vite-config.ts +++ b/packages/cli/src/migration/migrator/vite-config.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs'; import path from 'node:path'; import type { Program } from '@oxc-project/types'; @@ -24,10 +23,11 @@ import { import { type WorkspacePackage } from '../../types/index.ts'; import { BASEURL_TSCONFIG_WARNING, VITE_PLUS_NAME } from '../../utils/constants.ts'; import { documentationUrl } from '../../utils/documentation.ts'; -import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts'; import { displayRelative } from '../../utils/path.ts'; import { hasBaseUrlInTsconfig } from '../../utils/tsconfig.ts'; import { detectConfigs, type ConfigFiles } from '../detector.ts'; +import fs from '../files.ts'; +import { editJsonFile, isJsonFile, readJsonFile } from '../files.ts'; import { collectInstalledPackageNames, oxlintMigrationMarker, @@ -40,6 +40,7 @@ import { sanitizeInheritedJsPlugins, } from '../migrator.ts'; import { type MigrationReport } from '../report.ts'; +import { isMigrationIgnored, migrationScope } from '../scope.ts'; import { type OxlintConfigMigration, type OxlintModulePlan, @@ -195,6 +196,9 @@ export function mergeTsdownConfigFile( report?: MigrationReport, ): boolean { const configs = detectConfigs(projectPath); + if (isMigrationIgnored(path.join(projectPath, configs.viteConfig ?? 'vite.config.ts'))) { + return false; + } if (!configs.tsdownConfig) { return false; } @@ -203,6 +207,9 @@ export function mergeTsdownConfigFile( const fullViteConfigPath = path.join(projectPath, viteConfig); const fullTsdownConfigPath = path.join(projectPath, configs.tsdownConfig); + if (isMigrationIgnored(fullViteConfigPath) || isMigrationIgnored(fullTsdownConfigPath)) { + return false; + } // For JSON files, merge content directly and delete the file if (configs.tsdownConfig.endsWith('.json')) { @@ -271,6 +278,9 @@ export function mergeViteConfigFiles( preparedOxlintConfigs?: PreparedOxlintConfigs, ): void { const configs = detectConfigs(projectPath); + if (isMigrationIgnored(path.join(projectPath, configs.viteConfig ?? 'vite.config.ts'))) { + return; + } if (!configs.oxfmtConfig && !configs.oxlintConfig) { return; } @@ -587,6 +597,9 @@ function injectConfigDefaults( report?: MigrationReport, ): void { const configs = detectConfigs(projectPath); + if (isMigrationIgnored(path.join(projectPath, configs.viteConfig ?? 'vite.config.ts'))) { + return; + } if (configs.viteConfig && hasConfigKey(path.join(projectPath, configs.viteConfig), configKey)) { return; } @@ -647,6 +660,13 @@ function mergeAndRemoveJsonConfig( ): boolean { const fullViteConfigPath = path.join(projectPath, viteConfigPath); const fullJsonConfigPath = path.join(projectPath, jsonConfigPath); + if ( + isMigrationIgnored(fullViteConfigPath) || + isMigrationIgnored(fullJsonConfigPath) || + [...(modulePlan?.modules.keys() ?? [])].some((file) => isMigrationIgnored(file)) + ) { + return false; + } // Skip merge when the key is already present in vite.config.ts — the Rust // merge step always prepends, so without this guard a template that ships // both an inline `${configKey}:` block and a standalone JSON file (e.g. @@ -760,6 +780,9 @@ export function mergeStagedConfigToViteConfig( report?: MigrationReport, ): boolean { const configs = detectConfigs(projectPath); + if (isMigrationIgnored(path.join(projectPath, configs.viteConfig ?? 'vite.config.ts'))) { + return false; + } const viteConfig = ensureViteConfig(projectPath, configs, silent, report); const fullViteConfigPath = path.join(projectPath, viteConfig); @@ -901,7 +924,13 @@ export function rewriteAllImports( // `collectOxlintOwnerDirs`. oxlintOwnerDirs: string[] = [], ): boolean { - const result = rewriteImportsInDirectory(projectPath, preserveNuxtVitestImports, oxlintOwnerDirs); + const result = rewriteImportsInDirectory( + projectPath, + preserveNuxtVitestImports, + oxlintOwnerDirs, + migrationScope()?.root, + migrationScope()?.patterns, + ); const modified = result.modifiedFiles.length; const preserved = result.preservedVitestFiles.length; const errors = result.errors.length; diff --git a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts index 5a4197d669..33bf198171 100644 --- a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts +++ b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs'; import path from 'node:path'; import { PackageManager, type WorkspaceInfo, type WorkspacePackage } from '../../types/index.ts'; @@ -9,9 +8,10 @@ import { VITE_PLUS_VERSION, isForceOverrideMode, } from '../../utils/constants.ts'; -import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { type NpmWorkspaces } from '../../utils/workspace.ts'; -import { editYamlFile, readYamlFile, type YamlDocument } from '../../utils/yaml.ts'; +import fs from '../files.ts'; +import { editJsonFile, readJsonFile } from '../files.ts'; +import { editYamlFile, readYamlFile, type YamlDocument } from '../files.ts'; import { alignVitestEcosystemPackages, applyBuildAllowanceToPackageJsonPnpm, @@ -53,6 +53,7 @@ import { yarnrcSatisfiesVitePlus, } from '../migrator.ts'; import { type DependencyVersionChange, type MigrationReport } from '../report.ts'; +import { preserveSharedDependencies } from '../scope.ts'; import { BROWSER_PROVIDER_PEER_DEPS, hasProviderPeerDependency, @@ -135,6 +136,9 @@ export function overridesSatisfyVitePlus( keyStyle: ManagedOverrideKeyStyle = 'bare', usesWebdriverioProvider = false, ): boolean { + if (preserveSharedDependencies()) { + return true; + } const managed = managedOverridePackages(usesVitest, usesWebdriverioProvider); // Common case: a lingering managed `vitest` override is not satisfied. It // must be removed, so the bootstrap stays pending until then. Both key @@ -881,6 +885,9 @@ function ensureOverrideEntries( keyStyle: ManagedOverrideKeyStyle = 'bare', usesWebdriverioProvider = false, ): { overrides: Record; changed: boolean } { + if (preserveSharedDependencies()) { + return { overrides: overrides ?? {}, changed: false }; + } const next = { ...overrides }; let changed = false; // Common case: drop a lingering managed `vitest` override. diff --git a/packages/cli/src/migration/migrator/vitest-ecosystem.ts b/packages/cli/src/migration/migrator/vitest-ecosystem.ts index ffff16ac75..743d93b19f 100644 --- a/packages/cli/src/migration/migrator/vitest-ecosystem.ts +++ b/packages/cli/src/migration/migrator/vitest-ecosystem.ts @@ -10,7 +10,11 @@ import { VITE_PLUS_OVERRIDE_PACKAGES, } from '../../utils/constants.ts'; import { readJsonFile } from '../../utils/json.ts'; -import { extractOverrideTargetName } from '../../utils/package-overrides.ts'; +import { + extractOverrideParentSegments, + extractOverrideTargetName, +} from '../../utils/package-overrides.ts'; +export { extractOverrideParentSegments } from '../../utils/package-overrides.ts'; import { detectPackageMetadata } from '../../utils/package.ts'; import { isAlignableVitestEcosystemPackage } from '../../utils/vitest-ecosystem.ts'; import { @@ -60,16 +64,6 @@ function isRemovePackageOverrideKey(key: string): boolean { ); } -// Strip a trailing `@version`/range from a selector segment and keep its scope. -// Mirrors the version-suffix peeling in `extractOverrideTargetName`: the version -// `@` follows the name (after the `/` of a scoped name); the leading scope `@` -// is never a version separator. -function stripSegmentVersion(segment: string): string { - const nameStart = segment.startsWith('@') ? segment.indexOf('/') + 1 : 0; - const versionAt = segment.indexOf('@', nameStart); - return versionAt > 0 ? segment.slice(0, versionAt) : segment; -} - // True iff a single parent-NAME glob segment matches the given literal package // name. `*` matches any run of characters; all other glob/regex metacharacters // are escaped. Used for the concrete ancestor segments of a selector. @@ -130,64 +124,6 @@ function parentChainReachesVitePlus(segments: string[]): boolean { return true; } -// Extract the ordered PARENT chain of an override/resolution key — the ancestor -// segments above the forced TARGET — or `null` when the key has no parent -// selector (a bare/versioned global pin). Each segment's own `@version`/range is -// stripped and scoped names (`@scope/name`) are kept whole; glob segments (`**`, -// `vite-*`) are preserved verbatim for `parentChainReachesVitePlus`. -// -// Mirrors `extractOverrideTargetName`'s grammar so target and parent stay -// consistent (see that function for the full delimiter rationale): -// - pnpm `a>b>child`: every `>`-separated prefix is a parent level (`a`, `b`); -// pnpm has no globs, so a chain of length > 1 always carries a specific -// ancestor. -// - yarn `from/descriptor`: the descriptor is the trailing 1 (unscoped) or 2 -// (scoped) segments; the remaining leading `/`-segments are the `from` chain, -// with scoped ancestors (`@scope/name`) rejoined. -// - bare/versioned names (`pkg`, `@scope/pkg`, `pkg@4`) have NO parent → `null`. -export function extractOverrideParentSegments(key: string): string[] | null { - let rest = key.trim(); - // Peel every pnpm `>` parent level. pnpm splits at a `>` whose preceding char - // is NOT space, `|`, or `@` (its DELIMITER_REGEX), so semver comparators like - // `pkg@>=4` are not mistaken for a parent selector. - const pnpmParents: string[] = []; - for (let delim = rest.search(/[^ |@]>/); delim !== -1; delim = rest.search(/[^ |@]>/)) { - pnpmParents.push(stripSegmentVersion(rest.slice(0, delim + 1).trim())); - rest = rest.slice(delim + 2).trim(); - } - if (pnpmParents.length > 0) { - return pnpmParents; - } - // No pnpm parent — check for a yarn `from/descriptor` selector. `rest` is the - // child (target) descriptor; only a `/` beyond a single scoped name leaves a - // leading `from` (parent) chain. - if (!rest.includes('/')) { - return null; - } - const segments = rest.split('/'); - // The trailing descriptor occupies the last 2 segments when it is a scoped - // name (second-to-last segment starts with `@`), else the last 1. - const descriptorIsScoped = segments[segments.length - 2]?.startsWith('@') ?? false; - const descriptorSegmentCount = descriptorIsScoped ? 2 : 1; - const rawParents = segments.slice(0, segments.length - descriptorSegmentCount); - if (rawParents.length === 0) { - // The whole key was a bare scoped name (`@scope/pkg`) — no parent selector. - return null; - } - // Rejoin scoped ancestors (`@scope` + `name`) and strip each segment's version. - const parents: string[] = []; - for (let i = 0; i < rawParents.length; i += 1) { - const segment = rawParents[i]; - if (segment.startsWith('@') && i + 1 < rawParents.length) { - parents.push(stripSegmentVersion(`${segment}/${rawParents[i + 1]}`)); - i += 1; - } else { - parents.push(stripSegmentVersion(segment)); - } - } - return parents; -} - // True iff a provider override/resolution key (target ∈ // PROVIDER_OVERRIDE_DROP_NAMES) should be dropped because the pin would affect // vite-plus's OWN direct provider dep. The pin reaches that dep iff its parent diff --git a/packages/cli/src/migration/migrator/vitest-v5.ts b/packages/cli/src/migration/migrator/vitest-v5.ts index 3113afaf90..41907f0515 100644 --- a/packages/cli/src/migration/migrator/vitest-v5.ts +++ b/packages/cli/src/migration/migrator/vitest-v5.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs'; import path from 'node:path'; import { applyEdits, findNodeAtLocation, parse as parseJsonc, parseTree } from 'jsonc-parser'; @@ -22,7 +21,9 @@ import cliPackage from '../../../package.json' with { type: 'json' }; import { PackageManager, type WorkspaceInfoOptional } from '../../types/index.ts'; import { findGitRoot } from '../../utils/git.ts'; import { detectPackageMetadata } from '../../utils/package.ts'; +import fs from '../files.ts'; import { createCatalogDependencyResolver } from '../migrator.ts'; +import { isMigrationIgnored } from '../scope.ts'; import type { RewriteResult, SourceOptions, VitestV5Finding } from '../vitest-v5/ast.ts'; import { migrateVitestV5Command } from '../vitest-v5/commands.ts'; import { @@ -141,6 +142,9 @@ function filesInProject(directory: string): string[] { } for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const file = path.join(current, entry.name); + if (isMigrationIgnored(file, entry.isDirectory())) { + continue; + } // Never follow symlinks into another package or outside the workspace. if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) { walk(file); diff --git a/packages/cli/src/migration/migrator/yarn.ts b/packages/cli/src/migration/migrator/yarn.ts index fb39f76881..d773455258 100644 --- a/packages/cli/src/migration/migrator/yarn.ts +++ b/packages/cli/src/migration/migrator/yarn.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -11,8 +10,9 @@ import { VITE_PLUS_NAME, VITE_PLUS_VERSION, } from '../../utils/constants.ts'; -import { readJsonFile } from '../../utils/json.ts'; -import { editYamlFile, readYamlFile, scalarString } from '../../utils/yaml.ts'; +import fs from '../files.ts'; +import { readJsonFile } from '../files.ts'; +import { editYamlFile, readYamlFile, scalarString } from '../files.ts'; import { createCatalogDependencyResolverFromCatalogs, overridesSatisfyVitePlus, diff --git a/packages/cli/src/migration/scope.ts b/packages/cli/src/migration/scope.ts new file mode 100644 index 0000000000..6910c1e4c6 --- /dev/null +++ b/packages/cli/src/migration/scope.ts @@ -0,0 +1,218 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { readStaticMigrationConfig } from '../../binding/index.js'; +import { migrationIgnorePatterns } from '../migrate-config.ts'; +import { createMigrationIgnoreMatcher } from '../migration-ignore.ts'; +import { findViteConfig, resolveViteConfig } from '../resolve-vite-config.ts'; +import { PackageManager } from '../types/index.ts'; +import { VITEST_VERSION, VITE_PLUS_OVERRIDE_PACKAGES } from '../utils/constants.ts'; +import { readJsonFile } from '../utils/json.ts'; +import { + extractOverrideParentSegments, + extractOverrideTargetName, +} from '../utils/package-overrides.ts'; +import { isAlignableVitestEcosystemPackage } from '../utils/vitest-ecosystem.ts'; +import { readYamlFile } from '../utils/yaml.ts'; + +export class MigrationScope { + readonly matcher; + readonly ignoredManifests: string[] = []; + + constructor( + readonly root: string, + readonly patterns: string[], + ) { + this.matcher = createMigrationIgnoreMatcher(root, patterns); + if (patterns.length > 0) { + this.collectIgnoredManifests(root); + } + } + + ignores(file: string, isDirectory = false): boolean { + return this.matcher.isIgnored(path.resolve(file), isDirectory); + } + + private collectIgnoredManifests(dir: string): void { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const file = path.join(dir, entry.name); + if (entry.isDirectory() && !['node_modules', '.git', '.yarn'].includes(entry.name)) { + this.collectIgnoredManifests(file); + } else if (entry.isFile() && entry.name === 'package.json' && this.ignores(file)) { + this.ignoredManifests.push(file); + } + } + } + + get preserveSharedDependencies(): boolean { + return this.ignoredManifests.length > 0; + } +} + +const scopes = new AsyncLocalStorage(); +export const withMigrationScope = (scope: MigrationScope, run: () => T): T => + scopes.run(scope, run); +export const migrationScope = () => scopes.getStore(); +export const isMigrationIgnored = (file: string, directory = false): boolean => + migrationScope()?.ignores(file, directory) ?? false; +export const preserveSharedDependencies = (): boolean => + migrationScope()?.preserveSharedDependencies ?? false; + +async function readMigrationPatterns(root: string): Promise { + const extracted = readStaticMigrationConfig(root); + const config = + extracted == null ? (await resolveViteConfig(root)).migrate : JSON.parse(extracted); + return migrationIgnorePatterns(config); +} + +// Package-manager discovery treats a non-member example as a standalone project. +// An explicit target inside this repository must still honor the root policy. +// A nested Git repository (for example a cloned template) is independent. +export async function checkEnclosingMigrationPolicy(root: string): Promise { + let dir = root; + while (!fs.existsSync(path.join(dir, '.git'))) { + const parent = path.dirname(dir); + if (parent === dir) { + return; + } + dir = parent; + if (!fs.existsSync(path.join(dir, 'package.json')) || !findViteConfig(dir)) { + continue; + } + const patterns = await readMigrationPatterns(dir); + if (patterns.length > 0) { + const ignored = createMigrationIgnoreMatcher(dir, patterns).isIgnored(root, true); + throw new Error( + ignored + ? `Cannot migrate ${path.relative(dir, root)}: excluded by migrate.ignorePatterns in ${findViteConfig(dir)}. No project files were changed.` + : `Run vp migrate from ${dir} so its migrate.ignorePatterns policy also applies to nested files. No project files were changed.`, + ); + } + } +} + +export async function loadMigrationScope( + root: string, + packageManager?: PackageManager, +): Promise { + const scope = new MigrationScope(root, await readMigrationPatterns(root)); + if (scope.preserveSharedDependencies) { + checkSharedOverrides(scope, packageManager); + } + return scope; +} + +// Catalogs and overrides belong to every consumer, including templates outside +// the workspace package list. Use explicit specs for migrated consumers and +// retain these shared settings in their original locations. +export const sharedDependencyFields = [ + ['catalog'], + ['catalogs'], + ['overrides'], + ['resolutions'], + ['pnpm', 'overrides'], + ['workspaces', 'catalog'], + ['workspaces', 'catalogs'], +] as const; + +function checkSharedOverrides(scope: MigrationScope, packageManager?: PackageManager): void { + const pkg = readJsonFile(path.join(scope.root, 'package.json')); + const settings = [pkg]; + for (const name of ['pnpm-workspace.yaml', '.yarnrc.yml']) { + if ( + packageManager && + name !== + (packageManager === PackageManager.pnpm + ? 'pnpm-workspace.yaml' + : packageManager === PackageManager.yarn + ? '.yarnrc.yml' + : '') + ) { + continue; + } + const file = path.join(scope.root, name); + if (fs.existsSync(file)) { + settings.push(readYamlFile(file) ?? {}); + } + } + const catalogs = + settings.find((value) => value.catalog || value.catalogs) ?? + (pkg.workspaces as Record | undefined) ?? + {}; + const resolveSpec = (spec: string, name: string): unknown => { + if (spec.startsWith('catalog:')) { + const catalogName = spec.slice(8) || 'default'; + const named = catalogs.catalogs as Record> | undefined; + return ( + catalogName === 'default' + ? ((catalogs.catalog as Record | undefined) ?? named?.default) + : named?.[catalogName] + )?.[name]; + } + if (spec.startsWith('$')) { + const dependency = spec.slice(1); + return ( + (pkg.dependencies as Record | undefined)?.[dependency] ?? + (pkg.devDependencies as Record | undefined)?.[dependency] + ); + } + return spec; + }; + const excludedNames = new Set( + scope.ignoredManifests.flatMap((file) => { + try { + return [readJsonFile(file).name]; + } catch { + return []; + } + }), + ); + const check = (overrides: unknown, parents: string[] = []) => { + if (!overrides || typeof overrides !== 'object') { + return; + } + for (const [selector, value] of Object.entries(overrides)) { + const key = selector === '.' ? parents.at(-1)! : selector; + const name = extractOverrideTargetName(key); + const ancestors = selector === '.' ? parents.slice(0, -1) : parents; + const chain = ancestors + .map(extractOverrideTargetName) + .concat(extractOverrideParentSegments(key) ?? []); + // An override scoped exclusively to an excluded package can stay as-is. + // For other selectors we cannot prove that the managed graph is isolated. + const excludedOnly = chain.length > 0 && excludedNames.has(chain[0]); + if ( + typeof value === 'string' && + (name === 'vite' || name === 'vitest' || isAlignableVitestEcosystemPackage(name)) && + !excludedOnly + ) { + const target = name === 'vite' ? VITE_PLUS_OVERRIDE_PACKAGES.vite : VITEST_VERSION; + if (resolveSpec(value, name) !== target) { + throw new Error( + `Cannot migrate while preserving excluded packages: override ${JSON.stringify(selector)} (${JSON.stringify(value)}) conflicts with ${JSON.stringify(target)}. Review the root override or migrate the excluded packages first. No project files were changed.`, + ); + } + } + if (value && typeof value === 'object') { + check(value, [...parents, selector]); + } + } + }; + for (const value of settings) { + if ( + !packageManager || + packageManager === PackageManager.npm || + packageManager === PackageManager.bun || + (packageManager === PackageManager.pnpm && value !== pkg) + ) { + check(value.overrides); + } + if (!packageManager || packageManager === PackageManager.yarn) { + check(value.resolutions); + } + if (!packageManager || packageManager === PackageManager.pnpm) { + check((value.pnpm as Record | undefined)?.overrides); + } + } +} diff --git a/packages/cli/src/oxlint-plugin.ts b/packages/cli/src/oxlint-plugin.ts index 55ab819997..54be443f9a 100644 --- a/packages/cli/src/oxlint-plugin.ts +++ b/packages/cli/src/oxlint-plugin.ts @@ -5,6 +5,7 @@ import { definePlugin, defineRule } from '@oxlint/plugins'; import type { Context, ESTree } from '@oxlint/plugins'; import cliPackage from '../package.json' with { type: 'json' }; +import { createMigrationIgnoreMatcher } from './migration-ignore.ts'; import { PREFER_VITE_PLUS_IMPORTS_RULE_NAME, VITE_PLUS_OXLINT_PLUGIN_NAME, @@ -438,6 +439,8 @@ export const preferVitePlusImportsRule = defineRule({ }, }, createOnce(context: Context) { + const matchers = new Map>(); + let ignored = false; const options: ImportRewriteOptions = { preserveUpstreamVitest: false, fileIsViteConfig: false, @@ -447,6 +450,22 @@ export const preferVitePlusImportsRule = defineRule({ }; return { Program() { + const policy = context.settings['vite-plus/migrate'] as + | { root: string; ignorePatterns: string[] } + | undefined; + let ignore: ReturnType | undefined; + if (policy) { + const key = JSON.stringify(policy); + ignore = matchers.get(key); + if (!ignore) { + ignore = createMigrationIgnoreMatcher(policy.root, policy.ignorePatterns); + matchers.set(key, ignore); + } + } + ignored = ignore?.isIgnored(path.resolve(context.filename), false) ?? false; + if (ignored) { + return; + } options.preserveUpstreamVitest = nearestPackageUsesNuxtTestUtils(context.filename); options.fileIsViteConfig = isViteConfigFile(context.filename); options.ownsOxlintApi = nearestPackageOwnsOxlintApi(context.filename); @@ -454,23 +473,41 @@ export const preferVitePlusImportsRule = defineRule({ options.keepsVitest = nearestPackageKeepsVitest(context.filename); }, ImportDeclaration(node) { + if (ignored) { + return; + } maybeReportLiteral(context, node.source, options); reportLegacyOxlintPluginApiImport(context, node, options.ownsOxlintApi); }, ExportAllDeclaration(node) { + if (ignored) { + return; + } maybeReportLiteral(context, node.source, options); }, ExportNamedDeclaration(node) { + if (ignored) { + return; + } maybeReportLiteral(context, node.source, options); reportLegacyOxlintPluginApiExport(context, node, options.ownsOxlintApi); }, ImportExpression(node) { + if (ignored) { + return; + } maybeReportLiteral(context, node.source, options); }, TSImportType(node) { + if (ignored) { + return; + } maybeReportLiteral(context, node.source, options); }, TSExternalModuleReference(node) { + if (ignored) { + return; + } // Keep import-equals declarations unchanged, matching the migrator's // treatment of require calls. if ( @@ -483,6 +520,9 @@ export const preferVitePlusImportsRule = defineRule({ maybeReportLiteral(context, node.expression, options); }, TSModuleDeclaration(node) { + if (ignored) { + return; + } if (node.global) { return; } diff --git a/packages/cli/src/utils/agent.ts b/packages/cli/src/utils/agent.ts index 4c940f38f6..d3360a2854 100644 --- a/packages/cli/src/utils/agent.ts +++ b/packages/cli/src/utils/agent.ts @@ -418,6 +418,7 @@ export async function writeAgentInstructions({ interactive, conflictDecisions, silent = false, + shouldWriteFile = () => true, }: { projectRoot: string; targetPath?: string; @@ -425,6 +426,7 @@ export async function writeAgentInstructions({ interactive: boolean; conflictDecisions?: Map; silent?: boolean; + shouldWriteFile?: (file: string) => boolean; }) { const paths = [...(targetPaths ?? []), ...(targetPath ? [targetPath] : [])]; if (paths.length === 0) { @@ -445,6 +447,9 @@ export async function writeAgentInstructions({ for (const targetPathToWrite of orderAgentTargetPaths(projectRoot, paths)) { const destinationPath = path.join(projectRoot, targetPathToWrite); + if (!shouldWriteFile(destinationPath)) { + continue; + } await fsPromises.mkdir(path.dirname(destinationPath), { recursive: true }); diff --git a/packages/cli/src/utils/editor.ts b/packages/cli/src/utils/editor.ts index f986fb847f..4606781459 100644 --- a/packages/cli/src/utils/editor.ts +++ b/packages/cli/src/utils/editor.ts @@ -338,6 +338,7 @@ export async function writeEditorConfigs({ silent = false, extraVsCodeSettings, packageManager = PackageManager.pnpm, + shouldWriteFile = () => true, }: { projectRoot: string; editorId: EditorSelection; @@ -346,6 +347,7 @@ export async function writeEditorConfigs({ silent?: boolean; extraVsCodeSettings?: Record; packageManager?: PackageManager; + shouldWriteFile?: (file: string) => boolean; }) { const editorIds = normalizeEditorSelection(editorId); if (editorIds.length === 0) { @@ -361,6 +363,7 @@ export async function writeEditorConfigs({ silent, extraVsCodeSettings, packageManager, + shouldWriteFile, }); } } @@ -373,6 +376,7 @@ async function writeEditorConfig({ silent, extraVsCodeSettings, packageManager, + shouldWriteFile, }: { projectRoot: string; editorId: EditorId; @@ -381,6 +385,7 @@ async function writeEditorConfig({ silent: boolean; extraVsCodeSettings?: Record; packageManager: PackageManager; + shouldWriteFile: (file: string) => boolean; }) { const editorConfig = EDITORS.find((e) => e.id === editorId); if (!editorConfig) { @@ -388,7 +393,6 @@ async function writeEditorConfig({ } const targetDir = path.join(projectRoot, editorConfig.targetDir); - await fsPromises.mkdir(targetDir, { recursive: true }); for (const [fileName, baseIncoming] of Object.entries(editorConfig.files)) { const incoming: EditorConfigValue = @@ -398,6 +402,10 @@ async function writeEditorConfig({ ? jetbrainsWorkspaceConfig(packageManager) : baseIncoming; const filePath = path.join(targetDir, fileName); + if (!shouldWriteFile(filePath)) { + continue; + } + await fsPromises.mkdir(targetDir, { recursive: true }); const jsonFormat = isJsonLikeFile(fileName); if (fs.existsSync(filePath)) { diff --git a/packages/cli/src/utils/package-overrides.ts b/packages/cli/src/utils/package-overrides.ts index f5a7234034..a8dd146540 100644 --- a/packages/cli/src/utils/package-overrides.ts +++ b/packages/cli/src/utils/package-overrides.ts @@ -30,3 +30,71 @@ export function extractOverrideTargetName(key: string): string { const versionAt = target.indexOf('@', nameStart); return versionAt > 0 ? target.slice(0, versionAt) : target; } + +// Strip a trailing `@version`/range from a selector segment and keep its scope. +// Mirrors the version-suffix peeling in `extractOverrideTargetName`: the version +// `@` follows the name (after the `/` of a scoped name); the leading scope `@` +// is never a version separator. +function stripSegmentVersion(segment: string): string { + const nameStart = segment.startsWith('@') ? segment.indexOf('/') + 1 : 0; + const versionAt = segment.indexOf('@', nameStart); + return versionAt > 0 ? segment.slice(0, versionAt) : segment; +} + +// Extract the ordered PARENT chain of an override/resolution key — the ancestor +// segments above the forced TARGET — or `null` when the key has no parent +// selector (a bare/versioned global pin). Each segment's own `@version`/range is +// stripped and scoped names (`@scope/name`) are kept whole; glob segments (`**`, +// `vite-*`) are preserved verbatim for `parentChainReachesVitePlus`. +// +// Mirrors `extractOverrideTargetName`'s grammar so target and parent stay +// consistent (see that function for the full delimiter rationale): +// - pnpm `a>b>child`: every `>`-separated prefix is a parent level (`a`, `b`); +// pnpm has no globs, so a chain of length > 1 always carries a specific +// ancestor. +// - yarn `from/descriptor`: the descriptor is the trailing 1 (unscoped) or 2 +// (scoped) segments; the remaining leading `/`-segments are the `from` chain, +// with scoped ancestors (`@scope/name`) rejoined. +// - bare/versioned names (`pkg`, `@scope/pkg`, `pkg@4`) have NO parent → `null`. +export function extractOverrideParentSegments(key: string): string[] | null { + let rest = key.trim(); + // Peel every pnpm `>` parent level. pnpm splits at a `>` whose preceding char + // is NOT space, `|`, or `@` (its DELIMITER_REGEX), so semver comparators like + // `pkg@>=4` are not mistaken for a parent selector. + const pnpmParents: string[] = []; + for (let delim = rest.search(/[^ |@]>/); delim !== -1; delim = rest.search(/[^ |@]>/)) { + pnpmParents.push(stripSegmentVersion(rest.slice(0, delim + 1).trim())); + rest = rest.slice(delim + 2).trim(); + } + if (pnpmParents.length > 0) { + return pnpmParents; + } + // No pnpm parent — check for a yarn `from/descriptor` selector. `rest` is the + // child (target) descriptor; only a `/` beyond a single scoped name leaves a + // leading `from` (parent) chain. + if (!rest.includes('/')) { + return null; + } + const segments = rest.split('/'); + // The trailing descriptor occupies the last 2 segments when it is a scoped + // name (second-to-last segment starts with `@`), else the last 1. + const descriptorIsScoped = segments[segments.length - 2]?.startsWith('@') ?? false; + const descriptorSegmentCount = descriptorIsScoped ? 2 : 1; + const rawParents = segments.slice(0, segments.length - descriptorSegmentCount); + if (rawParents.length === 0) { + // The whole key was a bare scoped name (`@scope/pkg`) — no parent selector. + return null; + } + // Rejoin scoped ancestors (`@scope` + `name`) and strip each segment's version. + const parents: string[] = []; + for (let i = 0; i < rawParents.length; i += 1) { + const segment = rawParents[i]; + if (segment.startsWith('@') && i + 1 < rawParents.length) { + parents.push(stripSegmentVersion(`${segment}/${rawParents[i + 1]}`)); + i += 1; + } else { + parents.push(stripSegmentVersion(segment)); + } + } + return parents; +} From cee08dc995e14380a6b0b9221478d67b237ff7d2 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 10 Nov 2026 16:23:32 +0800 Subject: [PATCH 2/4] docs(migrate): place exclusions after migration steps --- docs/guide/migrate.md | 45 +++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/docs/guide/migrate.md b/docs/guide/migrate.md index 2ccd47aefa..15328fb36d 100644 --- a/docs/guide/migrate.md +++ b/docs/guide/migrate.md @@ -45,6 +45,24 @@ vp migrate my-app ## Migration Flow +### Steps + +The `migrate` command is designed to move existing projects onto Vite+ quickly. Here is what the command does: + +- Updates project dependencies +- Rewrites imports where needed +- Merges tool-specific config into `vite.config.ts` +- Updates scripts to the Vite+ command surface +- Can set up commit hooks +- Can write agent and editor configuration files +- Formats the migrated project + +See [Migration Rules](./migrate-rules.md) for the exact dependency, source rewrite, and package-manager behavior. + +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. + ### Excluding templates and examples Use `migrate.ignorePatterns` in the workspace-root `vite.config.ts` to keep @@ -86,9 +104,12 @@ an incompatible toolchain override must be resolved before migration can proceed Catalog references in an excluded template still require its original workspace. The `vite-plus/prefer-vite-plus-imports` lint rule also respects these exclusions, -including during `vp lint --fix` and `vp check --fix`. Other lint rules and normal -formatting remain enabled. To disable only the import rule with existing lint -configuration, use: +including during `vp lint --fix` and `vp check --fix`. No additional lint override +is needed when `migrate.ignorePatterns` is set. Other lint rules and normal +formatting remain enabled. + +For lint-only exclusions, or as a workaround in versions without +`migrate.ignorePatterns`, use the existing lint configuration: ```ts lint: { @@ -104,24 +125,6 @@ lint: { This lint override disables both diagnostics and autofix for that rule. It does not exclude files from `vp migrate`. -### Steps - -The `migrate` command is designed to move existing projects onto Vite+ quickly. Here is what the command does: - -- Updates project dependencies -- Rewrites imports where needed -- Merges tool-specific config into `vite.config.ts` -- Updates scripts to the Vite+ command surface -- Can set up commit hooks -- Can write agent and editor configuration files -- Formats the migrated project - -See [Migration Rules](./migrate-rules.md) for the exact dependency, source rewrite, and package-manager behavior. - -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. - ## Recommended Workflow Before running the migration: From cada9df4841f77eb023c3495b145d5f7d8fea0a5 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 10 Nov 2026 16:24:53 +0800 Subject: [PATCH 3/4] docs(migrate): remove redundant lint override example --- docs/guide/migrate.md | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/docs/guide/migrate.md b/docs/guide/migrate.md index 15328fb36d..41940819fb 100644 --- a/docs/guide/migrate.md +++ b/docs/guide/migrate.md @@ -103,27 +103,9 @@ add global toolchain overrides. Existing overrides still apply during installati an incompatible toolchain override must be resolved before migration can proceed. Catalog references in an excluded template still require its original workspace. -The `vite-plus/prefer-vite-plus-imports` lint rule also respects these exclusions, -including during `vp lint --fix` and `vp check --fix`. No additional lint override -is needed when `migrate.ignorePatterns` is set. Other lint rules and normal -formatting remain enabled. - -For lint-only exclusions, or as a workaround in versions without -`migrate.ignorePatterns`, use the existing lint configuration: - -```ts -lint: { - overrides: [ - { - files: ['templates/**', 'examples/**'], - rules: { 'vite-plus/prefer-vite-plus-imports': 'off' }, - }, - ], -}, -``` - -This lint override disables both diagnostics and autofix for that rule. It does -not exclude files from `vp migrate`. +`migrate.ignorePatterns` also applies to the `vite-plus/prefer-vite-plus-imports` +lint rule, including during `vp lint --fix` and `vp check --fix`. Other lint rules +and normal formatting remain enabled. ## Recommended Workflow From 80778bae28725c62a6f503579489db97e732ee31 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 12 Nov 2026 21:27:28 +0800 Subject: [PATCH 4/4] fix(migrate): respect import lint rule configuration --- Cargo.lock | 2 + .../migration_ignore_patterns/snapshots.toml | 126 ---------- ...tion_ignore_patterns_baseurl_references.md | 43 ---- ...gration_ignore_patterns_explicit_target.md | 27 --- .../migration_ignore_patterns_invalid.md | 28 --- ...ation_ignore_patterns_override_conflict.md | 47 ---- ...gration_ignore_patterns_parent_negation.md | 35 --- ...igration_ignore_patterns_runtime_config.md | 39 ---- .../migration_ignore_patterns/vite.config.ts | 8 - .../examples/standalone/package.json | 0 .../examples/standalone/unit.test.ts | 0 .../package.json | 2 +- .../packages/app/package.json | 0 .../packages/app/unit.test.ts | 0 .../packages/app/vite.config.ts | 0 .../pnpm-workspace.yaml | 0 .../migration_import_rule/snapshots.toml | 96 ++++++++ .../migration_import_rule_extends.md | 65 ++++++ .../snapshots/migration_import_rule_fresh.md} | 76 +++--- .../snapshots/migration_import_rule_full.md} | 84 +++---- .../migration_import_rule_global_off.md | 65 ++++++ .../migration_import_rule_runtime.md | 66 ++++++ .../migration_import_rule_upgrade.md} | 74 +++--- .../templates/managed/package.json | 0 .../templates/managed/unit.test.ts | 0 .../templates/starter/package.json | 0 .../templates/starter/tsconfig.json | 0 .../templates/starter/unit.test.ts | 0 .../templates/starter/vite.config.ts | 0 .../migration_import_rule/vite.config.ts | 11 + .../snapshots.toml | 4 +- .../migration_workspace_vite_template.md | 4 +- crates/vp_migration/Cargo.toml | 2 + crates/vp_migration/src/file_walker.rs | 68 ------ crates/vp_migration/src/import_rewriter.rs | 44 ++-- crates/vp_migration/src/import_rule.rs | 101 ++++++++ crates/vp_migration/src/lib.rs | 4 +- crates/vp_static_config/src/lib.rs | 22 +- docs/guide/migrate.md | 55 ++--- packages/cli/binding/index.cjs | 3 +- packages/cli/binding/index.d.cts | 12 +- packages/cli/binding/src/migration.rs | 37 +-- packages/cli/src/define-config.ts | 25 +- packages/cli/src/index.ts | 2 - packages/cli/src/migrate-config.ts | 21 -- packages/cli/src/migration-ignore.ts | 7 - .../__tests__/ignore-patterns.spec.ts | 162 ------------- .../migration/__tests__/import-rule.spec.ts | 101 ++++++++ packages/cli/src/migration/bin.ts | 43 +--- packages/cli/src/migration/files.ts | 113 --------- packages/cli/src/migration/format.ts | 25 +- packages/cli/src/migration/migrator/README.md | 7 - .../cli/src/migration/migrator/catalog.ts | 18 +- .../migration/migrator/core-finalization.ts | 12 +- packages/cli/src/migration/migrator/eslint.ts | 45 ++-- .../src/migration/migrator/framework-shim.ts | 8 +- .../cli/src/migration/migrator/git-hooks.ts | 13 +- .../src/migration/migrator/orchestrators.ts | 8 +- .../src/migration/migrator/oxlint-config.ts | 2 +- .../cli/src/migration/migrator/prettier.ts | 12 +- .../src/migration/migrator/script-runner.ts | 4 +- packages/cli/src/migration/migrator/setup.ts | 4 +- .../cli/src/migration/migrator/source-scan.ts | 9 +- .../cli/src/migration/migrator/tsconfig.ts | 9 +- packages/cli/src/migration/migrator/tsup.ts | 17 +- .../cli/src/migration/migrator/vite-config.ts | 91 +++++--- .../migration/migrator/vite-plus-bootstrap.ts | 13 +- .../migration/migrator/vitest-ecosystem.ts | 74 +++++- .../cli/src/migration/migrator/vitest-v5.ts | 6 +- packages/cli/src/migration/migrator/yarn.ts | 6 +- packages/cli/src/migration/scope.ts | 218 ------------------ packages/cli/src/oxlint-plugin.ts | 40 ---- packages/cli/src/utils/agent.ts | 5 - packages/cli/src/utils/editor.ts | 10 +- packages/cli/src/utils/package-overrides.ts | 68 ------ 75 files changed, 895 insertions(+), 1553 deletions(-) delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots.toml delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_baseurl_references.md delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_explicit_target.md delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_invalid.md delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_override_conflict.md delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_parent_negation.md delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_runtime_config.md delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/vite.config.ts rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/examples/standalone/package.json (100%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/examples/standalone/unit.test.ts (100%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/package.json (83%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/packages/app/package.json (100%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/packages/app/unit.test.ts (100%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/packages/app/vite.config.ts (100%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/pnpm-workspace.yaml (100%) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_extends.md rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns/snapshots/migration_ignore_patterns_fresh.md => migration_import_rule/snapshots/migration_import_rule_fresh.md} (54%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns/snapshots/migration_ignore_patterns_full.md => migration_import_rule/snapshots/migration_import_rule_full.md} (64%) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_global_off.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_runtime.md rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns/snapshots/migration_ignore_patterns_upgrade.md => migration_import_rule/snapshots/migration_import_rule_upgrade.md} (70%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/templates/managed/package.json (100%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/templates/managed/unit.test.ts (100%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/templates/starter/package.json (100%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/templates/starter/tsconfig.json (100%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/templates/starter/unit.test.ts (100%) rename crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/{migration_ignore_patterns => migration_import_rule}/templates/starter/vite.config.ts (100%) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/vite.config.ts create mode 100644 crates/vp_migration/src/import_rule.rs delete mode 100644 packages/cli/src/migrate-config.ts delete mode 100644 packages/cli/src/migration-ignore.ts delete mode 100644 packages/cli/src/migration/__tests__/ignore-patterns.spec.ts create mode 100644 packages/cli/src/migration/__tests__/import-rule.spec.ts delete mode 100644 packages/cli/src/migration/files.ts delete mode 100644 packages/cli/src/migration/scope.ts 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/migration_ignore_patterns/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots.toml deleted file mode 100644 index e5aaeafc16..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots.toml +++ /dev/null @@ -1,126 +0,0 @@ -[[case]] -name = "migration_ignore_patterns_upgrade" -vp = "local" -comment = "Preserve templates and their shared catalogs and overrides, re-include one template, and leave non-workspace examples alone." -steps = [ - { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, - { argv = ["vpt", "print-file", "templates/starter/package.json"] }, - { argv = ["vpt", "print-file", "templates/starter/vite.config.ts"] }, - { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"], continue-on-failure = true }, - { argv = ["vpt", "print-file", "templates/starter/tsconfig.json"] }, - { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, - { argv = ["vpt", "print-file", "packages/app/package.json"], continue-on-failure = true }, - { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, - { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, - { argv = ["vpt", "print-file", "pnpm-workspace.yaml"] }, - { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "Rerunning uses the same policy and explicit managed dependency specs." }, - { 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, comment = "Ignored imports remain upstream; other rules still report the template's debugger statement." }, - { 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_ignore_patterns_fresh" -vp = "local" -comment = "Initial migration honors the same exclusions." -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/package.json"] }, - { argv = ["vpt", "print-file", "templates/starter/vite.config.ts"] }, - { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"], continue-on-failure = true }, - { argv = ["vpt", "print-file", "templates/starter/tsconfig.json"] }, - { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, - { argv = ["vpt", "print-file", "packages/app/package.json"], continue-on-failure = true }, - { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, - { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, - { argv = ["vpt", "print-file", "pnpm-workspace.yaml"] }, -] - -[[case]] -name = "migration_ignore_patterns_override_conflict" -vp = "local" -comment = "A conflicting global override is reported before project files change." -steps = [ - { argv = ["vpt", "write-file", "pnpm-workspace.yaml", "packages: ['packages/*', 'templates/*']\noverrides:\n vite: ^8.0.0\n"] }, - { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], continue-on-failure = true }, - { argv = ["vpt", "print-file", "packages/app/package.json"], continue-on-failure = true }, - { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, - { argv = ["vpt", "print-file", "pnpm-workspace.yaml"] }, -] - - -[[case]] -name = "migration_ignore_patterns_full" -vp = "local" -comment = "Full setup honors exclusions, and vp check --fix keeps the import policy." -steps = [ - { argv = ["vp", "migrate", "--full", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, - { argv = ["vpt", "print-file", "templates/starter/package.json"] }, - { argv = ["vpt", "print-file", "templates/starter/vite.config.ts"] }, - { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"], continue-on-failure = true }, - { argv = ["vpt", "print-file", "templates/starter/tsconfig.json"] }, - { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, - { argv = ["vpt", "print-file", "packages/app/package.json"], continue-on-failure = true }, - { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, - { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, - { argv = ["vpt", "print-file", "pnpm-workspace.yaml"] }, - { argv = ["vp", "migrate", "--full", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "Rerunning uses the same policy and explicit managed dependency specs." }, - { 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, comment = "Ignored imports remain upstream; other rules still report the template's debugger statement." }, - { 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_ignore_patterns_parent_negation" -vp = "local" -steps = [ - { argv = ["vpt", "write-file", "vite.config.ts", "export default { migrate: { ignorePatterns: ['templates/', '!templates/managed/unit.test.ts'] } };\n"] }, - { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, - { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, - { argv = ["vpt", "print-file", "packages/app/unit.test.ts"] }, -] - -[[case]] -name = "migration_ignore_patterns_invalid" -vp = "local" -steps = [ - { argv = ["vpt", "write-file", "vite.config.ts", "export default { migrate: { ignorePatterns: 'templates/' } };\n"] }, - { argv = ["vp", "migrate", "--no-interactive"], continue-on-failure = true }, - { argv = ["vpt", "print-file", "packages/app/package.json"], continue-on-failure = true }, -] - -[[case]] -name = "migration_ignore_patterns_runtime_config" -vp = "local" -comment = "A runtime config export retains the policy, and the root config can be a plain object." -steps = [ - { argv = ["vpt", "write-file", "vite.config.ts", "const excluded = ['templates/*', '!templates/managed/'];\nexport default () => ({ migrate: { ignorePatterns: excluded } });\n"] }, - { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, - { argv = ["vpt", "print-file", "templates/starter/unit.test.ts"] }, - { argv = ["vpt", "print-file", "templates/managed/unit.test.ts"] }, -] - -[[case]] -name = "migration_ignore_patterns_explicit_target" -vp = "local" -comment = "An explicit non-workspace template target must not bypass the workspace policy." -steps = [ - { argv = ["vp", "migrate", "examples/standalone", "--full", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], continue-on-failure = true }, - { argv = ["vpt", "print-file", "examples/standalone/package.json"], continue-on-failure = true }, - { argv = ["vpt", "print-file", "examples/standalone/unit.test.ts"] }, -] - -[[case]] -name = "migration_ignore_patterns_baseurl_references" -vp = "local" -comment = "The external baseUrl fixer follows references and extends; skip it so excluded configs stay unchanged." -steps = [ - { argv = ["vpt", "write-file", "tsconfig.json", "{ \"compilerOptions\": { \"baseUrl\": \".\" }, \"references\": [{ \"path\": \"./templates/starter\" }] }\n"] }, - { argv = ["vpt", "write-file", "templates/starter/tsconfig.json", "{ \"extends\": \"../../tsconfig.json\", \"compilerOptions\": { \"baseUrl\": \".\", \"paths\": { \"@/*\": [\"src/*\"] } } }\n"] }, - { argv = ["vp", "migrate", "--full", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"] }, - { argv = ["vpt", "print-file", "templates/starter/tsconfig.json"] }, - { argv = ["vpt", "print-file", "tsconfig.json"] }, -] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_baseurl_references.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_baseurl_references.md deleted file mode 100644 index 30d48e1f1d..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_baseurl_references.md +++ /dev/null @@ -1,43 +0,0 @@ -# migration_ignore_patterns_baseurl_references - -The external baseUrl fixer follows references and extends; skip it so excluded configs stay unchanged. - -## `vpt write-file tsconfig.json '{ "compilerOptions": { "baseUrl": "." }, "references": [{ "path": "./templates/starter" }] } -'` - -``` -``` - -## `vpt write-file templates/starter/tsconfig.json '{ "extends": "../../tsconfig.json", "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } } -'` - -``` -``` - -## `vp migrate --full --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 -! Warnings: - - Skipped automatic tsconfig baseUrl removal because its external fixer cannot honor migrate.ignorePatterns. Remove baseUrl manually in migrated projects to enable type-aware linting. -``` - -## `vpt print-file templates/starter/tsconfig.json` - -``` -{ "extends": "../../tsconfig.json", "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } } -``` - -## `vpt print-file tsconfig.json` - -``` -{ "compilerOptions": { "baseUrl": "." }, "references": [{ "path": "./templates/starter" }] } -``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_explicit_target.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_explicit_target.md deleted file mode 100644 index 902581add3..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_explicit_target.md +++ /dev/null @@ -1,27 +0,0 @@ -# migration_ignore_patterns_explicit_target - -An explicit non-workspace template target must not bypass the workspace policy. - -## `vp migrate examples/standalone --full --no-interactive --no-hooks --no-agent --no-editor` - -**Exit code:** 1 - -``` -VITE+ - The Unified Toolchain for the Web - -■ Cannot migrate examples/standalone: excluded by migrate.ignorePatterns in /vite.config.ts. No project files were changed. -Error: Cannot migrate examples/standalone: excluded by migrate.ignorePatterns in /vite.config.ts. No project files were changed. -``` - -## `vpt print-file examples/standalone/package.json` - -``` -{ "name": "standalone-template", "private": true, "type": "module", "devDependencies": { "vite": "^8.0.0", "vitest": "^4.1.11" } } -``` - -## `vpt print-file examples/standalone/unit.test.ts` - -``` -import { expect, test } from 'vitest'; -test('outside-workspace', () => expect(1).toBe(1)); -``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_invalid.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_invalid.md deleted file mode 100644 index 87c5f49b0a..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_invalid.md +++ /dev/null @@ -1,28 +0,0 @@ -# migration_ignore_patterns_invalid - -## `vpt write-file vite.config.ts 'export default { migrate: { ignorePatterns: '\''templates/'\'' } }; -'` - -``` -``` - -## `vp migrate --no-interactive` - -**Exit code:** 1 - -``` -VITE+ - The Unified Toolchain for the Web - -■ migrate.ignorePatterns must be an array of strings. -Error: migrate.ignorePatterns must be an array of strings. -``` - -## `vpt print-file packages/app/package.json` - -``` -{ - "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_ignore_patterns/snapshots/migration_ignore_patterns_override_conflict.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_override_conflict.md deleted file mode 100644 index 935a3ee49a..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_override_conflict.md +++ /dev/null @@ -1,47 +0,0 @@ -# migration_ignore_patterns_override_conflict - -A conflicting global override is reported before project files change. - -## `vpt write-file pnpm-workspace.yaml 'packages: ['\''packages/*'\'', '\''templates/*'\''] -overrides: - vite: ^8.0.0 -'` - -``` -``` - -## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` - -**Exit code:** 1 - -``` -VITE+ - The Unified Toolchain for the Web - -■ Cannot migrate while preserving excluded packages: override "vite" ("^8.0.0") conflicts with "npm:@voidzero-dev/vite-plus-core@". Review the root override or migrate the excluded packages first. No project files were changed. -Error: Cannot migrate while preserving excluded packages: override "vite" ("^8.0.0") conflicts with "npm:@voidzero-dev/vite-plus-core@". Review the root override or migrate the excluded packages first. No project files were changed. -``` - -## `vpt print-file packages/app/package.json` - -``` -{ - "name": "app", "private": true, "type": "module", - "scripts": { "build": "vite build", "test": "vitest run" }, - "devDependencies": { "vite": "catalog:", "vitest": "catalog:legacy", "@vitest/coverage-v8": "catalog:legacy" } -} -``` - -## `vpt print-file packages/app/unit.test.ts` - -``` -import { expect, test } from 'vitest'; -test('application', () => expect(1).toBe(1)); -``` - -## `vpt print-file pnpm-workspace.yaml` - -``` -packages: ['packages/*', 'templates/*'] -overrides: - vite: ^8.0.0 -``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_parent_negation.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_parent_negation.md deleted file mode 100644 index c06a67cd3b..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_parent_negation.md +++ /dev/null @@ -1,35 +0,0 @@ -# migration_ignore_patterns_parent_negation - -## `vpt write-file vite.config.ts 'export default { migrate: { ignorePatterns: ['\''templates/'\'', '\'''\!'templates/managed/unit.test.ts'\''] } }; -'` - -``` -``` - -## `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/managed/unit.test.ts` - -``` -import { expect, test } from 'vitest'; -test('included', () => 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_ignore_patterns/snapshots/migration_ignore_patterns_runtime_config.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_runtime_config.md deleted file mode 100644 index 4cbf7d2eda..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_runtime_config.md +++ /dev/null @@ -1,39 +0,0 @@ -# migration_ignore_patterns_runtime_config - -A runtime config export retains the policy, and the root config can be a plain object. - -## `vpt write-file vite.config.ts 'const excluded = ['\''templates/*'\'', '\'''\!'templates/managed/'\'']; -export default () => ({ migrate: { ignorePatterns: excluded } }); -'` - -``` -``` - -## `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 → -• 4 files had imports rewritten -• Package manager settings configured -``` - -## `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)); -``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/vite.config.ts deleted file mode 100644 index e27914cdc7..0000000000 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/vite.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from 'vite-plus'; -export default defineConfig({ - migrate: { ignorePatterns: ['/templates/*', '!/templates/managed/', '/examples/'] }, - lint: { - jsPlugins: ['vite-plus/oxlint-plugin'], - rules: { 'vite-plus/prefer-vite-plus-imports': 'error', 'no-debugger': 'error' }, - }, -}); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/examples/standalone/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/examples/standalone/package.json similarity index 100% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/examples/standalone/package.json rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/examples/standalone/package.json diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/examples/standalone/unit.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/examples/standalone/unit.test.ts similarity index 100% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/examples/standalone/unit.test.ts rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/examples/standalone/unit.test.ts diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/package.json similarity index 83% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/package.json rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/package.json index 74a3099faf..ab4f2d8a6b 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/package.json +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/package.json @@ -1,5 +1,5 @@ { - "name": "migration-ignore-patterns", + "name": "migration-import-rule", "private": true, "type": "module", "devDependencies": { "vite-plus": "catalog:" }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/packages/app/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/package.json similarity index 100% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/packages/app/package.json rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/package.json diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/packages/app/unit.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/unit.test.ts similarity index 100% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/packages/app/unit.test.ts rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/unit.test.ts diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/packages/app/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/vite.config.ts similarity index 100% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/packages/app/vite.config.ts rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/packages/app/vite.config.ts diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/pnpm-workspace.yaml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/pnpm-workspace.yaml similarity index 100% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/pnpm-workspace.yaml rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/pnpm-workspace.yaml 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_ignore_patterns/snapshots/migration_ignore_patterns_fresh.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_fresh.md similarity index 54% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_fresh.md rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_fresh.md index fc01414323..ec881ca2aa 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_fresh.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_fresh.md @@ -1,6 +1,6 @@ -# migration_ignore_patterns_fresh +# migration_import_rule_fresh -Initial migration honors the same exclusions. +Initial migration respects the same import rule configuration. ## `vpt json-edit package.json devDependencies {}` @@ -14,24 +14,22 @@ VITE+ - The Unified Toolchain for the Web ◇ Migrated . to Vite+ • Node pnpm -• 3 files had imports rewritten -``` - -## `vpt print-file templates/starter/package.json` - -``` -{ - "name": "starter", "private": true, "type": "module", - "scripts": { "build": "vite build", "test": "vitest run" }, - "devDependencies": { "vite": "catalog:", "vitest": "catalog:legacy", "@vitest/coverage-v8": "catalog:legacy" } -} +• 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: {poolOptions: {threads: {singleThread: true}}}}); +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` @@ -42,10 +40,11 @@ debugger; test('template', () => expect(1).toBe(1)); ``` -## `vpt print-file templates/starter/tsconfig.json` +## `vpt print-file templates/managed/unit.test.ts` ``` -{ "compilerOptions": { "types": ["vite/client", "vitest/globals"], "esModuleInterop": false } } +import { expect, test } from 'vite-plus/test'; +test('included', () => expect(1).toBe(1)); ``` ## `vpt print-file examples/standalone/unit.test.ts` @@ -55,11 +54,18 @@ import { expect, test } from 'vitest'; test('outside-workspace', () => expect(1).toBe(1)); ``` -## `vpt print-file packages/app/package.json` +## `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": "app", + "name": "starter", "private": true, "type": "module", "scripts": { @@ -67,28 +73,14 @@ test('outside-workspace', () => expect(1).toBe(1)); "test": "vp test run" }, "devDependencies": { - "vite": "npm:@voidzero-dev/vite-plus-core@", - "vitest": "", - "@vitest/coverage-v8": "", - "vite-plus": "" + "vite": "catalog:", + "vitest": "catalog:legacy", + "@vitest/coverage-v8": "catalog:legacy", + "vite-plus": "catalog:" } } ``` -## `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/managed/unit.test.ts` - -``` -import { expect, test } from 'vite-plus/test'; -test('included', () => expect(1).toBe(1)); -``` - ## `vpt print-file pnpm-workspace.yaml` ``` @@ -96,14 +88,18 @@ packages: - packages/* - templates/* catalog: - vite-plus: latest - vite: ^8.0.0 + vite-plus: + vite: npm:@voidzero-dev/vite-plus-core@ + vitest: + '@vitest/coverage-v8': catalogs: legacy: - vitest: ^4.1.11 - '@vitest/coverage-v8': ^4.1.11 + vitest: + '@vitest/coverage-v8': overrides: unrelated-package: 1.0.0 + vite@*: 'catalog:' + vitest@*: 'catalog:' peerDependencyRules: allowAny: - vite diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_full.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_full.md similarity index 64% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_full.md rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_full.md index 12c5eddb3d..102ce5e374 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_full.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_full.md @@ -1,8 +1,8 @@ -# migration_ignore_patterns_full +# migration_import_rule_full -Full setup honors exclusions, and vp check --fix keeps the import policy. +Full migration and check autofix share the import rule; other lint rules remain active. -## `vp migrate --full --no-interactive --no-hooks --no-agent --no-editor` +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor --full` ``` VITE+ - The Unified Toolchain for the Web @@ -16,21 +16,17 @@ VITE+ - The Unified Toolchain for the Web • Package manager settings configured ``` -## `vpt print-file templates/starter/package.json` - -``` -{ - "name": "starter", "private": true, "type": "module", - "scripts": { "build": "vite build", "test": "vitest run" }, - "devDependencies": { "vite": "catalog:", "vitest": "catalog:legacy", "@vitest/coverage-v8": "catalog:legacy" } -} -``` - ## `vpt print-file templates/starter/vite.config.ts` ``` import { defineConfig } from 'vite'; -export default defineConfig({test: {poolOptions: {threads: {singleThread: true}}}}); +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` @@ -41,10 +37,11 @@ debugger; test('template', () => expect(1).toBe(1)); ``` -## `vpt print-file templates/starter/tsconfig.json` +## `vpt print-file templates/managed/unit.test.ts` ``` -{ "compilerOptions": { "types": ["vite/client", "vitest/globals"], "esModuleInterop": false } } +import { expect, test } from 'vite-plus/test'; +test('included', () => expect(1).toBe(1)); ``` ## `vpt print-file examples/standalone/unit.test.ts` @@ -54,11 +51,18 @@ import { expect, test } from 'vitest'; test('outside-workspace', () => expect(1).toBe(1)); ``` -## `vpt print-file packages/app/package.json` +## `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": "app", + "name": "starter", "private": true, "type": "module", "scripts": { @@ -66,27 +70,13 @@ test('outside-workspace', () => expect(1).toBe(1)); "test": "vp test run" }, "devDependencies": { - "vite": "npm:@voidzero-dev/vite-plus-core@", - "vitest": "", - "@vitest/coverage-v8": "" + "vite": "catalog:", + "vitest": "catalog:legacy", + "@vitest/coverage-v8": "catalog:legacy" } } ``` -## `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/managed/unit.test.ts` - -``` -import { expect, test } from 'vite-plus/test'; -test('included', () => expect(1).toBe(1)); -``` - ## `vpt print-file pnpm-workspace.yaml` ``` @@ -94,14 +84,18 @@ packages: - packages/* - templates/* catalog: - vite-plus: latest - vite: ^8.0.0 + vite-plus: + vite: npm:@voidzero-dev/vite-plus-core@ + vitest: + '@vitest/coverage-v8': catalogs: legacy: - vitest: ^4.1.11 - '@vitest/coverage-v8': ^4.1.11 + vitest: + '@vitest/coverage-v8': overrides: unrelated-package: 1.0.0 + vite@*: 'catalog:' + vitest@*: 'catalog:' peerDependencyRules: allowAny: - vite @@ -111,16 +105,6 @@ peerDependencyRules: vitest: '*' ``` -## `vp migrate --full --no-interactive --no-hooks --no-agent --no-editor` - -Rerunning uses the same policy and explicit managed dependency specs. - -``` -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'\'', () => {}); '` @@ -130,8 +114,6 @@ test('\''autofix'\'', () => {}); ## `vp check --no-fmt --fix templates/starter/unit.test.ts packages/app/unit.test.ts` -Ignored imports remain upstream; other rules still report the template's debugger statement. - **Exit code:** 1 ``` 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_ignore_patterns/snapshots/migration_ignore_patterns_upgrade.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_upgrade.md similarity index 70% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_upgrade.md rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_upgrade.md index 3e6a52f834..d3d8355f4e 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/snapshots/migration_ignore_patterns_upgrade.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/snapshots/migration_import_rule_upgrade.md @@ -1,6 +1,6 @@ -# migration_ignore_patterns_upgrade +# migration_import_rule_upgrade -Preserve templates and their shared catalogs and overrides, re-include one template, and leave non-workspace examples alone. +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` @@ -16,21 +16,17 @@ VITE+ - The Unified Toolchain for the Web • Package manager settings configured ``` -## `vpt print-file templates/starter/package.json` - -``` -{ - "name": "starter", "private": true, "type": "module", - "scripts": { "build": "vite build", "test": "vitest run" }, - "devDependencies": { "vite": "catalog:", "vitest": "catalog:legacy", "@vitest/coverage-v8": "catalog:legacy" } -} -``` - ## `vpt print-file templates/starter/vite.config.ts` ``` import { defineConfig } from 'vite'; -export default defineConfig({test: {poolOptions: {threads: {singleThread: true}}}}); +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` @@ -41,10 +37,11 @@ debugger; test('template', () => expect(1).toBe(1)); ``` -## `vpt print-file templates/starter/tsconfig.json` +## `vpt print-file templates/managed/unit.test.ts` ``` -{ "compilerOptions": { "types": ["vite/client", "vitest/globals"], "esModuleInterop": false } } +import { expect, test } from 'vite-plus/test'; +test('included', () => expect(1).toBe(1)); ``` ## `vpt print-file examples/standalone/unit.test.ts` @@ -54,11 +51,18 @@ import { expect, test } from 'vitest'; test('outside-workspace', () => expect(1).toBe(1)); ``` -## `vpt print-file packages/app/package.json` +## `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": "app", + "name": "starter", "private": true, "type": "module", "scripts": { @@ -66,27 +70,13 @@ test('outside-workspace', () => expect(1).toBe(1)); "test": "vp test run" }, "devDependencies": { - "vite": "npm:@voidzero-dev/vite-plus-core@", - "vitest": "", - "@vitest/coverage-v8": "" + "vite": "catalog:", + "vitest": "catalog:legacy", + "@vitest/coverage-v8": "catalog:legacy" } } ``` -## `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/managed/unit.test.ts` - -``` -import { expect, test } from 'vite-plus/test'; -test('included', () => expect(1).toBe(1)); -``` - ## `vpt print-file pnpm-workspace.yaml` ``` @@ -94,14 +84,18 @@ packages: - packages/* - templates/* catalog: - vite-plus: latest - vite: ^8.0.0 + vite-plus: + vite: npm:@voidzero-dev/vite-plus-core@ + vitest: + '@vitest/coverage-v8': catalogs: legacy: - vitest: ^4.1.11 - '@vitest/coverage-v8': ^4.1.11 + vitest: + '@vitest/coverage-v8': overrides: unrelated-package: 1.0.0 + vite@*: 'catalog:' + vitest@*: 'catalog:' peerDependencyRules: allowAny: - vite @@ -113,8 +107,6 @@ peerDependencyRules: ## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` -Rerunning uses the same policy and explicit managed dependency specs. - ``` VITE+ - The Unified Toolchain for the Web @@ -130,8 +122,6 @@ test('\''autofix'\'', () => {}); ## `vp lint --threads=1 --fix templates/starter/unit.test.ts packages/app/unit.test.ts` -Ignored imports remain upstream; other rules still report the template's debugger statement. - **Exit code:** 1 ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/managed/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/managed/package.json similarity index 100% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/managed/package.json rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/managed/package.json diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/managed/unit.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/managed/unit.test.ts similarity index 100% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/managed/unit.test.ts rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/managed/unit.test.ts diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/package.json similarity index 100% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/package.json rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/package.json diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/tsconfig.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/tsconfig.json similarity index 100% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/tsconfig.json rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/tsconfig.json diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/unit.test.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/unit.test.ts similarity index 100% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/unit.test.ts rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/unit.test.ts diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/vite.config.ts similarity index 100% rename from crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_ignore_patterns/templates/starter/vite.config.ts rename to crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_import_rule/templates/starter/vite.config.ts 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/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_vite_template/snapshots.toml index 2a81fc7335..a34266b28d 100644 --- 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 @@ -1,9 +1,9 @@ [[case]] name = "migration_workspace_vite_template" vp = "local" -comment = "Reproduce #3406: an existing Vite+ workspace converts an independently reusable Vite/Vitest template. This records the current behavior, not the desired exclusion behavior." +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 = "ordinary upgrade without --full also migrates the template" }, + { 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" }, 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 index a90c3b5806..dc86eb27d5 100644 --- 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 @@ -1,10 +1,10 @@ # migration_workspace_vite_template -Reproduce #3406: an existing Vite+ workspace converts an independently reusable Vite/Vitest template. This records the current behavior, not the desired exclusion behavior. +Without a lint override, migration rewrites template imports along with other workspace files. ## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` -ordinary upgrade without --full also migrates the template +an ordinary upgrade rewrites template imports by default ``` VITE+ - The Unified Toolchain for the Web 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/file_walker.rs b/crates/vp_migration/src/file_walker.rs index 636d3d273d..91585964f9 100644 --- a/crates/vp_migration/src/file_walker.rs +++ b/crates/vp_migration/src/file_walker.rs @@ -3,37 +3,6 @@ use std::path::{Path, PathBuf}; use ignore::{WalkBuilder, gitignore::GitignoreBuilder}; use vp_error::Error; -/// Config-relative gitignore rules shared by migration and its lint autofix. -#[derive(Clone)] -pub struct MigrationIgnoreMatcher { - root: PathBuf, - matcher: ignore::gitignore::Gitignore, -} - -impl MigrationIgnoreMatcher { - pub fn new(root: &Path, patterns: &[String]) -> Result { - let mut builder = GitignoreBuilder::new(root); - for pattern in patterns { - builder.add_line(None, pattern)?; - } - Ok(Self { root: root.to_path_buf(), matcher: builder.build()? }) - } - - pub fn is_ignored(&self, path: &Path, is_dir: bool) -> bool { - let Ok(relative) = path.strip_prefix(&self.root) else { return false }; - let mut current = self.root.clone(); - let mut components = relative.components().peekable(); - while let Some(component) = components.next() { - current.push(component); - // Git cannot re-include a child of an excluded directory. - if self.matcher.matched(¤t, components.peek().is_some() || is_dir).is_ignore() { - return true; - } - } - false - } -} - /// File extensions to process for import rewriting. /// /// Includes CommonJS (`.cjs`/`.cts`) variants in addition to ESM, because @@ -113,13 +82,6 @@ pub fn is_directory_gitignored(root: &Path, directory: &Path) -> Result Result { - find_ts_files_with_ignore(root, None) -} - -pub fn find_ts_files_with_ignore( - root: &Path, - ignore: Option, -) -> Result { let mut files = Vec::new(); let walker = WalkBuilder::new(root) @@ -128,12 +90,6 @@ pub fn find_ts_files_with_ignore( .git_global(true) // Respect global gitignore .git_exclude(true) // Respect .git/info/exclude .require_git(false) // Work even if not a git repo - .filter_entry(move |entry| { - !ignore.as_ref().is_some_and(|matcher| { - matcher - .is_ignored(entry.path(), entry.file_type().is_some_and(|kind| kind.is_dir())) - }) - }) .build(); for entry in walker { @@ -164,30 +120,6 @@ mod tests { use super::*; - #[test] - fn migration_patterns_follow_gitignore_semantics() { - let temp = tempdir().unwrap(); - for (patterns, file, is_dir, expected) in [ - (vec!["/templates/"], "templates/app/vite.config.ts", false, true), - (vec!["/templates/"], "nested/templates/a.ts", false, false), - (vec!["templates/"], "nested/templates/a.ts", false, true), - (vec!["templates/*", "!templates/app/"], "templates/app/a.ts", false, false), - (vec!["templates/", "!templates/app/a.ts"], "templates/app/a.ts", false, true), - (vec!["**/*.test.ts", "!app.test.ts"], "app.test.ts", false, false), - (vec!["# comment", "\\!literal/"], "!literal/a.ts", false, true), - (vec!["templates/"], "templates", false, false), - (vec!["templates/"], "templates", true, true), - ] { - let patterns = patterns.into_iter().map(String::from).collect::>(); - let matcher = MigrationIgnoreMatcher::new(temp.path(), &patterns).unwrap(); - assert_eq!( - matcher.is_ignored(&temp.path().join(file), is_dir), - expected, - "{patterns:?}: {file}" - ); - } - } - #[test] fn test_inherited_directory_gitignore() { for (root_rules, package_rules, expected) in [ diff --git a/crates/vp_migration/src/import_rewriter.rs b/crates/vp_migration/src/import_rewriter.rs index d99dfe8f21..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,9 +2062,8 @@ struct PackageRewriteContext { /// Options controlling directory-wide import rewriting. #[derive(Debug, Clone, Default)] pub struct RewriteImportsOptions { - /// Config-relative exclusions, also applied when the scan starts below the config root. - pub ignore_root: Option, - pub ignore_patterns: Vec, + /// 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, @@ -2288,11 +2287,7 @@ pub fn rewrite_imports_in_directory_with_options( root: &Path, options: RewriteImportsOptions, ) -> Result { - let ignore = file_walker::MigrationIgnoreMatcher::new( - options.ignore_root.as_deref().unwrap_or(root), - &options.ignore_patterns, - )?; - let walk_result = file_walker::find_ts_files_with_ignore(root, Some(ignore))?; + let walk_result = file_walker::find_ts_files(root)?; // Pre-compute package context for each file (requires mutable cache, done sequentially). let mut package_context_cache: HashMap = HashMap::new(); @@ -2333,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) => { @@ -2407,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)?; @@ -2426,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); @@ -3036,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 d6832db050..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; @@ -20,11 +21,12 @@ mod source_analysis; mod task_cache; mod vite_config; -pub use file_walker::{MigrationIgnoreMatcher, WalkResult, find_ts_files, is_directory_gitignored}; +pub use file_walker::{WalkResult, find_ts_files, is_directory_gitignored}; 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 26f488a98c..12e34c2c4c 100644 --- a/crates/vp_static_config/src/lib.rs +++ b/crates/vp_static_config/src/lib.rs @@ -791,7 +791,7 @@ mod tests { } #[test] - fn vitest_config_preserves_migration_fields_without_evaluating_plugins() { + fn vitest_config_preserves_lint_fields_without_evaluating_plugins() { for import in [ "import { defineConfig } from 'vitest/config';", "import { defineConfig } from 'vite-plus/test/config';", @@ -800,15 +800,15 @@ mod tests { import, "export default defineConfig({ plugins: [plugin()], - migrate: { ignorePatterns: ['templates/'] }, + lint: { rules: { 'vite-plus/prefer-vite-plus-imports': 'off' } }, });", ] .join("\n"); let result = parse(&source); assert_json( &result, - "migrate", - serde_json::json!({ "ignorePatterns": ["templates/"] }), + "lint", + serde_json::json!({ "rules": { "vite-plus/prefer-vite-plus-imports": "off" } }), ); assert!(result.get("run").is_none()); } @@ -817,19 +817,23 @@ mod tests { #[test] fn exported_const_config_is_static_only_without_possible_mutations() { let source = "import { defineConfig } from 'vitest/config'; - const config = defineConfig({ migrate: { ignorePatterns: ['templates/'] } });"; + const config = defineConfig({ lint: { rules: { 'vite-plus/prefer-vite-plus-imports': 'off' } } });"; let result = parse(&[source, "export default config;"].join("\n")); - assert_json(&result, "migrate", serde_json::json!({ "ignorePatterns": ["templates/"] })); + 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.migrate = { ignorePatterns: [] };", + "config.lint = {};", "Object.assign(config, other);", "const changed = mutate(config);", ] { let result = parse(&[source, mutation, "export default config;"].join("\n")); - assert_non_static(&result, "migrate"); + assert_non_static(&result, "lint"); } - assert_non_static(&parse("let config = {}; export default config;"), "migrate"); + assert_non_static(&parse("let config = {}; export default config;"), "lint"); } #[test] diff --git a/docs/guide/migrate.md b/docs/guide/migrate.md index 41940819fb..eb762a6a77 100644 --- a/docs/guide/migrate.md +++ b/docs/guide/migrate.md @@ -45,8 +45,6 @@ vp migrate my-app ## Migration Flow -### Steps - The `migrate` command is designed to move existing projects onto Vite+ quickly. Here is what the command does: - Updates project dependencies @@ -63,49 +61,36 @@ 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. -### Excluding templates and examples +### Keeping upstream imports in templates and examples -Use `migrate.ignorePatterns` in the workspace-root `vite.config.ts` to keep -independently reusable projects on Vite or Vitest: +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({ - migrate: { - ignorePatterns: ['templates/', 'examples/'], + lint: { + overrides: [ + { + files: ['templates/**', 'examples/**'], + rules: { + 'vite-plus/prefer-vite-plus-imports': 'off', + }, + }, + ], }, }); ``` -Patterns use `.gitignore` syntax and are relative to the root config directory. -They support comments, anchored paths, directory patterns, and ordered `!` -negation. To include one template, use `['templates/*', '!templates/managed/']`. -As with Git, a negation cannot include a file whose parent directory is excluded. - -The policy is read before migration makes edits. Literal config blocks can be -read without loading plugins. Dynamic configs require the dependencies needed -to load the config to be installed first. - -Excluded files keep their dependencies, scripts, configs, and imports, and are -omitted from automatic formatting. The same policy applies to initial migration, -upgrades, and `--full`. Workspace membership stays unchanged. Migration reads -excluded package manifests to identify shared dependency settings. - -Automatic ESLint comment renaming and `baseUrl` removal are skipped when -exclusions are configured because those external tools cannot honor the patterns. -ESLint disable comments continue to work. Remove `baseUrl` manually in migrated -projects to enable type-aware linting. - -When excluded packages are present, migration preserves existing catalogs and -overrides and uses explicit dependency versions in migrated packages. It does not -add global toolchain overrides. Existing overrides still apply during installation; -an incompatible toolchain override must be resolved before migration can proceed. -Catalog references in an excluded template still require its original workspace. - -`migrate.ignorePatterns` also applies to the `vite-plus/prefer-vite-plus-imports` -lint rule, including during `vp lint --fix` and `vp check --fix`. Other lint rules -and normal formatting remain enabled. +`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 diff --git a/packages/cli/binding/index.cjs b/packages/cli/binding/index.cjs index bb2b26c8d1..472be475d9 100644 --- a/packages/cli/binding/index.cjs +++ b/packages/cli/binding/index.cjs @@ -1102,7 +1102,6 @@ module.exports.resetNativeMemoryStats = nativeBinding.resetNativeMemoryStats; module.exports.resolveTsconfig = nativeBinding.resolveTsconfig; module.exports.shutdownAsyncRuntime = nativeBinding.shutdownAsyncRuntime; module.exports.startAsyncRuntime = nativeBinding.startAsyncRuntime; -module.exports.MigrationIgnoreMatcher = nativeBinding.MigrationIgnoreMatcher; module.exports.analyzeMigrationSource = nativeBinding.analyzeMigrationSource; module.exports.detectWorkspace = nativeBinding.detectWorkspace; module.exports.documentationUrl = nativeBinding.documentationUrl; @@ -1122,7 +1121,7 @@ module.exports.parseHooksArgs = nativeBinding.parseHooksArgs; module.exports.parseLiteralCommands = nativeBinding.parseLiteralCommands; module.exports.parseMigrateArgs = nativeBinding.parseMigrateArgs; module.exports.parseStagedArgs = nativeBinding.parseStagedArgs; -module.exports.readStaticMigrationConfig = nativeBinding.readStaticMigrationConfig; +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 4e219f82dc..870ead46db 100644 --- a/packages/cli/binding/index.d.cts +++ b/packages/cli/binding/index.d.cts @@ -3610,11 +3610,6 @@ export interface ViteImportGlobMeta { isSubImportsPattern?: boolean; } -export declare class MigrationIgnoreMatcher { - constructor(root: string, patterns: Array); - isIgnored(path: string, isDir: boolean): boolean; -} - /** Parse source and resolve lexical bindings for the TypeScript migration rules. */ export declare function analyzeMigrationSource(filename: string, source: string): string; @@ -3988,8 +3983,8 @@ export interface PathAccess { readDir: boolean; } -/** JSON for a static migrate block; undefined requests normal config resolution. */ -export declare function readStaticMigrationConfig(root: string): string | null; +/** 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( @@ -4052,8 +4047,7 @@ export declare function rewriteImportsInDirectory( root: string, preserveVitestInNuxtPackages?: boolean | undefined | null, oxlintOwnerDirs?: Array | undefined | null, - ignoreRoot?: string | undefined | null, - ignorePatterns?: 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 b4f9cdf156..c9ba983a25 100644 --- a/packages/cli/binding/src/migration.rs +++ b/packages/cli/binding/src/migration.rs @@ -3,34 +3,15 @@ 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 struct MigrationIgnoreMatcher(vp_migration::MigrationIgnoreMatcher); - -#[napi] -impl MigrationIgnoreMatcher { - #[napi(constructor)] - pub fn new(root: String, patterns: Vec) -> Result { - Ok(Self( - vp_migration::MigrationIgnoreMatcher::new(Path::new(&root), &patterns) - .map_err(anyhow::Error::from)?, - )) - } - - #[napi] - pub fn is_ignored(&self, path: String, is_dir: bool) -> bool { - self.0.is_ignored(Path::new(&path), is_dir) - } -} - -/// JSON for a static migrate block; undefined requests normal config resolution. -#[napi] -pub fn read_static_migration_config(root: String) -> Result> { +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("migrate") { + 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("{}".to_string()), + None => Some("null".to_string()), }) } @@ -398,14 +379,16 @@ pub fn rewrite_imports_in_directory( root: String, preserve_vitest_in_nuxt_packages: Option, oxlint_owner_dirs: Option>, - ignore_root: Option, - ignore_patterns: Option>, + import_rule_config: Option, ) -> Result { let result = vp_migration::rewrite_imports_in_directory_with_options( Path::new(&root), vp_migration::RewriteImportsOptions { - ignore_root: ignore_root.map(std::path::PathBuf::from), - ignore_patterns: ignore_patterns.unwrap_or_default(), + 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/define-config.ts b/packages/cli/src/define-config.ts index 33ca2d4263..ae22427c4f 100644 --- a/packages/cli/src/define-config.ts +++ b/packages/cli/src/define-config.ts @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'; import { ignoreInput, ignoreOutput } from '@voidzero-dev/vite-task-client'; import type { OxfmtConfig } from 'oxfmt'; import type { OxlintConfig } from 'oxlint'; -import { resolveConfig as viteResolveConfig, searchForWorkspaceRoot } from 'vite'; +import { resolveConfig, searchForWorkspaceRoot } from 'vite'; import type { Alias, InlineConfig as ViteInlineConfig, @@ -27,7 +27,6 @@ import { import type { InlineConfig as VitestInlineConfig } from 'vitest/node'; import type { CreateTemplateEntry } from './create/org-manifest.ts'; -import { migrationIgnorePatterns, type MigrateConfig } from './migrate-config.ts'; import type { PackUserConfig } from './pack.ts'; import type { RunConfig } from './run-config.ts'; import type { StagedConfig } from './staged-config.ts'; @@ -43,8 +42,6 @@ declare module 'vite' { fmt?: OxfmtConfig; - migrate?: MigrateConfig; - /** * Defaults for the `vp check` composite command. Each flag mirrors the * matching CLI option (`--no-fmt` / `--no-lint`) and only affects @@ -115,26 +112,6 @@ declare module 'vite' { } } -// Oxlint loads the lint block through this public resolver, including configs -// which export a plain object instead of calling our defineConfig helper. -export const resolveConfig: typeof viteResolveConfig = async (...args) => { - const config = await viteResolveConfig(...args); - const patterns = migrationIgnorePatterns(config.migrate); - if (config.lint && patterns.length > 0) { - Object.assign(config.lint, { - ...config.lint, - settings: { - ...config.lint.settings, - 'vite-plus/migrate': { - root: config.configFile ? dirname(config.configFile) : config.root, - ignorePatterns: patterns, - }, - }, - }); - } - return config; -}; - type ViteUserConfigFnObject = (env: ConfigEnv) => UserConfig; type ViteUserConfigFnPromise = (env: ConfigEnv) => Promise; type ViteUserConfigFn = (env: ConfigEnv) => UserConfig | Promise; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 5a449ea7f1..d8f7b7fff8 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -2,11 +2,9 @@ export { defineConfig, defineProject, lazyPlugins, - resolveConfig, resolveConfigFromFile, } from './define-config.ts'; export type { ResolveConfigFromFileOptions } from './define-config.ts'; -export type { MigrateConfig } from './migrate-config.ts'; export * from 'vite'; diff --git a/packages/cli/src/migrate-config.ts b/packages/cli/src/migrate-config.ts deleted file mode 100644 index 8986f081b5..0000000000 --- a/packages/cli/src/migrate-config.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface MigrateConfig { - /** Paths excluded from migration, using .gitignore syntax relative to this config. */ - ignorePatterns?: string[]; -} - -export function migrationIgnorePatterns(config: unknown): string[] { - if (config === undefined) { - return []; - } - if (config === null || typeof config !== 'object' || Array.isArray(config)) { - throw new Error('migrate must be an object.'); - } - const patterns = (config as MigrateConfig).ignorePatterns; - if (patterns === undefined) { - return []; - } - if (!Array.isArray(patterns) || patterns.some((pattern) => typeof pattern !== 'string')) { - throw new Error('migrate.ignorePatterns must be an array of strings.'); - } - return patterns; -} diff --git a/packages/cli/src/migration-ignore.ts b/packages/cli/src/migration-ignore.ts deleted file mode 100644 index 79ac324465..0000000000 --- a/packages/cli/src/migration-ignore.ts +++ /dev/null @@ -1,7 +0,0 @@ -import path from 'node:path'; - -import { MigrationIgnoreMatcher } from '../binding/index.js'; - -export function createMigrationIgnoreMatcher(root: string, patterns: string[]) { - return new MigrationIgnoreMatcher(path.resolve(root), patterns); -} diff --git a/packages/cli/src/migration/__tests__/ignore-patterns.spec.ts b/packages/cli/src/migration/__tests__/ignore-patterns.spec.ts deleted file mode 100644 index 699b5a346d..0000000000 --- a/packages/cli/src/migration/__tests__/ignore-patterns.spec.ts +++ /dev/null @@ -1,162 +0,0 @@ -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; - -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; - -import { VITEST_VERSION, VITE_PLUS_OVERRIDE_PACKAGES } from '../../utils/constants.ts'; -import { readJsonFile } from '../../utils/json.ts'; -import { readYamlFile } from '../../utils/yaml.ts'; -import { editJsonFile, editYamlFile } from '../files.ts'; -import { formatMigratedProject } from '../format.ts'; -import { mergeTsdownConfigFile, rewriteAllImports } from '../migrator.ts'; -import { createMigrationReport } from '../report.ts'; -import { checkEnclosingMigrationPolicy, loadMigrationScope, withMigrationScope } from '../scope.ts'; - -describe('migration exclusions', () => { - let root: string; - const write = (file: string, content: string) => { - const filename = path.join(root, file); - fs.mkdirSync(path.dirname(filename), { recursive: true }); - fs.writeFileSync(filename, content); - return filename; - }; - beforeEach(() => { - root = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-migration-ignore-')); - write('package.json', '{"private":true}'); - write( - 'vite.config.ts', - `export default { migrate: { ignorePatterns: ['templates/', 'keep.config.ts'] } };`, - ); - write('templates/starter/package.json', '{"name":"starter"}'); - }); - afterEach(() => fs.rmSync(root, { recursive: true, force: true })); - - it('loads a static policy without executing uninstalled plugins', async () => { - write( - 'vite.config.ts', - `import { defineConfig } from 'vite'; -import missing from 'uninstalled-plugin'; -export default defineConfig({ plugins: [missing()], migrate: { ignorePatterns: ['templates/'] } });`, - ); - const scope = await loadMigrationScope(root); - expect(scope.ignores(path.join(root, 'templates/starter/vite.config.ts'))).toBe(true); - }); - - it('rejects explicit excluded targets while allowing an independent clone', async () => { - const target = path.join(root, 'templates/starter'); - await expect(checkEnclosingMigrationPolicy(target)).rejects.toThrow( - 'excluded by migrate.ignorePatterns', - ); - fs.mkdirSync(path.join(target, '.git')); - await expect(checkEnclosingMigrationPolicy(target)).resolves.toBeUndefined(); - }); - - it('preserves excluded source and rewrites included source, including a scan below the root', async () => { - const excluded = write('templates/starter/test.ts', `import { test } from 'vitest';`); - const included = write('app/test.ts', `import { test } from 'vitest';`); - withMigrationScope(await loadMigrationScope(root), () => { - expect(rewriteAllImports(root, true)).toBe(true); - expect(rewriteAllImports(path.dirname(excluded), true)).toBe(false); - }); - expect(fs.readFileSync(excluded, 'utf8')).toBe(`import { test } from 'vitest';`); - expect(fs.readFileSync(included, 'utf8')).toBe(`import { test } from 'vite-plus/test';`); - }); - - it('preserves shared catalogs and overrides in YAML and JSON, including nested Bun catalogs', async () => { - const workspace = write( - 'pnpm-workspace.yaml', - 'catalog:\n vite: ^8.0.0\ncatalogs:\n old:\n vitest: ^4.0.0\noverrides:\n unrelated: 1.0.0\n', - ); - const manifest = write( - 'package.json', - JSON.stringify({ - private: true, - workspaces: { packages: ['templates/*'], catalogs: { old: { vitest: '^4.0.0' } } }, - pnpm: { overrides: { unrelated: '1.0.0' } }, - }), - ); - const before = readYamlFile(workspace); - withMigrationScope(await loadMigrationScope(root), () => { - editYamlFile(workspace, (doc) => { - doc.setIn(['catalog', 'vite'], VITE_PLUS_OVERRIDE_PACKAGES.vite); - doc.delete('catalogs'); - doc.setIn(['overrides', 'vite'], VITE_PLUS_OVERRIDE_PACKAGES.vite); - doc.set('unrelatedSetting', true); - }); - editJsonFile(manifest, (pkg) => { - pkg.workspaces = { packages: ['templates/*'] }; - delete pkg.pnpm; - pkg.overrides = { vite: VITE_PLUS_OVERRIDE_PACKAGES.vite }; - pkg.devDependencies = { 'vite-plus': 'latest' }; - return pkg; - }); - }); - expect(readYamlFile(workspace)).toEqual({ ...before, unrelatedSetting: true }); - expect(readJsonFile(manifest)).toMatchObject({ - workspaces: { catalogs: { old: { vitest: '^4.0.0' } } }, - pnpm: { overrides: { unrelated: '1.0.0' } }, - devDependencies: { 'vite-plus': 'latest' }, - }); - expect(readJsonFile(manifest)).not.toHaveProperty('overrides'); - }); - - it.each([ - { overrides: { 'vite@*': '^8.0.0' } }, - { resolutions: { '**/vitest': '^4.0.0' } }, - { overrides: { 'vite-plus': { vitest: '^4.0.0' } } }, - { overrides: { vitest: { '.': '^4.0.0' } } }, - ])('rejects conflicting shared overrides before any writes: %j', async (settings) => { - const file = write('package.json', JSON.stringify(settings)); - const before = fs.readFileSync(file, 'utf8'); - await expect(loadMigrationScope(root)).rejects.toThrow('No project files were changed.'); - expect(fs.readFileSync(file, 'utf8')).toBe(before); - }); - - it('accepts compatible global overrides and preserves overrides scoped to an excluded package', async () => { - write( - 'package.json', - JSON.stringify({ - overrides: { - vitest: VITEST_VERSION, - starter: { vitest: '^4.0.0' }, - }, - }), - ); - await expect(loadMigrationScope(root)).resolves.toBeDefined(); - }); - - it('does not consume a source config when its destination is ignored', async () => { - write('vite.config.ts', `export default { migrate: { ignorePatterns: ['vite.config.ts'] } };`); - const source = write('tsdown.config.json', '{"entry":["src/index.ts"]}'); - withMigrationScope(await loadMigrationScope(root), () => { - expect(mergeTsdownConfigFile(root, true)).toBe(false); - }); - expect(fs.readFileSync(source, 'utf8')).toBe('{"entry":["src/index.ts"]}'); - }); - - it('filters automatic formatting outside Git and when explicit changed paths are supplied', async () => { - write('app.ts', 'export const app=1;'); - write('templates/starter/app.ts', 'export const template=1;'); - const scope = await loadMigrationScope(root); - for (const changed of [undefined, ['app.ts', 'templates/starter/app.ts']]) { - const formatted: string[] = []; - await withMigrationScope(scope, () => - formatMigratedProject(root, false, createMigrationReport(), { - collectPaths: async () => changed, - format: async (_cwd, _interactive, paths) => { - expect(paths).toBeDefined(); - formatted.push(...paths!); - return { status: 'formatted', durationMs: 0 }; - }, - }), - ); - expect(formatted).toContain('app.ts'); - expect( - formatted.some( - (file) => file.startsWith(`templates${path.sep}`) || file.startsWith('templates/'), - ), - ).toBe(false); - } - }); -}); 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 b17f630ba4..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, @@ -100,13 +101,6 @@ import { import { prepareNpmViteAliasReinstall } from './npm-reinstall.ts'; import type { MigrationOptions } from './options.ts'; import { addMigrationWarning, createMigrationReport, type MigrationReport } from './report.ts'; -import { - checkEnclosingMigrationPolicy, - isMigrationIgnored, - loadMigrationScope, - migrationScope, - withMigrationScope, -} from './scope.ts'; import { collectMigrationSetupPlan, collectTsupMigrationDecision, @@ -204,15 +198,6 @@ async function fixBaseUrlForWorkspace( if (!fixBaseUrl) { return []; } - // ts5to6 follows references and extends outside its input config, but does - // not accept exclusions. Filtering input paths alone cannot protect them. - if (migrationScope()?.patterns.length) { - addMigrationWarning( - report, - 'Skipped automatic tsconfig baseUrl removal because its external fixer cannot honor migrate.ignorePatterns. Remove baseUrl manually in migrated projects to enable type-aware linting.', - ); - return []; - } const fixedProjectPaths: string[] = []; for (const projectPath of getWorkspaceProjectPaths(workspaceInfo)) { @@ -843,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); @@ -936,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, @@ -944,6 +931,7 @@ async function executeMigrationPlan( skipStagedMigration, true, report, + importRuleConfig, ); } @@ -965,7 +953,6 @@ async function executeMigrationPlan( // 9. Write agent instructions (using pre-resolved decisions) updateMigrationProgress('Writing agent instructions'); await writeAgentInstructions({ - shouldWriteFile: (file) => !isMigrationIgnored(file), projectRoot: workspaceInfo.rootDir, targetPaths: plan.selectedAgentTargetPaths, interactive, @@ -976,7 +963,6 @@ async function executeMigrationPlan( // 10. Write editor configs (using pre-resolved decisions) updateMigrationProgress('Writing editor configs'); await writeEditorConfigs({ - shouldWriteFile: (file) => !isMigrationIgnored(file), projectRoot: workspaceInfo.rootDir, editorId: plan.selectedEditor, interactive, @@ -1079,21 +1065,6 @@ async function main() { 1, ); } - await checkEnclosingMigrationPolicy(workspaceInfoOptional.rootDir); - const scope = await loadMigrationScope( - workspaceInfoOptional.rootDir, - workspaceInfoOptional.packageManager, - ); - workspaceInfoOptional.packages = workspaceInfoOptional.packages?.filter( - (pkg) => !scope.ignores(path.join(workspaceInfoOptional.rootDir, pkg.path), true), - ); - return withMigrationScope(scope, () => migrateWorkspace(workspaceInfoOptional, options)); -} - -async function migrateWorkspace( - workspaceInfoOptional: WorkspaceInfoOptional, - options: MigrationOptions, -) { const initialChangedPaths = await collectChangedFormatPaths(workspaceInfoOptional.rootDir); const preExistingChangedPaths = initialChangedPaths ? new Set(initialChangedPaths) : undefined; const resolvedPackageManager = workspaceInfoOptional.packageManager ?? 'unknown'; @@ -1246,11 +1217,13 @@ async function migrateWorkspace( // 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 || @@ -1687,7 +1660,6 @@ async function migrateWorkspace( if (plan.selectedAgentTargetPaths && plan.selectedAgentTargetPaths.length > 0) { updateMigrationProgress('Writing agent instructions'); await writeAgentInstructions({ - shouldWriteFile: (file) => !isMigrationIgnored(file), projectRoot: workspaceInfoOptional.rootDir, targetPaths: plan.selectedAgentTargetPaths, interactive: options.interactive, @@ -1700,7 +1672,6 @@ async function migrateWorkspace( if (plan.selectedEditor) { updateMigrationProgress('Writing editor configs'); await writeEditorConfigs({ - shouldWriteFile: (file) => !isMigrationIgnored(file), projectRoot: workspaceInfoOptional.rootDir, editorId: plan.selectedEditor, interactive: options.interactive, diff --git a/packages/cli/src/migration/files.ts b/packages/cli/src/migration/files.ts deleted file mode 100644 index d539b13cdb..0000000000 --- a/packages/cli/src/migration/files.ts +++ /dev/null @@ -1,113 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { parseDocument } from 'yaml'; - -import { readJsonFile, writeJsonFile as writeJson } from '../utils/json.ts'; -import { type YamlDocument } from '../utils/yaml.ts'; -import { isMigrationIgnored, migrationScope, sharedDependencyFields } from './scope.ts'; - -export { readJsonFile, isJsonFile } from '../utils/json.ts'; -export { readYamlFile, scalarString, type YamlDocument } from '../utils/yaml.ts'; - -function ignored(file: fs.PathLike | number): boolean { - return ( - typeof file !== 'number' && - isMigrationIgnored(file instanceof URL ? fileURLToPath(file) : file.toString()) - ); -} - -// Keep migration writes behind one boundary. Reads still see excluded files: -// shared dependency ownership must be determined from the complete project. -const migrationFs: typeof fs = Object.assign(Object.create(fs) as typeof fs, { - writeFileSync(file, ...args) { - if (!ignored(file)) { - fs.writeFileSync(file, ...args); - } - }, - appendFileSync(file, ...args) { - if (!ignored(file)) { - fs.appendFileSync(file, ...args); - } - }, - unlinkSync(file) { - if (!ignored(file)) { - fs.unlinkSync(file); - } - }, - rmSync(file, ...args) { - if (!ignored(file)) { - fs.rmSync(file, ...args); - } - }, - renameSync(from, to) { - if (!ignored(from) && !ignored(to)) { - fs.renameSync(from, to); - } - }, -} satisfies Pick< - typeof fs, - 'writeFileSync' | 'appendFileSync' | 'unlinkSync' | 'rmSync' | 'renameSync' ->); -export default migrationFs; - -function preserveShared(file: string): boolean { - const scope = migrationScope(); - return ( - !!scope?.preserveSharedDependencies && - path.dirname(file) === scope.root && - ['package.json', 'pnpm-workspace.yaml', '.yarnrc.yml'].includes(path.basename(file)) - ); -} - -export function writeJsonFile(file: string, data: Record): void { - if (isMigrationIgnored(file)) { - return; - } - if (preserveShared(file) && fs.existsSync(file)) { - const before = parseDocument(fs.readFileSync(file, 'utf8')); - const after = parseDocument(JSON.stringify(data)); - for (const keys of sharedDependencyFields) { - if (before.hasIn(keys)) { - after.setIn(keys, before.getIn(keys)); - } else if (after.hasIn(keys)) { - after.deleteIn(keys); - } - } - data = after.toJS() as Record; - } - writeJson(file, data); -} - -export function editJsonFile = Record>( - file: string, - callback: (content: T) => T | undefined, -): void { - if (isMigrationIgnored(file)) { - return; - } - const updated = callback(readJsonFile(file) as T); - if (updated) { - writeJsonFile(file, updated); - } -} - -export function editYamlFile(file: string, callback: (doc: YamlDocument) => void): void { - if (isMigrationIgnored(file)) { - return; - } - const before = parseDocument(fs.readFileSync(file, 'utf8')); - const after = parseDocument(fs.readFileSync(file, 'utf8')); - callback(after); - if (preserveShared(file)) { - for (const keys of sharedDependencyFields) { - if (before.hasIn(keys)) { - after.setIn(keys, before.getIn(keys)); - } else if (after.hasIn(keys)) { - after.deleteIn(keys); - } - } - } - fs.writeFileSync(file, after.toString({ singleQuote: true }), 'utf8'); -} diff --git a/packages/cli/src/migration/format.ts b/packages/cli/src/migration/format.ts index 5c9e6484a4..81ac5b1203 100644 --- a/packages/cli/src/migration/format.ts +++ b/packages/cli/src/migration/format.ts @@ -4,7 +4,6 @@ import path from 'node:path'; import { runCommandSilently } from '../utils/command.ts'; import { type CommandRunSummary, runViteFmt } from '../utils/prompts.ts'; import { addMigrationWarning, type MigrationReport } from './report.ts'; -import { isMigrationIgnored, migrationScope } from './scope.ts'; type FormatRunner = ( cwd: string, @@ -164,29 +163,7 @@ export async function formatMigratedProject( ): Promise { const { format = runViteFmt, collectPaths = collectChangedFormatPaths, excludedPaths } = options; try { - let paths = await collectPaths(projectRoot, excludedPaths); - if (migrationScope()?.patterns.length) { - // Outside Git, enumerate files rather than handing an excluded directory - // to the formatter. Do not follow dependency trees or symlinks. - if (paths === undefined) { - paths = []; - const walk = (dir: string) => { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const file = path.join(dir, entry.name); - if (isMigrationIgnored(file, entry.isDirectory())) { - continue; - } - if (entry.isDirectory() && !['node_modules', '.git', '.yarn'].includes(entry.name)) { - walk(file); - } else if (entry.isFile()) { - paths!.push(path.relative(projectRoot, file)); - } - } - }; - walk(projectRoot); - } - paths = paths.filter((file) => !isMigrationIgnored(path.resolve(projectRoot, file))); - } + const paths = await collectPaths(projectRoot, excludedPaths); if (paths?.length === 0) { return true; } diff --git a/packages/cli/src/migration/migrator/README.md b/packages/cli/src/migration/migrator/README.md index acc7cc7601..acd19f4eb8 100644 --- a/packages/cli/src/migration/migrator/README.md +++ b/packages/cli/src/migration/migrator/README.md @@ -48,13 +48,6 @@ Pick the file by what a function _does_, not by where it happens to be called. ## Rules for adding / changing code -Migration file writes use `../files.ts`, which applies the active exclusion -scope from `../scope.ts`. Keep reads available for shared dependency analysis. -For operations that consume one file to create another, check both paths before -the operation. External tools must receive filtered targets or skip a step whose -writes cannot respect the scope. `vp create` and callers without a migration -scope retain the normal write behavior. - 1. **Add a function to the module that matches its category.** Keep it `export`ed so the barrel surfaces it. If nothing fits, prefer extending an existing module over creating a new one; if you do add a module, add a line diff --git a/packages/cli/src/migration/migrator/catalog.ts b/packages/cli/src/migration/migrator/catalog.ts index 64a90241aa..76c5e8b932 100644 --- a/packages/cli/src/migration/migrator/catalog.ts +++ b/packages/cli/src/migration/migrator/catalog.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import { coerce, isGreaterThanOrEqual, isLessThan } from 'verkit'; @@ -12,10 +13,9 @@ import { VITE_PLUS_VERSION, isForceOverrideMode, } from '../../utils/constants.ts'; +import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { type NpmWorkspaces } from '../../utils/workspace.ts'; -import fs from '../files.ts'; -import { editJsonFile, readJsonFile } from '../files.ts'; -import { editYamlFile, readYamlFile, scalarString, type YamlDocument } from '../files.ts'; +import { editYamlFile, readYamlFile, scalarString, type YamlDocument } from '../../utils/yaml.ts'; import { dropRemovePackageOverrideKeys, ensurePnpmExoticSubdepsSetting, @@ -31,7 +31,6 @@ import { shouldDropProviderOverrideKey, workspaceUsesWebdriverioProvider, } from '../migrator.ts'; -import { preserveSharedDependencies } from '../scope.ts'; import { type PreparedOxlintConfigs, LEGACY_WRAPPER_FALLBACK_VERSIONS, @@ -167,9 +166,6 @@ export function supportsCatalog( version: string, isBunWorkspace = false, ): boolean { - if (preserveSharedDependencies()) { - return false; - } switch (packageManager) { case PackageManager.pnpm: return pnpmSupportsCatalog(version); @@ -623,7 +619,7 @@ export function getCatalogDependencySpec( ) { return version; } - if (!supportCatalog || preserveSharedDependencies() || version.startsWith('file:')) { + if (!supportCatalog || version.startsWith('file:')) { return version; } return currentValue?.startsWith('catalog:') @@ -905,9 +901,6 @@ export function rewriteCatalog( catalogs?: Record>; } | null; const preferredCatalogSpec = selectPreferredCatalogSpec(parsed?.catalog, parsed?.catalogs); - if (preserveSharedDependencies()) { - return preferredCatalogSpec; - } const preferredCatalogName = preferredCatalogSpec.slice('catalog:'.length); const targetPath: readonly string[] = preferredCatalogName && preferredCatalogName !== 'default' @@ -1097,9 +1090,6 @@ export function rewriteBunCatalog( vitestEcosystemPackages: ReadonlySet, usesWebdriverioProvider = false, ): void { - if (preserveSharedDependencies()) { - return; - } const packageJsonPath = path.join(projectPath, 'package.json'); if (!fs.existsSync(packageJsonPath)) { return; diff --git a/packages/cli/src/migration/migrator/core-finalization.ts b/packages/cli/src/migration/migrator/core-finalization.ts index eadcd558c7..a42053ccde 100644 --- a/packages/cli/src/migration/migrator/core-finalization.ts +++ b/packages/cli/src/migration/migrator/core-finalization.ts @@ -3,8 +3,8 @@ import path from 'node:path'; import { rewriteScripts } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; +import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { rulesDir } from '../../utils/path.ts'; -import { editJsonFile, readJsonFile } from '../files.ts'; import { dropDeadOxlintPluginsDependency, hasTsconfigTypesToRewrite, @@ -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/eslint.ts b/packages/cli/src/migration/migrator/eslint.ts index 72f8feb60b..bf9231d0e1 100644 --- a/packages/cli/src/migration/migrator/eslint.ts +++ b/packages/cli/src/migration/migrator/eslint.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { styleText } from 'node:util'; @@ -9,16 +10,14 @@ import { compareReversed, isLessThanOrEqual, normalize } from 'verkit'; import { ResolverFactory, rewriteEslint } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; import { getVpCommand, runCommandSilently } from '../../utils/command.ts'; +import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts'; import { fetchNpmResource, getNpmRegistry } from '../../utils/npm-config.ts'; import { displayRelative } from '../../utils/path.ts'; import { cancelAndExit } from '../../utils/prompts.ts'; import { getSilentSpinner, getSpinner } from '../../utils/spinner.ts'; import { hasBaseUrlInTsconfig } from '../../utils/tsconfig.ts'; import { detectConfigs } from '../detector.ts'; -import fs from '../files.ts'; -import { editJsonFile, isJsonFile, readJsonFile } from '../files.ts'; import { type MigrationReport } from '../report.ts'; -import { isMigrationIgnored, migrationScope } from '../scope.ts'; import { LINT_STAGED_JSON_CONFIG_FILES, LINT_STAGED_OTHER_CONFIG_FILES, @@ -218,13 +217,6 @@ export async function migrateEslintToOxlint( packages?: WorkspacePackage[], options?: { silent?: boolean; report?: MigrationReport }, ): Promise { - if ( - [eslintConfigFile, '.oxlintrc.json', '.oxlintrc.json.bak'].some( - (file) => file && isMigrationIgnored(path.join(projectPath, file)), - ) - ) { - return false; - } const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive); // Steps 1-2: Only run @oxlint/migrate if there's an eslint config at root @@ -260,22 +252,18 @@ export async function migrateEslintToOxlint( warnMigration(migrateResult.skippedRules, options?.report); } - // The upstream comment renamer scans the whole tree and cannot consume our - // exclusions. Existing ESLint disable comments already work with Oxlint. - if (!migrationScope()?.patterns.length) { - // Step 2: Replace eslint-disable comments with oxlint-disable - spinner.start('Replacing ESLint comments with Oxlint equivalents...'); - const replaceResult = await runOxlintMigrateStep( - projectPath, - migratePackage, - ['--replace-eslint-comments'], - spinner, - 'ESLint comment replacement failed', - `You can run \`vp dlx ${migratePackage} --replace-eslint-comments\` manually later`, - ); - if (replaceResult.ok) { - spinner.stop('ESLint comments replaced'); - } + // Step 2: Replace eslint-disable comments with oxlint-disable + spinner.start('Replacing ESLint comments with Oxlint equivalents...'); + const replaceResult = await runOxlintMigrateStep( + projectPath, + migratePackage, + ['--replace-eslint-comments'], + spinner, + 'ESLint comment replacement failed', + `You can run \`vp dlx ${migratePackage} --replace-eslint-comments\` manually later`, + ); + if (replaceResult.ok) { + spinner.stop('ESLint comments replaced'); } // Continue with cleanup regardless — .oxlintrc.json was generated successfully } @@ -297,7 +285,10 @@ export async function migrateEslintToOxlint( // package — delete eslint config files, scrub ESLint-ecosystem deps from // package.json, and rewrite eslint references in any local lint-staged // config. A monorepo running `vp migrate` is treated as adopted as a - // whole, with migration-ignored packages removed from this target list. + // whole; there's no per-package opt-out today. If a workspace package + // publishes a shared ESLint preset that you want to keep intact, exclude + // it from your `pnpm-workspace.yaml` / `workspaces` before running + // `vp migrate`, then add it back afterwards. const cleanupTargets = [ projectPath, ...(packages ?? []).map((p) => path.join(projectPath, p.path)), diff --git a/packages/cli/src/migration/migrator/framework-shim.ts b/packages/cli/src/migration/migrator/framework-shim.ts index 57acd7b936..9d2333a517 100644 --- a/packages/cli/src/migration/migrator/framework-shim.ts +++ b/packages/cli/src/migration/migrator/framework-shim.ts @@ -1,9 +1,8 @@ +import fs from 'node:fs'; import path from 'node:path'; -import fs from '../files.ts'; -import { readJsonFile } from '../files.ts'; +import { readJsonFile } from '../../utils/json.ts'; import { type MigrationReport } from '../report.ts'; -import { isMigrationIgnored } from '../scope.ts'; // .svelte files are handled by @sveltejs/vite-plugin-svelte (transpilation) // and svelte-check / Svelte Language Server (type checking). @@ -88,9 +87,6 @@ export function addFrameworkShim( report?: MigrationReport, ): void { const envDtsPath = getEnvDtsPath(projectPath); - if (isMigrationIgnored(envDtsPath)) { - return; - } const shim = FRAMEWORK_SHIMS[framework]; if (fs.existsSync(envDtsPath)) { const existing = fs.readFileSync(envDtsPath, 'utf-8'); diff --git a/packages/cli/src/migration/migrator/git-hooks.ts b/packages/cli/src/migration/migrator/git-hooks.ts index b82e67b4f9..7273b40d4c 100644 --- a/packages/cli/src/migration/migrator/git-hooks.ts +++ b/packages/cli/src/migration/migrator/git-hooks.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; @@ -19,8 +20,7 @@ import type { PackageManager, WorkspacePackage } from '../../types/index.ts'; import { getVpCommand } from '../../utils/command.ts'; import { documentationUrl } from '../../utils/documentation.ts'; import { findGitRoot } from '../../utils/git.ts'; -import fs from '../files.ts'; -import { editJsonFile, isJsonFile, readJsonFile } from '../files.ts'; +import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts'; import { hasStagedConfigInViteConfig, mergeStagedConfigToViteConfig, @@ -28,7 +28,6 @@ import { removeLintStagedFromPackageJson, } from '../migrator.ts'; import { type MigrationReport } from '../report.ts'; -import { isMigrationIgnored } from '../scope.ts'; import { getCommandStartIndex, LINT_STAGED_ALL_CONFIG_FILES, @@ -463,9 +462,6 @@ export function installGitHooks( } // Install git hooks via vp config (--no-agent to skip agent setup, handled by migration) - if (isMigrationIgnored(path.join(projectPath, hooksDir, 'pre-commit'))) { - return false; - } const { command, args } = getVpCommand(['config', '--no-agent']); const configResult = spawn.sync(command, args, { cwd: projectPath, @@ -525,11 +521,8 @@ const DEFAULT_STAGED_CONFIG: Record = { '*': 'vp check --fix' }; export function createPreCommitHook(projectPath: string, dir = '.vite-hooks'): void { const huskyDir = path.join(projectPath, dir); - const hookPath = path.join(huskyDir, 'pre-commit'); - if (isMigrationIgnored(hookPath)) { - return; - } fs.mkdirSync(huskyDir, { recursive: true }); + const hookPath = path.join(huskyDir, 'pre-commit'); if (fs.existsSync(hookPath)) { return; } diff --git a/packages/cli/src/migration/migrator/orchestrators.ts b/packages/cli/src/migration/migrator/orchestrators.ts index 89cdc07a85..bf4aed2879 100644 --- a/packages/cli/src/migration/migrator/orchestrators.ts +++ b/packages/cli/src/migration/migrator/orchestrators.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { PackageManager, type WorkspaceInfo, type WorkspacePackage } from '../../types/index.ts'; import { VITE_PLUS_NAME, VITE_PLUS_VERSION, isForceOverrideMode } from '../../utils/constants.ts'; -import { editJsonFile, readJsonFile } from '../files.ts'; +import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { applyBuildAllowanceToPackageJsonPnpm, applyYarnWorkspaceHoistingFix, @@ -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/oxlint-config.ts b/packages/cli/src/migration/migrator/oxlint-config.ts index 7d2ebb1a7f..b971d3fd49 100644 --- a/packages/cli/src/migration/migrator/oxlint-config.ts +++ b/packages/cli/src/migration/migrator/oxlint-config.ts @@ -1,4 +1,5 @@ import { createHash } from 'node:crypto'; +import fs from 'node:fs'; import path from 'node:path'; import type { Program } from '@oxc-project/types'; @@ -7,7 +8,6 @@ import type { OxlintConfig } from 'oxlint'; import { analyzeMigrationSource, ResolverFactory } from '../../../binding/index.js'; import { detectConfigs } from '../detector.ts'; -import fs from '../files.ts'; import { ensureSvelteRuneGlobals } from '../migrator.ts'; import { OXLINT_OWNER_CONFIG_KEYS, diff --git a/packages/cli/src/migration/migrator/prettier.ts b/packages/cli/src/migration/migrator/prettier.ts index a096f01978..f837621375 100644 --- a/packages/cli/src/migration/migrator/prettier.ts +++ b/packages/cli/src/migration/migrator/prettier.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import { styleText } from 'node:util'; @@ -6,15 +7,13 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts'; import { rewritePrettier } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; import { getVpCommand, runCommandSilently } from '../../utils/command.ts'; +import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { displayRelative } from '../../utils/path.ts'; import { cancelAndExit } from '../../utils/prompts.ts'; import { getSilentSpinner, getSpinner } from '../../utils/spinner.ts'; import { PRETTIER_CONFIG_FILES, PRETTIER_PACKAGE_JSON_CONFIG, detectConfigs } from '../detector.ts'; -import fs from '../files.ts'; -import { editJsonFile, readJsonFile } from '../files.ts'; import { rewriteToolLintStagedConfigFiles } from '../migrator.ts'; import { type MigrationReport } from '../report.ts'; -import { isMigrationIgnored } from '../scope.ts'; import { warnMigration } from './shared.ts'; export function detectPrettierProject( @@ -108,13 +107,6 @@ export async function migratePrettierToOxfmt( packages?: WorkspacePackage[], options?: { silent?: boolean; report?: MigrationReport }, ): Promise<'migrated' | 'skipped' | 'failed'> { - if ( - [prettierConfigFile, '.oxfmtrc.json'].some( - (file) => file && isMigrationIgnored(path.join(projectPath, file)), - ) - ) { - return 'skipped'; - } const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive); // Step 1: Generate .oxfmtrc.json from Prettier config diff --git a/packages/cli/src/migration/migrator/script-runner.ts b/packages/cli/src/migration/migrator/script-runner.ts index 351fc693a0..8d68ee0894 100644 --- a/packages/cli/src/migration/migrator/script-runner.ts +++ b/packages/cli/src/migration/migrator/script-runner.ts @@ -1,14 +1,14 @@ +import fs from 'node:fs'; import path from 'node:path'; import { parse as parseJsonc } from 'jsonc-parser'; import { type LiteralCommandToken, parseLiteralCommands } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; +import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts'; import { readNearestPackageJson } from '../../utils/package.ts'; import { displayRelative } from '../../utils/path.ts'; import { NODE_OPTIONS_WITH_VALUE, SCRIPT_EXTENSIONS } from '../../vpx-script.ts'; -import fs from '../files.ts'; -import { editJsonFile, isJsonFile, readJsonFile } from '../files.ts'; import { sourceTreeMatches } from '../migrator.ts'; import { type MigrationReport } from '../report.ts'; import { diff --git a/packages/cli/src/migration/migrator/setup.ts b/packages/cli/src/migration/migrator/setup.ts index 1183c89b37..a124ba8f25 100644 --- a/packages/cli/src/migration/migrator/setup.ts +++ b/packages/cli/src/migration/migrator/setup.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; @@ -7,9 +8,8 @@ import { parse as parseYaml } from 'yaml'; import { type DownloadPackageManagerResult } from '../../../binding/index.js'; import { SETUP_VP_VERSION } from '../../utils/constants.ts'; +import { editJsonFile } from '../../utils/json.ts'; import { detectConfigs } from '../detector.ts'; -import fs from '../files.ts'; -import { editJsonFile } from '../files.ts'; import { type MigrationReport } from '../report.ts'; import { isPlainRecord, warnMigration } from './shared.ts'; diff --git a/packages/cli/src/migration/migrator/source-scan.ts b/packages/cli/src/migration/migrator/source-scan.ts index df44b567e5..5f56f0731b 100644 --- a/packages/cli/src/migration/migrator/source-scan.ts +++ b/packages/cli/src/migration/migrator/source-scan.ts @@ -3,11 +3,10 @@ import path from 'node:path'; import { type WorkspacePackage } from '../../types/index.ts'; import { VITE_PLUS_NAME } from '../../utils/constants.ts'; +import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { detectPackageMetadata } from '../../utils/package.ts'; import { hasVitestTypesInTsconfig } from '../../utils/tsconfig.ts'; -import { editJsonFile, readJsonFile } from '../files.ts'; import { projectUsesVitestDirectly } from '../migrator.ts'; -import { isMigrationIgnored } from '../scope.ts'; import { OPT_IN_BROWSER_PROVIDERS, OXLINT_PLUGINS_PACKAGE, @@ -230,9 +229,6 @@ export function sourceTreeMatches( } for (const entry of entries) { const entryPath = path.join(dir, entry.name); - if (isMigrationIgnored(entryPath, entry.isDirectory())) { - continue; - } if (entry.isDirectory()) { if (skipDirs.has(entry.name)) { continue; @@ -294,9 +290,6 @@ function findPackageTsconfigFiles(projectPath: string): string[] { } for (const entry of entries) { const entryPath = path.join(dir, entry.name); - if (isMigrationIgnored(entryPath, entry.isDirectory())) { - continue; - } if (entry.isDirectory()) { if (!VITEST_SCAN_SKIP_DIRS.has(entry.name)) { scanDir(entryPath, false); diff --git a/packages/cli/src/migration/migrator/tsconfig.ts b/packages/cli/src/migration/migrator/tsconfig.ts index c1db8452c3..58782601f8 100644 --- a/packages/cli/src/migration/migrator/tsconfig.ts +++ b/packages/cli/src/migration/migrator/tsconfig.ts @@ -8,7 +8,6 @@ import { rewriteTypesInTsconfig, } from '../../utils/tsconfig.ts'; import { type MigrationReport } from '../report.ts'; -import { isMigrationIgnored } from '../scope.ts'; import { warnMigration } from './shared.ts'; export function cleanupDeprecatedTsconfigOptions( @@ -17,7 +16,7 @@ export function cleanupDeprecatedTsconfigOptions( report?: MigrationReport, ): void { const deprecatedOptions = ['esModuleInterop', 'allowSyntheticDefaultImports']; - const files = findTsconfigFiles(projectPath).filter((file) => !isMigrationIgnored(file)); + const files = findTsconfigFiles(projectPath); for (const filePath of files) { for (const name of deprecatedOptions) { if (removeDeprecatedTsconfigFalseOption(filePath, name)) { @@ -42,7 +41,7 @@ export function rewriteTsconfigTypes( report?: MigrationReport, ): boolean { let changed = false; - const files = findTsconfigFiles(projectPath).filter((file) => !isMigrationIgnored(file)); + const files = findTsconfigFiles(projectPath); for (const filePath of files) { if (rewriteTypesInTsconfig(filePath)) { changed = true; @@ -58,7 +57,5 @@ export function rewriteTsconfigTypes( } export function hasTsconfigTypesToRewrite(projectPath: string): boolean { - return findTsconfigFiles(projectPath) - .filter((file) => !isMigrationIgnored(file)) - .some((filePath) => hasTypesToRewriteInTsconfig(filePath)); + return findTsconfigFiles(projectPath).some((filePath) => hasTypesToRewriteInTsconfig(filePath)); } diff --git a/packages/cli/src/migration/migrator/tsup.ts b/packages/cli/src/migration/migrator/tsup.ts index 44f68b4479..d9e5846fe9 100644 --- a/packages/cli/src/migration/migrator/tsup.ts +++ b/packages/cli/src/migration/migrator/tsup.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import { styleText } from 'node:util'; @@ -7,15 +8,12 @@ import corePkg from 'vite/package.json' with { type: 'json' }; import { PackageManager, type WorkspacePackage } from '../../types/index.ts'; import { getVpCommand, runCommandSilently } from '../../utils/command.ts'; import { TSDOWN_MIGRATION_SKILL_URL } from '../../utils/constants.ts'; +import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { displayRelative } from '../../utils/path.ts'; import { cancelAndExit } from '../../utils/prompts.ts'; import { getSilentSpinner, getSpinner } from '../../utils/spinner.ts'; import { detectConfigs, TSUP_CONFIG_FILES, TSUP_PACKAGE_JSON_CONFIG } from '../detector.ts'; -import fs from '../files.ts'; -import { editJsonFile, readJsonFile } from '../files.ts'; import { addMigrationWarning, type MigrationReport } from '../report.ts'; -import { isMigrationIgnored } from '../scope.ts'; -import { warnMigration } from './shared.ts'; function showTsdownMigrationOptions( targetLabel = 'the project root', @@ -430,17 +428,6 @@ export async function migrateTsupToTsdown( const tsupTargets = targets.filter((target) => target === rootPath ? !!tsupConfigFile : !!detectConfigs(target).tsupConfig, ); - if ( - tsupTargets.some((target) => - TSDOWN_MIGRATION_FILES.some((file) => isMigrationIgnored(path.join(target, file))), - ) - ) { - warnMigration( - 'Skipped tsup migration because migrate.ignorePatterns excludes one of its input or output files.', - options?.report, - ); - return false; - } const configCollisions = collectTsupConfigCollisions(tsupTargets); if (configCollisions.length > 0) { prompts.log.warn( diff --git a/packages/cli/src/migration/migrator/vite-config.ts b/packages/cli/src/migration/migrator/vite-config.ts index 2eaf6b5b94..df21c40394 100644 --- a/packages/cli/src/migration/migrator/vite-config.ts +++ b/packages/cli/src/migration/migrator/vite-config.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import type { Program } from '@oxc-project/types'; @@ -11,6 +12,7 @@ import { mergeJsonConfig, mergeTsdownConfig, migrateTaskCacheConfig, + readStaticLintConfig, rewriteImportsInDirectory, rewriteScripts, wrapLazyPlugins, @@ -23,11 +25,10 @@ import { import { type WorkspacePackage } from '../../types/index.ts'; import { BASEURL_TSCONFIG_WARNING, VITE_PLUS_NAME } from '../../utils/constants.ts'; import { documentationUrl } from '../../utils/documentation.ts'; +import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts'; import { displayRelative } from '../../utils/path.ts'; import { hasBaseUrlInTsconfig } from '../../utils/tsconfig.ts'; import { detectConfigs, type ConfigFiles } from '../detector.ts'; -import fs from '../files.ts'; -import { editJsonFile, isJsonFile, readJsonFile } from '../files.ts'; import { collectInstalledPackageNames, oxlintMigrationMarker, @@ -40,7 +41,6 @@ import { sanitizeInheritedJsPlugins, } from '../migrator.ts'; import { type MigrationReport } from '../report.ts'; -import { isMigrationIgnored, migrationScope } from '../scope.ts'; import { type OxlintConfigMigration, type OxlintModulePlan, @@ -196,9 +196,6 @@ export function mergeTsdownConfigFile( report?: MigrationReport, ): boolean { const configs = detectConfigs(projectPath); - if (isMigrationIgnored(path.join(projectPath, configs.viteConfig ?? 'vite.config.ts'))) { - return false; - } if (!configs.tsdownConfig) { return false; } @@ -207,9 +204,6 @@ export function mergeTsdownConfigFile( const fullViteConfigPath = path.join(projectPath, viteConfig); const fullTsdownConfigPath = path.join(projectPath, configs.tsdownConfig); - if (isMigrationIgnored(fullViteConfigPath) || isMigrationIgnored(fullTsdownConfigPath)) { - return false; - } // For JSON files, merge content directly and delete the file if (configs.tsdownConfig.endsWith('.json')) { @@ -278,9 +272,6 @@ export function mergeViteConfigFiles( preparedOxlintConfigs?: PreparedOxlintConfigs, ): void { const configs = detectConfigs(projectPath); - if (isMigrationIgnored(path.join(projectPath, configs.viteConfig ?? 'vite.config.ts'))) { - return; - } if (!configs.oxfmtConfig && !configs.oxlintConfig) { return; } @@ -597,9 +588,6 @@ function injectConfigDefaults( report?: MigrationReport, ): void { const configs = detectConfigs(projectPath); - if (isMigrationIgnored(path.join(projectPath, configs.viteConfig ?? 'vite.config.ts'))) { - return; - } if (configs.viteConfig && hasConfigKey(path.join(projectPath, configs.viteConfig), configKey)) { return; } @@ -660,13 +648,6 @@ function mergeAndRemoveJsonConfig( ): boolean { const fullViteConfigPath = path.join(projectPath, viteConfigPath); const fullJsonConfigPath = path.join(projectPath, jsonConfigPath); - if ( - isMigrationIgnored(fullViteConfigPath) || - isMigrationIgnored(fullJsonConfigPath) || - [...(modulePlan?.modules.keys() ?? [])].some((file) => isMigrationIgnored(file)) - ) { - return false; - } // Skip merge when the key is already present in vite.config.ts — the Rust // merge step always prepends, so without this guard a template that ships // both an inline `${configKey}:` block and a standalone JSON file (e.g. @@ -780,9 +761,6 @@ export function mergeStagedConfigToViteConfig( report?: MigrationReport, ): boolean { const configs = detectConfigs(projectPath); - if (isMigrationIgnored(path.join(projectPath, configs.viteConfig ?? 'vite.config.ts'))) { - return false; - } const viteConfig = ensureViteConfig(projectPath, configs, silent, report); const fullViteConfigPath = path.join(projectPath, viteConfig); @@ -909,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 @@ -923,13 +960,13 @@ export function rewriteAllImports( // `rewritePackageJson` stripped `oxlint` from their manifests. See // `collectOxlintOwnerDirs`. oxlintOwnerDirs: string[] = [], + importRuleConfig?: string, ): boolean { const result = rewriteImportsInDirectory( projectPath, preserveNuxtVitestImports, oxlintOwnerDirs, - migrationScope()?.root, - migrationScope()?.patterns, + importRuleConfig, ); const modified = result.modifiedFiles.length; const preserved = result.preservedVitestFiles.length; diff --git a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts index 33bf198171..5a4197d669 100644 --- a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts +++ b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import { PackageManager, type WorkspaceInfo, type WorkspacePackage } from '../../types/index.ts'; @@ -8,10 +9,9 @@ import { VITE_PLUS_VERSION, isForceOverrideMode, } from '../../utils/constants.ts'; +import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { type NpmWorkspaces } from '../../utils/workspace.ts'; -import fs from '../files.ts'; -import { editJsonFile, readJsonFile } from '../files.ts'; -import { editYamlFile, readYamlFile, type YamlDocument } from '../files.ts'; +import { editYamlFile, readYamlFile, type YamlDocument } from '../../utils/yaml.ts'; import { alignVitestEcosystemPackages, applyBuildAllowanceToPackageJsonPnpm, @@ -53,7 +53,6 @@ import { yarnrcSatisfiesVitePlus, } from '../migrator.ts'; import { type DependencyVersionChange, type MigrationReport } from '../report.ts'; -import { preserveSharedDependencies } from '../scope.ts'; import { BROWSER_PROVIDER_PEER_DEPS, hasProviderPeerDependency, @@ -136,9 +135,6 @@ export function overridesSatisfyVitePlus( keyStyle: ManagedOverrideKeyStyle = 'bare', usesWebdriverioProvider = false, ): boolean { - if (preserveSharedDependencies()) { - return true; - } const managed = managedOverridePackages(usesVitest, usesWebdriverioProvider); // Common case: a lingering managed `vitest` override is not satisfied. It // must be removed, so the bootstrap stays pending until then. Both key @@ -885,9 +881,6 @@ function ensureOverrideEntries( keyStyle: ManagedOverrideKeyStyle = 'bare', usesWebdriverioProvider = false, ): { overrides: Record; changed: boolean } { - if (preserveSharedDependencies()) { - return { overrides: overrides ?? {}, changed: false }; - } const next = { ...overrides }; let changed = false; // Common case: drop a lingering managed `vitest` override. diff --git a/packages/cli/src/migration/migrator/vitest-ecosystem.ts b/packages/cli/src/migration/migrator/vitest-ecosystem.ts index 743d93b19f..ffff16ac75 100644 --- a/packages/cli/src/migration/migrator/vitest-ecosystem.ts +++ b/packages/cli/src/migration/migrator/vitest-ecosystem.ts @@ -10,11 +10,7 @@ import { VITE_PLUS_OVERRIDE_PACKAGES, } from '../../utils/constants.ts'; import { readJsonFile } from '../../utils/json.ts'; -import { - extractOverrideParentSegments, - extractOverrideTargetName, -} from '../../utils/package-overrides.ts'; -export { extractOverrideParentSegments } from '../../utils/package-overrides.ts'; +import { extractOverrideTargetName } from '../../utils/package-overrides.ts'; import { detectPackageMetadata } from '../../utils/package.ts'; import { isAlignableVitestEcosystemPackage } from '../../utils/vitest-ecosystem.ts'; import { @@ -64,6 +60,16 @@ function isRemovePackageOverrideKey(key: string): boolean { ); } +// Strip a trailing `@version`/range from a selector segment and keep its scope. +// Mirrors the version-suffix peeling in `extractOverrideTargetName`: the version +// `@` follows the name (after the `/` of a scoped name); the leading scope `@` +// is never a version separator. +function stripSegmentVersion(segment: string): string { + const nameStart = segment.startsWith('@') ? segment.indexOf('/') + 1 : 0; + const versionAt = segment.indexOf('@', nameStart); + return versionAt > 0 ? segment.slice(0, versionAt) : segment; +} + // True iff a single parent-NAME glob segment matches the given literal package // name. `*` matches any run of characters; all other glob/regex metacharacters // are escaped. Used for the concrete ancestor segments of a selector. @@ -124,6 +130,64 @@ function parentChainReachesVitePlus(segments: string[]): boolean { return true; } +// Extract the ordered PARENT chain of an override/resolution key — the ancestor +// segments above the forced TARGET — or `null` when the key has no parent +// selector (a bare/versioned global pin). Each segment's own `@version`/range is +// stripped and scoped names (`@scope/name`) are kept whole; glob segments (`**`, +// `vite-*`) are preserved verbatim for `parentChainReachesVitePlus`. +// +// Mirrors `extractOverrideTargetName`'s grammar so target and parent stay +// consistent (see that function for the full delimiter rationale): +// - pnpm `a>b>child`: every `>`-separated prefix is a parent level (`a`, `b`); +// pnpm has no globs, so a chain of length > 1 always carries a specific +// ancestor. +// - yarn `from/descriptor`: the descriptor is the trailing 1 (unscoped) or 2 +// (scoped) segments; the remaining leading `/`-segments are the `from` chain, +// with scoped ancestors (`@scope/name`) rejoined. +// - bare/versioned names (`pkg`, `@scope/pkg`, `pkg@4`) have NO parent → `null`. +export function extractOverrideParentSegments(key: string): string[] | null { + let rest = key.trim(); + // Peel every pnpm `>` parent level. pnpm splits at a `>` whose preceding char + // is NOT space, `|`, or `@` (its DELIMITER_REGEX), so semver comparators like + // `pkg@>=4` are not mistaken for a parent selector. + const pnpmParents: string[] = []; + for (let delim = rest.search(/[^ |@]>/); delim !== -1; delim = rest.search(/[^ |@]>/)) { + pnpmParents.push(stripSegmentVersion(rest.slice(0, delim + 1).trim())); + rest = rest.slice(delim + 2).trim(); + } + if (pnpmParents.length > 0) { + return pnpmParents; + } + // No pnpm parent — check for a yarn `from/descriptor` selector. `rest` is the + // child (target) descriptor; only a `/` beyond a single scoped name leaves a + // leading `from` (parent) chain. + if (!rest.includes('/')) { + return null; + } + const segments = rest.split('/'); + // The trailing descriptor occupies the last 2 segments when it is a scoped + // name (second-to-last segment starts with `@`), else the last 1. + const descriptorIsScoped = segments[segments.length - 2]?.startsWith('@') ?? false; + const descriptorSegmentCount = descriptorIsScoped ? 2 : 1; + const rawParents = segments.slice(0, segments.length - descriptorSegmentCount); + if (rawParents.length === 0) { + // The whole key was a bare scoped name (`@scope/pkg`) — no parent selector. + return null; + } + // Rejoin scoped ancestors (`@scope` + `name`) and strip each segment's version. + const parents: string[] = []; + for (let i = 0; i < rawParents.length; i += 1) { + const segment = rawParents[i]; + if (segment.startsWith('@') && i + 1 < rawParents.length) { + parents.push(stripSegmentVersion(`${segment}/${rawParents[i + 1]}`)); + i += 1; + } else { + parents.push(stripSegmentVersion(segment)); + } + } + return parents; +} + // True iff a provider override/resolution key (target ∈ // PROVIDER_OVERRIDE_DROP_NAMES) should be dropped because the pin would affect // vite-plus's OWN direct provider dep. The pin reaches that dep iff its parent diff --git a/packages/cli/src/migration/migrator/vitest-v5.ts b/packages/cli/src/migration/migrator/vitest-v5.ts index 41907f0515..3113afaf90 100644 --- a/packages/cli/src/migration/migrator/vitest-v5.ts +++ b/packages/cli/src/migration/migrator/vitest-v5.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import { applyEdits, findNodeAtLocation, parse as parseJsonc, parseTree } from 'jsonc-parser'; @@ -21,9 +22,7 @@ import cliPackage from '../../../package.json' with { type: 'json' }; import { PackageManager, type WorkspaceInfoOptional } from '../../types/index.ts'; import { findGitRoot } from '../../utils/git.ts'; import { detectPackageMetadata } from '../../utils/package.ts'; -import fs from '../files.ts'; import { createCatalogDependencyResolver } from '../migrator.ts'; -import { isMigrationIgnored } from '../scope.ts'; import type { RewriteResult, SourceOptions, VitestV5Finding } from '../vitest-v5/ast.ts'; import { migrateVitestV5Command } from '../vitest-v5/commands.ts'; import { @@ -142,9 +141,6 @@ function filesInProject(directory: string): string[] { } for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const file = path.join(current, entry.name); - if (isMigrationIgnored(file, entry.isDirectory())) { - continue; - } // Never follow symlinks into another package or outside the workspace. if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) { walk(file); diff --git a/packages/cli/src/migration/migrator/yarn.ts b/packages/cli/src/migration/migrator/yarn.ts index d773455258..fb39f76881 100644 --- a/packages/cli/src/migration/migrator/yarn.ts +++ b/packages/cli/src/migration/migrator/yarn.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -10,9 +11,8 @@ import { VITE_PLUS_NAME, VITE_PLUS_VERSION, } from '../../utils/constants.ts'; -import fs from '../files.ts'; -import { readJsonFile } from '../files.ts'; -import { editYamlFile, readYamlFile, scalarString } from '../files.ts'; +import { readJsonFile } from '../../utils/json.ts'; +import { editYamlFile, readYamlFile, scalarString } from '../../utils/yaml.ts'; import { createCatalogDependencyResolverFromCatalogs, overridesSatisfyVitePlus, diff --git a/packages/cli/src/migration/scope.ts b/packages/cli/src/migration/scope.ts deleted file mode 100644 index 6910c1e4c6..0000000000 --- a/packages/cli/src/migration/scope.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { AsyncLocalStorage } from 'node:async_hooks'; -import fs from 'node:fs'; -import path from 'node:path'; - -import { readStaticMigrationConfig } from '../../binding/index.js'; -import { migrationIgnorePatterns } from '../migrate-config.ts'; -import { createMigrationIgnoreMatcher } from '../migration-ignore.ts'; -import { findViteConfig, resolveViteConfig } from '../resolve-vite-config.ts'; -import { PackageManager } from '../types/index.ts'; -import { VITEST_VERSION, VITE_PLUS_OVERRIDE_PACKAGES } from '../utils/constants.ts'; -import { readJsonFile } from '../utils/json.ts'; -import { - extractOverrideParentSegments, - extractOverrideTargetName, -} from '../utils/package-overrides.ts'; -import { isAlignableVitestEcosystemPackage } from '../utils/vitest-ecosystem.ts'; -import { readYamlFile } from '../utils/yaml.ts'; - -export class MigrationScope { - readonly matcher; - readonly ignoredManifests: string[] = []; - - constructor( - readonly root: string, - readonly patterns: string[], - ) { - this.matcher = createMigrationIgnoreMatcher(root, patterns); - if (patterns.length > 0) { - this.collectIgnoredManifests(root); - } - } - - ignores(file: string, isDirectory = false): boolean { - return this.matcher.isIgnored(path.resolve(file), isDirectory); - } - - private collectIgnoredManifests(dir: string): void { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const file = path.join(dir, entry.name); - if (entry.isDirectory() && !['node_modules', '.git', '.yarn'].includes(entry.name)) { - this.collectIgnoredManifests(file); - } else if (entry.isFile() && entry.name === 'package.json' && this.ignores(file)) { - this.ignoredManifests.push(file); - } - } - } - - get preserveSharedDependencies(): boolean { - return this.ignoredManifests.length > 0; - } -} - -const scopes = new AsyncLocalStorage(); -export const withMigrationScope = (scope: MigrationScope, run: () => T): T => - scopes.run(scope, run); -export const migrationScope = () => scopes.getStore(); -export const isMigrationIgnored = (file: string, directory = false): boolean => - migrationScope()?.ignores(file, directory) ?? false; -export const preserveSharedDependencies = (): boolean => - migrationScope()?.preserveSharedDependencies ?? false; - -async function readMigrationPatterns(root: string): Promise { - const extracted = readStaticMigrationConfig(root); - const config = - extracted == null ? (await resolveViteConfig(root)).migrate : JSON.parse(extracted); - return migrationIgnorePatterns(config); -} - -// Package-manager discovery treats a non-member example as a standalone project. -// An explicit target inside this repository must still honor the root policy. -// A nested Git repository (for example a cloned template) is independent. -export async function checkEnclosingMigrationPolicy(root: string): Promise { - let dir = root; - while (!fs.existsSync(path.join(dir, '.git'))) { - const parent = path.dirname(dir); - if (parent === dir) { - return; - } - dir = parent; - if (!fs.existsSync(path.join(dir, 'package.json')) || !findViteConfig(dir)) { - continue; - } - const patterns = await readMigrationPatterns(dir); - if (patterns.length > 0) { - const ignored = createMigrationIgnoreMatcher(dir, patterns).isIgnored(root, true); - throw new Error( - ignored - ? `Cannot migrate ${path.relative(dir, root)}: excluded by migrate.ignorePatterns in ${findViteConfig(dir)}. No project files were changed.` - : `Run vp migrate from ${dir} so its migrate.ignorePatterns policy also applies to nested files. No project files were changed.`, - ); - } - } -} - -export async function loadMigrationScope( - root: string, - packageManager?: PackageManager, -): Promise { - const scope = new MigrationScope(root, await readMigrationPatterns(root)); - if (scope.preserveSharedDependencies) { - checkSharedOverrides(scope, packageManager); - } - return scope; -} - -// Catalogs and overrides belong to every consumer, including templates outside -// the workspace package list. Use explicit specs for migrated consumers and -// retain these shared settings in their original locations. -export const sharedDependencyFields = [ - ['catalog'], - ['catalogs'], - ['overrides'], - ['resolutions'], - ['pnpm', 'overrides'], - ['workspaces', 'catalog'], - ['workspaces', 'catalogs'], -] as const; - -function checkSharedOverrides(scope: MigrationScope, packageManager?: PackageManager): void { - const pkg = readJsonFile(path.join(scope.root, 'package.json')); - const settings = [pkg]; - for (const name of ['pnpm-workspace.yaml', '.yarnrc.yml']) { - if ( - packageManager && - name !== - (packageManager === PackageManager.pnpm - ? 'pnpm-workspace.yaml' - : packageManager === PackageManager.yarn - ? '.yarnrc.yml' - : '') - ) { - continue; - } - const file = path.join(scope.root, name); - if (fs.existsSync(file)) { - settings.push(readYamlFile(file) ?? {}); - } - } - const catalogs = - settings.find((value) => value.catalog || value.catalogs) ?? - (pkg.workspaces as Record | undefined) ?? - {}; - const resolveSpec = (spec: string, name: string): unknown => { - if (spec.startsWith('catalog:')) { - const catalogName = spec.slice(8) || 'default'; - const named = catalogs.catalogs as Record> | undefined; - return ( - catalogName === 'default' - ? ((catalogs.catalog as Record | undefined) ?? named?.default) - : named?.[catalogName] - )?.[name]; - } - if (spec.startsWith('$')) { - const dependency = spec.slice(1); - return ( - (pkg.dependencies as Record | undefined)?.[dependency] ?? - (pkg.devDependencies as Record | undefined)?.[dependency] - ); - } - return spec; - }; - const excludedNames = new Set( - scope.ignoredManifests.flatMap((file) => { - try { - return [readJsonFile(file).name]; - } catch { - return []; - } - }), - ); - const check = (overrides: unknown, parents: string[] = []) => { - if (!overrides || typeof overrides !== 'object') { - return; - } - for (const [selector, value] of Object.entries(overrides)) { - const key = selector === '.' ? parents.at(-1)! : selector; - const name = extractOverrideTargetName(key); - const ancestors = selector === '.' ? parents.slice(0, -1) : parents; - const chain = ancestors - .map(extractOverrideTargetName) - .concat(extractOverrideParentSegments(key) ?? []); - // An override scoped exclusively to an excluded package can stay as-is. - // For other selectors we cannot prove that the managed graph is isolated. - const excludedOnly = chain.length > 0 && excludedNames.has(chain[0]); - if ( - typeof value === 'string' && - (name === 'vite' || name === 'vitest' || isAlignableVitestEcosystemPackage(name)) && - !excludedOnly - ) { - const target = name === 'vite' ? VITE_PLUS_OVERRIDE_PACKAGES.vite : VITEST_VERSION; - if (resolveSpec(value, name) !== target) { - throw new Error( - `Cannot migrate while preserving excluded packages: override ${JSON.stringify(selector)} (${JSON.stringify(value)}) conflicts with ${JSON.stringify(target)}. Review the root override or migrate the excluded packages first. No project files were changed.`, - ); - } - } - if (value && typeof value === 'object') { - check(value, [...parents, selector]); - } - } - }; - for (const value of settings) { - if ( - !packageManager || - packageManager === PackageManager.npm || - packageManager === PackageManager.bun || - (packageManager === PackageManager.pnpm && value !== pkg) - ) { - check(value.overrides); - } - if (!packageManager || packageManager === PackageManager.yarn) { - check(value.resolutions); - } - if (!packageManager || packageManager === PackageManager.pnpm) { - check((value.pnpm as Record | undefined)?.overrides); - } - } -} diff --git a/packages/cli/src/oxlint-plugin.ts b/packages/cli/src/oxlint-plugin.ts index 54be443f9a..55ab819997 100644 --- a/packages/cli/src/oxlint-plugin.ts +++ b/packages/cli/src/oxlint-plugin.ts @@ -5,7 +5,6 @@ import { definePlugin, defineRule } from '@oxlint/plugins'; import type { Context, ESTree } from '@oxlint/plugins'; import cliPackage from '../package.json' with { type: 'json' }; -import { createMigrationIgnoreMatcher } from './migration-ignore.ts'; import { PREFER_VITE_PLUS_IMPORTS_RULE_NAME, VITE_PLUS_OXLINT_PLUGIN_NAME, @@ -439,8 +438,6 @@ export const preferVitePlusImportsRule = defineRule({ }, }, createOnce(context: Context) { - const matchers = new Map>(); - let ignored = false; const options: ImportRewriteOptions = { preserveUpstreamVitest: false, fileIsViteConfig: false, @@ -450,22 +447,6 @@ export const preferVitePlusImportsRule = defineRule({ }; return { Program() { - const policy = context.settings['vite-plus/migrate'] as - | { root: string; ignorePatterns: string[] } - | undefined; - let ignore: ReturnType | undefined; - if (policy) { - const key = JSON.stringify(policy); - ignore = matchers.get(key); - if (!ignore) { - ignore = createMigrationIgnoreMatcher(policy.root, policy.ignorePatterns); - matchers.set(key, ignore); - } - } - ignored = ignore?.isIgnored(path.resolve(context.filename), false) ?? false; - if (ignored) { - return; - } options.preserveUpstreamVitest = nearestPackageUsesNuxtTestUtils(context.filename); options.fileIsViteConfig = isViteConfigFile(context.filename); options.ownsOxlintApi = nearestPackageOwnsOxlintApi(context.filename); @@ -473,41 +454,23 @@ export const preferVitePlusImportsRule = defineRule({ options.keepsVitest = nearestPackageKeepsVitest(context.filename); }, ImportDeclaration(node) { - if (ignored) { - return; - } maybeReportLiteral(context, node.source, options); reportLegacyOxlintPluginApiImport(context, node, options.ownsOxlintApi); }, ExportAllDeclaration(node) { - if (ignored) { - return; - } maybeReportLiteral(context, node.source, options); }, ExportNamedDeclaration(node) { - if (ignored) { - return; - } maybeReportLiteral(context, node.source, options); reportLegacyOxlintPluginApiExport(context, node, options.ownsOxlintApi); }, ImportExpression(node) { - if (ignored) { - return; - } maybeReportLiteral(context, node.source, options); }, TSImportType(node) { - if (ignored) { - return; - } maybeReportLiteral(context, node.source, options); }, TSExternalModuleReference(node) { - if (ignored) { - return; - } // Keep import-equals declarations unchanged, matching the migrator's // treatment of require calls. if ( @@ -520,9 +483,6 @@ export const preferVitePlusImportsRule = defineRule({ maybeReportLiteral(context, node.expression, options); }, TSModuleDeclaration(node) { - if (ignored) { - return; - } if (node.global) { return; } diff --git a/packages/cli/src/utils/agent.ts b/packages/cli/src/utils/agent.ts index d3360a2854..4c940f38f6 100644 --- a/packages/cli/src/utils/agent.ts +++ b/packages/cli/src/utils/agent.ts @@ -418,7 +418,6 @@ export async function writeAgentInstructions({ interactive, conflictDecisions, silent = false, - shouldWriteFile = () => true, }: { projectRoot: string; targetPath?: string; @@ -426,7 +425,6 @@ export async function writeAgentInstructions({ interactive: boolean; conflictDecisions?: Map; silent?: boolean; - shouldWriteFile?: (file: string) => boolean; }) { const paths = [...(targetPaths ?? []), ...(targetPath ? [targetPath] : [])]; if (paths.length === 0) { @@ -447,9 +445,6 @@ export async function writeAgentInstructions({ for (const targetPathToWrite of orderAgentTargetPaths(projectRoot, paths)) { const destinationPath = path.join(projectRoot, targetPathToWrite); - if (!shouldWriteFile(destinationPath)) { - continue; - } await fsPromises.mkdir(path.dirname(destinationPath), { recursive: true }); diff --git a/packages/cli/src/utils/editor.ts b/packages/cli/src/utils/editor.ts index 4606781459..f986fb847f 100644 --- a/packages/cli/src/utils/editor.ts +++ b/packages/cli/src/utils/editor.ts @@ -338,7 +338,6 @@ export async function writeEditorConfigs({ silent = false, extraVsCodeSettings, packageManager = PackageManager.pnpm, - shouldWriteFile = () => true, }: { projectRoot: string; editorId: EditorSelection; @@ -347,7 +346,6 @@ export async function writeEditorConfigs({ silent?: boolean; extraVsCodeSettings?: Record; packageManager?: PackageManager; - shouldWriteFile?: (file: string) => boolean; }) { const editorIds = normalizeEditorSelection(editorId); if (editorIds.length === 0) { @@ -363,7 +361,6 @@ export async function writeEditorConfigs({ silent, extraVsCodeSettings, packageManager, - shouldWriteFile, }); } } @@ -376,7 +373,6 @@ async function writeEditorConfig({ silent, extraVsCodeSettings, packageManager, - shouldWriteFile, }: { projectRoot: string; editorId: EditorId; @@ -385,7 +381,6 @@ async function writeEditorConfig({ silent: boolean; extraVsCodeSettings?: Record; packageManager: PackageManager; - shouldWriteFile: (file: string) => boolean; }) { const editorConfig = EDITORS.find((e) => e.id === editorId); if (!editorConfig) { @@ -393,6 +388,7 @@ async function writeEditorConfig({ } const targetDir = path.join(projectRoot, editorConfig.targetDir); + await fsPromises.mkdir(targetDir, { recursive: true }); for (const [fileName, baseIncoming] of Object.entries(editorConfig.files)) { const incoming: EditorConfigValue = @@ -402,10 +398,6 @@ async function writeEditorConfig({ ? jetbrainsWorkspaceConfig(packageManager) : baseIncoming; const filePath = path.join(targetDir, fileName); - if (!shouldWriteFile(filePath)) { - continue; - } - await fsPromises.mkdir(targetDir, { recursive: true }); const jsonFormat = isJsonLikeFile(fileName); if (fs.existsSync(filePath)) { diff --git a/packages/cli/src/utils/package-overrides.ts b/packages/cli/src/utils/package-overrides.ts index a8dd146540..f5a7234034 100644 --- a/packages/cli/src/utils/package-overrides.ts +++ b/packages/cli/src/utils/package-overrides.ts @@ -30,71 +30,3 @@ export function extractOverrideTargetName(key: string): string { const versionAt = target.indexOf('@', nameStart); return versionAt > 0 ? target.slice(0, versionAt) : target; } - -// Strip a trailing `@version`/range from a selector segment and keep its scope. -// Mirrors the version-suffix peeling in `extractOverrideTargetName`: the version -// `@` follows the name (after the `/` of a scoped name); the leading scope `@` -// is never a version separator. -function stripSegmentVersion(segment: string): string { - const nameStart = segment.startsWith('@') ? segment.indexOf('/') + 1 : 0; - const versionAt = segment.indexOf('@', nameStart); - return versionAt > 0 ? segment.slice(0, versionAt) : segment; -} - -// Extract the ordered PARENT chain of an override/resolution key — the ancestor -// segments above the forced TARGET — or `null` when the key has no parent -// selector (a bare/versioned global pin). Each segment's own `@version`/range is -// stripped and scoped names (`@scope/name`) are kept whole; glob segments (`**`, -// `vite-*`) are preserved verbatim for `parentChainReachesVitePlus`. -// -// Mirrors `extractOverrideTargetName`'s grammar so target and parent stay -// consistent (see that function for the full delimiter rationale): -// - pnpm `a>b>child`: every `>`-separated prefix is a parent level (`a`, `b`); -// pnpm has no globs, so a chain of length > 1 always carries a specific -// ancestor. -// - yarn `from/descriptor`: the descriptor is the trailing 1 (unscoped) or 2 -// (scoped) segments; the remaining leading `/`-segments are the `from` chain, -// with scoped ancestors (`@scope/name`) rejoined. -// - bare/versioned names (`pkg`, `@scope/pkg`, `pkg@4`) have NO parent → `null`. -export function extractOverrideParentSegments(key: string): string[] | null { - let rest = key.trim(); - // Peel every pnpm `>` parent level. pnpm splits at a `>` whose preceding char - // is NOT space, `|`, or `@` (its DELIMITER_REGEX), so semver comparators like - // `pkg@>=4` are not mistaken for a parent selector. - const pnpmParents: string[] = []; - for (let delim = rest.search(/[^ |@]>/); delim !== -1; delim = rest.search(/[^ |@]>/)) { - pnpmParents.push(stripSegmentVersion(rest.slice(0, delim + 1).trim())); - rest = rest.slice(delim + 2).trim(); - } - if (pnpmParents.length > 0) { - return pnpmParents; - } - // No pnpm parent — check for a yarn `from/descriptor` selector. `rest` is the - // child (target) descriptor; only a `/` beyond a single scoped name leaves a - // leading `from` (parent) chain. - if (!rest.includes('/')) { - return null; - } - const segments = rest.split('/'); - // The trailing descriptor occupies the last 2 segments when it is a scoped - // name (second-to-last segment starts with `@`), else the last 1. - const descriptorIsScoped = segments[segments.length - 2]?.startsWith('@') ?? false; - const descriptorSegmentCount = descriptorIsScoped ? 2 : 1; - const rawParents = segments.slice(0, segments.length - descriptorSegmentCount); - if (rawParents.length === 0) { - // The whole key was a bare scoped name (`@scope/pkg`) — no parent selector. - return null; - } - // Rejoin scoped ancestors (`@scope` + `name`) and strip each segment's version. - const parents: string[] = []; - for (let i = 0; i < rawParents.length; i += 1) { - const segment = rawParents[i]; - if (segment.startsWith('@') && i + 1 < rawParents.length) { - parents.push(stripSegmentVersion(`${segment}/${rawParents[i + 1]}`)); - i += 1; - } else { - parents.push(stripSegmentVersion(segment)); - } - } - return parents; -}