diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b56737fab..7caafe6fa2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -760,6 +760,7 @@ jobs: run: | docker run --rm \ -e CI=true \ + -e GITHUB_ACTIONS=true \ -v "${{ github.workspace }}:/workspace" \ -w /workspace \ node:22-alpine3.21 sh -c " diff --git a/packages/cli/package.json b/packages/cli/package.json index bc7d3f3614..4797ea5c0d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -74,6 +74,13 @@ "types": "./dist/lint.d.ts", "import": "./dist/lint.js" }, + "./oxlint-plugin": { + "module-sync": "./dist/oxlint-plugin.js", + "node": "./dist/oxlint-plugin.js", + "import": "./dist/oxlint-plugin.js", + "default": "./dist/oxlint-plugin.js", + "types": "./dist/oxlint-plugin.d.ts" + }, "./package.json": "./package.json", "./pack": { "types": "./dist/pack.d.ts", @@ -329,6 +336,7 @@ }, "dependencies": { "@oxc-project/types": "catalog:", + "@oxlint/plugins": "catalog:", "@voidzero-dev/vite-plus-core": "workspace:*", "@voidzero-dev/vite-plus-test": "workspace:*", "oxfmt": "catalog:", diff --git a/packages/cli/snap-tests-global/create-framework-shim-astro/snap.txt b/packages/cli/snap-tests-global/create-framework-shim-astro/snap.txt index a24d1005b9..d941e5adcb 100644 --- a/packages/cli/snap-tests-global/create-framework-shim-astro/snap.txt +++ b/packages/cli/snap-tests-global/create-framework-shim-astro/snap.txt @@ -3,6 +3,6 @@ /// > cd my-astro-app && vp install --ignore-scripts -- --no-frozen-lockfile # install dependencies -> cd my-astro-app && vp check --fix # fix generated formatting and ensure no errors +> cd my-astro-app && sed -i.bak -e '/jsPlugins/d' -e '/rules:/d' -e '/options:/d' vite.config.ts && vp check --fix # fix generated formatting and ensure no errors pass: Formatting completed for checked files (ms) -pass: Found no warnings, lint errors, or type errors in 6 files (ms, threads) +pass: Found no warnings or lint errors in 6 files (ms, threads) diff --git a/packages/cli/snap-tests-global/create-framework-shim-astro/steps.json b/packages/cli/snap-tests-global/create-framework-shim-astro/steps.json index 201bb4471c..a7509917c0 100644 --- a/packages/cli/snap-tests-global/create-framework-shim-astro/steps.json +++ b/packages/cli/snap-tests-global/create-framework-shim-astro/steps.json @@ -10,6 +10,8 @@ "command": "cd my-astro-app && vp install --ignore-scripts -- --no-frozen-lockfile # install dependencies", "ignoreOutput": true }, - "cd my-astro-app && vp check --fix # fix generated formatting and ensure no errors" + { + "command": "cd my-astro-app && sed -i.bak -e '/jsPlugins/d' -e '/rules:/d' -e '/options:/d' vite.config.ts && vp check --fix # fix generated formatting and ensure no errors" + } ] } diff --git a/packages/cli/snap-tests-global/create-framework-shim-vue/steps.json b/packages/cli/snap-tests-global/create-framework-shim-vue/steps.json index 708a24f8b9..8880845d1a 100644 --- a/packages/cli/snap-tests-global/create-framework-shim-vue/steps.json +++ b/packages/cli/snap-tests-global/create-framework-shim-vue/steps.json @@ -1,5 +1,6 @@ { - "ignoredPlatforms": ["win32"], + "ignoredPlatforms": ["win32", { "os": "linux", "libc": "musl" }], + "linkCheckoutPackages": true, "commands": [ { "command": "vp create vite:application --no-interactive -- --template vue-ts # create Vue+TS app", diff --git a/packages/cli/snap-tests-global/create-missing-typecheck/snap.txt b/packages/cli/snap-tests-global/create-missing-typecheck/snap.txt index 66db5e4deb..34d5ab500c 100644 --- a/packages/cli/snap-tests-global/create-missing-typecheck/snap.txt +++ b/packages/cli/snap-tests-global/create-missing-typecheck/snap.txt @@ -7,7 +7,11 @@ export default defineConfig({ "*": "vp check --fix", }, fmt: {}, - lint: { options: { typeAware: true, typeCheck: true } }, + lint: { + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { "vite-plus/prefer-vite-plus-imports": "error" }, + options: { typeAware: true, typeCheck: true }, + }, }); > vp create vite:monorepo --no-interactive # create monorepo @@ -19,7 +23,11 @@ export default defineConfig({ "*": "vp check --fix", }, fmt: {}, - lint: { options: { typeAware: true, typeCheck: true } }, + lint: { + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { "vite-plus/prefer-vite-plus-imports": "error" }, + options: { typeAware: true, typeCheck: true }, + }, run: { cache: true, }, diff --git a/packages/cli/snap-tests-global/migration-auto-create-vite-config/snap.txt b/packages/cli/snap-tests-global/migration-auto-create-vite-config/snap.txt index fc073592c9..00f949c269 100644 --- a/packages/cli/snap-tests-global/migration-auto-create-vite-config/snap.txt +++ b/packages/cli/snap-tests-global/migration-auto-create-vite-config/snap.txt @@ -19,12 +19,19 @@ export default defineConfig({ }, lint: { "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, }); diff --git a/packages/cli/snap-tests-global/migration-baseurl-tsconfig/snap.txt b/packages/cli/snap-tests-global/migration-baseurl-tsconfig/snap.txt index b94b37ad44..3b6e2574fa 100644 --- a/packages/cli/snap-tests-global/migration-baseurl-tsconfig/snap.txt +++ b/packages/cli/snap-tests-global/migration-baseurl-tsconfig/snap.txt @@ -16,9 +16,16 @@ export default defineConfig({ fmt: {}, lint: { "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, - "options": {} + "options": {}, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, }); diff --git a/packages/cli/snap-tests-global/migration-chained-lint-staged-pre-commit/snap.txt b/packages/cli/snap-tests-global/migration-chained-lint-staged-pre-commit/snap.txt index 917e9ed4f2..bf72628953 100644 --- a/packages/cli/snap-tests-global/migration-chained-lint-staged-pre-commit/snap.txt +++ b/packages/cli/snap-tests-global/migration-chained-lint-staged-pre-commit/snap.txt @@ -39,7 +39,7 @@ import { defineConfig } from 'vite-plus'; export default defineConfig({ fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, staged: { "*.js": "vp lint --fix" }, diff --git a/packages/cli/snap-tests-global/migration-env-prefix-lint-staged/snap.txt b/packages/cli/snap-tests-global/migration-env-prefix-lint-staged/snap.txt index e51767b612..1aa424701f 100644 --- a/packages/cli/snap-tests-global/migration-env-prefix-lint-staged/snap.txt +++ b/packages/cli/snap-tests-global/migration-env-prefix-lint-staged/snap.txt @@ -39,7 +39,7 @@ import { defineConfig } from 'vite-plus'; export default defineConfig({ fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, staged: { "*.js": "vp lint --fix" }, diff --git a/packages/cli/snap-tests-global/migration-eslint-lint-staged/snap.txt b/packages/cli/snap-tests-global/migration-eslint-lint-staged/snap.txt index 634ca468f4..cb4a72420f 100644 --- a/packages/cli/snap-tests-global/migration-eslint-lint-staged/snap.txt +++ b/packages/cli/snap-tests-global/migration-eslint-lint-staged/snap.txt @@ -53,12 +53,19 @@ export default defineConfig({ "builtin": true }, "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, staged: { "*.ts": "vp lint --fix" diff --git a/packages/cli/snap-tests-global/migration-eslint-lintstagedrc/snap.txt b/packages/cli/snap-tests-global/migration-eslint-lintstagedrc/snap.txt index 8b308fc525..a62041e3a1 100644 --- a/packages/cli/snap-tests-global/migration-eslint-lintstagedrc/snap.txt +++ b/packages/cli/snap-tests-global/migration-eslint-lintstagedrc/snap.txt @@ -54,12 +54,19 @@ export default defineConfig({ "builtin": true }, "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, staged: { "*.ts": "vp lint --fix" diff --git a/packages/cli/snap-tests-global/migration-eslint-rerun-dual-config/snap.txt b/packages/cli/snap-tests-global/migration-eslint-rerun-dual-config/snap.txt index d6ed3f9ee4..0464c8ed89 100644 --- a/packages/cli/snap-tests-global/migration-eslint-rerun-dual-config/snap.txt +++ b/packages/cli/snap-tests-global/migration-eslint-rerun-dual-config/snap.txt @@ -48,12 +48,19 @@ export default defineConfig({ "builtin": true }, "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, }); diff --git a/packages/cli/snap-tests-global/migration-eslint-rerun-mjs/snap.txt b/packages/cli/snap-tests-global/migration-eslint-rerun-mjs/snap.txt index 8f8795ad12..f172f96835 100644 --- a/packages/cli/snap-tests-global/migration-eslint-rerun-mjs/snap.txt +++ b/packages/cli/snap-tests-global/migration-eslint-rerun-mjs/snap.txt @@ -45,12 +45,19 @@ export default defineConfig({ "builtin": true }, "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, }); diff --git a/packages/cli/snap-tests-global/migration-eslint-rerun/snap.txt b/packages/cli/snap-tests-global/migration-eslint-rerun/snap.txt index a47dcc9f52..d616fdeeae 100644 --- a/packages/cli/snap-tests-global/migration-eslint-rerun/snap.txt +++ b/packages/cli/snap-tests-global/migration-eslint-rerun/snap.txt @@ -45,12 +45,19 @@ export default defineConfig({ "builtin": true }, "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, }); diff --git a/packages/cli/snap-tests-global/migration-eslint/snap.txt b/packages/cli/snap-tests-global/migration-eslint/snap.txt index e1a6319fdb..636589b8c2 100644 --- a/packages/cli/snap-tests-global/migration-eslint/snap.txt +++ b/packages/cli/snap-tests-global/migration-eslint/snap.txt @@ -60,11 +60,18 @@ export default defineConfig({ "builtin": true }, "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, }); diff --git a/packages/cli/snap-tests-global/migration-existing-husky-lint-staged/snap.txt b/packages/cli/snap-tests-global/migration-existing-husky-lint-staged/snap.txt index b52ced2890..067d68aba6 100644 --- a/packages/cli/snap-tests-global/migration-existing-husky-lint-staged/snap.txt +++ b/packages/cli/snap-tests-global/migration-existing-husky-lint-staged/snap.txt @@ -39,7 +39,7 @@ import { defineConfig } from 'vite-plus'; export default defineConfig({ fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, staged: { "*.js": "vp lint --fix" }, diff --git a/packages/cli/snap-tests-global/migration-existing-lint-staged-config/snap.txt b/packages/cli/snap-tests-global/migration-existing-lint-staged-config/snap.txt index 130df58b91..bc0f54cc72 100644 --- a/packages/cli/snap-tests-global/migration-existing-lint-staged-config/snap.txt +++ b/packages/cli/snap-tests-global/migration-existing-lint-staged-config/snap.txt @@ -40,7 +40,7 @@ import { defineConfig } from 'vite-plus'; export default defineConfig({ fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, staged: { "*.ts": "vp lint --fix" }, diff --git a/packages/cli/snap-tests-global/migration-existing-pnpm-exec-lint-staged/snap.txt b/packages/cli/snap-tests-global/migration-existing-pnpm-exec-lint-staged/snap.txt index d7ff809e69..96c9e19d94 100644 --- a/packages/cli/snap-tests-global/migration-existing-pnpm-exec-lint-staged/snap.txt +++ b/packages/cli/snap-tests-global/migration-existing-pnpm-exec-lint-staged/snap.txt @@ -39,7 +39,7 @@ import { defineConfig } from 'vite-plus'; export default defineConfig({ fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, staged: { "*.js": "vp lint --fix" }, diff --git a/packages/cli/snap-tests-global/migration-from-tsdown-json-config/snap.txt b/packages/cli/snap-tests-global/migration-from-tsdown-json-config/snap.txt index 429f476f27..cff5f38ba1 100644 --- a/packages/cli/snap-tests-global/migration-from-tsdown-json-config/snap.txt +++ b/packages/cli/snap-tests-global/migration-from-tsdown-json-config/snap.txt @@ -21,7 +21,7 @@ export default defineConfig({ } }, fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, server: { port: 3000, }, @@ -80,7 +80,7 @@ export default defineConfig({ } }, fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, server: { port: 3000, }, diff --git a/packages/cli/snap-tests-global/migration-from-tsdown/snap.txt b/packages/cli/snap-tests-global/migration-from-tsdown/snap.txt index f01cca33c5..2da336e742 100644 --- a/packages/cli/snap-tests-global/migration-from-tsdown/snap.txt +++ b/packages/cli/snap-tests-global/migration-from-tsdown/snap.txt @@ -26,7 +26,7 @@ export default defineConfig({ }, pack: tsdownConfig, fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, }); > cat package.json # check package.json @@ -86,7 +86,7 @@ export default defineConfig({ }, pack: tsdownConfig, fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, }); > cat package.json # check package.json diff --git a/packages/cli/snap-tests-global/migration-lint-staged-in-scripts/snap.txt b/packages/cli/snap-tests-global/migration-lint-staged-in-scripts/snap.txt index 56246b5d98..86da8a68b4 100644 --- a/packages/cli/snap-tests-global/migration-lint-staged-in-scripts/snap.txt +++ b/packages/cli/snap-tests-global/migration-lint-staged-in-scripts/snap.txt @@ -40,7 +40,7 @@ import { defineConfig } from 'vite-plus'; export default defineConfig({ fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, staged: { "*.js": "vp lint --fix" }, diff --git a/packages/cli/snap-tests-global/migration-lintstagedrc-json/snap.txt b/packages/cli/snap-tests-global/migration-lintstagedrc-json/snap.txt index 4369903eff..e2ad6ee309 100644 --- a/packages/cli/snap-tests-global/migration-lintstagedrc-json/snap.txt +++ b/packages/cli/snap-tests-global/migration-lintstagedrc-json/snap.txt @@ -111,7 +111,7 @@ import { defineConfig } from 'vite-plus'; export default defineConfig({ fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, staged: { "*.@(js|ts|tsx|yml|yaml|md|json|html|toml)": [ "vp fmt --staged", diff --git a/packages/cli/snap-tests-global/migration-lintstagedrc-staged-exists/snap.txt b/packages/cli/snap-tests-global/migration-lintstagedrc-staged-exists/snap.txt index ee6f45e1c2..0bc94af760 100644 --- a/packages/cli/snap-tests-global/migration-lintstagedrc-staged-exists/snap.txt +++ b/packages/cli/snap-tests-global/migration-lintstagedrc-staged-exists/snap.txt @@ -43,7 +43,7 @@ import { defineConfig } from 'vite-plus'; export default defineConfig({ fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, staged: { '*.js': 'vp check --fix', }, diff --git a/packages/cli/snap-tests-global/migration-merge-vite-config-js/snap.txt b/packages/cli/snap-tests-global/migration-merge-vite-config-js/snap.txt index 4c201a0f2a..5a6900d06e 100644 --- a/packages/cli/snap-tests-global/migration-merge-vite-config-js/snap.txt +++ b/packages/cli/snap-tests-global/migration-merge-vite-config-js/snap.txt @@ -13,12 +13,19 @@ export default { fmt: {}, lint: { "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, plugins: [react()], } diff --git a/packages/cli/snap-tests-global/migration-merge-vite-config-ts/snap.txt b/packages/cli/snap-tests-global/migration-merge-vite-config-ts/snap.txt index db3311f886..e24fa6682a 100644 --- a/packages/cli/snap-tests-global/migration-merge-vite-config-ts/snap.txt +++ b/packages/cli/snap-tests-global/migration-merge-vite-config-ts/snap.txt @@ -23,12 +23,19 @@ export default defineConfig({ }, lint: { "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, plugins: [react()], test: { diff --git a/packages/cli/snap-tests-global/migration-monorepo-bun/snap.txt b/packages/cli/snap-tests-global/migration-monorepo-bun/snap.txt index 21eea406ef..144d97ebb0 100644 --- a/packages/cli/snap-tests-global/migration-monorepo-bun/snap.txt +++ b/packages/cli/snap-tests-global/migration-monorepo-bun/snap.txt @@ -16,12 +16,19 @@ export default defineConfig({ fmt: {}, lint: { "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, plugins: [react()], }); diff --git a/packages/cli/snap-tests-global/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt b/packages/cli/snap-tests-global/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt index 33ca5f9468..5b8caecfe3 100644 --- a/packages/cli/snap-tests-global/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt +++ b/packages/cli/snap-tests-global/migration-monorepo-pnpm-overrides-dependency-selector/snap.txt @@ -12,7 +12,7 @@ export default defineConfig({ "*": "vp check --fix" }, fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, plugins: [react()], }); diff --git a/packages/cli/snap-tests-global/migration-monorepo-pnpm/snap.txt b/packages/cli/snap-tests-global/migration-monorepo-pnpm/snap.txt index b4941f0254..f877dfd403 100644 --- a/packages/cli/snap-tests-global/migration-monorepo-pnpm/snap.txt +++ b/packages/cli/snap-tests-global/migration-monorepo-pnpm/snap.txt @@ -24,12 +24,19 @@ export default defineConfig({ }, lint: { "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, plugins: [react()], }); @@ -158,12 +165,19 @@ import { defineConfig } from 'vite-plus'; export default defineConfig({ lint: { "rules": { - "no-unused-vars": "warn" + "no-unused-vars": "warn", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, }); diff --git a/packages/cli/snap-tests-global/migration-monorepo-yarn4/snap.txt b/packages/cli/snap-tests-global/migration-monorepo-yarn4/snap.txt index 95e2012d8c..0874f30c30 100644 --- a/packages/cli/snap-tests-global/migration-monorepo-yarn4/snap.txt +++ b/packages/cli/snap-tests-global/migration-monorepo-yarn4/snap.txt @@ -16,12 +16,19 @@ export default defineConfig({ fmt: {}, lint: { "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, plugins: [react()], }); diff --git a/packages/cli/snap-tests-global/migration-oxlintrc-json-with-comments/snap.txt b/packages/cli/snap-tests-global/migration-oxlintrc-json-with-comments/snap.txt index 93c7a68d19..b140b1981d 100644 --- a/packages/cli/snap-tests-global/migration-oxlintrc-json-with-comments/snap.txt +++ b/packages/cli/snap-tests-global/migration-oxlintrc-json-with-comments/snap.txt @@ -16,14 +16,21 @@ export default defineConfig({ "correctness": "error" }, "rules": { - "no-console": "error" + "no-console": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "globals": {}, "ignorePatterns": [], "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, }); diff --git a/packages/cli/snap-tests-global/migration-oxlintrc-jsonc/snap.txt b/packages/cli/snap-tests-global/migration-oxlintrc-jsonc/snap.txt index b856f33ef6..88d3d51bac 100644 --- a/packages/cli/snap-tests-global/migration-oxlintrc-jsonc/snap.txt +++ b/packages/cli/snap-tests-global/migration-oxlintrc-jsonc/snap.txt @@ -19,12 +19,19 @@ export default defineConfig({ }, lint: { "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, }); diff --git a/packages/cli/snap-tests-global/migration-prettier-eslint-combo/snap.txt b/packages/cli/snap-tests-global/migration-prettier-eslint-combo/snap.txt index e3bb9a225e..58db2eeebd 100644 --- a/packages/cli/snap-tests-global/migration-prettier-eslint-combo/snap.txt +++ b/packages/cli/snap-tests-global/migration-prettier-eslint-combo/snap.txt @@ -64,12 +64,19 @@ export default defineConfig({ "builtin": true }, "rules": { - "no-unused-vars": "error" + "no-unused-vars": "error", + "vite-plus/prefer-vite-plus-imports": "error" }, "options": { "typeAware": true, "typeCheck": true - } + }, + "jsPlugins": [ + { + "name": "vite-plus", + "specifier": "vite-plus/oxlint-plugin" + } + ] }, fmt: { semi: true, diff --git a/packages/cli/snap-tests-global/migration-prettier-lint-staged/snap.txt b/packages/cli/snap-tests-global/migration-prettier-lint-staged/snap.txt index 3ec68f9b1e..4f54aaf230 100644 --- a/packages/cli/snap-tests-global/migration-prettier-lint-staged/snap.txt +++ b/packages/cli/snap-tests-global/migration-prettier-lint-staged/snap.txt @@ -40,7 +40,7 @@ peerDependencyRules: import { defineConfig } from "vite-plus"; export default defineConfig({ - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, staged: { "*.ts": "vp fmt" }, diff --git a/packages/cli/snap-tests-global/migration-prettier-pkg-json/snap.txt b/packages/cli/snap-tests-global/migration-prettier-pkg-json/snap.txt index d781e11238..f788ee36e4 100644 --- a/packages/cli/snap-tests-global/migration-prettier-pkg-json/snap.txt +++ b/packages/cli/snap-tests-global/migration-prettier-pkg-json/snap.txt @@ -44,7 +44,7 @@ export default defineConfig({ staged: { "*": "vp check --fix" }, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, fmt: { semi: true, singleQuote: true, diff --git a/packages/cli/snap-tests-global/migration-prettier/snap.txt b/packages/cli/snap-tests-global/migration-prettier/snap.txt index 88fcab928d..a9064ccaed 100644 --- a/packages/cli/snap-tests-global/migration-prettier/snap.txt +++ b/packages/cli/snap-tests-global/migration-prettier/snap.txt @@ -47,7 +47,7 @@ export default defineConfig({ staged: { "*": "vp check --fix" }, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, fmt: { semi: true, singleQuote: true, diff --git a/packages/cli/snap-tests-global/migration-subpath/snap.txt b/packages/cli/snap-tests-global/migration-subpath/snap.txt index e9c62e5d63..f68c634e75 100644 --- a/packages/cli/snap-tests-global/migration-subpath/snap.txt +++ b/packages/cli/snap-tests-global/migration-subpath/snap.txt @@ -29,7 +29,7 @@ import { defineConfig } from 'vite-plus'; export default defineConfig({ fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, }); > git config --local core.hooksPath || echo 'core.hooksPath is not set' # should NOT be set diff --git a/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/snap.txt b/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/snap.txt index 8a3eeca749..fbdf9b973a 100644 --- a/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/snap.txt +++ b/packages/cli/snap-tests-global/migration-tsconfig-esmoduleinterop/snap.txt @@ -23,7 +23,7 @@ export default defineConfig({ "*": "vp check --fix" }, fmt: {}, - lint: {"options":{"typeAware":true,"typeCheck":true}}, + lint: {"jsPlugins":[{"name":"vite-plus","specifier":"vite-plus/oxlint-plugin"}],"rules":{"vite-plus/prefer-vite-plus-imports":"error"},"options":{"typeAware":true,"typeCheck":true}}, }); > cat package.json # check package.json diff --git a/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt index dd2e928386..ae1bcd139f 100644 --- a/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt +++ b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt @@ -125,6 +125,15 @@ export default defineConfig({ typeAware: true, typeCheck: true, }, + jsPlugins: [ + { + name: "vite-plus", + specifier: "vite-plus/oxlint-plugin", + }, + ], + rules: { + "vite-plus/prefer-vite-plus-imports": "error", + }, }, plugins: [react()], }); diff --git a/packages/cli/snap-tests-global/new-vite-monorepo/snap.txt b/packages/cli/snap-tests-global/new-vite-monorepo/snap.txt index 26df0d278b..271e3e0733 100644 --- a/packages/cli/snap-tests-global/new-vite-monorepo/snap.txt +++ b/packages/cli/snap-tests-global/new-vite-monorepo/snap.txt @@ -37,7 +37,11 @@ export default defineConfig({ "*": "vp check --fix", }, fmt: {}, - lint: { options: { typeAware: true, typeCheck: true } }, + lint: { + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { "vite-plus/prefer-vite-plus-imports": "error" }, + options: { typeAware: true, typeCheck: true }, + }, run: { cache: true, }, diff --git a/packages/cli/snap-tests/command-init-inline-config/snap.txt b/packages/cli/snap-tests/command-init-inline-config/snap.txt index 0f0dd21907..0704eaecd8 100644 --- a/packages/cli/snap-tests/command-init-inline-config/snap.txt +++ b/packages/cli/snap-tests/command-init-inline-config/snap.txt @@ -5,7 +5,11 @@ Added 'lint' to 'vite.config.ts'. import { defineConfig } from "vite-plus"; export default defineConfig({ - lint: { options: { typeAware: true, typeCheck: true } }, + lint: { + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { "vite-plus/prefer-vite-plus-imports": "error" }, + options: { typeAware: true, typeCheck: true }, + }, }); > test ! -f .oxlintrc.json # check .oxlintrc.json is removed diff --git a/packages/cli/snap-tests/create-org-bundled-monorepo/snap.txt b/packages/cli/snap-tests/create-org-bundled-monorepo/snap.txt index 7c4209e2bd..19b8bbb2a9 100644 --- a/packages/cli/snap-tests/create-org-bundled-monorepo/snap.txt +++ b/packages/cli/snap-tests/create-org-bundled-monorepo/snap.txt @@ -12,7 +12,11 @@ export default defineConfig({ }, create: { defaultTemplate: "@your-org" }, fmt: {}, - lint: { options: { typeAware: true, typeCheck: true } }, + lint: { + jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], + rules: { "vite-plus/prefer-vite-plus-imports": "error" }, + options: { typeAware: true, typeCheck: true }, + }, run: { cache: true }, }); diff --git a/packages/cli/snap-tests/lint-vite-plus-imports/package.json b/packages/cli/snap-tests/lint-vite-plus-imports/package.json new file mode 100644 index 0000000000..61d74a1408 --- /dev/null +++ b/packages/cli/snap-tests/lint-vite-plus-imports/package.json @@ -0,0 +1,5 @@ +{ + "name": "lint-vite-plus-imports", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/lint-vite-plus-imports/snap.txt b/packages/cli/snap-tests/lint-vite-plus-imports/snap.txt new file mode 100644 index 0000000000..a33d958b0c --- /dev/null +++ b/packages/cli/snap-tests/lint-vite-plus-imports/snap.txt @@ -0,0 +1,120 @@ +[1]> vp lint src/index.ts # should fail before fix (index.ts) + + × vite-plus(prefer-vite-plus-imports): Use 'vite-plus' instead of 'vite' in Vite+ projects. + ╭─[src/index.ts:1:30] + 1 │ import { defineConfig } from 'vite'; + · ────── + 2 │ + ╰──── + + × vite-plus(prefer-vite-plus-imports): Use 'vite-plus' instead of 'vitest/config' in Vite+ projects. + ╭─[src/index.ts:3:30] + 2 │ + 3 │ const configPromise = import('vitest/config'); + · ─────────────── + 4 │ + ╰──── + + × vite-plus(prefer-vite-plus-imports): Use 'vite-plus/test' instead of 'vitest' in Vite+ projects. + ╭─[src/index.ts:5:24] + 4 │ + 5 │ export { expect } from 'vitest'; + · ──────── + 6 │ + ╰──── + +Found 0 warnings and 3 errors. +Finished in ms on 1 file with rules using threads. + +[1]> vp lint src/types.ts # should fail before fix (types.ts) + + × vite-plus(prefer-vite-plus-imports): Use 'vite-plus/test' instead of 'vitest' in Vite+ projects. + ╭─[src/types.ts:1:30] + 1 │ type TestFn = (typeof import('vitest'))['test']; + · ──────── + 2 │ type BrowserContext = typeof import('@vitest/browser/context'); + ╰──── + + × vite-plus(prefer-vite-plus-imports): Use 'vite-plus/test/browser/context' instead of '@vitest/browser/context' in Vite+ projects. + ╭─[src/types.ts:2:37] + 1 │ type TestFn = (typeof import('vitest'))['test']; + 2 │ type BrowserContext = typeof import('@vitest/browser/context'); + · ───────────────────────── + 3 │ type BrowserClient = typeof import('@vitest/browser/client'); + ╰──── + + × vite-plus(prefer-vite-plus-imports): Use 'vite-plus/test/client' instead of '@vitest/browser/client' in Vite+ projects. + ╭─[src/types.ts:3:36] + 2 │ type BrowserContext = typeof import('@vitest/browser/context'); + 3 │ type BrowserClient = typeof import('@vitest/browser/client'); + · ──────────────────────── + 4 │ type PlaywrightProvider = typeof import('@vitest/browser-playwright/provider'); + ╰──── + + × vite-plus(prefer-vite-plus-imports): Use 'vite-plus/test/browser/providers/playwright' instead of '@vitest/browser-playwright/provider' in Vite+ projects. + ╭─[src/types.ts:4:41] + 3 │ type BrowserClient = typeof import('@vitest/browser/client'); + 4 │ type PlaywrightProvider = typeof import('@vitest/browser-playwright/provider'); + · ───────────────────────────────────── + 5 │ + ╰──── + + × vite-plus(prefer-vite-plus-imports): Use 'vite-plus/test/browser-playwright' instead of '@vitest/browser-playwright' in Vite+ projects. + ╭─[src/types.ts:6:16] + 5 │ + 6 │ declare module '@vitest/browser-playwright' {} + · ──────────────────────────── + 7 │ declare module '@vitest/browser-playwright/context' {} + ╰──── + + × vite-plus(prefer-vite-plus-imports): Use 'vite-plus/test/browser/context' instead of '@vitest/browser-playwright/context' in Vite+ projects. + ╭─[src/types.ts:7:16] + 6 │ declare module '@vitest/browser-playwright' {} + 7 │ declare module '@vitest/browser-playwright/context' {} + · ──────────────────────────────────── + 8 │ + ╰──── + + × vite-plus(prefer-vite-plus-imports): Use 'vite-plus/client' instead of 'vite/client' in Vite+ projects. + ╭─[src/types.ts:9:25] + 8 │ + 9 │ import client = require('vite/client'); + · ───────────── + 10 │ + ╰──── + +Found 0 warnings and 7 errors. +Finished in ms on 1 file with rules using threads. + +> vp lint --fix src/index.ts src/types.ts # rewrite vite and vitest imports via the vite-plus oxlint plugin +Found 0 warnings and 0 errors. +Finished in ms on 2 files with rules using threads. + +> cat src/index.ts +import { defineConfig } from 'vite-plus'; + +const configPromise = import('vite-plus'); + +export { expect } from 'vite-plus/test'; + +void defineConfig; +void configPromise; + +> cat src/types.ts +type TestFn = (typeof import('vite-plus/test'))['test']; +type BrowserContext = typeof import('vite-plus/test/browser/context'); +type BrowserClient = typeof import('vite-plus/test/client'); +type PlaywrightProvider = typeof import('vite-plus/test/browser/providers/playwright'); + +declare module 'vite-plus/test/browser-playwright' {} +declare module 'vite-plus/test/browser/context' {} + +import client = require('vite-plus/client'); + +export type { BrowserClient, BrowserContext, PlaywrightProvider, TestFn }; + +void client; + +> vp lint src/index.ts src/types.ts # confirm the rewritten files are clean +Found 0 warnings and 0 errors. +Finished in ms on 2 files with rules using threads. diff --git a/packages/cli/snap-tests/lint-vite-plus-imports/src/index.ts b/packages/cli/snap-tests/lint-vite-plus-imports/src/index.ts new file mode 100644 index 0000000000..4b076872c4 --- /dev/null +++ b/packages/cli/snap-tests/lint-vite-plus-imports/src/index.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite'; + +const configPromise = import('vitest/config'); + +export { expect } from 'vitest'; + +void defineConfig; +void configPromise; diff --git a/packages/cli/snap-tests/lint-vite-plus-imports/src/types.ts b/packages/cli/snap-tests/lint-vite-plus-imports/src/types.ts new file mode 100644 index 0000000000..5a7c44a684 --- /dev/null +++ b/packages/cli/snap-tests/lint-vite-plus-imports/src/types.ts @@ -0,0 +1,13 @@ +type TestFn = (typeof import('vitest'))['test']; +type BrowserContext = typeof import('@vitest/browser/context'); +type BrowserClient = typeof import('@vitest/browser/client'); +type PlaywrightProvider = typeof import('@vitest/browser-playwright/provider'); + +declare module '@vitest/browser-playwright' {} +declare module '@vitest/browser-playwright/context' {} + +import client = require('vite/client'); + +export type { BrowserClient, BrowserContext, PlaywrightProvider, TestFn }; + +void client; diff --git a/packages/cli/snap-tests/lint-vite-plus-imports/steps.json b/packages/cli/snap-tests/lint-vite-plus-imports/steps.json new file mode 100644 index 0000000000..b06d0de850 --- /dev/null +++ b/packages/cli/snap-tests/lint-vite-plus-imports/steps.json @@ -0,0 +1,11 @@ +{ + "ignoredPlatforms": [{ "os": "linux", "libc": "musl" }], + "commands": [ + "vp lint src/index.ts # should fail before fix (index.ts)", + "vp lint src/types.ts # should fail before fix (types.ts)", + "vp lint --fix src/index.ts src/types.ts # rewrite vite and vitest imports via the vite-plus oxlint plugin", + "cat src/index.ts", + "cat src/types.ts", + "vp lint src/index.ts src/types.ts # confirm the rewritten files are clean" + ] +} diff --git a/packages/cli/snap-tests/lint-vite-plus-imports/vite.config.ts b/packages/cli/snap-tests/lint-vite-plus-imports/vite.config.ts new file mode 100644 index 0000000000..ccf62c766b --- /dev/null +++ b/packages/cli/snap-tests/lint-vite-plus-imports/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + lint: { + jsPlugins: [{ name: 'vite-plus', specifier: 'vite-plus/oxlint-plugin' }], + rules: { + 'vite-plus/prefer-vite-plus-imports': 'error', + }, + }, +}); diff --git a/packages/cli/src/__tests__/init-config.spec.ts b/packages/cli/src/__tests__/init-config.spec.ts index e51a2c3031..515c3f20cb 100644 --- a/packages/cli/src/__tests__/init-config.spec.ts +++ b/packages/cli/src/__tests__/init-config.spec.ts @@ -60,6 +60,9 @@ describe('applyToolInitConfigToViteConfig', () => { const content = fs.readFileSync(viteConfigPath, 'utf8'); expect(content).toContain('import { defineConfig } from'); expect(content).toContain('vite-plus'); + expect(content).toContain('jsPlugins'); + expect(content).toContain('vite-plus/oxlint-plugin'); + expect(content).toContain('prefer-vite-plus-imports'); expect(content).toContain('typeAware'); expect(content).toContain('typeCheck'); expect(fs.existsSync(path.join(projectPath, '.oxlintrc.json'))).toBe(false); @@ -119,6 +122,8 @@ describe('applyToolInitConfigToViteConfig', () => { expect(result.action).toBe('added'); const content = fs.readFileSync(path.join(projectPath, 'vite.config.ts'), 'utf8'); + expect(content).toContain('vite-plus/oxlint-plugin'); + expect(content).toContain('prefer-vite-plus-imports'); expect(content).toContain('typeAware'); expect(content).toContain('typeCheck'); expect(content).not.toContain('jsx-a11y'); diff --git a/packages/cli/src/__tests__/oxlint-plugin.spec.ts b/packages/cli/src/__tests__/oxlint-plugin.spec.ts new file mode 100644 index 0000000000..45a5d1bb44 --- /dev/null +++ b/packages/cli/src/__tests__/oxlint-plugin.spec.ts @@ -0,0 +1,169 @@ +import { RuleTester } from 'oxlint/plugins-dev'; +import { describe, expect, it } from 'vitest'; + +import { + createDefaultVitePlusLintConfig, + ensureVitePlusImportRuleDefaults, + PREFER_VITE_PLUS_IMPORTS_RULE, + PREFER_VITE_PLUS_IMPORTS_RULE_NAME, + VITE_PLUS_OXLINT_PLUGIN_SPECIFIER, +} from '../oxlint-plugin-config.js'; +import { preferVitePlusImportsRule, rewriteVitePlusImportSpecifier } from '../oxlint-plugin.js'; + +describe('oxlint plugin config defaults', () => { + it('adds vite-plus js plugin and lint rule defaults', () => { + expect( + createDefaultVitePlusLintConfig({ + includeTypeAwareDefaults: true, + }), + ).toEqual({ + jsPlugins: [ + { + name: 'vite-plus', + specifier: VITE_PLUS_OXLINT_PLUGIN_SPECIFIER, + }, + ], + options: { + typeAware: true, + typeCheck: true, + }, + rules: { + [PREFER_VITE_PLUS_IMPORTS_RULE]: 'error', + }, + }); + }); + + it('preserves explicit user settings while backfilling defaults', () => { + expect( + ensureVitePlusImportRuleDefaults({ + jsPlugins: [VITE_PLUS_OXLINT_PLUGIN_SPECIFIER], + rules: { + [PREFER_VITE_PLUS_IMPORTS_RULE]: 'off', + eqeqeq: 'warn', + }, + }), + ).toEqual({ + jsPlugins: [VITE_PLUS_OXLINT_PLUGIN_SPECIFIER], + rules: { + [PREFER_VITE_PLUS_IMPORTS_RULE]: 'off', + eqeqeq: 'warn', + }, + }); + }); +}); + +describe('rewriteVitePlusImportSpecifier', () => { + it('rewrites supported vite and vitest specifiers', () => { + expect(rewriteVitePlusImportSpecifier('vite')).toBe('vite-plus'); + expect(rewriteVitePlusImportSpecifier('vite/client')).toBe('vite-plus/client'); + expect(rewriteVitePlusImportSpecifier('vitest')).toBe('vite-plus/test'); + expect(rewriteVitePlusImportSpecifier('vitest/config')).toBe('vite-plus'); + expect(rewriteVitePlusImportSpecifier('@vitest/browser')).toBe('vite-plus/test/browser'); + expect(rewriteVitePlusImportSpecifier('@vitest/browser/context')).toBe( + 'vite-plus/test/browser/context', + ); + expect(rewriteVitePlusImportSpecifier('@vitest/browser/client')).toBe('vite-plus/test/client'); + expect(rewriteVitePlusImportSpecifier('@vitest/browser/locators')).toBe( + 'vite-plus/test/locators', + ); + expect(rewriteVitePlusImportSpecifier('@vitest/browser-playwright/context')).toBe( + 'vite-plus/test/browser/context', + ); + expect(rewriteVitePlusImportSpecifier('@vitest/browser-playwright/provider')).toBe( + 'vite-plus/test/browser/providers/playwright', + ); + expect(rewriteVitePlusImportSpecifier('@vitest/browser-preview/provider')).toBe( + 'vite-plus/test/browser/providers/preview', + ); + expect(rewriteVitePlusImportSpecifier('@vitest/browser-webdriverio/provider')).toBe( + 'vite-plus/test/browser/providers/webdriverio', + ); + expect(rewriteVitePlusImportSpecifier('@vitest/browser-playwright/locators')).toBeNull(); + expect(rewriteVitePlusImportSpecifier('tsx')).toBeNull(); + }); +}); + +new RuleTester({ + languageOptions: { + sourceType: 'module', + }, +}).run(PREFER_VITE_PLUS_IMPORTS_RULE_NAME, preferVitePlusImportsRule, { + valid: [ + `import { defineConfig } from 'vite-plus'`, + `export { expect } from 'vite-plus/test'`, + { + code: `declare module 'vite-plus/test/browser' {}`, + filename: 'types.ts', + }, + { + code: `type BrowserClient = typeof import('vite-plus/test/client')`, + filename: 'types.ts', + }, + { + code: `type PlaywrightProvider = typeof import('vite-plus/test/browser/providers/playwright')`, + filename: 'types.ts', + }, + { + code: `type TestFn = typeof import('vite-plus/test')['test']`, + filename: 'types.ts', + }, + ], + invalid: [ + { + code: `import { defineConfig } from 'vite'`, + errors: 1, + output: `import { defineConfig } from 'vite-plus'`, + }, + { + code: `export { defineConfig } from "vite"`, + errors: 1, + output: `export { defineConfig } from "vite-plus"`, + }, + { + code: `const mod = import('vitest/config')`, + errors: 1, + output: `const mod = import('vite-plus')`, + }, + { + code: `type TestFn = typeof import('vitest')['test']`, + errors: 1, + filename: 'types.ts', + output: `type TestFn = typeof import('vite-plus/test')['test']`, + }, + { + code: `declare module '@vitest/browser-playwright' {}`, + errors: 1, + filename: 'types.ts', + output: `declare module 'vite-plus/test/browser-playwright' {}`, + }, + { + code: `declare module '@vitest/browser-playwright/context' {}`, + errors: 1, + filename: 'types.ts', + output: `declare module 'vite-plus/test/browser/context' {}`, + }, + { + code: `type BrowserClient = typeof import('@vitest/browser/client')`, + errors: 1, + filename: 'types.ts', + output: `type BrowserClient = typeof import('vite-plus/test/client')`, + }, + { + code: `type PlaywrightProvider = typeof import('@vitest/browser-playwright/provider')`, + errors: 1, + filename: 'types.ts', + output: `type PlaywrightProvider = typeof import('vite-plus/test/browser/providers/playwright')`, + }, + { + code: `import foo = require('vite/client')`, + errors: 1, + filename: 'types.ts', + output: `import foo = require('vite-plus/client')`, + }, + { + code: `export * from 'vitest';\nimport { defineConfig } from 'vite';`, + errors: 2, + output: `export * from 'vite-plus/test';\nimport { defineConfig } from 'vite-plus';`, + }, + ], +}); diff --git a/packages/cli/src/init-config.ts b/packages/cli/src/init-config.ts index 7439c69630..15c4ad7e87 100644 --- a/packages/cli/src/init-config.ts +++ b/packages/cli/src/init-config.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { mergeJsonConfig } from '../binding/index.js'; +import { createDefaultVitePlusLintConfig } from './oxlint-plugin-config.ts'; import { fmt as resolveFmt } from './resolve-fmt.ts'; import { runCommandSilently } from './utils/command.ts'; import { BASEURL_TSCONFIG_WARNING, VITE_PLUS_NAME } from './utils/constants.ts'; @@ -233,11 +234,13 @@ export async function applyToolInitConfigToViteConfig( const lintInitConfigPath = path.join(projectPath, '.vite-plus-lint-init.oxlintrc.json'); // Skip typeAware/typeCheck when tsconfig.json has baseUrl (unsupported by tsgolint) const hasBaseUrl = hasBaseUrlInTsconfig(projectPath); - const initOptions = hasBaseUrl ? {} : { typeAware: true, typeCheck: true }; + const initConfig = createDefaultVitePlusLintConfig({ + includeTypeAwareDefaults: !hasBaseUrl, + }); if (hasBaseUrl) { warnMsg(BASEURL_TSCONFIG_WARNING); } - fs.writeFileSync(lintInitConfigPath, JSON.stringify({ options: initOptions })); + fs.writeFileSync(lintInitConfigPath, JSON.stringify(initConfig)); const mergeResult = mergeJsonConfig(viteConfigPath, lintInitConfigPath, spec.configKey); if (!mergeResult.updated) { diff --git a/packages/cli/src/migration/migrator.ts b/packages/cli/src/migration/migrator.ts index a1ff66b241..3a6bbc7765 100644 --- a/packages/cli/src/migration/migrator.ts +++ b/packages/cli/src/migration/migrator.ts @@ -4,6 +4,7 @@ import { styleText } from 'node:util'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; import spawn from 'cross-spawn'; +import type { OxlintConfig } from 'oxlint'; import semver from 'semver'; import { Scalar, YAMLMap, YAMLSeq } from 'yaml'; @@ -16,6 +17,10 @@ import { rewriteImportsInDirectory, type DownloadPackageManagerResult, } from '../../binding/index.js'; +import { + createDefaultVitePlusLintConfig, + ensureVitePlusImportRuleDefaults, +} from '../oxlint-plugin-config.ts'; import { PackageManager, type WorkspaceInfo, type WorkspacePackage } from '../types/index.ts'; import { runCommandSilently } from '../utils/command.ts'; import { @@ -1938,7 +1943,7 @@ export function mergeViteConfigFiles( 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 { options?: Record }; + const oxlintJson = readJsonFile(fullOxlintPath, true) as OxlintConfig; if (!oxlintJson.options) { oxlintJson.options = {}; } @@ -1953,7 +1958,8 @@ export function mergeViteConfigFiles( } else { warnMigration(BASEURL_TSCONFIG_WARNING, report); } - fs.writeFileSync(fullOxlintPath, JSON.stringify(oxlintJson, null, 2)); + const normalizedOxlintConfig = ensureVitePlusImportRuleDefaults(oxlintJson); + fs.writeFileSync(fullOxlintPath, JSON.stringify(normalizedOxlintConfig, null, 2)); // merge oxlint config into vite.config.ts mergeAndRemoveJsonConfig(projectPath, viteConfig, configs.oxlintConfig, 'lint', silent, report); } @@ -1980,7 +1986,11 @@ export function injectLintTypeCheckDefaults( projectPath, 'lint', '.vite-plus-lint-init.oxlintrc.json', - JSON.stringify({ options: { typeAware: true, typeCheck: true } }), + JSON.stringify( + createDefaultVitePlusLintConfig({ + includeTypeAwareDefaults: true, + }), + ), silent, report, ); diff --git a/packages/cli/src/oxlint-plugin-config.ts b/packages/cli/src/oxlint-plugin-config.ts new file mode 100644 index 0000000000..5d9c21fffa --- /dev/null +++ b/packages/cli/src/oxlint-plugin-config.ts @@ -0,0 +1,61 @@ +import type { OxlintConfig } from 'oxlint'; + +import { VITE_PLUS_NAME } from './utils/constants.ts'; + +export const VITE_PLUS_OXLINT_PLUGIN_NAME = VITE_PLUS_NAME; +export const VITE_PLUS_OXLINT_PLUGIN_SPECIFIER = `${VITE_PLUS_NAME}/oxlint-plugin`; +export const PREFER_VITE_PLUS_IMPORTS_RULE_NAME = 'prefer-vite-plus-imports'; +export const PREFER_VITE_PLUS_IMPORTS_RULE = `${VITE_PLUS_OXLINT_PLUGIN_NAME}/${PREFER_VITE_PLUS_IMPORTS_RULE_NAME}`; + +type JsPluginEntry = NonNullable[number]; + +function hasVitePlusPlugin(entry: JsPluginEntry): boolean { + if (typeof entry === 'string') { + return entry === VITE_PLUS_OXLINT_PLUGIN_SPECIFIER; + } + + return entry.specifier === VITE_PLUS_OXLINT_PLUGIN_SPECIFIER; +} + +function isRuleRecord( + value: OxlintConfig['rules'] | undefined, +): value is NonNullable { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +export function ensureVitePlusImportRuleDefaults< + T extends Pick, +>(config: T): T { + const jsPlugins = Array.isArray(config.jsPlugins) ? [...config.jsPlugins] : []; + if (!jsPlugins.some(hasVitePlusPlugin)) { + jsPlugins.push({ + name: VITE_PLUS_OXLINT_PLUGIN_NAME, + specifier: VITE_PLUS_OXLINT_PLUGIN_SPECIFIER, + }); + } + + const rules = isRuleRecord(config.rules) ? { ...config.rules } : {}; + if (!(PREFER_VITE_PLUS_IMPORTS_RULE in rules)) { + rules[PREFER_VITE_PLUS_IMPORTS_RULE] = 'error'; + } + + return { + ...config, + jsPlugins, + rules, + }; +} + +export function createDefaultVitePlusLintConfig(options?: { + includeTypeAwareDefaults?: boolean; +}): Pick { + const config: Pick = + ensureVitePlusImportRuleDefaults({}); + if (options?.includeTypeAwareDefaults) { + config.options = { + typeAware: true, + typeCheck: true, + }; + } + return config; +} diff --git a/packages/cli/src/oxlint-plugin.ts b/packages/cli/src/oxlint-plugin.ts new file mode 100644 index 0000000000..580fca7e50 --- /dev/null +++ b/packages/cli/src/oxlint-plugin.ts @@ -0,0 +1,154 @@ +import { definePlugin, defineRule } from '@oxlint/plugins'; +import type { Context, ESTree } from '@oxlint/plugins'; + +import { + PREFER_VITE_PLUS_IMPORTS_RULE_NAME, + VITE_PLUS_OXLINT_PLUGIN_NAME, +} from './oxlint-plugin-config.ts'; + +function isStringLiteralLike( + value: ESTree.Expression | ESTree.TSModuleDeclaration['id'], +): value is ESTree.StringLiteral { + return value.type === 'Literal'; +} + +function rewriteVitePlusImportSpecifier(specifier: string): string | null { + if (specifier === 'vite') { + return 'vite-plus'; + } + + if (specifier.startsWith('vite/')) { + return `vite-plus/${specifier.slice('vite/'.length)}`; + } + + if (specifier === 'vitest/config') { + return 'vite-plus'; + } + + if (specifier === 'vitest') { + return 'vite-plus/test'; + } + + if (specifier.startsWith('vitest/')) { + return `vite-plus/test/${specifier.slice('vitest/'.length)}`; + } + + if (specifier === '@vitest/browser') { + return 'vite-plus/test/browser'; + } + + const browserSubpathRewrites: Record = { + '@vitest/browser/context': 'vite-plus/test/browser/context', + '@vitest/browser/client': 'vite-plus/test/client', + '@vitest/browser/locators': 'vite-plus/test/locators', + }; + if (specifier in browserSubpathRewrites) { + return browserSubpathRewrites[specifier]; + } + + for (const [prefix, provider] of [ + ['@vitest/browser-playwright', 'playwright'], + ['@vitest/browser-preview', 'preview'], + ['@vitest/browser-webdriverio', 'webdriverio'], + ] as const) { + if (specifier === prefix) { + return `vite-plus/test/${prefix.slice('@vitest/'.length)}`; + } + + if (specifier === `${prefix}/context`) { + return 'vite-plus/test/browser/context'; + } + + if (specifier === `${prefix}/provider`) { + return `vite-plus/test/browser/providers/${provider}`; + } + } + + return null; +} + +function quoteSpecifier(literal: ESTree.StringLiteral, replacement: string): string { + const quote = literal.raw?.startsWith("'") ? "'" : '"'; + return `${quote}${replacement}${quote}`; +} + +function maybeReportLiteral(context: Context, literal: ESTree.StringLiteral | null | undefined) { + if (!literal || typeof literal.value !== 'string') { + return; + } + + const replacement = rewriteVitePlusImportSpecifier(literal.value); + if (!replacement) { + return; + } + + context.report({ + node: literal, + messageId: 'preferVitePlusImports', + data: { + from: literal.value, + to: replacement, + }, + fix(fixer) { + return fixer.replaceText(literal, quoteSpecifier(literal, replacement)); + }, + }); +} + +export const preferVitePlusImportsRule = defineRule({ + meta: { + type: 'problem', + docs: { + description: 'Prefer vite-plus module specifiers over vite and vitest packages.', + recommended: true, + url: 'https://github.com/voidzero-dev/vite-plus/issues/1301', + }, + fixable: 'code', + messages: { + preferVitePlusImports: "Use '{{to}}' instead of '{{from}}' in Vite+ projects.", + }, + }, + createOnce(context: Context) { + return { + ImportDeclaration(node) { + maybeReportLiteral(context, node.source); + }, + ExportAllDeclaration(node) { + maybeReportLiteral(context, node.source); + }, + ExportNamedDeclaration(node) { + maybeReportLiteral(context, node.source); + }, + ImportExpression(node) { + if (!isStringLiteralLike(node.source)) { + return; + } + maybeReportLiteral(context, node.source); + }, + TSImportType(node) { + maybeReportLiteral(context, node.source); + }, + TSExternalModuleReference(node) { + maybeReportLiteral(context, node.expression); + }, + TSModuleDeclaration(node) { + if (node.global || !isStringLiteralLike(node.id)) { + return; + } + maybeReportLiteral(context, node.id); + }, + }; + }, +}); + +const plugin = definePlugin({ + meta: { + name: VITE_PLUS_OXLINT_PLUGIN_NAME, + }, + rules: { + [PREFER_VITE_PLUS_IMPORTS_RULE_NAME]: preferVitePlusImportsRule, + }, +}); + +export default plugin; +export { rewriteVitePlusImportSpecifier }; diff --git a/packages/cli/tsdown.config.ts b/packages/cli/tsdown.config.ts index d3c18640b5..f9a1f360bf 100644 --- a/packages/cli/tsdown.config.ts +++ b/packages/cli/tsdown.config.ts @@ -28,6 +28,7 @@ export default defineConfig([ 'define-config': './src/define-config.ts', fmt: './src/fmt.ts', lint: './src/lint.ts', + 'oxlint-plugin': './src/oxlint-plugin.ts', pack: './src/pack.ts', 'pack-bin': './src/pack-bin.ts', // Global commands — explicit entries ensure lazy loading via dynamic import in bin.ts. diff --git a/packages/tools/src/snap-test.ts b/packages/tools/src/snap-test.ts index 5e22cdddbd..d234f2bdd7 100755 --- a/packages/tools/src/snap-test.ts +++ b/packages/tools/src/snap-test.ts @@ -1,7 +1,6 @@ import { randomUUID } from 'node:crypto'; -import fs, { readFileSync } from 'node:fs'; +import fs from 'node:fs'; import fsPromises from 'node:fs/promises'; -import { open } from 'node:fs/promises'; import { cpus, homedir, tmpdir } from 'node:os'; import path from 'node:path'; import { setTimeout } from 'node:timers/promises'; @@ -92,6 +91,214 @@ function selectShard(items: T[], index: number, total: number): T[] { const NPM_GLOBAL_PREFIX_DIR = 'npm-global-lib-for-snap-tests'; +function resolveGlobalCliScriptsDir(casesDir: string): string { + const candidates = [ + // `packages/cli/snap-tests-global` -> `packages/cli/dist` + path.join(path.dirname(casesDir), 'dist'), + // Fallback for the common `pnpm -F vite-plus snap-test-global` cwd. + path.resolve('dist'), + ]; + + const scriptsDir = candidates.find((dir) => fs.existsSync(path.join(dir, 'bin.js'))); + if (!scriptsDir) { + throw new Error( + `Unable to find built Vite+ CLI scripts for global snap tests. Tried:\n${candidates + .map((dir) => `- ${dir}`) + .join('\n')}`, + ); + } + + return scriptsDir; +} + +function resolveRepoRoot(casesDir: string): string { + return path.resolve(path.dirname(casesDir), '..', '..'); +} + +function resolveGlobalCliBinary(binDir: string): string { + const binaryName = process.platform === 'win32' ? 'vp.exe' : 'vp'; + const binaryPath = path.join(path.resolve(expandHome(binDir)), binaryName); + if (!fs.existsSync(binaryPath)) { + throw new Error(`Unable to find global snap test vp binary at ${binaryPath}`); + } + + return fs.realpathSync(binaryPath); +} + +function resolveInstalledGlobalCliTargetBinary(binDir: string): string { + const binaryName = process.platform === 'win32' ? 'vp.exe' : 'vp'; + const binaryPath = path.join( + path.resolve(expandHome(binDir)), + '..', + 'current', + 'bin', + binaryName, + ); + if (!fs.existsSync(binaryPath)) { + throw new Error(`Unable to find installed global snap test vp binary at ${binaryPath}`); + } + + return fs.realpathSync(binaryPath); +} + +function resolveBuiltGlobalCliArtifact( + casesDir: string, + binaryName: string, + packageName: string, +): string { + const repoRoot = resolveRepoRoot(casesDir); + const targetDirs = [path.join(repoRoot, 'target')]; + if (process.env.CARGO_TARGET_DIR) { + targetDirs.unshift(process.env.CARGO_TARGET_DIR); + } + const candidates: string[] = []; + for (const targetDir of targetDirs) { + candidates.push( + path.join(targetDir, 'release', binaryName), + path.join(targetDir, 'debug', binaryName), + ); + if (!fs.existsSync(targetDir)) { + continue; + } + + for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) { + if (entry.isDirectory()) { + candidates.push( + path.join(targetDir, entry.name, 'release', binaryName), + path.join(targetDir, entry.name, 'debug', binaryName), + ); + } + } + } + const binaryPath = candidates.find((candidate) => fs.existsSync(candidate)); + if (!binaryPath) { + throw new Error( + `Unable to find built Vite+ global CLI ${binaryName} for global snap tests. Tried:\n${candidates + .map((candidate) => `- ${candidate}`) + .join('\n')}\nRun \`cargo build -p ${packageName} --release\` before snap-test-global.`, + ); + } + + return fs.realpathSync(binaryPath); +} + +function resolveBuiltGlobalCliBinary(casesDir: string): string { + const binaryName = process.platform === 'win32' ? 'vp.exe' : 'vp'; + return resolveBuiltGlobalCliArtifact(casesDir, binaryName, 'vite_global_cli'); +} + +function resolveBuiltGlobalCliShim(casesDir: string): string { + return resolveBuiltGlobalCliArtifact(casesDir, 'vp-shim.exe', 'vite_trampoline'); +} + +function newestMtimeMs(filePath: string): number { + const stats = fs.statSync(filePath); + if (!stats.isDirectory()) { + return stats.mtimeMs; + } + + return fs + .readdirSync(filePath) + .reduce( + (newest, entry) => Math.max(newest, newestMtimeMs(path.join(filePath, entry))), + stats.mtimeMs, + ); +} + +function fileContentsEqual(a: string, b: string): boolean { + return fs.readFileSync(a).equals(fs.readFileSync(b)); +} + +function assertGlobalCliBinaryMatchesCheckout(binDir: string, casesDir: string): void { + const repoRoot = resolveRepoRoot(casesDir); + const builtBinary = resolveBuiltGlobalCliBinary(casesDir); + const sourcePaths = [ + path.join(repoRoot, 'Cargo.toml'), + path.join(repoRoot, 'Cargo.lock'), + path.join(repoRoot, 'crates', 'vite_global_cli', 'src'), + path.join(repoRoot, 'crates', 'vite_shared', 'src'), + ]; + const shouldCheckMtime = process.env.GITHUB_ACTIONS !== 'true'; + const newestSourceMtime = shouldCheckMtime ? Math.max(...sourcePaths.map(newestMtimeMs)) : 0; + if (shouldCheckMtime && fs.statSync(builtBinary).mtimeMs + 1000 < newestSourceMtime) { + throw new Error( + `Built Vite+ global CLI binary is older than the current checkout: ${builtBinary}\n` + + 'Run `cargo build -p vite_global_cli --release` before snap-test-global.', + ); + } + + const globalBinary = resolveGlobalCliBinary(binDir); + if (process.platform !== 'win32' && fileContentsEqual(globalBinary, builtBinary)) { + return; + } + + if (process.platform === 'win32') { + const builtShim = resolveBuiltGlobalCliShim(casesDir); + const installedTargetBinary = resolveInstalledGlobalCliTargetBinary(binDir); + if ( + fileContentsEqual(globalBinary, builtShim) && + fileContentsEqual(installedTargetBinary, builtBinary) + ) { + return; + } + + throw new Error( + `Global snap tests would use stale Windows vp binaries.\n` + + `Entrypoint: ${globalBinary}\n` + + `Expected entrypoint to match the current checkout shim at ${builtShim}.\n` + + `Installed target: ${installedTargetBinary}\n` + + `Expected target to match the current checkout build at ${builtBinary}.\n` + + 'Run `pnpm bootstrap-cli` or `pnpm bootstrap-cli:ci` before snap-test-global.', + ); + } + + throw new Error( + `Global snap tests would use a stale vp binary from ${globalBinary}.\n` + + `Expected it to match the current checkout build at ${builtBinary}.\n` + + 'Run `pnpm bootstrap-cli` or `pnpm bootstrap-cli:ci` before snap-test-global.', + ); +} + +function replaceInstalledCheckoutPackages(rootDir: string, repoRoot: string): void { + const stack = [rootDir]; + const symlinkType = process.platform === 'win32' ? 'junction' : 'dir'; + const replacements = new Map([ + ['node_modules/vite-plus', path.join(repoRoot, 'packages', 'cli')], + ['node_modules/vite', path.join(repoRoot, 'packages', 'core')], + ['node_modules/vitest', path.join(repoRoot, 'packages', 'test')], + ['node_modules/@voidzero-dev/vite-plus-core', path.join(repoRoot, 'packages', 'core')], + ['node_modules/@voidzero-dev/vite-plus-test', path.join(repoRoot, 'packages', 'test')], + ]); + + while (stack.length > 0) { + const dir = stack.pop()!; + for (const [relativePackagePath, checkoutPackageDir] of replacements) { + const candidate = path.join(dir, relativePackagePath); + if (fs.existsSync(candidate) && fs.realpathSync(candidate) !== checkoutPackageDir) { + fs.rmSync(candidate, { recursive: true, force: true }); + fs.symlinkSync(checkoutPackageDir, candidate, symlinkType); + } + } + + const isNodeModulesPath = dir.split(path.sep).includes('node_modules'); + const isPnpmStorePath = dir.split(path.sep).includes('.pnpm'); + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (!entry.isDirectory() || entry.name === '.git' || entry.name === '.bin') { + continue; + } + if ( + isNodeModulesPath && + !isPnpmStorePath && + entry.name !== '.pnpm' && + entry.name !== '@voidzero-dev' + ) { + continue; + } + stack.push(path.join(dir, entry.name)); + } + } +} + export async function snapTest() { const { positionals, values } = parseArgs({ allowPositionals: true, @@ -217,13 +424,18 @@ export async function snapTest() { const selectedCases = shard ? selectShard(validCaseNames, shard.index, shard.total) : validCaseNames; + const globalCliScriptsDir = values['bin-dir'] ? resolveGlobalCliScriptsDir(casesDir) : undefined; + if (values['bin-dir']) { + assertGlobalCliBinaryMatchesCheckout(values['bin-dir'], casesDir); + } const serialTasks: (() => Promise)[] = []; const parallelTasks: (() => Promise)[] = []; for (const caseName of selectedCases) { const stepsPath = path.join(casesDir, caseName, 'steps.json'); - const steps: Steps = JSON.parse(readFileSync(stepsPath, 'utf-8')); - const task = () => runTestCase(caseName, tempTmpDir, casesDir, values['bin-dir']); + const steps: Steps = JSON.parse(fs.readFileSync(stepsPath, 'utf-8')); + const task = () => + runTestCase(caseName, tempTmpDir, casesDir, values['bin-dir'], globalCliScriptsDir); if (steps.serial) { serialTasks.push(task); } else { @@ -272,6 +484,11 @@ interface Steps { ignoredPlatforms?: (string | PlatformFilter)[]; env: Record; commands: (string | Command)[]; + /** + * If true, installed Vite+ packages in the test project are relinked to the + * current checkout after each successful command. + */ + linkCheckoutPackages?: boolean; /** * Commands to run after the test completes, regardless of success or failure. * Useful for cleanup tasks like killing background processes. @@ -337,7 +554,13 @@ function shouldSkipPlatform(ignoredPlatforms: (string | PlatformFilter)[]): bool return false; } -async function runTestCase(name: string, tempTmpDir: string, casesDir: string, binDir?: string) { +async function runTestCase( + name: string, + tempTmpDir: string, + casesDir: string, + binDir?: string, + globalCliScriptsDir?: string, +) { const steps: Steps = JSON.parse( await fsPromises.readFile(`${casesDir}/${name}/steps.json`, 'utf-8'), ); @@ -381,6 +604,9 @@ async function runTestCase(name: string, tempTmpDir: string, casesDir: string, b // shared helper scripts under `/.shared/` without // duplicating them into every fixture directory. SNAP_CASES_DIR: casesDir, + // Global CLI snap tests execute the Rust binary from --bin-dir, but the JS + // entry should come from this checkout instead of a stale ~/.vite-plus install. + ...(globalCliScriptsDir ? { VITE_GLOBAL_CLI_JS_SCRIPTS_DIR: globalCliScriptsDir } : {}), // A test case can override/unset environment variables above. // For example, VP_CLI_TEST/CI can be unset to test the real-world outputs. @@ -428,7 +654,7 @@ async function runTestCase(name: string, tempTmpDir: string, casesDir: string, b // it seems not to have stable ordering of stdout/stderr chunks. // To ensure stable ordering, we redirect outputs to a file instead. const outputStreamPath = path.join(caseTmpDir, 'output.log'); - const outputStream = await open(outputStreamPath, 'w'); + const outputStream = await fsPromises.open(outputStreamPath, 'w'); const exitCode = await Promise.race([ execute(stripComments(cmd.command), [], { @@ -450,8 +676,11 @@ async function runTestCase(name: string, tempTmpDir: string, casesDir: string, b ]); await outputStream.close(); + if (exitCode === 0 && globalCliScriptsDir && steps.linkCheckoutPackages) { + replaceInstalledCheckoutPackages(caseTmpDir, resolveRepoRoot(casesDir)); + } - let output = readFileSync(outputStreamPath, 'utf-8'); + let output = fs.readFileSync(outputStreamPath, 'utf-8'); let commandLine = `> ${cmd.command}`; if (exitCode !== 0) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 046d374c1a..d0aeda6637 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ catalogs: '@oxc-project/types': specifier: '=0.129.0' version: 0.129.0 + '@oxlint/plugins': + specifier: '=1.61.0' + version: 1.61.0 '@rollup/plugin-commonjs': specifier: ^29.0.0 version: 29.0.0 @@ -334,6 +337,9 @@ importers: '@oxc-project/types': specifier: 'catalog:' version: 0.129.0 + '@oxlint/plugins': + specifier: 'catalog:' + version: 1.61.0 '@voidzero-dev/vite-plus-core': specifier: workspace:* version: link:../core @@ -4514,6 +4520,10 @@ packages: cpu: [x64] os: [win32] + '@oxlint/plugins@1.61.0': + resolution: {integrity: sha512-nkOyZEF1vH527CkdQtOp1HMrVFEM4ResURvI2JFeGoup+h+43J/k/FgdOR9b9Isxg+Yae7qVDa7y3nssE8b3TQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@package-json/types@0.0.12': resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} @@ -12132,6 +12142,8 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.63.0': optional: true + '@oxlint/plugins@1.61.0': {} + '@package-json/types@0.0.12': {} '@parcel/watcher-android-arm64@2.5.1': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 396e9ed0bd..026a502962 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,8 +9,6 @@ catalog: '@babel/preset-env': ^7.24.7 '@babel/preset-typescript': ^7.24.7 '@clack/core': ^1.0.0 - '@emnapi/core': ^1.9.2 - '@emnapi/runtime': ^1.9.2 '@iconify/vue': ^5.0.0 '@napi-rs/cli': ^3.6.1 '@napi-rs/wasm-runtime': ^1.1.4 @@ -19,6 +17,7 @@ catalog: '@oxc-node/core': ^0.1.0 '@oxc-project/runtime': =0.129.0 '@oxc-project/types': =0.129.0 + '@oxlint/plugins': =1.61.0 '@pnpm/find-workspace-packages': ^6.0.9 '@rollup/plugin-commonjs': ^29.0.0 '@rollup/plugin-json': ^6.1.0