Skip to content

refactor(use-cache): move server function directives to user land - #2156

Draft
james-elicx wants to merge 38 commits into
mainfrom
codex/pr-1871-userland-server-functions
Draft

refactor(use-cache): move server function directives to user land#2156
james-elicx wants to merge 38 commits into
mainfrom
codex/pr-1871-userland-server-functions

Conversation

@james-elicx

@james-elicx james-elicx commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

POC for implementing callable "use cache" server references entirely in Vinext user land instead of adding the serverFunctionDirectives option and orchestration plugin originally proposed in vitejs/vite-plugin-react#1246.

This now targets the stable @vitejs/plugin-rsc@0.5.34 release and follows the maintainer's final callable-cache plugin shape:

https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-rsc/examples/use-cache-persistent/callable-cache-plugin.ts

This PR includes the still-open #1871 callable-cache foundation and references it below.

Plugin composition

Vinext inserts vinext:server-function-directives immediately before rsc:use-server and uses plugin-rsc's public transform primitives:

When rsc() is configured manually, vinext({ rsc: false }) also installs the Vinext directive plugin. Vinext must appear before rsc() in the Vite plugin array; reversed order now fails fast instead of silently dropping cache transforms.

  • transformWrapExport() for module-level "use cache"
  • transformHoistInlineDirective() for inline "use cache"
  • transformDirectiveProxyExport() for SSR/client proxies
  • RscPluginManager.serverReferences.resolve() for canonical dev/build identities
  • replaceClaim() and deleteClaim() for Vinext-owned references

The generated RSC transform imports registerServerReference from @vitejs/plugin-rsc/react/rsc/server.

With plugin-rsc 0.5.34, compatible Vinext and rsc:use-server claims can coexist for mixed directive modules. Vinext no longer deletes or takes ownership of plugin-rsc's claims. It preserves leading file directives when injecting runtime imports so the built-in transform can run afterwards.

That supports both mixed cases:

  • file-level "use server" with an inline "use cache" function
  • file-level "use cache" with an inline "use server" function

Differences from the upstream example

The orchestration now matches the upstream example, while Vinext keeps framework-specific cache behavior:

  • parses and validates "use cache: <kind>" variants
  • passes Vinext cache identity and variant data to registerCachedFunction()
  • records argumentCount and acceptsSecondArgument for metadata/viewport behavior
  • marks cached App Page default exports for page-specific runtime validation
  • leaves layout/template default exports uncached because their children slot is not cache-serializable yet
  • encrypts closure captures through Vinext's cache runtime envelope
  • rejects standalone inline cache functions in client/SSR graphs with a Next-compatible diagnostic
  • resolves plugin-rsc from the application project root so linked/workspace installs share the application's Vite instance

Vinext's cache runtime, persistence, invalidation, cache kinds, and request API restrictions remain outside plugin-rsc.

Cache replay

Cached Flight replay uses plugin-rsc's supported API:

createFromReadableStream(stream, {}, { preserveServerReferences: true })

This preserves opaque server references while replaying cached RSC without importing their implementations into the replaying RSC runtime.

Upstream requirements

This POC now depends on the public features shipped in @vitejs/plugin-rsc@0.5.34:

  • low-level directive transform primitives
  • getPluginApi() access to RscPluginManager
  • independently owned server-reference claims and compatible claim aggregation
  • mixed "use cache" / "use server" transform composition
  • preserveServerReferences during cache replay
  • split RSC runtime entrypoints

It does not depend on:

  • a serverFunctionDirectives plugin option
  • plugin-rsc-owned custom directive orchestration
  • private transform markers
  • direct writes to serverReferenceMetaMap
  • local copies of plugin-rsc reference hashing or Vite URL normalization

Validation

  • vp check
  • frozen lockfile install with @vitejs/plugin-rsc@0.5.34
  • 36/36 server-function directive transform tests
  • 13/13 development App Router cache/HMR E2Es
  • 8/8 RSC plugin registration integration tests
  • 4/4 production callable-cache E2Es
  • production direct Client Component calls for file-level cache modules with inline server overrides
  • production nested cached-function action round trips and cached Flight replay

Refs #1871

…forward-reference module-level code

The previous approach used `noExport: true` and appended module-level
`const ${name}_$$vcf` declarations at the end of the transformed file,
then referenced them via forward reference at the call-site. This caused
a temporal dead zone (TDZ) error because `const` bindings are not
hoisted — the call-site assignment evaluated before the TLA const was
initialized, crashing all RSC files that contain function-level "use
cache" (HTTP 500 for use-cache pages, route handlers, etc.).

Fix: keep the existing hoisting/export behaviour (`noExport` stays
false) and instead wrap `registerCachedFunction(...)` with
`registerServerReference(...)` inline at call-site in the RSC
environment. This adds the RSC serialisation metadata ($$typeof, $$id)
so cached functions can be passed as props to client components
(useActionState / formAction), while not disturbing the existing
exported binding that loadServerAction relies on.
…r nested function props

The previous approach passed the raw absolute file path as the $$id to
registerServerReference. @vitejs/plugin-rsc resolves server references by a
normalised key (sha256(toRelativeId) in build; URL-path in dev), so production
would throw "server reference not found" for any cached function passed as a
client-component prop.

Also, the module was never added to the virtual:vite-rsc/server-references
manifest because only the plugin's own "use server" transform writes to
manager.serverReferenceMetaMap. Without a manifest entry, the production
serverReferences lookup has no entry for the module at all.

Fix:
- Capture the plugin-rsc manager via the rsc:minimal plugin API in
  configResolved so we can write to serverReferenceMetaMap directly.
- Compute normalizedRefKey to match vitePluginUseServer's getNormalizedId():
    build → sha256(toRelativeId(id)).hex.slice(0,12)
    dev   → id.slice(root.length)  (Vite URL path)
- After transformHoistInlineDirective succeeds, register the hoisted export
  names in manager.serverReferenceMetaMap[id] so the manifest is populated.
- Pass normalizedRefKey (not raw id) to registerServerReference.

Add unit tests verifying the hash formula matches plugin-rsc's own logic.
…register manifest after rsc:use-server

- Derive the build-mode reference key via plugin-rsc's own
  manager.toRelativeId() instead of a string slice, so the hash input is
  byte-for-byte identical to the plugin's hashString(toRelativeId(id)).
- Reassign each hoisted inline 'use cache' export at module level to
  registerServerReference(registerCachedFunction(fn)) so the module
  export itself is the cached wrapper (Next.js parity: direct action
  invocation goes through the cache) and call sites/manifest imports all
  observe the same wrapped function.
- Register serverReferenceMetaMap entries from a new
  vinext:use-cache-server-references plugin placed after the plugin-rsc
  plugins: rsc:use-server deletes metaMap entries for modules without
  'use server', which wiped the entries written during the use-cache
  transform (prod actions 404'd with 'server reference not found').
- Deduplicate the RSC/non-RSC transform branches into a single
  transformHoistInlineDirective call and hoist the
  @vitejs/plugin-rsc/react/rsc resolution out of the per-module path.
- Replace the self-referential key-formula unit test with the ported
  Next.js fixture (use-cache-with-server-function-props/nested-cache), a
  dev-mode Playwright round-trip test, and a production-server
  integration test that resolves the serialized references via action
  POSTs and asserts cached-invoke semantics.
…erver references when the plugin-rsc manager is missing

When the @vitejs/plugin-rsc manager is unavailable in the rsc environment,
the inline 'use cache' transform previously fell back to a locally computed
reference key and still wrapped the hoisted exports — but the manifest
registration plugin bails without the manager, so the emitted reference
would serialize into the RSC payload yet never resolve (silent 404 on
action POST in production). Fail loudly at transform time instead; the
manager is a structural invariant whenever the rsc environment exists.

Adds transform-level unit tests for the fail-loud path (build + dev), the
non-rsc no-manager control, and build reference-key parity with plugin-rsc.
…ment the divergence

Extends the nested-fn-props fixture with a cached function that closes over
a value from the cached component's scope, exercising the .bind(null, ...)
bound-arg path end to end: the production round-trip test asserts the
captured value appears in plaintext in the flight payload (pinning the
documented divergence from Next.js, which encrypts bound args by default)
and that invoking the bound reference observes the captured value; the
Playwright test covers the real flight-client encodeReply round-trip in
dev. A transform-level test pins that captures are emitted as plain bind
args. The divergence is now also documented in the README's Known
limitations section.
…him to decouple from plugin-rsc module-id normalisation

The inline 'use cache' prepend imported registerServerReference from a
file:// URL of @vitejs/plugin-rsc/react/rsc while the cache runtime
imports the same package via the bare specifier, relying on Vite
normalising both to a single module id. Re-export it instead from a new
vinext-owned cache-server-reference shim whose only react/rsc specifier
is the same bare one cache-runtime uses, resolved from the same importer
location — one module instance by construction. The transform unit test
now pins that the emitted import targets the shim and never a plugin-rsc
file URL.
…tMessage path

Mirror the getDate cache assertion on the closure-bound path: the
fixture's getMessage now appends a Math.random() suffix so cache hits
are observable, and the production-server round-trip asserts that two
identical bound-arg invocations return the same cached value while a
different bound arg misses instead of reusing the entry. The Playwright
assertion matches the suffixed message via regex.
# Conflicts:
#	packages/vinext/src/index.ts
#	pnpm-lock.yaml
@pkg-pr-new

pkg-pr-new Bot commented Jun 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2156
npm i https://pkg.pr.new/create-vinext-app@2156
npm i https://pkg.pr.new/@vinext/types@2156
npm i https://pkg.pr.new/vinext@2156

commit: 36c26e3

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

/bigbonk review — PR #2156

Reviewed the userland server-function directive plugin, the cache-runtime replay change, and the test updates. I installed the pinned prerelease (@vitejs/plugin-rsc@5a2fd750.5.28) via a frozen vp install, then ran focused checks/tests:

  • vp check on server-function-directives.ts, index.ts, cache-runtime.ts, use-cache-transform.test.ts — clean
  • vp test run tests/use-cache-transform.test.ts — 28/28 pass
  • vp test run tests/app-router-production-server.test.ts -t "resolves nested 'use cache' functions passed as props" — pass
  • vp run vinext#build — completes

Heads-up on local envs: with a stale node_modules (plugin-rsc 0.5.27), vp check reports 12 TS errors in server-function-directives.ts (missing moduleDirective/parameters/directive on the transform types). These vanish after installing the pinned prerelease. Not a PR defect, but anyone reviewing locally must vp install first or they'll chase phantom errors.

Parity verification (positive)

I diffed the locally-reproduced helpers against the installed plugin-rsc source and they line up with the authoritative implementation:

  • hashString matches transforms hashString exactly (sha256, hex, first 12 chars).
  • The build/dev normalizedId branch mirrors plugin-rsc's command === "build" ? hashString(toRelativeId(id)) : normalizeViteImportAnalysisUrl(rscEnv, id).
  • The plugin ordering splice produces [vinext:server-function-directives, rsc:use-server, vinext:server-function-directive-metadata], which the new test asserts.

Findings

1. (low) matchDirective breaks for a RegExp directive carrying the g flag.
server-function-directives.ts:129. new RegExp(directive.source, directive.flags) preserves g, and value.match(pattern) on a global regex returns all match strings with no capture groups — so directiveMatch/moduleMatch[0] would be wrong for the whole directive lifecycle. The pattern.lastIndex = 0 line is a no-op for .match(). No live bug (the only definition is /^use cache.*$/, no g), but this file now exposes a generic userland directive: string | RegExp API, so stripping g (or using new RegExp(source, flags.replace("g",""))) would harden it.

2. (low) normalizeViteImportAnalysisUrl reproduction uses split("?") instead of Vite's cleanUrl.
server-function-directives.ts:221. Vite strips both ? and # (/[?#].*$/); this only splits on ?, so an id with a # fragment would keep it in the fs.existsSync check. Also the /@id/ fallback doesn't guard against an already-/@id/-prefixed id the way Vite's wrapId does. In practice these branches don't fire for RSC server ids (the server env has consumer === "server", so plugin-rsc's HMR-timestamp branch is also skipped, meaning outputs match), so this is a robustness note, not a correctness bug for the current use. Worth a comment pointing at the upstream normalizeResolvedIdToUrl so the intentional simplification is documented until the shared public-utils work lands.

3. (note) expandExportAll load uses only transformWithOxc.
server-function-directives.ts:241. Plugin-rsc's equivalent uses transformWithOxc ?? transformWithEsbuild. Fine under the Vite 8 baseline (oxc preferred per AGENTS.md), just flagging the deliberate drop of the esbuild fallback.

4. (note) cross-environment ordering assumption for the stale-proxy guard.
The serverReferenceOwnership/ownedReferences module maps drive both the "restore RSC-owned metadata after a non-owning SSR proxy pass" and "don't resurrect RSC-removed metadata" behaviors. The 28 tests exercise these transitions in a fixed order (RSC then SSR). The scheme assumes the RSC env transform lands before the SSR proxy pass for a given id in dev; genuine cross-env interleaving isn't directly tested. Given plugin-rsc drives this the same way, I don't consider it a blocker, but it's the one area I'd want an eye on if flaky metadata shows up in dev HMR.

Verdict

Logic, parity, and tests look solid. The two (low) items are hardening of the newly-exposed generic directive API rather than defects in the use cache path. No blocking issues found.

@ask-bonk

ask-bonk Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2156.

Summary

I reviewed PR #2156 (userland server-function directives). After installing the pinned prerelease via a frozen vp install, all focused checks/tests pass, the build completes, and I verified parity against the installed plugin-rsc source.

Key findings (none blocking):

  1. (low) matchDirective (server-function-directives.ts:129) preserves a g flag on RegExp directives — .match() on a global regex returns match strings without capture groups, so directiveMatch would be wrong. No live bug (/^use cache.*$/ has no g), but the file now exposes a generic userland directive API, so it's a hardening gap.

  2. (low) normalizeViteImportAnalysisUrl reproduction (:221) uses split("?") instead of Vite's cleanUrl (which also strips #), and its /@id/ fallback doesn't guard against already-prefixed ids like Vite's wrapId. Doesn't fire for RSC server ids in practice, but worth a comment.

  3. (note) expandExportAll load drops the transformWithEsbuild fallback — fine under Vite 8.

  4. (note) The stale-proxy metadata guard assumes RSC-before-SSR transform ordering in dev; tests cover the ordered cases but not genuine interleaving.

Positive verification: hashString, the build/dev normalizedId branch, and the plugin splice ordering all match the authoritative plugin-rsc implementation.

I also flagged an environment gotcha: with a stale node_modules (plugin-rsc 0.5.27), vp check surfaces 12 phantom TS errors that disappear once the pinned prerelease is installed — reviewers must vp install first.

github run

@hi-ogawa

Copy link
Copy Markdown

I've added a support of user land server reference registration via vitejs/vite-plugin-react#1310, which is expected to simplify manual server reference management from use cache transform plugin here.

…d-server-functions

# Conflicts:
#	.github/workflows/ci.yml
#	packages/vinext/src/deploy.ts
#	packages/vinext/src/index.ts
#	packages/vinext/src/init.ts
#	packages/vinext/src/shims/cache-runtime.ts
#	playwright.config.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
#	tests/shims.test.ts
#	tests/use-cache-transform.test.ts
@james-elicx
james-elicx force-pushed the codex/pr-1871-userland-server-functions branch from cc55a10 to 2b26266 Compare August 6, 2026 20:31
@james-elicx james-elicx closed this Aug 6, 2026
@james-elicx james-elicx reopened this Aug 6, 2026
@james-elicx
james-elicx changed the base branch from fix/use-cache-nested-function-props to main August 7, 2026 09:49
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 36c26e3 against base 884259a using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 1 regressed · 5 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 134.7 KB 134.7 KB ⚫ 0.0%
Client entry size (gzip) vinext 122.2 KB 122.2 KB ⚫ +0.0%
Dev server cold start vinext 2.84 s 2.89 s 🔴 +2.0%
Production build time vinext 3.09 s 3.09 s ⚫ -0.2%
RSC entry closure size (gzip) vinext 113.9 KB 114.3 KB ⚫ +0.4%
Server bundle size (gzip) vinext 191.4 KB 191.9 KB ⚫ +0.3%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

error: Your local changes to the following files would be overwritten by checkout:
pnpm-lock.yaml
Please commit your changes or stash them before you switch branches.
Aborting

github run

@ask-bonk

ask-bonk Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

@@ -0,0 +1,374 @@
import { createRequire } from "node:module";

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

/bigbonk review — PR #2156 (userland "use cache" server references)

Reviewed the new plugins/use-cache-callable.ts, the cache-callable-runtime.ts closure envelope, the cache-runtime.ts argument/replay changes, and the plugin-composition wiring in index.ts. Installed the pinned @vitejs/plugin-rsc@0.5.34 and ran focused checks/tests:

  • vp check on use-cache-callable.ts, cache-callable-runtime.ts, cache-runtime.ts — clean
  • vp test run tests/use-cache-transform.test.ts36/36 pass
  • vp test run tests/app-router-production-server.test.ts -t "resolves nested" — pass
  • vp test run tests/app-router-rsc-plugin.test.ts -t "rejects a manually registered" — pass

Parity / correctness verification (positive)

  • Prior /bigbonk (low) findings are resolved. The earlier review flagged locally-reproduced hashString and normalizeViteImportAnalysisUrl helpers and a g-flag matchDirective hazard. The rewritten plugin deletes all of that and delegates to the public manager.serverReferences.resolve() / replaceClaim() / deleteClaim() API. The USE_CACHE_DIRECTIVE regex has no g flag. Good cleanup.

  • deleteClaim-on-every-source-file is intentional parity, not a hot-path regression. The transform filter only filters by id (no code: "use cache"), so the handler runs for all source modules and calls deleteClaim(PLUGIN_NAME, id) when the directive is absent. I confirmed plugin-rsc's own rsc:use-server does the exact same thing (plugin-Cbs9j6lP.js:1654-1657: no code filter, deleteClaim on miss). Required for claim-clearing semantics after edits.

  • argumentCount truncation is correct for useActionState-passed cached functions. getArgumentCount reads meta.valueNode, which plugin-rsc documents as "original source function before closure captures are added as parameters" — i.e. user-declared arity, excluding the prepended captures param. The admittedArgs/executionArgs slicing in cache-runtime.ts:486-495 then:

    • drops framework-supplied args beyond declared arity (so a nullary getDate passed to useActionState(getDate, null) ignores React's (prevState, formData) and keeps a stable cache key — the prod test's date2 === date1 assertion depends on this), and
    • correctly re-slots the decrypted captures array as the hoisted fn's first param on the closure-bound path (getMessage), so plaintext captures still drive the cache key.
      I traced getDate (0 params, no captures), getMessage (0 params, 1 capture), and the (1 param + capture) case — all line up with fn(captures, ...userArgs).
  • importPosition correctly preserves the leading file directive. ast.body.find(node => !("directive" in node)) lands imports after the "use cache" prologue. I verified parseAstAsync only sets directive on directive-prologue ExpressionStatements (regular foo() statements have no directive key), so the insertion point is right.

  • Ordering enforcement (vinext({ rsc: false }) must precede rsc()) throws with a clear message and is covered by a new test.

Findings (none blocking)

1. (note) Documented divergence from Next.js arg semantics. Next.js's cache() wrapper (next/dist/server/use-cache/use-cache-wrapper.js:700-993) uses boundArgsLength only to split/validate encrypted bound args, then passes all remaining args to fn and into the cache key — it never truncates to declared arity. vinext's new argumentCount truncation intentionally diverges to make useActionState-passed cached functions cacheable. This is the right call for the prop-passing use case and matches the PR's stated "framework-specific cache behavior," but it is a real behavioral difference worth a one-line comment near the admittedArgs block pointing at the Next.js wrapper, so a future reader doesn't "fix" it back toward Next parity and break the useActionState tests. The shims test "excludes framework arguments from zero-arity cached functions" pins the behavior.

2. (low) No transformExpandExportAll for module-level "use cache" re-export barrels. plugin-rsc's rsc:use-server expands export * from "./x" before wrapping so re-exported references get claimed. The new plugin has no load hook and doesn't call transformExpandExportAll, so a module that combines a file-level "use cache" directive with export * re-exports would not wrap the re-exported names. Unusual pattern and no test covers it; flagging as an edge-case gap rather than a defect on the use cache paths that are exercised.

3. (low) Manual rsc: false with no RSC plugin silently no-ops "use cache". configResolved early-returns leaving manager undefined when allowMissingRsc && !hasRscPlugin, so "use cache" code is left untransformed instead of erroring. The removed vinext:use-cache plugin used to throw "requires @vitejs/plugin-rsc". Acceptable (no RSC plugin means no RSC anyway), but it's a quiet behavior change from the old explicit error.

Verdict

Logic, plugin composition, and the capture/arity runtime all check out against the plugin-rsc 0.5.34 public API and the ported Next.js prop-passing test. The prior review's hardening concerns are gone. No blocking issues — the three items above are a documentation note and two low-severity edge-case gaps.

@ask-bonk

ask-bonk Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2156.

Summary

I reviewed PR #2156 (moving callable "use cache" server references to userland). Installed the pinned @vitejs/plugin-rsc@0.5.34 and ran focused checks/tests — all green:

  • vp check on the three changed source files — clean
  • tests/use-cache-transform.test.ts — 36/36 pass
  • nested-fn-props prod test + manual-ordering rejection test — pass

Key positive verifications:

  • The prior /bigbonk review's (low) findings (reproduced hashString/normalizeViteImportAnalysisUrl, g-flag matchDirective) are resolved — the rewrite delegates to plugin-rsc's public serverReferences API.
  • The deleteClaim-on-every-file pattern is intentional parity with plugin-rsc's own rsc:use-server (verified in the installed source), not a hot-path regression.
  • The argumentCount truncation logic is correct for useActionState-passed cached functions — I traced the nullary, capture-only, and param+capture cases against plugin-rsc's valueNode semantics.
  • importPosition correctly preserves the leading "use cache" directive (verified the parser only tags directive-prologue statements).

Findings (none blocking):

  1. (note) argumentCount truncation is a deliberate but real divergence from Next.js (whose cache() wrapper never truncates to declared arity) — worth a one-line comment so it isn't "corrected" back and broken.
  2. (low) No transformExpandExportAll, so a file-level "use cache" + export * barrel wouldn't wrap re-exported names — untested edge case.
  3. (low) Manual rsc: false with no RSC plugin now silently no-ops "use cache" instead of throwing the old explicit error.

No blocking issues found.

github run

ask-bonk Bot added a commit that referenced this pull request Aug 7, 2026
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
@james-elicx
james-elicx force-pushed the codex/pr-1871-userland-server-functions branch from bf7bdb4 to 36c26e3 Compare August 7, 2026 11:09
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.

2 participants