Skip to content

feat: detect Vite+ projects and launch vp lint/fmt --lsp (POC)#199

Draft
fengmk2 wants to merge 4 commits into
oxc-project:mainfrom
fengmk2:poc/vite-plus-detection
Draft

feat: detect Vite+ projects and launch vp lint/fmt --lsp (POC)#199
fengmk2 wants to merge 4 commits into
oxc-project:mainfrom
fengmk2:poc/vite-plus-detection

Conversation

@fengmk2

@fengmk2 fengmk2 commented May 30, 2026

Copy link
Copy Markdown

What

POC of the editor-extension detection rule from the Vite+ RFC voidzero-dev/vite-plus#1614 ("Vite+ Project Detection for Editor Extensions").

Draft, do not merge. This tracks a draft RFC, and its premise (vite-plus dropping the bin/oxlint / bin/oxfmt wrappers) lands in vite-plus#1557, which has not shipped. Opening this to validate the approach and feed findings back to the RFC.

Why

Today oxc-zed implicitly relies on vite-plus's per-package bin/oxlint / bin/oxfmt wrappers: when a project declares vite-plus, lsp.rs launches node node_modules/vite-plus/bin/oxlint --lsp. vite-plus#1557 removes those wrappers, which breaks the current extension. The RFC's fix: each editor explicitly detects a Vite+ project and launches vp lint --lsp / vp fmt --lsp instead.

How

  • src/vite_plus.rs: the RFC algorithm ported to Rust behind a small FileSystem trait so the core logic is host-testable. Unit tests cover the RFC conformance fixtures plus edge cases. 13 tests, all green.
    • Phase 1: walk up for the first package.json that directly declares vite-plus, bounded by the root workspace.
    • Phase 2: project-scoped node_modules/vite-plus/bin/vp (Zed targets the real launcher, not the pnpm .bin/vp shell shim).
    • The RFC's Phase 3 (a global vp on $PATH) is deliberately not implemented, see finding 2.
  • src/lsp.rs: a WorktreeFs adapter backs the FileSystem trait with Zed's Worktree. oxlint/oxfmt launch vp <lint|fmt> --lsp when Vite+ is detected, and fall back to plain oxlint/oxfmt otherwise.

Findings for the RFC

  1. node_modules reads are unreliable in Zed, the RFC's Phase 2 "validate against a real vite-plus package (name === "vite-plus")" is not portable to Zed. zed#10760 (closed not planned) confirms Worktree::read_text_file on node_modules "works sometimes but mostly doesn't," and Path::exists doesn't work in the WASM sandbox. The shipping extension already sidesteps this by reading only the root package.json and constructing-and-trusting the binary path. So this POC trusts node_modules/vite-plus/bin/vp rather than read-verifying it; a missing install surfaces as a clear Cannot find module …/vite-plus/bin/vp ("run pnpm install").
  2. Phase 3 is implementable but low-value in Zed, so it's omitted. The RFC says Zed "exposes neither $PATH nor a process-spawn capability," but Worktree::which / shell_env have existed since WIT v0.0.1, so a $PATH lookup does work. However, live testing showed it can't actually help: a real project's vite.config.ts imports vite-plus, so with no local install vp (global or not) dies with UNRESOLVED_IMPORT; a missing node_modules breaks vp regardless of binary source. So this POC deliberately uses a local-only design with no global fallback (the which lookup was prototyped, then removed).
  3. The real Phase-1 limitation is the upward walk. Worktree::read_text_file is confined to the worktree, so Phase 1 cannot read above worktree.root_path(). Opening a monorepo root where only a subpackage declares vite-plus returns null, the single-root gap the RFC already acknowledges for Zed. Captured by zed_worktree_root_misses_subpackage_declaration.

Verification

cargo fmt --check, cargo check --all-targets, cargo test (13/13), cargo clippy -D warnings, cargo doc -D warnings, and cargo build --release --target wasm32-wasip2 all pass.

Live smoke test in Zed 1.4.4 (dev extension): verified against a real vite-plus project (node-modules/urllib), Zed's LSP log shows node …/urllib/node_modules/vite-plus/bin/vp lint --lsp and … fmt --lsp, with the servers running. A non-Vite+ project launches plain oxfmt --lsp. Deleting node_modules produces the expected Cannot find module …/vite-plus/bin/vp (vp needs the install regardless, see finding 2).

