feat(tauri): cross-language IPC bridge resolver (#772)#878
Open
vdavid wants to merge 1 commit into
Open
Conversation
Joins Tauri's TS<->Rust IPC boundary that tree-sitter can't see, for both
conventions:
- Typed (tauri-specta): `commands.fooBar()` / `events.fooBar.listen()` are
member accesses the JS extractor already emits a ref for, joined in `resolve()`
to the Rust `#[tauri::command] fn foo_bar` / `#[derive(...Event)] struct FooBar`
(call-site granularity).
- Raw: `invoke('foo_bar')` / `listen('foo-bar')` / `once(...)` carry the wire
name as a string literal the extractor drops, so `extract()` surfaces it as a
`calls` ref attributed to the file node (file granularity — a fn-node id is a
sha256 of path:kind:name:line a string scan can't rebuild).
Across tauri-specta's snake/camel/Pascal/kebab name conversions. Edges marked
`metadata.resolvedBy:'framework'`, confidence 0.7 commands / 0.6 events, exact
name-match wins a tie; over-matching is harmless (resolve() only joins names that
hit a real command/event). Detected via `tauri.conf.json[5]` (root or nested
monorepo), an `@tauri-apps/api` dependency, or any `tauri::command` in tracked
Rust.
Validated on three real apps: Cmdr (tauri-specta, Tauri 2 + Svelte 5, 25k nodes)
-> `callers(get_mcp_port)` "no callers" -> 4, 291 distinct commands gain TS
callers, events bridge across the case change; clash-verge-rev (raw invoke) ->
76 of 79 commands bridge via its cmds.ts wrapper, all spot-checked real commands;
pot-desktop (raw invoke) -> 3/3 literal invokes. `impact` and `explore` span the
wire too. 23 unit + fixture-integration tests (incl. a raw-invoke end-to-end);
full suite green (1558).
- src/resolution/frameworks/tauri.ts the resolver (resolve + extract)
- src/resolution/frameworks/index.ts registration
- __tests__/tauri-ipc-bridge.test.ts unit + end-to-end fixture tests
- CHANGELOG.md, docs/design/dynamic-dispatch-coverage-playbook.md records
Author
|
@colbymchenry, not sure if you get notified about PRs, so here is a quick ping. If you agree that this is a good contrib, this is meant to be good to merge. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #772.
Why
I build Cmdr, a keyboard-first, two-pane file manager in Rust + Tauri 2 + Svelte 5 (source at github.com/vdavid/cmdr), and CodeGraph has become how my agents navigate it. The one place it kept coming up short was the IPC seam: ask "what calls this command?" and you get nothing back, even though the entire frontend calls it. That's exactly where a Tauri app's refactors are scariest ("if I change this command, what in the UI breaks?"), so closing it felt worth doing. Huge fan of what you've built, and your native-bridge resolvers (React Native / Fabric / Swift↔ObjC) made this a pleasure to write: the shape was already there, I just taught it Tauri's dialect.
What
CodeGraph is blind across Tauri's IPC boundary today:
callers/impacton a#[tauri::command]come back empty even when the whole frontend calls it, because the call is a runtime IPC hop between two languages whose names don't match (tauri-specta manglesget_mcp_port→getMcpPort).This PR adds
frameworks/tauri.ts, a cross-language bridge in the vein of the React Native / Fabric / Swift↔ObjC resolvers:commands.fooBar()and rawinvoke('foo_bar')→ Rust#[tauri::command] fn foo_bar.events.fooBar.listen(...)and rawlisten('foo-bar')/once(...)→ Rust#[derive(…Event)] struct FooBar.Handles tauri-specta's full name-conversion set (
snake/camelfor commands;Pascal/camel/kebabfor events), stacked attributes (#[specta::specta]+#[tauri::command]), bareEventderives withuse tauri_specta::Eventin scope, and#[tauri_specta(event_name = "…")]overrides.How it resolves
Two TS shapes. Typed
commands.*/events.*are member accesses the JS extractor already emits a ref for, joined inresolve()(call-site granularity). Rawinvoke('x')/listen('y')carry the wire name as a string literal the extractor drops, so a smallextract()surfaces it as a ref attributed to the file node (file granularity, since a fn-node id is a hash ofpath:kind:name:linea string scan can't rebuild). Like the RN/Fabric bridges, both only redirect JS/TS callers (Rust-side refs use the normal Rust extractor); edges carrymetadata.resolvedBy:'framework', confidence 0.7 (commands) / 0.6 (events), so exact name-match wins a tie. I kept these off theprovenance:'heuristic'column, which iscallback-synthesizer's territory for dispatch with no statically-resolvable name, and Tauri names are statically joinable. Happy to tag them heuristic if you'd rather.Detection
Any of:
tauri.conf.json[5](root or nested in a monorepo), an@tauri-apps/apidependency, or anytauri::commandin tracked Rust.Validation
I followed what looked like this repo's standard (the dynamic-dispatch playbook: deterministic probes + an agent A/B), across three real apps:
callers(get_mcp_port)went "no callers" → 4; 291 distinct commands gained framework-tagged TS callers;impact/explorenow span the wire; events bridge across the case change.invoke): 76 of 79 commands bridge through itscmds.tswrapper, all spot-checked as real#[tauri::command]s.invoke): 3/3 of its literal invokes resolve.Precision held: framework edges only hit real commands/events and are cleanly separable from CodeGraph's existing
exact-matchmatching (the generic-name noise likedescribe/filteris exact-match, not this).Honest note on the agent A/B: the graph correctness is solid and MCP-verified (
codegraph_callersreturns the real cross-IPC consumers). But I couldn't show a free-choice read-reduction: Sonnet greps these "who-calls-X" questions by reflex, and Tauri's wire name is the same string on both sides (or paired in the generatedbindings.ts), so grep already bridges it. The win shows up under codegraph-only/impact/exploreuse, not against an agent that's free to grep. So I'm claiming graph correctness, not a read-reduction number.Limits
invoke/listenedges are file-granular (which file uses the command), vs the typed path's call-site granularity.invoke(varName),app.emit(var), templatedviewer:file-changed:<id>) are skipped: no literal to read.Test plan
__tests__/tauri-ipc-bridge.test.ts): unit forresolve()across commands/events, JS-only redirection, name-conversion +event_nameoverride, andextract()(incl. dynamic-name skipping); plus three end-to-end tests that index a minimal Tauri project through the realCodeGraphpipeline and assert the cross-language edges in SQLite (typed command, typed event, rawinvoke). Full suite green (1558 passed, 2 skipped).