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