Open / out of scope

  • "Declared but not installed" UX is a silent fallback + debug log (RFC open question fix: fix server start error @oxclint -> @oxclint #2). In Zed, because installs can't be verified, this state mostly can't be detected, so a missing vp surfaces at spawn time instead.
  • Phase 1/2 stay at the worktree root in Zed (no per-file resolution available in the WASM API).

fengmk2 added 2 commits May 30, 2026 23:32
POC of the editor-extension detection rule from the Vite+ RFC
voidzero-dev/vite-plus#1614. Today oxc-zed implicitly relies on
vite-plus's `bin/oxlint` / `bin/oxfmt` wrappers (launched via
`node_modules/vite-plus/bin/oxlint --lsp`); those wrappers are removed
by vite-plus#1557, which would break the extension. This replaces the
implicit handoff with explicit Vite+ detection ported into Rust.

- src/vite_plus.rs: portable Phase 1-3 detector behind a `FileSystem`
  trait, host-testable; unit tests cover all 10 RFC conformance
  fixtures plus edge cases.
- src/lsp.rs: `WorktreeFs` adapter backing the detector with Zed's
  `Worktree`; oxlint/oxfmt launch `vp lint --lsp` / `vp fmt --lsp`
  when Vite+ is runnable, and fall back to plain oxlint/oxfmt
  otherwise. The detector targets `node_modules/vite-plus/bin/vp`
  validated by `name === "vite-plus"`.

Phase 3 (`$PATH` lookup) is implemented via `Worktree::which`,
contrary to the RFC's note that it is unimplementable in Zed.
…ble)

Discovered while preparing the live smoke test: zed#10760 (closed as
not planned) confirms `Worktree::read_text_file` on `node_modules`
"works sometimes but mostly doesn't" and `Path::exists` doesn't work in
the WASM sandbox. The shipping extension already sidesteps this by
reading only the root `package.json` and constructing-and-trusting the
`node_modules/...` binary path.

So read-verifying `node_modules/vite-plus/bin/vp` (and validating its
package.json `name`) — which the RFC's core rule calls for — is not
reliably implementable in Zed and would mostly yield false
"declared but not installed" results for real installs.

- Add `FileSystem::can_read_node_modules()` (default true).
- When false (Zed), Phase 2 trusts the conventional vp path at the
  declaring directory instead of probing; a missing install surfaces
  as a spawn-time error, the RFC's anticipated upgrade-hint trigger.
- `WorktreeFs` returns false; the read-verify path stays for the
  portable algorithm and conformance tests.
@fengmk2

fengmk2 commented May 30, 2026

Copy link
Copy Markdown
Author

✅ Live smoke test in Zed 1.4.4 (dev extension)

Installed this branch as a dev extension and opened two projects. node_modules is gitignored in the fixture (as in real projects), exercising the zed#10760 construct-and-trust path.

Vite+ project (vite-plus-app, declares vite-plus, has node_modules/vite-plus/bin/vp) — from Zed's own LSP log:

starting language server process. binary path: ".../.vite-plus/bin/node",
  working directory: ".../vite-plus-app",
  args: [".../vite-plus-app/node_modules/vite-plus/bin/vp", "lint", "--lsp"]
starting language server process. binary path: ".../.vite-plus/bin/node",
  working directory: ".../vite-plus-app",
  args: [".../vite-plus-app/node_modules/vite-plus/bin/vp", "fmt", "--lsp"]

→ Detected Vite+ and launched vp lint --lsp / vp fmt --lsp. ✅ (A mock vp that logs its argv recorded the same.)

Non-Vite+ project (the oxc-zed repo itself) — same log:

starting language server process. ... working directory: ".../oxc-zed",
  args: [".../extensions/work/oxc/node_modules/oxfmt/bin/oxfmt", "--lsp"]

→ No vp; plain oxfmt --lsp. ✅ Negative case confirmed.

Zed also compiled the extension cleanly through its own build pipeline (compiled Rust extension … encoding wasm component … finished).

fengmk2 added 2 commits May 31, 2026 18:52
The RFC's Phase 3 (a global `vp` on $PATH) is implementable in Zed
(`Worktree::which` exists) but can't actually rescue a missing install:
a real project's `vite.config.ts` imports `vite-plus`, so a global `vp`
dies with UNRESOLVED_IMPORT regardless of binary source. Drop it; the
detector is now purely Phases 1-2 (detect declaration -> resolve a
project-scoped `vp`).

- Remove `FileSystem::which`, the Phase 3 block, `WorktreeFs::which`,
  and the test scaffolding (`path_vp` / `vp_on_path` / `which` impl).
- Remove the two tests that only exercised the fallback; a missing
  install now uniformly yields `{ root, vp_path: None }` (install hint).
- README: in a Vite+ project the LSP is `vp lint/fmt --lsp`, which
  resolves config from the Vite config, so `configPath` (oxlint) and
  `fmt.configPath` (oxfmt) are ignored; other LSP settings still apply.
- examples/both: comment the two ignored config-path settings.
- Replace em dashes in doc comments with commas/colons/parentheses.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant