diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/.gitignore b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/.gitignore new file mode 100644 index 0000000000..cf7098890e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/.gitignore @@ -0,0 +1 @@ +**/node_modules diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/package.json new file mode 100644 index 0000000000..2459b1e885 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/package.json @@ -0,0 +1,6 @@ +{ + "name": "create-oxlint-workspace", + "private": true, + "type": "module", + "packageManager": "pnpm@10.0.0" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/packages/starter-template/bin/index.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/packages/starter-template/bin/index.mjs new file mode 100644 index 0000000000..fe60b3838f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/packages/starter-template/bin/index.mjs @@ -0,0 +1,15 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; + +const directoryFlag = process.argv.indexOf('--directory'); +const directory = directoryFlag === -1 ? 'app' : process.argv[directoryFlag + 1]; +mkdirSync(directory, { recursive: true }); + +function write(filename, content) { + writeFileSync(path.join(directory, filename), content); +} + +write('package.json', JSON.stringify({ name: path.basename(directory), type: 'module' })); +write('vite.config.mjs', 'export default {};\n'); +write('.oxlintrc.json', JSON.stringify({ extends: ['../../shared/lint.json'] })); +write('index.js', 'debugger;\n'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/packages/starter-template/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/packages/starter-template/package.json new file mode 100644 index 0000000000..6ed094f451 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/packages/starter-template/package.json @@ -0,0 +1,7 @@ +{ + "name": "starter-template", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": "./bin/index.mjs" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/pnpm-workspace.yaml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/pnpm-workspace.yaml new file mode 100644 index 0000000000..924b55f42e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/* diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/shared/lint.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/shared/lint.json new file mode 100644 index 0000000000..e54ba4dbb0 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/shared/lint.json @@ -0,0 +1,4 @@ +{ + "rules": { "no-debugger": "error" }, + "options": { "typeAware": false, "typeCheck": false } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/snapshots.toml new file mode 100644 index 0000000000..058083f841 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/snapshots.toml @@ -0,0 +1,17 @@ +[[case]] +name = "create_monorepo_oxlint_extends" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["node", "packages/starter-template/bin/index.mjs", "--directory", "packages/before"], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/before", comment = "Original template rules inherit from the workspace", continue-on-failure = true }, + { argv = ["vpt", "rm", "-r", "packages/before"], snapshot = false }, + { argv = ["vp", "create", "starter", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks", "--", "--directory", "app"] }, + { argv = ["vpt", "print-file", "packages/app/vite.config.mjs"] }, + { argv = ["vpt", "print-file", "shared/lint.ts"] }, + { argv = ["vpt", "stat-file", "packages/app/.oxlintrc.json", "--assert", "missing"] }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/app", comment = "The created package keeps the original diagnostic without a config flag", continue-on-failure = true }, + { argv = ["vpt", "replace-file-content", "shared/lint.ts", '"error"', '"warn"'] }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/app", comment = "The created package follows shared module edits" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/snapshots/create_monorepo_oxlint_extends.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/snapshots/create_monorepo_oxlint_extends.md new file mode 100644 index 0000000000..9debd837a0 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/snapshots/create_monorepo_oxlint_extends.md @@ -0,0 +1,123 @@ +# create_monorepo_oxlint_extends + +## `node packages/starter-template/bin/index.mjs --directory packages/before` + + +## `cd packages/before && vp lint --threads=1 -c .oxlintrc.json index.js` + +Original template rules inherit from the workspace + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vpt rm -r packages/before` + + +## `vp create starter --no-interactive --no-agent --no-editor --no-hooks -- --directory app` + +``` +◇ Generating project… +● Running: node /packages/starter-template/bin/index.mjs --directory app +◇ Monorepo integration... +◆ ✔ Merged packages/app/.oxlintrc.json into packages/app/vite.config.mjs +● Installing dependencies... +● Dependencies installed +● Formatting code... +● Code formatted +◇ Scaffolded packages/app +• Node pnpm +✓ Dependencies installed in +→ Next: cd packages/app && vp run +``` + +## `vpt print-file packages/app/vite.config.mjs` + +``` +import oxlintConfig1 from "../../shared/lint.js"; +export default { + lint: { + extends: [oxlintConfig1], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, +}; +``` + +## `vpt print-file shared/lint.ts` + +``` +// Generated by vp migrate from lint.json. +import type { OxlintConfig } from "vite-plus/lint"; + +export default { + "rules": { + "no-debugger": "error" + }, + "options": { + "typeAware": false, + "typeCheck": false + } +} satisfies OxlintConfig; +``` + +## `vpt stat-file packages/app/.oxlintrc.json --assert missing` + +``` +packages/app/.oxlintrc.json: missing +``` + +## `cd packages/app && vp lint --threads=1 index.js` + +The created package keeps the original diagnostic without a config flag + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vpt replace-file-content shared/lint.ts '"error"' '"warn"'` + +``` +``` + +## `cd packages/app && vp lint --threads=1 index.js` + +The created package follows shared module edits + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/vite.config.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/vite.config.mjs new file mode 100644 index 0000000000..60906c1d8a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_monorepo_oxlint_extends/vite.config.mjs @@ -0,0 +1,11 @@ +export default { + create: { + templates: [ + { + name: 'starter', + description: 'A package that inherits workspace lint rules.', + template: './packages/starter-template', + }, + ], + }, +}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_bun/snapshots/migration_monorepo_bun.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_bun/snapshots/migration_monorepo_bun.md index 7870efd952..eaf0906253 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_bun/snapshots/migration_monorepo_bun.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_bun/snapshots/migration_monorepo_bun.md @@ -19,6 +19,7 @@ VITE+ - The Unified Toolchain for the Web check vite.config.ts ``` +// vp migrate: merged .oxlintrc.json (aba19784a1d2c46ab69279abb5cab90f). import react from '@vitejs/plugin-react'; import { defineConfig, lazyPlugins } from 'vite-plus'; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_pnpm/snapshots/migration_monorepo_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_pnpm/snapshots/migration_monorepo_pnpm.md index 705de1f549..9b74a136c7 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_pnpm/snapshots/migration_monorepo_pnpm.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_pnpm/snapshots/migration_monorepo_pnpm.md @@ -20,6 +20,7 @@ VITE+ - The Unified Toolchain for the Web check vite.config.ts ``` +// vp migrate: merged .oxlintrc.json (aba19784a1d2c46ab69279abb5cab90f). import react from '@vitejs/plugin-react'; import { defineConfig, lazyPlugins } from 'vite-plus'; @@ -225,6 +226,7 @@ check only-oxlint package.json check only-oxlint vite.config.ts ``` +// vp migrate: merged packages%2Fonly-oxlint%2F.oxlintrc.json (b648b436b7a8c2627c4008844cff0e45). import { defineConfig } from 'vite-plus'; export default defineConfig({ diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_yarn4/snapshots/migration_monorepo_yarn4.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_yarn4/snapshots/migration_monorepo_yarn4.md index 32aef9609d..cf17652598 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_yarn4/snapshots/migration_monorepo_yarn4.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_yarn4/snapshots/migration_monorepo_yarn4.md @@ -27,6 +27,7 @@ VITE+ - The Unified Toolchain for the Web check vite.config.ts ``` +// vp migrate: merged .oxlintrc.json (aba19784a1d2c46ab69279abb5cab90f). import react from '@vitejs/plugin-react'; import { defineConfig, lazyPlugins } from 'vite-plus'; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/.gitignore b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/.oxlintrc.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/.oxlintrc.json new file mode 100644 index 0000000000..d35c8e9638 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/.oxlintrc.json @@ -0,0 +1,14 @@ +{ + "extends": [ + "./shared/nested/first.json", + "./shared/second.jsonc" + ], + "rules": { + "no-alert": "off", + "local/no-foo": "error" + }, + "ignorePatterns": [ + "shared/**", + "vite.config.ts" + ] +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/create-directory-link.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/create-directory-link.mjs new file mode 100644 index 0000000000..cc0eef5dec --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/create-directory-link.mjs @@ -0,0 +1,3 @@ +import { symlinkSync } from 'node:fs'; + +symlinkSync('./configs/nested', 'aliases', process.platform === 'win32' ? 'junction' : 'dir'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/example.test.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/example.test.js new file mode 100644 index 0000000000..f4faff2fd2 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/example.test.js @@ -0,0 +1,2 @@ +debugger; +export const bar = 1; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/index.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/index.js new file mode 100644 index 0000000000..4074824586 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/index.js @@ -0,0 +1,5 @@ +debugger; +console.log("test"); +alert("allowed"); +export const foo = 1; +export const bar = 1; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/package.json new file mode 100644 index 0000000000..55f399dac4 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/package.json @@ -0,0 +1,10 @@ +{ + "name": "migration-oxlint-extends", + "private": true, + "type": "module", + "devDependencies": { + "oxlint": "1.88.0", + "typescript": "6.0.3", + "@types/node": "24.10.3" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/base.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/base.json new file mode 100644 index 0000000000..da4b481d02 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/base.json @@ -0,0 +1,11 @@ +{ + "options": { + "typeAware": false, + "typeCheck": false + }, + "rules": { + "no-debugger": "error", + "no-console": "error", + "no-alert": "error" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/nested/first.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/nested/first.json new file mode 100644 index 0000000000..68d8867e1e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/nested/first.json @@ -0,0 +1,22 @@ +{ + "extends": [ + "../base.json" + ], + "overrides": [ + { + "files": [ + "**/*.test.js" + ], + "rules": { + "no-debugger": "warn", + "override/no-bar": "error" + }, + "jsPlugins": [ + { + "name": "override", + "specifier": "../override-plugin.mjs" + } + ] + } + ] +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/override-plugin.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/override-plugin.mjs new file mode 100644 index 0000000000..96d244c985 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/override-plugin.mjs @@ -0,0 +1,17 @@ +export default { + meta: { name: 'override' }, + rules: { + 'no-bar': { + meta: { schema: [] }, + create(context) { + return { + Identifier(node) { + if (node.name === 'bar') { + context.report({ node, message: 'Do not use bar.' }); + } + }, + }; + }, + }, + }, +}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/plugin.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/plugin.mjs new file mode 100644 index 0000000000..a332c05333 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/plugin.mjs @@ -0,0 +1,17 @@ +export default { + meta: { name: 'local' }, + rules: { + 'no-foo': { + meta: { schema: [] }, + create(context) { + return { + Identifier(node) { + if (node.name === 'foo') { + context.report({ node, message: 'Do not use foo.' }); + } + }, + }; + }, + }, + }, +}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/second.jsonc b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/second.jsonc new file mode 100644 index 0000000000..6134e8bd2b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/shared/second.jsonc @@ -0,0 +1,5 @@ +{ + // Later entries take priority. + "rules": { "no-console": "warn" }, + "jsPlugins": ["./plugin.mjs"] +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots.toml new file mode 100644 index 0000000000..762a835758 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots.toml @@ -0,0 +1,265 @@ +[[case]] +name = "migration_oxlint_extends" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +# Lint each file separately because directory traversal order differs across platforms. +steps = [ + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "Original inherited errors and warnings", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "example.test.js"], comment = "Original override rules and plugin", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "vite.config.ts"], comment = "Path extends become imports of shared TypeScript configs" }, + { argv = ["vpt", "print-file", "shared/nested/first.ts"], comment = "Nested imports preserve overrides and their plugin declarations" }, + { argv = ["vpt", "print-file", "shared/base.ts"] }, + { argv = ["vpt", "print-file", "shared/second.ts"], comment = "Relative plugin paths resolve from the shared module" }, + { argv = ["vpt", "stat-file", ".oxlintrc.json", "--assert-not", "file"] }, + { argv = ["vpt", "print-file", "shared/second.jsonc"], comment = "Shared JSON configs are retained" }, + { argv = ["vp", "exec", "tsc", "--noEmit"], comment = "Generated config types accept the resolved inheritance" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The diagnostics and exit code match the original config without a config flag", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "example.test.js"], comment = "The override diagnostics and exit code match the original config", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "vite.config.ts"], comment = "A second migration leaves the config unchanged" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The diagnostics and exit code remain unchanged after the second migration", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "example.test.js"], comment = "The override diagnostics and exit code also remain unchanged", continue-on-failure = true }, + { argv = ["vpt", "replace-file-content", "shared/base.ts", '"no-debugger": "error"', '"no-debugger": "warn"'], comment = "Edits to the shared TS config propagate through its imports" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The edited base rule now warns in index.js", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "example.test.js"], comment = "The test override remains unchanged after the shared config edit", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_type_options" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "json-edit", "shared/base.json", "options", '{"typeAware":true,"typeCheck":true}'], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "rules.typescript/no-floating-promises", '"error"'], snapshot = false }, + { argv = ["vpt", "write-file", "tsconfig.json", '{"compilerOptions":{"target":"ESNext","strict":true},"include":["*.ts"]}'], snapshot = false }, + { argv = ["vpt", "write-file", "type-error.ts", "export const value: string = 1;\n"], snapshot = false }, + { argv = ["vpt", "write-file", "type-aware.ts", "Promise.resolve(1);\n"], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "type-error.ts"], comment = "Inherited typeCheck reports the original TypeScript diagnostic", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "type-aware.ts"], comment = "Inherited typeAware enables no-floating-promises", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "vite.config.ts"], comment = "Local options stay empty so inherited settings remain live" }, + { argv = ["vp", "lint", "--threads=1", "type-error.ts"], comment = "Imported typeCheck produces the same diagnostic and exit code", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "type-aware.ts"], comment = "Imported typeAware produces the same diagnostic and exit code", continue-on-failure = true }, + { argv = ["vpt", "replace-file-content", "shared/base.ts", "typeAware: true", "typeAware: false"] }, + { argv = ["vpt", "replace-file-content", "shared/base.ts", "typeCheck: true", "typeCheck: false"] }, + { argv = ["vp", "lint", "--threads=1", "type-error.ts"], comment = "Disabling the shared options removes the TypeScript diagnostic" }, + { argv = ["vp", "lint", "--threads=1", "type-aware.ts"], comment = "Disabling the shared options removes the type-aware diagnostic" }, +] + +[[case]] +name = "migration_oxlint_extends_import_paths" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "cp", "shared/base.json", "shared/base#%.json"], snapshot = false }, + { argv = ["vpt", "cp", "shared/nested/first.json", "shared/nested/first#%.json"], snapshot = false }, + { argv = ["vpt", "json-edit", "shared/nested/first#%.json", "extends", '["../base#%.json"]'], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "extends", '["./shared/nested/first#%.json","./shared/second.jsonc"]'], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "Original diagnostics with special characters in shared config paths", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "vite.config.ts"] }, + { argv = ["vpt", "print-file", "shared/nested/first#%.ts"] }, + { argv = ["vp", "exec", "tsc", "--noEmit"], comment = "TypeScript resolves the literal import paths" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The migrated imports retain the same diagnostics and exit code", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_backslash_path" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +skip-platforms = ["windows"] +steps = [ + { argv = ["vpt", "cp", "shared/base.json", 'shared/base\name.json'], snapshot = false }, + { argv = ["vpt", "json-edit", "shared/nested/first.json", "extends", '["../base\\name.json"]'], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "Original diagnostics with a literal backslash in the shared config filename", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"], comment = "Warn and preserve the JSON because TypeScript treats the backslash as a separator" }, + { argv = ["vpt", "stat-file", ".oxlintrc.json", "--assert", "file"] }, + { argv = ["vpt", "stat-file", "shared/nested/first.ts", "--assert", "missing"] }, + { argv = ["vpt", "stat-file", 'shared/base\name.ts', "--assert", "missing"] }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "The retained JSON still produces the same diagnostics and exit code", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_query_path" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +skip-platforms = ["windows"] +steps = [ + { argv = ["vpt", "cp", "shared/base.json", "shared/base?.json"], snapshot = false }, + { argv = ["vpt", "json-edit", "shared/nested/first.json", "extends", '["../base?.json"]'], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "The original JSON resolves the question mark as part of the filename", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"], comment = "Warn and preserve the JSON instead of emitting an unresolvable import" }, + { argv = ["vpt", "stat-file", ".oxlintrc.json", "--assert", "file"] }, + { argv = ["vpt", "stat-file", "shared/nested/first.ts", "--assert", "missing"] }, + { argv = ["vpt", "stat-file", "shared/base?.ts", "--assert", "missing"] }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "The retained JSON still produces the same diagnostics and exit code", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_override_paths" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "write-file", ".oxlintrc.json", '{"extends":["./configs/base.json"]}'], snapshot = false }, + { argv = ["vpt", "write-file", "configs/base.json", '{"options":{"typeAware":false,"typeCheck":false},"rules":{"no-debugger":"error"},"overrides":[{"files":["src/**"],"excludeFiles":["src/skip.js"],"rules":{"no-debugger":"off"}}]}'], snapshot = false }, + { argv = ["vpt", "write-file", "src/allowed.js", "debugger;\n"], snapshot = false }, + { argv = ["vpt", "write-file", "src/skip.js", "debugger;\n"], snapshot = false }, + { argv = ["vpt", "write-file", "configs/src/allowed.js", "debugger;\n"], snapshot = false }, + { argv = ["vpt", "write-file", "configs/src/skip.js", "debugger;\n"], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "src/allowed.js"], comment = "Original inherited override matches the consumer-relative path", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "src/skip.js"], comment = "Original excludeFiles prevents the override", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "configs/src/allowed.js"], comment = "The declaring directory does not rebase the inherited glob", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "configs/src/skip.js"], continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vp", "lint", "--threads=1", "src/allowed.js"], comment = "The imported override matches the same path", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "src/skip.js"], comment = "The imported exclusion has the same effect", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "configs/src/allowed.js"], comment = "The declaring-directory path retains its original diagnostic", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "configs/src/skip.js"], continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_directory_alias" +vp = "local" +skip-platforms = ["windows"] +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "write-file", ".oxlintrc.json", '{"extends":["./aliases/base.json"]}'], snapshot = false }, + { argv = ["vpt", "write-file", "configs/nested/base.json", '{"extends":["../common.json"],"options":{"typeAware":false,"typeCheck":false}}'], snapshot = false }, + { argv = ["vpt", "write-file", "configs/common.json", '{"rules":{"no-debugger":"error"}}'], snapshot = false }, + { argv = ["vpt", "write-file", "common.json", '{"rules":{"no-debugger":"off"}}'], snapshot = false }, + { argv = ["vpt", "write-file", "index.js", "debugger;\n"], snapshot = false }, + { argv = ["node", "create-directory-link.mjs"], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "Oxlint follows the directory link before resolving the parent component", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "configs/nested/base.ts"] }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The migrated graph selects the same physical parent config", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_directory_junction" +vp = "local" +skip-platforms = ["linux", "macos"] +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "write-file", ".oxlintrc.json", '{"extends":["./aliases/base.json"]}'], snapshot = false }, + { argv = ["vpt", "write-file", "configs/nested/base.json", '{"extends":["../common.json"],"options":{"typeAware":false,"typeCheck":false}}'], snapshot = false }, + { argv = ["vpt", "write-file", "configs/common.json", '{"rules":{"no-debugger":"error"}}'], snapshot = false }, + { argv = ["vpt", "write-file", "common.json", '{"rules":{"no-debugger":"off"}}'], snapshot = false }, + { argv = ["vpt", "write-file", "index.js", "debugger;\n"], snapshot = false }, + { argv = ["node", "create-directory-link.mjs"], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "Windows resolves the parent component before following the directory junction", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "configs/nested/base.ts"] }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The migrated graph selects the same lexical parent config", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_check_js" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "write-file", "vite.config.js", "export default {};\n"], snapshot = false }, + { argv = ["vpt", "json-edit", "tsconfig.json", "compilerOptions.allowJs", "true"], snapshot = false }, + { argv = ["vpt", "json-edit", "tsconfig.json", "compilerOptions.checkJs", "true"], snapshot = false }, + { argv = ["vpt", "json-edit", "tsconfig.json", "include", '["vite.config.js"]'], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "Original inherited diagnostics", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "vite.config.js"] }, + { argv = ["vp", "exec", "tsc", "--noEmit"], comment = "JavaScript config imports type-check without allowImportingTsExtensions" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The generated imports preserve the diagnostics and exit code", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_check_mjs" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "write-file", "vite.config.mjs", "export default {};\n"], snapshot = false }, + { argv = ["vpt", "json-edit", "tsconfig.json", "compilerOptions.allowJs", "true"], snapshot = false }, + { argv = ["vpt", "json-edit", "tsconfig.json", "compilerOptions.checkJs", "true"], snapshot = false }, + { argv = ["vpt", "json-edit", "tsconfig.json", "include", '["vite.config.mjs"]'], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "Original inherited diagnostics", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "vite.config.mjs"] }, + { argv = ["vp", "exec", "tsc", "--noEmit"], comment = "JavaScript config imports type-check without allowImportingTsExtensions" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The generated imports preserve the diagnostics and exit code", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_isolated_retry" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "write-file", ".oxlintrc.json", '{"extends":["./missing.json"]}'], snapshot = false }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "vite.config.ts"], comment = "The failed migration records a pending marker without a lint block" }, + { argv = ["vpt", "write-file", "missing.json", '{"rules":{"no-debugger":"error"},"options":{"typeAware":false,"typeCheck":false}}'], comment = "Repair the only unreadable config" }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "The repaired JSON loads before retrying", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "stat-file", ".oxlintrc.json", "--assert", "missing"] }, + { argv = ["vpt", "print-file", "vite.config.ts"], comment = "The successful retry replaces the pending marker with the lint config" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The migrated config retains the same diagnostic and exit code", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_lint_conflict_retry" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "write-file", "vite.config.mjs", "export default { lint: {} };\n"], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "The original JSON inheritance is selected explicitly", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "vite.config.mjs"], comment = "The lint conflict records a pending marker" }, + { argv = ["vpt", "replace-file-content", "vite.config.mjs", "lint: {},", ""], comment = "Remove the conflicting lint block" }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "stat-file", ".oxlintrc.json", "--assert", "missing"] }, + { argv = ["vpt", "print-file", "vite.config.mjs"] }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The retried migration retains the original diagnostics and exit code", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_ts_check_pragma" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "write-file", "vite.config.mjs", "// @ts-check\n/** @type {string} */\nexport const value = 1;\nexport default {};\n"], snapshot = false }, + { argv = ["vpt", "json-edit", "tsconfig.json", "compilerOptions.allowJs", "true"], snapshot = false }, + { argv = ["vpt", "json-edit", "tsconfig.json", "include", '["vite.config.mjs"]'], snapshot = false }, + { argv = ["vp", "install"], snapshot = false }, + { argv = ["vp", "exec", "tsc", "--noEmit"], comment = "The file-level pragma enables the original type diagnostic", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "vite.config.mjs"], comment = "Generated imports follow the file-level pragma and precede the statement's type annotation" }, + { argv = ["vp", "exec", "tsc", "--noEmit"], comment = "The pragma still enables the same type diagnostic after migration", continue-on-failure = true }, + { argv = ["vpt", "replace-file-content", "vite.config.mjs", "@ts-check", "@ts-nocheck"] }, + { argv = ["vpt", "json-edit", "tsconfig.json", "compilerOptions.checkJs", "true"] }, + { argv = ["vp", "exec", "tsc", "--noEmit"], comment = "A leading ts-nocheck pragma can still disable project-level JavaScript checking" }, +] + +[[case]] +name = "migration_oxlint_extends_missing_plugin" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "Original inherited diagnostics before a missing generated plugin is added", continue-on-failure = true }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "jsPlugins", '["eslint-plugin-vp-missing"]'], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "rules.vp-missing/no-foo", '"error"'], snapshot = false }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"], comment = "Sanitize missing generated package references without discarding inheritance" }, + { argv = ["vpt", "print-file", "vite.config.ts"], comment = "The missing plugin and its rule are removed while inherited plugin rules remain" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "Lint loads successfully and reports the inherited diagnostics", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "example.test.js"], comment = "Inherited override plugins still run", continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends.md new file mode 100644 index 0000000000..2156fb2f54 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends.md @@ -0,0 +1,427 @@ +# migration_oxlint_extends + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +Original inherited errors and warnings + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 -c .oxlintrc.json example.test.js` + +Original override rules and plugin + +**Exit code:** 1 + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[example.test.js:1:1] + 1 │ debugger; + · ───────── + 2 │ export const bar = 1; + ╰──── + help: Remove the debugger statement + + × override(no-bar): Do not use bar. + ╭─[example.test.js:2:14] + 1 │ debugger; + 2 │ export const bar = 1; + · ─── + ╰──── + +Found 1 warning and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt print-file vite.config.ts` + +Path extends become imports of shared TypeScript configs + +``` +import oxlintConfig1 from "./shared/nested/first.js"; +import oxlintConfig2 from "./shared/second.js"; +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + fmt: {}, + lint: { + extends: [oxlintConfig1, oxlintConfig2], + rules: { + "no-alert": "off", + "local/no-foo": "error", + "vite-plus/prefer-vite-plus-imports": "error", + }, + ignorePatterns: ["shared/**", "vite.config.ts"], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + }, +}); +``` + +## `vpt print-file shared/nested/first.ts` + +Nested imports preserve overrides and their plugin declarations + +``` +// Generated by vp migrate from first.json. +import { fileURLToPath } from "node:url"; +import type { OxlintConfig } from "vite-plus/lint"; +import oxlintConfig1 from "../base.js"; + +export default { + extends: [oxlintConfig1], + overrides: [ + { + files: ["**/*.test.js"], + rules: { + "no-debugger": "warn", + "override/no-bar": "error", + }, + jsPlugins: [ + { + name: "override", + specifier: fileURLToPath(new URL("../override-plugin.mjs", import.meta.url)), + }, + ], + }, + ], +} satisfies OxlintConfig; +``` + +## `vpt print-file shared/base.ts` + +``` +// Generated by vp migrate from base.json. +import type { OxlintConfig } from "vite-plus/lint"; + +export default { + options: { + typeAware: false, + typeCheck: false, + }, + rules: { + "no-debugger": "error", + "no-console": "error", + "no-alert": "error", + }, +} satisfies OxlintConfig; +``` + +## `vpt print-file shared/second.ts` + +Relative plugin paths resolve from the shared module + +``` +// Generated by vp migrate from second.jsonc. +import { fileURLToPath } from "node:url"; +import type { OxlintConfig } from "vite-plus/lint"; + +export default { + rules: { + "no-console": "warn", + }, + jsPlugins: [fileURLToPath(new URL("./plugin.mjs", import.meta.url))], +} satisfies OxlintConfig; +``` + +## `vpt stat-file .oxlintrc.json --assert-not file` + +``` +.oxlintrc.json: missing +``` + +## `vpt print-file shared/second.jsonc` + +Shared JSON configs are retained + +``` +{ + // Later entries take priority. + "rules": { "no-console": "warn" }, + "jsPlugins": ["./plugin.mjs"], +} +``` + +## `vp exec tsc --noEmit` + +Generated config types accept the resolved inheritance + +``` +``` + +## `vp lint --threads=1 index.js` + +The diagnostics and exit code match the original config without a config flag + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 example.test.js` + +The override diagnostics and exit code match the original config + +**Exit code:** 1 + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[example.test.js:1:1] + 1 │ debugger; + · ───────── + 2 │ export const bar = 1; + ╰──── + help: Remove the debugger statement + + × override(no-bar): Do not use bar. + ╭─[example.test.js:2:14] + 1 │ debugger; + 2 │ export const bar = 1; + · ─── + ╰──── + +Found 1 warning and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +This project is already using Vite+! Happy coding! +``` + +## `vpt print-file vite.config.ts` + +A second migration leaves the config unchanged + +``` +import oxlintConfig1 from "./shared/nested/first.js"; +import oxlintConfig2 from "./shared/second.js"; +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + fmt: {}, + lint: { + extends: [oxlintConfig1, oxlintConfig2], + rules: { + "no-alert": "off", + "local/no-foo": "error", + "vite-plus/prefer-vite-plus-imports": "error", + }, + ignorePatterns: ["shared/**", "vite.config.ts"], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + }, +}); +``` + +## `vp lint --threads=1 index.js` + +The diagnostics and exit code remain unchanged after the second migration + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 example.test.js` + +The override diagnostics and exit code also remain unchanged + +**Exit code:** 1 + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[example.test.js:1:1] + 1 │ debugger; + · ───────── + 2 │ export const bar = 1; + ╰──── + help: Remove the debugger statement + + × override(no-bar): Do not use bar. + ╭─[example.test.js:2:14] + 1 │ debugger; + 2 │ export const bar = 1; + · ─── + ╰──── + +Found 1 warning and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vpt replace-file-content shared/base.ts '"no-debugger": "error"' '"no-debugger": "warn"'` + +Edits to the shared TS config propagate through its imports + +``` +``` + +## `vp lint --threads=1 index.js` + +The edited base rule now warns in index.js + +**Exit code:** 1 + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 2 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 example.test.js` + +The test override remains unchanged after the shared config edit + +**Exit code:** 1 + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[example.test.js:1:1] + 1 │ debugger; + · ───────── + 2 │ export const bar = 1; + ╰──── + help: Remove the debugger statement + + × override(no-bar): Do not use bar. + ╭─[example.test.js:2:14] + 1 │ debugger; + 2 │ export const bar = 1; + · ─── + ╰──── + +Found 1 warning and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_backslash_path.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_backslash_path.md new file mode 100644 index 0000000000..4caf70435f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_backslash_path.md @@ -0,0 +1,116 @@ +# migration_oxlint_extends_backslash_path + +## `vpt cp shared/base.json shared/base\name.json` + + +## `vpt json-edit shared/nested/first.json extends '["../base\\name.json"]'` + + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +Original diagnostics with a literal backslash in the shared config filename + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +Warn and preserve the JSON because TypeScript treats the backslash as a separator + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 1 config update applied +! Warnings: + - Could not migrate .oxlintrc.json: Error: Cannot import a shared Oxlint config whose relative path contains "\\": /shared/base\name.ts. The original config was preserved; use vp lint -c .oxlintrc.json until it is migrated manually. +``` + +## `vpt stat-file .oxlintrc.json --assert file` + +``` +.oxlintrc.json: file +``` + +## `vpt stat-file shared/nested/first.ts --assert missing` + +``` +shared/nested/first.ts: missing +``` + +## `vpt stat-file shared/base\name.ts --assert missing` + +``` +shared/base\name.ts: missing +``` + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +The retained JSON still produces the same diagnostics and exit code + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_check_js.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_check_js.md new file mode 100644 index 0000000000..56645a7e54 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_check_js.md @@ -0,0 +1,129 @@ +# migration_oxlint_extends_check_js + +## `vpt write-file vite.config.js 'export default {}; +'` + + +## `vpt json-edit tsconfig.json compilerOptions.allowJs true` + + +## `vpt json-edit tsconfig.json compilerOptions.checkJs true` + + +## `vpt json-edit tsconfig.json include '["vite.config.js"]'` + + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +Original inherited diagnostics + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 1 config update applied +``` + +## `vpt print-file vite.config.js` + +``` +import oxlintConfig1 from "./shared/nested/first.js"; +import oxlintConfig2 from "./shared/second.js"; +export default { + fmt: {}, + lint: { + extends: [oxlintConfig1, oxlintConfig2], + rules: { + "no-alert": "off", + "local/no-foo": "error", + "vite-plus/prefer-vite-plus-imports": "error", + }, + ignorePatterns: ["shared/**", "vite.config.ts"], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + }, +}; +``` + +## `vp exec tsc --noEmit` + +JavaScript config imports type-check without allowImportingTsExtensions + +``` +``` + +## `vp lint --threads=1 index.js` + +The generated imports preserve the diagnostics and exit code + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_check_mjs.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_check_mjs.md new file mode 100644 index 0000000000..847205f635 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_check_mjs.md @@ -0,0 +1,129 @@ +# migration_oxlint_extends_check_mjs + +## `vpt write-file vite.config.mjs 'export default {}; +'` + + +## `vpt json-edit tsconfig.json compilerOptions.allowJs true` + + +## `vpt json-edit tsconfig.json compilerOptions.checkJs true` + + +## `vpt json-edit tsconfig.json include '["vite.config.mjs"]'` + + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +Original inherited diagnostics + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 1 config update applied +``` + +## `vpt print-file vite.config.mjs` + +``` +import oxlintConfig1 from "./shared/nested/first.js"; +import oxlintConfig2 from "./shared/second.js"; +export default { + fmt: {}, + lint: { + extends: [oxlintConfig1, oxlintConfig2], + rules: { + "no-alert": "off", + "local/no-foo": "error", + "vite-plus/prefer-vite-plus-imports": "error", + }, + ignorePatterns: ["shared/**", "vite.config.ts"], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + }, +}; +``` + +## `vp exec tsc --noEmit` + +JavaScript config imports type-check without allowImportingTsExtensions + +``` +``` + +## `vp lint --threads=1 index.js` + +The generated imports preserve the diagnostics and exit code + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_directory_alias.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_directory_alias.md new file mode 100644 index 0000000000..3a37443704 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_directory_alias.md @@ -0,0 +1,87 @@ +# migration_oxlint_extends_directory_alias + +## `vpt write-file .oxlintrc.json '{"extends":["./aliases/base.json"]}'` + + +## `vpt write-file configs/nested/base.json '{"extends":["../common.json"],"options":{"typeAware":false,"typeCheck":false}}'` + + +## `vpt write-file configs/common.json '{"rules":{"no-debugger":"error"}}'` + + +## `vpt write-file common.json '{"rules":{"no-debugger":"off"}}'` + + +## `vpt write-file index.js 'debugger; +'` + + +## `node create-directory-link.mjs` + + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +Oxlint follows the directory link before resolving the parent component + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt print-file configs/nested/base.ts` + +``` +// Generated by vp migrate from base.json. +import type { OxlintConfig } from "vite-plus/lint"; +import oxlintConfig1 from "../common.js"; + +export default { + extends: [oxlintConfig1], + options: { + typeAware: false, + typeCheck: false, + }, +} satisfies OxlintConfig; +``` + +## `vp lint --threads=1 index.js` + +The migrated graph selects the same physical parent config + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_directory_junction.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_directory_junction.md new file mode 100644 index 0000000000..baa3d3ba14 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_directory_junction.md @@ -0,0 +1,67 @@ +# migration_oxlint_extends_directory_junction + +## `vpt write-file .oxlintrc.json '{"extends":["./aliases/base.json"]}'` + + +## `vpt write-file configs/nested/base.json '{"extends":["../common.json"],"options":{"typeAware":false,"typeCheck":false}}'` + + +## `vpt write-file configs/common.json '{"rules":{"no-debugger":"error"}}'` + + +## `vpt write-file common.json '{"rules":{"no-debugger":"off"}}'` + + +## `vpt write-file index.js 'debugger; +'` + + +## `node create-directory-link.mjs` + + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +Windows resolves the parent component before following the directory junction + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt print-file configs/nested/base.ts` + +``` +// Generated by vp migrate from ..%2F..%2Faliases%2Fbase.json. +import type { OxlintConfig } from "vite-plus/lint"; +import oxlintConfig1 from "../../common.js"; + +export default { + extends: [oxlintConfig1], + options: { + typeAware: false, + typeCheck: false, + }, +} satisfies OxlintConfig; +``` + +## `vp lint --threads=1 index.js` + +The migrated graph selects the same lexical parent config + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_import_paths.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_import_paths.md new file mode 100644 index 0000000000..96bcddd749 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_import_paths.md @@ -0,0 +1,158 @@ +# migration_oxlint_extends_import_paths + +## `vpt cp shared/base.json shared/base#%.json` + + +## `vpt cp shared/nested/first.json shared/nested/first#%.json` + + +## `vpt json-edit shared/nested/first#%.json extends '["../base#%.json"]'` + + +## `vpt json-edit .oxlintrc.json extends '["./shared/nested/first#%.json","./shared/second.jsonc"]'` + + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +Original diagnostics with special characters in shared config paths + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt print-file vite.config.ts` + +``` +import oxlintConfig1 from "./shared/nested/first#%.js"; +import oxlintConfig2 from "./shared/second.js"; +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + fmt: {}, + lint: { + extends: [oxlintConfig1, oxlintConfig2], + ignorePatterns: ["shared/**", "vite.config.ts"], + rules: { + "local/no-foo": "error", + "no-alert": "off", + "vite-plus/prefer-vite-plus-imports": "error", + }, + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + }, +}); +``` + +## `vpt print-file shared/nested/first#%.ts` + +``` +// Generated by vp migrate from first%23%25.json. +import { fileURLToPath } from "node:url"; +import type { OxlintConfig } from "vite-plus/lint"; +import oxlintConfig1 from "../base#%.js"; + +export default { + extends: [oxlintConfig1], + overrides: [ + { + files: ["**/*.test.js"], + jsPlugins: [ + { + name: "override", + specifier: fileURLToPath(new URL("../override-plugin.mjs", import.meta.url)), + }, + ], + rules: { + "no-debugger": "warn", + "override/no-bar": "error", + }, + }, + ], +} satisfies OxlintConfig; +``` + +## `vp exec tsc --noEmit` + +TypeScript resolves the literal import paths + +``` +``` + +## `vp lint --threads=1 index.js` + +The migrated imports retain the same diagnostics and exit code + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_isolated_retry.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_isolated_retry.md new file mode 100644 index 0000000000..f87910b6b2 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_isolated_retry.md @@ -0,0 +1,122 @@ +# migration_oxlint_extends_isolated_retry + +## `vpt write-file .oxlintrc.json '{"extends":["./missing.json"]}'` + + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 1 config update applied +! Warnings: + - Could not migrate .oxlintrc.json: Error: ENOENT: no such file or directory, lstat '/missing.json'. The original config was preserved; use vp lint -c .oxlintrc.json until it is migrated manually. +``` + +## `vpt print-file vite.config.ts` + +The failed migration records a pending marker without a lint block + +``` +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + fmt: {}, +}); +// vp migrate: pending .oxlintrc.json. +``` + +## `vpt write-file missing.json '{"rules":{"no-debugger":"error"},"options":{"typeAware":false,"typeCheck":false}}'` + +Repair the only unreadable config + +``` +``` + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +The repaired JSON loads before retrying + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +● Formatting code... +● Code formatted +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite → +• 1 config update applied +``` + +## `vpt stat-file .oxlintrc.json --assert missing` + +``` +.oxlintrc.json: missing +``` + +## `vpt print-file vite.config.ts` + +The successful retry replaces the pending marker with the lint config + +``` +// vp migrate: merged .oxlintrc.json (ddb1d7ee7999d49e4e56c1776d652081). +import oxlintConfig1 from "./missing.js"; +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + lint: { + extends: [oxlintConfig1], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, + fmt: {}, +}); +``` + +## `vp lint --threads=1 index.js` + +The migrated config retains the same diagnostic and exit code + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_lint_conflict_retry.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_lint_conflict_retry.md new file mode 100644 index 0000000000..766e9d3219 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_lint_conflict_retry.md @@ -0,0 +1,155 @@ +# migration_oxlint_extends_lint_conflict_retry + +## `vpt write-file vite.config.mjs 'export default { lint: {} }; +'` + + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +The original JSON inheritance is selected explicitly + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +! Warnings: + - Preserved .oxlintrc.json because vite.config.mjs already has a lint config. Merge its inherited rules manually. +``` + +## `vpt print-file vite.config.mjs` + +The lint conflict records a pending marker + +``` +export default { + fmt: {}, + lint: {}, +}; +// vp migrate: pending .oxlintrc.json. +``` + +## `vpt replace-file-content vite.config.mjs 'lint: {},' ` + +Remove the conflicting lint block + +``` +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +● Formatting code... +● Code formatted +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite → +• 1 config update applied +``` + +## `vpt stat-file .oxlintrc.json --assert missing` + +``` +.oxlintrc.json: missing +``` + +## `vpt print-file vite.config.mjs` + +``` +// vp migrate: merged .oxlintrc.json (6e95c926c620f96efc4952a2de4580b0). +import oxlintConfig1 from "./shared/nested/first.js"; +import oxlintConfig2 from "./shared/second.js"; +export default { + lint: { + extends: [oxlintConfig1, oxlintConfig2], + rules: { + "no-alert": "off", + "local/no-foo": "error", + "vite-plus/prefer-vite-plus-imports": "error", + }, + ignorePatterns: ["shared/**", "vite.config.ts"], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + }, + fmt: {}, +}; +``` + +## `vp lint --threads=1 index.js` + +The retried migration retains the original diagnostics and exit code + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_missing_plugin.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_missing_plugin.md new file mode 100644 index 0000000000..313371e36f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_missing_plugin.md @@ -0,0 +1,150 @@ +# migration_oxlint_extends_missing_plugin + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +Original inherited diagnostics before a missing generated plugin is added + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` + +## `vpt json-edit .oxlintrc.json jsPlugins '["eslint-plugin-vp-missing"]'` + + +## `vpt json-edit .oxlintrc.json rules.vp-missing/no-foo '"error"'` + + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +Sanitize missing generated package references without discarding inheritance + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +! Warnings: + - Stripped JS plugin reference(s) from the generated lint config: eslint-plugin-vp-missing. No matching package is present in this workspace, so loading them at lint time would fail. If you want their Oxlint coverage back, install each package (e.g. `vp install `) and add its name back to `lint.jsPlugins` in vite.config.ts. +``` + +## `vpt print-file vite.config.ts` + +The missing plugin and its rule are removed while inherited plugin rules remain + +``` +import oxlintConfig1 from "./shared/nested/first.js"; +import oxlintConfig2 from "./shared/second.js"; +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + fmt: {}, + lint: { + extends: [oxlintConfig1, oxlintConfig2], + ignorePatterns: ["shared/**", "vite.config.ts"], + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "local/no-foo": "error", + "no-alert": "off", + "vite-plus/prefer-vite-plus-imports": "error", + }, + options: {}, + }, +}); +``` + +## `vp lint --threads=1 index.js` + +Lint loads successfully and reports the inherited diagnostics + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 example.test.js` + +Inherited override plugins still run + +**Exit code:** 1 + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[example.test.js:1:1] + 1 │ debugger; + · ───────── + 2 │ export const bar = 1; + ╰──── + help: Remove the debugger statement + + × override(no-bar): Do not use bar. + ╭─[example.test.js:2:14] + 1 │ debugger; + 2 │ export const bar = 1; + · ─── + ╰──── + +Found 1 warning and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_override_paths.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_override_paths.md new file mode 100644 index 0000000000..3d251c5c9f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_override_paths.md @@ -0,0 +1,164 @@ +# migration_oxlint_extends_override_paths + +## `vpt write-file .oxlintrc.json '{"extends":["./configs/base.json"]}'` + + +## `vpt write-file configs/base.json '{"options":{"typeAware":false,"typeCheck":false},"rules":{"no-debugger":"error"},"overrides":[{"files":["src/**"],"excludeFiles":["src/skip.js"],"rules":{"no-debugger":"off"}}]}'` + + +## `vpt write-file src/allowed.js 'debugger; +'` + + +## `vpt write-file src/skip.js 'debugger; +'` + + +## `vpt write-file configs/src/allowed.js 'debugger; +'` + + +## `vpt write-file configs/src/skip.js 'debugger; +'` + + +## `vp lint --threads=1 -c .oxlintrc.json src/allowed.js` + +Original inherited override matches the consumer-relative path + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 -c .oxlintrc.json src/skip.js` + +Original excludeFiles prevents the override + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[src/skip.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 -c .oxlintrc.json configs/src/allowed.js` + +The declaring directory does not rebase the inherited glob + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[configs/src/allowed.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 -c .oxlintrc.json configs/src/skip.js` + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[configs/src/skip.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vp lint --threads=1 src/allowed.js` + +The imported override matches the same path + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 src/skip.js` + +The imported exclusion has the same effect + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[src/skip.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 configs/src/allowed.js` + +The declaring-directory path retains its original diagnostic + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[configs/src/allowed.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 configs/src/skip.js` + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[configs/src/skip.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_query_path.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_query_path.md new file mode 100644 index 0000000000..84909a7ead --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_query_path.md @@ -0,0 +1,116 @@ +# migration_oxlint_extends_query_path + +## `vpt cp shared/base.json shared/base?.json` + + +## `vpt json-edit shared/nested/first.json extends '["../base?.json"]'` + + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +The original JSON resolves the question mark as part of the filename + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +Warn and preserve the JSON instead of emitting an unresolvable import + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 1 config update applied +! Warnings: + - Could not migrate .oxlintrc.json: Error: Cannot import a shared Oxlint config whose relative path contains "?": /shared/base?.ts. The original config was preserved; use vp lint -c .oxlintrc.json until it is migrated manually. +``` + +## `vpt stat-file .oxlintrc.json --assert file` + +``` +.oxlintrc.json: file +``` + +## `vpt stat-file shared/nested/first.ts --assert missing` + +``` +shared/nested/first.ts: missing +``` + +## `vpt stat-file shared/base?.ts --assert missing` + +``` +shared/base?.ts: missing +``` + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +The retained JSON still produces the same diagnostics and exit code + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + 2 │ console.log("test"); + ╰──── + help: Remove the debugger statement + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:2:1] + 1 │ debugger; + 2 │ console.log("test"); + · ─────────── + 3 │ alert("allowed"); + ╰──── + help: Delete this console statement. + + × local(no-foo): Do not use foo. + ╭─[index.js:4:14] + 3 │ alert("allowed"); + 4 │ export const foo = 1; + · ─── + 5 │ export const bar = 1; + ╰──── + +Found 1 warning and 2 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_ts_check_pragma.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_ts_check_pragma.md new file mode 100644 index 0000000000..69886e9f1f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_ts_check_pragma.md @@ -0,0 +1,105 @@ +# migration_oxlint_extends_ts_check_pragma + +## `vpt write-file vite.config.mjs '// @ts-check +/** @type {string} */ +export const value = 1; +export default {}; +'` + + +## `vpt json-edit tsconfig.json compilerOptions.allowJs true` + + +## `vpt json-edit tsconfig.json include '["vite.config.mjs"]'` + + +## `vp install` + + +## `vp exec tsc --noEmit` + +The file-level pragma enables the original type diagnostic + +**Exit code:** 2 + +``` +vite.config.mjs:3:14 - error TS2322: Type 'number' is not assignable to type 'string'. + +3 export const value = 1; + ~~~~~ + +Found 1 error in vite.config.mjs:3 +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 1 config update applied +``` + +## `vpt print-file vite.config.mjs` + +Generated imports follow the file-level pragma and precede the statement's type annotation + +``` +// @ts-check +import oxlintConfig1 from "./shared/nested/first.js"; +import oxlintConfig2 from "./shared/second.js"; + +/** @type {string} */ +export const value = 1; +export default { + fmt: {}, + lint: { + extends: [oxlintConfig1, oxlintConfig2], + rules: { + "no-alert": "off", + "local/no-foo": "error", + "vite-plus/prefer-vite-plus-imports": "error", + }, + ignorePatterns: ["shared/**", "vite.config.ts"], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + }, +}; +``` + +## `vp exec tsc --noEmit` + +The pragma still enables the same type diagnostic after migration + +**Exit code:** 2 + +``` +vite.config.mjs:6:14 - error TS2322: Type 'number' is not assignable to type 'string'. + +6 export const value = 1; + ~~~~~ + +Found 1 error in vite.config.mjs:6 +``` + +## `vpt replace-file-content vite.config.mjs @ts-check @ts-nocheck` + +``` +``` + +## `vpt json-edit tsconfig.json compilerOptions.checkJs true` + +``` +``` + +## `vp exec tsc --noEmit` + +A leading ts-nocheck pragma can still disable project-level JavaScript checking + +``` +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_type_options.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_type_options.md new file mode 100644 index 0000000000..dd4f60cfa4 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/snapshots/migration_oxlint_extends_type_options.md @@ -0,0 +1,161 @@ +# migration_oxlint_extends_type_options + +## `vpt json-edit shared/base.json options '{"typeAware":true,"typeCheck":true}'` + + +## `vpt json-edit .oxlintrc.json rules.typescript/no-floating-promises '"error"'` + + +## `vpt write-file tsconfig.json '{"compilerOptions":{"target":"ESNext","strict":true},"include":["*.ts"]}'` + + +## `vpt write-file type-error.ts 'export const value: string = 1; +'` + + +## `vpt write-file type-aware.ts 'Promise.resolve(1); +'` + + +## `vp lint --threads=1 -c .oxlintrc.json type-error.ts` + +Inherited typeCheck reports the original TypeScript diagnostic + +**Exit code:** 1 + +``` + + × typescript(TS2322): Type 'number' is not assignable to type 'string'. + ╭─[type-error.ts:1:14] + 1 │ export const value: string = 1; + · ───── + ╰──── + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 -c .oxlintrc.json type-aware.ts` + +Inherited typeAware enables no-floating-promises + +**Exit code:** 1 + +``` + + × typescript(no-floating-promises): Promises must be awaited, add void operator to ignore. + ╭─[type-aware.ts:1:1] + 1 │ Promise.resolve(1); + · ─────────┬───────── + · ╰── This unhandled promise-like value has type `Promise`. + ╰──── + help: The promise must end with a call to .catch, or end with a call to .then with a rejection handler, or be explicitly marked as ignored with the `void` operator. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt print-file vite.config.ts` + +Local options stay empty so inherited settings remain live + +``` +import oxlintConfig1 from "./shared/nested/first.js"; +import oxlintConfig2 from "./shared/second.js"; +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + fmt: {}, + lint: { + extends: [oxlintConfig1, oxlintConfig2], + ignorePatterns: ["shared/**", "vite.config.ts"], + rules: { + "local/no-foo": "error", + "no-alert": "off", + "typescript/no-floating-promises": "error", + "vite-plus/prefer-vite-plus-imports": "error", + }, + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + }, +}); +``` + +## `vp lint --threads=1 type-error.ts` + +Imported typeCheck produces the same diagnostic and exit code + +**Exit code:** 1 + +``` + + × typescript(TS2322): Type 'number' is not assignable to type 'string'. + ╭─[type-error.ts:1:14] + 1 │ export const value: string = 1; + · ───── + ╰──── + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 type-aware.ts` + +Imported typeAware produces the same diagnostic and exit code + +**Exit code:** 1 + +``` + + × typescript(no-floating-promises): Promises must be awaited, add void operator to ignore. + ╭─[type-aware.ts:1:1] + 1 │ Promise.resolve(1); + · ─────────┬───────── + · ╰── This unhandled promise-like value has type `Promise`. + ╰──── + help: The promise must end with a call to .catch, or end with a call to .then with a rejection handler, or be explicitly marked as ignored with the `void` operator. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vpt replace-file-content shared/base.ts 'typeAware: true' 'typeAware: false'` + +``` +``` + +## `vpt replace-file-content shared/base.ts 'typeCheck: true' 'typeCheck: false'` + +``` +``` + +## `vp lint --threads=1 type-error.ts` + +Disabling the shared options removes the TypeScript diagnostic + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 type-aware.ts` + +Disabling the shared options removes the type-aware diagnostic + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/tsconfig.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/tsconfig.json new file mode 100644 index 0000000000..fa17f555fe --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "noEmit": true, + "skipLibCheck": true, + "types": [ + "node" + ] + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/.gitignore b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/.oxlintrc.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/.oxlintrc.json new file mode 100644 index 0000000000..e36e1ae721 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/.oxlintrc.json @@ -0,0 +1,9 @@ +{ + "options": { + "typeAware": false, + "typeCheck": false + }, + "rules": { + "no-debugger": "warn" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/create-config-links.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/create-config-links.mjs new file mode 100644 index 0000000000..d8ad36af5c --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/create-config-links.mjs @@ -0,0 +1,13 @@ +import { symlinkSync } from 'node:fs'; + +if (process.argv.includes('--owner-alias')) { + symlinkSync('../../.oxlintrc.json', 'packages/a/alias.json', 'file'); + process.exit(0); +} + +for (const name of ['a', 'b']) { + symlinkSync('../../shared/lint.json', `packages/${name}/.oxlintrc.json`, 'file'); + if (process.argv.includes('--with-base')) { + symlinkSync('../../shared/base.json', `packages/${name}/base.json`, 'file'); + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/index.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/index.js new file mode 100644 index 0000000000..eab7469213 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/index.js @@ -0,0 +1 @@ +debugger; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/package.json new file mode 100644 index 0000000000..c15b74847e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/package.json @@ -0,0 +1,8 @@ +{ + "name": "migration-oxlint-extends-workspace", + "private": true, + "type": "module", + "devDependencies": { + "oxlint": "1.88.0" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/a/.oxlintrc.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/a/.oxlintrc.json new file mode 100644 index 0000000000..804b9227be --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/a/.oxlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../../.oxlintrc.json"] +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/a/index.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/a/index.js new file mode 100644 index 0000000000..eab7469213 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/a/index.js @@ -0,0 +1 @@ +debugger; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/a/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/a/package.json new file mode 100644 index 0000000000..ee8ace35a5 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/a/package.json @@ -0,0 +1,5 @@ +{ + "name": "a", + "private": true, + "type": "module" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/a/vite.config.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/a/vite.config.mjs new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/a/vite.config.mjs @@ -0,0 +1 @@ +export default {}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/.oxlintrc.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/.oxlintrc.json new file mode 100644 index 0000000000..1104dda707 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/.oxlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../a/.oxlintrc.json", "./extra.json"] +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/extra.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/extra.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/extra.json @@ -0,0 +1 @@ +{} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/extra.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/extra.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/extra.ts @@ -0,0 +1 @@ +export default {}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/index.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/index.js new file mode 100644 index 0000000000..eab7469213 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/index.js @@ -0,0 +1 @@ +debugger; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/package.json new file mode 100644 index 0000000000..f1741f9879 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/package.json @@ -0,0 +1,5 @@ +{ + "name": "b", + "private": true, + "type": "module" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/vite.config.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/vite.config.mjs new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/packages/b/vite.config.mjs @@ -0,0 +1 @@ +export default {}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/pnpm-workspace.yaml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/pnpm-workspace.yaml new file mode 100644 index 0000000000..924b55f42e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/* diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/settings-plugin.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/settings-plugin.mjs new file mode 100644 index 0000000000..eb8770c458 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/settings-plugin.mjs @@ -0,0 +1,17 @@ +export default { + meta: { name: 'fixture' }, + rules: { + 'require-setting': { + meta: { schema: [] }, + create(context) { + return { + Program(node) { + if (context.settings.fixture?.enabled !== true) { + context.report({ node, message: 'The fixture setting must be enabled.' }); + } + }, + }; + }, + }, + }, +}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots.toml new file mode 100644 index 0000000000..f814be252f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots.toml @@ -0,0 +1,333 @@ +[[case]] +name = "migration_oxlint_extends_workspace_shared_owner" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "rm", "packages/b/extra.ts"], snapshot = false }, + { argv = ["vp", "lint", "-c", ".oxlintrc.json", "index.js"], comment = "The original owner uses the shared warning" }, + { argv = ["vp", "lint", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/a", comment = "The direct consumer inherits the same warning" }, + { argv = ["vp", "lint", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/b", comment = "The transitive consumer inherits the same warning" }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "vite.config.mjs"], comment = "The owner imports its own shared config" }, + { argv = ["vpt", "print-file", "packages/a/vite.config.mjs"], comment = "The intermediate owner also imports its own shared config" }, + { argv = ["vpt", "print-file", "packages/b/vite.config.mjs"] }, + { argv = ["vpt", "print-file", ".oxlintrc.ts"], comment = "Source rules remain in one shared module" }, + { argv = ["vpt", "print-file", "packages/a/.oxlintrc.ts"] }, + { argv = ["vpt", "stat-file", ".oxlintrc.json", "--assert-not", "file"] }, + { argv = ["vpt", "stat-file", "packages/a/.oxlintrc.json", "--assert-not", "file"] }, + { argv = ["vpt", "stat-file", "packages/b/.oxlintrc.json", "--assert-not", "file"] }, + { argv = ["vp", "lint", "index.js"], comment = "The migrated owner reports the same warning" }, + { argv = ["vp", "lint", "index.js"], cwd = "packages/a", comment = "The migrated direct consumer reports the same warning" }, + { argv = ["vp", "lint", "index.js"], cwd = "packages/b", comment = "The migrated transitive consumer reports the same warning" }, + { argv = ["vpt", "replace-file-content", ".oxlintrc.ts", '"no-debugger": "warn"', '"no-debugger": "error"'], comment = "Change the shared rule once" }, + { argv = ["vp", "lint", "index.js"], comment = "The owner now reports an error", continue-on-failure = true }, + { argv = ["vp", "lint", "index.js"], cwd = "packages/a", comment = "The direct consumer now reports the same error", continue-on-failure = true }, + { argv = ["vp", "lint", "index.js"], cwd = "packages/b", comment = "The transitive consumer now reports the same error", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_workspace_preserved_consumer" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "replace-file-content", ".oxlintrc.json", '"no-debugger": "warn"', '"no-debugger": "error"'], snapshot = false }, + { argv = ["vp", "lint", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/b", comment = "The original JSON consumer inherits an error through both workspaces", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"], comment = "Root and package a migrate; package b keeps its JSON because extra.ts exists" }, + { argv = ["vpt", "stat-file", ".oxlintrc.json", "--assert", "file"] }, + { argv = ["vpt", "stat-file", "packages/a/.oxlintrc.json", "--assert", "file"] }, + { argv = ["vpt", "stat-file", "packages/b/.oxlintrc.json", "--assert", "file"] }, + { argv = ["vpt", "print-file", "vite.config.mjs"], comment = "Retaining JSON does not undo the owner's successful migration" }, + { argv = ["vpt", "print-file", "packages/a/vite.config.mjs"] }, + { argv = ["vpt", "print-file", "packages/b/vite.config.mjs"], comment = "The preserved consumer's Vite config has no partial lint config" }, + { argv = ["vpt", "print-file", "packages/b/extra.ts"], comment = "The conflicting file is unchanged" }, + { argv = ["vp", "lint", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/b", comment = "The JSON fallback still reports the same diagnostic and exit code", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", "packages/b/.oxlintrc.json", "packages/b/index.js"], comment = "The fallback command shown at the workspace root selects the preserved package config", continue-on-failure = true }, + { argv = ["vp", "lint", "index.js"], comment = "The migrated owner uses its shared module", continue-on-failure = true }, + { argv = ["vp", "lint", "index.js"], cwd = "packages/a", comment = "The migrated intermediate owner uses its shared module", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_workspace_owner_ignores" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "rm", "packages/b/extra.ts"], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "ignorePatterns", '["generated/**"]'], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "rules.no-debugger", '"error"'], snapshot = false }, + { argv = ["vpt", "write-file", "index.js", "export const clean = true;\n"], snapshot = false }, + { argv = ["vpt", "write-file", "packages/a/index.js", "export const clean = true;\n"], snapshot = false }, + { argv = ["vpt", "write-file", "generated/built.js", "debugger;\n"], snapshot = false }, + { argv = ["vpt", "write-file", "packages/a/generated/built.js", "debugger;\n"], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js", "generated/built.js"], comment = "The original owner excludes its generated file" }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js", "generated/built.js"], cwd = "packages/a", comment = "The dependent does not inherit the owner's ignore patterns", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "vite.config.mjs"], comment = "The owner's top-level ignore patterns reference its shared module" }, + { argv = ["vpt", "print-file", "packages/a/vite.config.mjs"], comment = "The dependent does not receive top-level ignore patterns" }, + { argv = ["vp", "lint", "--threads=1", "index.js", "generated/built.js"], comment = "The owner still excludes the generated file and exits successfully" }, + { argv = ["vp", "lint", "--threads=1", "index.js", "generated/built.js"], cwd = "packages/a", comment = "The dependent still reports the same diagnostic and exit code", continue-on-failure = true }, + { argv = ["vpt", "replace-file-content", ".oxlintrc.ts", '"generated/**"', '"other/**"'], comment = "Edit the shared ignore patterns once" }, + { argv = ["vp", "lint", "--threads=1", "index.js", "generated/built.js"], comment = "The owner follows the edit and now reports the generated file", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "index.js", "generated/built.js"], cwd = "packages/a", comment = "The dependent's diagnostics remain unchanged", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_workspace_symlinks" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "rm", "packages/a/.oxlintrc.json"], snapshot = false }, + { argv = ["vpt", "rm", "packages/b/.oxlintrc.json"], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "rules.no-console", '"off"'], snapshot = false }, + { argv = ["vpt", "write-file", "shared/lint.json", '{"rules":{"no-console":"error"},"options":{"typeAware":false,"typeCheck":false}}'], snapshot = false }, + { argv = ["vpt", "write-file", "packages/a/index.js", 'console.log("hello");'], snapshot = false }, + { argv = ["vpt", "write-file", "packages/b/index.js", 'console.log("hello");'], snapshot = false }, + { argv = ["node", "create-config-links.mjs"], snapshot = false }, + { argv = ["vpt", "stat-file", "packages/a/.oxlintrc.json", "--assert", "symlink"] }, + { argv = ["vpt", "stat-file", "packages/b/.oxlintrc.json", "--assert", "symlink"] }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/a", comment = "The first symlinked consumer reports no-console before migration", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/b", comment = "The second symlinked consumer reports the same error before migration", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "packages/a/vite.config.mjs"], comment = "Each consumer receives its own lint block" }, + { argv = ["vpt", "print-file", "packages/b/vite.config.mjs"] }, + { argv = ["vpt", "stat-file", "packages/a/.oxlintrc.json", "--assert", "missing"] }, + { argv = ["vpt", "stat-file", "packages/b/.oxlintrc.json", "--assert", "missing"] }, + { argv = ["vpt", "print-file", "shared/lint.json"], comment = "Removing workspace symlinks preserves their shared JSON target" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/a", comment = "The first migrated consumer keeps the same diagnostic and exit code", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/b", comment = "The second migrated consumer keeps the same diagnostic and exit code", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_workspace_retry" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"], comment = "Package b cannot migrate while extra.ts conflicts with its JSON config" }, + { argv = ["vpt", "replace-file-content", ".oxlintrc.ts", '"no-debugger": "warn"', '"no-debugger": "error"'], comment = "Edit the shared module created by the successful owner" }, + { argv = ["vpt", "rm", "packages/b/extra.ts"], comment = "Remove the conflict before retrying" }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"], comment = "Retry reuses previously generated modules" }, + { argv = ["vpt", "print-file", "packages/b/vite.config.mjs"] }, + { argv = ["vpt", "stat-file", "packages/b/.oxlintrc.json", "--assert", "missing"] }, + { argv = ["vpt", "stat-file", ".oxlintrc.json", "--assert", "missing"], comment = "The previously migrated owner no longer needs its JSON" }, + { argv = ["vpt", "stat-file", "packages/a/.oxlintrc.json", "--assert", "missing"], comment = "The previously migrated intermediate config is also cleaned up" }, + { argv = ["vpt", "print-file", ".oxlintrc.ts"], comment = "The user's shared rule edit is preserved" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/b", comment = "The recovered consumer uses the edited shared rule without a config flag", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_workspace_owner_fields" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "rm", "packages/b/extra.ts"], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "env", '{"browser":true}'], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "globals", '{"fixtureGlobal":"readonly"}'], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "settings", '{"fixture":{"enabled":true}}'], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "jsPlugins", '["./settings-plugin.mjs"]'], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "rules", '{"no-undef":"error"}'], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "overrides", '[{"files":["settings.js"],"rules":{"fixture/require-setting":"error"}}]'], snapshot = false }, + { argv = ["vpt", "write-file", "env.js", "export const value = window;\n"], snapshot = false }, + { argv = ["vpt", "write-file", "globals.js", "export const value = fixtureGlobal;\n"], snapshot = false }, + { argv = ["vpt", "write-file", "settings.js", "export const value = 1;\n"], snapshot = false }, + { argv = ["vpt", "cp", "env.js", "packages/a/env.js"], snapshot = false }, + { argv = ["vpt", "cp", "globals.js", "packages/a/globals.js"], snapshot = false }, + { argv = ["vpt", "cp", "settings.js", "packages/a/settings.js"], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "env.js"], comment = "The original owner uses its environment, globals, and plugin settings" }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "globals.js"], comment = "The original owner uses its environment, globals, and plugin settings" }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "settings.js"], comment = "The original owner uses its environment, globals, and plugin settings" }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "env.js"], cwd = "packages/a", comment = "A dependent does not inherit the owner's local fields", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "globals.js"], cwd = "packages/a", comment = "A dependent does not inherit the owner's local fields", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "settings.js"], cwd = "packages/a", comment = "A dependent does not inherit the owner's local fields", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "vite.config.mjs"], comment = "The owner references its fields at the top level" }, + { argv = ["vp", "lint", "--threads=1", "env.js"], comment = "The owner retains its exclusions from no-undef and the plugin diagnostic" }, + { argv = ["vp", "lint", "--threads=1", "globals.js"], comment = "The owner retains its exclusions from no-undef and the plugin diagnostic" }, + { argv = ["vp", "lint", "--threads=1", "settings.js"], comment = "The owner retains its exclusions from no-undef and the plugin diagnostic" }, + { argv = ["vp", "lint", "--threads=1", "env.js"], cwd = "packages/a", comment = "The dependent retains the same diagnostics and exit code", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "globals.js"], cwd = "packages/a", comment = "The dependent retains the same diagnostics and exit code", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "settings.js"], cwd = "packages/a", comment = "The dependent retains the same diagnostics and exit code", continue-on-failure = true }, + { argv = ["vpt", "replace-file-content", ".oxlintrc.ts", "browser: true", "browser: false"] }, + { argv = ["vpt", "replace-file-content", ".oxlintrc.ts", 'fixtureGlobal: "readonly"', 'fixtureGlobal: "off"'] }, + { argv = ["vpt", "replace-file-content", ".oxlintrc.ts", "enabled: true", "enabled: false"] }, + { argv = ["vp", "lint", "--threads=1", "env.js"], comment = "Shared-module edits update all three owner fields", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "globals.js"], comment = "Shared-module edits update all three owner fields", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "settings.js"], comment = "Shared-module edits update all three owner fields", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_workspace_symlink_retry" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "rm", "packages/a/.oxlintrc.json"], snapshot = false }, + { argv = ["vpt", "rm", "packages/b/.oxlintrc.json"], snapshot = false }, + { argv = ["vpt", "write-file", "shared/lint.json", '{"extends":["./base.json"],"rules":{"no-console":"error"},"options":{"typeAware":false,"typeCheck":false}}'], snapshot = false }, + { argv = ["vpt", "write-file", "shared/base.json", '{}'], snapshot = false }, + { argv = ["vpt", "write-file", "packages/a/index.js", 'console.log("hello");'], snapshot = false }, + { argv = ["vpt", "write-file", "packages/b/index.js", 'console.log("hello");'], snapshot = false }, + { argv = ["node", "create-config-links.mjs", "--with-base"], snapshot = false }, + { argv = ["vpt", "write-file", "packages/b/vite.config.mjs", "export { default } from './another-config.mjs';"], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/a", comment = "The original symlinked consumer reports no-console", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/b", comment = "The second alias uses the same rules", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "stat-file", "packages/a/.oxlintrc.json", "--assert", "missing"], comment = "The successful alias is removed independently" }, + { argv = ["vpt", "stat-file", "packages/b/.oxlintrc.json", "--assert", "symlink"], comment = "The failed consumer retains its own alias" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/a", comment = "The migrated consumer keeps the same diagnostic", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/b", comment = "The preserved consumer keeps a working JSON fallback", continue-on-failure = true }, + { argv = ["vpt", "replace-file-content", "packages/b/vite.config.mjs", 'export { default } from "./another-config.mjs";', "export default {};"], comment = "Fix the unsupported export while retaining the workspace's pending marker" }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "stat-file", "packages/b/.oxlintrc.json", "--assert", "missing"], comment = "The repaired alias is removed after its own migration completes" }, + { argv = ["vpt", "stat-file", "shared/lint.json", "--assert", "file"], comment = "Unlinking both consumers preserves the shared source" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/b", comment = "The recovered consumer keeps the original diagnostic", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_workspace_symlink_plugins" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "rm", "packages/a/.oxlintrc.json"], snapshot = false }, + { argv = ["vpt", "rm", "packages/b/.oxlintrc.json"], snapshot = false }, + { argv = ["vpt", "write-file", "shared/lint.json", '{"jsPlugins":[{"name":"fixture","specifier":"./plugin.mjs"}],"rules":{"fixture/require-setting":"error"},"options":{"typeAware":false,"typeCheck":false}}'], snapshot = false }, + { argv = ["vpt", "cp", "settings-plugin.mjs", "packages/a/plugin.mjs"], snapshot = false }, + { argv = ["vpt", "cp", "settings-plugin.mjs", "packages/b/plugin.mjs"], snapshot = false }, + { argv = ["vpt", "write-file", "packages/a/index.js", "export const value = 1;\n"], snapshot = false }, + { argv = ["vpt", "write-file", "packages/b/index.js", "export const value = 1;\n"], snapshot = false }, + { argv = ["node", "create-config-links.mjs"], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/a", comment = "Oxlint resolves the relative root plugin from the alias directory", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/b", comment = "Each workspace uses its own relative plugin", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "packages/a/vite.config.mjs"], comment = "The root plugin path stays relative to the workspace" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/a", comment = "The first consumer keeps the original plugin diagnostic", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/b", comment = "The second consumer keeps the original plugin diagnostic", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_workspace_symlink_contexts" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "rm", "packages/a/.oxlintrc.json"], snapshot = false }, + { argv = ["vpt", "rm", "packages/b/.oxlintrc.json"], snapshot = false }, + { argv = ["vpt", "write-file", "shared/lint.json", '{"extends":["./base.json"],"options":{"typeAware":false,"typeCheck":false}}'], snapshot = false }, + { argv = ["vpt", "write-file", "shared/base.json", '{"rules":{"no-console":"off"}}'], snapshot = false }, + { argv = ["vpt", "write-file", "packages/a/base.json", '{"rules":{"no-console":"error"}}'], snapshot = false }, + { argv = ["vpt", "write-file", "packages/b/base.json", '{"rules":{"no-console":"warn"}}'], snapshot = false }, + { argv = ["vpt", "write-file", "packages/a/index.js", 'console.log("hello");'], snapshot = false }, + { argv = ["vpt", "write-file", "packages/b/index.js", 'console.log("hello");'], snapshot = false }, + { argv = ["node", "create-config-links.mjs"], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/a", comment = "The first alias resolves its local base to an error", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/b", comment = "The second alias resolves the same extends string to a warning" }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "print-file", "packages/a/vite.config.mjs"] }, + { argv = ["vpt", "print-file", "packages/b/vite.config.mjs"] }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/a", comment = "The migrated first consumer retains its error", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/b", comment = "The migrated second consumer retains its warning" }, +] +[[case]] +name = "migration_oxlint_extends_workspace_incomplete_graph_retry" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "rm", ".oxlintrc.json"], snapshot = false }, + { argv = ["vpt", "write-file", "packages/a/.oxlintrc.json", '{"rules":{"no-debugger":"error"},"options":{"typeAware":false,"typeCheck":false}}'], snapshot = false }, + { argv = ["vpt", "write-file", "packages/b/.oxlintrc.json", '{"extends":["./missing.json"]}'], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/a", comment = "Original rules without inheritance", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "stat-file", "packages/a/.oxlintrc.json", "--assert", "file"], comment = "An unreadable graph keeps completed JSON configs on disk" }, + { argv = ["vpt", "stat-file", "packages/a/.oxlintrc.ts", "--assert", "file"] }, + { argv = ["vpt", "write-file", "packages/b/missing.json", '{"rules":{"no-debugger":"error"},"options":{"typeAware":false,"typeCheck":false}}'], comment = "Repair the unreadable inheritance graph" }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/b", comment = "The repaired JSON loads before retrying", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "stat-file", "packages/a/.oxlintrc.json", "--assert", "missing"] }, + { argv = ["vpt", "stat-file", "packages/b/.oxlintrc.json", "--assert", "missing"] }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/a", comment = "The completed consumer retains its diagnostic", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/b", comment = "The repaired consumer retains its diagnostic after retrying", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_workspace_inherited_alias" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "rm", "packages/b/extra.ts"], snapshot = false }, + { argv = ["vpt", "write-file", "packages/a/.oxlintrc.json", '{"extends":["./alias.json"]}'], snapshot = false }, + { argv = ["node", "create-config-links.mjs", "--owner-alias"], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", "alias.json", "index.js"], cwd = "packages/a", comment = "The inherited alias loads the original owner config" }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "stat-file", ".oxlintrc.json", "--assert", "file"], comment = "The surviving inherited alias retains its target" }, + { argv = ["vpt", "stat-file", "packages/a/alias.json", "--assert", "symlink"] }, + { argv = ["vpt", "stat-file", "packages/a/.oxlintrc.json", "--assert", "missing"] }, + { argv = ["vp", "lint", "--threads=1", "-c", "alias.json", "index.js"], cwd = "packages/a", comment = "The inherited alias still loads with the same diagnostic" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/a", comment = "The migrated consumer uses its shared module" }, +] + +[[case]] +name = "migration_oxlint_extends_workspace_repaired_shared_owner" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "rm", "packages/b/.oxlintrc.json"], snapshot = false }, + { argv = ["vpt", "write-file", "packages/a/.oxlintrc.json", '{"extends":["./missing.json"]}'], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], comment = "The original owner reports a warning" }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "write-file", "packages/a/missing.json", '{"extends":["../../.oxlintrc.json"]}'], comment = "The repaired graph reveals that the root is a shared owner" }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/a", comment = "The repaired JSON inherits the original warning" }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The migrated owner retains its warning" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/a", comment = "The repaired consumer retains the same warning" }, + { argv = ["vpt", "replace-file-content", ".oxlintrc.ts", '"no-debugger": "warn"', '"no-debugger": "error"'] }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The owner follows the shared module edit", continue-on-failure = true }, + { argv = ["vp", "lint", "--threads=1", "index.js"], cwd = "packages/a", comment = "The repaired consumer follows the same edit", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_workspace_new_consumer" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vpt", "rm", "packages/b/extra.ts"], snapshot = false }, + { argv = ["vpt", "write-file", "shared/base.json", '{"rules":{"no-console":"error"},"options":{"typeAware":false,"typeCheck":false}}'], snapshot = false }, + { argv = ["vpt", "json-edit", ".oxlintrc.json", "extends", '["./shared/base.json"]'], snapshot = false }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"] }, + { argv = ["vpt", "stat-file", "shared/base.ts", "--assert", "file"] }, + { argv = ["vpt", "write-file", "packages/c/package.json", '{"name":"new-consumer","type":"module"}'], snapshot = false }, + { argv = ["vpt", "write-file", "packages/c/vite.config.mjs", "export default {};\n"], snapshot = false }, + { argv = ["vpt", "write-file", "packages/c/.oxlintrc.json", '{"extends":["../../shared/base.json"]}'], snapshot = false }, + { argv = ["vpt", "write-file", "packages/c/index.js", 'console.log("hello");'], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/c", comment = "A new workspace uses the retained shared JSON", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"], comment = "An upgrade does not treat shared modules as a pending migration for the new workspace" }, + { argv = ["vpt", "print-file", "packages/c/vite.config.mjs"], comment = "The unmarked workspace keeps its original Vite config" }, + { argv = ["vpt", "print-file", "packages/c/.oxlintrc.json"], comment = "Its newly authored JSON config remains intact" }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/c", comment = "The JSON config retains the same lint output", continue-on-failure = true }, +] + +[[case]] +name = "migration_oxlint_extends_workspace_full_setup" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL"] +steps = [ + { argv = ["vp", "add", "-D", "vite-plus", "prettier@3.6.2"], snapshot = false }, + { argv = ["vpt", "write-file", ".prettierrc.json", '{"singleQuote":true}'], snapshot = false }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/a", comment = "The existing workspace consumer inherits the root JSON rule" }, + { argv = ["vp", "migrate", "--full", "--no-interactive", "--no-agent", "--no-editor", "--no-hooks"], comment = "Prettier migration merges the root configs without migrating package configs" }, + { argv = ["vpt", "print-file", "vite.config.mjs"], comment = "The root uses its shared module" }, + { argv = ["vpt", "stat-file", ".oxlintrc.json", "--assert", "file"], comment = "The root JSON remains available to untouched consumers" }, + { argv = ["vpt", "stat-file", ".oxlintrc.ts", "--assert", "file"] }, + { argv = ["vpt", "print-file", "packages/a/vite.config.mjs"], comment = "The package Vite config remains unchanged" }, + { argv = ["vp", "lint", "--threads=1", "-c", ".oxlintrc.json", "index.js"], cwd = "packages/a", comment = "The preserved package JSON still reports the same diagnostic" }, + { argv = ["vp", "lint", "--threads=1", "index.js"], comment = "The migrated root enforces the same rule" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_full_setup.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_full_setup.md new file mode 100644 index 0000000000..8ce5b6ecc2 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_full_setup.md @@ -0,0 +1,126 @@ +# migration_oxlint_extends_workspace_full_setup + +## `vp add -D vite-plus prettier@3.6.2` + + +## `vpt write-file .prettierrc.json '{"singleQuote":true}'` + + +## `cd packages/a && vp lint --threads=1 -c .oxlintrc.json index.js` + +The existing workspace consumer inherits the root JSON rule + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --full --no-interactive --no-agent --no-editor --no-hooks` + +Prettier migration merges the root configs without migrating package configs + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +● Prettier configuration detected. Auto-migrating to Oxfmt... +● Formatting code... +● Code formatted +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite → +✓ Dependencies installed in +• 2 config updates applied +• Prettier migrated to Oxfmt +• Package manager settings configured +``` + +## `vpt print-file vite.config.mjs` + +The root uses its shared module + +``` +// vp migrate: merged .oxlintrc.json (cca48660331feaa20cf74e4f147c8e10). +import oxlintConfig1 from './.oxlintrc.js'; +export default { + lint: { + extends: [oxlintConfig1], + options: {}, + jsPlugins: [{ name: 'vite-plus', specifier: 'vite-plus/oxlint-plugin' }], + rules: { + 'vite-plus/prefer-vite-plus-imports': 'error', + }, + }, + fmt: { + singleQuote: true, + printWidth: 80, + sortPackageJson: false, + ignorePatterns: [], + }, +}; +``` + +## `vpt stat-file .oxlintrc.json --assert file` + +The root JSON remains available to untouched consumers + +``` +.oxlintrc.json: file +``` + +## `vpt stat-file .oxlintrc.ts --assert file` + +``` +.oxlintrc.ts: file +``` + +## `vpt print-file packages/a/vite.config.mjs` + +The package Vite config remains unchanged + +``` +export default {}; +``` + +## `cd packages/a && vp lint --threads=1 -c .oxlintrc.json index.js` + +The preserved package JSON still reports the same diagnostic + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 index.js` + +The migrated root enforces the same rule + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_incomplete_graph_retry.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_incomplete_graph_retry.md new file mode 100644 index 0000000000..428990e2d5 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_incomplete_graph_retry.md @@ -0,0 +1,150 @@ +# migration_oxlint_extends_workspace_incomplete_graph_retry + +## `vpt rm .oxlintrc.json` + + +## `vpt write-file packages/a/.oxlintrc.json '{"rules":{"no-debugger":"error"},"options":{"typeAware":false,"typeCheck":false}}'` + + +## `vpt write-file packages/b/.oxlintrc.json '{"extends":["./missing.json"]}'` + + +## `cd packages/a && vp lint --threads=1 -c .oxlintrc.json index.js` + +Original rules without inheritance + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 1 config update applied +! Warnings: + - Could not migrate packages/b/.oxlintrc.json: Error: ENOENT: no such file or directory, lstat '/packages/b/missing.json'. The original config was preserved; use vp lint -c packages/b/.oxlintrc.json until it is migrated manually. +``` + +## `vpt stat-file packages/a/.oxlintrc.json --assert file` + +An unreadable graph keeps completed JSON configs on disk + +``` +packages/a/.oxlintrc.json: file +``` + +## `vpt stat-file packages/a/.oxlintrc.ts --assert file` + +``` +packages/a/.oxlintrc.ts: file +``` + +## `vpt write-file packages/b/missing.json '{"rules":{"no-debugger":"error"},"options":{"typeAware":false,"typeCheck":false}}'` + +Repair the unreadable inheritance graph + +``` +``` + +## `cd packages/b && vp lint --threads=1 -c .oxlintrc.json index.js` + +The repaired JSON loads before retrying + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +● Formatting code... +● Code formatted +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite → +• 1 config update applied +``` + +## `vpt stat-file packages/a/.oxlintrc.json --assert missing` + +``` +packages/a/.oxlintrc.json: missing +``` + +## `vpt stat-file packages/b/.oxlintrc.json --assert missing` + +``` +packages/b/.oxlintrc.json: missing +``` + +## `cd packages/a && vp lint --threads=1 index.js` + +The completed consumer retains its diagnostic + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/b && vp lint --threads=1 index.js` + +The repaired consumer retains its diagnostic after retrying + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_inherited_alias.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_inherited_alias.md new file mode 100644 index 0000000000..7fdfd813ae --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_inherited_alias.md @@ -0,0 +1,96 @@ +# migration_oxlint_extends_workspace_inherited_alias + +## `vpt rm packages/b/extra.ts` + + +## `vpt write-file packages/a/.oxlintrc.json '{"extends":["./alias.json"]}'` + + +## `node create-config-links.mjs --owner-alias` + + +## `cd packages/a && vp lint --threads=1 -c alias.json index.js` + +The inherited alias loads the original owner config + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +◆ ✔ Merged .oxlintrc.json into vite.config.mjs +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt stat-file .oxlintrc.json --assert file` + +The surviving inherited alias retains its target + +``` +.oxlintrc.json: file +``` + +## `vpt stat-file packages/a/alias.json --assert symlink` + +``` +packages/a/alias.json: symlink +``` + +## `vpt stat-file packages/a/.oxlintrc.json --assert missing` + +``` +packages/a/.oxlintrc.json: missing +``` + +## `cd packages/a && vp lint --threads=1 -c alias.json index.js` + +The inherited alias still loads with the same diagnostic + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint --threads=1 index.js` + +The migrated consumer uses its shared module + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_new_consumer.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_new_consumer.md new file mode 100644 index 0000000000..0fd8c5438f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_new_consumer.md @@ -0,0 +1,109 @@ +# migration_oxlint_extends_workspace_new_consumer + +## `vpt rm packages/b/extra.ts` + + +## `vpt write-file shared/base.json '{"rules":{"no-console":"error"},"options":{"typeAware":false,"typeCheck":false}}'` + + +## `vpt json-edit .oxlintrc.json extends '["./shared/base.json"]'` + + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +◆ ✔ Merged .oxlintrc.json into vite.config.mjs +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt stat-file shared/base.ts --assert file` + +``` +shared/base.ts: file +``` + +## `vpt write-file packages/c/package.json '{"name":"new-consumer","type":"module"}'` + + +## `vpt write-file packages/c/vite.config.mjs 'export default {}; +'` + + +## `vpt write-file packages/c/.oxlintrc.json '{"extends":["../../shared/base.json"]}'` + + +## `vpt write-file packages/c/index.js 'console.log("hello");'` + + +## `cd packages/c && vp lint --threads=1 -c .oxlintrc.json index.js` + +A new workspace uses the retained shared JSON + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +An upgrade does not treat shared modules as a pending migration for the new workspace + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +This project is already using Vite+! Happy coding! +``` + +## `vpt print-file packages/c/vite.config.mjs` + +The unmarked workspace keeps its original Vite config + +``` +export default {}; +``` + +## `vpt print-file packages/c/.oxlintrc.json` + +Its newly authored JSON config remains intact + +``` +{"extends":["../../shared/base.json"]} +``` + +## `cd packages/c && vp lint --threads=1 -c .oxlintrc.json index.js` + +The JSON config retains the same lint output + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_owner_fields.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_owner_fields.md new file mode 100644 index 0000000000..f18b5799b7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_owner_fields.md @@ -0,0 +1,318 @@ +# migration_oxlint_extends_workspace_owner_fields + +## `vpt rm packages/b/extra.ts` + + +## `vpt json-edit .oxlintrc.json env '{"browser":true}'` + + +## `vpt json-edit .oxlintrc.json globals '{"fixtureGlobal":"readonly"}'` + + +## `vpt json-edit .oxlintrc.json settings '{"fixture":{"enabled":true}}'` + + +## `vpt json-edit .oxlintrc.json jsPlugins '["./settings-plugin.mjs"]'` + + +## `vpt json-edit .oxlintrc.json rules '{"no-undef":"error"}'` + + +## `vpt json-edit .oxlintrc.json overrides '[{"files":["settings.js"],"rules":{"fixture/require-setting":"error"}}]'` + + +## `vpt write-file env.js 'export const value = window; +'` + + +## `vpt write-file globals.js 'export const value = fixtureGlobal; +'` + + +## `vpt write-file settings.js 'export const value = 1; +'` + + +## `vpt cp env.js packages/a/env.js` + + +## `vpt cp globals.js packages/a/globals.js` + + +## `vpt cp settings.js packages/a/settings.js` + + +## `vp lint --threads=1 -c .oxlintrc.json env.js` + +The original owner uses its environment, globals, and plugin settings + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 -c .oxlintrc.json globals.js` + +The original owner uses its environment, globals, and plugin settings + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 -c .oxlintrc.json settings.js` + +The original owner uses its environment, globals, and plugin settings + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint --threads=1 -c .oxlintrc.json env.js` + +A dependent does not inherit the owner's local fields + +**Exit code:** 1 + +``` + + × eslint(no-undef): 'window' is not defined. + ╭─[env.js:1:22] + 1 │ export const value = window; + · ────── + ╰──── + help: Either define 'window' or remove the reference to it. If 'window' is a global variable, add it to the 'globals' configuration. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint --threads=1 -c .oxlintrc.json globals.js` + +A dependent does not inherit the owner's local fields + +**Exit code:** 1 + +``` + + × eslint(no-undef): 'fixtureGlobal' is not defined. + ╭─[globals.js:1:22] + 1 │ export const value = fixtureGlobal; + · ───────────── + ╰──── + help: Either define 'fixtureGlobal' or remove the reference to it. If 'fixtureGlobal' is a global variable, add it to the 'globals' configuration. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint --threads=1 -c .oxlintrc.json settings.js` + +A dependent does not inherit the owner's local fields + +**Exit code:** 1 + +``` + + × fixture(require-setting): The fixture setting must be enabled. + ╭─[settings.js:1:1] + 1 │ export const value = 1; + · ──────────────────────── + ╰──── + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +◆ ✔ Merged .oxlintrc.json into vite.config.mjs +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt print-file vite.config.mjs` + +The owner references its fields at the top level + +``` +// vp migrate: merged .oxlintrc.json (776c7037d8d471e35fd2209aa88a3a33). +import oxlintConfig1 from "./.oxlintrc.js"; +export default { + fmt: {}, + lint: { + extends: [oxlintConfig1], + env: oxlintConfig1.env, + globals: oxlintConfig1.globals, + settings: oxlintConfig1.settings, + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, +}; +``` + +## `vp lint --threads=1 env.js` + +The owner retains its exclusions from no-undef and the plugin diagnostic + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 globals.js` + +The owner retains its exclusions from no-undef and the plugin diagnostic + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 settings.js` + +The owner retains its exclusions from no-undef and the plugin diagnostic + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint --threads=1 env.js` + +The dependent retains the same diagnostics and exit code + +**Exit code:** 1 + +``` + + × eslint(no-undef): 'window' is not defined. + ╭─[env.js:1:22] + 1 │ export const value = window; + · ────── + ╰──── + help: Either define 'window' or remove the reference to it. If 'window' is a global variable, add it to the 'globals' configuration. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint --threads=1 globals.js` + +The dependent retains the same diagnostics and exit code + +**Exit code:** 1 + +``` + + × eslint(no-undef): 'fixtureGlobal' is not defined. + ╭─[globals.js:1:22] + 1 │ export const value = fixtureGlobal; + · ───────────── + ╰──── + help: Either define 'fixtureGlobal' or remove the reference to it. If 'fixtureGlobal' is a global variable, add it to the 'globals' configuration. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint --threads=1 settings.js` + +The dependent retains the same diagnostics and exit code + +**Exit code:** 1 + +``` + + × fixture(require-setting): The fixture setting must be enabled. + ╭─[settings.js:1:1] + 1 │ export const value = 1; + · ──────────────────────── + ╰──── + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vpt replace-file-content .oxlintrc.ts 'browser: true' 'browser: false'` + +``` +``` + +## `vpt replace-file-content .oxlintrc.ts 'fixtureGlobal: "readonly"' 'fixtureGlobal: "off"'` + +``` +``` + +## `vpt replace-file-content .oxlintrc.ts 'enabled: true' 'enabled: false'` + +``` +``` + +## `vp lint --threads=1 env.js` + +Shared-module edits update all three owner fields + +**Exit code:** 1 + +``` + + × eslint(no-undef): 'window' is not defined. + ╭─[env.js:1:22] + 1 │ export const value = window; + · ────── + ╰──── + help: Either define 'window' or remove the reference to it. If 'window' is a global variable, add it to the 'globals' configuration. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 globals.js` + +Shared-module edits update all three owner fields + +**Exit code:** 1 + +``` + + × eslint(no-undef): 'fixtureGlobal' is not defined. + ╭─[globals.js:1:22] + 1 │ export const value = fixtureGlobal; + · ───────────── + ╰──── + help: Either define 'fixtureGlobal' or remove the reference to it. If 'fixtureGlobal' is a global variable, add it to the 'globals' configuration. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 settings.js` + +Shared-module edits update all three owner fields + +**Exit code:** 1 + +``` + + × fixture(require-setting): The fixture setting must be enabled. + ╭─[settings.js:1:1] + 1 │ export const value = 1; + · ──────────────────────── + ╰──── + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_owner_ignores.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_owner_ignores.md new file mode 100644 index 0000000000..6c0792d246 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_owner_ignores.md @@ -0,0 +1,182 @@ +# migration_oxlint_extends_workspace_owner_ignores + +## `vpt rm packages/b/extra.ts` + + +## `vpt json-edit .oxlintrc.json ignorePatterns '["generated/**"]'` + + +## `vpt json-edit .oxlintrc.json rules.no-debugger '"error"'` + + +## `vpt write-file index.js 'export const clean = true; +'` + + +## `vpt write-file packages/a/index.js 'export const clean = true; +'` + + +## `vpt write-file generated/built.js 'debugger; +'` + + +## `vpt write-file packages/a/generated/built.js 'debugger; +'` + + +## `vp lint --threads=1 -c .oxlintrc.json index.js generated/built.js` + +The original owner excludes its generated file + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint --threads=1 -c .oxlintrc.json index.js generated/built.js` + +The dependent does not inherit the owner's ignore patterns + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[generated/built.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 2 files with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +◆ ✔ Merged .oxlintrc.json into vite.config.mjs +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt print-file vite.config.mjs` + +The owner's top-level ignore patterns reference its shared module + +``` +// vp migrate: merged .oxlintrc.json (4d2ecd8da8b74c19aab2ebbba9d98fe6). +import oxlintConfig1 from "./.oxlintrc.js"; +export default { + fmt: {}, + lint: { + extends: [oxlintConfig1], + ignorePatterns: oxlintConfig1.ignorePatterns, + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, +}; +``` + +## `vpt print-file packages/a/vite.config.mjs` + +The dependent does not receive top-level ignore patterns + +``` +// vp migrate: merged packages%2Fa%2F.oxlintrc.json (b829c0806f4ea2e6444bd91df0061b20). +import oxlintConfig1 from "./.oxlintrc.js"; +export default { + lint: { + extends: [oxlintConfig1], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, +}; +``` + +## `vp lint --threads=1 index.js generated/built.js` + +The owner still excludes the generated file and exits successfully + +``` +Found 0 warnings and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint --threads=1 index.js generated/built.js` + +The dependent still reports the same diagnostic and exit code + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[generated/built.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 2 files with rules using threads. +``` + +## `vpt replace-file-content .oxlintrc.ts '"generated/**"' '"other/**"'` + +Edit the shared ignore patterns once + +``` +``` + +## `vp lint --threads=1 index.js generated/built.js` + +The owner follows the edit and now reports the generated file + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[generated/built.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 2 files with rules using threads. +``` + +## `cd packages/a && vp lint --threads=1 index.js generated/built.js` + +The dependent's diagnostics remain unchanged + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[generated/built.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 2 files with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_preserved_consumer.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_preserved_consumer.md new file mode 100644 index 0000000000..f0deb5f077 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_preserved_consumer.md @@ -0,0 +1,190 @@ +# migration_oxlint_extends_workspace_preserved_consumer + +## `vpt replace-file-content .oxlintrc.json '"no-debugger": "warn"' '"no-debugger": "error"'` + + +## `cd packages/b && vp lint -c .oxlintrc.json index.js` + +The original JSON consumer inherits an error through both workspaces + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +Root and package a migrate; package b keeps its JSON because extra.ts exists + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +◆ ✔ Merged .oxlintrc.json into vite.config.mjs +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 1 config update applied +! Warnings: + - Could not migrate packages/b/.oxlintrc.json: Error: Cannot create shared Oxlint config because /packages/b/extra.ts or /packages/b/extra.js already exists. The original config was preserved; use vp lint -c packages/b/.oxlintrc.json until it is migrated manually. +``` + +## `vpt stat-file .oxlintrc.json --assert file` + +``` +.oxlintrc.json: file +``` + +## `vpt stat-file packages/a/.oxlintrc.json --assert file` + +``` +packages/a/.oxlintrc.json: file +``` + +## `vpt stat-file packages/b/.oxlintrc.json --assert file` + +``` +packages/b/.oxlintrc.json: file +``` + +## `vpt print-file vite.config.mjs` + +Retaining JSON does not undo the owner's successful migration + +``` +// vp migrate: merged .oxlintrc.json (bf57dd1b2fb51cd1cb30e45e36742e3b). +import oxlintConfig1 from "./.oxlintrc.js"; +export default { + fmt: {}, + lint: { + extends: [oxlintConfig1], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, +}; +``` + +## `vpt print-file packages/a/vite.config.mjs` + +``` +// vp migrate: merged packages%2Fa%2F.oxlintrc.json (c59f567503f7d6b118c01736f9206ec4). +import oxlintConfig1 from "./.oxlintrc.js"; +export default { + lint: { + extends: [oxlintConfig1], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, +}; +``` + +## `vpt print-file packages/b/vite.config.mjs` + +The preserved consumer's Vite config has no partial lint config + +``` +export default {}; +// vp migrate: pending .oxlintrc.json. +``` + +## `vpt print-file packages/b/extra.ts` + +The conflicting file is unchanged + +``` +export default {}; +``` + +## `cd packages/b && vp lint -c .oxlintrc.json index.js` + +The JSON fallback still reports the same diagnostic and exit code + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp lint --threads=1 -c packages/b/.oxlintrc.json packages/b/index.js` + +The fallback command shown at the workspace root selects the preserved package config + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[packages/b/index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp lint index.js` + +The migrated owner uses its shared module + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint index.js` + +The migrated intermediate owner uses its shared module + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_repaired_shared_owner.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_repaired_shared_owner.md new file mode 100644 index 0000000000..30ede20f28 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_repaired_shared_owner.md @@ -0,0 +1,156 @@ +# migration_oxlint_extends_workspace_repaired_shared_owner + +## `vpt rm packages/b/.oxlintrc.json` + + +## `vpt write-file packages/a/.oxlintrc.json '{"extends":["./missing.json"]}'` + + +## `vp lint --threads=1 -c .oxlintrc.json index.js` + +The original owner reports a warning + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +◆ ✔ Merged .oxlintrc.json into vite.config.mjs +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +! Warnings: + - Could not migrate packages/a/.oxlintrc.json: Error: ENOENT: no such file or directory, lstat '/packages/a/missing.json'. The original config was preserved; use vp lint -c packages/a/.oxlintrc.json until it is migrated manually. +``` + +## `vpt write-file packages/a/missing.json '{"extends":["../../.oxlintrc.json"]}'` + +The repaired graph reveals that the root is a shared owner + +``` +``` + +## `cd packages/a && vp lint --threads=1 -c .oxlintrc.json index.js` + +The repaired JSON inherits the original warning + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +● Formatting code... +● Code formatted +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite → +• 1 config update applied +``` + +## `vp lint --threads=1 index.js` + +The migrated owner retains its warning + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint --threads=1 index.js` + +The repaired consumer retains the same warning + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vpt replace-file-content .oxlintrc.ts '"no-debugger": "warn"' '"no-debugger": "error"'` + +``` +``` + +## `vp lint --threads=1 index.js` + +The owner follows the shared module edit + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint --threads=1 index.js` + +The repaired consumer follows the same edit + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_retry.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_retry.md new file mode 100644 index 0000000000..3a9de1d787 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_retry.md @@ -0,0 +1,129 @@ +# migration_oxlint_extends_workspace_retry + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +Package b cannot migrate while extra.ts conflicts with its JSON config + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +◆ ✔ Merged .oxlintrc.json into vite.config.mjs +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 1 config update applied +! Warnings: + - Could not migrate packages/b/.oxlintrc.json: Error: Cannot create shared Oxlint config because /packages/b/extra.ts or /packages/b/extra.js already exists. The original config was preserved; use vp lint -c packages/b/.oxlintrc.json until it is migrated manually. +``` + +## `vpt replace-file-content .oxlintrc.ts '"no-debugger": "warn"' '"no-debugger": "error"'` + +Edit the shared module created by the successful owner + +``` +``` + +## `vpt rm packages/b/extra.ts` + +Remove the conflict before retrying + +``` +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +Retry reuses previously generated modules + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +● Formatting code... +● Code formatted +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite → +• 1 config update applied +``` + +## `vpt print-file packages/b/vite.config.mjs` + +``` +// vp migrate: merged packages%2Fb%2F.oxlintrc.json (e5c417b92177153603bad60d94de6188). +import oxlintConfig1 from "../a/.oxlintrc.js"; +import oxlintConfig2 from "./extra.js"; +export default { + lint: { + extends: [oxlintConfig1, oxlintConfig2], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, +}; +``` + +## `vpt stat-file packages/b/.oxlintrc.json --assert missing` + +``` +packages/b/.oxlintrc.json: missing +``` + +## `vpt stat-file .oxlintrc.json --assert missing` + +The previously migrated owner no longer needs its JSON + +``` +.oxlintrc.json: missing +``` + +## `vpt stat-file packages/a/.oxlintrc.json --assert missing` + +The previously migrated intermediate config is also cleaned up + +``` +packages/a/.oxlintrc.json: missing +``` + +## `vpt print-file .oxlintrc.ts` + +The user's shared rule edit is preserved + +``` +// Generated by vp migrate from .oxlintrc.json. +import type { OxlintConfig } from "vite-plus/lint"; + +export default { + options: { + typeAware: false, + typeCheck: false, + }, + rules: { + "no-debugger": "error", + }, +} satisfies OxlintConfig; +``` + +## `cd packages/b && vp lint --threads=1 index.js` + +The recovered consumer uses the edited shared rule without a config flag + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_shared_owner.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_shared_owner.md new file mode 100644 index 0000000000..b6ed2c23e9 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_shared_owner.md @@ -0,0 +1,291 @@ +# migration_oxlint_extends_workspace_shared_owner + +## `vpt rm packages/b/extra.ts` + + +## `vp lint -c .oxlintrc.json index.js` + +The original owner uses the shared warning + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint -c .oxlintrc.json index.js` + +The direct consumer inherits the same warning + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/b && vp lint -c .oxlintrc.json index.js` + +The transitive consumer inherits the same warning + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +◆ ✔ Merged .oxlintrc.json into vite.config.mjs +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt print-file vite.config.mjs` + +The owner imports its own shared config + +``` +// vp migrate: merged .oxlintrc.json (cca48660331feaa20cf74e4f147c8e10). +import oxlintConfig1 from "./.oxlintrc.js"; +export default { + fmt: {}, + lint: { + extends: [oxlintConfig1], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, +}; +``` + +## `vpt print-file packages/a/vite.config.mjs` + +The intermediate owner also imports its own shared config + +``` +// vp migrate: merged packages%2Fa%2F.oxlintrc.json (e93b0fadd405db856ff1b8847eb5f2c3). +import oxlintConfig1 from "./.oxlintrc.js"; +export default { + lint: { + extends: [oxlintConfig1], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, +}; +``` + +## `vpt print-file packages/b/vite.config.mjs` + +``` +// vp migrate: merged packages%2Fb%2F.oxlintrc.json (e5c417b92177153603bad60d94de6188). +import oxlintConfig1 from "../a/.oxlintrc.js"; +import oxlintConfig2 from "./extra.js"; +export default { + lint: { + extends: [oxlintConfig1, oxlintConfig2], + options: {}, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, +}; +``` + +## `vpt print-file .oxlintrc.ts` + +Source rules remain in one shared module + +``` +// Generated by vp migrate from .oxlintrc.json. +import type { OxlintConfig } from "vite-plus/lint"; + +export default { + options: { + typeAware: false, + typeCheck: false, + }, + rules: { + "no-debugger": "warn", + }, +} satisfies OxlintConfig; +``` + +## `vpt print-file packages/a/.oxlintrc.ts` + +``` +// Generated by vp migrate from .oxlintrc.json. +import type { OxlintConfig } from "vite-plus/lint"; +import oxlintConfig1 from "../../.oxlintrc.js"; + +export default { + extends: [oxlintConfig1], +} satisfies OxlintConfig; +``` + +## `vpt stat-file .oxlintrc.json --assert-not file` + +``` +.oxlintrc.json: missing +``` + +## `vpt stat-file packages/a/.oxlintrc.json --assert-not file` + +``` +packages/a/.oxlintrc.json: missing +``` + +## `vpt stat-file packages/b/.oxlintrc.json --assert-not file` + +``` +packages/b/.oxlintrc.json: missing +``` + +## `vp lint index.js` + +The migrated owner reports the same warning + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint index.js` + +The migrated direct consumer reports the same warning + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/b && vp lint index.js` + +The migrated transitive consumer reports the same warning + +``` + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vpt replace-file-content .oxlintrc.ts '"no-debugger": "warn"' '"no-debugger": "error"'` + +Change the shared rule once + +``` +``` + +## `vp lint index.js` + +The owner now reports an error + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/a && vp lint index.js` + +The direct consumer now reports the same error + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/b && vp lint index.js` + +The transitive consumer now reports the same error + +**Exit code:** 1 + +``` + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:1:1] + 1 │ debugger; + · ───────── + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_symlink_contexts.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_symlink_contexts.md new file mode 100644 index 0000000000..32ea79b396 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_symlink_contexts.md @@ -0,0 +1,155 @@ +# migration_oxlint_extends_workspace_symlink_contexts + +## `vpt rm packages/a/.oxlintrc.json` + + +## `vpt rm packages/b/.oxlintrc.json` + + +## `vpt write-file shared/lint.json '{"extends":["./base.json"],"options":{"typeAware":false,"typeCheck":false}}'` + + +## `vpt write-file shared/base.json '{"rules":{"no-console":"off"}}'` + + +## `vpt write-file packages/a/base.json '{"rules":{"no-console":"error"}}'` + + +## `vpt write-file packages/b/base.json '{"rules":{"no-console":"warn"}}'` + + +## `vpt write-file packages/a/index.js 'console.log("hello");'` + + +## `vpt write-file packages/b/index.js 'console.log("hello");'` + + +## `node create-config-links.mjs` + + +## `cd packages/a && vp lint --threads=1 -c .oxlintrc.json index.js` + +The first alias resolves its local base to an error + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/b && vp lint --threads=1 -c .oxlintrc.json index.js` + +The second alias resolves the same extends string to a warning + +``` + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +◆ ✔ Merged .oxlintrc.json into vite.config.mjs +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt print-file packages/a/vite.config.mjs` + +``` +// vp migrate: merged packages%2Fa%2F.oxlintrc.json (5aa76017b601250136404623f0a0a3dd). +import oxlintConfig1 from "./base.js"; +export default { + lint: { + extends: [oxlintConfig1], + options: { + typeAware: false, + typeCheck: false, + }, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, +}; +``` + +## `vpt print-file packages/b/vite.config.mjs` + +``` +// vp migrate: merged packages%2Fb%2F.oxlintrc.json (56118339eefbe7bfe3d4f1a40696a3e9). +import oxlintConfig1 from "./base.js"; +export default { + lint: { + extends: [oxlintConfig1], + options: { + typeAware: false, + typeCheck: false, + }, + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, + }, +}; +``` + +## `cd packages/a && vp lint --threads=1 index.js` + +The migrated first consumer retains its error + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/b && vp lint --threads=1 index.js` + +The migrated second consumer retains its warning + +``` + + ⚠ eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 1 warning and 0 errors. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_symlink_plugins.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_symlink_plugins.md new file mode 100644 index 0000000000..ca10abe614 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_symlink_plugins.md @@ -0,0 +1,144 @@ +# migration_oxlint_extends_workspace_symlink_plugins + +## `vpt rm packages/a/.oxlintrc.json` + + +## `vpt rm packages/b/.oxlintrc.json` + + +## `vpt write-file shared/lint.json '{"jsPlugins":[{"name":"fixture","specifier":"./plugin.mjs"}],"rules":{"fixture/require-setting":"error"},"options":{"typeAware":false,"typeCheck":false}}'` + + +## `vpt cp settings-plugin.mjs packages/a/plugin.mjs` + + +## `vpt cp settings-plugin.mjs packages/b/plugin.mjs` + + +## `vpt write-file packages/a/index.js 'export const value = 1; +'` + + +## `vpt write-file packages/b/index.js 'export const value = 1; +'` + + +## `node create-config-links.mjs` + + +## `cd packages/a && vp lint --threads=1 -c .oxlintrc.json index.js` + +Oxlint resolves the relative root plugin from the alias directory + +**Exit code:** 1 + +``` + + × fixture(require-setting): The fixture setting must be enabled. + ╭─[index.js:1:1] + 1 │ export const value = 1; + · ──────────────────────── + ╰──── + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/b && vp lint --threads=1 -c .oxlintrc.json index.js` + +Each workspace uses its own relative plugin + +**Exit code:** 1 + +``` + + × fixture(require-setting): The fixture setting must be enabled. + ╭─[index.js:1:1] + 1 │ export const value = 1; + · ──────────────────────── + ╰──── + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +◆ ✔ Merged .oxlintrc.json into vite.config.mjs +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt print-file packages/a/vite.config.mjs` + +The root plugin path stays relative to the workspace + +``` +// vp migrate: merged packages%2Fa%2F.oxlintrc.json (2e3968680d769859f24911af4896427f). +export default { + lint: { + jsPlugins: [ + { + name: "fixture", + specifier: "./plugin.mjs", + }, + { + name: "vite-plus", + specifier: "vite-plus/oxlint-plugin", + }, + ], + rules: { + "fixture/require-setting": "error", + "vite-plus/prefer-vite-plus-imports": "error", + }, + options: { + typeAware: false, + typeCheck: false, + }, + }, +}; +``` + +## `cd packages/a && vp lint --threads=1 index.js` + +The first consumer keeps the original plugin diagnostic + +**Exit code:** 1 + +``` + + × fixture(require-setting): The fixture setting must be enabled. + ╭─[index.js:1:1] + 1 │ export const value = 1; + · ──────────────────────── + ╰──── + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/b && vp lint --threads=1 index.js` + +The second consumer keeps the original plugin diagnostic + +**Exit code:** 1 + +``` + + × fixture(require-setting): The fixture setting must be enabled. + ╭─[index.js:1:1] + 1 │ export const value = 1; + · ──────────────────────── + ╰──── + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_symlink_retry.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_symlink_retry.md new file mode 100644 index 0000000000..c4f9530c43 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_symlink_retry.md @@ -0,0 +1,191 @@ +# migration_oxlint_extends_workspace_symlink_retry + +## `vpt rm packages/a/.oxlintrc.json` + + +## `vpt rm packages/b/.oxlintrc.json` + + +## `vpt write-file shared/lint.json '{"extends":["./base.json"],"rules":{"no-console":"error"},"options":{"typeAware":false,"typeCheck":false}}'` + + +## `vpt write-file shared/base.json {}` + + +## `vpt write-file packages/a/index.js 'console.log("hello");'` + + +## `vpt write-file packages/b/index.js 'console.log("hello");'` + + +## `node create-config-links.mjs --with-base` + + +## `vpt write-file packages/b/vite.config.mjs 'export { default } from '\''./another-config.mjs'\'';'` + + +## `cd packages/a && vp lint --threads=1 -c .oxlintrc.json index.js` + +The original symlinked consumer reports no-console + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/b && vp lint --threads=1 -c .oxlintrc.json index.js` + +The second alias uses the same rules + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +◆ ✔ Merged .oxlintrc.json into vite.config.mjs +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 1 config update applied +! Warnings: + - Could not migrate packages/b/.oxlintrc.json: Error: Shared Oxlint configs require a Vite config with an ESM default export. The original config was preserved; use vp lint -c packages/b/.oxlintrc.json until it is migrated manually. +``` + +## `vpt stat-file packages/a/.oxlintrc.json --assert missing` + +The successful alias is removed independently + +``` +packages/a/.oxlintrc.json: missing +``` + +## `vpt stat-file packages/b/.oxlintrc.json --assert symlink` + +The failed consumer retains its own alias + +``` +packages/b/.oxlintrc.json: symlink +``` + +## `cd packages/a && vp lint --threads=1 index.js` + +The migrated consumer keeps the same diagnostic + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/b && vp lint --threads=1 -c .oxlintrc.json index.js` + +The preserved consumer keeps a working JSON fallback + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vpt replace-file-content packages/b/vite.config.mjs 'export { default } from "./another-config.mjs";' 'export default {};'` + +Fix the unsupported export while retaining the workspace's pending marker + +``` +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +● Formatting code... +● Code formatted +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite → +• 1 config update applied +``` + +## `vpt stat-file packages/b/.oxlintrc.json --assert missing` + +The repaired alias is removed after its own migration completes + +``` +packages/b/.oxlintrc.json: missing +``` + +## `vpt stat-file shared/lint.json --assert file` + +Unlinking both consumers preserves the shared source + +``` +shared/lint.json: file +``` + +## `cd packages/b && vp lint --threads=1 index.js` + +The recovered consumer keeps the original diagnostic + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_symlinks.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_symlinks.md new file mode 100644 index 0000000000..50b03e98d5 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/snapshots/migration_oxlint_extends_workspace_symlinks.md @@ -0,0 +1,195 @@ +# migration_oxlint_extends_workspace_symlinks + +## `vpt rm packages/a/.oxlintrc.json` + + +## `vpt rm packages/b/.oxlintrc.json` + + +## `vpt json-edit .oxlintrc.json rules.no-console '"off"'` + + +## `vpt write-file shared/lint.json '{"rules":{"no-console":"error"},"options":{"typeAware":false,"typeCheck":false}}'` + + +## `vpt write-file packages/a/index.js 'console.log("hello");'` + + +## `vpt write-file packages/b/index.js 'console.log("hello");'` + + +## `node create-config-links.mjs` + + +## `vpt stat-file packages/a/.oxlintrc.json --assert symlink` + +``` +packages/a/.oxlintrc.json: symlink +``` + +## `vpt stat-file packages/b/.oxlintrc.json --assert symlink` + +``` +packages/b/.oxlintrc.json: symlink +``` + +## `cd packages/a && vp lint --threads=1 -c .oxlintrc.json index.js` + +The first symlinked consumer reports no-console before migration + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/b && vp lint --threads=1 -c .oxlintrc.json index.js` + +The second symlinked consumer reports the same error before migration + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `vp migrate --no-interactive --no-agent --no-editor --no-hooks` + +``` +VITE+ - The Unified Toolchain for the Web + +▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly. +◆ ✔ Merged .oxlintrc.json into vite.config.mjs +● Formatting code... +● Code formatted +◇ Migrated . to Vite+ +• Node pnpm +✓ Dependencies installed in +• 2 config updates applied +``` + +## `vpt print-file packages/a/vite.config.mjs` + +Each consumer receives its own lint block + +``` +// vp migrate: merged packages%2Fa%2F.oxlintrc.json (c8b042efd6403025ed6a8239c29b3114). +export default { + lint: { + rules: { + "no-console": "error", + "vite-plus/prefer-vite-plus-imports": "error", + }, + options: { + typeAware: false, + typeCheck: false, + }, + jsPlugins: [ + { + name: "vite-plus", + specifier: "vite-plus/oxlint-plugin", + }, + ], + }, +}; +``` + +## `vpt print-file packages/b/vite.config.mjs` + +``` +// vp migrate: merged packages%2Fb%2F.oxlintrc.json (b9baaacbd5e6b450f2ea1acce04edac9). +export default { + lint: { + rules: { + "no-console": "error", + "vite-plus/prefer-vite-plus-imports": "error", + }, + options: { + typeAware: false, + typeCheck: false, + }, + jsPlugins: [ + { + name: "vite-plus", + specifier: "vite-plus/oxlint-plugin", + }, + ], + }, +}; +``` + +## `vpt stat-file packages/a/.oxlintrc.json --assert missing` + +``` +packages/a/.oxlintrc.json: missing +``` + +## `vpt stat-file packages/b/.oxlintrc.json --assert missing` + +``` +packages/b/.oxlintrc.json: missing +``` + +## `vpt print-file shared/lint.json` + +Removing workspace symlinks preserves their shared JSON target + +``` +{ "rules": { "no-console": "error" }, "options": { "typeAware": false, "typeCheck": false } } +``` + +## `cd packages/a && vp lint --threads=1 index.js` + +The first migrated consumer keeps the same diagnostic and exit code + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` + +## `cd packages/b && vp lint --threads=1 index.js` + +The second migrated consumer keeps the same diagnostic and exit code + +**Exit code:** 1 + +``` + + × eslint(no-console): Unexpected console statement. + ╭─[index.js:1:1] + 1 │ console.log("hello"); + · ─────────── + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in on 1 file with rules using threads. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/vite.config.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/vite.config.mjs new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlint_extends_workspace/vite.config.mjs @@ -0,0 +1 @@ +export default {}; diff --git a/docs/guide/migrate-rules.md b/docs/guide/migrate-rules.md index 83a5a95dab..7f46795dcf 100644 --- a/docs/guide/migrate-rules.md +++ b/docs/guide/migrate-rules.md @@ -23,6 +23,26 @@ On a project that already depends on `vite-plus`, `vp migrate` performs an upgra When a default upgrade skips setup actions that would apply, it prints a hint to run `vp migrate --full`. Fresh (non Vite+) projects always run the full migration. +## Oxlint JSON Inheritance + +When `.oxlintrc.json` or `.oxlintrc.jsonc` uses `extends`, `vp migrate` resolves each file path relative to the config that declares it. It creates shared TypeScript configs and imports them through `lint.extends`, including nested inheritance. Oxlint applies the existing merge order, rule severities, and overrides. Plain `vp lint` uses the migrated rules without a `-c` flag. + +Migration assumes ESM projects. Shared configs become sibling `.ts` files. Imports from JavaScript and TypeScript configs use `.js` specifiers, which Vite and TypeScript resolve to the generated source files without changing compiler settings. File-level TypeScript pragmas stay before generated imports. Workspace consumers reuse the same generated modules, including the workspace that owns a shared config. Migration defaults stay in each workspace's local `lint` block. Edit these TypeScript files to update shared settings. Original shared JSON and JSONC files remain available to other consumers; edits to those originals do not update the migrated configs. + +JS-plugin declarations stay in their original config or override. Relative plugin paths become absolute paths at runtime through `fileURLToPath(new URL(..., import.meta.url))`. Explicit inherited `typeAware` and `typeCheck` settings take precedence over migration defaults. + +Cleanup removes each completed workspace symlink independently. Shared aliases that remain on disk keep their targets and dependencies available. + +A shared config's owner keeps `env`, `globals`, `settings`, and `ignorePatterns` at the top level of its `lint` block through references to the shared module. Edits to these fields apply to the owner. Other consumers do not inherit them. + +Generated modules include a source marker. A later migration attempt reuses these modules without replacing user edits, so a preserved consumer can migrate after its conflict is resolved. Existing modules without a matching marker remain conflicts. + +Failed merges and existing `lint` blocks record a pending marker in the Vite config, so an isolated failure can be retried after the conflict is resolved. A successful merge removes this marker. Completed workspace merges record a completion marker, including configs merged inline. After fixing an unreadable config, run `vp migrate` again to migrate the remaining consumers and remove JSON files that are no longer needed. + +Shared config imports use literal paths so Vite and TypeScript can resolve filenames containing `#` or `%`. A relative import path containing `?` or a literal backslash requires a manual migration: Vite treats `?` as a query delimiter, and TypeScript treats backslashes as path separators. Migration preserves the JSON config and reports a warning in these cases. + +Missing files, invalid JSON, inheritance cycles, conflicting output files, and package-local plugins that resolve differently from the Vite config directory produce a warning and leave the original config in place. Configs outside the project or workspace, configs inside `node_modules`, CommonJS Vite configs, and an existing `lint` block require a manual merge. Workspace JSON removal waits until all migration outcomes are known. Preserved JSON consumers retain their transitive dependencies, including configs whose owners migrated successfully. If an inheritance graph cannot be read, workspace JSON files are kept and their owners use shared modules, because the unreadable consumer can reference them. `vp lint` continues to reject string paths in `lint.extends`; path resolution happens only during migration. + ## Pack Configuration `vp migrate` updates static `pack` objects in `vite.config.*` and exported objects in `tsdown.config.*` for [tsdown 0.23](https://github.com/rolldown/tsdown/releases/tag/v0.23.0). This also runs on existing Vite+ projects without `--full`, including workspace packages. Arrays and direct objects returned by `defineConfig` callbacks are supported. JSON tsdown configs receive the same updates after they merge into `vite.config.ts`. diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index 355f7838e9..bca384dd70 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -58,6 +58,7 @@ import { import { accent, formatDuration, muted, log, printHeader, success } from '../utils/terminal.ts'; import { detectWorkspace, + discoverWorkspacePackages, updatePackageJsonWithDeps, updateWorkspaceConfig, } from '../utils/workspace.ts'; @@ -1266,6 +1267,13 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h workspaceInfo.packageManager === PackageManager.pnpm ? createCatalogDependencyResolver(workspaceInfo.rootDir, workspaceInfo.packageManager) : undefined, + { + rootDir: workspaceInfo.rootDir, + packages: discoverWorkspacePackages( + [...workspaceInfo.workspacePatterns, projectDir], + workspaceInfo.rootDir, + ), + }, ); if (workspaceInfo.packageManager === PackageManager.pnpm) { ensurePnpmCreateCatalogEntries(workspaceInfo.rootDir, fullPath); diff --git a/packages/cli/src/migration/__tests__/migrator.spec.ts b/packages/cli/src/migration/__tests__/migrator.spec.ts index f8b1490549..bd868a769a 100644 --- a/packages/cli/src/migration/__tests__/migrator.spec.ts +++ b/packages/cli/src/migration/__tests__/migrator.spec.ts @@ -55,6 +55,7 @@ const { rewriteEslintPackageJson, collectInstalledPackageNames, sanitizeMigratedOxlintConfig, + sanitizeInheritedJsPlugins, detectIncompatibleEslintIntegration, hasExistingViteHooksPolicy, installGitHooks, @@ -1174,6 +1175,54 @@ describe('collectInstalledPackageNames', () => { }); describe('sanitizeMigratedOxlintConfig', () => { + it('removes missing generated plugins without discarding inherited or local plugin rules', () => { + const inherited = { jsPlugins: ['./base-plugin.mjs'] }; + const config: import('oxlint').OxlintConfig = { + extends: [inherited], + jsPlugins: ['eslint-plugin-missing', './local-plugin.mjs', 'eslint-plugin-installed/subpath'], + rules: { + 'missing/custom': 'error', + 'base/custom': 'warn', + 'local/custom': 'error', + 'installed/custom': 'error', + 'no-debugger': 'error', + }, + overrides: [ + { + files: ['**/*.test.js'], + jsPlugins: ['eslint-plugin-override-missing'], + rules: { + 'missing/custom': 'warn', + 'override-missing/custom': 'error', + 'base/custom': 'off', + }, + }, + { + files: ['**/*.local.js'], + jsPlugins: [{ name: 'missing', specifier: './replacement.mjs' }], + rules: { 'missing/custom': 'error' }, + }, + ], + }; + const report = createMigrationReport(); + + sanitizeInheritedJsPlugins(config, new Set(['eslint-plugin-installed']), os.tmpdir(), report); + + expect(config.extends).toEqual([inherited]); + expect(config.jsPlugins).toEqual(['./local-plugin.mjs', 'eslint-plugin-installed/subpath']); + expect(config.rules).toEqual({ + 'base/custom': 'warn', + 'local/custom': 'error', + 'installed/custom': 'error', + 'no-debugger': 'error', + }); + expect(config.overrides![0].jsPlugins).toEqual([]); + expect(config.overrides![0].rules).toEqual({ 'base/custom': 'off' }); + expect(config.overrides![1].rules).toEqual({ 'missing/custom': 'error' }); + expect(report.warnings).toHaveLength(1); + expect(report.warnings[0]).toContain('eslint-plugin-missing, eslint-plugin-override-missing'); + }); + it('removes unsupported rules', () => { const config: import('oxlint').OxlintConfig = { rules: { diff --git a/packages/cli/src/migration/__tests__/oxlint-extends.spec.ts b/packages/cli/src/migration/__tests__/oxlint-extends.spec.ts new file mode 100644 index 0000000000..4f083d1ef1 --- /dev/null +++ b/packages/cli/src/migration/__tests__/oxlint-extends.spec.ts @@ -0,0 +1,1165 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { loadConfigFromFile } from 'vite'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { PackageManager } from '../../types/index.ts'; +import { + injectLintTypeCheckDefaults, + mergeViteConfigFiles, + prepareOxlintConfigs, + resolveOxlintConfigExtends, + retryPreservedOxlintConfigs, + rewriteMonorepo, +} from '../migrator.ts'; +import { createMigrationReport } from '../report.ts'; + +describe('Oxlint JSON extends migration', () => { + let project: string; + + function write(filename: string, content: string | object) { + const filenamePath = path.join(project, filename); + fs.mkdirSync(path.dirname(filenamePath), { recursive: true }); + fs.writeFileSync(filenamePath, typeof content === 'string' ? content : JSON.stringify(content)); + return filenamePath; + } + + async function loadConfig(directory = project) { + const result = await loadConfigFromFile( + { command: 'build', mode: 'test' }, + path.join(directory, 'vite.config.mjs'), + directory, + 'silent', + ); + return result!.config; + } + + function createWorkspace(names = ['a', 'b']) { + write('package.json', { + name: 'root', + type: 'module', + private: true, + workspaces: ['packages/*'], + }); + for (const name of names) { + write(`packages/${name}/package.json`, { name, type: 'module' }); + write(`packages/${name}/vite.config.mjs`, 'export default {};'); + } + } + + function migrateWorkspace(names = ['a', 'b']) { + const report = createMigrationReport(); + rewriteMonorepo( + { + rootDir: project, + isMonorepo: true, + monorepoScope: '', + workspacePatterns: ['packages/*'], + parentDirs: ['packages'], + packageManager: PackageManager.npm, + packageManagerVersion: '11.0.0', + downloadPackageManager: { + name: PackageManager.npm, + installDir: project, + binPrefix: project, + packageName: 'npm', + version: '11.0.0', + }, + packages: names.map((name) => ({ name, path: `packages/${name}` })), + }, + true, + true, + report, + ); + return report; + } + + beforeEach(() => { + project = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'vp-oxlint-extends-'))); + write('package.json', { name: 'extends-test', type: 'module' }); + write('vite.config.mjs', 'export default {};'); + }); + + afterEach(() => { + fs.rmSync(project, { recursive: true, force: true }); + }); + + it('resolves nested JSONC paths relative to each declaring file and retains inheritance order', async () => { + const base = write( + 'shared/base.jsonc', + `{ + // Inherited options must not be replaced by migration defaults. + "$schema": "./schema.json", + "options": { "typeAware": false, "typeCheck": false }, + "rules": { "no-debugger": "error" }, + }`, + ); + write('shared/nested/first.json', { + extends: ['../base.jsonc'], + rules: { 'no-console': 'warn' }, + overrides: [{ files: ['**/*.test.js'], rules: { 'no-debugger': 'off' } }], + }); + write('second.json', { rules: { 'no-console': 'error' } }); + write('.oxlintrc.json', { + extends: ['./shared/nested/first.json', './second.json'], + rules: { 'no-alert': 'off' }, + }); + + const report = createMigrationReport(); + mergeViteConfigFiles(project, true, report); + const configPath = path.join(project, 'vite.config.mjs'); + const config = await loadConfig(); + expect(config.lint).toMatchObject({ + extends: [ + { + extends: [ + { options: { typeAware: false, typeCheck: false }, rules: { 'no-debugger': 'error' } }, + ], + rules: { 'no-console': 'warn' }, + overrides: [{ files: ['**/*.test.js'], rules: { 'no-debugger': 'off' } }], + }, + { rules: { 'no-console': 'error' } }, + ], + options: {}, + rules: { 'no-alert': 'off' }, + }); + expect(JSON.stringify(config)).not.toContain('$schema'); + expect(fs.existsSync(path.join(project, '.oxlintrc.json'))).toBe(false); + expect(fs.readFileSync(base, 'utf8')).toContain('// Inherited options'); + expect(report.mergedConfigCount).toBe(1); + expect(report.warnings).toEqual([]); + + const migrated = fs.readFileSync(configPath, 'utf8'); + mergeViteConfigFiles(project, true, report); + injectLintTypeCheckDefaults(project, true, report); + expect(fs.readFileSync(configPath, 'utf8')).toBe(migrated); + expect(report.mergedConfigCount).toBe(1); + }); + + it('keeps shared module edits live through nested imports', async () => { + write('base.json', { rules: { 'no-debugger': 'warn' } }); + write('first.json', { extends: ['./base.json'] }); + write('second.json', { extends: ['./base.json'] }); + write('.oxlintrc.json', { extends: ['./first.json', './second.json'] }); + mergeViteConfigFiles(project, true); + + const modulePath = path.join(project, 'base.ts'); + expect(fs.readFileSync(path.join(project, 'first.ts'), 'utf8')).toContain('./base.js'); + expect(fs.readFileSync(path.join(project, 'second.ts'), 'utf8')).toContain('./base.js'); + fs.writeFileSync(modulePath, fs.readFileSync(modulePath, 'utf8').replace('"warn"', '"error"')); + const config = await loadConfig(); + for (const branch of config.lint!.extends!) { + expect(branch.extends![0].rules!['no-debugger']).toBe('error'); + } + }); + + it.each([false, true])( + 'sanitizes missing root plugins with a shared owner: %s', + async (shared) => { + write('base.json', { + jsPlugins: ['./local-plugin.mjs'], + rules: { 'base/custom': 'error' }, + }); + write('.oxlintrc.json', { + extends: ['./base.json'], + jsPlugins: ['eslint-plugin-vp-missing'], + rules: { 'vp-missing/custom': 'error', 'base/custom': 'warn' }, + }); + const report = createMigrationReport(); + if (shared) { + createWorkspace(['a']); + write('packages/a/.oxlintrc.json', { extends: ['../../.oxlintrc.json'] }); + const packagePath = path.join(project, 'packages/a'); + const packages = [{ name: 'a', path: 'packages/a' }]; + const prepared = prepareOxlintConfigs([project, packagePath]); + mergeViteConfigFiles(project, true, report, packages, project, prepared); + mergeViteConfigFiles(packagePath, true, report, packages, project, prepared); + } else { + mergeViteConfigFiles(project, true, report); + } + + expect(report.warnings).toHaveLength(1); + expect(report.warnings[0]).toContain('eslint-plugin-vp-missing'); + const { lint } = await loadConfig(); + const migrated = shared ? lint!.extends![0] : lint!; + expect(migrated.jsPlugins).not.toContain('eslint-plugin-vp-missing'); + expect(migrated.rules).not.toHaveProperty('vp-missing/custom'); + expect(migrated.rules).toHaveProperty('base/custom', 'warn'); + expect(migrated.extends![0].rules).toHaveProperty('base/custom', 'error'); + if (shared) { + expect((await loadConfig(path.join(project, 'packages/a'))).lint!.extends![0]).toEqual( + migrated, + ); + } + }, + ); + + it.each(['base.ts', 'base.js'])('preserves configs when %s already exists', (filename) => { + write('base.json', { rules: { 'no-debugger': 'warn' } }); + const original = 'export default { rules: { "no-debugger": "off" } };'; + write(filename, original); + write('.oxlintrc.json', { extends: ['./base.json'] }); + const report = createMigrationReport(); + mergeViteConfigFiles(project, true, report); + expect(fs.readFileSync(path.join(project, filename), 'utf8')).toBe(original); + expect(fs.existsSync(path.join(project, '.oxlintrc.json'))).toBe(true); + expect(fs.readFileSync(path.join(project, 'vite.config.mjs'), 'utf8')).toBe( + 'export default {};\n// vp migrate: pending .oxlintrc.json.\n', + ); + expect(report.warnings[0]).toContain('already exists'); + }); + + it('does not create partial modules when two JSON sources map to the same TS file', () => { + write('base.json', { rules: { 'no-debugger': 'warn' } }); + write('base.jsonc', { rules: { 'no-console': 'warn' } }); + write('.oxlintrc.json', { extends: ['./base.json', './base.jsonc'] }); + const report = createMigrationReport(); + mergeViteConfigFiles(project, true, report); + expect(report.warnings[0]).toContain('Multiple Oxlint configs'); + expect(fs.existsSync(path.join(project, 'base.ts'))).toBe(false); + expect(fs.existsSync(path.join(project, '.oxlintrc.json'))).toBe(true); + }); + + it('avoids import binding conflicts and preserves existing config content', async () => { + write( + 'vite.config.mjs', + 'const oxlintConfig1 = "original"; export default { custom: oxlintConfig1 };', + ); + write('base.json', { rules: { 'no-debugger': 'warn' } }); + write('.oxlintrc.json', { extends: ['./base.json'] }); + mergeViteConfigFiles(project, true); + expect(fs.readFileSync(path.join(project, 'vite.config.mjs'), 'utf8')).toContain( + 'import oxlintConfig2', + ); + expect(await loadConfig()).toHaveProperty('custom', 'original'); + }); + + it('allows a shared ancestor to occur in separate branches', () => { + write('base.json', { rules: { 'no-debugger': 'error' } }); + write('a.json', { extends: ['./base.json'] }); + write('b.json', { extends: ['./base.json'] }); + const root = write('.oxlintrc.json', { extends: ['./a.json', './b.json'] }); + expect(resolveOxlintConfigExtends(root).config.extends).toEqual([ + { extends: [{ rules: { 'no-debugger': 'error' } }] }, + { extends: [{ rules: { 'no-debugger': 'error' } }] }, + ]); + }); + + it('encodes URL characters in relative plugin paths', async () => { + write('base.json', { jsPlugins: ['./plugin #1%.mjs'] }); + write('.oxlintrc.json', { extends: ['./base.json'] }); + mergeViteConfigFiles(project, true); + expect((await loadConfig()).lint!.extends![0].jsPlugins).toEqual([ + path.join(project, 'plugin #1%.mjs'), + ]); + }); + + it.each(['#', '%'])('loads shared config imports with %s in their paths', async (character) => { + write(`shared${character}/base${character}.json`, { + rules: { 'no-debugger': 'error' }, + }); + write(`shared${character}/nested/first${character}.json`, { + extends: [`../base${character}.json`], + }); + write('.oxlintrc.json', { + extends: [`./shared${character}/nested/first${character}.json`], + }); + + const report = createMigrationReport(); + mergeViteConfigFiles(project, true, report); + + expect(report.warnings).toEqual([]); + const config = await loadConfig(); + expect(config.lint!.extends![0].extends![0].rules!['no-debugger']).toBe('error'); + }); + + it.skipIf(process.platform === 'win32').each(['?', '\\'])( + 'preserves JSON when an import resolver cannot handle a shared config path containing %s', + (character) => { + write(`shared${character}/base.json`, { rules: { 'no-debugger': 'error' } }); + write('first.json', { extends: [`./shared${character}/base.json`] }); + write('.oxlintrc.json', { extends: ['./first.json'] }); + const report = createMigrationReport(); + + mergeViteConfigFiles(project, true, report); + + expect(report.warnings[0]).toContain(`relative path contains ${JSON.stringify(character)}`); + expect(fs.existsSync(path.join(project, '.oxlintrc.json'))).toBe(true); + expect(fs.existsSync(path.join(project, 'first.ts'))).toBe(false); + expect(fs.existsSync(path.join(project, `shared${character}/base.ts`))).toBe(false); + expect(fs.readFileSync(path.join(project, 'vite.config.mjs'), 'utf8')).toBe( + 'export default {};\n// vp migrate: pending .oxlintrc.json.\n', + ); + }, + ); + + it('preserves external shared configs without writing outside the project', () => { + write('external/base.json', { rules: { 'no-debugger': 'warn' } }); + write('app/vite.config.mjs', 'export default {};'); + write('app/.oxlintrc.json', { extends: ['../external/base.json'] }); + const report = createMigrationReport(); + mergeViteConfigFiles(path.join(project, 'app'), true, report); + expect(report.warnings[0]).toContain('outside the project'); + expect(fs.existsSync(path.join(project, 'external/base.ts'))).toBe(false); + expect(fs.existsSync(path.join(project, 'app/.oxlintrc.json'))).toBe(true); + }); + + it('removes newly created modules if writing a module fails', () => { + write('base.json', { rules: { 'no-debugger': 'warn' } }); + write('.oxlintrc.json', { extends: ['./base.json'] }); + const report = createMigrationReport(); + const writeFile = fs.writeFileSync; + const spy = vi.spyOn(fs, 'writeFileSync').mockImplementation((file, data, options) => { + if (typeof file === 'number') { + throw new Error('module write failed'); + } + return writeFile(file, data, options); + }); + try { + mergeViteConfigFiles(project, true, report); + } finally { + spy.mockRestore(); + } + expect(report.warnings[0]).toContain('module write failed'); + expect(fs.existsSync(path.join(project, 'base.ts'))).toBe(false); + expect(fs.existsSync(path.join(project, '.oxlintrc.json'))).toBe(true); + expect(fs.readFileSync(path.join(project, 'vite.config.mjs'), 'utf8')).toBe( + 'export default {};\n// vp migrate: pending .oxlintrc.json.\n', + ); + }); + + it.each([false, true])( + 'shares live modules with their owning workspace (extends root: %s)', + async (extendsRoot) => { + createWorkspace(); + write('packages/a/.oxlintrc.json', { rules: { 'no-debugger': 'error' } }); + write('packages/b/.oxlintrc.json', { + extends: [extendsRoot ? '../../.oxlintrc.json' : '../a/.oxlintrc.json'], + }); + write('.oxlintrc.json', { + extends: [extendsRoot ? './packages/a/.oxlintrc.json' : './packages/b/.oxlintrc.json'], + }); + const report = migrateWorkspace(); + const modulePath = path.join(project, 'packages/a/.oxlintrc.ts'); + for (const severity of ['error', 'warn']) { + if (severity === 'warn') { + fs.writeFileSync( + modulePath, + fs.readFileSync(modulePath, 'utf8').replace('"error"', '"warn"'), + ); + } + const root = await loadConfig(); + const sibling = await loadConfig(path.join(project, 'packages/b')); + const owner = await loadConfig(path.join(project, 'packages/a')); + expect(root.lint!.extends![0].extends![0].rules!['no-debugger']).toBe(severity); + expect(sibling.lint!.extends![0].extends![0].rules!['no-debugger']).toBe(severity); + expect(owner.lint!.extends![0].rules!['no-debugger']).toBe(severity); + expect(owner.lint!.rules!['no-debugger']).toBeUndefined(); + expect(owner.lint!.options).toEqual({ typeAware: true, typeCheck: true }); + expect(owner.lint!.extends![0].options).toBeUndefined(); + } + for (const dir of ['', 'packages/a', 'packages/b']) { + expect(fs.existsSync(path.join(project, dir, '.oxlintrc.json'))).toBe(false); + } + expect(report.warnings).toEqual([]); + }, + ); + + it('keeps ignore patterns live at each shared owner without applying them to dependents', async () => { + createWorkspace(); + write( + 'vite.config.mjs', + 'const oxlintConfig1 = "original"; export default { custom: oxlintConfig1 };', + ); + write('.oxlintrc.json', { + ignorePatterns: ['generated/**'], + rules: { 'no-debugger': 'error' }, + }); + write('packages/a/.oxlintrc.json', { + extends: ['../../.oxlintrc.json'], + ignorePatterns: ['local/**'], + }); + write('packages/b/.oxlintrc.json', { extends: ['../a/.oxlintrc.json'] }); + + const report = migrateWorkspace(); + + expect(report.warnings).toEqual([]); + expect(fs.readFileSync(path.join(project, 'vite.config.mjs'), 'utf8')).toContain( + 'oxlintConfig2.ignorePatterns', + ); + for (const updated of [false, true]) { + if (updated) { + const rootModule = path.join(project, '.oxlintrc.ts'); + fs.writeFileSync( + rootModule, + fs.readFileSync(rootModule, 'utf8').replace('generated/**', 'built/**'), + ); + const packageModule = path.join(project, 'packages/a/.oxlintrc.ts'); + fs.writeFileSync( + packageModule, + fs.readFileSync(packageModule, 'utf8').replace('local/**', 'vendor/**'), + ); + } + const owner = await loadConfig(); + const intermediate = await loadConfig(path.join(project, 'packages/a')); + const dependent = await loadConfig(path.join(project, 'packages/b')); + expect(owner.lint!.ignorePatterns).toEqual([updated ? 'built/**' : 'generated/**']); + expect(intermediate.lint!.ignorePatterns).toEqual([updated ? 'vendor/**' : 'local/**']); + expect(dependent.lint!.ignorePatterns).toBeUndefined(); + } + }); + + it.each([false, true])( + 'migrates both symlinked workspace configs (shared target extends root: %s)', + async (extendsRoot) => { + createWorkspace(); + const root = write('.oxlintrc.json', { rules: { 'no-console': 'off' } }); + const shared = write('shared/lint.json', { + ...(extendsRoot ? { extends: [root] } : {}), + rules: { 'no-console': 'error' }, + options: { typeAware: false, typeCheck: false }, + }); + const original = fs.readFileSync(shared, 'utf8'); + for (const name of ['a', 'b']) { + fs.symlinkSync( + '../../shared/lint.json', + path.join(project, `packages/${name}/.oxlintrc.json`), + ); + } + + const report = migrateWorkspace(); + + expect(report.warnings).toEqual([]); + for (const name of ['a', 'b']) { + const directory = path.join(project, `packages/${name}`); + const { lint } = await loadConfig(directory); + expect(lint!.rules!['no-console']).toBe('error'); + expect(lint!.options).toEqual({ typeAware: false, typeCheck: false }); + expect(fs.existsSync(path.join(directory, '.oxlintrc.json'))).toBe(false); + if (extendsRoot) { + expect(lint!.extends![0].rules!['no-console']).toBe('off'); + } + } + expect(fs.readFileSync(shared, 'utf8')).toBe(original); + // Removing the links leaves their target on disk, so its dependencies must remain too. + expect(fs.existsSync(root)).toBe(extendsRoot); + if (extendsRoot) { + expect(resolveOxlintConfigExtends(shared).config.extends![0].rules!['no-console']).toBe( + 'off', + ); + } + }, + ); + + it('preserves a failed symlinked consumer after its sibling completes migration', async () => { + createWorkspace(); + const root = write('.oxlintrc.json', { rules: { 'no-console': 'off' } }); + write('shared/lint.json', { + extends: [root], + rules: { 'no-console': 'error' }, + }); + for (const name of ['a', 'b']) { + fs.symlinkSync( + '../../shared/lint.json', + path.join(project, `packages/${name}/.oxlintrc.json`), + ); + } + const original = "export { default } from './another-config.mjs';"; + const viteConfig = write('packages/b/vite.config.mjs', original); + + const report = migrateWorkspace(); + + expect(report.warnings).toHaveLength(1); + expect(report.warnings[0]).toContain('ESM default export'); + expect((await loadConfig(path.join(project, 'packages/a'))).lint!.rules!['no-console']).toBe( + 'error', + ); + expect(fs.existsSync(path.join(project, 'packages/a/.oxlintrc.json'))).toBe(false); + expect(fs.readFileSync(viteConfig, 'utf8')).toBe( + `${original}\n// vp migrate: pending .oxlintrc.json.\n`, + ); + const preserved = path.join(project, 'packages/b/.oxlintrc.json'); + expect(fs.lstatSync(preserved).isSymbolicLink()).toBe(true); + expect(resolveOxlintConfigExtends(preserved).config.extends![0].rules!['no-console']).toBe( + 'off', + ); + + fs.writeFileSync( + viteConfig, + fs.readFileSync(viteConfig, 'utf8').replace(original, 'export default {};'), + ); + const retryReport = createMigrationReport(); + expect( + retryPreservedOxlintConfigs( + project, + true, + retryReport, + ['a', 'b'].map((name) => ({ name, path: `packages/${name}` })), + ), + ).toBe(true); + expect(retryReport.warnings).toEqual([]); + expect(fs.existsSync(preserved)).toBe(false); + expect(fs.existsSync(path.join(project, 'shared/lint.json'))).toBe(true); + }); + + it.each(['extends', 'symlink'])( + 'retains an alias referenced through a preserved %s', + (reference) => { + createWorkspace(); + write('shared/lint.json', { rules: { 'no-console': 'error' } }); + const alias = path.join(project, 'packages/a/.oxlintrc.json'); + const consumer = path.join(project, 'packages/b/.oxlintrc.json'); + fs.symlinkSync('../../shared/lint.json', alias); + if (reference === 'symlink') { + fs.symlinkSync('../a/.oxlintrc.json', consumer); + } else { + write('packages/b/.oxlintrc.json', { extends: ['../a/.oxlintrc.json'] }); + } + write('packages/b/vite.config.mjs', "export { default } from './another-config.mjs';"); + + expect(migrateWorkspace().warnings).toHaveLength(1); + + expect(fs.lstatSync(alias).isSymbolicLink()).toBe(true); + const fallback = resolveOxlintConfigExtends(consumer).config; + const lint = reference === 'symlink' ? fallback : fallback.extends![0]; + expect(lint.rules!['no-console']).toBe('error'); + }, + ); + + it('keeps relative plugins in symlinked roots anchored to their workspace', async () => { + createWorkspace(['a']); + write('shared/lint.json', { jsPlugins: ['./plugin.mjs'] }); + fs.symlinkSync('../../shared/lint.json', path.join(project, 'packages/a/.oxlintrc.json')); + + expect(migrateWorkspace(['a']).warnings).toEqual([]); + + const { lint } = await loadConfig(path.join(project, 'packages/a')); + expect(lint!.jsPlugins).toContain('./plugin.mjs'); + }); + + it('resolves each symlinked root inheritance graph from its own directory', async () => { + createWorkspace(); + write('shared/lint.json', { extends: ['./base.json'] }); + write('shared/base.json', { rules: { 'no-console': 'off' } }); + for (const [name, severity] of [ + ['a', 'error'], + ['b', 'warn'], + ]) { + write(`packages/${name}/base.json`, { rules: { 'no-console': severity } }); + fs.symlinkSync( + '../../shared/lint.json', + path.join(project, `packages/${name}/.oxlintrc.json`), + ); + } + + expect(migrateWorkspace().warnings).toEqual([]); + + expect( + (await loadConfig(path.join(project, 'packages/a'))).lint!.extends![0].rules!['no-console'], + ).toBe('error'); + expect( + (await loadConfig(path.join(project, 'packages/b'))).lint!.extends![0].rules!['no-console'], + ).toBe('warn'); + }); + + it('resolves nested symlink dependencies and plugins from their declared directory', async () => { + write('shared/lint.json', { extends: ['./base.json'], jsPlugins: ['./plugin.mjs'] }); + write('shared/base.json', { rules: { 'no-console': 'off' } }); + write('base.json', { rules: { 'no-console': 'error' } }); + fs.symlinkSync('./shared/lint.json', path.join(project, 'alias.json')); + write('.oxlintrc.json', { extends: ['./alias.json'] }); + + const report = createMigrationReport(); + mergeViteConfigFiles(project, true, report); + + expect(report.warnings).toEqual([]); + const { lint } = await loadConfig(); + expect(lint!.extends![0].extends![0].rules!['no-console']).toBe('error'); + expect(lint!.extends![0].jsPlugins).toEqual([path.join(project, 'plugin.mjs')]); + }); + + it('resolves parent dependencies through directory links with native filesystem semantics', async () => { + write('configs/nested/base.json', { extends: ['../common.json'] }); + write('configs/common.json', { rules: { 'no-console': 'error' } }); + write('common.json', { rules: { 'no-console': 'off' } }); + fs.symlinkSync( + path.join(project, 'configs/nested'), + path.join(project, 'aliases'), + process.platform === 'win32' ? 'junction' : 'dir', + ); + write('.oxlintrc.json', { extends: ['./aliases/base.json'] }); + + const report = createMigrationReport(); + mergeViteConfigFiles(project, true, report); + + expect(report.warnings).toEqual([]); + const { lint } = await loadConfig(); + expect(lint!.extends![0].extends![0].rules!['no-console']).toBe( + process.platform === 'win32' ? 'off' : 'error', + ); + }); + + it('does not reuse a shared module resolved from a different symlink context', async () => { + createWorkspace(); + write('shared/lint.json', { extends: ['./base.json'] }); + write('shared/base.json', { rules: { 'no-console': 'off' } }); + for (const [name, severity] of [ + ['a', 'error'], + ['b', 'warn'], + ]) { + write(`packages/${name}/base.json`, { rules: { 'no-console': severity } }); + fs.symlinkSync('../../shared/lint.json', path.join(project, `packages/${name}/alias.json`)); + write(`packages/${name}/.oxlintrc.json`, { extends: ['./alias.json'] }); + } + + const report = migrateWorkspace(); + + expect(report.warnings).toHaveLength(1); + expect(report.warnings[0]).toContain('already exists'); + expect( + (await loadConfig(path.join(project, 'packages/a'))).lint!.extends![0].extends![0].rules![ + 'no-console' + ], + ).toBe('error'); + expect( + resolveOxlintConfigExtends(path.join(project, 'packages/b/.oxlintrc.json')).config.extends![0] + .extends![0].rules!['no-console'], + ).toBe('warn'); + }); + + it('shares module edits with an owner reached through a symlink alias', async () => { + createWorkspace(['a']); + write('.oxlintrc.json', { rules: { 'no-console': 'warn' } }); + fs.symlinkSync('../../.oxlintrc.json', path.join(project, 'packages/a/alias.json')); + write('packages/a/.oxlintrc.json', { extends: ['./alias.json'] }); + + expect(migrateWorkspace(['a']).warnings).toEqual([]); + + expect(fs.existsSync(path.join(project, '.oxlintrc.json'))).toBe(true); + expect(fs.realpathSync(path.join(project, 'packages/a/alias.json'))).toBe( + path.join(project, '.oxlintrc.json'), + ); + const modulePath = path.join(project, '.oxlintrc.ts'); + fs.writeFileSync(modulePath, fs.readFileSync(modulePath, 'utf8').replace('"warn"', '"error"')); + expect((await loadConfig()).lint!.extends![0].rules!['no-console']).toBe('error'); + expect( + (await loadConfig(path.join(project, 'packages/a'))).lint!.extends![0].rules!['no-console'], + ).toBe('error'); + }); + + it.skipIf(process.platform === 'win32')( + 'preserves literal backslashes in plugin filenames', + async () => { + write('base.json', { jsPlugins: ['./plugin\\name.mjs'] }); + write('.oxlintrc.json', { extends: ['./base.json'] }); + + mergeViteConfigFiles(project, true); + + expect((await loadConfig()).lint!.extends![0].jsPlugins).toEqual([ + path.join(project, 'plugin\\name.mjs'), + ]); + }, + ); + + it('retains transitive JSON dependencies of a workspace whose module output conflicts', async () => { + createWorkspace(['a', 'b', 'c']); + const root = write('.oxlintrc.json', { rules: { 'no-debugger': 'error' } }); + const shared = write('packages/a/.oxlintrc.json', { extends: ['../../.oxlintrc.json'] }); + const consumer = write('packages/b/.oxlintrc.json', { + extends: ['../a/.oxlintrc.json', './extra.json'], + }); + write('packages/b/extra.json', {}); + write('packages/b/extra.ts', 'export default {};'); + write('packages/c/.oxlintrc.json', { rules: { 'no-console': 'warn' } }); + const original = [root, shared, consumer].map((file) => fs.readFileSync(file, 'utf8')); + + const report = migrateWorkspace(['a', 'b', 'c']); + + expect(report.warnings).toHaveLength(1); + expect(report.warnings[0]).toContain('already exists'); + expect([root, shared, consumer].map((file) => fs.readFileSync(file, 'utf8'))).toEqual(original); + const fallback = resolveOxlintConfigExtends(consumer); + expect(fallback.config.extends![0].extends![0].rules!['no-debugger']).toBe('error'); + expect((await loadConfig()).lint!.extends![0].rules!['no-debugger']).toBe('error'); + expect((await loadConfig(path.join(project, 'packages/a'))).lint).toBeDefined(); + expect((await loadConfig(path.join(project, 'packages/b'))).lint).toBeUndefined(); + expect(fs.existsSync(path.join(project, 'packages/c/.oxlintrc.json'))).toBe(false); + expect(fs.readFileSync(path.join(project, 'packages/b/extra.ts'), 'utf8')).toBe( + 'export default {};', + ); + }); + + it('reuses edited modules on a retry after another workspace could not be merged', async () => { + createWorkspace(); + write('.oxlintrc.json', { rules: { 'no-debugger': 'warn' } }); + write('packages/a/.oxlintrc.json', { extends: ['../../.oxlintrc.json'] }); + write('packages/b/.oxlintrc.json', { extends: ['../a/.oxlintrc.json'] }); + const original = "export { default } from './other.mjs';"; + const viteConfig = write('packages/b/vite.config.mjs', original); + expect(migrateWorkspace().warnings).toHaveLength(1); + + const modulePath = path.join(project, '.oxlintrc.ts'); + const edited = fs.readFileSync(modulePath, 'utf8').replace('"warn"', '"error"'); + fs.writeFileSync(modulePath, edited); + fs.writeFileSync( + viteConfig, + fs.readFileSync(viteConfig, 'utf8').replace(original, 'export default {};'), + ); + + const report = createMigrationReport(); + expect( + retryPreservedOxlintConfigs( + project, + true, + report, + ['a', 'b'].map((name) => ({ name, path: `packages/${name}` })), + ), + ).toBe(true); + + expect(report.warnings).toEqual([]); + expect(fs.existsSync(path.join(project, 'packages/b/.oxlintrc.json'))).toBe(false); + expect(fs.existsSync(path.join(project, '.oxlintrc.json'))).toBe(false); + expect(fs.existsSync(path.join(project, 'packages/a/.oxlintrc.json'))).toBe(false); + expect(fs.readFileSync(modulePath, 'utf8')).toBe(edited); + const { lint } = await loadConfig(path.join(project, 'packages/b')); + expect(lint!.extends![0].extends![0].rules!['no-debugger']).toBe('error'); + }); + + it.each(['lint block', 'JSON config'])( + 'retains an owner whose %s changed after migration', + (changed) => { + createWorkspace(['a']); + write('.oxlintrc.json', { rules: { 'no-debugger': 'warn' } }); + write('packages/a/.oxlintrc.json', { extends: ['../../.oxlintrc.json'] }); + write('packages/a/vite.config.mjs', "export { default } from './another.mjs';"); + expect(migrateWorkspace(['a']).warnings).toHaveLength(1); + fs.unlinkSync(path.join(project, 'packages/a/.oxlintrc.json')); + if (changed === 'lint block') { + write('vite.config.mjs', 'export default { lint: { rules: { "no-debugger": "off" } } };'); + } else { + write('.oxlintrc.json', { rules: { 'no-debugger': 'error' } }); + } + + expect(retryPreservedOxlintConfigs(project, true)).toBe(false); + + expect(fs.existsSync(path.join(project, '.oxlintrc.json'))).toBe(true); + }, + ); + + it('does not retry unrelated JSON configs in an existing Vite+ project', () => { + write('base.json', { rules: { 'no-debugger': 'error' } }); + write('.oxlintrc.json', { extends: ['./base.json'] }); + const report = createMigrationReport(); + + expect(retryPreservedOxlintConfigs(project, true, report)).toBe(false); + expect(report.warnings).toEqual([]); + expect(fs.existsSync(path.join(project, '.oxlintrc.json'))).toBe(true); + expect(fs.existsSync(path.join(project, 'base.ts'))).toBe(false); + expect(fs.readFileSync(path.join(project, 'vite.config.mjs'), 'utf8')).toBe( + 'export default {};', + ); + }); + + it('does not retry a new workspace config that references a previously generated module', () => { + write('base.json', { rules: { 'no-debugger': 'error' } }); + write('.oxlintrc.json', { extends: ['./base.json'] }); + mergeViteConfigFiles(project, true); + createWorkspace(['a']); + const source = write('packages/a/.oxlintrc.json', { extends: ['../../base.json'] }); + const original = fs.readFileSync(source, 'utf8'); + const report = createMigrationReport(); + + expect( + retryPreservedOxlintConfigs(project, true, report, [{ name: 'a', path: 'packages/a' }]), + ).toBe(false); + + expect(report.warnings).toEqual([]); + expect(fs.readFileSync(source, 'utf8')).toBe(original); + expect(fs.readFileSync(path.join(project, 'packages/a/vite.config.mjs'), 'utf8')).toBe( + 'export default {};', + ); + expect(fs.existsSync(path.join(project, 'base.ts'))).toBe(true); + }); + + it('resumes completed workspace merges after an unreadable graph is repaired', async () => { + createWorkspace(); + write('packages/a/.oxlintrc.json', { rules: { 'no-debugger': 'error' } }); + write('packages/b/.oxlintrc.json', { extends: ['./missing.json'] }); + expect(migrateWorkspace().warnings).toHaveLength(1); + expect(fs.existsSync(path.join(project, 'packages/a/.oxlintrc.json'))).toBe(true); + expect(fs.existsSync(path.join(project, 'packages/a/.oxlintrc.ts'))).toBe(true); + write('packages/b/missing.json', { rules: { 'no-console': 'warn' } }); + + const report = createMigrationReport(); + expect( + retryPreservedOxlintConfigs( + project, + true, + report, + ['b', 'a'].map((name) => ({ name, path: `packages/${name}` })), + ), + ).toBe(true); + + expect(report.warnings).toEqual([]); + for (const name of ['a', 'b']) { + expect(fs.existsSync(path.join(project, `packages/${name}/.oxlintrc.json`))).toBe(false); + } + expect( + (await loadConfig(path.join(project, 'packages/a'))).lint!.extends![0].rules!['no-debugger'], + ).toBe('error'); + expect( + (await loadConfig(path.join(project, 'packages/b'))).lint!.extends![0].rules!['no-console'], + ).toBe('warn'); + }); + + it.each(['missing source', 'output conflict'])( + 'retries an isolated config after repairing a %s', + async (failure) => { + write('.oxlintrc.json', { extends: ['./base.json'] }); + if (failure === 'output conflict') { + write('base.json', { rules: { 'no-debugger': 'error' } }); + write('base.ts', 'export default {};'); + } + const firstReport = createMigrationReport(); + mergeViteConfigFiles(project, true, firstReport); + expect(firstReport.warnings).toHaveLength(1); + if (failure === 'missing source') { + const pendingReport = createMigrationReport(); + expect(retryPreservedOxlintConfigs(project, true, pendingReport)).toBe(false); + expect(pendingReport.warnings).toHaveLength(1); + write('base.json', { rules: { 'no-debugger': 'error' } }); + } else { + fs.unlinkSync(path.join(project, 'base.ts')); + } + + const report = createMigrationReport(); + expect(retryPreservedOxlintConfigs(project, true, report)).toBe(true); + + expect(report.warnings).toEqual([]); + expect(fs.existsSync(path.join(project, '.oxlintrc.json'))).toBe(false); + expect(fs.readFileSync(path.join(project, 'vite.config.mjs'), 'utf8')).not.toContain( + 'vp migrate: pending', + ); + expect((await loadConfig()).lint!.extends![0].rules!['no-debugger']).toBe('error'); + }, + ); + + it('keeps newly discovered shared owners live after repairing an unreadable graph', async () => { + createWorkspace(['a']); + write('.oxlintrc.json', { rules: { 'no-debugger': 'warn' } }); + write('packages/a/.oxlintrc.json', { extends: ['./missing.json'] }); + expect(migrateWorkspace(['a']).warnings).toHaveLength(1); + write('packages/a/missing.json', { extends: ['../../.oxlintrc.json'] }); + + const report = createMigrationReport(); + expect( + retryPreservedOxlintConfigs(project, true, report, [{ name: 'a', path: 'packages/a' }]), + ).toBe(true); + expect(report.warnings).toEqual([]); + const modulePath = path.join(project, '.oxlintrc.ts'); + fs.writeFileSync(modulePath, fs.readFileSync(modulePath, 'utf8').replace('"warn"', '"error"')); + expect((await loadConfig()).lint!.extends![0].rules!['no-debugger']).toBe('error'); + expect( + (await loadConfig(path.join(project, 'packages/a'))).lint!.extends![0].extends![0].rules![ + 'no-debugger' + ], + ).toBe('error'); + }); + + it('does not reuse a generated module for a different JSON source', () => { + write('base.json', { rules: { 'no-debugger': 'warn' } }); + write('.oxlintrc.json', { extends: ['./base.json'] }); + mergeViteConfigFiles(project, true); + const original = fs.readFileSync(path.join(project, 'base.ts'), 'utf8'); + write('base.jsonc', { rules: { 'no-debugger': 'error' } }); + write('vite.config.mjs', 'export default {};'); + write('.oxlintrc.json', { extends: ['./base.jsonc'] }); + + const report = createMigrationReport(); + mergeViteConfigFiles(project, true, report); + + expect(report.warnings[0]).toContain('already exists'); + expect(fs.readFileSync(path.join(project, 'base.ts'), 'utf8')).toBe(original); + expect(fs.existsSync(path.join(project, '.oxlintrc.json'))).toBe(true); + }); + + it('keeps owner env, globals, and settings live without applying them to dependents', async () => { + createWorkspace(['a']); + write('.oxlintrc.json', { + env: { browser: true }, + globals: { fixtureGlobal: 'readonly' }, + settings: { fixture: { enabled: true } }, + }); + write('packages/a/.oxlintrc.json', { extends: ['../../.oxlintrc.json'] }); + expect(migrateWorkspace(['a']).warnings).toEqual([]); + + for (const enabled of [true, false]) { + if (!enabled) { + const modulePath = path.join(project, '.oxlintrc.ts'); + fs.writeFileSync( + modulePath, + fs + .readFileSync(modulePath, 'utf8') + .replace('"browser": true', '"browser": false') + .replace('"readonly"', '"off"') + .replace('"enabled": true', '"enabled": false'), + ); + } + const owner = await loadConfig(); + const dependent = await loadConfig(path.join(project, 'packages/a')); + expect(owner.lint!.env).toEqual({ browser: enabled }); + expect(owner.lint!.globals).toEqual({ fixtureGlobal: enabled ? 'readonly' : 'off' }); + expect(owner.lint!.settings).toEqual({ fixture: { enabled } }); + expect(dependent.lint!.env).toBeUndefined(); + expect(dependent.lint!.globals).toBeUndefined(); + expect(dependent.lint!.settings).toBeUndefined(); + } + }); + + it('retains dependencies of shared JSON files even when all workspace consumers migrate', () => { + createWorkspace(['a']); + write('.oxlintrc.json', { rules: { 'no-debugger': 'error' } }); + const shared = write('shared.json', { extends: ['./.oxlintrc.json'] }); + write('packages/a/.oxlintrc.json', { extends: ['../../shared.json'] }); + + const report = migrateWorkspace(['a']); + + expect(report.warnings).toEqual([]); + expect(resolveOxlintConfigExtends(shared).config.extends![0].rules!['no-debugger']).toBe( + 'error', + ); + expect(fs.existsSync(path.join(project, 'packages/a/.oxlintrc.json'))).toBe(false); + }); + + it('preserves JSON candidates when a failed graph leaves its dependencies unknown', () => { + createWorkspace(['a']); + const root = write('.oxlintrc.json', { rules: { 'no-debugger': 'error' } }); + write('packages/a/.oxlintrc.json', { extends: ['./missing.json', '../../.oxlintrc.json'] }); + + const report = migrateWorkspace(['a']); + + expect(report.warnings).toHaveLength(1); + expect(fs.existsSync(root)).toBe(true); + }); + + it('retains a bare plugin when it resolves to the same package from both directories', () => { + write('node_modules/eslint-plugin-test/package.json', { + name: 'eslint-plugin-test', + main: 'index.js', + }); + write('node_modules/eslint-plugin-test/index.js', 'module.exports = {};'); + write('shared/base.json', { + jsPlugins: ['eslint-plugin-test'], + rules: { 'test/custom': 'error' }, + }); + const configPath = write('.oxlintrc.json', { extends: ['./shared/base.json'] }); + + expect(resolveOxlintConfigExtends(configPath).config.extends?.[0]).toEqual({ + jsPlugins: ['eslint-plugin-test'], + rules: { 'test/custom': 'error' }, + }); + }); + + it('preserves configs whose inherited package-local plugin would resolve differently', () => { + write('shared/node_modules/eslint-plugin-test/package.json', { + name: 'eslint-plugin-test', + main: 'index.js', + }); + write('shared/node_modules/eslint-plugin-test/index.js', 'module.exports = {};'); + write('shared/base.json', { jsPlugins: ['eslint-plugin-test'] }); + const source = write('.oxlintrc.json', { extends: ['./shared/base.json'] }); + const original = fs.readFileSync(source, 'utf8'); + const report = createMigrationReport(); + + mergeViteConfigFiles(project, true, report); + + expect(fs.readFileSync(source, 'utf8')).toBe(original); + expect(report.mergedConfigCount).toBe(0); + expect(report.warnings[0]).toContain('resolves differently'); + }); + + it('retains an inherited root plugin that resolves on disk without a direct dependency', async () => { + write('base.json', { rules: { 'no-debugger': 'error' } }); + write('node_modules/eslint-plugin-test/package.json', { + name: 'eslint-plugin-test', + main: 'index.js', + }); + write('node_modules/eslint-plugin-test/index.js', 'module.exports = {};'); + write('.oxlintrc.json', { + extends: ['./base.json'], + jsPlugins: ['eslint-plugin-test'], + rules: { 'test/custom': 'error' }, + }); + const report = createMigrationReport(); + + mergeViteConfigFiles(project, true, report); + + expect(report.warnings).toEqual([]); + const { lint } = await loadConfig(); + expect(lint!.jsPlugins).toContain('eslint-plugin-test'); + expect(lint!.rules).toHaveProperty('test/custom', 'error'); + }); + + it('does not override an inherited opt-out of the default import rule', async () => { + write('base.json', { rules: { 'vite-plus/prefer-vite-plus-imports': 'off' } }); + write('.oxlintrc.json', { extends: ['./base.json'] }); + + mergeViteConfigFiles(project, true); + + const config = await loadConfig(); + expect(config.lint!.rules!['vite-plus/prefer-vite-plus-imports']).toBeUndefined(); + expect(config.lint!.extends![0].rules!['vite-plus/prefer-vite-plus-imports']).toBe('off'); + }); + + it('keeps inherited JS plugins and overrides in their modules with portable absolute paths', async () => { + write('shared/base.json', { + jsPlugins: [{ name: 'import', specifier: './plugin.mjs' }], + overrides: [{ files: ['**/*.js'], jsPlugins: ['../override.mjs'] }], + }); + write('.oxlintrc.json', { + extends: ['./shared/base.json'], + rules: { 'import/custom-rule': 'error' }, + }); + + mergeViteConfigFiles(project, true); + + const configPath = path.join(project, 'vite.config.mjs'); + expect(fs.readFileSync(configPath, 'utf8')).not.toContain(project); + const config = await loadConfig(); + expect(config.lint!.extends![0].jsPlugins).toEqual([ + { + name: 'import', + specifier: path.join(project, 'shared/plugin.mjs'), + }, + ]); + expect(config.lint!.extends![0].overrides![0].jsPlugins).toEqual([ + path.join(project, 'override.mjs'), + ]); + expect(config.lint!.jsPlugins).toEqual([ + { name: 'vite-plus', specifier: 'vite-plus/oxlint-plugin' }, + ]); + const module = fs.readFileSync(path.join(project, 'shared/base.ts'), 'utf8'); + expect(module).toContain('import.meta.url'); + expect(module).not.toContain(project); + expect(config.lint!.rules!['import/custom-rule']).toBe('error'); + }); + + it.each([ + ['missing file', { extends: ['./missing.json'] }], + ['cycle', { extends: ['./.oxlintrc.json'] }], + ['invalid extends', { extends: './base.json' }], + ['object extends in JSON', { extends: [{}] }], + ['invalid JSON', '{ "extends": ["./base.json"], broken }'], + ['non-object JSON', '[]'], + ])('preserves the source and avoids default lint config on %s', (_name, content) => { + const source = write('.oxlintrc.json', content); + const original = fs.readFileSync(source, 'utf8'); + const report = createMigrationReport(); + + mergeViteConfigFiles(project, true, report); + injectLintTypeCheckDefaults(project, true, report); + + expect(fs.readFileSync(source, 'utf8')).toBe(original); + expect(fs.readFileSync(path.join(project, 'vite.config.mjs'), 'utf8')).toBe( + 'export default {};\n// vp migrate: pending .oxlintrc.json.\n', + ); + expect(report.warnings).toHaveLength(1); + expect(report.warnings[0]).toContain( + `vp lint -c ${path.relative(process.cwd(), source).replaceAll('\\', '/')}`, + ); + expect(report.mergedConfigCount).toBe(0); + }); + + it('preserves the source when the Vite config cannot be merged', () => { + write('base.json', { rules: { 'no-debugger': 'error' } }); + const source = write('.oxlintrc.json', { extends: ['./base.json'] }); + const original = fs.readFileSync(source, 'utf8'); + write('vite.config.mjs', "export { default } from './another-config.mjs';"); + const report = createMigrationReport(); + + mergeViteConfigFiles(project, true, report); + + expect(fs.readFileSync(source, 'utf8')).toBe(original); + expect(report.mergedConfigCount).toBe(0); + expect(report.warnings[0]).toContain('ESM default export'); + expect(fs.existsSync(path.join(project, 'base.ts'))).toBe(false); + expect(fs.readdirSync(project).some((file) => file.startsWith('.vite-plus-lint-'))).toBe(false); + }); + + it('retries a preserved inheritance tree after removing a conflicting lint block', async () => { + write('base.json', { rules: { 'no-debugger': 'error' } }); + const source = write('.oxlintrc.json', { extends: ['./base.json'] }); + const original = fs.readFileSync(source, 'utf8'); + const viteConfig = 'export default { lint: { rules: { "no-console": "warn" } } };'; + write('vite.config.mjs', viteConfig); + const report = createMigrationReport(); + + mergeViteConfigFiles(project, true, report); + + expect(fs.readFileSync(source, 'utf8')).toBe(original); + const configPath = path.join(project, 'vite.config.mjs'); + expect(fs.readFileSync(configPath, 'utf8')).toBe( + `${viteConfig}\n// vp migrate: pending .oxlintrc.json.\n`, + ); + expect(report.warnings[0]).toContain('already has a lint config'); + + fs.writeFileSync( + configPath, + fs.readFileSync(configPath, 'utf8').replace(viteConfig, 'export default {};'), + ); + const retryReport = createMigrationReport(); + expect(retryPreservedOxlintConfigs(project, true, retryReport)).toBe(true); + expect(retryReport.warnings).toEqual([]); + expect(fs.existsSync(source)).toBe(false); + expect((await loadConfig()).lint!.extends![0].rules!['no-debugger']).toBe('error'); + }); + + it.each([ + '// @ts-check', + '// @ts-nocheck', + '/// ', + '/** @jsxImportSource react */', + ])( + 'keeps %s before generated imports and statement directives on their original statement', + (pragma) => { + write('base.json', { rules: { 'no-debugger': 'error' } }); + write('.oxlintrc.json', { extends: ['./base.json'] }); + const header = `#!/usr/bin/env node\n${pragma}\n`; + write('vite.config.mjs', `${header}// @ts-expect-error\nexport default {};`); + + mergeViteConfigFiles(project, true); + + const source = fs.readFileSync(path.join(project, 'vite.config.mjs'), 'utf8'); + expect(source).toContain(`${header}import oxlintConfig1`); + expect(source).toContain('// @ts-expect-error\nexport default'); + }, + ); + + it('keeps shebangs and directive prologues before imports without moving triple-slash statement directives', () => { + write('base.json', { rules: { 'no-debugger': 'error' } }); + write('.oxlintrc.json', { extends: ['./base.json'] }); + write( + 'vite.config.mjs', + '#!/usr/bin/env node\n"";\n"use client";\n/// @ts-expect-error\nexport default {};', + ); + + mergeViteConfigFiles(project, true); + + const source = fs.readFileSync(path.join(project, 'vite.config.mjs'), 'utf8'); + expect(source).toMatch(/^#!\/usr\/bin\/env node\n["']{2};\n["']use client["'];\nimport /); + expect(source).toContain('/// @ts-expect-error\nexport default'); + }); + + it('creates no shared modules when the config merger does not support the export shape', () => { + write('base.json', { rules: { 'no-debugger': 'warn' } }); + write('.oxlintrc.json', { extends: ['./base.json'] }); + write('vite.config.mjs', 'const makeConfig = () => ({}); export default makeConfig();'); + const report = createMigrationReport(); + mergeViteConfigFiles(project, true, report); + expect(report.warnings[0]).toContain('Failed to merge'); + expect(fs.existsSync(path.join(project, 'base.ts'))).toBe(false); + expect(fs.existsSync(path.join(project, '.oxlintrc.json'))).toBe(true); + }); +}); diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index e4ed7080d1..a699b6d2be 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -77,6 +77,9 @@ import { rewriteStandaloneProject, planVitestV5Migration, refreshVitestV5Migration, + retryPreservedOxlintConfigs, + prepareOxlintConfigs, + removeMigratedOxlintConfigs, shouldSkipStagedMigrationForHooks, warnPackageLevelPrettier, vitestV5NeedsMigration, @@ -1202,6 +1205,17 @@ async function main() { didMigrate = true; } + if ( + retryPreservedOxlintConfigs( + workspaceInfoOptional.rootDir, + true, + report, + workspaceInfoOptional.packages, + ) + ) { + didMigrate = true; + } + // finalizeCore has finished; stop the spinner before the interactive setup // prompts below (collectMigrationSetupPlan / framework shims, in --full mode) // so they don't render on top of the still-running progress line. @@ -1452,12 +1466,25 @@ async function main() { // Merge configs and reinstall once if any tool or bootstrap migration happened if (eslintMigrated || prettierMigrated || tsupMigrated) { updateMigrationProgress('Rewriting configs'); + const rootDir = workspaceInfoOptional.rootDir; + // Full setup only merges the root here; untouched packages still need their JSON dependencies. + const oxlintConfigs = workspaceInfoOptional.isMonorepo + ? prepareOxlintConfigs([ + rootDir, + ...workspaceInfoOptional.packages.map((pkg) => path.join(rootDir, pkg.path)), + ]) + : undefined; mergeViteConfigFiles( - workspaceInfoOptional.rootDir, + rootDir, true, report, workspaceInfoOptional.packages, + rootDir, + oxlintConfigs, ); + if (oxlintConfigs) { + removeMigratedOxlintConfigs(oxlintConfigs); + } if (tsupMigrated) { mergeTsdownConfigFile(workspaceInfoOptional.rootDir, true, report); for (const pkg of workspaceInfoOptional.packages ?? []) { diff --git a/packages/cli/src/migration/migrator.ts b/packages/cli/src/migration/migrator.ts index 1c745797e0..4433d1d157 100644 --- a/packages/cli/src/migration/migrator.ts +++ b/packages/cli/src/migration/migrator.ts @@ -12,6 +12,7 @@ export * from './migrator/source-scan.ts'; export * from './migrator/vite-plus-bootstrap.ts'; export * from './migrator/package-json.ts'; export * from './migrator/vite-config.ts'; +export * from './migrator/oxlint-config.ts'; export * from './migrator/git-hooks.ts'; export * from './migrator/setup.ts'; export * from './migrator/core-finalization.ts'; diff --git a/packages/cli/src/migration/migrator/README.md b/packages/cli/src/migration/migrator/README.md index 4e824f020a..b93087cfde 100644 --- a/packages/cli/src/migration/migrator/README.md +++ b/packages/cli/src/migration/migrator/README.md @@ -30,6 +30,7 @@ Pick the file by what a function _does_, not by where it happens to be called. | `yarn.ts` | `.yarnrc.yml`, Yarn PnP detection, workspace-hoisting fix, webdriverio detection. | | `source-scan.ts` | Scanning a project's source tree for signals (browser-mode, opt-in providers, `@nuxt/test-utils`, retained upstream-vitest references). | | `vite-config.ts` | `vite.config.ts` merging, default-config injection, staged-config merge, lazy-plugin wrapping, import rewriting (`rewriteAllImports`), migrated-oxlint-config sanitization, lint-staged removal. | +| `oxlint-config.ts` | Oxlint JSON inheritance graphs and shared TypeScript config generation. | | `eslint.ts` | ESLint → Oxlint migration, oxlint JS-plugin namespace handling, ESLint prompts/warnings. | | `prettier.ts` | Prettier → Oxfmt migration and its prompts/warnings. | | `tsup.ts` | tsup → tsdown (`vp pack`) migration via `tsdown-migrate` and its prompts/warnings. | diff --git a/packages/cli/src/migration/migrator/catalog.ts b/packages/cli/src/migration/migrator/catalog.ts index 34a30c24da..cd6b13e998 100644 --- a/packages/cli/src/migration/migrator/catalog.ts +++ b/packages/cli/src/migration/migrator/catalog.ts @@ -32,6 +32,7 @@ import { workspaceUsesWebdriverioProvider, } from '../migrator.ts'; import { + type PreparedOxlintConfigs, LEGACY_WRAPPER_FALLBACK_VERSIONS, PROVIDER_OVERRIDE_DROP_NAMES, REMOVE_PACKAGES, @@ -1151,6 +1152,7 @@ export function rewriteRootWorkspacePackageJson( // catalogs require Yarn >= 4.10.0; pnpm/bun monorepos always manage a catalog. // When false, the root `vite-plus` edge and the direct `vite` edge are concrete. supportCatalog = true, + preparedOxlintConfigs?: PreparedOxlintConfigs, ): void { const packageJsonPath = path.join(projectPath, 'package.json'); if (!fs.existsSync(packageJsonPath)) { @@ -1291,7 +1293,7 @@ export function rewriteRootWorkspacePackageJson( undefined, undefined, catalogDependencyResolver, - packages ? { rootDir: projectPath, packages } : undefined, + packages ? { rootDir: projectPath, packages, oxlintConfigs: preparedOxlintConfigs } : undefined, true, supportCatalog, ); diff --git a/packages/cli/src/migration/migrator/eslint.ts b/packages/cli/src/migration/migrator/eslint.ts index e5e8a69903..992a6e7e5f 100644 --- a/packages/cli/src/migration/migrator/eslint.ts +++ b/packages/cli/src/migration/migrator/eslint.ts @@ -7,7 +7,7 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts'; import { type OxlintConfig } from 'oxlint'; import { compareReversed, isLessThanOrEqual, normalize } from 'verkit'; -import { rewriteEslint } from '../../../binding/index.js'; +import { ResolverFactory, rewriteEslint } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; import { runCommandSilently } from '../../utils/command.ts'; import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts'; @@ -764,6 +764,81 @@ function jsPluginsToNamespaces(entries: NonNullable): return ns; } +function warnMissingJsPlugins(plugins: Set, report?: MigrationReport): void { + if (plugins.size > 0) { + warnMigration( + `Stripped JS plugin reference(s) from the generated lint config: ${[...plugins].join(', ')}. ` + + 'No matching package is present in this workspace, so loading them at lint time would fail. ' + + 'If you want their Oxlint coverage back, install each package (e.g. `vp install `) and add its name back to `lint.jsPlugins` in vite.config.ts.', + report, + ); + } +} + +/** Only remove known missing references: inherited plugins can supply other namespaces at runtime. */ +export function sanitizeInheritedJsPlugins( + config: OxlintConfig, + availablePackages: Set, + configDir: string, + report?: MigrationReport, +): void { + const availableSpecifiers = new Set(availablePackages); + let resolver: ResolverFactory | undefined; + for (const layer of [config, ...(config.overrides ?? [])]) { + for (const entry of layer.jsPlugins ?? []) { + if (typeof entry !== 'string' || availableSpecifiers.has(entry)) { + continue; + } + const packageName = entry + .split('/') + .slice(0, entry.startsWith('@') ? 2 : 1) + .join('/'); + if ( + availablePackages.has(packageName) || + /^\.{1,2}[/\\]/.test(entry) || + path.win32.isAbsolute(entry) + ) { + availableSpecifiers.add(entry); + continue; + } + resolver ??= new ResolverFactory({ conditionNames: ['module-sync', 'node', 'import'] }); + if (resolver.sync(configDir, entry).path) { + availableSpecifiers.add(entry); + } + } + } + const droppedPlugins = new Set(); + function sanitizeLayer(layer: OxlintConfig, inheritedDropped = new Set()): Set { + const { kept, dropped } = partitionJsPlugins(layer.jsPlugins ?? [], availableSpecifiers); + if (dropped.length > 0) { + layer.jsPlugins = kept; + for (const plugin of dropped) { + droppedPlugins.add(plugin); + } + } + const droppedNamespaces = new Set([...inheritedDropped, ...jsPluginsToNamespaces(dropped)]); + for (const namespace of jsPluginsToNamespaces(kept)) { + droppedNamespaces.delete(namespace); + } + if (layer.plugins) { + layer.plugins = layer.plugins.filter( + (plugin) => !droppedNamespaces.has(plugin) || OXLINT_NATIVE_PLUGINS.has(plugin), + ); + } + for (const rule of Object.keys(layer.rules ?? {})) { + if (ruleKeyMatchesNamespace(rule, droppedNamespaces) && !isBuiltinOxlintRule(rule)) { + delete layer.rules![rule]; + } + } + return droppedNamespaces; + } + const droppedNamespaces = sanitizeLayer(config); + for (const override of config.overrides ?? []) { + sanitizeLayer(override, droppedNamespaces); + } + warnMissingJsPlugins(droppedPlugins, report); +} + /** * Sanitize the `.oxlintrc.json` produced by `@oxlint/migrate` (in-place) * before it gets merged into `vite.config.ts`. Drop references that @@ -908,14 +983,7 @@ export function sanitizeMigratedOxlintConfig( // phantom-reference case (e.g. `@unocss/eslint-config` translating // into `eslint-plugin-unocss` even though the user never had it). // A single accurate message covers both paths. - if (allDroppedJsPlugins.size > 0) { - warnMigration( - `Stripped JS plugin reference(s) from the generated lint config: ${[...allDroppedJsPlugins].join(', ')}. ` + - 'No matching package is present in this workspace, so loading them at lint time would fail. ' + - 'If you want their Oxlint coverage back, install each package (e.g. `vp install `) and add its name back to `lint.jsPlugins` in vite.config.ts.', - report, - ); - } + warnMissingJsPlugins(allDroppedJsPlugins, report); if (allDroppedPlugins.size > 0) { warnMigration( `Stripped unknown plugin reference(s) from the generated lint config: ${[...allDroppedPlugins].join(', ')}. ` + diff --git a/packages/cli/src/migration/migrator/orchestrators.ts b/packages/cli/src/migration/migrator/orchestrators.ts index a551ba2f79..dc4a2c5550 100644 --- a/packages/cli/src/migration/migrator/orchestrators.ts +++ b/packages/cli/src/migration/migrator/orchestrators.ts @@ -32,11 +32,13 @@ import { migratePnpmSettingsToWorkspaceYaml, migrateTaskCacheConfigInViteConfig, pnpmSupportsWorkspaceSettings, + prepareOxlintConfigs, supportsCatalog, projectListsRequiredVitestPeer, projectUsesVitestDirectly, pruneLegacyWrapperAliases, removeLintStagedFromPackageJson, + removeMigratedOxlintConfigs, removeManagedVitestEntry, removeVitestPeerDependencyRule, rewriteAllImports, @@ -63,6 +65,7 @@ import { pnpmMajor, type CatalogDependencyResolver, type PnpmPackageJsonSettings, + type PreparedOxlintConfigs, } from './shared.ts'; export function rewriteStandaloneProject( @@ -399,6 +402,14 @@ export function rewriteMonorepo( workspaceInfo.downloadPackageManager.version, true, ); + const workspaceContext = { + rootDir: workspaceInfo.rootDir, + packages: workspaceInfo.packages, + oxlintConfigs: prepareOxlintConfigs([ + workspaceInfo.rootDir, + ...workspaceInfo.packages.map((pkg) => path.join(workspaceInfo.rootDir, pkg.path)), + ]), + }; // rewrite root workspace if (workspaceInfo.packageManager === PackageManager.yarn) { rewriteYarnrcYml( @@ -427,6 +438,7 @@ export function rewriteMonorepo( workspaceShouldAllowBrowserBuilds, workspaceUsesVitest, supportCatalog, + workspaceContext.oxlintConfigs, ); if (workspaceInfo.packageManager === PackageManager.pnpm) { rewritePnpmWorkspaceYaml( @@ -452,10 +464,6 @@ export function rewriteMonorepo( // rewrite packages — pass workspace context so the per-package // sanitizer can see hoisted deps that live elsewhere in the // workspace, not just this sub-package's own `package.json`. - const workspaceContext = { - rootDir: workspaceInfo.rootDir, - packages: workspaceInfo.packages, - }; // Yarn `node-modules` + an isolating `nmHoistingLimits` would give each // vite-plus-receiving workspace its own physical `vitest` copy, splitting the // runner across two `@vitest/runner` instances. `rewriteMonorepoProject` detects @@ -481,7 +489,15 @@ export function rewriteMonorepo( } cleanupDeprecatedTsconfigOptions(workspaceInfo.rootDir, silent, report); rewriteTsconfigTypes(workspaceInfo.rootDir, silent, report); - mergeViteConfigFiles(workspaceInfo.rootDir, silent, report, workspaceInfo.packages); + mergeViteConfigFiles( + workspaceInfo.rootDir, + silent, + report, + workspaceInfo.packages, + workspaceInfo.rootDir, + workspaceContext.oxlintConfigs, + ); + removeMigratedOxlintConfigs(workspaceContext.oxlintConfigs); injectLintTypeCheckDefaults(workspaceInfo.rootDir, silent, report); injectFmtDefaults(workspaceInfo.rootDir, silent, report); mergeTsdownConfigFile(workspaceInfo.rootDir, silent, report); @@ -517,7 +533,11 @@ export function rewriteMonorepoProject( silent = false, report?: MigrationReport, catalogDependencyResolver?: CatalogDependencyResolver, - workspaceContext?: { rootDir: string; packages: WorkspacePackage[] }, + workspaceContext?: { + rootDir: string; + packages: WorkspacePackage[]; + oxlintConfigs?: PreparedOxlintConfigs; + }, deferLazyPluginWrapping = false, // Whether this workspace's edges use catalog references. Yarn catalogs require // Yarn >= 4.10.0; pnpm/bun monorepos always manage a catalog. Defaults to true @@ -536,6 +556,7 @@ export function rewriteMonorepoProject( report, workspaceContext?.packages, workspaceContext?.rootDir, + workspaceContext?.oxlintConfigs, ); mergeTsdownConfigFile(projectPath, silent, report); diff --git a/packages/cli/src/migration/migrator/oxlint-config.ts b/packages/cli/src/migration/migrator/oxlint-config.ts new file mode 100644 index 0000000000..b971d3fd49 --- /dev/null +++ b/packages/cli/src/migration/migrator/oxlint-config.ts @@ -0,0 +1,477 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +import type { Program } from '@oxc-project/types'; +import { parse as parseJsonc, type ParseError } from 'jsonc-parser'; +import type { OxlintConfig } from 'oxlint'; + +import { analyzeMigrationSource, ResolverFactory } from '../../../binding/index.js'; +import { detectConfigs } from '../detector.ts'; +import { ensureSvelteRuneGlobals } from '../migrator.ts'; +import { + OXLINT_OWNER_CONFIG_KEYS, + type OxlintConfigFile, + type OxlintModulePlan, + type PreparedOxlintConfigs, +} from './shared.ts'; + +interface OxlintConfigCache { + sources: Map>; + files: Map; +} + +function createConfigCache(): OxlintConfigCache { + return { + sources: new Map(), + files: new Map(), + }; +} + +/** Read the inheritance graph before migration removes any workspace configs. */ +export function resolveOxlintConfigExtends( + configPath: string, + cache = createConfigCache(), + stack: string[] = [], +): OxlintConfigFile { + // Windows collapses parent components before following directory junctions. + // POSIX follows the directory link first, so its physical parent is the context. + const filename = + process.platform === 'win32' ? path.resolve(configPath) : configPathInRealDirectory(configPath); + const sourcePath = fs.realpathSync(filename); + if (stack.includes(sourcePath)) { + throw new Error(`Circular Oxlint extends: ${[...stack, sourcePath].join(' -> ')}`); + } + const cached = cache.files.get(filename); + if (cached) { + return cached; + } + let source = cache.sources.get(sourcePath); + if (!source) { + const errors: ParseError[] = []; + source = parseJsonc(fs.readFileSync(sourcePath, 'utf8'), errors, { + allowTrailingComma: true, + }) as Record; + if (errors.length > 0 || !source || typeof source !== 'object' || Array.isArray(source)) { + throw new Error(`Invalid Oxlint JSON config: ${filename}`); + } + cache.sources.set(sourcePath, source); + } + // Parsing is shared by source identity, but relative paths belong to each declaring config. + const config = structuredClone(source); + delete config.$schema; + const inherited = config.extends; + if ( + inherited !== undefined && + (!Array.isArray(inherited) || inherited.some((entry) => typeof entry !== 'string')) + ) { + throw new Error(`Expected an array of file paths in ${filename}: extends`); + } + const extendsPaths = ((inherited ?? []) as string[]).map((entry) => + path.resolve(path.dirname(filename), entry), + ); + const children = extendsPaths.map((childPath) => { + const child = resolveOxlintConfigExtends(childPath, cache, [...stack, sourcePath]); + child.shared = true; + return child; + }); + if (inherited !== undefined) { + config.extends = children.map((child) => child.config); + } + const file: OxlintConfigFile = { + filename, + config, + extends: children, + extendsPaths, + }; + cache.files.set(filename, file); + return file; +} + +export function prepareOxlintConfigs(projectPaths: string[]): PreparedOxlintConfigs { + const configs: PreparedOxlintConfigs = new Map(); + const cache = createConfigCache(); + for (const projectPath of projectPaths) { + const filename = detectConfigs(projectPath).oxlintConfig; + if (!filename) { + continue; + } + const configPath = path.join(projectPath, filename); + try { + configs.set(configPath, { file: resolveOxlintConfigExtends(configPath, cache) }); + } catch (error) { + configs.set(configPath, error instanceof Error ? error : new Error(String(error))); + } + } + // An owner must also import its source when another consumer reaches it through an alias. + const sharedSources = new Set( + [...cache.files.values()] + .filter((file) => file.shared) + .map((file) => fs.realpathSync(file.filename)), + ); + // An unreadable consumer can still refer to any workspace's own config. + // Keep those owners in modules so a later repair cannot create an inline copy. + if ([...configs.values()].some((migration) => migration instanceof Error)) { + for (const [filename, migration] of configs) { + if (!(migration instanceof Error)) { + sharedSources.add(fs.realpathSync(filename)); + } + } + } + for (const file of cache.files.values()) { + if (sharedSources.has(fs.realpathSync(file.filename))) { + file.shared = true; + } + } + return configs; +} + +function collectConfigFiles(file: OxlintConfigFile, target: Set): void { + if (target.has(file)) { + return; + } + target.add(file); + for (const child of file.extends) { + collectConfigFiles(child, target); + } +} + +/** Identify a completed merge by its original workspace path and JSON graph. */ +export function oxlintMigrationMarker( + filename: string, + root: OxlintConfigFile, + workspaceRoot: string, +): string { + function relative(filename: string): string { + return path.relative(workspaceRoot, filename).split(path.sep).join('/'); + } + const files = new Set(); + collectConfigFiles(root, files); + const inputs: unknown[] = [relative(filename)]; + for (const file of files) { + const config = { ...file.config }; + delete config.extends; + inputs.push([relative(file.filename), config, file.extendsPaths.map(relative)]); + } + const serialized = JSON.stringify(inputs, (_key, value) => { + if (value && typeof value === 'object' && !Array.isArray(value)) { + return Object.fromEntries( + Object.keys(value) + .toSorted() + .map((key) => [key, value[key]]), + ); + } + return value; + }); + const digest = createHash('sha256').update(serialized).digest('hex').slice(0, 32); + return `// vp migrate: merged ${encodeURIComponent(relative(filename))} (${digest}).`; +} + +function configPathInRealDirectory(filename: string): string { + return path.join(fs.realpathSync(path.dirname(filename)), path.basename(filename)); +} + +/** Retain each link in a chain, without conflating independent aliases of its target. */ +function retainConfigPath(filename: string, retained: Set): void { + filename = configPathInRealDirectory(filename); + while (!retained.has(filename)) { + retained.add(filename); + if (!fs.lstatSync(filename).isSymbolicLink()) { + return; + } + filename = configPathInRealDirectory( + path.resolve(path.dirname(filename), fs.readlinkSync(filename)), + ); + } +} + +/** Retained JSON consumers still need their full inheritance graph on disk. */ +export function removeMigratedOxlintConfigs(configs: PreparedOxlintConfigs): boolean { + const files = new Set(); + const retained = new Set(); + const retainedPaths = new Set(); + const removableSources = new Set(); + const removalCandidates = new Set(); + for (const [filename, migration] of configs) { + // An unreadable graph can contain references we could not discover. + // Preserve all candidates rather than break that consumer's fallback. + if (migration instanceof Error) { + return false; + } + const { file, migrated } = migration; + collectConfigFiles(file, files); + if (!migrated) { + retainConfigPath(filename, retainedPaths); + collectConfigFiles(file, retained); + continue; + } + removalCandidates.add(filename); + // Unlinking a workspace symlink leaves its shared JSON target on disk. + if (!fs.lstatSync(filename).isSymbolicLink()) { + removableSources.add(fs.realpathSync(filename)); + } + } + const removablePaths = new Set(Array.from(removalCandidates, configPathInRealDirectory)); + const cache = createConfigCache(); + try { + for (const file of files) { + const sourcePath = fs.realpathSync(file.filename); + const survivingAlias = + fs.lstatSync(file.filename).isSymbolicLink() && + !removablePaths.has(configPathInRealDirectory(file.filename)); + if (survivingAlias) { + collectConfigFiles(file, retained); + } + if (survivingAlias || !removableSources.has(sourcePath)) { + // A symlink target can resolve different dependencies from its aliases. + const source = resolveOxlintConfigExtends(sourcePath, cache); + collectConfigFiles(source, retained); + collectConfigFiles(source, files); + } + } + } catch { + // Keep all JSON paths when a remaining source's dependencies cannot be determined. + return false; + } + for (const file of retained) { + retainConfigPath(file.filename, retainedPaths); + for (const filename of file.extendsPaths) { + retainConfigPath(filename, retainedPaths); + } + } + let removed = false; + for (const filename of removalCandidates) { + if (!retainedPaths.has(configPathInRealDirectory(filename))) { + fs.unlinkSync(filename); + removed = true; + } + } + return removed; +} + +function relativeImport(from: string, to: string): string { + const relative = path.relative(path.dirname(from), to).split(path.sep).join('/'); + if (path.isAbsolute(relative)) { + throw new Error(`Cannot import an Oxlint config across drives: ${to}`); + } + // Vite treats `?` as a query delimiter; TypeScript treats literal `\` as a separator. + // Percent-encoding also breaks resolution of otherwise valid paths such as `#`. + const unsupported = ['?', '\\'].find((character) => relative.includes(character)); + if (unsupported) { + throw new Error( + `Cannot import a shared Oxlint config whose relative path contains ${JSON.stringify(unsupported)}: ${to}`, + ); + } + // Vite and TypeScript resolve .js imports to .ts source files. + // This also works with NodeNext without changing allowImportingTsExtensions. + const specifier = relative.replace(/\.ts$/, '.js'); + return specifier.startsWith('../') ? specifier : `./${specifier}`; +} + +function printConfig( + config: OxlintConfig, + inheritedNames: string[], + { + pluginExpression, + ownerConfigName, + }: { + pluginExpression?: (specifier: string) => string; + ownerConfigName?: string; + } = {}, +): string { + function printPlugin(entry: NonNullable[number]): string { + const specifier = typeof entry === 'string' ? entry : entry.specifier; + const expression = pluginExpression?.(specifier) ?? JSON.stringify(specifier); + return typeof entry === 'string' + ? expression + : `{ "name": ${JSON.stringify(entry.name)}, "specifier": ${expression} }`; + } + const properties = Object.entries(config).map(([key, value]) => { + let code: string; + if (key === 'extends') { + code = `[${inheritedNames.join(', ')}]`; + } else if (ownerConfigName && OXLINT_OWNER_CONFIG_KEYS.has(key)) { + code = `${ownerConfigName}.${key}`; + } else if (key === 'jsPlugins' && Array.isArray(config.jsPlugins)) { + code = `[${config.jsPlugins.map(printPlugin).join(', ')}]`; + } else if (key === 'overrides' && config.overrides) { + code = `[${config.overrides.map((override) => printConfig(override, [], { pluginExpression })).join(',\n')}]`; + } else { + code = JSON.stringify(value, null, 2); + } + return ` ${JSON.stringify(key)}: ${code.replaceAll('\n', '\n ')}`; + }); + return `{\n${properties.join(',\n')}\n}`; +} + +function isRelativePluginPath(specifier: string): boolean { + return /^\.{1,2}(?:[/\\]|$)/.test(specifier); +} + +function moduleProvenance(file: OxlintConfigFile): string { + const sourcePath = fs.realpathSync(file.filename); + const hasRelativePlugins = [file.config, ...(file.config.overrides ?? [])].some((layer) => + layer.jsPlugins?.some((entry) => + isRelativePluginPath(typeof entry === 'string' ? entry : entry.specifier), + ), + ); + const contextPath = file.extends.length || hasRelativePlugins ? file.filename : sourcePath; + const relative = path.relative(path.dirname(sourcePath), contextPath).split(path.sep).join('/'); + return `// Generated by vp migrate from ${encodeURIComponent(relative)}.`; +} + +function generatedModulePath(file: OxlintConfigFile): string { + return fs.realpathSync(file.filename).replace(/\.jsonc?$/i, '') + '.ts'; +} + +function isReusableModule(file: OxlintConfigFile, filename: string): boolean { + return ( + !fs.existsSync(filename.replace(/\.ts$/, '.js')) && + fs.existsSync(filename) && + fs.readFileSync(filename, 'utf8').split(/\r?\n/, 1)[0] === moduleProvenance(file) + ); +} + +/** Generate portable shared modules, leaving their plugins in their original layers. */ +export function prepareOxlintConfigModules( + root: OxlintConfigFile, + config: OxlintConfig, + viteConfigPath: string, + workspaceRoot: string, +): OxlintModulePlan { + viteConfigPath = fs.realpathSync(viteConfigPath); + const viteConfigDir = path.dirname(viteConfigPath); + const source = fs.readFileSync(viteConfigPath, 'utf8'); + const names = new Set(); + const { ast } = JSON.parse(analyzeMigrationSource(viteConfigPath, source), (key, value) => { + if (key === 'name' && typeof value === 'string') { + names.add(value); + } + return value; + }) as { ast: { node: Program } }; + if ( + /\.c[jt]s$/.test(viteConfigPath) || + !ast.node.body.some((node) => node.type === 'ExportDefaultDeclaration') + ) { + throw new Error('Shared Oxlint configs require a Vite config with an ESM default export'); + } + const modules: OxlintModulePlan['modules'] = new Map(); + const boundary = fs.realpathSync(workspaceRoot); + let resolver: ResolverFactory | undefined; + + const importedFiles = root.shared ? [root] : root.extends; + const files = new Set(); + for (const file of importedFiles) { + collectConfigFiles(file, files); + } + for (const file of files) { + const relative = path.relative(boundary, fs.realpathSync(file.filename)); + if ( + path.isAbsolute(relative) || + relative === '..' || + relative.startsWith(`..${path.sep}`) || + relative.split(path.sep).includes('node_modules') + ) { + throw new Error( + `Shared Oxlint config is outside the project or in node_modules: ${file.filename}`, + ); + } + const configDir = path.dirname(file.filename); + for (const layer of [file.config, ...(file.config.overrides ?? [])]) { + for (const entry of layer.jsPlugins ?? []) { + const specifier = typeof entry === 'string' ? entry : entry.specifier; + if ( + isRelativePluginPath(specifier) || + path.isAbsolute(specifier) || + configDir === viteConfigDir + ) { + continue; + } + resolver ??= new ResolverFactory({ conditionNames: ['module-sync', 'node', 'import'] }); + const original = resolver.sync(configDir, specifier); + const migrated = resolver.sync(viteConfigDir, specifier); + if (original.path && original.path !== migrated.path) { + throw new Error( + `JS plugin "${specifier}" in ${file.filename} resolves differently from the Vite config directory; migrate this plugin manually`, + ); + } + } + } + } + + function generate(file: OxlintConfigFile, sourceConfig: OxlintConfig): string { + const filename = generatedModulePath(file); + const existing = modules.get(filename); + if (existing && moduleProvenance(existing.file) !== moduleProvenance(file)) { + throw new Error(`Multiple Oxlint configs would create ${filename}`); + } + if (file.modulePath) { + return file.modulePath; + } + if (existing) { + return filename; + } + const runtimePath = filename.replace(/\.ts$/, '.js'); + if (isReusableModule(file, filename)) { + // Reuse earlier output, including user edits, without rewriting it. + file.modulePath = filename; + return filename; + } + if (fs.existsSync(filename) || fs.existsSync(runtimePath)) { + throw new Error( + `Cannot create shared Oxlint config because ${filename} or ${runtimePath} already exists`, + ); + } + const module = { file, content: '' }; + modules.set(filename, module); + const imports = ['import type { OxlintConfig } from "vite-plus/lint";']; + const inherited = file.extends.map((child, index) => { + const name = `oxlintConfig${index + 1}`; + imports.push( + `import ${name} from ${JSON.stringify(relativeImport(filename, generate(child, sourceConfig.extends![index])))};`, + ); + return name; + }); + let needsFileUrl = false; + function pluginExpression(specifier: string): string { + if (!isRelativePluginPath(specifier)) { + return JSON.stringify(specifier); + } + needsFileUrl = true; + const pluginPath = path.resolve(path.dirname(file.filename), specifier); + const relative = path.relative(path.dirname(filename), pluginPath); + const encoded = relative.split(path.sep).map(encodeURIComponent).join('/'); + const url = encoded.startsWith('../') ? encoded : `./${encoded}`; + return `fileURLToPath(new URL(${JSON.stringify(url)}, import.meta.url))`; + } + const config = structuredClone(sourceConfig); + ensureSvelteRuneGlobals(config); + const body = printConfig(config, inherited, { pluginExpression }); + if (needsFileUrl) { + imports.unshift('import { fileURLToPath } from "node:url";'); + } + module.content = `${moduleProvenance(file)}\n${imports.join('\n')}\n\nexport default ${body} satisfies OxlintConfig;\n`; + return filename; + } + + const imports: string[] = []; + let nextName = 1; + const inherited = importedFiles.map((file, index) => { + let name: string; + do { + name = `oxlintConfig${nextName++}`; + } while (names.has(name)); + names.add(name); + imports.push( + `import ${name} from ${JSON.stringify(relativeImport(viteConfigPath, generate(file, config.extends![index])))};`, + ); + return name; + }); + return { + configContent: printConfig(config, inherited, { + ownerConfigName: root.shared ? inherited[0] : undefined, + }), + imports, + modules, + }; +} diff --git a/packages/cli/src/migration/migrator/shared.ts b/packages/cli/src/migration/migrator/shared.ts index 23a2a02d67..ecff286023 100644 --- a/packages/cli/src/migration/migrator/shared.ts +++ b/packages/cli/src/migration/migrator/shared.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; +import type { OxlintConfig } from 'oxlint'; import { coerce, satisfies } from 'verkit'; import { VITEST_VERSION, VITE_PLUS_OVERRIDE_PACKAGES } from '../../utils/constants.ts'; @@ -38,6 +39,36 @@ export function getCommandStartIndex(argv: readonly string[]): number { return start; } +// Oxlint does not inherit these fields through `extends`. +export const OXLINT_OWNER_CONFIG_KEYS = new Set(['env', 'globals', 'settings', 'ignorePatterns']); + +export interface OxlintConfigFile { + filename: string; + config: OxlintConfig; + extends: OxlintConfigFile[]; + // Keep declared paths as well as canonical sources for JSON fallback cleanup. + extendsPaths: string[]; + // A dependency must use the same generated module in its owning workspace. + shared?: boolean; + // A committed or previously generated module that workspace consumers can reuse. + modulePath?: string; +} + +export interface OxlintConfigMigration { + file: OxlintConfigFile; + // Completion belongs to each workspace path, even when symlinks share a source. + migrated?: boolean; +} + +export type PreparedOxlintConfigs = Map; + +export interface OxlintModulePlan { + configContent: string; + imports: string[]; + completionMarker?: string; + modules: Map; +} + // All known lint-staged config file names. // JSON-parseable ones come first so rewriteLintStagedConfigFile can rewrite them. export const LINT_STAGED_JSON_CONFIG_FILES = ['.lintstagedrc.json', '.lintstagedrc'] as const; diff --git a/packages/cli/src/migration/migrator/vite-config.ts b/packages/cli/src/migration/migrator/vite-config.ts index a96e182b64..96e6e6d46f 100644 --- a/packages/cli/src/migration/migrator/vite-config.ts +++ b/packages/cli/src/migration/migrator/vite-config.ts @@ -1,11 +1,13 @@ import fs from 'node:fs'; import path from 'node:path'; +import type { Program } from '@oxc-project/types'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; import { braceExpand } from 'minimatch'; import { type OxlintConfig } from 'oxlint'; import { + analyzeMigrationSource, hasConfigKey, mergeJsonConfig, mergeTsdownConfig, @@ -17,21 +19,32 @@ import { import { createDefaultVitePlusLintConfig, ensureVitePlusImportRuleDefaults, + PREFER_VITE_PLUS_IMPORTS_RULE, } from '../../oxlint-plugin-config.ts'; 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, writeJsonFile } from '../../utils/json.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 { collectInstalledPackageNames, + oxlintMigrationMarker, + prepareOxlintConfigs, readRulesYaml, + removeMigratedOxlintConfigs, + resolveOxlintConfigExtends, + prepareOxlintConfigModules, sanitizeMigratedOxlintConfig, + sanitizeInheritedJsPlugins, } from '../migrator.ts'; import { type MigrationReport } from '../report.ts'; import { + type OxlintConfigMigration, + type OxlintModulePlan, + type PreparedOxlintConfigs, + OXLINT_OWNER_CONFIG_KEYS, LINT_STAGED_JSON_CONFIG_FILES, LINT_STAGED_OTHER_CONFIG_FILES, infoMigration, @@ -48,6 +61,10 @@ const SVELTE_RUNE_GLOBALS = { $host: 'readonly', } satisfies Record; +function oxlintConfigLayers(config: OxlintConfig): OxlintConfig[] { + return [...(config.extends ?? []).flatMap(oxlintConfigLayers), config]; +} + // Add Svelte's built-in rune globals to migrated Svelte overrides. // https://github.com/oxc-project/oxc/issues/20191 export function ensureSvelteRuneGlobals(config: OxlintConfig): void { @@ -251,6 +268,7 @@ export function mergeViteConfigFiles( // (correct for the top-level standalone/monorepo callers, where // projectPath IS the workspace root). workspaceRoot?: string, + preparedOxlintConfigs?: PreparedOxlintConfigs, ): void { const configs = detectConfigs(projectPath); if (!configs.oxfmtConfig && !configs.oxlintConfig) { @@ -258,46 +276,230 @@ export function mergeViteConfigFiles( } const viteConfig = ensureViteConfig(projectPath, configs, silent, report); if (configs.oxlintConfig) { - // Inject options.typeAware and options.typeCheck defaults before merging - const fullOxlintPath = path.join(projectPath, configs.oxlintConfig); - const oxlintJson = readJsonFile(fullOxlintPath, true) as OxlintConfig; - if (!oxlintJson.options) { - oxlintJson.options = {}; - } - // Skip typeAware/typeCheck when tsconfig.json has baseUrl (unsupported by tsgolint) - if (!hasBaseUrlInTsconfig(projectPath)) { - if (oxlintJson.options.typeAware === undefined) { - oxlintJson.options.typeAware = true; + mergeOxlintConfigFile( + projectPath, + viteConfig, + configs.oxlintConfig, + silent, + report, + packages, + workspaceRoot, + preparedOxlintConfigs, + ); + } + if (configs.oxfmtConfig) { + // merge oxfmt config into vite.config.ts + mergeAndRemoveJsonConfig(projectPath, viteConfig, configs.oxfmtConfig, 'fmt', silent, report); + } +} + +function pendingOxlintMigrationMarker(filename: string): string { + return `// vp migrate: pending ${encodeURIComponent(path.basename(filename))}.`; +} + +/** Resume partial migrations without applying lint setup to other existing Vite+ projects. */ +export function retryPreservedOxlintConfigs( + workspaceRoot: string, + silent = false, + report?: MigrationReport, + packages?: WorkspacePackage[], +): boolean { + const prepared = prepareOxlintConfigs([ + workspaceRoot, + ...(packages ?? []).map((pkg) => path.join(workspaceRoot, pkg.path)), + ]); + let updated = false; + for (const [filename, migration] of prepared) { + const projectPath = path.dirname(filename); + const configs = detectConfigs(projectPath); + const existingViteConfig = configs.viteConfig && path.join(projectPath, configs.viteConfig); + const source = existingViteConfig ? fs.readFileSync(existingViteConfig, 'utf8') : ''; + const lines = source.split(/\r?\n/); + if (existingViteConfig && hasConfigKey(existingViteConfig, 'lint')) { + if (!(migration instanceof Error)) { + const marker = oxlintMigrationMarker(filename, migration.file, workspaceRoot); + migration.migrated = lines.includes(marker); } - if (oxlintJson.options.typeCheck === undefined) { - oxlintJson.options.typeCheck = true; + continue; + } + if (!lines.includes(pendingOxlintMigrationMarker(filename))) { + continue; + } + const viteConfig = ensureViteConfig(projectPath, configs, silent, report); + mergeOxlintConfigFile( + projectPath, + viteConfig, + path.basename(filename), + silent, + report, + packages, + workspaceRoot, + prepared, + ); + updated ||= !(migration instanceof Error) && migration.migrated === true; + } + return removeMigratedOxlintConfigs(prepared) || updated; +} + +function mergeOxlintConfigFile( + projectPath: string, + viteConfig: string, + oxlintConfig: string, + silent: boolean, + report?: MigrationReport, + packages?: WorkspacePackage[], + workspaceRoot?: string, + preparedOxlintConfigs?: PreparedOxlintConfigs, +): void { + const fullOxlintPath = path.join(projectPath, oxlintConfig); + function recordPendingMigration(): void { + const filename = path.join(projectPath, viteConfig); + const marker = pendingOxlintMigrationMarker(oxlintConfig); + try { + const source = fs.readFileSync(filename, 'utf8'); + if (!source.split(/\r?\n/).includes(marker)) { + fs.appendFileSync(filename, `${source.endsWith('\n') ? '' : '\n'}${marker}\n`); } - } else { - warnMigration(BASEURL_TSCONFIG_WARNING, report); - } - // Drop references to plugins / jsPlugins / rules that won't resolve - // at lint time (e.g. `@oxlint/migrate` translating `@unocss/eslint-config` - // → `eslint-plugin-unocss` even when that package isn't installed). - // Resolve workspace package paths against `workspaceRoot` when the - // caller is processing a sub-package — otherwise the sanitizer would - // mistakenly look for `subPath/` and miss the - // hoisted deps it's supposed to see. + } catch (error) { + warnMigration( + `Could not record pending migration in ${displayRelative(filename)}: ${String(error)}`, + report, + ); + } + } + function warnPreservedConfig(error: unknown): void { + recordPendingMigration(); + warnMigration( + `Could not migrate ${displayRelative(fullOxlintPath)}: ${String(error)}. The original config was preserved; use vp lint -c ${displayRelative(fullOxlintPath)} until it is migrated manually.`, + report, + ); + } + let migration: OxlintConfigMigration; + try { + const prepared = preparedOxlintConfigs?.get(fullOxlintPath); + if (prepared instanceof Error) { + throw prepared; + } + migration = prepared ?? { file: resolveOxlintConfigExtends(fullOxlintPath) }; + } catch (error) { + warnPreservedConfig(error); + return; + } + if (migration.migrated) { + return; + } + const { file } = migration; + // Keep shared rules in one module, including for the workspace that owns it. + // Fields that Oxlint does not inherit must also remain in the owner's local block. + const sourceConfig: OxlintConfig = structuredClone(file.config); + let oxlintJson = sourceConfig; + if (file.shared) { + oxlintJson = { + extends: [oxlintJson], + ...Object.fromEntries( + Object.entries(file.config).filter( + ([key, value]) => OXLINT_OWNER_CONFIG_KEYS.has(key) && value !== undefined, + ), + ), + }; + } + // Never discard an inheritance tree when a lint block already exists. + if (oxlintJson.extends?.length && hasConfigKey(path.join(projectPath, viteConfig), 'lint')) { + recordPendingMigration(); + warnMigration( + `Preserved ${displayRelative(fullOxlintPath)} because ${viteConfig} already has a lint config. Merge its inherited rules manually.`, + report, + ); + return; + } + const layers = oxlintConfigLayers(oxlintJson); + const inheritedOptions: NonNullable = {}; + for (const layer of layers) { + Object.assign(inheritedOptions, layer.options); + } + oxlintJson.options ||= {}; + // Skip typeAware/typeCheck when tsconfig.json has baseUrl (unsupported by tsgolint) + if (!hasBaseUrlInTsconfig(projectPath)) { + if (inheritedOptions.typeAware === undefined) { + oxlintJson.options.typeAware = true; + } + if (inheritedOptions.typeCheck === undefined) { + oxlintJson.options.typeCheck = true; + } + } else { + warnMigration(BASEURL_TSCONFIG_WARNING, report); + } + // Drop references to plugins / jsPlugins / rules that won't resolve + // at lint time (e.g. `@oxlint/migrate` translating `@unocss/eslint-config` + // → `eslint-plugin-unocss` even when that package isn't installed). + // Resolve workspace package paths against `workspaceRoot` when the + // caller is processing a sub-package — otherwise the sanitizer would + // mistakenly look for `subPath/` and miss the + // hoisted deps it's supposed to see. + // A pre-existing inheritance tree can declare JS plugins in another layer, + // including local plugins whose namespace is only known at runtime. The + // ESLint migration sanitizer cannot validate these rules independently. + if (!oxlintJson.extends?.length) { sanitizeMigratedOxlintConfig( oxlintJson, collectInstalledPackageNames(workspaceRoot ?? projectPath, packages), report, ); - ensureSvelteRuneGlobals(oxlintJson); - const normalizedOxlintConfig = ensureVitePlusImportRuleDefaults(oxlintJson); - // writeJsonFile preserves the user file's existing indent/newline (and adds a - // trailing newline) instead of forcing 2-space + no EOL. - writeJsonFile(fullOxlintPath, normalizedOxlintConfig as Record); - // merge oxlint config into vite.config.ts - mergeAndRemoveJsonConfig(projectPath, viteConfig, configs.oxlintConfig, 'lint', silent, report); + } else { + sanitizeInheritedJsPlugins( + sourceConfig, + collectInstalledPackageNames(workspaceRoot ?? projectPath, packages), + path.dirname(file.filename), + report, + ); } - if (configs.oxfmtConfig) { - // merge oxfmt config into vite.config.ts - mergeAndRemoveJsonConfig(projectPath, viteConfig, configs.oxfmtConfig, 'fmt', silent, report); + for (const layer of layers) { + ensureSvelteRuneGlobals(layer); + } + const hasInheritedImportRule = layers.some( + (layer) => layer.rules?.[PREFER_VITE_PLUS_IMPORTS_RULE] !== undefined, + ); + const normalizedOxlintConfig = ensureVitePlusImportRuleDefaults(oxlintJson); + if (hasInheritedImportRule && oxlintJson.rules?.[PREFER_VITE_PLUS_IMPORTS_RULE] === undefined) { + // Keep the shared rule live: copying its value into the root would mask later edits. + delete normalizedOxlintConfig.rules![PREFER_VITE_PLUS_IMPORTS_RULE]; + } + try { + const plan: OxlintModulePlan = + file.shared || file.extends.length > 0 + ? prepareOxlintConfigModules( + file, + normalizedOxlintConfig, + path.join(projectPath, viteConfig), + workspaceRoot ?? projectPath, + ) + : { + configContent: JSON.stringify(normalizedOxlintConfig, null, 2), + imports: [], + modules: new Map(), + }; + if (preparedOxlintConfigs) { + plan.completionMarker = oxlintMigrationMarker( + fullOxlintPath, + file, + workspaceRoot ?? projectPath, + ); + } + migration.migrated = mergeAndRemoveJsonConfig( + projectPath, + viteConfig, + oxlintConfig, + 'lint', + silent, + report, + plan, + preparedOxlintConfigs !== undefined, + ); + if (!migration.migrated) { + recordPendingMigration(); + } + } catch (error) { + warnPreservedConfig(error); } } @@ -311,6 +513,11 @@ export function injectLintTypeCheckDefaults( silent = false, report?: MigrationReport, ): void { + // An unresolved JSON inheritance tree needs manual attention. Do not mask it + // with a new lint block, which would also prevent a later migration retry. + if (detectConfigs(projectPath).oxlintConfig) { + return; + } if (hasBaseUrlInTsconfig(projectPath)) { warnMigration(BASEURL_TSCONFIG_WARNING, report); return; @@ -399,6 +606,35 @@ function injectConfigDefaults( } } +function configImportOffset(filename: string, source: string): number { + const { ast, comments } = JSON.parse(analyzeMigrationSource(filename, source)) as { + ast: { node: Program }; + comments: Array<{ start: number; end: number }>; + }; + let offset = ast.node.hashbang?.end ?? 0; + for (const statement of ast.node.body) { + if (statement.type !== 'ExpressionStatement' || typeof statement.directive !== 'string') { + break; + } + offset = statement.end; + } + const firstStatement = ast.node.body[0]?.start ?? source.length; + for (const comment of comments) { + if (comment.end > firstStatement) { + break; + } + const text = source.slice(comment.start, comment.end); + // File pragmas must precede imports; next-statement directives must stay in place. + if ( + /^\/\/\/\s* line !== pendingMarker) + .join('\n'); + } + if (modulePlan && (modulePlan.imports.length || modulePlan.completionMarker)) { + const offset = configImportOffset(fullViteConfigPath, content); + const prefix = [modulePlan.completionMarker, ...modulePlan.imports] + .filter(Boolean) + .join('\n'); + const header = content.slice(0, offset); + content = + header + + (offset && !header.endsWith('\n') ? '\n' : '') + + prefix + + '\n' + + content.slice(offset); + } + fs.writeFileSync(fullViteConfigPath, content); + if (!deferRemoval) { + fs.unlinkSync(fullJsonConfigPath); + } + } catch (error) { + for (const filename of writtenModules) { + fs.unlinkSync(filename); + } + fs.writeFileSync(fullViteConfigPath, originalViteConfig); + throw error; + } + for (const [filename, module] of modulePlan?.modules ?? []) { + module.file.modulePath = filename; + } + if (report) { + report.mergedConfigCount++; + } + if (!silent) { + prompts.log.success( + `✔ Merged ${displayRelative(fullJsonConfigPath)} into ${displayRelative(fullViteConfigPath)}`, + ); } + return true; } /**