From ea50d9dcd032df586ec64c0810e0166a9197a8bf Mon Sep 17 00:00:00 2001 From: James Date: Tue, 9 Jun 2026 14:07:21 +0100 Subject: [PATCH 01/33] fix(use-cache): support nested cache functions passed as props --- packages/vinext/src/index.ts | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index a75faaf54a..d467499e70 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -4253,21 +4253,84 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { resolveShimModulePath(shimsDir, "cache-runtime"), ).href; + // In the RSC environment, inline "use cache" functions that are + // passed as props to client components must also be registered as + // server references. Without this, the RSC serializer cannot + // include them in the RSC payload (they're plain async functions + // with no server-reference metadata), so `useActionState` and + // `formAction` props fail to serialize. + // + // Strategy: + // - noExport: true — suppress the automatic `export` on the + // hoisted raw function so we can export the cached wrapper + // instead (needed for loadServerAction to find the right fn). + // - runtime: return `${name}_$$vcf` — a reference to the + // module-level const created in the appended code below. + // - Append per-name module-level code that: + // 1. Creates the cached wrapper via registerCachedFunction + // 2. Registers it as a server reference via + // registerServerReference so RSC can serialize it + // 3. Exports it under the original hoisted name so that + // loadServerAction("fileId#name") resolves correctly + const isRscEnv = this.environment?.name === "rsc"; + const hoistedVariants = new Map(); + try { const result = transformHoistInlineDirective(code, ast, { directive: /^use cache(:\s*\w+)?$/, + noExport: isRscEnv, runtime: (value: string, name: string, meta: { directiveMatch: string[] }) => { const directiveMatch = meta.directiveMatch[0]; const variant = directiveMatch === "use cache" ? "" : directiveMatch.replace("use cache:", "").replace("use cache: ", "").trim(); + if (isRscEnv) { + // Store variant for module-level code generation below. + hoistedVariants.set(name, variant); + // The inline position references the module-level cached + // wrapper, which is defined before the component function + // runs (module-level consts execute at import time). + return `${name}_$$vcf`; + } return `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`; }, rejectNonAsyncFunction: false, }); if (result.names.length > 0) { + if (isRscEnv) { + // Resolve @vitejs/plugin-rsc/react/rsc for registerServerReference. + // This module is always present in the RSC environment. + const resolvedRscReactRscPath = resolveOptionalDependency( + earlyBaseDir, + "@vitejs/plugin-rsc/react/rsc", + ); + const rscReactRscUrl = resolvedRscReactRscPath + ? pathToFileURL(resolvedRscReactRscPath).href + : "@vitejs/plugin-rsc/react/rsc"; + + // For each hoisted name, append module-level code that: + // - creates the cached wrapper (registerCachedFunction) + // - registers it as a server reference (registerServerReference) + // so RSC can serialize it as a prop to client components + // - exports it under the original hoisted name so that + // loadServerAction(fileId + "#" + name) returns the + // cached wrapper (not the raw function) + const moduleExports = result.names + .map((name: string) => { + const variant = hoistedVariants.get(name) ?? ""; + return ( + `\nconst ${name}_$$vcf = /* #__PURE__ */ (await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${name}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)});` + + `\n(await import(${JSON.stringify(rscReactRscUrl)})).registerServerReference(${name}_$$vcf, ${JSON.stringify(id)}, ${JSON.stringify(name + "_$$vcf")});` + + `\nexport { ${name}_$$vcf as ${name} };` + ); + }) + .join("\n"); + + result.output.append(moduleExports); + } + return { code: result.output.toString(), map: result.output.generateMap({ hires: "boundary" }), From 71f2c2288400047f37314535afc43a98c530a320 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 9 Jun 2026 15:05:55 +0100 Subject: [PATCH 02/33] fix(use-cache): use inline registerServerReference instead of broken forward-reference module-level code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/vinext/src/index.ts | 73 ++++++++++-------------------------- 1 file changed, 20 insertions(+), 53 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index d467499e70..9893537204 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -4260,77 +4260,44 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // with no server-reference metadata), so `useActionState` and // `formAction` props fail to serialize. // - // Strategy: - // - noExport: true — suppress the automatic `export` on the - // hoisted raw function so we can export the cached wrapper - // instead (needed for loadServerAction to find the right fn). - // - runtime: return `${name}_$$vcf` — a reference to the - // module-level const created in the appended code below. - // - Append per-name module-level code that: - // 1. Creates the cached wrapper via registerCachedFunction - // 2. Registers it as a server reference via - // registerServerReference so RSC can serialize it - // 3. Exports it under the original hoisted name so that - // loadServerAction("fileId#name") resolves correctly + // Strategy: wrap the registerCachedFunction call with + // registerServerReference inline at call-site. This keeps the + // existing hoisting/export behaviour intact — the hoisted function + // is still exported under its mangled name, and loadServerAction + // can resolve it correctly. The registerServerReference call just + // adds RSC serialisation metadata ($$typeof, $$id) to the cached + // wrapper without changing its runtime behaviour. const isRscEnv = this.environment?.name === "rsc"; - const hoistedVariants = new Map(); + // Resolve @vitejs/plugin-rsc/react/rsc once (only needed for RSC env). + const rscReactRscUrl = isRscEnv + ? (() => { + const p = resolveOptionalDependency(earlyBaseDir, "@vitejs/plugin-rsc/react/rsc"); + return p ? pathToFileURL(p).href : "@vitejs/plugin-rsc/react/rsc"; + })() + : ""; try { const result = transformHoistInlineDirective(code, ast, { directive: /^use cache(:\s*\w+)?$/, - noExport: isRscEnv, runtime: (value: string, name: string, meta: { directiveMatch: string[] }) => { const directiveMatch = meta.directiveMatch[0]; const variant = directiveMatch === "use cache" ? "" : directiveMatch.replace("use cache:", "").replace("use cache: ", "").trim(); + const cachedFnExpr = `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`; if (isRscEnv) { - // Store variant for module-level code generation below. - hoistedVariants.set(name, variant); - // The inline position references the module-level cached - // wrapper, which is defined before the component function - // runs (module-level consts execute at import time). - return `${name}_$$vcf`; + // Wrap with registerServerReference so the cached function + // can be serialised in the RSC payload when passed as a + // prop to a client component (e.g. useActionState / formAction). + return `(await import(${JSON.stringify(rscReactRscUrl)})).registerServerReference(${cachedFnExpr}, ${JSON.stringify(id)}, ${JSON.stringify(name)})`; } - return `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`; + return cachedFnExpr; }, rejectNonAsyncFunction: false, }); if (result.names.length > 0) { - if (isRscEnv) { - // Resolve @vitejs/plugin-rsc/react/rsc for registerServerReference. - // This module is always present in the RSC environment. - const resolvedRscReactRscPath = resolveOptionalDependency( - earlyBaseDir, - "@vitejs/plugin-rsc/react/rsc", - ); - const rscReactRscUrl = resolvedRscReactRscPath - ? pathToFileURL(resolvedRscReactRscPath).href - : "@vitejs/plugin-rsc/react/rsc"; - - // For each hoisted name, append module-level code that: - // - creates the cached wrapper (registerCachedFunction) - // - registers it as a server reference (registerServerReference) - // so RSC can serialize it as a prop to client components - // - exports it under the original hoisted name so that - // loadServerAction(fileId + "#" + name) returns the - // cached wrapper (not the raw function) - const moduleExports = result.names - .map((name: string) => { - const variant = hoistedVariants.get(name) ?? ""; - return ( - `\nconst ${name}_$$vcf = /* #__PURE__ */ (await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${name}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)});` + - `\n(await import(${JSON.stringify(rscReactRscUrl)})).registerServerReference(${name}_$$vcf, ${JSON.stringify(id)}, ${JSON.stringify(name + "_$$vcf")});` + - `\nexport { ${name}_$$vcf as ${name} };` - ); - }) - .join("\n"); - - result.output.append(moduleExports); - } - return { code: result.output.toString(), map: result.output.generateMap({ hires: "boundary" }), From e08d6f4faf3ce419b69f79ca9e8328ddde0bc742 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 9 Jun 2026 15:41:49 +0100 Subject: [PATCH 03/33] fix(use-cache): use correct normalised id and register in manifest for nested function props MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/vinext/src/index.ts | 175 +++++++++++++++++++------ tests/use-cache-server-ref-key.test.ts | 99 ++++++++++++++ 2 files changed, 237 insertions(+), 37 deletions(-) create mode 100644 tests/use-cache-server-ref-key.test.ts diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 9893537204..bfc191f83c 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -175,7 +175,7 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { createRequire } from "node:module"; import fs from "node:fs"; -import { randomBytes, randomUUID } from "node:crypto"; +import { createHash, randomBytes, randomUUID } from "node:crypto"; import commonjs from "vite-plugin-commonjs"; import { normalizePathSeparators } from "./utils/path.js"; @@ -907,6 +907,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // resolves to the configured RSC plugin array. Vite's asyncFlatten // will resolve this before processing the plugin list. let rscPluginPromise: Promise | null = null; + // Captured in configResolved so the use-cache transform can register hoisted + // functions into the plugin-rsc server-reference manifest. + // oxlint-disable-next-line typescript/no-explicit-any + let rscPluginApi: { manager: any } | null = null; if (earlyAppDirExists && autoRsc) { if (!resolvedRscPath) { throw new Error( @@ -4069,6 +4073,21 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { { name: "vinext:use-cache", + configResolved(config) { + // Capture the @vitejs/plugin-rsc manager so we can register hoisted + // "use cache" functions in the server-reference manifest (build) and + // use the correct normalised id for registerServerReference (dev+build). + // getPluginApi is defined on the "rsc:minimal" plugin's .api property. + if (resolvedRscPath) { + // oxlint-disable-next-line typescript/no-explicit-any + const api = (config.plugins as any[]).find( + // oxlint-disable-next-line typescript/no-explicit-any + (p: any) => p && typeof p === "object" && p.name === "rsc:minimal", + )?.api; + if (api) rscPluginApi = api; + } + }, + transform: { // Hook filter: only invoke JS when code contains 'use cache'. // The vast majority of files don't use this directive. @@ -4264,47 +4283,129 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // registerServerReference inline at call-site. This keeps the // existing hoisting/export behaviour intact — the hoisted function // is still exported under its mangled name, and loadServerAction - // can resolve it correctly. The registerServerReference call just - // adds RSC serialisation metadata ($$typeof, $$id) to the cached - // wrapper without changing its runtime behaviour. + // can resolve it correctly. The registerServerReference call adds + // RSC serialisation metadata ($$typeof, $$id) to the cached wrapper. + // + // The $$id passed to registerServerReference must match how + // @vitejs/plugin-rsc resolves server references: + // - build: hashString(toRelativeId(absoluteId)) + // - dev: URL path relative to root (e.g. "/src/app/page.tsx") + // + // We also register the hoisted export names in the plugin-rsc + // serverReferenceMetaMap so the build-time + // virtual:vite-rsc/server-references manifest includes the module. const isRscEnv = this.environment?.name === "rsc"; - // Resolve @vitejs/plugin-rsc/react/rsc once (only needed for RSC env). - const rscReactRscUrl = isRscEnv - ? (() => { - const p = resolveOptionalDependency(earlyBaseDir, "@vitejs/plugin-rsc/react/rsc"); - return p ? pathToFileURL(p).href : "@vitejs/plugin-rsc/react/rsc"; - })() - : ""; - try { - const result = transformHoistInlineDirective(code, ast, { - directive: /^use cache(:\s*\w+)?$/, - runtime: (value: string, name: string, meta: { directiveMatch: string[] }) => { - const directiveMatch = meta.directiveMatch[0]; - const variant = - directiveMatch === "use cache" - ? "" - : directiveMatch.replace("use cache:", "").replace("use cache: ", "").trim(); - const cachedFnExpr = `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`; - if (isRscEnv) { - // Wrap with registerServerReference so the cached function - // can be serialised in the RSC payload when passed as a - // prop to a client component (e.g. useActionState / formAction). - return `(await import(${JSON.stringify(rscReactRscUrl)})).registerServerReference(${cachedFnExpr}, ${JSON.stringify(id)}, ${JSON.stringify(name)})`; + if (isRscEnv) { + // Compute the normalised reference key that matches what + // @vitejs/plugin-rsc's "use server" transform writes. Mirror the + // logic from vitePluginUseServer's getNormalizedId(): + // build → sha256(toRelativeId).hex.slice(0,12) + // dev → id.slice(root.length) (URL path under Vite root) + const envConfig = this.environment?.config; + const projectRoot = envConfig?.root ?? root; + const isBuild = this.environment?.mode === "build"; + const normalizedRefKey = isBuild + ? createHash("sha256") + .update( + id.replace(/\\/g, "/").slice(projectRoot.replace(/\\/g, "/").length + 1), + ) + .digest() + .toString("hex") + .slice(0, 12) + : id.startsWith(projectRoot + "/") || id.startsWith(projectRoot + "\\") + ? id.slice(projectRoot.length) + : id; + + // Resolve @vitejs/plugin-rsc/react/rsc for the registerServerReference call. + const rscReactRscUrl = (() => { + const p = resolveOptionalDependency(earlyBaseDir, "@vitejs/plugin-rsc/react/rsc"); + return p ? pathToFileURL(p).href : "@vitejs/plugin-rsc/react/rsc"; + })(); + + try { + const result = transformHoistInlineDirective(code, ast, { + directive: /^use cache(:\s*\w+)?$/, + runtime: (value: string, name: string, meta: { directiveMatch: string[] }) => { + const directiveMatch = meta.directiveMatch[0]; + const variant = + directiveMatch === "use cache" + ? "" + : directiveMatch + .replace("use cache:", "") + .replace("use cache: ", "") + .trim(); + const cachedFnExpr = `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`; + // Use the normalised key so loadServerAction can resolve the + // module via the server-references manifest (build) or direct + // Vite URL import (dev). Using the raw absolute id here would + // cause "server reference not found" in production. + return `(await import(${JSON.stringify(rscReactRscUrl)})).registerServerReference(${cachedFnExpr}, ${JSON.stringify(normalizedRefKey)}, ${JSON.stringify(name)})`; + }, + rejectNonAsyncFunction: false, + }); + + if (result.names.length > 0) { + // Register hoisted export names in the plugin-rsc manager's + // serverReferenceMetaMap so virtual:vite-rsc/server-references + // includes this module in the production manifest. + if (rscPluginApi?.manager) { + const existing = rscPluginApi.manager.serverReferenceMetaMap[id]; + if (existing) { + // Merge: preserve any names already registered (e.g., from + // "use server" — unlikely but safe to handle). + const merged = Array.from( + new Set([...existing.exportNames, ...result.names]), + ); + rscPluginApi.manager.serverReferenceMetaMap[id] = { + importId: id, + referenceKey: normalizedRefKey, + exportNames: merged, + }; + } else { + rscPluginApi.manager.serverReferenceMetaMap[id] = { + importId: id, + referenceKey: normalizedRefKey, + exportNames: result.names, + }; + } } - return cachedFnExpr; - }, - rejectNonAsyncFunction: false, - }); + return { + code: result.output.toString(), + map: result.output.generateMap({ hires: "boundary" }), + }; + } + } catch { + // If hoisting fails (e.g., complex closure), fall through + } + } else { + // Non-RSC env: no server reference wrapping needed. + try { + const result = transformHoistInlineDirective(code, ast, { + directive: /^use cache(:\s*\w+)?$/, + runtime: (value: string, name: string, meta: { directiveMatch: string[] }) => { + const directiveMatch = meta.directiveMatch[0]; + const variant = + directiveMatch === "use cache" + ? "" + : directiveMatch + .replace("use cache:", "") + .replace("use cache: ", "") + .trim(); + return `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`; + }, + rejectNonAsyncFunction: false, + }); - if (result.names.length > 0) { - return { - code: result.output.toString(), - map: result.output.generateMap({ hires: "boundary" }), - }; + if (result.names.length > 0) { + return { + code: result.output.toString(), + map: result.output.generateMap({ hires: "boundary" }), + }; + } + } catch { + // If hoisting fails (e.g., complex closure), fall through } - } catch { - // If hoisting fails (e.g., complex closure), fall through } } diff --git a/tests/use-cache-server-ref-key.test.ts b/tests/use-cache-server-ref-key.test.ts new file mode 100644 index 0000000000..5fa7143b63 --- /dev/null +++ b/tests/use-cache-server-ref-key.test.ts @@ -0,0 +1,99 @@ +/** + * Tests for the normalised server-reference key used by the "use cache" + * inline-directive transform. + * + * The key must match what @vitejs/plugin-rsc's "use server" transform + * produces so that loadServerAction() can resolve the module at runtime. + * + * build key: sha256(toRelativeId(absoluteId)).hex.slice(0,12) + * where toRelativeId = normalizePath(path.relative(root, id)) + * and normalizePath = forward-slash conversion + * + * dev key: id.slice(root.length) (URL path served by Vite dev server) + */ +import { describe, expect, it } from "vite-plus/test"; +import { createHash } from "node:crypto"; +import path from "node:path"; + +// Replicate the helper from @vitejs/plugin-rsc (plugin-BK29Va7z.js). +function hashString(v: string): string { + return createHash("sha256").update(v).digest().toString("hex").slice(0, 12); +} + +// Replicate Vite's normalizePath (converts backslashes → forward slashes). +function normalizePath(p: string): string { + return p.replace(/\\/g, "/"); +} + +// The exact formula used in vinext's use-cache transform for build mode. +function buildNormalizedRefKey(root: string, id: string): string { + const relId = normalizePath(path.relative(root, id)); + return hashString(relId); +} + +// The exact formula used in vinext's use-cache transform for dev mode. +function devNormalizedRefKey(root: string, id: string): string { + if (id.startsWith(root + "/") || id.startsWith(root + "\\")) { + return id.slice(root.length); + } + return id; +} + +describe("use-cache inline function: build-mode normalised reference key", () => { + const root = "/home/user/project"; + + it("matches plugin-rsc hashString(toRelativeId) for a nested source file", () => { + const id = "/home/user/project/src/app/actions.ts"; + const relId = "src/app/actions.ts"; + const expected = hashString(relId); + expect(buildNormalizedRefKey(root, id)).toBe(expected); + }); + + it("matches for a file at the root", () => { + const id = "/home/user/project/page.tsx"; + const expected = hashString("page.tsx"); + expect(buildNormalizedRefKey(root, id)).toBe(expected); + }); + + it("produces a 12-character hex string", () => { + const id = "/home/user/project/app/page.tsx"; + const key = buildNormalizedRefKey(root, id); + expect(key).toMatch(/^[0-9a-f]{12}$/); + }); + + it("different files produce different keys (no collisions for realistic paths)", () => { + const files = [ + "/home/user/project/src/app/actions.ts", + "/home/user/project/src/app/other-actions.ts", + "/home/user/project/src/lib/data.ts", + "/home/user/project/app/page.tsx", + ]; + const keys = files.map((f) => buildNormalizedRefKey(root, f)); + const unique = new Set(keys); + expect(unique.size).toBe(files.length); + }); + + it("is stable across calls (deterministic)", () => { + const id = "/home/user/project/src/app/actions.ts"; + expect(buildNormalizedRefKey(root, id)).toBe(buildNormalizedRefKey(root, id)); + }); +}); + +describe("use-cache inline function: dev-mode normalised reference key", () => { + const root = "/home/user/project"; + + it("strips root prefix leaving a /...-prefixed path", () => { + const id = "/home/user/project/src/app/actions.ts"; + expect(devNormalizedRefKey(root, id)).toBe("/src/app/actions.ts"); + }); + + it("handles files directly under root", () => { + const id = "/home/user/project/page.tsx"; + expect(devNormalizedRefKey(root, id)).toBe("/page.tsx"); + }); + + it("returns id unchanged for ids outside root (e.g. node_modules absolute path)", () => { + const id = "/home/user/other-project/something.ts"; + expect(devNormalizedRefKey(root, id)).toBe(id); + }); +}); From 1d3c833642284db77bbde3d1042f5faafc4d66bb Mon Sep 17 00:00:00 2001 From: James Date: Wed, 10 Jun 2026 11:50:25 +0100 Subject: [PATCH 04/33] fix(use-cache): wrap hoisted exports as cached server references and 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. --- packages/vinext/src/index.ts | 286 +++++++++++------- tests/app-router-production-server.test.ts | 54 ++++ tests/e2e/app-router/use-cache.spec.ts | 30 ++ .../app/use-cache-nested-fn-props/form.tsx | 29 ++ .../app/use-cache-nested-fn-props/page.tsx | 44 +++ tests/use-cache-server-ref-key.test.ts | 99 ------ 6 files changed, 331 insertions(+), 211 deletions(-) create mode 100644 tests/fixtures/app-basic/app/use-cache-nested-fn-props/form.tsx create mode 100644 tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx delete mode 100644 tests/use-cache-server-ref-key.test.ts diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index bfc191f83c..d2f1ad285c 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -911,6 +911,27 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // functions into the plugin-rsc server-reference manifest. // oxlint-disable-next-line typescript/no-explicit-any let rscPluginApi: { manager: any } | null = null; + // Hoisted "use cache" functions (per module id) pending registration in the + // plugin-rsc server-reference manifest. Written by the "vinext:use-cache" + // transform and consumed by "vinext:use-cache-server-references", which runs + // AFTER plugin-rsc's "rsc:use-server" transform. That ordering is load-bearing: + // rsc:use-server deletes manager.serverReferenceMetaMap[id] for any module + // whose code does not contain "use server", so writing the entry from the + // vinext:use-cache transform directly would be wiped out a moment later. + const useCacheServerRefMeta = new Map(); + // Resolved file URL of @vitejs/plugin-rsc/react/rsc for generated + // registerServerReference imports. Invariant per process — resolved once. + // This resolves to the same file as the bare "@vitejs/plugin-rsc/react/rsc" + // import inside the cache-runtime shim (Vite normalises file:// URLs to the + // same module id), so the RSC environment does not load two module copies. + let cachedRscReactRscUrl: string | undefined; + const getRscReactRscUrl = (): string => { + if (cachedRscReactRscUrl === undefined) { + const p = resolveOptionalDependency(earlyBaseDir, "@vitejs/plugin-rsc/react/rsc"); + cachedRscReactRscUrl = p ? pathToFileURL(p).href : "@vitejs/plugin-rsc/react/rsc"; + } + return cachedRscReactRscUrl; + }; if (earlyAppDirExists && autoRsc) { if (!resolvedRscPath) { throw new Error( @@ -4279,133 +4300,136 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // with no server-reference metadata), so `useActionState` and // `formAction` props fail to serialize. // - // Strategy: wrap the registerCachedFunction call with - // registerServerReference inline at call-site. This keeps the - // existing hoisting/export behaviour intact — the hoisted function - // is still exported under its mangled name, and loadServerAction - // can resolve it correctly. The registerServerReference call adds - // RSC serialisation metadata ($$typeof, $$id) to the cached wrapper. + // Strategy (mirrors Next.js' use-cache SWC transform, where the + // exported $$RSC_SERVER_CACHE_n binding IS the cache wrapper): + // after hoisting, reassign each hoisted export at module level to + // registerServerReference(registerCachedFunction(fn)). Exported + // function declarations are live bindings, so both the original + // call sites and the server-reference manifest (which imports the + // module export by name on action POST) observe the wrapped + // function. This means a direct client→server invocation of the + // cached function goes through the cache, matching Next.js + // semantics — not just "callable but uncached". // // The $$id passed to registerServerReference must match how // @vitejs/plugin-rsc resolves server references: // - build: hashString(toRelativeId(absoluteId)) // - dev: URL path relative to root (e.g. "/src/app/page.tsx") // - // We also register the hoisted export names in the plugin-rsc - // serverReferenceMetaMap so the build-time - // virtual:vite-rsc/server-references manifest includes the module. + // The hoisted export names are also queued for registration in the + // plugin-rsc serverReferenceMetaMap (via the + // "vinext:use-cache-server-references" plugin below) so the + // build-time virtual:vite-rsc/server-references manifest includes + // the module and dev-mode reference validation accepts the key. + // + // Known limitation: closure-captured variables (hoisted into + // `.bind(null, ...)` args) are serialised to the client + // unencrypted when such a function is passed as a prop, unlike + // plugin-rsc's "use server" transform which encrypts bound args by + // default. Encrypting them here interacts with cache-key + // determinism (AES-GCM ciphertext differs per render) and needs + // its own design pass. const isRscEnv = this.environment?.name === "rsc"; + // Compute the normalised reference key that matches what + // @vitejs/plugin-rsc's "use server" transform writes. Mirror the + // logic from vitePluginUseServer's getNormalizedId(): + // build → hashString(manager.toRelativeId(id)) + // dev → URL path under the Vite root + let normalizedRefKey: string | null = null; if (isRscEnv) { - // Compute the normalised reference key that matches what - // @vitejs/plugin-rsc's "use server" transform writes. Mirror the - // logic from vitePluginUseServer's getNormalizedId(): - // build → sha256(toRelativeId).hex.slice(0,12) - // dev → id.slice(root.length) (URL path under Vite root) - const envConfig = this.environment?.config; - const projectRoot = envConfig?.root ?? root; - const isBuild = this.environment?.mode === "build"; - const normalizedRefKey = isBuild - ? createHash("sha256") - .update( - id.replace(/\\/g, "/").slice(projectRoot.replace(/\\/g, "/").length + 1), - ) - .digest() - .toString("hex") - .slice(0, 12) - : id.startsWith(projectRoot + "/") || id.startsWith(projectRoot + "\\") - ? id.slice(projectRoot.length) - : id; - - // Resolve @vitejs/plugin-rsc/react/rsc for the registerServerReference call. - const rscReactRscUrl = (() => { - const p = resolveOptionalDependency(earlyBaseDir, "@vitejs/plugin-rsc/react/rsc"); - return p ? pathToFileURL(p).href : "@vitejs/plugin-rsc/react/rsc"; - })(); + // oxlint-disable-next-line typescript/no-explicit-any + const manager: any = rscPluginApi?.manager ?? null; + const projectRoot: string = + manager?.config?.root ?? this.environment?.config?.root ?? root; + if (this.environment?.mode === "build") { + // Prefer the plugin's own toRelativeId so the hash input is + // byte-for-byte identical to what plugin-rsc would produce + // (path.relative against manager.config.root). The fallback + // replicates it for the manager-less case (where the manifest + // can't be populated anyway, but the key stays sane). + const relativeId: string = manager + ? manager.toRelativeId(id) + : normalizePathSeparators(path.relative(projectRoot, id)); + // hashString from plugin-rsc: sha256 → hex → first 12 chars. + normalizedRefKey = createHash("sha256") + .update(relativeId) + .digest() + .toString("hex") + .slice(0, 12); + } else { + normalizedRefKey = + id.startsWith(projectRoot + "/") || id.startsWith(projectRoot + "\\") + ? id.slice(projectRoot.length) + : id; + } + } - try { - const result = transformHoistInlineDirective(code, ast, { - directive: /^use cache(:\s*\w+)?$/, - runtime: (value: string, name: string, meta: { directiveMatch: string[] }) => { - const directiveMatch = meta.directiveMatch[0]; - const variant = - directiveMatch === "use cache" - ? "" - : directiveMatch - .replace("use cache:", "") - .replace("use cache: ", "") - .trim(); - const cachedFnExpr = `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`; - // Use the normalised key so loadServerAction can resolve the - // module via the server-references manifest (build) or direct - // Vite URL import (dev). Using the raw absolute id here would - // cause "server reference not found" in production. - return `(await import(${JSON.stringify(rscReactRscUrl)})).registerServerReference(${cachedFnExpr}, ${JSON.stringify(normalizedRefKey)}, ${JSON.stringify(name)})`; - }, - rejectNonAsyncFunction: false, - }); + const parseVariant = (directiveMatch: string): string => + directiveMatch === "use cache" + ? "" + : directiveMatch.replace("use cache:", "").replace("use cache: ", "").trim(); - if (result.names.length > 0) { - // Register hoisted export names in the plugin-rsc manager's - // serverReferenceMetaMap so virtual:vite-rsc/server-references - // includes this module in the production manifest. - if (rscPluginApi?.manager) { - const existing = rscPluginApi.manager.serverReferenceMetaMap[id]; - if (existing) { - // Merge: preserve any names already registered (e.g., from - // "use server" — unlikely but safe to handle). - const merged = Array.from( - new Set([...existing.exportNames, ...result.names]), - ); - rscPluginApi.manager.serverReferenceMetaMap[id] = { - importId: id, - referenceKey: normalizedRefKey, - exportNames: merged, - }; - } else { - rscPluginApi.manager.serverReferenceMetaMap[id] = { - importId: id, - referenceKey: normalizedRefKey, - exportNames: result.names, - }; - } + try { + // Hoisted function metadata collected during the transform so the + // RSC branch can emit the module-level wrapping afterwards. + const hoisted: { name: string; variant: string }[] = []; + const result = transformHoistInlineDirective(code, ast, { + directive: /^use cache(:\s*\w+)?$/, + runtime: (value: string, name: string, meta: { directiveMatch: string[] }) => { + const variant = parseVariant(meta.directiveMatch[0]); + if (isRscEnv) { + // The hoisted export is wrapped once at module level + // (below); the call site just references it. `.bind()` on + // the wrapped export is handled by registerServerReference's + // patched bind, which tracks $$bound for serialisation. + hoisted.push({ name, variant }); + return value; } - return { - code: result.output.toString(), - map: result.output.generateMap({ hires: "boundary" }), - }; - } - } catch { - // If hoisting fails (e.g., complex closure), fall through - } - } else { - // Non-RSC env: no server reference wrapping needed. - try { - const result = transformHoistInlineDirective(code, ast, { - directive: /^use cache(:\s*\w+)?$/, - runtime: (value: string, name: string, meta: { directiveMatch: string[] }) => { - const directiveMatch = meta.directiveMatch[0]; - const variant = - directiveMatch === "use cache" - ? "" - : directiveMatch - .replace("use cache:", "") - .replace("use cache: ", "") - .trim(); - return `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`; - }, - rejectNonAsyncFunction: false, - }); + // Non-RSC env: no server-reference metadata needed — wrap the + // call site with the cache runtime only. + return `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`; + }, + rejectNonAsyncFunction: false, + }); - if (result.names.length > 0) { - return { - code: result.output.toString(), - map: result.output.generateMap({ hires: "boundary" }), - }; + if (result.names.length > 0) { + if (isRscEnv && normalizedRefKey !== null) { + // Reassign each hoisted export to the cached + registered + // wrapper at module level. The assignments are PREPENDED: + // hoisted function declarations are initialised before any + // statement executes, so the reassignment runs first and + // every later reader — top-level call sites (e.g. + // `const getData = ` initialisers, which + // copy the binding value at evaluation time), render-time + // call sites, and the server-references manifest import on + // action POST — observes the wrapped function. + const lines: string[] = [ + `import { registerCachedFunction as __vinext_registerCachedFunction } from ${JSON.stringify(runtimeModuleUrl2)};`, + `import { registerServerReference as __vinext_registerServerReference } from ${JSON.stringify(getRscReactRscUrl())};`, + ]; + for (const { name, variant } of hoisted) { + lines.push( + `${name} = __vinext_registerServerReference(__vinext_registerCachedFunction(${name}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)}), ${JSON.stringify(normalizedRefKey)}, ${JSON.stringify(name)});`, + ); + } + result.output.prepend(lines.join("\n") + "\n"); + + // Queue the manifest registration — performed by + // "vinext:use-cache-server-references" after rsc:use-server + // has run (see useCacheServerRefMeta for why). + useCacheServerRefMeta.set(id, { + referenceKey: normalizedRefKey, + exportNames: result.names, + }); } - } catch { - // If hoisting fails (e.g., complex closure), fall through + return { + code: result.output.toString(), + map: result.output.generateMap({ hires: "boundary" }), + }; } + } catch { + // If hoisting fails (e.g., complex closure), fall through } } @@ -5087,6 +5111,44 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (rscPluginPromise) { plugins.push(rscPluginPromise); plugins.push(createRscClientReferenceLoadersPlugin()); + // Registers hoisted "use cache" functions in the plugin-rsc + // serverReferenceMetaMap so the build-time + // virtual:vite-rsc/server-references manifest includes their modules and + // dev-mode reference validation accepts their keys. This plugin MUST be + // placed after the plugin-rsc plugins: plugin-rsc's "rsc:use-server" + // transform deletes serverReferenceMetaMap[id] for every module whose code + // lacks "use server", which would wipe an entry written earlier in the + // same transform pipeline by "vinext:use-cache". + plugins.push({ + name: "vinext:use-cache-server-references", + transform: { + handler(_code, id) { + // Consume (get + delete) the pending entry so a later re-transform + // of a module that no longer contains "use cache" can't re-register + // stale export names. + const pending = useCacheServerRefMeta.get(id); + if (!pending || this.environment?.name !== "rsc" || !rscPluginApi?.manager) { + return null; + } + useCacheServerRefMeta.delete(id); + const metaMap = rscPluginApi.manager.serverReferenceMetaMap; + const existing = metaMap[id]; + // A module can contain both inline "use server" and inline + // "use cache" functions. In that case rsc:use-server has already + // written an entry for this id — its referenceKey is computed with + // the same formula as ours, so the keys agree and only the export + // names need to be unioned. + metaMap[id] = { + importId: id, + referenceKey: pending.referenceKey, + exportNames: existing + ? Array.from(new Set([...existing.exportNames, ...pending.exportNames])) + : pending.exportNames, + }; + return null; + }, + }, + }); } return plugins; diff --git a/tests/app-router-production-server.test.ts b/tests/app-router-production-server.test.ts index eb4607d6f2..9220e49154 100644 --- a/tests/app-router-production-server.test.ts +++ b/tests/app-router-production-server.test.ts @@ -897,6 +897,60 @@ describe("App Router Production server (startProdServer)", () => { expect(body3.timestamp).not.toBe(body1.timestamp); }); + // Ported from Next.js: test/e2e/app-dir/use-cache-with-server-function-props + // ("should be able to use nested cache functions as props"). + // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/use-cache-with-server-function-props/use-cache-with-server-function-props.test.ts + // + // Inline "use cache" functions defined inside a cached component and passed + // as props to a client component must (a) serialize as server references in + // the RSC payload and (b) resolve back through the production + // server-references manifest on the action POST. (b) can only fail in + // production builds — the manifest is keyed by the plugin-rsc normalised + // reference key and generated from serverReferenceMetaMap — so this test + // must run against the built output, not the dev server. + it("resolves nested 'use cache' functions passed as props when invoked as actions", async () => { + const res = await fetch(`${baseUrl}/use-cache-nested-fn-props`); + expect(res.status).toBe(200); + const html = await res.text(); + + // The flight payload embeds each cached function prop as a server + // reference whose id is "<12-hex normalised key>#". + // Serialization order follows the props order: getDate first, getRandom + // second. + const refIds = [...new Set(html.match(/[0-9a-f]{12}#\$\$hoist_\d+_[A-Za-z0-9_$]+/g) ?? [])]; + expect(refIds.length).toBe(2); + const [getDateRefId, getRandomRefId] = refIds; + + const invokeAction = async (actionId: string): Promise => { + const actionRes = await fetch(`${baseUrl}/use-cache-nested-fn-props.rsc`, { + method: "POST", + headers: { + "Content-Type": "text/plain", + "x-rsc-action": actionId, + }, + body: JSON.stringify([]), + }); + expect(actionRes.status).toBe(200); + expect(actionRes.headers.get("x-nextjs-action-not-found")).toBeNull(); + const text = await actionRes.text(); + expect(text).not.toContain("Server action not found"); + return text; + }; + + const isoDateRegExp = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/; + const date1 = (await invokeAction(getDateRefId)).match(isoDateRegExp)?.[0]; + expect(date1).toBeDefined(); + + // The resolved server reference is the cached wrapper (Next.js parity: + // the exported cached binding IS the server reference), so a second + // invocation with identical arguments returns the cached value. + const date2 = (await invokeAction(getDateRefId)).match(isoDateRegExp)?.[0]; + expect(date2).toBe(date1); + + const randomText = await invokeAction(getRandomRefId); + expect(randomText).toMatch(/\d+\.\d+/); + }); + it("middleware request header overrides still apply after middleware calls headers() first", async () => { // Regression for a bug where a middleware that reads `next/headers` → // `headers()` *before* returning `NextResponse.next({ request: { headers } })` diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index 2752d42c40..82ed5c17b3 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -99,6 +99,36 @@ test.describe('"use cache" function-level directive', () => { }); }); +test.describe('"use cache" nested cache functions as props', () => { + // Ported from Next.js: test/e2e/app-dir/use-cache-with-server-function-props + // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/use-cache-with-server-function-props/use-cache-with-server-function-props.test.ts + // + // Inline "use cache" functions defined inside a cached component are passed + // as props to a client component and invoked via useActionState. This is a + // full client→server round-trip: the cached functions must serialize as + // server references in the RSC payload AND resolve back on the action POST. + const isoDateRegExp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + const randomRegExp = /^\d+\.\d+$/; + + test("should be able to use nested cache functions as props", async ({ page }) => { + await page.goto(`${BASE}/use-cache-nested-fn-props`); + + // Click + assert inside a polling loop: a click that lands before + // hydration completes falls back to a native form POST (full document + // reload) and loses the useActionState output, so retry until the + // hydrated client-side round-trip succeeds. + await expect(async () => { + await page.locator("#submit-button-date").click(); + await expect(page.locator("#date")).toHaveText(isoDateRegExp, { timeout: 2000 }); + }).toPass({ timeout: 15_000 }); + + await expect(async () => { + await page.locator("#submit-button-random").click(); + await expect(page.locator("#random")).toHaveText(randomRegExp, { timeout: 2000 }); + }).toPass({ timeout: 15_000 }); + }); +}); + test.describe('"use cache: private"', () => { test("allows reading cookies inside private caches", async ({ request }) => { // Ported from Next.js: test/e2e/app-dir/use-cache-private/use-cache-private.test.ts diff --git a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/form.tsx b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/form.tsx new file mode 100644 index 0000000000..90577adff4 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/form.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useActionState } from "react"; + +// Ported from Next.js: test/e2e/app-dir/use-cache-with-server-function-props +// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/use-cache-with-server-function-props/app/nested-cache/form.tsx + +export function Form({ + getDate, + getRandom, +}: { + getDate: () => Promise; + getRandom: () => Promise; +}) { + const [date, formAction, isDatePending] = useActionState(getDate, null); + + const [random, buttonAction, isRandomPending] = useActionState(getRandom, null); + + return ( +
+ {" "} + +

{isDatePending ? "loading..." : date}

+

{isRandomPending ? "loading..." : random}

+
+ ); +} diff --git a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx new file mode 100644 index 0000000000..aa99758435 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx @@ -0,0 +1,44 @@ +import { connection } from "next/server"; +import { Suspense } from "react"; +import { Form } from "./form"; + +// Ported from Next.js: test/e2e/app-dir/use-cache-with-server-function-props +// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/use-cache-with-server-function-props/app/nested-cache/page.tsx +// +// Inline "use cache" functions defined inside a cached component are passed +// as props to a client component, which invokes them via useActionState. +// This requires the cached functions to be registered as server references +// (serializable into the RSC payload, resolvable on action POST). + +export default function UseCacheNestedFnPropsPage() { + return ( +
+ Loading...}> + + + +
+ ); +} + +async function CachedForm() { + "use cache"; + + return ( +
{ + "use cache"; + return new Date().toISOString(); + }} + getRandom={async function getRandom() { + "use cache"; + return Math.random(); + }} + /> + ); +} + +const Dynamic = async () => { + await connection(); + return

Dynamic

; +}; diff --git a/tests/use-cache-server-ref-key.test.ts b/tests/use-cache-server-ref-key.test.ts deleted file mode 100644 index 5fa7143b63..0000000000 --- a/tests/use-cache-server-ref-key.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Tests for the normalised server-reference key used by the "use cache" - * inline-directive transform. - * - * The key must match what @vitejs/plugin-rsc's "use server" transform - * produces so that loadServerAction() can resolve the module at runtime. - * - * build key: sha256(toRelativeId(absoluteId)).hex.slice(0,12) - * where toRelativeId = normalizePath(path.relative(root, id)) - * and normalizePath = forward-slash conversion - * - * dev key: id.slice(root.length) (URL path served by Vite dev server) - */ -import { describe, expect, it } from "vite-plus/test"; -import { createHash } from "node:crypto"; -import path from "node:path"; - -// Replicate the helper from @vitejs/plugin-rsc (plugin-BK29Va7z.js). -function hashString(v: string): string { - return createHash("sha256").update(v).digest().toString("hex").slice(0, 12); -} - -// Replicate Vite's normalizePath (converts backslashes → forward slashes). -function normalizePath(p: string): string { - return p.replace(/\\/g, "/"); -} - -// The exact formula used in vinext's use-cache transform for build mode. -function buildNormalizedRefKey(root: string, id: string): string { - const relId = normalizePath(path.relative(root, id)); - return hashString(relId); -} - -// The exact formula used in vinext's use-cache transform for dev mode. -function devNormalizedRefKey(root: string, id: string): string { - if (id.startsWith(root + "/") || id.startsWith(root + "\\")) { - return id.slice(root.length); - } - return id; -} - -describe("use-cache inline function: build-mode normalised reference key", () => { - const root = "/home/user/project"; - - it("matches plugin-rsc hashString(toRelativeId) for a nested source file", () => { - const id = "/home/user/project/src/app/actions.ts"; - const relId = "src/app/actions.ts"; - const expected = hashString(relId); - expect(buildNormalizedRefKey(root, id)).toBe(expected); - }); - - it("matches for a file at the root", () => { - const id = "/home/user/project/page.tsx"; - const expected = hashString("page.tsx"); - expect(buildNormalizedRefKey(root, id)).toBe(expected); - }); - - it("produces a 12-character hex string", () => { - const id = "/home/user/project/app/page.tsx"; - const key = buildNormalizedRefKey(root, id); - expect(key).toMatch(/^[0-9a-f]{12}$/); - }); - - it("different files produce different keys (no collisions for realistic paths)", () => { - const files = [ - "/home/user/project/src/app/actions.ts", - "/home/user/project/src/app/other-actions.ts", - "/home/user/project/src/lib/data.ts", - "/home/user/project/app/page.tsx", - ]; - const keys = files.map((f) => buildNormalizedRefKey(root, f)); - const unique = new Set(keys); - expect(unique.size).toBe(files.length); - }); - - it("is stable across calls (deterministic)", () => { - const id = "/home/user/project/src/app/actions.ts"; - expect(buildNormalizedRefKey(root, id)).toBe(buildNormalizedRefKey(root, id)); - }); -}); - -describe("use-cache inline function: dev-mode normalised reference key", () => { - const root = "/home/user/project"; - - it("strips root prefix leaving a /...-prefixed path", () => { - const id = "/home/user/project/src/app/actions.ts"; - expect(devNormalizedRefKey(root, id)).toBe("/src/app/actions.ts"); - }); - - it("handles files directly under root", () => { - const id = "/home/user/project/page.tsx"; - expect(devNormalizedRefKey(root, id)).toBe("/page.tsx"); - }); - - it("returns id unchanged for ids outside root (e.g. node_modules absolute path)", () => { - const id = "/home/user/other-project/something.ts"; - expect(devNormalizedRefKey(root, id)).toBe(id); - }); -}); From e8a434b90cb36fc67ad9757239e81aa58e5d0f77 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 10 Jun 2026 12:04:13 +0100 Subject: [PATCH 05/33] docs(use-cache): document dev-key normalisation scope for inline cache server references --- packages/vinext/src/index.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index d2f1ad285c..9de6162ac0 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -4358,6 +4358,20 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { .toString("hex") .slice(0, 12); } else { + // Dev key. plugin-rsc's getNormalizedId() additionally runs + // cleanUrl() on node_modules ids and uses an /@fs/-prefixed + // URL for ids outside the project root. Those shapes are not + // replicated here on purpose: this transform's filter excludes + // node_modules entirely, and the extension-anchored id regex + // (/\.(tsx?|jsx?|mjs)$/) rejects ids carrying a ?query, so + // neither can reach this point. For under-root source files + // the plugin's normalisation reduces to exactly this + // root-prefix slice. Ids outside the root (e.g. linked + // packages) keep the raw absolute path — unlike the plugin's + // /@fs/ URL, but still self-consistent: the same key is + // registered in serverReferenceMetaMap (which dev validation + // checks) and passed to the dev loader's import(id), which + // accepts absolute paths. normalizedRefKey = id.startsWith(projectRoot + "/") || id.startsWith(projectRoot + "\\") ? id.slice(projectRoot.length) From f6002305760c6d1bc44bd7ac6d56f58a97c7c6bc Mon Sep 17 00:00:00 2001 From: James Date: Wed, 10 Jun 2026 12:27:39 +0100 Subject: [PATCH 06/33] fix(use-cache): throw instead of emitting unresolvable inline cache server references when the plugin-rsc manager is missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/vinext/src/index.ts | 33 ++++-- tests/use-cache-transform.test.ts | 170 ++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 tests/use-cache-transform.test.ts diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 9de6162ac0..e04105b7d0 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -4340,17 +4340,34 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (isRscEnv) { // oxlint-disable-next-line typescript/no-explicit-any const manager: any = rscPluginApi?.manager ?? null; + // Fail loudly when the plugin-rsc manager is unavailable. + // Without it, "vinext:use-cache-server-references" cannot write + // the serverReferenceMetaMap entry, so wrapping the hoisted + // exports anyway would emit a serializable-but-unresolvable + // server reference: the RSC payload serialises fine, but the + // action POST 404s because the key is absent from the built + // server-references manifest (and fails dev-mode reference + // validation). The manager is a structural invariant whenever + // the "rsc" environment exists ("rsc:minimal" is always part of + // the plugin-rsc set and exposes it via .api), so this throw is + // believed unreachable — but a loud transform error beats a + // silent production 404 if that invariant ever breaks. + if (!manager) { + throw new Error( + `vinext: cannot register inline "use cache" function(s) in ${id} as server ` + + `references: the @vitejs/plugin-rsc manager is unavailable (no "rsc:minimal" ` + + `plugin with a manager api was found in the resolved Vite config). Refusing ` + + `to emit a server reference that would serialize but never resolve (it would ` + + `404 on action POST).`, + ); + } const projectRoot: string = - manager?.config?.root ?? this.environment?.config?.root ?? root; + manager.config?.root ?? this.environment?.config?.root ?? root; if (this.environment?.mode === "build") { - // Prefer the plugin's own toRelativeId so the hash input is + // Use the plugin's own toRelativeId so the hash input is // byte-for-byte identical to what plugin-rsc would produce - // (path.relative against manager.config.root). The fallback - // replicates it for the manager-less case (where the manifest - // can't be populated anyway, but the key stays sane). - const relativeId: string = manager - ? manager.toRelativeId(id) - : normalizePathSeparators(path.relative(projectRoot, id)); + // (path.relative against manager.config.root). + const relativeId: string = manager.toRelativeId(id); // hashString from plugin-rsc: sha256 → hex → first 12 chars. normalizedRefKey = createHash("sha256") .update(relativeId) diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts new file mode 100644 index 0000000000..d3b4ec044a --- /dev/null +++ b/tests/use-cache-transform.test.ts @@ -0,0 +1,170 @@ +/** + * Unit tests for the "vinext:use-cache" transform's server-reference wrapping + * of inline (function-level) "use cache" directives in the RSC environment. + * + * These call the plugin's transform hook directly (same pattern as + * optimize-imports.test.ts) so they can exercise environment/manager + * combinations that are impractical to reproduce through a full Vite server: + * + * 1. The manager-less fail-loud path: when the @vitejs/plugin-rsc manager is + * unavailable, wrapping must throw instead of emitting a + * serializable-but-unresolvable server reference (which would surface as a + * silent 404 on action POST in production). + * 2. The happy path's reference key: hashString(toRelativeId(id)) in build. + * 3. The documented divergence from Next.js: closure-captured variables are + * hoisted into plain `.bind(null, ...)` bound args — no encryption wrapper + * is emitted. (Next.js encrypts bound args by default. Pinned here at the + * transform level; the production-server and Playwright round-trip tests + * pin the runtime behavior.) + */ +import path from "node:path"; +import { createHash } from "node:crypto"; +import { describe, expect, it } from "vite-plus/test"; +import type { Plugin } from "vite"; +import vinext from "../packages/vinext/src/index.js"; +import { APP_FIXTURE_DIR } from "./helpers.js"; + +// oxlint-disable-next-line typescript/no-explicit-any +function unwrapHook(hook: any): ((...args: any[]) => any) | undefined { + return typeof hook === "function" ? hook : hook?.handler; +} + +/** Instantiate vinext() and return its "vinext:use-cache" plugin. */ +function getUseCachePlugin(): Plugin { + // oxlint-disable-next-line typescript/no-explicit-any + const rawPlugins = vinext({ appDir: APP_FIXTURE_DIR }) as any[]; + const plugin = rawPlugins + .flat(Infinity) + .find((p) => p && typeof p === "object" && p.name === "vinext:use-cache"); + expect(plugin).toBeDefined(); + return plugin as Plugin; +} + +const moduleId = path.join(APP_FIXTURE_DIR, "app", "unit-test-inline-cache.tsx"); + +const inlineCacheCode = [ + `export async function getData() {`, + ` "use cache";`, + ` return 1;`, + `}`, +].join("\n"); + +function fakeManager(root: string) { + return { + config: { root }, + toRelativeId: (id: string) => path.relative(root, id).split(path.sep).join("/"), + serverReferenceMetaMap: {} as Record, + }; +} + +describe("vinext:use-cache inline transform (RSC server references)", () => { + it("throws in the RSC build environment when the plugin-rsc manager is unavailable", async () => { + // configResolved is intentionally NOT called: rscPluginApi stays null, + // simulating a build where the "rsc:minimal" plugin (and its manager api) + // is missing. Wrapping anyway would emit a reference that serializes into + // the RSC payload but is never registered in the server-references + // manifest — a silent prod 404 on action POST — so the transform must + // fail loudly at build time instead. + const plugin = getUseCachePlugin(); + const transform = unwrapHook(plugin.transform)!; + + await expect( + transform.call( + { environment: { name: "rsc", mode: "build", config: { root: APP_FIXTURE_DIR } } }, + inlineCacheCode, + moduleId, + ), + ).rejects.toThrow(/plugin-rsc manager is unavailable/); + }); + + it("throws in the RSC dev environment when the plugin-rsc manager is unavailable", async () => { + // Dev has the same failure shape: the dev-mode reference validation reads + // serverReferenceMetaMap, which cannot be populated without the manager. + const plugin = getUseCachePlugin(); + const transform = unwrapHook(plugin.transform)!; + + await expect( + transform.call( + { environment: { name: "rsc", mode: "dev", config: { root: APP_FIXTURE_DIR } } }, + inlineCacheCode, + moduleId, + ), + ).rejects.toThrow(/plugin-rsc manager is unavailable/); + }); + + it("does not require the manager outside the RSC environment", async () => { + // SSR/client environments wrap call sites with the cache runtime only — + // no server-reference metadata is involved, so no manager is needed. + const plugin = getUseCachePlugin(); + const transform = unwrapHook(plugin.transform)!; + + const result = await transform.call( + { environment: { name: "ssr", mode: "build", config: { root: APP_FIXTURE_DIR } } }, + inlineCacheCode, + moduleId, + ); + expect(result).not.toBeNull(); + expect(result!.code).toContain("registerCachedFunction"); + expect(result!.code).not.toContain("registerServerReference"); + }); + + it("wraps hoisted exports with the plugin-rsc build reference key when the manager is present", async () => { + const plugin = getUseCachePlugin(); + const manager = fakeManager(APP_FIXTURE_DIR); + const configResolved = unwrapHook(plugin.configResolved)!; + configResolved.call(plugin, { plugins: [{ name: "rsc:minimal", api: { manager } }] }); + + const transform = unwrapHook(plugin.transform)!; + const result = await transform.call( + { environment: { name: "rsc", mode: "build", config: { root: APP_FIXTURE_DIR } } }, + inlineCacheCode, + moduleId, + ); + expect(result).not.toBeNull(); + + // Build key parity with plugin-rsc: hashString(toRelativeId(id)) where + // hashString = sha256 → hex → first 12 chars. + const expectedKey = createHash("sha256") + .update(manager.toRelativeId(moduleId)) + .digest("hex") + .slice(0, 12); + expect(result!.code).toContain("__vinext_registerServerReference"); + expect(result!.code).toContain(JSON.stringify(expectedKey)); + }); + + it("emits closure-captured variables as plain (unencrypted) bound args", async () => { + // Pins the documented Next.js divergence at the transform level: the + // hoist transform is invoked without encode/decode options, so captured + // variables appear verbatim in a `.bind(null, ...)` call site instead of + // being encrypted like plugin-rsc's "use server" transform does. See the + // "Known limitation" note in packages/vinext/src/index.ts and the README + // "Known limitations" section. If encryption is implemented, update this + // test alongside the round-trip tests. + const plugin = getUseCachePlugin(); + const manager = fakeManager(APP_FIXTURE_DIR); + const configResolved = unwrapHook(plugin.configResolved)!; + configResolved.call(plugin, { plugins: [{ name: "rsc:minimal", api: { manager } }] }); + + const closureCode = [ + `export async function CachedSection() {`, + ` "use cache";`, + ` const capturedSecret = "do-not-leak";`, + ` const getMessage = async () => {`, + ` "use cache";`, + ` return "message:" + capturedSecret;`, + ` };`, + ` return getMessage;`, + `}`, + ].join("\n"); + + const transform = unwrapHook(plugin.transform)!; + const result = await transform.call( + { environment: { name: "rsc", mode: "build", config: { root: APP_FIXTURE_DIR } } }, + closureCode, + moduleId, + ); + expect(result).not.toBeNull(); + // The captured variable is passed as a raw bind arg — no encrypt wrapper. + expect(result!.code).toMatch(/\.bind\(null,\s*capturedSecret\)/); + }); +}); From eeda6cfa923d7c57a882ab9bb8e78de9670e8a3d Mon Sep 17 00:00:00 2001 From: James Date: Wed, 10 Jun 2026 12:27:39 +0100 Subject: [PATCH 07/33] test(use-cache): pin unencrypted closure-captured bound args and document 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. --- README.md | 1 + tests/app-router-production-server.test.ts | 31 +++++++++++++++---- tests/e2e/app-router/use-cache.spec.ts | 16 ++++++++++ .../app/use-cache-nested-fn-props/form.tsx | 10 ++++++ .../app/use-cache-nested-fn-props/page.tsx | 14 +++++++++ 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 76182308f5..a1a6635be6 100644 --- a/README.md +++ b/README.md @@ -599,6 +599,7 @@ These are gaps we'd like to close — distinct from the [intentional exclusions] - **Image optimization doesn't happen at build time.** Remote images work via `@unpic/react` (auto-detects 28 CDN providers). Local images are routed through a `/_next/image` endpoint that can resize and transcode on Cloudflare Workers (via the Images binding) in production, but no build-time optimization or static resizing occurs. - **Google Fonts are loaded from the CDN, not self-hosted.** No `size-adjust` fallback font metrics. Local fonts work but `@font-face` CSS is injected at runtime, not extracted at build time. - **Route segment config** — `runtime` and `preferredRegion` are ignored (everything runs in the same environment). +- **Closure-captured arguments of inline `"use cache"` functions are not encrypted.** When an inline `"use cache"` function that closes over server-scope variables is passed to a client component (e.g. as a `formAction` / `useActionState` prop), the captured values are serialized into the RSC payload as plain, unencrypted bound arguments — Next.js encrypts these by default. Until this gap is closed, don't close over secrets in cached functions you pass to the client; pass an identifier and re-read the secret on the server instead. (Encrypting bound args interacts with cache-key determinism — ciphertext differs per render — and needs its own design pass.) - **Node.js production server (`vinext start`)** works for testing but is less complete than Workers deployment. Cloudflare Workers is the primary target. - **Native Node modules (sharp, resvg, satori, lightningcss, @napi-rs/canvas)** crash Vite's RSC dev environment. Dynamic OG image/icon routes using these work in production builds but not in dev mode. These are auto-stubbed during `vinext deploy`. - **`next.config.ts` `baseUrl` bare imports require Vite 8.** A `next.config.ts` that imports a bare specifier resolved through `tsconfig.json`'s `compilerOptions.baseUrl` (e.g. `import { bar } from "bar"` resolving to a local `bar.ts`) relies on Vite 8's native `resolve.tsconfigPaths` (Rolldown/oxc-resolver). On Vite 7 there is no native equivalent, so these imports are not resolved. `compilerOptions.paths` aliases (e.g. `@/foo`) work on both Vite 7 and 8. Note that if a bare import matches both a `baseUrl`-local file and an installed package of the same name, the installed package wins (vinext keeps packages externalized so CJS config plugins like `@next/mdx` keep working). diff --git a/tests/app-router-production-server.test.ts b/tests/app-router-production-server.test.ts index 9220e49154..1b54a37e6d 100644 --- a/tests/app-router-production-server.test.ts +++ b/tests/app-router-production-server.test.ts @@ -916,19 +916,30 @@ describe("App Router Production server (startProdServer)", () => { // The flight payload embeds each cached function prop as a server // reference whose id is "<12-hex normalised key>#". // Serialization order follows the props order: getDate first, getRandom - // second. + // second, getMessage third. const refIds = [...new Set(html.match(/[0-9a-f]{12}#\$\$hoist_\d+_[A-Za-z0-9_$]+/g) ?? [])]; - expect(refIds.length).toBe(2); - const [getDateRefId, getRandomRefId] = refIds; - - const invokeAction = async (actionId: string): Promise => { + expect(refIds.length).toBe(3); + const [getDateRefId, getRandomRefId, getMessageRefId] = refIds; + + // Pin the documented divergence from Next.js: closure-captured variables + // are hoisted into `.bind(null, ...)` bound args and serialized into the + // RSC payload UNENCRYPTED (Next.js encrypts bound args by default). The + // fixture's getMessage closes over this string inside the cached + // component, and it must appear verbatim (plaintext) in the page payload. + // If bound-arg encryption is ever implemented, this assertion should be + // inverted and the "Known limitation" notes in packages/vinext/src/index.ts + // and the README removed. + const capturedScopeValue = "closure-captured-bound-arg-vinext"; + expect(html).toContain(capturedScopeValue); + + const invokeAction = async (actionId: string, args: unknown[] = []): Promise => { const actionRes = await fetch(`${baseUrl}/use-cache-nested-fn-props.rsc`, { method: "POST", headers: { "Content-Type": "text/plain", "x-rsc-action": actionId, }, - body: JSON.stringify([]), + body: JSON.stringify(args), }); expect(actionRes.status).toBe(200); expect(actionRes.headers.get("x-nextjs-action-not-found")).toBeNull(); @@ -949,6 +960,14 @@ describe("App Router Production server (startProdServer)", () => { const randomText = await invokeAction(getRandomRefId); expect(randomText).toMatch(/\d+\.\d+/); + + // Closure round-trip: the client invokes a bound server reference by + // sending the bound args ahead of the call args in the POST body. The + // hoisted function's leading parameter is the closure-captured value, so + // passing it as the first arg replicates what the flight client sends, + // and the result must observe the captured value. + const messageText = await invokeAction(getMessageRefId, [capturedScopeValue]); + expect(messageText).toContain(`message:${capturedScopeValue}`); }); it("middleware request header overrides still apply after middleware calls headers() first", async () => { diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index 82ed5c17b3..89b8bc7b0c 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -126,6 +126,22 @@ test.describe('"use cache" nested cache functions as props', () => { await page.locator("#submit-button-random").click(); await expect(page.locator("#random")).toHaveText(randomRegExp, { timeout: 2000 }); }).toPass({ timeout: 15_000 }); + + // Closure-captured bound args: getMessage closes over a value from the + // cached component's scope, which the hoist transform turns into a + // `.bind(null, ...)` bound arg on the server reference. Invoking it from + // the client exercises the full flight round-trip for bound args: + // $$bound serialized into the RSC payload → encodeReply on click → + // decode + prepend on the server. Note the bound arg travels unencrypted — + // a documented divergence from Next.js, pinned by the production-server + // test's plaintext-payload assertion. + await expect(async () => { + await page.locator("#submit-button-message").click(); + await expect(page.locator("#message")).toHaveText( + "message:closure-captured-bound-arg-vinext", + { timeout: 2000 }, + ); + }).toPass({ timeout: 15_000 }); }); }); diff --git a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/form.tsx b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/form.tsx index 90577adff4..f6a8662e43 100644 --- a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/form.tsx +++ b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/form.tsx @@ -8,22 +8,32 @@ import { useActionState } from "react"; export function Form({ getDate, getRandom, + getMessage, }: { getDate: () => Promise; getRandom: () => Promise; + // Closure-capturing cached function — exercises bound-arg serialization + // (the captured value travels as an unencrypted `.bind(null, ...)` arg). + getMessage: () => Promise; }) { const [date, formAction, isDatePending] = useActionState(getDate, null); const [random, buttonAction, isRandomPending] = useActionState(getRandom, null); + const [message, messageAction, isMessagePending] = useActionState(getMessage, null); + return ( {" "} {" "} +

{isDatePending ? "loading..." : date}

{isRandomPending ? "loading..." : random}

+

{isMessagePending ? "loading..." : message}

); } diff --git a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx index aa99758435..e241913922 100644 --- a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx +++ b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx @@ -24,6 +24,16 @@ export default function UseCacheNestedFnPropsPage() { async function CachedForm() { "use cache"; + // Closure-captured by getMessage below. The hoist transform lifts the + // capture into a `.bind(null, capturedScopeValue)` bound argument on the + // server reference, which is serialized into the RSC payload UNENCRYPTED — + // a deliberate, documented divergence from Next.js (which encrypts bound + // args by default). The production-server test asserts this value appears + // in plaintext in the flight payload to pin the behavior; see the "Known + // limitation" note on the vinext:use-cache transform in + // packages/vinext/src/index.ts and the README "Known limitations" section. + const capturedScopeValue = "closure-captured-bound-arg-vinext"; + return (
{ @@ -34,6 +44,10 @@ async function CachedForm() { "use cache"; return Math.random(); }} + getMessage={async () => { + "use cache"; + return `message:${capturedScopeValue}`; + }} /> ); } From a9cd049f0bc46c80589747a8e99ad515a2502aae Mon Sep 17 00:00:00 2001 From: James Date: Wed, 10 Jun 2026 13:10:02 +0100 Subject: [PATCH 08/33] refactor(use-cache): route registerServerReference through a vinext shim to decouple from plugin-rsc module-id normalisation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/vinext/src/index.ts | 28 +++++++++---------- .../src/shims/cache-server-reference.ts | 25 +++++++++++++++++ tests/use-cache-transform.test.ts | 12 ++++++++ 3 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 packages/vinext/src/shims/cache-server-reference.ts diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index e04105b7d0..1140dc7544 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -919,19 +919,6 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // whose code does not contain "use server", so writing the entry from the // vinext:use-cache transform directly would be wiped out a moment later. const useCacheServerRefMeta = new Map(); - // Resolved file URL of @vitejs/plugin-rsc/react/rsc for generated - // registerServerReference imports. Invariant per process — resolved once. - // This resolves to the same file as the bare "@vitejs/plugin-rsc/react/rsc" - // import inside the cache-runtime shim (Vite normalises file:// URLs to the - // same module id), so the RSC environment does not load two module copies. - let cachedRscReactRscUrl: string | undefined; - const getRscReactRscUrl = (): string => { - if (cachedRscReactRscUrl === undefined) { - const p = resolveOptionalDependency(earlyBaseDir, "@vitejs/plugin-rsc/react/rsc"); - cachedRscReactRscUrl = p ? pathToFileURL(p).href : "@vitejs/plugin-rsc/react/rsc"; - } - return cachedRscReactRscUrl; - }; if (earlyAppDirExists && autoRsc) { if (!resolvedRscPath) { throw new Error( @@ -4435,9 +4422,22 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // copy the binding value at evaluation time), render-time // call sites, and the server-references manifest import on // action POST — observes the wrapped function. + // + // registerServerReference is imported via the vinext + // cache-server-reference shim rather than from + // @vitejs/plugin-rsc/react/rsc directly: the shim re-exports + // it through the same bare specifier the cache runtime uses, + // so the RSC environment shares one react/rsc module + // instance by construction instead of relying on Vite + // normalising a file:// URL of the package entry to the same + // module id as the bare import (see the shim's header + // comment). + const serverRefShimUrl = pathToFileURL( + resolveShimModulePath(shimsDir, "cache-server-reference"), + ).href; const lines: string[] = [ `import { registerCachedFunction as __vinext_registerCachedFunction } from ${JSON.stringify(runtimeModuleUrl2)};`, - `import { registerServerReference as __vinext_registerServerReference } from ${JSON.stringify(getRscReactRscUrl())};`, + `import { registerServerReference as __vinext_registerServerReference } from ${JSON.stringify(serverRefShimUrl)};`, ]; for (const { name, variant } of hoisted) { lines.push( diff --git a/packages/vinext/src/shims/cache-server-reference.ts b/packages/vinext/src/shims/cache-server-reference.ts new file mode 100644 index 0000000000..213a87d7c3 --- /dev/null +++ b/packages/vinext/src/shims/cache-server-reference.ts @@ -0,0 +1,25 @@ +/** + * Single `registerServerReference` import site for the inline "use cache" + * transform ("vinext:use-cache", RSC environment only), which prepends an + * import of this shim — by absolute file:// URL, exactly like the + * cache-runtime import next to it — into modules containing inline + * "use cache" functions. + * + * Why not import "@vitejs/plugin-rsc/react/rsc" from the transformed module + * directly? The prepended import must use an absolute file:// specifier: the + * transformed module can live outside the project root (e.g. linked + * workspace sources), where the bare specifier may not resolve. But the + * cache runtime (./cache-runtime.ts) loads the same package via the bare + * specifier, and pairing a file:// URL of the package entry file with a bare + * import of the package would rely on Vite normalising both to a single + * module id — a latent coupling to plugin-rsc's module-id normalisation. + * + * Routing the transform's import through this vinext-owned shim removes that + * coupling by construction: the only react/rsc specifier in play is the bare + * one below, resolved from the same importer location (vinext's shims + * directory) as cache-runtime's, so both observe the same module instance no + * matter how plugin-rsc normalises ids. The shim itself is only ever + * imported via the transform's file:// URL, so it has a single module id + * too. + */ +export { registerServerReference } from "@vitejs/plugin-rsc/react/rsc"; diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index d3b4ec044a..2892a610e7 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -130,6 +130,18 @@ describe("vinext:use-cache inline transform (RSC server references)", () => { .slice(0, 12); expect(result!.code).toContain("__vinext_registerServerReference"); expect(result!.code).toContain(JSON.stringify(expectedKey)); + + // registerServerReference must be imported via the vinext + // cache-server-reference shim (which re-exports it through the same bare + // "@vitejs/plugin-rsc/react/rsc" specifier the cache runtime uses), NOT + // via a file:// URL of the plugin-rsc package entry — the latter would + // couple correctness to Vite normalising the file:// URL and the bare + // import to a single module id. + const importSpecifiers = [...result!.code.matchAll(/from "([^"]+)"/g)].map((m) => m[1]); + expect(importSpecifiers).toContainEqual( + expect.stringContaining("/shims/cache-server-reference"), + ); + expect(importSpecifiers).not.toContainEqual(expect.stringContaining("@vitejs/plugin-rsc")); }); it("emits closure-captured variables as plain (unencrypted) bound args", async () => { From 67d7fb276809e3e5769d32fb6e8847fff9b20fe0 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 10 Jun 2026 13:10:02 +0100 Subject: [PATCH 09/33] test(use-cache): pin cached-invoke semantics for the closure-bound getMessage 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. --- tests/app-router-production-server.test.ts | 29 +++++++++++++++++-- tests/e2e/app-router/use-cache.spec.ts | 6 ++-- .../app/use-cache-nested-fn-props/page.tsx | 7 ++++- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/tests/app-router-production-server.test.ts b/tests/app-router-production-server.test.ts index 1b54a37e6d..91bbbe61e9 100644 --- a/tests/app-router-production-server.test.ts +++ b/tests/app-router-production-server.test.ts @@ -966,8 +966,33 @@ describe("App Router Production server (startProdServer)", () => { // hoisted function's leading parameter is the closure-captured value, so // passing it as the first arg replicates what the flight client sends, // and the result must observe the captured value. - const messageText = await invokeAction(getMessageRefId, [capturedScopeValue]); - expect(messageText).toContain(`message:${capturedScopeValue}`); + const messageRegExpFor = (boundArg: string): RegExp => + new RegExp(`message:${boundArg}:[0-9.e+-]+`); + const message1 = (await invokeAction(getMessageRefId, [capturedScopeValue])).match( + messageRegExpFor(capturedScopeValue), + )?.[0]; + expect(message1).toBeDefined(); + + // Cached-invoke semantics for the closure-BOUND path, mirroring the + // getDate assertion above so caching is pinned across both paths + // (unbound getDate AND bound getMessage): the fixture appends a + // Math.random() suffix, so a second invocation with the same bound arg + // can only return the identical value if the bound arg produced the same + // cache key and the entry was hit (a recompute would change the suffix). + const message2 = (await invokeAction(getMessageRefId, [capturedScopeValue])).match( + messageRegExpFor(capturedScopeValue), + )?.[0]; + expect(message2).toBe(message1); + + // ...and the bound arg PARTICIPATES in the cache key: invoking with a + // different bound arg must miss (fresh value observing the new arg), not + // serve the entry cached above. + const otherBoundArg = "other-bound-arg-vinext"; + const otherMessage = (await invokeAction(getMessageRefId, [otherBoundArg])).match( + messageRegExpFor(otherBoundArg), + )?.[0]; + expect(otherMessage).toBeDefined(); + expect(otherMessage).not.toBe(message1); }); it("middleware request header overrides still apply after middleware calls headers() first", async () => { diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index 89b8bc7b0c..d171e85ca1 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -134,11 +134,13 @@ test.describe('"use cache" nested cache functions as props', () => { // $$bound serialized into the RSC payload → encodeReply on click → // decode + prepend on the server. Note the bound arg travels unencrypted — // a documented divergence from Next.js, pinned by the production-server - // test's plaintext-payload assertion. + // test's plaintext-payload assertion. The trailing numeric suffix is the + // fixture's Math.random() marker, which the production-server test uses + // to pin cached-invoke semantics for the bound path. await expect(async () => { await page.locator("#submit-button-message").click(); await expect(page.locator("#message")).toHaveText( - "message:closure-captured-bound-arg-vinext", + /^message:closure-captured-bound-arg-vinext:[0-9.e+-]+$/, { timeout: 2000 }, ); }).toPass({ timeout: 15_000 }); diff --git a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx index e241913922..79bc3268ef 100644 --- a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx +++ b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx @@ -46,7 +46,12 @@ async function CachedForm() { }} getMessage={async () => { "use cache"; - return `message:${capturedScopeValue}`; + // The Math.random() suffix makes cache hits on the closure-BOUND path + // observable: only a cache hit can repeat the suffix, so the + // production-server test can assert that two invocations with the + // same bound arg return the identical cached value (and that a + // different bound arg misses instead of reusing the entry). + return `message:${capturedScopeValue}:${Math.random()}`; }} /> ); From 9937ec72e175b1ae49ed455f0c2294fac738455e Mon Sep 17 00:00:00 2001 From: James Date: Thu, 11 Jun 2026 11:46:57 +0100 Subject: [PATCH 10/33] fix(use-cache): encrypt closure-bound arguments --- README.md | 1 - packages/vinext/src/index.ts | 38 +++++-------- .../src/shims/cache-server-reference.ts | 54 ++++++++++--------- tests/app-router-production-server.test.ts | 37 ++++--------- tests/e2e/app-router/use-cache.spec.ts | 4 +- .../app/use-cache-nested-fn-props/page.tsx | 8 +-- tests/use-cache-transform.test.ts | 35 +++++------- 7 files changed, 70 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index a1a6635be6..76182308f5 100644 --- a/README.md +++ b/README.md @@ -599,7 +599,6 @@ These are gaps we'd like to close — distinct from the [intentional exclusions] - **Image optimization doesn't happen at build time.** Remote images work via `@unpic/react` (auto-detects 28 CDN providers). Local images are routed through a `/_next/image` endpoint that can resize and transcode on Cloudflare Workers (via the Images binding) in production, but no build-time optimization or static resizing occurs. - **Google Fonts are loaded from the CDN, not self-hosted.** No `size-adjust` fallback font metrics. Local fonts work but `@font-face` CSS is injected at runtime, not extracted at build time. - **Route segment config** — `runtime` and `preferredRegion` are ignored (everything runs in the same environment). -- **Closure-captured arguments of inline `"use cache"` functions are not encrypted.** When an inline `"use cache"` function that closes over server-scope variables is passed to a client component (e.g. as a `formAction` / `useActionState` prop), the captured values are serialized into the RSC payload as plain, unencrypted bound arguments — Next.js encrypts these by default. Until this gap is closed, don't close over secrets in cached functions you pass to the client; pass an identifier and re-read the secret on the server instead. (Encrypting bound args interacts with cache-key determinism — ciphertext differs per render — and needs its own design pass.) - **Node.js production server (`vinext start`)** works for testing but is less complete than Workers deployment. Cloudflare Workers is the primary target. - **Native Node modules (sharp, resvg, satori, lightningcss, @napi-rs/canvas)** crash Vite's RSC dev environment. Dynamic OG image/icon routes using these work in production builds but not in dev mode. These are auto-stubbed during `vinext deploy`. - **`next.config.ts` `baseUrl` bare imports require Vite 8.** A `next.config.ts` that imports a bare specifier resolved through `tsconfig.json`'s `compilerOptions.baseUrl` (e.g. `import { bar } from "bar"` resolving to a local `bar.ts`) relies on Vite 8's native `resolve.tsconfigPaths` (Rolldown/oxc-resolver). On Vite 7 there is no native equivalent, so these imports are not resolved. `compilerOptions.paths` aliases (e.g. `@/foo`) work on both Vite 7 and 8. Note that if a bare import matches both a `baseUrl`-local file and an installed package of the same name, the installed package wins (vinext keeps packages externalized so CJS config plugins like `@next/mdx` keep working). diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 1140dc7544..e9c5ede2d9 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -4290,7 +4290,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Strategy (mirrors Next.js' use-cache SWC transform, where the // exported $$RSC_SERVER_CACHE_n binding IS the cache wrapper): // after hoisting, reassign each hoisted export at module level to - // registerServerReference(registerCachedFunction(fn)). Exported + // registerCachedServerReference(fn). Exported // function declarations are live bindings, so both the original // call sites and the server-reference manifest (which imports the // module export by name on action POST) observe the wrapped @@ -4309,13 +4309,6 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // build-time virtual:vite-rsc/server-references manifest includes // the module and dev-mode reference validation accepts the key. // - // Known limitation: closure-captured variables (hoisted into - // `.bind(null, ...)` args) are serialised to the client - // unencrypted when such a function is passed as a prop, unlike - // plugin-rsc's "use server" transform which encrypts bound args by - // default. Encrypting them here interacts with cache-key - // determinism (AES-GCM ciphertext differs per render) and needs - // its own design pass. const isRscEnv = this.environment?.name === "rsc"; // Compute the normalised reference key that matches what @@ -4391,7 +4384,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { try { // Hoisted function metadata collected during the transform so the // RSC branch can emit the module-level wrapping afterwards. - const hoisted: { name: string; variant: string }[] = []; + const hoisted: { name: string; variant: string; hasBoundArgs: boolean }[] = []; const result = transformHoistInlineDirective(code, ast, { directive: /^use cache(:\s*\w+)?$/, runtime: (value: string, name: string, meta: { directiveMatch: string[] }) => { @@ -4399,15 +4392,22 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (isRscEnv) { // The hoisted export is wrapped once at module level // (below); the call site just references it. `.bind()` on - // the wrapped export is handled by registerServerReference's + // the wrapped export is handled by the server reference's // patched bind, which tracks $$bound for serialisation. - hoisted.push({ name, variant }); + hoisted.push({ name, variant, hasBoundArgs: false }); return value; } // Non-RSC env: no server-reference metadata needed — wrap the // call site with the cache runtime only. return `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`; }, + encode: isRscEnv + ? (value: string) => { + const current = hoisted.at(-1); + if (current) current.hasBoundArgs = true; + return `__vinext_encryptActionBoundArgs(${value})`; + } + : undefined, rejectNonAsyncFunction: false, }); @@ -4423,25 +4423,15 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // call sites, and the server-references manifest import on // action POST — observes the wrapped function. // - // registerServerReference is imported via the vinext - // cache-server-reference shim rather than from - // @vitejs/plugin-rsc/react/rsc directly: the shim re-exports - // it through the same bare specifier the cache runtime uses, - // so the RSC environment shares one react/rsc module - // instance by construction instead of relying on Vite - // normalising a file:// URL of the package entry to the same - // module id as the bare import (see the shim's header - // comment). const serverRefShimUrl = pathToFileURL( resolveShimModulePath(shimsDir, "cache-server-reference"), ).href; const lines: string[] = [ - `import { registerCachedFunction as __vinext_registerCachedFunction } from ${JSON.stringify(runtimeModuleUrl2)};`, - `import { registerServerReference as __vinext_registerServerReference } from ${JSON.stringify(serverRefShimUrl)};`, + `import { encryptActionBoundArgs as __vinext_encryptActionBoundArgs, registerCachedServerReference as __vinext_registerCachedServerReference } from ${JSON.stringify(serverRefShimUrl)};`, ]; - for (const { name, variant } of hoisted) { + for (const { name, variant, hasBoundArgs } of hoisted) { lines.push( - `${name} = __vinext_registerServerReference(__vinext_registerCachedFunction(${name}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)}), ${JSON.stringify(normalizedRefKey)}, ${JSON.stringify(name)});`, + `${name} = __vinext_registerCachedServerReference(${name}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)}, ${JSON.stringify(normalizedRefKey)}, ${JSON.stringify(name)}, ${hasBoundArgs});`, ); } result.output.prepend(lines.join("\n") + "\n"); diff --git a/packages/vinext/src/shims/cache-server-reference.ts b/packages/vinext/src/shims/cache-server-reference.ts index 213a87d7c3..4247cf91b1 100644 --- a/packages/vinext/src/shims/cache-server-reference.ts +++ b/packages/vinext/src/shims/cache-server-reference.ts @@ -1,25 +1,29 @@ -/** - * Single `registerServerReference` import site for the inline "use cache" - * transform ("vinext:use-cache", RSC environment only), which prepends an - * import of this shim — by absolute file:// URL, exactly like the - * cache-runtime import next to it — into modules containing inline - * "use cache" functions. - * - * Why not import "@vitejs/plugin-rsc/react/rsc" from the transformed module - * directly? The prepended import must use an absolute file:// specifier: the - * transformed module can live outside the project root (e.g. linked - * workspace sources), where the bare specifier may not resolve. But the - * cache runtime (./cache-runtime.ts) loads the same package via the bare - * specifier, and pairing a file:// URL of the package entry file with a bare - * import of the package would rely on Vite normalising both to a single - * module id — a latent coupling to plugin-rsc's module-id normalisation. - * - * Routing the transform's import through this vinext-owned shim removes that - * coupling by construction: the only react/rsc specifier in play is the bare - * one below, resolved from the same importer location (vinext's shims - * directory) as cache-runtime's, so both observe the same module instance no - * matter how plugin-rsc normalises ids. The shim itself is only ever - * imported via the transform's file:// URL, so it has a single module id - * too. - */ -export { registerServerReference } from "@vitejs/plugin-rsc/react/rsc"; +import { registerServerReference } from "@vitejs/plugin-rsc/react/rsc"; +import { + decryptActionBoundArgs, + encryptActionBoundArgs, +} from "@vitejs/plugin-rsc/utils/encryption-runtime"; +import { registerCachedFunction } from "./cache-runtime.js"; + +export { encryptActionBoundArgs }; + +export function registerCachedServerReference( + fn: (...args: unknown[]) => Promise, + cacheId: string, + cacheVariant: string, + referenceId: string, + referenceName: string, + hasEncryptedBoundArgs: boolean, +): (...args: unknown[]) => Promise { + const cached = registerCachedFunction(fn, cacheId, cacheVariant); + const callable: (...args: unknown[]) => Promise = hasEncryptedBoundArgs + ? async (encryptedBoundArgs: unknown, ...args: unknown[]) => { + const boundArgs = (await decryptActionBoundArgs( + encryptedBoundArgs as Promise, + )) as unknown[]; + return cached(...boundArgs, ...args); + } + : cached; + + return registerServerReference(callable, referenceId, referenceName) as typeof callable; +} diff --git a/tests/app-router-production-server.test.ts b/tests/app-router-production-server.test.ts index 91bbbe61e9..231f8af9d6 100644 --- a/tests/app-router-production-server.test.ts +++ b/tests/app-router-production-server.test.ts @@ -921,16 +921,13 @@ describe("App Router Production server (startProdServer)", () => { expect(refIds.length).toBe(3); const [getDateRefId, getRandomRefId, getMessageRefId] = refIds; - // Pin the documented divergence from Next.js: closure-captured variables - // are hoisted into `.bind(null, ...)` bound args and serialized into the - // RSC payload UNENCRYPTED (Next.js encrypts bound args by default). The - // fixture's getMessage closes over this string inside the cached - // component, and it must appear verbatim (plaintext) in the page payload. - // If bound-arg encryption is ever implemented, this assertion should be - // inverted and the "Known limitation" notes in packages/vinext/src/index.ts - // and the README removed. + // The fixture's getMessage closes over this string. Match Next.js and + // plugin-rsc's "use server" transform by serializing an encrypted binding, + // never the plaintext capture, into the Flight payload. const capturedScopeValue = "closure-captured-bound-arg-vinext"; - expect(html).toContain(capturedScopeValue); + expect(html).not.toContain(capturedScopeValue); + const encryptedBoundArg = html.match(/rsc\.push\("[0-9a-f]+:\\"([A-Za-z0-9+/=]{64,})\\"/)?.[1]; + expect(encryptedBoundArg).toBeDefined(); const invokeAction = async (actionId: string, args: unknown[] = []): Promise => { const actionRes = await fetch(`${baseUrl}/use-cache-nested-fn-props.rsc`, { @@ -961,14 +958,12 @@ describe("App Router Production server (startProdServer)", () => { const randomText = await invokeAction(getRandomRefId); expect(randomText).toMatch(/\d+\.\d+/); - // Closure round-trip: the client invokes a bound server reference by - // sending the bound args ahead of the call args in the POST body. The - // hoisted function's leading parameter is the closure-captured value, so - // passing it as the first arg replicates what the flight client sends, - // and the result must observe the captured value. + // Closure round-trip: the client sends the encrypted binding ahead of the + // call args. The server-reference wrapper decrypts it before entering the + // cached function, so plaintext values still determine the cache key. const messageRegExpFor = (boundArg: string): RegExp => new RegExp(`message:${boundArg}:[0-9.e+-]+`); - const message1 = (await invokeAction(getMessageRefId, [capturedScopeValue])).match( + const message1 = (await invokeAction(getMessageRefId, [encryptedBoundArg])).match( messageRegExpFor(capturedScopeValue), )?.[0]; expect(message1).toBeDefined(); @@ -979,20 +974,10 @@ describe("App Router Production server (startProdServer)", () => { // Math.random() suffix, so a second invocation with the same bound arg // can only return the identical value if the bound arg produced the same // cache key and the entry was hit (a recompute would change the suffix). - const message2 = (await invokeAction(getMessageRefId, [capturedScopeValue])).match( + const message2 = (await invokeAction(getMessageRefId, [encryptedBoundArg])).match( messageRegExpFor(capturedScopeValue), )?.[0]; expect(message2).toBe(message1); - - // ...and the bound arg PARTICIPATES in the cache key: invoking with a - // different bound arg must miss (fresh value observing the new arg), not - // serve the entry cached above. - const otherBoundArg = "other-bound-arg-vinext"; - const otherMessage = (await invokeAction(getMessageRefId, [otherBoundArg])).match( - messageRegExpFor(otherBoundArg), - )?.[0]; - expect(otherMessage).toBeDefined(); - expect(otherMessage).not.toBe(message1); }); it("middleware request header overrides still apply after middleware calls headers() first", async () => { diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index d171e85ca1..81f0d2f513 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -132,9 +132,7 @@ test.describe('"use cache" nested cache functions as props', () => { // `.bind(null, ...)` bound arg on the server reference. Invoking it from // the client exercises the full flight round-trip for bound args: // $$bound serialized into the RSC payload → encodeReply on click → - // decode + prepend on the server. Note the bound arg travels unencrypted — - // a documented divergence from Next.js, pinned by the production-server - // test's plaintext-payload assertion. The trailing numeric suffix is the + // decrypt + prepend on the server. The trailing numeric suffix is the // fixture's Math.random() marker, which the production-server test uses // to pin cached-invoke semantics for the bound path. await expect(async () => { diff --git a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx index 79bc3268ef..894d59acc1 100644 --- a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx +++ b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx @@ -26,12 +26,8 @@ async function CachedForm() { // Closure-captured by getMessage below. The hoist transform lifts the // capture into a `.bind(null, capturedScopeValue)` bound argument on the - // server reference, which is serialized into the RSC payload UNENCRYPTED — - // a deliberate, documented divergence from Next.js (which encrypts bound - // args by default). The production-server test asserts this value appears - // in plaintext in the flight payload to pin the behavior; see the "Known - // limitation" note on the vinext:use-cache transform in - // packages/vinext/src/index.ts and the README "Known limitations" section. + // server reference. The binding is encrypted before RSC serialization and + // decrypted before the cached wrapper builds its argument-based cache key. const capturedScopeValue = "closure-captured-bound-arg-vinext"; return ( diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index 2892a610e7..575847b7a1 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -11,11 +11,8 @@ * serializable-but-unresolvable server reference (which would surface as a * silent 404 on action POST in production). * 2. The happy path's reference key: hashString(toRelativeId(id)) in build. - * 3. The documented divergence from Next.js: closure-captured variables are - * hoisted into plain `.bind(null, ...)` bound args — no encryption wrapper - * is emitted. (Next.js encrypts bound args by default. Pinned here at the - * transform level; the production-server and Playwright round-trip tests - * pin the runtime behavior.) + * 3. Closure-captured variables use plugin-rsc's action encryption runtime, + * matching the encryption path used by its own "use server" transform. */ import path from "node:path"; import { createHash } from "node:crypto"; @@ -128,15 +125,12 @@ describe("vinext:use-cache inline transform (RSC server references)", () => { .update(manager.toRelativeId(moduleId)) .digest("hex") .slice(0, 12); - expect(result!.code).toContain("__vinext_registerServerReference"); + expect(result!.code).toContain("__vinext_registerCachedServerReference"); expect(result!.code).toContain(JSON.stringify(expectedKey)); - // registerServerReference must be imported via the vinext - // cache-server-reference shim (which re-exports it through the same bare - // "@vitejs/plugin-rsc/react/rsc" specifier the cache runtime uses), NOT - // via a file:// URL of the plugin-rsc package entry — the latter would - // couple correctness to Vite normalising the file:// URL and the bare - // import to a single module id. + // Server-reference registration and action encryption are imported via a + // vinext-owned integration module so transformed application modules do + // not need to resolve plugin-rsc's runtime package from their location. const importSpecifiers = [...result!.code.matchAll(/from "([^"]+)"/g)].map((m) => m[1]); expect(importSpecifiers).toContainEqual( expect.stringContaining("/shims/cache-server-reference"), @@ -144,14 +138,7 @@ describe("vinext:use-cache inline transform (RSC server references)", () => { expect(importSpecifiers).not.toContainEqual(expect.stringContaining("@vitejs/plugin-rsc")); }); - it("emits closure-captured variables as plain (unencrypted) bound args", async () => { - // Pins the documented Next.js divergence at the transform level: the - // hoist transform is invoked without encode/decode options, so captured - // variables appear verbatim in a `.bind(null, ...)` call site instead of - // being encrypted like plugin-rsc's "use server" transform does. See the - // "Known limitation" note in packages/vinext/src/index.ts and the README - // "Known limitations" section. If encryption is implemented, update this - // test alongside the round-trip tests. + it("encrypts closure-captured variables before binding the server reference", async () => { const plugin = getUseCachePlugin(); const manager = fakeManager(APP_FIXTURE_DIR); const configResolved = unwrapHook(plugin.configResolved)!; @@ -176,7 +163,11 @@ describe("vinext:use-cache inline transform (RSC server references)", () => { moduleId, ); expect(result).not.toBeNull(); - // The captured variable is passed as a raw bind arg — no encrypt wrapper. - expect(result!.code).toMatch(/\.bind\(null,\s*capturedSecret\)/); + expect(result!.code).toMatch( + /\.bind\(null,\s*__vinext_encryptActionBoundArgs\(\[capturedSecret\]\)\)/, + ); + expect(result!.code).not.toMatch(/\.bind\(null,\s*capturedSecret\)/); + expect(result!.code).toContain("__vinext_registerCachedServerReference"); + expect(result!.code).toContain(", true);"); }); }); From b8a116cf61642fc9b02903ad619f701c67139771 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 12 Jun 2026 02:49:56 +0100 Subject: [PATCH 11/33] refactor(use-cache): use plugin-rsc directive transforms --- packages/vinext/src/index.ts | 531 +++--------------- packages/vinext/src/shims/cache-runtime.ts | 21 +- .../src/shims/cache-server-reference.ts | 29 - pnpm-lock.yaml | 120 ++-- pnpm-workspace.yaml | 4 +- 5 files changed, 168 insertions(+), 537 deletions(-) delete mode 100644 packages/vinext/src/shims/cache-server-reference.ts diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index e9c5ede2d9..f96858bfa1 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -18,7 +18,6 @@ import { import { createSSRHandler } from "./server/dev-server.js"; import { handleApiRoute } from "./server/api-handler.js"; import { isImageOptimizationPath } from "./server/image-optimization.js"; - import { installSocketErrorBackstop } from "./server/socket-error-backstop.js"; import { shouldInvalidateAppRouteFile } from "./server/dev-route-files.js"; import { createDirectRunner } from "./server/dev-module-runner.js"; @@ -175,10 +174,27 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { createRequire } from "node:module"; import fs from "node:fs"; -import { createHash, randomBytes, randomUUID } from "node:crypto"; +import { randomBytes, randomUUID } from "node:crypto"; import commonjs from "vite-plugin-commonjs"; import { normalizePathSeparators } from "./utils/path.js"; +type ServerFunctionDirectiveContext = { + value: string; + name: string; + id: string; + directiveMatch: RegExpMatchArray; + location: "inline" | "module"; + hasBoundArgs: boolean; + parameters?: { count: number; hasRest: boolean }; + runtime?: string; +}; + +function parseUseCacheVariant(directive: string): string { + return directive === "use cache" + ? "" + : directive.replace("use cache:", "").replace("use cache: ", "").trim(); +} + // Install the process-level peer-disconnect backstop at module load. // Vite plugin lifecycle hooks (config / configureServer) proved // timing-fragile in vite-plus — install was silently skipped, @@ -888,37 +904,20 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // different class identities. Resolving from the project root ensures a // single shared vite instance. // - // Pre-resolve both the main plugin and the /transforms subpath eagerly - // so all import() calls in this module use consistent resolution. + // Pre-resolve the main plugin eagerly so all import() calls in this module + // use consistent resolution. let resolvedReactPath: string | null = null; let resolvedRscPath: string | null = null; - let resolvedRscTransformsPath: string | null = null; // Prefer the user's project graph so vinext shares the app's Vite/plugin // instances. In source/workspace development, test fixtures may not declare // peer deps explicitly, so fall back to vinext's own install location. resolvedReactPath = resolveOptionalDependency(earlyBaseDir, "@vitejs/plugin-react"); resolvedRscPath = resolveOptionalDependency(earlyBaseDir, "@vitejs/plugin-rsc"); - resolvedRscTransformsPath = resolveOptionalDependency( - earlyBaseDir, - "@vitejs/plugin-rsc/transforms", - ); // If app/ exists and auto-RSC is enabled, create a lazy Promise that // resolves to the configured RSC plugin array. Vite's asyncFlatten // will resolve this before processing the plugin list. let rscPluginPromise: Promise | null = null; - // Captured in configResolved so the use-cache transform can register hoisted - // functions into the plugin-rsc server-reference manifest. - // oxlint-disable-next-line typescript/no-explicit-any - let rscPluginApi: { manager: any } | null = null; - // Hoisted "use cache" functions (per module id) pending registration in the - // plugin-rsc server-reference manifest. Written by the "vinext:use-cache" - // transform and consumed by "vinext:use-cache-server-references", which runs - // AFTER plugin-rsc's "rsc:use-server" transform. That ordering is load-bearing: - // rsc:use-server deletes manager.serverReferenceMetaMap[id] for any module - // whose code does not contain "use server", so writing the entry from the - // vinext:use-cache transform directly would be wiped out a moment later. - const useCacheServerRefMeta = new Map(); if (earlyAppDirExists && autoRsc) { if (!resolvedRscPath) { throw new Error( @@ -938,6 +937,74 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { ssr: VIRTUAL_APP_SSR_ENTRY, client: VIRTUAL_APP_BROWSER_ENTRY, }, + serverFunctionDirectives: [ + { + directive: /^use cache.*$/, + test: (code: string) => code.includes("use cache"), + filter: (id: string) => + /\.(tsx?|jsx?|mjs)$/.test(id) && !id.includes("/node_modules/"), + rejectNonAsyncFunction: true, + rejectNonAsyncModule: false, + runtime: pathToFileURL(resolveShimModulePath(shimsDir, "cache-runtime")).href, + validate: ({ directive }: { directive: string }) => { + if (!/^use cache(?:: ([^\s].*))?$/.test(directive)) { + const cacheKind = directive.includes(":") + ? directive.slice(directive.indexOf(":") + 1).trim() + : directive.slice("use cache".length).trim(); + const expected = cacheKind ? `use cache: ${cacheKind}` : "use cache"; + throw new Error( + `Invalid cache directive ${JSON.stringify(directive)}. Did you mean ${JSON.stringify(expected)}?`, + ); + } + }, + clientError: ({ id, environment }: { id: string; environment: string }) => + `It is not allowed to define inline "use cache" annotated functions in Client Components. Export them from a separate file with a module-level "use cache" or "use server" directive, or pass them down through props from a Server Component. (${environment}: ${id})`, + wrap: ({ + value, + name, + id, + directiveMatch, + location, + parameters, + runtime, + }: ServerFunctionDirectiveContext) => { + const variant = parseUseCacheVariant(directiveMatch[0]); + const modulePath = stripViteModuleQuery(id); + const moduleFileName = path.basename(modulePath); + const isAppPageDefault = + location === "module" && + name === "default" && + hasAppDir && + isInsideDirectory(appDir, modulePath) && + path.parse(moduleFileName).name === "page" && + fileMatcher.extensionRegex.test(moduleFileName); + const runtimeOptions = { + ...(isAppPageDefault ? { appPageDefaultExport: true } : {}), + ...(parameters ? { parameters } : {}), + }; + const pageOptions = + Object.keys(runtimeOptions).length > 0 + ? `, ${JSON.stringify(runtimeOptions)}` + : ""; + return `${runtime}.registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)}${pageOptions})`; + }, + filterExport: ({ + name, + id, + meta, + }: { + name: string; + id: string; + meta: { isFunction?: boolean }; + }) => { + if (meta.isFunction === false) return false; + if (/\/(layout|template)\.(tsx?|jsx?|mjs)$/.test(id) && name === "default") { + return false; + } + return true; + }, + }, + ], }); }) .catch((cause) => { @@ -4074,390 +4141,6 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { () => nextConfig, () => root, ), - // "use cache" directive transform: - // Detects "use cache" at file-level or function-level and wraps the - // exports/functions with registerCachedFunction() from vinext/cache-runtime. - // Runs without enforce so it executes after JSX transform (parseAst needs plain JS). - { - name: "vinext:use-cache", - - configResolved(config) { - // Capture the @vitejs/plugin-rsc manager so we can register hoisted - // "use cache" functions in the server-reference manifest (build) and - // use the correct normalised id for registerServerReference (dev+build). - // getPluginApi is defined on the "rsc:minimal" plugin's .api property. - if (resolvedRscPath) { - // oxlint-disable-next-line typescript/no-explicit-any - const api = (config.plugins as any[]).find( - // oxlint-disable-next-line typescript/no-explicit-any - (p: any) => p && typeof p === "object" && p.name === "rsc:minimal", - )?.api; - if (api) rscPluginApi = api; - } - }, - - transform: { - // Hook filter: only invoke JS when code contains 'use cache'. - // The vast majority of files don't use this directive. - filter: { - id: { - include: /\.(tsx?|jsx?|mjs)$/, - exclude: /node_modules/, - }, - code: "use cache", - }, - async handler(code, id) { - // Defensive guard — duplicates filter logic - if (id.includes("node_modules")) return null; - if (id.startsWith("\0")) return null; - if (!id.match(/\.(tsx?|jsx?|mjs)$/)) return null; - if (!code.includes("use cache")) return null; - - // Parse the AST first to check for actual "use cache" directives before - // throwing the missing-RSC error. The fast-path string check above can - // fire on files that contain "use cache" only in comments or string - // literals (e.g., in error messages), not as real directives. - const ast = parseAst(code); - - // Check for file-level "use cache" directive - const cacheDirective = ast.body.find( - (node) => - node.type === "ExpressionStatement" && - node.expression?.type === "Literal" && - typeof node.expression.value === "string" && - node.expression.value.startsWith("use cache"), - ); - - // Check for function-level "use cache" directives by walking function bodies. - // Accepts any function-like node: FunctionDeclaration/Expression, ArrowFunctionExpression, - // or MethodDefinition. MethodDefinition stores its FunctionExpression in `.value`, not - // `.body`, so we unwrap it here rather than at each call site to keep the callee safe. - function nodeHasInlineCacheDirective(node: ASTNode): boolean { - if (!node || typeof node !== "object") return false; - // MethodDefinition wraps its FunctionExpression in .value; unwrap to reach .body. - const fn = node.type === "MethodDefinition" ? node.value : node; - // fn.body is a BlockStatement node ({type:"BlockStatement", body:Statement[]}), not - // a raw array. Unwrap it. Arrow functions with expression bodies have a non-array - // .body — the BlockStatement check handles that case (body.body would be undefined). - const stmts: ASTNode[] | null = - // oxlint-disable-next-line typescript/no-explicit-any - (fn as any)?.body?.type === "BlockStatement" ? (fn as any).body.body : null; - if (Array.isArray(stmts)) { - for (const stmt of stmts) { - if ( - stmt?.type === "ExpressionStatement" && - stmt.expression?.type === "Literal" && - typeof stmt.expression?.value === "string" && - /^use cache(:\s*\w+)?$/.test(stmt.expression.value) - ) { - return true; - } - } - } - return false; - } - function astHasInlineCache(nodes: ASTNode[]): boolean { - for (const node of nodes) { - if (!node || typeof node !== "object") continue; - if ( - (node.type === "FunctionDeclaration" || - node.type === "FunctionExpression" || - node.type === "ArrowFunctionExpression" || - node.type === "MethodDefinition") && - nodeHasInlineCacheDirective(node) - ) { - return true; - } - // Walk into variable declarations, export declarations, etc. - for (const key of Object.keys(node)) { - if (key === "type" || key === "start" || key === "end" || key === "loc") continue; - const child = node[key as keyof typeof node] as ASTNode; - if (Array.isArray(child) && child.some((c) => c && typeof c === "object")) { - if (astHasInlineCache(child)) return true; - } else if (child && typeof child === "object" && child.type) { - if (astHasInlineCache([child])) return true; - } - } - } - return false; - } - const hasInlineCache = !cacheDirective && astHasInlineCache(ast.body); - - if (!cacheDirective && !hasInlineCache) return null; - - if (!resolvedRscTransformsPath) { - throw new Error( - "vinext: 'use cache' requires @vitejs/plugin-rsc to be installed.\n" + - "Run: " + - detectPackageManager(process.cwd()) + - " @vitejs/plugin-rsc", - ); - } - const { transformWrapExport, transformHoistInlineDirective } = await import( - pathToFileURL(resolvedRscTransformsPath).href - ); - - if (cacheDirective) { - // File-level "use cache" — wrap function exports with - // registerCachedFunction. Page default exports are wrapped directly - // (they're leaf components). Layout/template defaults are excluded - // because they receive {children} from the framework. - // oxlint-disable-next-line typescript/no-explicit-any - const directiveValue = (cacheDirective as any).expression.value; - const variant = - directiveValue === "use cache" - ? "" - : directiveValue.replace("use cache:", "").replace("use cache: ", "").trim(); - - // Only skip default export wrapping for layouts and templates — - // they receive {children} from the framework which requires - // temporary reference handling that registerCachedFunction doesn't - // support yet. Pages, not-found, loading, error, and default are - // leaf components with no {children} prop and can be cached directly. - const isLayoutOrTemplate = /\/(layout|template)\.(tsx?|jsx?|mjs)$/.test(id); - const modulePath = stripViteModuleQuery(id); - const moduleFileName = path.basename(modulePath); - const isAppPageModule = - hasAppDir && - isInsideDirectory(appDir, modulePath) && - path.parse(moduleFileName).name === "page" && - fileMatcher.extensionRegex.test(moduleFileName); - - const runtimeModuleUrl = pathToFileURL( - resolveShimModulePath(shimsDir, "cache-runtime"), - ).href; - const result = transformWrapExport(code, ast, { - runtime: (value: string, name: string) => { - const pageOptions = - name === "default" && isAppPageModule ? `, { appPageDefaultExport: true }` : ""; - return `(await import(${JSON.stringify(runtimeModuleUrl)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)}${pageOptions})`; - }, - rejectNonAsyncFunction: false, - filter: (name: string, meta: { isFunction?: boolean }) => { - // Skip non-functions (constants, types, etc.) - if (meta.isFunction === false) return false; - // Skip the default export on layout/template files — these - // receive {children} from the framework, and caching them - // requires temporary reference handling for the children slot. - // Named exports (e.g. generateMetadata) are still wrapped. - if (isLayoutOrTemplate && name === "default") return false; - return true; - }, - }); - - if (result.exportNames.length > 0) { - // Remove the directive itself so it doesn't cause runtime errors - const output = result.output; - output.overwrite( - cacheDirective.start, - cacheDirective.end, - `/* "use cache" — wrapped by vinext */`, - ); - return { - code: output.toString(), - map: output.generateMap({ hires: "boundary" }), - }; - } - - // Even if no exports were wrapped, still strip the directive - // (e.g., layout/template file with only a default export) - const output = new MagicString(code); - output.overwrite( - cacheDirective.start, - cacheDirective.end, - `/* "use cache" — handled by vinext */`, - ); - return { - code: output.toString(), - map: output.generateMap({ hires: "boundary" }), - }; - } - - // Check for function-level "use cache" directives - // (e.g., async function getData() { "use cache"; ... }) - if (hasInlineCache) { - const runtimeModuleUrl2 = pathToFileURL( - resolveShimModulePath(shimsDir, "cache-runtime"), - ).href; - - // In the RSC environment, inline "use cache" functions that are - // passed as props to client components must also be registered as - // server references. Without this, the RSC serializer cannot - // include them in the RSC payload (they're plain async functions - // with no server-reference metadata), so `useActionState` and - // `formAction` props fail to serialize. - // - // Strategy (mirrors Next.js' use-cache SWC transform, where the - // exported $$RSC_SERVER_CACHE_n binding IS the cache wrapper): - // after hoisting, reassign each hoisted export at module level to - // registerCachedServerReference(fn). Exported - // function declarations are live bindings, so both the original - // call sites and the server-reference manifest (which imports the - // module export by name on action POST) observe the wrapped - // function. This means a direct client→server invocation of the - // cached function goes through the cache, matching Next.js - // semantics — not just "callable but uncached". - // - // The $$id passed to registerServerReference must match how - // @vitejs/plugin-rsc resolves server references: - // - build: hashString(toRelativeId(absoluteId)) - // - dev: URL path relative to root (e.g. "/src/app/page.tsx") - // - // The hoisted export names are also queued for registration in the - // plugin-rsc serverReferenceMetaMap (via the - // "vinext:use-cache-server-references" plugin below) so the - // build-time virtual:vite-rsc/server-references manifest includes - // the module and dev-mode reference validation accepts the key. - // - const isRscEnv = this.environment?.name === "rsc"; - - // Compute the normalised reference key that matches what - // @vitejs/plugin-rsc's "use server" transform writes. Mirror the - // logic from vitePluginUseServer's getNormalizedId(): - // build → hashString(manager.toRelativeId(id)) - // dev → URL path under the Vite root - let normalizedRefKey: string | null = null; - if (isRscEnv) { - // oxlint-disable-next-line typescript/no-explicit-any - const manager: any = rscPluginApi?.manager ?? null; - // Fail loudly when the plugin-rsc manager is unavailable. - // Without it, "vinext:use-cache-server-references" cannot write - // the serverReferenceMetaMap entry, so wrapping the hoisted - // exports anyway would emit a serializable-but-unresolvable - // server reference: the RSC payload serialises fine, but the - // action POST 404s because the key is absent from the built - // server-references manifest (and fails dev-mode reference - // validation). The manager is a structural invariant whenever - // the "rsc" environment exists ("rsc:minimal" is always part of - // the plugin-rsc set and exposes it via .api), so this throw is - // believed unreachable — but a loud transform error beats a - // silent production 404 if that invariant ever breaks. - if (!manager) { - throw new Error( - `vinext: cannot register inline "use cache" function(s) in ${id} as server ` + - `references: the @vitejs/plugin-rsc manager is unavailable (no "rsc:minimal" ` + - `plugin with a manager api was found in the resolved Vite config). Refusing ` + - `to emit a server reference that would serialize but never resolve (it would ` + - `404 on action POST).`, - ); - } - const projectRoot: string = - manager.config?.root ?? this.environment?.config?.root ?? root; - if (this.environment?.mode === "build") { - // Use the plugin's own toRelativeId so the hash input is - // byte-for-byte identical to what plugin-rsc would produce - // (path.relative against manager.config.root). - const relativeId: string = manager.toRelativeId(id); - // hashString from plugin-rsc: sha256 → hex → first 12 chars. - normalizedRefKey = createHash("sha256") - .update(relativeId) - .digest() - .toString("hex") - .slice(0, 12); - } else { - // Dev key. plugin-rsc's getNormalizedId() additionally runs - // cleanUrl() on node_modules ids and uses an /@fs/-prefixed - // URL for ids outside the project root. Those shapes are not - // replicated here on purpose: this transform's filter excludes - // node_modules entirely, and the extension-anchored id regex - // (/\.(tsx?|jsx?|mjs)$/) rejects ids carrying a ?query, so - // neither can reach this point. For under-root source files - // the plugin's normalisation reduces to exactly this - // root-prefix slice. Ids outside the root (e.g. linked - // packages) keep the raw absolute path — unlike the plugin's - // /@fs/ URL, but still self-consistent: the same key is - // registered in serverReferenceMetaMap (which dev validation - // checks) and passed to the dev loader's import(id), which - // accepts absolute paths. - normalizedRefKey = - id.startsWith(projectRoot + "/") || id.startsWith(projectRoot + "\\") - ? id.slice(projectRoot.length) - : id; - } - } - - const parseVariant = (directiveMatch: string): string => - directiveMatch === "use cache" - ? "" - : directiveMatch.replace("use cache:", "").replace("use cache: ", "").trim(); - - try { - // Hoisted function metadata collected during the transform so the - // RSC branch can emit the module-level wrapping afterwards. - const hoisted: { name: string; variant: string; hasBoundArgs: boolean }[] = []; - const result = transformHoistInlineDirective(code, ast, { - directive: /^use cache(:\s*\w+)?$/, - runtime: (value: string, name: string, meta: { directiveMatch: string[] }) => { - const variant = parseVariant(meta.directiveMatch[0]); - if (isRscEnv) { - // The hoisted export is wrapped once at module level - // (below); the call site just references it. `.bind()` on - // the wrapped export is handled by the server reference's - // patched bind, which tracks $$bound for serialisation. - hoisted.push({ name, variant, hasBoundArgs: false }); - return value; - } - // Non-RSC env: no server-reference metadata needed — wrap the - // call site with the cache runtime only. - return `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`; - }, - encode: isRscEnv - ? (value: string) => { - const current = hoisted.at(-1); - if (current) current.hasBoundArgs = true; - return `__vinext_encryptActionBoundArgs(${value})`; - } - : undefined, - rejectNonAsyncFunction: false, - }); - - if (result.names.length > 0) { - if (isRscEnv && normalizedRefKey !== null) { - // Reassign each hoisted export to the cached + registered - // wrapper at module level. The assignments are PREPENDED: - // hoisted function declarations are initialised before any - // statement executes, so the reassignment runs first and - // every later reader — top-level call sites (e.g. - // `const getData = ` initialisers, which - // copy the binding value at evaluation time), render-time - // call sites, and the server-references manifest import on - // action POST — observes the wrapped function. - // - const serverRefShimUrl = pathToFileURL( - resolveShimModulePath(shimsDir, "cache-server-reference"), - ).href; - const lines: string[] = [ - `import { encryptActionBoundArgs as __vinext_encryptActionBoundArgs, registerCachedServerReference as __vinext_registerCachedServerReference } from ${JSON.stringify(serverRefShimUrl)};`, - ]; - for (const { name, variant, hasBoundArgs } of hoisted) { - lines.push( - `${name} = __vinext_registerCachedServerReference(${name}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)}, ${JSON.stringify(normalizedRefKey)}, ${JSON.stringify(name)}, ${hasBoundArgs});`, - ); - } - result.output.prepend(lines.join("\n") + "\n"); - - // Queue the manifest registration — performed by - // "vinext:use-cache-server-references" after rsc:use-server - // has run (see useCacheServerRefMeta for why). - useCacheServerRefMeta.set(id, { - referenceKey: normalizedRefKey, - exportNames: result.names, - }); - } - return { - code: result.output.toString(), - map: result.output.generateMap({ hires: "boundary" }), - }; - } - } catch { - // If hoisting fails (e.g., complex closure), fall through - } - } - - return null; - }, - }, - }, createImportMetaUrlPlugin({ getRoot: () => root, }), @@ -5132,44 +4815,6 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (rscPluginPromise) { plugins.push(rscPluginPromise); plugins.push(createRscClientReferenceLoadersPlugin()); - // Registers hoisted "use cache" functions in the plugin-rsc - // serverReferenceMetaMap so the build-time - // virtual:vite-rsc/server-references manifest includes their modules and - // dev-mode reference validation accepts their keys. This plugin MUST be - // placed after the plugin-rsc plugins: plugin-rsc's "rsc:use-server" - // transform deletes serverReferenceMetaMap[id] for every module whose code - // lacks "use server", which would wipe an entry written earlier in the - // same transform pipeline by "vinext:use-cache". - plugins.push({ - name: "vinext:use-cache-server-references", - transform: { - handler(_code, id) { - // Consume (get + delete) the pending entry so a later re-transform - // of a module that no longer contains "use cache" can't re-register - // stale export names. - const pending = useCacheServerRefMeta.get(id); - if (!pending || this.environment?.name !== "rsc" || !rscPluginApi?.manager) { - return null; - } - useCacheServerRefMeta.delete(id); - const metaMap = rscPluginApi.manager.serverReferenceMetaMap; - const existing = metaMap[id]; - // A module can contain both inline "use server" and inline - // "use cache" functions. In that case rsc:use-server has already - // written an entry for this id — its referenceKey is computed with - // the same formula as ours, so the keys agree and only the export - // names need to be unioned. - metaMap[id] = { - importId: id, - referenceKey: pending.referenceKey, - exportNames: existing - ? Array.from(new Set([...existing.exportNames, ...pending.exportNames])) - : pending.exportNames, - }; - return null; - }, - }, - }); } return plugins; diff --git a/packages/vinext/src/shims/cache-runtime.ts b/packages/vinext/src/shims/cache-runtime.ts index 2d0ff992db..5273118685 100644 --- a/packages/vinext/src/shims/cache-runtime.ts +++ b/packages/vinext/src/shims/cache-runtime.ts @@ -168,7 +168,10 @@ export function getCacheContext(): CacheContext | null { */ type RscModule = { renderToReadableStream: (data: unknown, options?: object) => ReadableStream; - createFromReadableStream: (stream: ReadableStream, options?: object) => Promise; + createFromReadableStream: ( + stream: ReadableStream, + options?: { serverReferences?: "resolve" | "preserve" }, + ) => Promise; encodeReply: (v: unknown[], options?: unknown) => Promise; createTemporaryReferenceSet: () => unknown; createClientTemporaryReferenceSet: () => unknown; @@ -426,6 +429,8 @@ type RegisterCachedFunctionOptions = { * rather than on the intermediate createElement config object. */ appPageDefaultExport?: boolean; + /** Declared function parameter shape supplied by the directive transform. */ + parameters?: { count: number; hasRest: boolean }; }; /** @@ -463,11 +468,15 @@ export function registerCachedFunction( // from key). Falls back to stableStringify when RSC is unavailable. let cacheKey: string; try { + const keyArgs = + options.parameters && !options.parameters.hasRest + ? args.slice(0, options.parameters.count) + : args; const processedArgs = - args.length > 0 - ? unwrapThenableObjectArray(args, { omitAppPageSearchParamsFromFirstArg }) + keyArgs.length > 0 + ? unwrapThenableObjectArray(keyArgs, { omitAppPageSearchParamsFromFirstArg }) : []; - if (rsc && args.length > 0) { + if (rsc && keyArgs.length > 0) { // Temporary references let encodeReply handle non-serializable values // (like React elements in args) by excluding them from the key. const tempRefs = rsc.createClientTemporaryReferenceSet(); @@ -546,7 +555,9 @@ export function registerCachedFunction( // RSC-serialized entry: base64 → bytes → stream → deserialize const bytes = base64ToUint8(existing.value.data.body); const stream = uint8ToStream(bytes); - const result = await rsc.createFromReadableStream(stream); + const result = await rsc.createFromReadableStream(stream, { + serverReferences: "preserve", + }); recordRequestScopedCacheControl(existing.cacheControl); return result; } diff --git a/packages/vinext/src/shims/cache-server-reference.ts b/packages/vinext/src/shims/cache-server-reference.ts deleted file mode 100644 index 4247cf91b1..0000000000 --- a/packages/vinext/src/shims/cache-server-reference.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { registerServerReference } from "@vitejs/plugin-rsc/react/rsc"; -import { - decryptActionBoundArgs, - encryptActionBoundArgs, -} from "@vitejs/plugin-rsc/utils/encryption-runtime"; -import { registerCachedFunction } from "./cache-runtime.js"; - -export { encryptActionBoundArgs }; - -export function registerCachedServerReference( - fn: (...args: unknown[]) => Promise, - cacheId: string, - cacheVariant: string, - referenceId: string, - referenceName: string, - hasEncryptedBoundArgs: boolean, -): (...args: unknown[]) => Promise { - const cached = registerCachedFunction(fn, cacheId, cacheVariant); - const callable: (...args: unknown[]) => Promise = hasEncryptedBoundArgs - ? async (encryptedBoundArgs: unknown, ...args: unknown[]) => { - const boundArgs = (await decryptActionBoundArgs( - encryptedBoundArgs as Promise, - )) as unknown[]; - return cached(...boundArgs, ...args); - } - : cached; - - return registerServerReference(callable, referenceId, referenceName) as typeof callable; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b1cf9c64f..df553e7a7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,10 +88,10 @@ catalogs: specifier: ^0.8.6 version: 0.8.6 '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1 + specifier: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12 + version: 6.0.2 '@vitejs/plugin-rsc': - specifier: ^0.5.27 + specifier: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12 version: 0.5.27 '@vitest/coverage-istanbul': specifier: 4.1.6 @@ -274,7 +274,7 @@ importers: version: 7.0.0-dev.20260217.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.6(@voidzero-dev/vite-plus-test@0.1.24) @@ -356,10 +356,10 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) drizzle-kit: specifier: 'catalog:' version: 0.31.10 @@ -386,7 +386,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -402,7 +402,7 @@ importers: devDependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) vite: specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' @@ -417,10 +417,10 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -451,7 +451,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) nitro: specifier: 'catalog:' version: nitro-nightly@3.0.1-20260512-093145-0498ce70(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(better-sqlite3@12.6.2)(chokidar@5.0.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.6.2)(kysely@0.28.15))(jiti@2.7.0)(miniflare@4.20260401.0) @@ -488,7 +488,7 @@ importers: version: 2.0.13 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) clsx: specifier: 'catalog:' version: 2.1.1 @@ -552,7 +552,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -588,10 +588,10 @@ importers: version: 4.2.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -671,10 +671,10 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -704,10 +704,10 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) ms: specifier: 'catalog:' version: 3.0.0-canary.1 @@ -747,7 +747,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -778,7 +778,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) typescript: specifier: 'catalog:' version: 5.9.3 @@ -799,7 +799,7 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -824,7 +824,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -867,10 +867,10 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -913,10 +913,10 @@ importers: version: link:../../packages/cloudflare '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -999,10 +999,10 @@ importers: version: link:../cloudflare '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react-server-dom-webpack: specifier: 'catalog:' version: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1014,7 +1014,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) fake-context-lib: specifier: file:./__test_packages__/fake-context-lib version: file:tests/fixtures/app-basic/__test_packages__/fake-context-lib @@ -1048,7 +1048,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1073,7 +1073,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1098,7 +1098,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1126,10 +1126,10 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1164,7 +1164,7 @@ importers: version: 1.9.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) better-auth: specifier: 'catalog:' version: 1.5.6(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-istanbul@4.1.6(@voidzero-dev/vite-plus-test@0.1.24))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(better-sqlite3@12.6.2)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.6.2)(kysely@0.28.15))(esbuild@0.27.3)(jiti@2.7.0)(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0) @@ -1195,7 +1195,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-intl: specifier: 'catalog:' version: 4.11.1(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@5.9.3) @@ -1223,7 +1223,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1251,7 +1251,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-view-transitions: specifier: 'catalog:' version: 0.3.5(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1279,7 +1279,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) nuqs: specifier: 'catalog:' version: 2.8.8(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) @@ -1316,7 +1316,7 @@ importers: version: 1.2.4(@types/react@19.2.16)(react@19.2.7) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) class-variance-authority: specifier: 'catalog:' version: 0.7.1 @@ -1372,7 +1372,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1397,7 +1397,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1422,7 +1422,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1485,7 +1485,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -3815,9 +3815,6 @@ packages: '@rolldown/pluginutils@1.0.0': resolution: {integrity: sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==} - '@rolldown/pluginutils@1.0.0-rc.7': - resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} - '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} @@ -4352,8 +4349,9 @@ packages: resolution: {integrity: sha512-hBcWIOppZV14bi+eAmCZj8Elj8hVSUZJTpf1lgGBhVD85pervzQ1poM/qYfFUlPraYSZYP+ASg6To5BwYmUSGQ==} engines: {node: '>=16'} - '@vitejs/plugin-react@6.0.1': - resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + '@vitejs/plugin-react@https://pkg.pr.new/@vitejs/plugin-react@2ae42d12': + resolution: {integrity: sha512-0KNIsFan88aSruEH1bs+Bk3n3solO57k7AvvtTNCg0rb2Yb4W1Ci+5PrAkp41A/6JXldWvZVBeyQEKCsR0j9iQ==, tarball: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12} + version: 6.0.2 engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 @@ -4365,8 +4363,9 @@ packages: babel-plugin-react-compiler: optional: true - '@vitejs/plugin-rsc@0.5.27': - resolution: {integrity: sha512-s1fd5DUkPXk86DDHPM/kP93WrvI0MoA8klxdDZmD1fMSaA9xujfgunsm8ZoUH0FemR+63vNalFsIDR0AJH4ktg==} + '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12': + resolution: {integrity: sha512-dhd7fBB3vSsIm3JmNZPWZRzrKRCCvvw3cU7TBDHEdQu+L/CBhxLZ7iWWsSd5YDe8AIAblHOr1A7I16jYiIsT7g==, tarball: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12} + version: 0.5.27 peerDependencies: react: '*' react-dom: '*' @@ -6619,6 +6618,11 @@ packages: engines: {node: '>=20.16.0'} hasBin: true + srvx@0.11.16: + resolution: {integrity: sha512-bp07zRuycfTY43IjAvvTFnmnJi8ikW0VFiHwOhhYcVW/L4xQ1XY4PAd4Nuum1rsA17C39zL7x+CDhrn5AL32Rw==} + engines: {node: '>=20.16.0'} + hasBin: true + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -8929,8 +8933,6 @@ snapshots: '@rolldown/pluginutils@1.0.0': {} - '@rolldown/pluginutils@1.0.0-rc.7': {} - '@rolldown/pluginutils@1.0.1': {} '@rollup/pluginutils@5.3.0': @@ -9368,12 +9370,12 @@ snapshots: '@resvg/resvg-wasm': 2.4.0 satori: 0.16.0 - '@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))': + '@vitejs/plugin-react@https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))': dependencies: - '@rolldown/pluginutils': 1.0.0-rc.7 + '@rolldown/pluginutils': 1.0.1 vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' - '@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': + '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': dependencies: '@rolldown/pluginutils': 1.0.1 es-module-lexer: 2.1.0 @@ -9381,7 +9383,7 @@ snapshots: magic-string: 0.30.21 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - srvx: 0.11.13 + srvx: 0.11.16 strip-literal: 3.1.0 turbo-stream: 3.2.0 vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' @@ -11903,6 +11905,8 @@ snapshots: srvx@0.11.13: {} + srvx@0.11.16: {} + statuses@2.0.2: {} std-env@4.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 088302deda..2c8a149f62 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,11 +39,11 @@ catalog: "@typescript/native-preview": ^7.0.0-dev.20260213.1 "@unpic/react": ^1.0.2 "@vercel/og": ^0.8.6 - "@vitejs/plugin-react": ^6.0.1 + "@vitejs/plugin-react": https://pkg.pr.new/@vitejs/plugin-react@2ae42d12 "@next/mdx": 16.2.7 recma-codehike: 0.0.1 remark-codehike: 0.0.1 - "@vitejs/plugin-rsc": ^0.5.27 + "@vitejs/plugin-rsc": https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12 "@vitest/coverage-istanbul": 4.1.6 better-auth: ^1.5.6 better-sqlite3: ^12.0.0 From 7e057900debd127b46d19581f7e00654253811bb Mon Sep 17 00:00:00 2001 From: James Date: Fri, 12 Jun 2026 02:49:56 +0100 Subject: [PATCH 12/33] test(use-cache): cover directive transforms across environments --- .github/workflows/ci.yml | 4 + playwright.config.ts | 12 + tests/app-router-production-server.test.ts | 6 +- tests/e2e/app-router-prod/use-cache.spec.ts | 30 ++ tests/e2e/app-router/use-cache.spec.ts | 86 ++++ .../app/use-cache-client-import/actions.ts | 5 + .../app/use-cache-client-import/form.tsx | 24 + .../app/use-cache-client-import/page.tsx | 10 + .../app-basic/app/use-cache-hmr/actions.ts | 5 + .../app-basic/app/use-cache-hmr/client.tsx | 17 + .../app-basic/app/use-cache-hmr/page.tsx | 5 + .../custom-kind.ts | 4 + .../destructured.ts | 5 + .../use-cache-transform-coverage/methods.ts | 13 + .../app/use-cache-transform-coverage/page.tsx | 24 + .../server-boundary-client.tsx | 20 + .../server-boundary.ts | 6 + .../star-source.ts | 3 + .../app/use-cache-transform-coverage/star.ts | 3 + tests/shims.test.ts | 57 +++ tests/use-cache-transform.test.ts | 480 ++++++++++++++---- 21 files changed, 708 insertions(+), 111 deletions(-) create mode 100644 tests/e2e/app-router-prod/use-cache.spec.ts create mode 100644 tests/fixtures/app-basic/app/use-cache-client-import/actions.ts create mode 100644 tests/fixtures/app-basic/app/use-cache-client-import/form.tsx create mode 100644 tests/fixtures/app-basic/app/use-cache-client-import/page.tsx create mode 100644 tests/fixtures/app-basic/app/use-cache-hmr/actions.ts create mode 100644 tests/fixtures/app-basic/app/use-cache-hmr/client.tsx create mode 100644 tests/fixtures/app-basic/app/use-cache-hmr/page.tsx create mode 100644 tests/fixtures/app-basic/app/use-cache-transform-coverage/custom-kind.ts create mode 100644 tests/fixtures/app-basic/app/use-cache-transform-coverage/destructured.ts create mode 100644 tests/fixtures/app-basic/app/use-cache-transform-coverage/methods.ts create mode 100644 tests/fixtures/app-basic/app/use-cache-transform-coverage/page.tsx create mode 100644 tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary-client.tsx create mode 100644 tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary.ts create mode 100644 tests/fixtures/app-basic/app/use-cache-transform-coverage/star-source.ts create mode 100644 tests/fixtures/app-basic/app/use-cache-transform-coverage/star.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b8f1dff64..b4d244969d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -302,6 +302,10 @@ jobs: label: pages-router-prod shardIndex: 1 shardTotal: 1 + - project: app-router-prod + label: app-router-prod + shardIndex: 1 + shardTotal: 1 - project: cloudflare-workers label: cloudflare-workers shardIndex: 1 diff --git a/playwright.config.ts b/playwright.config.ts index 96ef3a8995..fac4e3e78a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -49,6 +49,18 @@ const projectServers = { use: { baseURL: "http://localhost:4174" }, server: appRouterServer, }, + "app-router-prod": { + testDir: "./tests/e2e/app-router-prod", + use: { baseURL: "http://localhost:4184" }, + server: { + command: + "npx tsc -p ../../../packages/vinext/tsconfig.json && node ../../../packages/vinext/dist/cli.js build && node ../../../packages/vinext/dist/cli.js start --port 4184", + cwd: "./tests/fixtures/app-basic", + port: 4184, + reuseExistingServer: !process.env.CI, + timeout: 60_000, + }, + }, "app-router-chrome-browser-specific": { testDir: "./tests/e2e", testMatch: [appRouterBrowserSpecificTests], diff --git a/tests/app-router-production-server.test.ts b/tests/app-router-production-server.test.ts index 231f8af9d6..caf0b9874d 100644 --- a/tests/app-router-production-server.test.ts +++ b/tests/app-router-production-server.test.ts @@ -915,9 +915,9 @@ describe("App Router Production server (startProdServer)", () => { // The flight payload embeds each cached function prop as a server // reference whose id is "<12-hex normalised key>#". - // Serialization order follows the props order: getDate first, getRandom - // second, getMessage third. - const refIds = [...new Set(html.match(/[0-9a-f]{12}#\$\$hoist_\d+_[A-Za-z0-9_$]+/g) ?? [])]; + const refIds = [ + ...new Set(html.match(/[0-9a-f]{12}#\$\$hoist_[a-z0-9]+_\d+_[A-Za-z0-9_$]+/g) ?? []), + ]; expect(refIds.length).toBe(3); const [getDateRefId, getRandomRefId, getMessageRefId] = refIds; diff --git a/tests/e2e/app-router-prod/use-cache.spec.ts b/tests/e2e/app-router-prod/use-cache.spec.ts new file mode 100644 index 0000000000..627858bdd1 --- /dev/null +++ b/tests/e2e/app-router-prod/use-cache.spec.ts @@ -0,0 +1,30 @@ +import { expect, test } from "@playwright/test"; + +test.describe('production "use cache" server function references', () => { + test("replays cached RSC through SSR and invokes nested functions from the browser", async ({ + page, + }) => { + await page.goto("/use-cache-nested-fn-props"); + await expect(page.getByTestId("use-cache-nested-fn-props-page")).toBeVisible(); + + await page.locator("#submit-button-date").click(); + await expect(page.locator("#date")).toHaveText(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + const firstDate = await page.locator("#date").textContent(); + await page.locator("#submit-button-date").click(); + await expect(page.locator("#date")).toHaveText(firstDate!); + + await page.locator("#submit-button-random").click(); + await expect(page.locator("#random")).toHaveText(/^\d+\.\d+$/); + const firstRandom = await page.locator("#random").textContent(); + await page.locator("#submit-button-random").click(); + await expect(page.locator("#random")).toHaveText(firstRandom!); + + await page.locator("#submit-button-message").click(); + await expect(page.locator("#message")).toHaveText( + /^message:closure-captured-bound-arg-vinext:[0-9.e+-]+$/, + ); + const firstMessage = await page.locator("#message").textContent(); + await page.locator("#submit-button-message").click(); + await expect(page.locator("#message")).toHaveText(firstMessage!); + }); +}); diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index 81f0d2f513..1a36f7164b 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -1,6 +1,30 @@ import { test, expect } from "@playwright/test"; +import { readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; const BASE = "http://localhost:4174"; +const USE_CACHE_HMR_ACTIONS_FILE = path.join( + process.cwd(), + "tests/fixtures/app-basic/app/use-cache-hmr/actions.ts", +); +const USE_CACHE_HMR_CACHED = `"use cache"; + +export async function getMode() { + return "cached"; +} +`; +const USE_CACHE_HMR_PLAIN = `"use server"; + +export async function getMode() { + return "plain"; +} +`; + +async function writeUseCacheHmrActions(content: string) { + if ((await readFile(USE_CACHE_HMR_ACTIONS_FILE, "utf8")) !== content) { + await writeFile(USE_CACHE_HMR_ACTIONS_FILE, content); + } +} test.describe('"use cache" file-level directive', () => { test("use-cache page renders correctly", async ({ page }) => { @@ -99,6 +123,68 @@ test.describe('"use cache" function-level directive', () => { }); }); +test.describe('"use cache" direct client imports', () => { + test("file-level cached exports become callable server references", async ({ page }) => { + await page.goto(`${BASE}/use-cache-client-import`); + await expect(async () => { + await page.locator("#call-client-imported-cache").click(); + await expect(page.getByTestId("client-imported-cache-result")).toHaveText( + /^client-cache:direct:[0-9.e+-]+$/, + { timeout: 2000 }, + ); + }).toPass({ timeout: 15_000 }); + }); +}); + +test.describe('"use cache" transform coverage', () => { + test("supports advanced function and export forms", async ({ page }) => { + await page.goto(`${BASE}/use-cache-transform-coverage`); + await expect(page.getByTestId("use-cache-transform-coverage")).toHaveText( + "destructured|export-star|object-method|static-method|server-boundary|custom-kind", + ); + await expect(async () => { + await page.locator("#call-cached-server-boundary").click(); + await expect(page.getByTestId("cached-server-boundary-result")).toHaveText( + "server-boundary", + { timeout: 2000 }, + ); + }).toPass({ timeout: 15_000 }); + }); + + test("removes and restores directive metadata during HMR", async ({ page }) => { + await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED); + try { + await page.goto(`${BASE}/use-cache-hmr`); + await expect(async () => { + await page.locator("#call-use-cache-hmr").click(); + await expect(page.getByTestId("use-cache-hmr-result")).toHaveText("cached", { + timeout: 2000, + }); + }).toPass({ timeout: 15_000 }); + + await writeUseCacheHmrActions(USE_CACHE_HMR_PLAIN); + await expect(async () => { + await page.reload(); + await page.locator("#call-use-cache-hmr").click(); + await expect(page.getByTestId("use-cache-hmr-result")).toHaveText("plain", { + timeout: 2000, + }); + }).toPass({ timeout: 15_000 }); + + await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED); + await expect(async () => { + await page.reload(); + await page.locator("#call-use-cache-hmr").click(); + await expect(page.getByTestId("use-cache-hmr-result")).toHaveText("cached", { + timeout: 2000, + }); + }).toPass({ timeout: 15_000 }); + } finally { + await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED); + } + }); +}); + test.describe('"use cache" nested cache functions as props', () => { // Ported from Next.js: test/e2e/app-dir/use-cache-with-server-function-props // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/use-cache-with-server-function-props/use-cache-with-server-function-props.test.ts diff --git a/tests/fixtures/app-basic/app/use-cache-client-import/actions.ts b/tests/fixtures/app-basic/app/use-cache-client-import/actions.ts new file mode 100644 index 0000000000..e27f4713d3 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-client-import/actions.ts @@ -0,0 +1,5 @@ +"use cache"; + +export async function getCachedMessage(value: string) { + return `client-cache:${value}:${Math.random()}`; +} diff --git a/tests/fixtures/app-basic/app/use-cache-client-import/form.tsx b/tests/fixtures/app-basic/app/use-cache-client-import/form.tsx new file mode 100644 index 0000000000..a80152df09 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-client-import/form.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useState } from "react"; +import { getCachedMessage } from "./actions"; + +export function ClientCacheCaller() { + const [message, setMessage] = useState(""); + + return ( +
+ + {message} +
+ ); +} diff --git a/tests/fixtures/app-basic/app/use-cache-client-import/page.tsx b/tests/fixtures/app-basic/app/use-cache-client-import/page.tsx new file mode 100644 index 0000000000..9907e3a252 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-client-import/page.tsx @@ -0,0 +1,10 @@ +import { ClientCacheCaller } from "./form"; + +export default function Page() { + return ( +
+

Client Imported Cache

+ +
+ ); +} diff --git a/tests/fixtures/app-basic/app/use-cache-hmr/actions.ts b/tests/fixtures/app-basic/app/use-cache-hmr/actions.ts new file mode 100644 index 0000000000..3bfd5ae378 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-hmr/actions.ts @@ -0,0 +1,5 @@ +"use cache"; + +export async function getMode() { + return "cached"; +} diff --git a/tests/fixtures/app-basic/app/use-cache-hmr/client.tsx b/tests/fixtures/app-basic/app/use-cache-hmr/client.tsx new file mode 100644 index 0000000000..27073b6801 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-hmr/client.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { useState } from "react"; +import { getMode } from "./actions"; + +export function UseCacheHmrClient() { + const [mode, setMode] = useState(""); + + return ( +
+ + {mode} +
+ ); +} diff --git a/tests/fixtures/app-basic/app/use-cache-hmr/page.tsx b/tests/fixtures/app-basic/app/use-cache-hmr/page.tsx new file mode 100644 index 0000000000..88be68e771 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-hmr/page.tsx @@ -0,0 +1,5 @@ +import { UseCacheHmrClient } from "./client"; + +export default function UseCacheHmrPage() { + return ; +} diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/custom-kind.ts b/tests/fixtures/app-basic/app/use-cache-transform-coverage/custom-kind.ts new file mode 100644 index 0000000000..a72a3b089f --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-transform-coverage/custom-kind.ts @@ -0,0 +1,4 @@ +export async function customKind() { + "use cache: durable-cache"; + return "custom-kind"; +} diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/destructured.ts b/tests/fixtures/app-basic/app/use-cache-transform-coverage/destructured.ts new file mode 100644 index 0000000000..50ad3524fa --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-transform-coverage/destructured.ts @@ -0,0 +1,5 @@ +"use cache"; + +export const { value: destructured } = { + value: async () => "destructured", +}; diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/methods.ts b/tests/fixtures/app-basic/app/use-cache-transform-coverage/methods.ts new file mode 100644 index 0000000000..affede44bd --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-transform-coverage/methods.ts @@ -0,0 +1,13 @@ +export const objectMethods = { + async getValue() { + "use cache"; + return "object-method"; + }, +}; + +export class StaticMethods { + static async getValue() { + "use cache"; + return "static-method"; + } +} diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/page.tsx b/tests/fixtures/app-basic/app/use-cache-transform-coverage/page.tsx new file mode 100644 index 0000000000..cb962ecc1c --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-transform-coverage/page.tsx @@ -0,0 +1,24 @@ +import { destructured } from "./destructured"; +import { fromStar } from "./star"; +import { objectMethods, StaticMethods } from "./methods"; +import { fromServerBoundary } from "./server-boundary"; +import { customKind } from "./custom-kind"; +import { ServerBoundaryClientCaller } from "./server-boundary-client"; + +export default async function UseCacheTransformCoveragePage() { + const values = await Promise.all([ + destructured(), + fromStar(), + objectMethods.getValue(), + StaticMethods.getValue(), + fromServerBoundary(), + customKind(), + ]); + + return ( + <> + {values.join("|")} + + + ); +} diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary-client.tsx b/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary-client.tsx new file mode 100644 index 0000000000..f197377fab --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary-client.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useState } from "react"; +import { fromServerBoundary } from "./server-boundary"; + +export function ServerBoundaryClientCaller() { + const [value, setValue] = useState(""); + + return ( +
+ + {value} +
+ ); +} diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary.ts b/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary.ts new file mode 100644 index 0000000000..3ac3300496 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary.ts @@ -0,0 +1,6 @@ +"use server"; + +export async function fromServerBoundary() { + "use cache"; + return "server-boundary"; +} diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/star-source.ts b/tests/fixtures/app-basic/app/use-cache-transform-coverage/star-source.ts new file mode 100644 index 0000000000..4cca5d9349 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-transform-coverage/star-source.ts @@ -0,0 +1,3 @@ +export async function fromStar() { + return "export-star"; +} diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/star.ts b/tests/fixtures/app-basic/app/use-cache-transform-coverage/star.ts new file mode 100644 index 0000000000..43173a5147 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-transform-coverage/star.ts @@ -0,0 +1,3 @@ +"use cache"; + +export * from "./star-source"; diff --git a/tests/shims.test.ts b/tests/shims.test.ts index 70aaea1321..a561ceeb5d 100644 --- a/tests/shims.test.ts +++ b/tests/shims.test.ts @@ -4563,6 +4563,63 @@ describe('"use cache" runtime', () => { expect(callCount).toBe(2); }); + it("registerCachedFunction excludes arguments beyond declared arity from cache keys", async () => { + const { registerCachedFunction } = + await import("../packages/vinext/src/shims/cache-runtime.js"); + let calls = 0; + const cached = registerCachedFunction( + async (value: number, ...extra: unknown[]) => { + calls++; + return { value, extra }; + }, + "test:declared-arity", + "", + { parameters: { count: 1, hasRest: false } }, + ); + + expect(await cached(1, "first")).toEqual({ value: 1, extra: ["first"] }); + expect(await cached(1, "second")).toEqual({ value: 1, extra: ["first"] }); + expect(calls).toBe(1); + }); + + it("registerCachedFunction excludes framework-injected arguments for zero-arity functions", async () => { + const { registerCachedFunction } = + await import("../packages/vinext/src/shims/cache-runtime.js"); + let calls = 0; + const cached = registerCachedFunction( + async (...injected: unknown[]) => { + calls++; + return injected; + }, + "test:zero-arity", + "", + { parameters: { count: 0, hasRest: false } }, + ); + + expect(await cached("first")).toEqual(["first"]); + expect(await cached("second")).toEqual(["first"]); + expect(calls).toBe(1); + }); + + it("registerCachedFunction includes all arguments for rest parameters", async () => { + const { registerCachedFunction } = + await import("../packages/vinext/src/shims/cache-runtime.js"); + let calls = 0; + const cached = registerCachedFunction( + async (...values: number[]) => { + calls++; + return values; + }, + "test:rest-args", + "", + { parameters: { count: 1, hasRest: true } }, + ); + + expect(await cached(1, 2)).toEqual([1, 2]); + expect(await cached(1, 3)).toEqual([1, 3]); + expect(calls).toBe(2); + }); + it("scopes shared cache entries by build ID", async () => { const { registerCachedFunction } = await import("../packages/vinext/src/shims/cache-runtime.js"); diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index 575847b7a1..1b2455caf7 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -1,23 +1,13 @@ /** - * Unit tests for the "vinext:use-cache" transform's server-reference wrapping - * of inline (function-level) "use cache" directives in the RSC environment. - * - * These call the plugin's transform hook directly (same pattern as - * optimize-imports.test.ts) so they can exercise environment/manager - * combinations that are impractical to reproduce through a full Vite server: - * - * 1. The manager-less fail-loud path: when the @vitejs/plugin-rsc manager is - * unavailable, wrapping must throw instead of emitting a - * serializable-but-unresolvable server reference (which would surface as a - * silent 404 on action POST in production). - * 2. The happy path's reference key: hashString(toRelativeId(id)) in build. - * 3. Closure-captured variables use plugin-rsc's action encryption runtime, - * matching the encryption path used by its own "use server" transform. + * Tests the plugin-rsc serverFunctionDirectives integration used for function-level + * "use cache" directives. Vinext supplies cache wrapper expressions; plugin-rsc + * owns directive discovery, closure hoisting, encryption, reference ids, and + * server-reference manifest metadata. */ import path from "node:path"; import { createHash } from "node:crypto"; import { describe, expect, it } from "vite-plus/test"; -import type { Plugin } from "vite"; +import { parseAst, type Plugin } from "vite"; import vinext from "../packages/vinext/src/index.js"; import { APP_FIXTURE_DIR } from "./helpers.js"; @@ -26,124 +16,123 @@ function unwrapHook(hook: any): ((...args: any[]) => any) | undefined { return typeof hook === "function" ? hook : hook?.handler; } -/** Instantiate vinext() and return its "vinext:use-cache" plugin. */ -function getUseCachePlugin(): Plugin { +async function getPlugins(): Promise { // oxlint-disable-next-line typescript/no-explicit-any - const rawPlugins = vinext({ appDir: APP_FIXTURE_DIR }) as any[]; - const plugin = rawPlugins - .flat(Infinity) - .find((p) => p && typeof p === "object" && p.name === "vinext:use-cache"); - expect(plugin).toBeDefined(); - return plugin as Plugin; + const rawPlugins = (vinext({ appDir: APP_FIXTURE_DIR }) as any[]).flat(Infinity); + const resolved = await Promise.all(rawPlugins.map((plugin) => Promise.resolve(plugin))); + return resolved.flat(Infinity).filter(Boolean) as Plugin[]; } const moduleId = path.join(APP_FIXTURE_DIR, "app", "unit-test-inline-cache.tsx"); - const inlineCacheCode = [ `export async function getData() {`, ` "use cache";`, ` return 1;`, `}`, ].join("\n"); +const fileCacheCode = [ + `"use cache";`, + `export async function getData() {`, + ` return 1;`, + `}`, +].join("\n"); -function fakeManager(root: string) { - return { - config: { root }, - toRelativeId: (id: string) => path.relative(root, id).split(path.sep).join("/"), - serverReferenceMetaMap: {} as Record, - }; -} - -describe("vinext:use-cache inline transform (RSC server references)", () => { - it("throws in the RSC build environment when the plugin-rsc manager is unavailable", async () => { - // configResolved is intentionally NOT called: rscPluginApi stays null, - // simulating a build where the "rsc:minimal" plugin (and its manager api) - // is missing. Wrapping anyway would emit a reference that serializes into - // the RSC payload but is never registered in the server-references - // manifest — a silent prod 404 on action POST — so the transform must - // fail loudly at build time instead. - const plugin = getUseCachePlugin(); - const transform = unwrapHook(plugin.transform)!; - - await expect( - transform.call( - { environment: { name: "rsc", mode: "build", config: { root: APP_FIXTURE_DIR } } }, - inlineCacheCode, - moduleId, - ), - ).rejects.toThrow(/plugin-rsc manager is unavailable/); - }); - - it("throws in the RSC dev environment when the plugin-rsc manager is unavailable", async () => { - // Dev has the same failure shape: the dev-mode reference validation reads - // serverReferenceMetaMap, which cannot be populated without the manager. - const plugin = getUseCachePlugin(); - const transform = unwrapHook(plugin.transform)!; - - await expect( - transform.call( - { environment: { name: "rsc", mode: "dev", config: { root: APP_FIXTURE_DIR } } }, - inlineCacheCode, - moduleId, - ), - ).rejects.toThrow(/plugin-rsc manager is unavailable/); - }); - - it("does not require the manager outside the RSC environment", async () => { - // SSR/client environments wrap call sites with the cache runtime only — - // no server-reference metadata is involved, so no manager is needed. - const plugin = getUseCachePlugin(); - const transform = unwrapHook(plugin.transform)!; - - const result = await transform.call( - { environment: { name: "ssr", mode: "build", config: { root: APP_FIXTURE_DIR } } }, - inlineCacheCode, - moduleId, - ); - expect(result).not.toBeNull(); - expect(result!.code).toContain("registerCachedFunction"); - expect(result!.code).not.toContain("registerServerReference"); +async function configurePluginRsc(plugins: Plugin[]) { + const minimal = plugins.find((plugin) => plugin.name === "rsc:minimal")!; + const configResolved = unwrapHook(minimal.configResolved)!; + configResolved.call(minimal, { + root: APP_FIXTURE_DIR, + command: "build", + environments: { + rsc: { build: { outDir: path.join(APP_FIXTURE_DIR, "dist/rsc") } }, + }, }); + // oxlint-disable-next-line typescript/no-explicit-any + return (minimal as any).api.manager; +} - it("wraps hoisted exports with the plugin-rsc build reference key when the manager is present", async () => { - const plugin = getUseCachePlugin(); - const manager = fakeManager(APP_FIXTURE_DIR); - const configResolved = unwrapHook(plugin.configResolved)!; - configResolved.call(plugin, { plugins: [{ name: "rsc:minimal", api: { manager } }] }); - +describe("plugin-rsc inline use-cache references", () => { + it("wraps and registers inline cache functions with plugin-rsc's build reference key", async () => { + const plugins = await getPlugins(); + const manager = await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( - { environment: { name: "rsc", mode: "build", config: { root: APP_FIXTURE_DIR } } }, + { environment: { name: "rsc", mode: "build" } }, inlineCacheCode, moduleId, ); expect(result).not.toBeNull(); - // Build key parity with plugin-rsc: hashString(toRelativeId(id)) where - // hashString = sha256 → hex → first 12 chars. const expectedKey = createHash("sha256") .update(manager.toRelativeId(moduleId)) .digest("hex") .slice(0, 12); - expect(result!.code).toContain("__vinext_registerCachedServerReference"); + expect(result!.code).toContain("$$ReactServer.registerServerReference"); + expect(result!.code).toContain("registerCachedFunction"); expect(result!.code).toContain(JSON.stringify(expectedKey)); + expect(manager.serverReferenceMetaMap[moduleId]).toEqual({ + importId: moduleId, + referenceKey: expectedKey, + exportNames: [expect.stringMatching(/^\$\$hoist_[a-z0-9]+_0_getData$/)], + }); + }); - // Server-reference registration and action encryption are imported via a - // vinext-owned integration module so transformed application modules do - // not need to resolve plugin-rsc's runtime package from their location. - const importSpecifiers = [...result!.code.matchAll(/from "([^"]+)"/g)].map((m) => m[1]); - expect(importSpecifiers).toContainEqual( - expect.stringContaining("/shims/cache-server-reference"), + it("keeps hoist names stable when unrelated cached functions are inserted", async () => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + const original = await transform.call( + { environment: { name: "rsc", mode: "build" } }, + inlineCacheCode, + moduleId, + ); + const withUnrelated = await transform.call( + { environment: { name: "rsc", mode: "build" } }, + [`async function unrelated() {`, ` "use cache";`, ` return 0;`, `}`, inlineCacheCode].join( + "\n", + ), + moduleId, ); - expect(importSpecifiers).not.toContainEqual(expect.stringContaining("@vitejs/plugin-rsc")); + const getDataName = (code: string) => + code.match(/function (\$\$hoist_[a-z0-9]+_0_getData)/)?.[1]; + expect(getDataName(original!.code)).toBeDefined(); + expect(getDataName(withUnrelated!.code)).toBe(getDataName(original!.code)); }); - it("encrypts closure-captured variables before binding the server reference", async () => { - const plugin = getUseCachePlugin(); - const manager = fakeManager(APP_FIXTURE_DIR); - const configResolved = unwrapHook(plugin.configResolved)!; - configResolved.call(plugin, { plugins: [{ name: "rsc:minimal", api: { manager } }] }); + it("removes owned reference metadata when the directive is removed", async () => { + const plugins = await getPlugins(); + const manager = await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + await transform.call( + { environment: { name: "rsc", mode: "build" } }, + inlineCacheCode, + moduleId, + ); + expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); + await transform.call( + { environment: { name: "rsc", mode: "build" } }, + `export async function getData() { return 1; }`, + moduleId, + ); + expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); + }); + it("encrypts closure captures and reports bound-argument metadata to vinext", async () => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; const closureCode = [ `export async function CachedSection() {`, ` "use cache";`, @@ -156,18 +145,293 @@ describe("vinext:use-cache inline transform (RSC server references)", () => { `}`, ].join("\n"); - const transform = unwrapHook(plugin.transform)!; const result = await transform.call( - { environment: { name: "rsc", mode: "build", config: { root: APP_FIXTURE_DIR } } }, + { environment: { name: "rsc", mode: "build" } }, closureCode, moduleId, ); expect(result).not.toBeNull(); expect(result!.code).toMatch( - /\.bind\(null,\s*__vinext_encryptActionBoundArgs\(\[capturedSecret\]\)\)/, + /\.bind\(null,\s*__vite_rsc_encryption_runtime\.encryptActionBoundArgs\(\[capturedSecret\]\)\)/, ); expect(result!.code).not.toMatch(/\.bind\(null,\s*capturedSecret\)/); - expect(result!.code).toContain("__vinext_registerCachedServerReference"); - expect(result!.code).toContain(", true);"); + expect(result!.code).toContain("decryptActionBoundArgs($$encoded)"); + }); + + it.each(["ssr", "client"])( + "rejects standalone inline cache functions in the %s graph", + async (environmentName) => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + + await expect( + transform.call( + { environment: { name: environmentName, mode: "build" } }, + inlineCacheCode, + moduleId, + ), + ).rejects.toThrow(/inline "use cache".*Client Component/); + }, + ); + + it("supports destructured file-level exports", async () => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + const result = await transform.call( + { environment: { name: "rsc", mode: "build" } }, + [`"use cache";`, `export const { value: getData } = { value: async () => 1 };`].join("\n"), + moduleId, + ); + expect(result!.code).toContain("registerCachedFunction(getData"); + }); + + it("supports named re-exports from file-level cache modules", async () => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + const result = await transform.call( + { environment: { name: "rsc", mode: "build" } }, + [`"use cache";`, `export { getData } from "./data";`].join("\n"), + moduleId, + ); + expect(result!.code).toContain("registerCachedFunction($$import_getData"); + }); + + it("accepts configured cache kinds containing punctuation", async () => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + const result = await transform.call( + { environment: { name: "rsc", mode: "build" } }, + [`export async function getData() {`, ` "use cache: durable-cache";`, `}`].join("\n"), + moduleId, + ); + expect(result?.code).toContain('"durable-cache"'); + }); + + it("wraps mixed file-level export forms", async () => { + const plugins = await getPlugins(); + const manager = await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + const result = await transform.call( + { environment: { name: "rsc", mode: "build" } }, + [ + `"use cache";`, + `const imported = async () => 1;`, + `export const direct = async () => 2;`, + `export const alias = imported;`, + `const named = async function named() { return 3; };`, + `export { named, imported as renamed };`, + `export default imported;`, + ].join("\n"), + moduleId, + ); + expect(result!.code).toContain("registerCachedFunction(direct"); + expect(result!.code).toContain("registerCachedFunction(alias"); + expect(result!.code).toContain("registerCachedFunction(named"); + expect(result!.code).toContain("registerCachedFunction(imported"); + expect(manager.serverReferenceMetaMap[moduleId].exportNames).toEqual( + expect.arrayContaining(["direct", "alias", "named", "renamed", "default"]), + ); + }); + + it("rejects statically known synchronous cached functions", async () => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + await expect( + transform.call( + { environment: { name: "rsc", mode: "build" } }, + [`export function getData() {`, ` "use cache";`, `}`].join("\n"), + moduleId, + ), + ).rejects.toThrow(/non async function/); + }); + + it.each(["use cache:remote", "use cache remote", "use cache : remote"])( + "rejects malformed cache directive %s", + async (directive) => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + await expect( + transform.call( + { environment: { name: "rsc", mode: "build" } }, + [`export async function getData() {`, ` ${JSON.stringify(directive)};`, `}`].join("\n"), + moduleId, + ), + ).rejects.toThrow(/Invalid cache directive/); + }, + ); + + it.each([ + [ + "object method", + [ + `const object = {`, + ` async getData() {`, + ` "use cache";`, + ` return 1;`, + ` },`, + `};`, + `export { object };`, + ].join("\n"), + ], + [ + "static class method", + [ + `export class CacheClass {`, + ` static async getData() {`, + ` "use cache";`, + ` return 1;`, + ` }`, + `}`, + ].join("\n"), + ], + ])("handles inline directives in %s syntax", async (_label, code) => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + const result = await transform.call( + { environment: { name: "rsc", mode: "build" } }, + code, + moduleId, + ); + expect(result?.code).toContain("registerCachedFunction"); + expect(() => parseAst(result!.code)).not.toThrow(); }); + + it("rejects inline directives in class instance methods", async () => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + await expect( + transform.call( + { environment: { name: "rsc", mode: "build" } }, + [`export class CacheClass {`, ` async getData() {`, ` "use cache";`, ` }`, `}`].join( + "\n", + ), + moduleId, + ), + ).rejects.toThrow(/class instance methods/); + }); + + it("preserves inline cache semantics inside a module-level use-server boundary", async () => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const context = { environment: { name: "rsc", mode: "build" } }; + const source = [ + `"use server";`, + `export async function getData() {`, + ` "use cache";`, + ` return 1;`, + `}`, + ].join("\n"); + const result = await unwrapHook(plugin.transform)!.call(context, source, moduleId); + expect(result?.code).toContain("registerCachedFunction"); + expect(result?.code).not.toContain("registerServerReference"); + }); + + it("rejects conflicting file-level cache and use-server directives", async () => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + await expect( + transform.call( + { environment: { name: "rsc", mode: "build" } }, + [`"use server";`, `"use cache";`, `export async function getData() {}`].join("\n"), + moduleId, + ), + ).rejects.toThrow(/cannot contain both/); + }); + + it("returns a source map for transformed modules", async () => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const result = await unwrapHook(plugin.transform)!.call( + { environment: { name: "rsc", mode: "build" } }, + inlineCacheCode, + moduleId, + ); + expect(result?.map).toBeTruthy(); + }); + + it("wraps and registers file-level cache exports in the RSC graph", async () => { + const plugins = await getPlugins(); + const manager = await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + const result = await transform.call( + { environment: { name: "rsc", mode: "build" } }, + fileCacheCode, + moduleId, + ); + expect(result).not.toBeNull(); + expect(result!.code).toContain("$$ReactServer.registerServerReference"); + expect(result!.code).toContain("registerCachedFunction"); + expect(result!.code).not.toContain('"use cache";'); + expect(manager.serverReferenceMetaMap[moduleId].exportNames).toEqual(["getData"]); + }); + + it.each(["ssr", "client"])( + "emits server-reference proxies for file-level cache exports in the %s graph", + async (environmentName) => { + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "rsc:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + const result = await transform.call( + { environment: { name: environmentName, mode: "build" } }, + fileCacheCode, + moduleId, + ); + expect(result).not.toBeNull(); + expect(result!.code).toContain("createServerReference"); + expect(result!.code).toContain("#getData"); + expect(result!.code).not.toContain("registerCachedFunction"); + expect(result!.code).not.toContain("registerCachedServerReference"); + }, + ); }); From 653775530f80c85aa75cde068998901c8ac39d13 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 12 Jun 2026 03:21:11 +0100 Subject: [PATCH 13/33] fix(cache): update RSC directive prerelease --- packages/vinext/src/index.ts | 5 -- pnpm-lock.yaml | 102 +++++++++++++++++------------------ pnpm-workspace.yaml | 4 +- 3 files changed, 53 insertions(+), 58 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index afbc24a903..08d2e23a0c 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -922,16 +922,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // so all import() calls in this module use consistent resolution. let resolvedReactPath: string | null = null; let resolvedRscPath: string | null = null; - let resolvedRscTransformsPath: string | null = null; // Prefer the user's project graph so vinext shares the app's Vite/plugin // instances. In source/workspace development, test fixtures may not declare // peer deps explicitly, so fall back to vinext's own install location. resolvedReactPath = resolveOptionalDependency(earlyBaseDir, "@vitejs/plugin-react"); resolvedRscPath = resolveOptionalDependency(earlyBaseDir, "@vitejs/plugin-rsc"); - resolvedRscTransformsPath = resolveOptionalDependency( - earlyBaseDir, - "@vitejs/plugin-rsc/transforms", - ); // If app/ exists and auto-RSC is enabled, create a lazy Promise that // resolves to the configured RSC plugin array. Vite's asyncFlatten diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f5dce32db..49b3d043d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,10 +88,10 @@ catalogs: specifier: ^0.8.6 version: 0.8.6 '@vitejs/plugin-react': - specifier: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12 + specifier: https://pkg.pr.new/@vitejs/plugin-react@06e58416 version: 6.0.2 '@vitejs/plugin-rsc': - specifier: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12 + specifier: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416 version: 0.5.27 '@vitest/coverage-istanbul': specifier: 4.1.6 @@ -277,7 +277,7 @@ importers: version: 7.0.0-dev.20260217.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.6(@voidzero-dev/vite-plus-test@0.1.24) @@ -362,10 +362,10 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) drizzle-kit: specifier: 'catalog:' version: 0.31.10 @@ -392,7 +392,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -408,7 +408,7 @@ importers: devDependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) vite: specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' @@ -423,10 +423,10 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -457,7 +457,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) nitro: specifier: 'catalog:' version: nitro-nightly@3.0.1-20260512-093145-0498ce70(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(better-sqlite3@12.6.2)(chokidar@5.0.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.6.2)(kysely@0.28.15))(jiti@2.7.0)(miniflare@4.20260401.0) @@ -494,7 +494,7 @@ importers: version: 2.0.13 '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) clsx: specifier: 'catalog:' version: 2.1.1 @@ -558,7 +558,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -594,10 +594,10 @@ importers: version: 4.2.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -677,10 +677,10 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -710,10 +710,10 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) ms: specifier: 'catalog:' version: 3.0.0-canary.1 @@ -753,7 +753,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -784,7 +784,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) typescript: specifier: 'catalog:' version: 5.9.3 @@ -805,7 +805,7 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -830,7 +830,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -873,10 +873,10 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -919,10 +919,10 @@ importers: version: link:../../packages/cloudflare '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1005,10 +1005,10 @@ importers: version: link:../cloudflare '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react-server-dom-webpack: specifier: 'catalog:' version: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1020,7 +1020,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) fake-context-lib: specifier: file:./__test_packages__/fake-context-lib version: file:tests/fixtures/app-basic/__test_packages__/fake-context-lib @@ -1054,7 +1054,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1079,7 +1079,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1104,7 +1104,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1132,10 +1132,10 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1170,7 +1170,7 @@ importers: version: 1.9.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) better-auth: specifier: 'catalog:' version: 1.5.6(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-istanbul@4.1.6(@voidzero-dev/vite-plus-test@0.1.24))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(better-sqlite3@12.6.2)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.6.2)(kysely@0.28.15))(esbuild@0.27.3)(jiti@2.7.0)(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0) @@ -1201,7 +1201,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-intl: specifier: 'catalog:' version: 4.11.1(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react@19.2.7)(typescript@5.9.3) @@ -1229,7 +1229,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1257,7 +1257,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-view-transitions: specifier: 'catalog:' version: 0.3.5(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1285,7 +1285,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) nuqs: specifier: 'catalog:' version: 2.8.8(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react@19.2.7) @@ -1322,7 +1322,7 @@ importers: version: 1.2.4(@types/react@19.2.16)(react@19.2.7) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) class-variance-authority: specifier: 'catalog:' version: 0.7.1 @@ -1378,7 +1378,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1403,7 +1403,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1428,7 +1428,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1472,7 +1472,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1516,7 +1516,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -4380,8 +4380,8 @@ packages: resolution: {integrity: sha512-hBcWIOppZV14bi+eAmCZj8Elj8hVSUZJTpf1lgGBhVD85pervzQ1poM/qYfFUlPraYSZYP+ASg6To5BwYmUSGQ==} engines: {node: '>=16'} - '@vitejs/plugin-react@https://pkg.pr.new/@vitejs/plugin-react@2ae42d12': - resolution: {integrity: sha512-0KNIsFan88aSruEH1bs+Bk3n3solO57k7AvvtTNCg0rb2Yb4W1Ci+5PrAkp41A/6JXldWvZVBeyQEKCsR0j9iQ==, tarball: https://pkg.pr.new/@vitejs/plugin-react@2ae42d12} + '@vitejs/plugin-react@https://pkg.pr.new/@vitejs/plugin-react@06e58416': + resolution: {integrity: sha512-0KNIsFan88aSruEH1bs+Bk3n3solO57k7AvvtTNCg0rb2Yb4W1Ci+5PrAkp41A/6JXldWvZVBeyQEKCsR0j9iQ==, tarball: https://pkg.pr.new/@vitejs/plugin-react@06e58416} version: 6.0.2 engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: @@ -4394,8 +4394,8 @@ packages: babel-plugin-react-compiler: optional: true - '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12': - resolution: {integrity: sha512-dhd7fBB3vSsIm3JmNZPWZRzrKRCCvvw3cU7TBDHEdQu+L/CBhxLZ7iWWsSd5YDe8AIAblHOr1A7I16jYiIsT7g==, tarball: https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12} + '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@06e58416': + resolution: {integrity: sha512-zHAzr7DvQsfXJQZwxZ0oNNVH3MKrfgYjbt0dNRT8pYxlW4mPGf0B57DhfBSei1PfY/Zr6I43A0TuCyJMTxwmsg==, tarball: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416} version: 0.5.27 peerDependencies: react: '*' @@ -9409,12 +9409,12 @@ snapshots: '@resvg/resvg-wasm': 2.4.0 satori: 0.16.0 - '@vitejs/plugin-react@https://pkg.pr.new/@vitejs/plugin-react@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))': + '@vitejs/plugin-react@https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' - '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': + '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': dependencies: '@rolldown/pluginutils': 1.0.1 es-module-lexer: 2.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 965885840d..95f896c3d0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,11 +39,11 @@ catalog: "@typescript/native-preview": ^7.0.0-dev.20260213.1 "@unpic/react": ^1.0.2 "@vercel/og": ^0.8.6 - "@vitejs/plugin-react": https://pkg.pr.new/@vitejs/plugin-react@2ae42d12 + "@vitejs/plugin-react": https://pkg.pr.new/@vitejs/plugin-react@06e58416 "@next/mdx": 16.2.7 recma-codehike: 0.0.1 remark-codehike: 0.0.1 - "@vitejs/plugin-rsc": https://pkg.pr.new/@vitejs/plugin-rsc@2ae42d12 + "@vitejs/plugin-rsc": https://pkg.pr.new/@vitejs/plugin-rsc@06e58416 "@vitest/coverage-istanbul": 4.1.6 better-auth: ^1.5.6 better-sqlite3: ^12.0.0 From 5ca1ee252a6421244a27e5e207447f0ccd852809 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 12 Jun 2026 03:35:48 +0100 Subject: [PATCH 14/33] fix(cache): stabilize directive reference tests --- pnpm-lock.yaml | 102 ++++++++++++------------- pnpm-workspace.yaml | 4 +- tests/e2e/app-router/use-cache.spec.ts | 30 ++++++-- 3 files changed, 75 insertions(+), 61 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49b3d043d0..f18a38b7f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,10 +88,10 @@ catalogs: specifier: ^0.8.6 version: 0.8.6 '@vitejs/plugin-react': - specifier: https://pkg.pr.new/@vitejs/plugin-react@06e58416 + specifier: https://pkg.pr.new/@vitejs/plugin-react@82d2c578 version: 6.0.2 '@vitejs/plugin-rsc': - specifier: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416 + specifier: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578 version: 0.5.27 '@vitest/coverage-istanbul': specifier: 4.1.6 @@ -277,7 +277,7 @@ importers: version: 7.0.0-dev.20260217.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.6(@voidzero-dev/vite-plus-test@0.1.24) @@ -362,10 +362,10 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) drizzle-kit: specifier: 'catalog:' version: 0.31.10 @@ -392,7 +392,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -408,7 +408,7 @@ importers: devDependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) vite: specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' @@ -423,10 +423,10 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -457,7 +457,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) nitro: specifier: 'catalog:' version: nitro-nightly@3.0.1-20260512-093145-0498ce70(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(better-sqlite3@12.6.2)(chokidar@5.0.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.6.2)(kysely@0.28.15))(jiti@2.7.0)(miniflare@4.20260401.0) @@ -494,7 +494,7 @@ importers: version: 2.0.13 '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) clsx: specifier: 'catalog:' version: 2.1.1 @@ -558,7 +558,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -594,10 +594,10 @@ importers: version: 4.2.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -677,10 +677,10 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -710,10 +710,10 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) ms: specifier: 'catalog:' version: 3.0.0-canary.1 @@ -753,7 +753,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -784,7 +784,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) typescript: specifier: 'catalog:' version: 5.9.3 @@ -805,7 +805,7 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -830,7 +830,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -873,10 +873,10 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -919,10 +919,10 @@ importers: version: link:../../packages/cloudflare '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1005,10 +1005,10 @@ importers: version: link:../cloudflare '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react-server-dom-webpack: specifier: 'catalog:' version: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1020,7 +1020,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) fake-context-lib: specifier: file:./__test_packages__/fake-context-lib version: file:tests/fixtures/app-basic/__test_packages__/fake-context-lib @@ -1054,7 +1054,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1079,7 +1079,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1104,7 +1104,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1132,10 +1132,10 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1170,7 +1170,7 @@ importers: version: 1.9.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) better-auth: specifier: 'catalog:' version: 1.5.6(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-istanbul@4.1.6(@voidzero-dev/vite-plus-test@0.1.24))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(better-sqlite3@12.6.2)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.6.2)(kysely@0.28.15))(esbuild@0.27.3)(jiti@2.7.0)(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0) @@ -1201,7 +1201,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-intl: specifier: 'catalog:' version: 4.11.1(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react@19.2.7)(typescript@5.9.3) @@ -1229,7 +1229,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1257,7 +1257,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-view-transitions: specifier: 'catalog:' version: 0.3.5(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1285,7 +1285,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) nuqs: specifier: 'catalog:' version: 2.8.8(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react@19.2.7) @@ -1322,7 +1322,7 @@ importers: version: 1.2.4(@types/react@19.2.16)(react@19.2.7) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) class-variance-authority: specifier: 'catalog:' version: 0.7.1 @@ -1378,7 +1378,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1403,7 +1403,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1428,7 +1428,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1472,7 +1472,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1516,7 +1516,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -4380,8 +4380,8 @@ packages: resolution: {integrity: sha512-hBcWIOppZV14bi+eAmCZj8Elj8hVSUZJTpf1lgGBhVD85pervzQ1poM/qYfFUlPraYSZYP+ASg6To5BwYmUSGQ==} engines: {node: '>=16'} - '@vitejs/plugin-react@https://pkg.pr.new/@vitejs/plugin-react@06e58416': - resolution: {integrity: sha512-0KNIsFan88aSruEH1bs+Bk3n3solO57k7AvvtTNCg0rb2Yb4W1Ci+5PrAkp41A/6JXldWvZVBeyQEKCsR0j9iQ==, tarball: https://pkg.pr.new/@vitejs/plugin-react@06e58416} + '@vitejs/plugin-react@https://pkg.pr.new/@vitejs/plugin-react@82d2c578': + resolution: {integrity: sha512-0KNIsFan88aSruEH1bs+Bk3n3solO57k7AvvtTNCg0rb2Yb4W1Ci+5PrAkp41A/6JXldWvZVBeyQEKCsR0j9iQ==, tarball: https://pkg.pr.new/@vitejs/plugin-react@82d2c578} version: 6.0.2 engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: @@ -4394,8 +4394,8 @@ packages: babel-plugin-react-compiler: optional: true - '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@06e58416': - resolution: {integrity: sha512-zHAzr7DvQsfXJQZwxZ0oNNVH3MKrfgYjbt0dNRT8pYxlW4mPGf0B57DhfBSei1PfY/Zr6I43A0TuCyJMTxwmsg==, tarball: https://pkg.pr.new/@vitejs/plugin-rsc@06e58416} + '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578': + resolution: {integrity: sha512-vos4mwhdaMBHkA1HND+rhs0GWlKTKnP0XrmADiBWRb3LmHdauz+KBQaNKuKuCEHcKkkpI56WZ8MCi8otkJtoIg==, tarball: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578} version: 0.5.27 peerDependencies: react: '*' @@ -9409,12 +9409,12 @@ snapshots: '@resvg/resvg-wasm': 2.4.0 satori: 0.16.0 - '@vitejs/plugin-react@https://pkg.pr.new/@vitejs/plugin-react@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))': + '@vitejs/plugin-react@https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' - '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@06e58416(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': + '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': dependencies: '@rolldown/pluginutils': 1.0.1 es-module-lexer: 2.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 95f896c3d0..3458c4433e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,11 +39,11 @@ catalog: "@typescript/native-preview": ^7.0.0-dev.20260213.1 "@unpic/react": ^1.0.2 "@vercel/og": ^0.8.6 - "@vitejs/plugin-react": https://pkg.pr.new/@vitejs/plugin-react@06e58416 + "@vitejs/plugin-react": https://pkg.pr.new/@vitejs/plugin-react@82d2c578 "@next/mdx": 16.2.7 recma-codehike: 0.0.1 remark-codehike: 0.0.1 - "@vitejs/plugin-rsc": https://pkg.pr.new/@vitejs/plugin-rsc@06e58416 + "@vitejs/plugin-rsc": https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578 "@vitest/coverage-istanbul": 4.1.6 better-auth: ^1.5.6 better-sqlite3: ^12.0.0 diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index 1a36f7164b..f3b1ecf238 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { test, expect, type APIRequestContext } from "@playwright/test"; import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; @@ -20,12 +20,24 @@ export async function getMode() { } `; -async function writeUseCacheHmrActions(content: string) { - if ((await readFile(USE_CACHE_HMR_ACTIONS_FILE, "utf8")) !== content) { - await writeFile(USE_CACHE_HMR_ACTIONS_FILE, content); +async function writeUseCacheHmrActions(content: string, forceUpdate = false) { + const nextContent = forceUpdate ? `${content}// hmr-update:${Date.now()}\n` : content; + if ((await readFile(USE_CACHE_HMR_ACTIONS_FILE, "utf8")) !== nextContent) { + await writeFile(USE_CACHE_HMR_ACTIONS_FILE, nextContent); } } +async function waitForUseCacheHmrTransform(request: APIRequestContext) { + await expect + .poll(async () => { + const response = await request.get( + `${BASE}/app/use-cache-hmr/actions.ts?t=${Date.now()}`, + ); + return response.ok(); + }) + .toBe(true); +} + test.describe('"use cache" file-level directive', () => { test("use-cache page renders correctly", async ({ page }) => { await page.goto(`${BASE}/use-cache-test`); @@ -151,8 +163,8 @@ test.describe('"use cache" transform coverage', () => { }).toPass({ timeout: 15_000 }); }); - test("removes and restores directive metadata during HMR", async ({ page }) => { - await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED); + test("removes and restores directive metadata during HMR", async ({ page, request }) => { + await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED, true); try { await page.goto(`${BASE}/use-cache-hmr`); await expect(async () => { @@ -162,7 +174,8 @@ test.describe('"use cache" transform coverage', () => { }); }).toPass({ timeout: 15_000 }); - await writeUseCacheHmrActions(USE_CACHE_HMR_PLAIN); + await writeUseCacheHmrActions(USE_CACHE_HMR_PLAIN, true); + await waitForUseCacheHmrTransform(request); await expect(async () => { await page.reload(); await page.locator("#call-use-cache-hmr").click(); @@ -171,7 +184,8 @@ test.describe('"use cache" transform coverage', () => { }); }).toPass({ timeout: 15_000 }); - await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED); + await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED, true); + await waitForUseCacheHmrTransform(request); await expect(async () => { await page.reload(); await page.locator("#call-use-cache-hmr").click(); From 9f4b6ee48a36c538deb0140d5aef818265a2d13a Mon Sep 17 00:00:00 2001 From: James Date: Fri, 12 Jun 2026 03:38:44 +0100 Subject: [PATCH 15/33] style(cache): format HMR test --- tests/e2e/app-router/use-cache.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index f3b1ecf238..44fd145187 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -30,9 +30,7 @@ async function writeUseCacheHmrActions(content: string, forceUpdate = false) { async function waitForUseCacheHmrTransform(request: APIRequestContext) { await expect .poll(async () => { - const response = await request.get( - `${BASE}/app/use-cache-hmr/actions.ts?t=${Date.now()}`, - ); + const response = await request.get(`${BASE}/app/use-cache-hmr/actions.ts?t=${Date.now()}`); return response.ok(); }) .toBe(true); From 0da1dc9544b4e1ee3c3aeadfe9c53ddf0bfe3d92 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 18 Jun 2026 19:16:45 +0100 Subject: [PATCH 16/33] refactor(use-cache): move server function directives to user land --- packages/vinext/src/index.ts | 40 +- .../src/plugins/use-cache-server-functions.ts | 441 ++++++++++++++++++ tests/app-router-production-server.test.ts | 7 + tests/e2e/app-router-prod/use-cache.spec.ts | 8 + tests/use-cache-transform.test.ts | 121 ++++- 5 files changed, 575 insertions(+), 42 deletions(-) create mode 100644 packages/vinext/src/plugins/use-cache-server-functions.ts diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 08d2e23a0c..59bf70d879 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -6,6 +6,7 @@ import type { UserConfig, ViteDevServer, } from "vite"; +import type { ServerFunctionDirectiveContext } from "@vitejs/plugin-rsc/plugin"; import { loadEnv, parseAst, transformWithOxc } from "vite"; import { pagesRouter, @@ -117,6 +118,7 @@ import { createMiddlewareServerOnlyPlugin } from "./plugins/middleware-server-on import { createOptimizeImportsPlugin } from "./plugins/optimize-imports.js"; import { createDynamicPreloadMetadataPlugin } from "./plugins/dynamic-preload-metadata.js"; import { createOgInlineFetchAssetsPlugin, createOgAssetsPlugin } from "./plugins/og-assets.js"; +import { createUseCacheServerFunctionPlugins } from "./plugins/use-cache-server-functions.js"; import { generateRouteTypes } from "./typegen.js"; import { mergeOptimizeDepsExclude, @@ -187,16 +189,6 @@ import { randomBytes, randomUUID } from "node:crypto"; import commonjs from "vite-plugin-commonjs"; import { normalizePathSeparators, stripViteModuleQuery } from "./utils/path.js"; -type ServerFunctionDirectiveContext = { - value: string; - name: string; - id: string; - directiveMatch: RegExpMatchArray; - location: "inline" | "module"; - parameters?: { count: number; hasRest: boolean }; - runtime?: string; -}; - function parseUseCacheVariant(directive: string): string { return directive === "use cache" ? "" @@ -943,15 +935,18 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } const rscImport = import(pathToFileURL(resolvedRscPath).href); rscPluginPromise = rscImport - .then((mod) => { + .then(async (mod) => { const rsc = mod.default; - return rsc({ + const plugins: Plugin[] = rsc({ entries: { rsc: VIRTUAL_RSC_ENTRY, ssr: VIRTUAL_APP_SSR_ENTRY, client: VIRTUAL_APP_BROWSER_ENTRY, }, - serverFunctionDirectives: [ + }); + const [serverFunctionPlugin, metadataPlugin] = await createUseCacheServerFunctionPlugins({ + projectRoot: earlyBaseDir, + definitions: [ { directive: /^use cache.*$/, test: (code: string) => code.includes("use cache"), @@ -1002,15 +997,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { : ""; return `${runtime}.registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)}${pageOptions})`; }, - filterExport: ({ - name, - id, - meta, - }: { - name: string; - id: string; - meta: { isFunction?: boolean }; - }) => { + filterExport: ({ name, id, meta }) => { if (meta.isFunction === false) return false; if (/\/(layout|template)\.(tsx?|jsx?|mjs)$/.test(id) && name === "default") { return false; @@ -1019,7 +1006,16 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { }, }, ], + serverEnvironmentName: "rsc", + browserEnvironmentName: "client", }); + const useServerIndex = plugins.findIndex((plugin) => plugin.name === "rsc:use-server"); + if (useServerIndex === -1 || !serverFunctionPlugin || !metadataPlugin) { + throw new Error("vinext: Failed to locate @vitejs/plugin-rsc use-server plugin."); + } + plugins.splice(useServerIndex, 0, serverFunctionPlugin); + plugins.splice(useServerIndex + 2, 0, metadataPlugin); + return plugins; }) .catch((cause) => { throw new Error("vinext: Failed to load @vitejs/plugin-rsc.", { diff --git a/packages/vinext/src/plugins/use-cache-server-functions.ts b/packages/vinext/src/plugins/use-cache-server-functions.ts new file mode 100644 index 0000000000..463345ccef --- /dev/null +++ b/packages/vinext/src/plugins/use-cache-server-functions.ts @@ -0,0 +1,441 @@ +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import type { RscPluginManager, ServerFunctionDirective } from "@vitejs/plugin-rsc/plugin"; +import type { SourceMap } from "magic-string"; +import type { Plugin, Rollup, ViteDevServer } from "vite"; +import { parseAstAsync, transformWithOxc } from "vite"; +import { isUnknownRecord } from "../utils/record.js"; +import { escapeRegExp } from "../utils/regex.js"; + +type RscTransforms = typeof import("@vitejs/plugin-rsc/transforms"); +type Program = Parameters[0]; +type ModuleDirective = NonNullable< + Parameters[2]["moduleDirective"] +> & { start?: number }; +type StringDirective = ModuleDirective & { type: "Literal"; value: string }; + +type Options = { + projectRoot: string; + definitions: ServerFunctionDirective[]; + serverEnvironmentName: string; + browserEnvironmentName: string; +}; + +type OwnedServerReference = { + referenceKey: string; + exportNames: string[]; +}; + +const SERVER_FUNCTION_DIRECTIVE_MARKER = "/* __vinext_server_function_directives__ */"; + +function resolvePluginRscModule(projectRoot: string, specifier: string): string { + try { + return createRequire(path.join(projectRoot, "package.json")).resolve(specifier); + } catch {} + + try { + return createRequire(import.meta.url).resolve(specifier); + } catch { + throw new Error(`vinext: Installed @vitejs/plugin-rsc does not expose ${specifier}.`); + } +} + +async function parseProgram(code: string): Promise { + return (await parseAstAsync(code)) as unknown as Program; +} + +function matchDirective(value: string, directive: string | RegExp): RegExpMatchArray | undefined { + const pattern = + typeof directive === "string" + ? new RegExp(`^${escapeRegExp(directive)}$`) + : new RegExp(directive.source, directive.flags); + pattern.lastIndex = 0; + return value.match(pattern) ?? undefined; +} + +function isStringLiteral(value: unknown): value is StringDirective { + return isUnknownRecord(value) && value.type === "Literal" && typeof value.value === "string"; +} + +function isExpressionStatement( + value: unknown, +): value is Record & { type: "ExpressionStatement"; expression: unknown } { + return isUnknownRecord(value) && value.type === "ExpressionStatement" && "expression" in value; +} + +function isBlockStatement( + value: unknown, +): value is Record & { type: "BlockStatement"; body: unknown[] } { + return isUnknownRecord(value) && value.type === "BlockStatement" && Array.isArray(value.body); +} + +function findModuleDirective( + ast: Program, + directive: string | RegExp, +): StringDirective | undefined { + for (const node of ast.body) { + if (node.type !== "ExpressionStatement") continue; + if (isStringLiteral(node.expression) && matchDirective(node.expression.value, directive)) { + return node.expression; + } + } +} + +function findInlineDirective( + ast: Program, + directive: string | RegExp, +): StringDirective | undefined { + let result: StringDirective | undefined; + + function visit(value: unknown): void { + if (result) return; + if (Array.isArray(value)) { + for (const child of value) visit(child); + return; + } + if (!isUnknownRecord(value)) return; + + const nodeType = typeof value.type === "string" ? value.type : undefined; + if ( + (nodeType === "FunctionDeclaration" || + nodeType === "FunctionExpression" || + nodeType === "ArrowFunctionExpression") && + isBlockStatement(value.body) + ) { + for (const statement of value.body.body) { + if ( + isExpressionStatement(statement) && + isStringLiteral(statement.expression) && + matchDirective(statement.expression.value, directive) + ) { + result = statement.expression; + return; + } + } + } + + for (const [key, child] of Object.entries(value)) { + if (key === "parent" || key === "loc" || key === "start" || key === "end") continue; + visit(child); + } + } + + visit(ast); + return result; +} + +function hashString(value: string): string { + return createHash("sha256").update(value).digest("hex").slice(0, 12); +} + +function normalizeViteImportAnalysisUrl( + environment: ViteDevServer["environments"][string], + id: string, +): string { + const root = environment.config.root; + const rootPrefix = root.endsWith("/") ? root : `${root}/`; + if (id.startsWith(rootPrefix)) return id.slice(root.length); + + const cleanId = id.split("?", 1)[0] ?? id; + if (path.isAbsolute(cleanId) && fs.existsSync(cleanId)) return path.posix.join("/@fs/", id); + if (id.startsWith(".") || id.startsWith("/")) return id; + return `/@id/${id.replace("\0", "__x00__")}`; +} + +async function expandExportAll( + transforms: RscTransforms, + context: Rollup.TransformPluginContext, + code: string, + ast: Program, + id: string, +): Promise<{ code: string } | undefined> { + return transforms.transformExpandExportAll({ + code, + ast, + importer: id, + resolve: async (source, importer) => (await context.resolve(source, importer))?.id, + load: async (resolvedId) => { + const source = await fs.promises.readFile(resolvedId, "utf8"); + const transformed = await transformWithOxc(source, resolvedId, { sourcemap: false }); + return parseProgram(transformed.code); + }, + }); +} + +export async function createUseCacheServerFunctionPlugins(options: Options): Promise { + const rscModulePath = resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc"); + const transformsPath = resolvePluginRscModule( + options.projectRoot, + "@vitejs/plugin-rsc/transforms", + ); + const rscRuntime = pathToFileURL( + resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc/react/rsc"), + ).href; + const browserRuntime = pathToFileURL( + resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc/react/browser"), + ).href; + const ssrRuntime = pathToFileURL( + resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc/react/ssr"), + ).href; + const encryptionRuntime = pathToFileURL( + resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc/utils/encryption-runtime"), + ).href; + const rscModule: typeof import("@vitejs/plugin-rsc") = await import( + pathToFileURL(rscModulePath).href + ); + const transforms: RscTransforms = await import(pathToFileURL(transformsPath).href); + const { getPluginApi } = rscModule; + let manager: RscPluginManager | undefined; + const ownedReferences = new Map(); + + const transformPlugin: Plugin = { + name: "vinext:use-cache-server-functions", + + configResolved(config) { + manager = getPluginApi(config)?.manager; + }, + + transform: { + async handler(code, id) { + if (code.includes(SERVER_FUNCTION_DIRECTIVE_MARKER)) return; + + const active = options.definitions.filter( + (definition) => + (definition.test?.(code) ?? code.includes("use ")) && + (!definition.filter || definition.filter(id)), + ); + const isServer = this.environment.name === options.serverEnvironmentName; + if (active.length === 0) { + if (isServer) { + ownedReferences.delete(id); + if (manager) delete manager.serverReferenceMetaMap[id]; + } + return; + } + if (!manager) { + throw new Error("vinext: failed to access @vitejs/plugin-rsc through getPluginApi()."); + } + + let ast = await parseProgram(code); + const useServerBoundary = transforms.hasDirective(ast.body, "use server"); + if (!isServer && useServerBoundary) return; + + const normalizedId = + manager.config.command === "build" + ? hashString(manager.toRelativeId(id)) + : normalizeViteImportAnalysisUrl( + manager.server.environments[options.serverEnvironmentName], + id, + ); + + if (!isServer) { + for (const definition of active) { + const inlineDirective = findInlineDirective(ast, definition.directive); + if (inlineDirective && definition.clientError) { + throw Object.assign( + new Error(definition.clientError({ id, environment: this.environment.name })), + { pos: inlineDirective.start }, + ); + } + } + + const matches: Array = []; + for (const definition of active) { + const moduleDirective = findModuleDirective(ast, definition.directive); + if (moduleDirective) matches.push([definition, moduleDirective]); + } + if (matches.length === 0) return; + if (matches.length > 1) { + throw Object.assign( + new Error("Multiple server function directives match this module."), + { + pos: matches[1]?.[1].start, + }, + ); + } + + const match = matches[0]; + if (!match) return; + const [, moduleDirective] = match; + const result = transforms.transformDirectiveProxyExport(ast, { + code, + directive: moduleDirective.value, + runtime: (name) => + `$$ReactClient.createServerReference(${JSON.stringify(`${normalizedId}#${name}`)},$$ReactClient.callServer,undefined,${this.environment.mode === "dev" ? "$$ReactClient.findSourceMapURL" : "undefined"},${JSON.stringify(name)})`, + }); + if (!result?.output.hasChanged()) return; + const ownedReference = { + referenceKey: normalizedId, + exportNames: result.exportNames, + }; + ownedReferences.set(id, ownedReference); + manager.serverReferenceMetaMap[id] = { + importId: id, + ...ownedReference, + }; + result.output.prepend( + `${SERVER_FUNCTION_DIRECTIVE_MARKER}\nimport * as $$ReactClient from ${JSON.stringify(this.environment.name === options.browserEnvironmentName ? browserRuntime : ssrRuntime)};\n`, + ); + return { + code: result.output.toString(), + map: result.output.generateMap({ hires: "boundary", source: id }), + }; + } + + const exportNames = new Set(); + let needsReactRuntime = false; + let needsEncryptionRuntime = false; + let outputMap: SourceMap | undefined; + + for (const definition of active) { + const runtimeName = definition.runtime + ? `$$server_function_directive_${hashString(definition.runtime)}` + : undefined; + let runtimeUsed = false; + const getRuntime = () => { + if (runtimeName) runtimeUsed = true; + return runtimeName; + }; + + let moduleDirective = findModuleDirective(ast, definition.directive); + if (moduleDirective) { + if (useServerBoundary) { + throw Object.assign( + new Error( + `A module cannot contain both ${JSON.stringify(moduleDirective.value)} and "use server" directives.`, + ), + { pos: moduleDirective.start }, + ); + } + const expanded = await expandExportAll(transforms, this, code, ast, id); + if (expanded) { + code = expanded.code; + ast = await parseProgram(code); + moduleDirective = findModuleDirective(ast, definition.directive); + } + } + + const moduleMatch = moduleDirective + ? matchDirective(moduleDirective.value, definition.directive) + : undefined; + if (moduleMatch) { + definition.validate?.({ id, directive: moduleMatch[0], location: "module" }); + } + + const result = transforms.transformServerActionServer(code, ast, { + runtime: (value, name) => + `$$ReactServer.registerServerReference(${value}, ${JSON.stringify(normalizedId)}, ${JSON.stringify(name)})`, + directive: definition.directive, + moduleDirective, + moduleRuntime: (value, name, meta) => { + if (!moduleMatch) return value; + needsReactRuntime = true; + return `$$ReactServer.registerServerReference(${definition.wrap({ value, name, id, directiveMatch: moduleMatch, location: "module", hasBoundArgs: false, parameters: meta.parameters, runtime: getRuntime(), meta })}, ${JSON.stringify(normalizedId)}, ${JSON.stringify(name)})`; + }, + inlineRuntime: (value, name, meta) => { + definition.validate?.({ + id, + directive: meta.directiveMatch[0], + location: "inline", + }); + const wrapped = definition.wrap({ + value, + name, + id, + directiveMatch: meta.directiveMatch, + location: "inline", + hasBoundArgs: meta.hasBoundArgs, + parameters: meta.parameters, + runtime: getRuntime(), + }); + if (useServerBoundary) return wrapped; + + needsReactRuntime = true; + if (meta.hasBoundArgs) { + needsEncryptionRuntime = true; + return `$$ReactServer.registerServerReference((($$wrapped) => async ($$encoded, ...$$args) => $$wrapped(...await __vite_rsc_encryption_runtime.decryptActionBoundArgs($$encoded), ...$$args))(${wrapped}), ${JSON.stringify(normalizedId)}, ${JSON.stringify(name)})`; + } + return `$$ReactServer.registerServerReference(${wrapped}, ${JSON.stringify(normalizedId)}, ${JSON.stringify(name)})`; + }, + filter: (name, meta) => definition.filterExport?.({ name, id, meta }) ?? true, + rejectNonAsyncFunction: definition.rejectNonAsyncFunction, + rejectNonAsyncModule: definition.rejectNonAsyncModule, + encode: (value) => { + needsEncryptionRuntime = true; + return `__vite_rsc_encryption_runtime.encryptActionBoundArgs(${value})`; + }, + stableName: true, + exportWrappedHoist: !useServerBoundary, + detectUseServerModule: false, + rejectForbiddenExpressions: true, + }); + if (!result.output.hasChanged()) continue; + + if (runtimeUsed && definition.runtime && runtimeName) { + result.output.prepend( + `import * as ${runtimeName} from ${JSON.stringify(definition.runtime)};\n`, + ); + } + + const transformedNames = "names" in result ? result.names : result.exportNames; + transformedNames.forEach((name) => exportNames.add(name)); + outputMap = result.output.generateMap({ hires: "boundary", source: id }); + code = result.output.toString(); + ast = await parseProgram(code); + } + + if (!useServerBoundary) { + if (exportNames.size === 0) { + ownedReferences.delete(id); + delete manager.serverReferenceMetaMap[id]; + } else { + const ownedReference = { + referenceKey: normalizedId, + exportNames: [...exportNames], + }; + ownedReferences.set(id, ownedReference); + manager.serverReferenceMetaMap[id] = { + importId: id, + ...ownedReference, + }; + } + } + + const imports = [ + needsReactRuntime && `import * as $$ReactServer from ${JSON.stringify(rscRuntime)};`, + needsEncryptionRuntime && + `import * as __vite_rsc_encryption_runtime from ${JSON.stringify(encryptionRuntime)};`, + ].filter(Boolean); + return { + code: `${SERVER_FUNCTION_DIRECTIVE_MARKER}\n${imports.join("\n")}\n${code}`, + map: outputMap, + }; + }, + }, + }; + + const metadataPlugin: Plugin = { + name: "vinext:use-cache-server-function-metadata", + transform: { + handler(_code, id) { + if (!manager) return; + const ownedReference = ownedReferences.get(id); + if (!ownedReference) return; + + const existing = manager.serverReferenceMetaMap[id]; + manager.serverReferenceMetaMap[id] = { + importId: id, + referenceKey: ownedReference.referenceKey, + exportNames: existing + ? [...new Set([...existing.exportNames, ...ownedReference.exportNames])] + : ownedReference.exportNames, + }; + }, + }, + }; + + return [transformPlugin, metadataPlugin]; +} diff --git a/tests/app-router-production-server.test.ts b/tests/app-router-production-server.test.ts index 9b4ca4725a..84f2ddb0ea 100644 --- a/tests/app-router-production-server.test.ts +++ b/tests/app-router-production-server.test.ts @@ -1452,6 +1452,13 @@ describe("App Router Production server (startProdServer)", () => { expect(res.status).toBe(200); const html = await res.text(); + // React Flight encodes server-function props with the `$h` token used by + // this React build's SERVER_DECODE_REFERENCE_PREFIX path. Keep this + // assertion next to the action round-trip so the test proves both halves: + // the payload uses the server-reference encoding and the decoded reference + // resolves through vinext's production manifest below. + expect(html).toContain('\\"getDate\\":\\"$h'); + // The flight payload embeds each cached function prop as a server // reference whose id is "<12-hex normalised key>#". const refIds = [ diff --git a/tests/e2e/app-router-prod/use-cache.spec.ts b/tests/e2e/app-router-prod/use-cache.spec.ts index 627858bdd1..ef915601d8 100644 --- a/tests/e2e/app-router-prod/use-cache.spec.ts +++ b/tests/e2e/app-router-prod/use-cache.spec.ts @@ -1,6 +1,14 @@ import { expect, test } from "@playwright/test"; test.describe('production "use cache" server function references', () => { + test("invokes file-level cached exports imported by a Client Component", async ({ page }) => { + await page.goto("/use-cache-client-import"); + await page.locator("#call-client-imported-cache").click(); + await expect(page.getByTestId("client-imported-cache-result")).toHaveText( + /^client-cache:direct:[0-9.e+-]+$/, + ); + }); + test("replays cached RSC through SSR and invokes nested functions from the browser", async ({ page, }) => { diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index 1b2455caf7..5c6a20a3c1 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -1,6 +1,6 @@ /** - * Tests the plugin-rsc serverFunctionDirectives integration used for function-level - * "use cache" directives. Vinext supplies cache wrapper expressions; plugin-rsc + * Tests the vinext user-land server function directive integration used for function-level + * "use cache" directives. Vinext owns the directive plugin while plugin-rsc * owns directive discovery, closure hoisting, encryption, reference ids, and * server-reference manifest metadata. */ @@ -47,16 +47,97 @@ async function configurePluginRsc(plugins: Plugin[]) { rsc: { build: { outDir: path.join(APP_FIXTURE_DIR, "dist/rsc") } }, }, }); + const useCachePlugin = plugins.find( + (plugin) => plugin.name === "vinext:use-cache-server-functions", + )!; + unwrapHook(useCachePlugin.configResolved)!.call(useCachePlugin, { plugins }); // oxlint-disable-next-line typescript/no-explicit-any return (minimal as any).api.manager; } describe("plugin-rsc inline use-cache references", () => { + it("restores user-land reference metadata after rsc:use-server clears it", async () => { + const plugins = await getPlugins(); + const manager = await configurePluginRsc(plugins); + const useCacheIndex = plugins.findIndex( + (candidate) => candidate.name === "vinext:use-cache-server-functions", + ); + const useServerIndex = plugins.findIndex((candidate) => candidate.name === "rsc:use-server"); + const metadataIndex = plugins.findIndex( + (candidate) => candidate.name === "vinext:use-cache-server-function-metadata", + ); + const manifestIndex = plugins.findIndex( + (candidate) => candidate.name === "rsc:virtual-vite-rsc/server-references", + ); + expect(useCacheIndex).toBeLessThan(useServerIndex); + expect(metadataIndex).toBeGreaterThan(useServerIndex); + expect(metadataIndex).toBeLessThan(manifestIndex); + + const context = { environment: { name: "rsc", mode: "build" } }; + const transformed = await unwrapHook(plugins[useCacheIndex]!.transform)!.call( + context, + inlineCacheCode, + moduleId, + ); + expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); + + await unwrapHook(plugins[useServerIndex]!.transform)!.call( + context, + transformed!.code, + moduleId, + ); + expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); + + unwrapHook(plugins[metadataIndex]!.transform)!.call(context, transformed!.code, moduleId); + expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); + + const ssrContext = { environment: { name: "ssr", mode: "build" } }; + const proxied = await unwrapHook(plugins[useCacheIndex]!.transform)!.call( + ssrContext, + fileCacheCode, + moduleId, + ); + await unwrapHook(plugins[useServerIndex]!.transform)!.call(ssrContext, proxied!.code, moduleId); + expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); + + unwrapHook(plugins[metadataIndex]!.transform)!.call(ssrContext, proxied!.code, moduleId); + expect(manager.serverReferenceMetaMap[moduleId]).toMatchObject({ + importId: moduleId, + exportNames: ["getData"], + }); + }); + + it("matches Vite's dev reference key for files outside the project root", async () => { + const plugins = await getPlugins(); + const manager = await configurePluginRsc(plugins); + manager.config.command = "serve"; + manager.server = { + environments: { + rsc: { + config: { root: APP_FIXTURE_DIR }, + moduleGraph: { getModuleById: () => undefined }, + }, + }, + }; + const plugin = plugins.find( + (candidate) => candidate.name === "vinext:use-cache-server-functions", + )!; + const externalId = import.meta.filename; + const result = await unwrapHook(plugin.transform)!.call( + { environment: { name: "rsc", mode: "dev" } }, + inlineCacheCode, + externalId, + ); + const expectedKey = path.posix.join("/@fs/", externalId); + expect(result!.code).toContain(JSON.stringify(expectedKey)); + expect(manager.serverReferenceMetaMap[externalId].referenceKey).toBe(expectedKey); + }); + it("wraps and registers inline cache functions with plugin-rsc's build reference key", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -84,7 +165,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; const original = await transform.call( @@ -109,7 +190,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; await transform.call( @@ -130,7 +211,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; const closureCode = [ @@ -164,7 +245,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; @@ -182,7 +263,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -197,7 +278,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -212,7 +293,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -227,7 +308,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -256,7 +337,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; await expect( @@ -274,7 +355,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; await expect( @@ -315,7 +396,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -331,7 +412,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; await expect( @@ -349,7 +430,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const context = { environment: { name: "rsc", mode: "build" } }; const source = [ @@ -368,7 +449,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; await expect( @@ -384,7 +465,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const result = await unwrapHook(plugin.transform)!.call( { environment: { name: "rsc", mode: "build" } }, @@ -398,7 +479,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -419,7 +500,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "rsc:server-function-directives", + (candidate) => candidate.name === "vinext:use-cache-server-functions", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( From 6d2d5e95fef025aed7d66536609dbe8b366e0f8e Mon Sep 17 00:00:00 2001 From: James Date: Fri, 19 Jun 2026 00:01:40 +0100 Subject: [PATCH 17/33] refactor(use-cache): clarify generic directive plugin naming --- packages/vinext/src/index.ts | 4 +- ...tions.ts => server-function-directives.ts} | 6 +-- tests/use-cache-transform.test.ts | 44 +++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) rename packages/vinext/src/plugins/{use-cache-server-functions.ts => server-function-directives.ts} (98%) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 59bf70d879..f64ca377af 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -118,7 +118,7 @@ import { createMiddlewareServerOnlyPlugin } from "./plugins/middleware-server-on import { createOptimizeImportsPlugin } from "./plugins/optimize-imports.js"; import { createDynamicPreloadMetadataPlugin } from "./plugins/dynamic-preload-metadata.js"; import { createOgInlineFetchAssetsPlugin, createOgAssetsPlugin } from "./plugins/og-assets.js"; -import { createUseCacheServerFunctionPlugins } from "./plugins/use-cache-server-functions.js"; +import { createServerFunctionDirectivePlugins } from "./plugins/server-function-directives.js"; import { generateRouteTypes } from "./typegen.js"; import { mergeOptimizeDepsExclude, @@ -944,7 +944,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { client: VIRTUAL_APP_BROWSER_ENTRY, }, }); - const [serverFunctionPlugin, metadataPlugin] = await createUseCacheServerFunctionPlugins({ + const [serverFunctionPlugin, metadataPlugin] = await createServerFunctionDirectivePlugins({ projectRoot: earlyBaseDir, definitions: [ { diff --git a/packages/vinext/src/plugins/use-cache-server-functions.ts b/packages/vinext/src/plugins/server-function-directives.ts similarity index 98% rename from packages/vinext/src/plugins/use-cache-server-functions.ts rename to packages/vinext/src/plugins/server-function-directives.ts index 463345ccef..bfc65bdc70 100644 --- a/packages/vinext/src/plugins/use-cache-server-functions.ts +++ b/packages/vinext/src/plugins/server-function-directives.ts @@ -165,7 +165,7 @@ async function expandExportAll( }); } -export async function createUseCacheServerFunctionPlugins(options: Options): Promise { +export async function createServerFunctionDirectivePlugins(options: Options): Promise { const rscModulePath = resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc"); const transformsPath = resolvePluginRscModule( options.projectRoot, @@ -192,7 +192,7 @@ export async function createUseCacheServerFunctionPlugins(options: Options): Pro const ownedReferences = new Map(); const transformPlugin: Plugin = { - name: "vinext:use-cache-server-functions", + name: "vinext:server-function-directives", configResolved(config) { manager = getPluginApi(config)?.manager; @@ -418,7 +418,7 @@ export async function createUseCacheServerFunctionPlugins(options: Options): Pro }; const metadataPlugin: Plugin = { - name: "vinext:use-cache-server-function-metadata", + name: "vinext:server-function-directive-metadata", transform: { handler(_code, id) { if (!manager) return; diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index 5c6a20a3c1..585fe49ba5 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -48,7 +48,7 @@ async function configurePluginRsc(plugins: Plugin[]) { }, }); const useCachePlugin = plugins.find( - (plugin) => plugin.name === "vinext:use-cache-server-functions", + (plugin) => plugin.name === "vinext:server-function-directives", )!; unwrapHook(useCachePlugin.configResolved)!.call(useCachePlugin, { plugins }); // oxlint-disable-next-line typescript/no-explicit-any @@ -60,11 +60,11 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const useCacheIndex = plugins.findIndex( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", ); const useServerIndex = plugins.findIndex((candidate) => candidate.name === "rsc:use-server"); const metadataIndex = plugins.findIndex( - (candidate) => candidate.name === "vinext:use-cache-server-function-metadata", + (candidate) => candidate.name === "vinext:server-function-directive-metadata", ); const manifestIndex = plugins.findIndex( (candidate) => candidate.name === "rsc:virtual-vite-rsc/server-references", @@ -120,7 +120,7 @@ describe("plugin-rsc inline use-cache references", () => { }, }; const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const externalId = import.meta.filename; const result = await unwrapHook(plugin.transform)!.call( @@ -137,7 +137,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -165,7 +165,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; const original = await transform.call( @@ -190,7 +190,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; await transform.call( @@ -211,7 +211,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; const closureCode = [ @@ -245,7 +245,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; @@ -263,7 +263,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -278,7 +278,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -293,7 +293,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -308,7 +308,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -337,7 +337,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; await expect( @@ -355,7 +355,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; await expect( @@ -396,7 +396,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -412,7 +412,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; await expect( @@ -430,7 +430,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const context = { environment: { name: "rsc", mode: "build" } }; const source = [ @@ -449,7 +449,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; await expect( @@ -465,7 +465,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const result = await unwrapHook(plugin.transform)!.call( { environment: { name: "rsc", mode: "build" } }, @@ -479,7 +479,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( @@ -500,7 +500,7 @@ describe("plugin-rsc inline use-cache references", () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( - (candidate) => candidate.name === "vinext:use-cache-server-functions", + (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; const result = await transform.call( From 4bd11b1ad899b9a4e54f1cf53f71cca48a747791 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 19 Jun 2026 00:14:41 +0100 Subject: [PATCH 18/33] refactor(use-cache): own directive plugin types --- packages/vinext/src/index.ts | 6 ++-- .../src/plugins/server-function-directives.ts | 32 ++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index f64ca377af..9307d017d4 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -6,7 +6,6 @@ import type { UserConfig, ViteDevServer, } from "vite"; -import type { ServerFunctionDirectiveContext } from "@vitejs/plugin-rsc/plugin"; import { loadEnv, parseAst, transformWithOxc } from "vite"; import { pagesRouter, @@ -118,7 +117,10 @@ import { createMiddlewareServerOnlyPlugin } from "./plugins/middleware-server-on import { createOptimizeImportsPlugin } from "./plugins/optimize-imports.js"; import { createDynamicPreloadMetadataPlugin } from "./plugins/dynamic-preload-metadata.js"; import { createOgInlineFetchAssetsPlugin, createOgAssetsPlugin } from "./plugins/og-assets.js"; -import { createServerFunctionDirectivePlugins } from "./plugins/server-function-directives.js"; +import { + createServerFunctionDirectivePlugins, + type ServerFunctionDirectiveContext, +} from "./plugins/server-function-directives.js"; import { generateRouteTypes } from "./typegen.js"; import { mergeOptimizeDepsExclude, diff --git a/packages/vinext/src/plugins/server-function-directives.ts b/packages/vinext/src/plugins/server-function-directives.ts index bfc65bdc70..728190408a 100644 --- a/packages/vinext/src/plugins/server-function-directives.ts +++ b/packages/vinext/src/plugins/server-function-directives.ts @@ -3,7 +3,6 @@ import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { pathToFileURL } from "node:url"; -import type { RscPluginManager, ServerFunctionDirective } from "@vitejs/plugin-rsc/plugin"; import type { SourceMap } from "magic-string"; import type { Plugin, Rollup, ViteDevServer } from "vite"; import { parseAstAsync, transformWithOxc } from "vite"; @@ -11,11 +10,42 @@ import { isUnknownRecord } from "../utils/record.js"; import { escapeRegExp } from "../utils/regex.js"; type RscTransforms = typeof import("@vitejs/plugin-rsc/transforms"); +type RscPluginManager = NonNullable< + ReturnType +>["manager"]; type Program = Parameters[0]; type ModuleDirective = NonNullable< Parameters[2]["moduleDirective"] > & { start?: number }; type StringDirective = ModuleDirective & { type: "Literal"; value: string }; +type ExportFilter = NonNullable[2]["filter"]>; +type ExportMeta = Parameters[1]; +type FunctionParameters = NonNullable; + +export type ServerFunctionDirectiveContext = { + value: string; + name: string; + id: string; + directiveMatch: RegExpMatchArray; + location: "inline" | "module"; + hasBoundArgs: boolean; + parameters?: FunctionParameters; + runtime?: string; + meta?: ExportMeta; +}; + +export type ServerFunctionDirective = { + directive: string | RegExp; + test?: (code: string) => boolean; + filter?: (id: string) => boolean; + validate?: (context: { id: string; directive: string; location: "inline" | "module" }) => void; + rejectNonAsyncFunction?: boolean; + rejectNonAsyncModule?: boolean; + runtime?: string; + wrap: (context: ServerFunctionDirectiveContext) => string; + filterExport?: (context: { name: string; id: string; meta: ExportMeta }) => boolean; + clientError?: (context: { id: string; environment: string }) => string; +}; type Options = { projectRoot: string; From 75f7fb08534c4600a4fbdd374ff2d6be69ab5761 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 19 Jun 2026 00:32:35 +0100 Subject: [PATCH 19/33] refactor(use-cache): use plugin-rsc metadata map directly --- packages/vinext/src/index.ts | 5 +- .../src/plugins/server-function-directives.ts | 52 +++---------------- tests/use-cache-transform.test.ts | 16 +----- 3 files changed, 10 insertions(+), 63 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 9307d017d4..6f5b04353e 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -946,7 +946,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { client: VIRTUAL_APP_BROWSER_ENTRY, }, }); - const [serverFunctionPlugin, metadataPlugin] = await createServerFunctionDirectivePlugins({ + const [serverFunctionPlugin] = await createServerFunctionDirectivePlugins({ projectRoot: earlyBaseDir, definitions: [ { @@ -1012,11 +1012,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { browserEnvironmentName: "client", }); const useServerIndex = plugins.findIndex((plugin) => plugin.name === "rsc:use-server"); - if (useServerIndex === -1 || !serverFunctionPlugin || !metadataPlugin) { + if (useServerIndex === -1 || !serverFunctionPlugin) { throw new Error("vinext: Failed to locate @vitejs/plugin-rsc use-server plugin."); } plugins.splice(useServerIndex, 0, serverFunctionPlugin); - plugins.splice(useServerIndex + 2, 0, metadataPlugin); return plugins; }) .catch((cause) => { diff --git a/packages/vinext/src/plugins/server-function-directives.ts b/packages/vinext/src/plugins/server-function-directives.ts index 728190408a..0f38eea34c 100644 --- a/packages/vinext/src/plugins/server-function-directives.ts +++ b/packages/vinext/src/plugins/server-function-directives.ts @@ -54,12 +54,7 @@ type Options = { browserEnvironmentName: string; }; -type OwnedServerReference = { - referenceKey: string; - exportNames: string[]; -}; - -const SERVER_FUNCTION_DIRECTIVE_MARKER = "/* __vinext_server_function_directives__ */"; +const SERVER_FUNCTION_DIRECTIVE_MARKER = "/* __vite_rsc_server_function_directives__ */"; function resolvePluginRscModule(projectRoot: string, specifier: string): string { try { @@ -219,7 +214,6 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr const transforms: RscTransforms = await import(pathToFileURL(transformsPath).href); const { getPluginApi } = rscModule; let manager: RscPluginManager | undefined; - const ownedReferences = new Map(); const transformPlugin: Plugin = { name: "vinext:server-function-directives", @@ -239,10 +233,7 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr ); const isServer = this.environment.name === options.serverEnvironmentName; if (active.length === 0) { - if (isServer) { - ownedReferences.delete(id); - if (manager) delete manager.serverReferenceMetaMap[id]; - } + if (isServer && manager) delete manager.serverReferenceMetaMap[id]; return; } if (!manager) { @@ -297,14 +288,10 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr `$$ReactClient.createServerReference(${JSON.stringify(`${normalizedId}#${name}`)},$$ReactClient.callServer,undefined,${this.environment.mode === "dev" ? "$$ReactClient.findSourceMapURL" : "undefined"},${JSON.stringify(name)})`, }); if (!result?.output.hasChanged()) return; - const ownedReference = { - referenceKey: normalizedId, - exportNames: result.exportNames, - }; - ownedReferences.set(id, ownedReference); manager.serverReferenceMetaMap[id] = { importId: id, - ...ownedReference, + referenceKey: normalizedId, + exportNames: result.exportNames, }; result.output.prepend( `${SERVER_FUNCTION_DIRECTIVE_MARKER}\nimport * as $$ReactClient from ${JSON.stringify(this.environment.name === options.browserEnvironmentName ? browserRuntime : ssrRuntime)};\n`, @@ -419,17 +406,12 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr if (!useServerBoundary) { if (exportNames.size === 0) { - ownedReferences.delete(id); delete manager.serverReferenceMetaMap[id]; } else { - const ownedReference = { - referenceKey: normalizedId, - exportNames: [...exportNames], - }; - ownedReferences.set(id, ownedReference); manager.serverReferenceMetaMap[id] = { importId: id, - ...ownedReference, + referenceKey: normalizedId, + exportNames: [...exportNames], }; } } @@ -447,25 +429,5 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr }, }; - const metadataPlugin: Plugin = { - name: "vinext:server-function-directive-metadata", - transform: { - handler(_code, id) { - if (!manager) return; - const ownedReference = ownedReferences.get(id); - if (!ownedReference) return; - - const existing = manager.serverReferenceMetaMap[id]; - manager.serverReferenceMetaMap[id] = { - importId: id, - referenceKey: ownedReference.referenceKey, - exportNames: existing - ? [...new Set([...existing.exportNames, ...ownedReference.exportNames])] - : ownedReference.exportNames, - }; - }, - }, - }; - - return [transformPlugin, metadataPlugin]; + return [transformPlugin]; } diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index 585fe49ba5..abd1fa7c7e 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -56,22 +56,14 @@ async function configurePluginRsc(plugins: Plugin[]) { } describe("plugin-rsc inline use-cache references", () => { - it("restores user-land reference metadata after rsc:use-server clears it", async () => { + it("preserves user-land reference metadata through rsc:use-server", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const useCacheIndex = plugins.findIndex( (candidate) => candidate.name === "vinext:server-function-directives", ); const useServerIndex = plugins.findIndex((candidate) => candidate.name === "rsc:use-server"); - const metadataIndex = plugins.findIndex( - (candidate) => candidate.name === "vinext:server-function-directive-metadata", - ); - const manifestIndex = plugins.findIndex( - (candidate) => candidate.name === "rsc:virtual-vite-rsc/server-references", - ); expect(useCacheIndex).toBeLessThan(useServerIndex); - expect(metadataIndex).toBeGreaterThan(useServerIndex); - expect(metadataIndex).toBeLessThan(manifestIndex); const context = { environment: { name: "rsc", mode: "build" } }; const transformed = await unwrapHook(plugins[useCacheIndex]!.transform)!.call( @@ -86,9 +78,6 @@ describe("plugin-rsc inline use-cache references", () => { transformed!.code, moduleId, ); - expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); - - unwrapHook(plugins[metadataIndex]!.transform)!.call(context, transformed!.code, moduleId); expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); const ssrContext = { environment: { name: "ssr", mode: "build" } }; @@ -98,9 +87,6 @@ describe("plugin-rsc inline use-cache references", () => { moduleId, ); await unwrapHook(plugins[useServerIndex]!.transform)!.call(ssrContext, proxied!.code, moduleId); - expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); - - unwrapHook(plugins[metadataIndex]!.transform)!.call(ssrContext, proxied!.code, moduleId); expect(manager.serverReferenceMetaMap[moduleId]).toMatchObject({ importId: moduleId, exportNames: ["getData"], From b07d3174924c4e3036bf177417984815fc5c696c Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2026 23:40:23 +0100 Subject: [PATCH 20/33] refactor(use-cache): own server reference metadata lifecycle --- packages/vinext/src/index.ts | 6 +- .../src/plugins/server-function-directives.ts | 115 +++++++++++--- packages/vinext/src/shims/cache-runtime.ts | 11 +- pnpm-lock.yaml | 76 +++++----- pnpm-workspace.yaml | 2 +- tests/use-cache-transform.test.ts | 140 ++++++++++++++++-- 6 files changed, 270 insertions(+), 80 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 6f5b04353e..2a4d3eba18 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -946,7 +946,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { client: VIRTUAL_APP_BROWSER_ENTRY, }, }); - const [serverFunctionPlugin] = await createServerFunctionDirectivePlugins({ + const serverFunctionPlugins = await createServerFunctionDirectivePlugins({ projectRoot: earlyBaseDir, definitions: [ { @@ -1011,11 +1011,13 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { serverEnvironmentName: "rsc", browserEnvironmentName: "client", }); + const [serverFunctionPlugin, serverFunctionMetadataPlugin] = serverFunctionPlugins; const useServerIndex = plugins.findIndex((plugin) => plugin.name === "rsc:use-server"); - if (useServerIndex === -1 || !serverFunctionPlugin) { + if (useServerIndex === -1 || !serverFunctionPlugin || !serverFunctionMetadataPlugin) { throw new Error("vinext: Failed to locate @vitejs/plugin-rsc use-server plugin."); } plugins.splice(useServerIndex, 0, serverFunctionPlugin); + plugins.splice(useServerIndex + 2, 0, serverFunctionMetadataPlugin); return plugins; }) .catch((cause) => { diff --git a/packages/vinext/src/plugins/server-function-directives.ts b/packages/vinext/src/plugins/server-function-directives.ts index 0f38eea34c..664499a17f 100644 --- a/packages/vinext/src/plugins/server-function-directives.ts +++ b/packages/vinext/src/plugins/server-function-directives.ts @@ -54,7 +54,61 @@ type Options = { browserEnvironmentName: string; }; -const SERVER_FUNCTION_DIRECTIVE_MARKER = "/* __vite_rsc_server_function_directives__ */"; +const SERVER_FUNCTION_DIRECTIVE_MARKER = "/* __vinext_server_function_directives__ */"; + +type ServerReferenceMetadata = RscPluginManager["serverReferenceMetaMap"][string]; + +function mergeServerReferenceMetadata( + manager: RscPluginManager, + id: string, + referenceKey: string, + exportNames: Iterable, +): void { + const existing = manager.serverReferenceMetaMap[id]; + manager.serverReferenceMetaMap[id] = { + importId: existing?.importId ?? id, + referenceKey: existing?.referenceKey ?? referenceKey, + exportNames: [...new Set([...(existing?.exportNames ?? []), ...exportNames])], + }; +} + +function removeOwnedServerReferenceMetadata( + manager: RscPluginManager, + ownedReferences: Map, + id: string, +): void { + const owned = ownedReferences.get(id); + if (!owned) return; + ownedReferences.delete(id); + + const existing = manager.serverReferenceMetaMap[id]; + if (!existing) return; + + const ownedExportNames = new Set(owned.exportNames); + const exportNames = existing.exportNames.filter((name) => !ownedExportNames.has(name)); + if (exportNames.length === 0) { + delete manager.serverReferenceMetaMap[id]; + } else { + manager.serverReferenceMetaMap[id] = { ...existing, exportNames }; + } +} + +function setOwnedServerReferenceMetadata( + manager: RscPluginManager, + ownedReferences: Map, + id: string, + referenceKey: string, + exportNames: Iterable, +): void { + removeOwnedServerReferenceMetadata(manager, ownedReferences, id); + const metadata = { + importId: id, + referenceKey, + exportNames: [...new Set(exportNames)], + }; + ownedReferences.set(id, metadata); + mergeServerReferenceMetadata(manager, id, referenceKey, metadata.exportNames); +} function resolvePluginRscModule(projectRoot: string, specifier: string): string { try { @@ -197,7 +251,7 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr "@vitejs/plugin-rsc/transforms", ); const rscRuntime = pathToFileURL( - resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc/react/rsc"), + resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc/react/rsc/server"), ).href; const browserRuntime = pathToFileURL( resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc/react/browser"), @@ -214,6 +268,8 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr const transforms: RscTransforms = await import(pathToFileURL(transformsPath).href); const { getPluginApi } = rscModule; let manager: RscPluginManager | undefined; + const ownedReferences = new Map(); + const serverReferenceOwnership = new Map(); const transformPlugin: Plugin = { name: "vinext:server-function-directives", @@ -232,13 +288,16 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr (!definition.filter || definition.filter(id)), ); const isServer = this.environment.name === options.serverEnvironmentName; - if (active.length === 0) { - if (isServer && manager) delete manager.serverReferenceMetaMap[id]; - return; - } if (!manager) { throw new Error("vinext: failed to access @vitejs/plugin-rsc through getPluginApi()."); } + if (active.length === 0) { + if (isServer) { + serverReferenceOwnership.set(id, false); + removeOwnedServerReferenceMetadata(manager, ownedReferences, id); + } + return; + } let ast = await parseProgram(code); const useServerBoundary = transforms.hasDirective(ast.body, "use server"); @@ -288,11 +347,15 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr `$$ReactClient.createServerReference(${JSON.stringify(`${normalizedId}#${name}`)},$$ReactClient.callServer,undefined,${this.environment.mode === "dev" ? "$$ReactClient.findSourceMapURL" : "undefined"},${JSON.stringify(name)})`, }); if (!result?.output.hasChanged()) return; - manager.serverReferenceMetaMap[id] = { - importId: id, - referenceKey: normalizedId, - exportNames: result.exportNames, - }; + if (serverReferenceOwnership.get(id) !== false) { + setOwnedServerReferenceMetadata( + manager, + ownedReferences, + id, + normalizedId, + result.exportNames, + ); + } result.output.prepend( `${SERVER_FUNCTION_DIRECTIVE_MARKER}\nimport * as $$ReactClient from ${JSON.stringify(this.environment.name === options.browserEnvironmentName ? browserRuntime : ssrRuntime)};\n`, ); @@ -404,16 +467,12 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr ast = await parseProgram(code); } - if (!useServerBoundary) { - if (exportNames.size === 0) { - delete manager.serverReferenceMetaMap[id]; - } else { - manager.serverReferenceMetaMap[id] = { - importId: id, - referenceKey: normalizedId, - exportNames: [...exportNames], - }; - } + if (!useServerBoundary && exportNames.size > 0) { + serverReferenceOwnership.set(id, true); + setOwnedServerReferenceMetadata(manager, ownedReferences, id, normalizedId, exportNames); + } else if (isServer) { + serverReferenceOwnership.set(id, false); + removeOwnedServerReferenceMetadata(manager, ownedReferences, id); } const imports = [ @@ -429,5 +488,17 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr }, }; - return [transformPlugin]; + const metadataPlugin: Plugin = { + name: "vinext:server-function-directive-metadata", + transform: { + handler(_code, id) { + if (!manager) return; + const owned = ownedReferences.get(id); + if (!owned) return; + mergeServerReferenceMetadata(manager, id, owned.referenceKey, owned.exportNames); + }, + }, + }; + + return [transformPlugin, metadataPlugin]; } diff --git a/packages/vinext/src/shims/cache-runtime.ts b/packages/vinext/src/shims/cache-runtime.ts index 5273118685..99575ab39b 100644 --- a/packages/vinext/src/shims/cache-runtime.ts +++ b/packages/vinext/src/shims/cache-runtime.ts @@ -170,7 +170,8 @@ type RscModule = { renderToReadableStream: (data: unknown, options?: object) => ReadableStream; createFromReadableStream: ( stream: ReadableStream, - options?: { serverReferences?: "resolve" | "preserve" }, + options?: object, + extraOptions?: { preserveServerReferences?: boolean }, ) => Promise; encodeReply: (v: unknown[], options?: unknown) => Promise; createTemporaryReferenceSet: () => unknown; @@ -555,9 +556,11 @@ export function registerCachedFunction( // RSC-serialized entry: base64 → bytes → stream → deserialize const bytes = base64ToUint8(existing.value.data.body); const stream = uint8ToStream(bytes); - const result = await rsc.createFromReadableStream(stream, { - serverReferences: "preserve", - }); + const result = await rsc.createFromReadableStream( + stream, + {}, + { preserveServerReferences: true }, + ); recordRequestScopedCacheControl(existing.cacheControl); return result; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f18a38b7f0..84e22e533e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,8 +91,8 @@ catalogs: specifier: https://pkg.pr.new/@vitejs/plugin-react@82d2c578 version: 6.0.2 '@vitejs/plugin-rsc': - specifier: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578 - version: 0.5.27 + specifier: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75 + version: 0.5.28 '@vitest/coverage-istanbul': specifier: 4.1.6 version: 4.1.6 @@ -277,7 +277,7 @@ importers: version: 7.0.0-dev.20260217.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.6(@voidzero-dev/vite-plus-test@0.1.24) @@ -365,7 +365,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) drizzle-kit: specifier: 'catalog:' version: 0.31.10 @@ -408,7 +408,7 @@ importers: devDependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) vite: specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' @@ -426,7 +426,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -558,7 +558,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -597,7 +597,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -680,7 +680,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -713,7 +713,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) ms: specifier: 'catalog:' version: 3.0.0-canary.1 @@ -784,7 +784,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) typescript: specifier: 'catalog:' version: 5.9.3 @@ -876,7 +876,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -922,7 +922,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1008,7 +1008,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react-server-dom-webpack: specifier: 'catalog:' version: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1020,7 +1020,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) fake-context-lib: specifier: file:./__test_packages__/fake-context-lib version: file:tests/fixtures/app-basic/__test_packages__/fake-context-lib @@ -1054,7 +1054,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1079,7 +1079,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1104,7 +1104,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1135,7 +1135,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1170,7 +1170,7 @@ importers: version: 1.9.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) better-auth: specifier: 'catalog:' version: 1.5.6(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-istanbul@4.1.6(@voidzero-dev/vite-plus-test@0.1.24))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(better-sqlite3@12.6.2)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.6.2)(kysely@0.28.15))(esbuild@0.27.3)(jiti@2.7.0)(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0) @@ -1201,7 +1201,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-intl: specifier: 'catalog:' version: 4.11.1(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react@19.2.7)(typescript@5.9.3) @@ -1229,7 +1229,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1257,7 +1257,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-view-transitions: specifier: 'catalog:' version: 0.3.5(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1285,7 +1285,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) nuqs: specifier: 'catalog:' version: 2.8.8(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react@19.2.7) @@ -1322,7 +1322,7 @@ importers: version: 1.2.4(@types/react@19.2.16)(react@19.2.7) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) class-variance-authority: specifier: 'catalog:' version: 0.7.1 @@ -1378,7 +1378,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1403,7 +1403,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1428,7 +1428,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1472,7 +1472,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1516,7 +1516,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -4394,9 +4394,9 @@ packages: babel-plugin-react-compiler: optional: true - '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578': - resolution: {integrity: sha512-vos4mwhdaMBHkA1HND+rhs0GWlKTKnP0XrmADiBWRb3LmHdauz+KBQaNKuKuCEHcKkkpI56WZ8MCi8otkJtoIg==, tarball: https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578} - version: 0.5.27 + '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75': + resolution: {integrity: sha512-Q4cHFFyarDlR6ZcMqlIg5XbGdxYfxgyee77hTPgq4RmT2fbu9sB503cgZ1A3DSzsykufBQB0Ix69fquE2CSRMA==, tarball: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75} + version: 0.5.28 peerDependencies: react: '*' react-dom: '*' @@ -5085,8 +5085,8 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-module-lexer@2.1.0: - resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -9414,10 +9414,10 @@ snapshots: '@rolldown/pluginutils': 1.0.1 vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' - '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': + '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': dependencies: '@rolldown/pluginutils': 1.0.1 - es-module-lexer: 2.1.0 + es-module-lexer: 2.3.1 estree-walker: 3.0.3 magic-string: 0.30.21 react: 19.2.7 @@ -9893,7 +9893,7 @@ snapshots: es-module-lexer@1.7.0: {} - es-module-lexer@2.1.0: {} + es-module-lexer@2.3.1: {} esast-util-from-estree@2.0.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3458c4433e..90ee02a165 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -43,7 +43,7 @@ catalog: "@next/mdx": 16.2.7 recma-codehike: 0.0.1 remark-codehike: 0.0.1 - "@vitejs/plugin-rsc": https://pkg.pr.new/@vitejs/plugin-rsc@82d2c578 + "@vitejs/plugin-rsc": https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75 "@vitest/coverage-istanbul": 4.1.6 better-auth: ^1.5.6 better-sqlite3: ^12.0.0 diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index abd1fa7c7e..9a4b72d81b 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -1,8 +1,8 @@ /** * Tests the vinext user-land server function directive integration used for function-level * "use cache" directives. Vinext owns the directive plugin while plugin-rsc - * owns directive discovery, closure hoisting, encryption, reference ids, and - * server-reference manifest metadata. + * provides directive transforms and the shared reference metadata map. Vinext + * owns directive orchestration and merges its references after rsc:use-server. */ import path from "node:path"; import { createHash } from "node:crypto"; @@ -56,14 +56,18 @@ async function configurePluginRsc(plugins: Plugin[]) { } describe("plugin-rsc inline use-cache references", () => { - it("preserves user-land reference metadata through rsc:use-server", async () => { + it("restores user-land reference metadata after rsc:use-server", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const useCacheIndex = plugins.findIndex( (candidate) => candidate.name === "vinext:server-function-directives", ); const useServerIndex = plugins.findIndex((candidate) => candidate.name === "rsc:use-server"); + const metadataIndex = plugins.findIndex( + (candidate) => candidate.name === "vinext:server-function-directive-metadata", + ); expect(useCacheIndex).toBeLessThan(useServerIndex); + expect(metadataIndex).toBeGreaterThan(useServerIndex); const context = { environment: { name: "rsc", mode: "build" } }; const transformed = await unwrapHook(plugins[useCacheIndex]!.transform)!.call( @@ -78,6 +82,9 @@ describe("plugin-rsc inline use-cache references", () => { transformed!.code, moduleId, ); + expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); + + unwrapHook(plugins[metadataIndex]!.transform)!.call(context, transformed!.code, moduleId); expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); const ssrContext = { environment: { name: "ssr", mode: "build" } }; @@ -87,12 +94,119 @@ describe("plugin-rsc inline use-cache references", () => { moduleId, ); await unwrapHook(plugins[useServerIndex]!.transform)!.call(ssrContext, proxied!.code, moduleId); + unwrapHook(plugins[metadataIndex]!.transform)!.call(ssrContext, proxied!.code, moduleId); expect(manager.serverReferenceMetaMap[moduleId]).toMatchObject({ importId: moduleId, - exportNames: ["getData"], + exportNames: expect.arrayContaining(["getData"]), }); }); + it("merges and deduplicates use-server and user-land reference metadata", async () => { + const plugins = await getPlugins(); + const manager = await configurePluginRsc(plugins); + const useCachePlugin = plugins.find( + (candidate) => candidate.name === "vinext:server-function-directives", + )!; + const useServerPlugin = plugins.find((candidate) => candidate.name === "rsc:use-server")!; + const metadataPlugin = plugins.find( + (candidate) => candidate.name === "vinext:server-function-directive-metadata", + )!; + const context = { environment: { name: "rsc", mode: "build" } }; + const source = [ + `export async function action() {`, + ` "use server";`, + `}`, + `export async function getData() {`, + ` "use cache";`, + ` return 1;`, + `}`, + ].join("\n"); + + const useCacheResult = await unwrapHook(useCachePlugin.transform)!.call( + context, + source, + moduleId, + ); + const useServerResult = await unwrapHook(useServerPlugin.transform)!.call( + context, + useCacheResult!.code, + moduleId, + ); + const upstreamMetadata = manager.serverReferenceMetaMap[moduleId]; + const upstreamExport = upstreamMetadata.exportNames[0]!; + upstreamMetadata.exportNames.push(upstreamExport); + + unwrapHook(metadataPlugin.transform)!.call(context, useServerResult!.code, moduleId); + + const merged = manager.serverReferenceMetaMap[moduleId]; + expect(merged.importId).toBe(upstreamMetadata.importId); + expect(merged.referenceKey).toBe(upstreamMetadata.referenceKey); + expect(merged.exportNames).toContain(upstreamExport); + expect(merged.exportNames).toContainEqual(expect.stringMatching(/getData/)); + expect(merged.exportNames).toHaveLength(new Set(merged.exportNames).size); + }); + + it("restores RSC-owned metadata after a non-owning proxy pass", async () => { + const plugins = await getPlugins(); + const manager = await configurePluginRsc(plugins); + const useCachePlugin = plugins.find( + (candidate) => candidate.name === "vinext:server-function-directives", + )!; + const useServerPlugin = plugins.find((candidate) => candidate.name === "rsc:use-server")!; + const metadataPlugin = plugins.find( + (candidate) => candidate.name === "vinext:server-function-directive-metadata", + )!; + const rscContext = { environment: { name: "rsc", mode: "build" } }; + const transformed = await unwrapHook(useCachePlugin.transform)!.call( + rscContext, + inlineCacheCode, + moduleId, + ); + const ownedExportNames = manager.serverReferenceMetaMap[moduleId].exportNames; + + const ssrContext = { environment: { name: "ssr", mode: "build" } }; + await unwrapHook(useCachePlugin.transform)!.call(ssrContext, transformed!.code, moduleId); + await unwrapHook(useServerPlugin.transform)!.call(ssrContext, transformed!.code, moduleId); + expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); + + unwrapHook(metadataPlugin.transform)!.call(ssrContext, transformed!.code, moduleId); + expect(manager.serverReferenceMetaMap[moduleId].exportNames).toEqual(ownedExportNames); + }); + + it("does not let a stale proxy transform resurrect RSC-removed metadata", async () => { + const plugins = await getPlugins(); + const manager = await configurePluginRsc(plugins); + const useCachePlugin = plugins.find( + (candidate) => candidate.name === "vinext:server-function-directives", + )!; + const metadataPlugin = plugins.find( + (candidate) => candidate.name === "vinext:server-function-directive-metadata", + )!; + const rscContext = { environment: { name: "rsc", mode: "build" } }; + const ssrContext = { environment: { name: "ssr", mode: "build" } }; + + await unwrapHook(useCachePlugin.transform)!.call(rscContext, fileCacheCode, moduleId); + expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); + + await unwrapHook(useCachePlugin.transform)!.call( + rscContext, + `export async function getData() { return 1; }`, + moduleId, + ); + expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); + + const staleProxy = await unwrapHook(useCachePlugin.transform)!.call( + ssrContext, + fileCacheCode, + moduleId, + ); + unwrapHook(metadataPlugin.transform)!.call(ssrContext, staleProxy!.code, moduleId); + expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); + + await unwrapHook(useCachePlugin.transform)!.call(rscContext, fileCacheCode, moduleId); + expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); + }); + it("matches Vite's dev reference key for files outside the project root", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); @@ -172,24 +286,24 @@ describe("plugin-rsc inline use-cache references", () => { expect(getDataName(withUnrelated!.code)).toBe(getDataName(original!.code)); }); - it("removes owned reference metadata when the directive is removed", async () => { + it("leaves directive removal cleanup to the preceding use-server pass", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const plugin = plugins.find( (candidate) => candidate.name === "vinext:server-function-directives", )!; const transform = unwrapHook(plugin.transform)!; - await transform.call( - { environment: { name: "rsc", mode: "build" } }, - inlineCacheCode, - moduleId, - ); + const useServerPlugin = plugins.find((candidate) => candidate.name === "rsc:use-server")!; + const context = { environment: { name: "rsc", mode: "build" } }; + await transform.call(context, inlineCacheCode, moduleId); expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); - await transform.call( - { environment: { name: "rsc", mode: "build" } }, - `export async function getData() { return 1; }`, + const source = `export async function getData() { return 1; }`; + const useServerResult = await unwrapHook(useServerPlugin.transform)!.call( + context, + source, moduleId, ); + await transform.call(context, useServerResult?.code ?? source, moduleId); expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); }); From 622f0b915156fbfcb03ca4f1596cc0d0f6725ebe Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2026 23:43:35 +0100 Subject: [PATCH 21/33] chore(use-cache): keep directive type internal --- packages/vinext/src/plugins/server-function-directives.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vinext/src/plugins/server-function-directives.ts b/packages/vinext/src/plugins/server-function-directives.ts index 664499a17f..f0560696d0 100644 --- a/packages/vinext/src/plugins/server-function-directives.ts +++ b/packages/vinext/src/plugins/server-function-directives.ts @@ -34,7 +34,7 @@ export type ServerFunctionDirectiveContext = { meta?: ExportMeta; }; -export type ServerFunctionDirective = { +type ServerFunctionDirective = { directive: string | RegExp; test?: (code: string) => boolean; filter?: (id: string) => boolean; From 250707cbc35c009d5bd1243d67044a6fecbd66b5 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 24 Jul 2026 12:04:45 +0100 Subject: [PATCH 22/33] refactor(use-cache): adopt server reference claims --- packages/vinext/src/index.ts | 8 +- .../src/plugins/server-function-directives.ts | 279 +++++++----------- packages/vinext/src/shims/cache-runtime.ts | 15 +- pnpm-lock.yaml | 68 ++--- pnpm-workspace.yaml | 2 +- tests/use-cache-transform.test.ts | 140 ++++----- 6 files changed, 216 insertions(+), 296 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 2a4d3eba18..298e25bf26 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -118,7 +118,7 @@ import { createOptimizeImportsPlugin } from "./plugins/optimize-imports.js"; import { createDynamicPreloadMetadataPlugin } from "./plugins/dynamic-preload-metadata.js"; import { createOgInlineFetchAssetsPlugin, createOgAssetsPlugin } from "./plugins/og-assets.js"; import { - createServerFunctionDirectivePlugins, + createServerFunctionDirectivePlugin, type ServerFunctionDirectiveContext, } from "./plugins/server-function-directives.js"; import { generateRouteTypes } from "./typegen.js"; @@ -946,7 +946,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { client: VIRTUAL_APP_BROWSER_ENTRY, }, }); - const serverFunctionPlugins = await createServerFunctionDirectivePlugins({ + const serverFunctionPlugin = await createServerFunctionDirectivePlugin({ projectRoot: earlyBaseDir, definitions: [ { @@ -1011,13 +1011,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { serverEnvironmentName: "rsc", browserEnvironmentName: "client", }); - const [serverFunctionPlugin, serverFunctionMetadataPlugin] = serverFunctionPlugins; const useServerIndex = plugins.findIndex((plugin) => plugin.name === "rsc:use-server"); - if (useServerIndex === -1 || !serverFunctionPlugin || !serverFunctionMetadataPlugin) { + if (useServerIndex === -1) { throw new Error("vinext: Failed to locate @vitejs/plugin-rsc use-server plugin."); } plugins.splice(useServerIndex, 0, serverFunctionPlugin); - plugins.splice(useServerIndex + 2, 0, serverFunctionMetadataPlugin); return plugins; }) .catch((cause) => { diff --git a/packages/vinext/src/plugins/server-function-directives.ts b/packages/vinext/src/plugins/server-function-directives.ts index f0560696d0..2dd6982b21 100644 --- a/packages/vinext/src/plugins/server-function-directives.ts +++ b/packages/vinext/src/plugins/server-function-directives.ts @@ -1,23 +1,22 @@ -import { createHash } from "node:crypto"; import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { pathToFileURL } from "node:url"; +import type { RscPluginManager } from "@vitejs/plugin-rsc"; import type { SourceMap } from "magic-string"; -import type { Plugin, Rollup, ViteDevServer } from "vite"; +import type { Plugin, Rollup } from "vite"; import { parseAstAsync, transformWithOxc } from "vite"; import { isUnknownRecord } from "../utils/record.js"; import { escapeRegExp } from "../utils/regex.js"; type RscTransforms = typeof import("@vitejs/plugin-rsc/transforms"); -type RscPluginManager = NonNullable< - ReturnType ->["manager"]; type Program = Parameters[0]; -type ModuleDirective = NonNullable< - Parameters[2]["moduleDirective"] -> & { start?: number }; -type StringDirective = ModuleDirective & { type: "Literal"; value: string }; +type ProgramExpressionStatement = Extract; +type StringDirective = Extract & { + value: string; + start: number; + end: number; +}; type ExportFilter = NonNullable[2]["filter"]>; type ExportMeta = Parameters[1]; type FunctionParameters = NonNullable; @@ -55,60 +54,8 @@ type Options = { }; const SERVER_FUNCTION_DIRECTIVE_MARKER = "/* __vinext_server_function_directives__ */"; - -type ServerReferenceMetadata = RscPluginManager["serverReferenceMetaMap"][string]; - -function mergeServerReferenceMetadata( - manager: RscPluginManager, - id: string, - referenceKey: string, - exportNames: Iterable, -): void { - const existing = manager.serverReferenceMetaMap[id]; - manager.serverReferenceMetaMap[id] = { - importId: existing?.importId ?? id, - referenceKey: existing?.referenceKey ?? referenceKey, - exportNames: [...new Set([...(existing?.exportNames ?? []), ...exportNames])], - }; -} - -function removeOwnedServerReferenceMetadata( - manager: RscPluginManager, - ownedReferences: Map, - id: string, -): void { - const owned = ownedReferences.get(id); - if (!owned) return; - ownedReferences.delete(id); - - const existing = manager.serverReferenceMetaMap[id]; - if (!existing) return; - - const ownedExportNames = new Set(owned.exportNames); - const exportNames = existing.exportNames.filter((name) => !ownedExportNames.has(name)); - if (exportNames.length === 0) { - delete manager.serverReferenceMetaMap[id]; - } else { - manager.serverReferenceMetaMap[id] = { ...existing, exportNames }; - } -} - -function setOwnedServerReferenceMetadata( - manager: RscPluginManager, - ownedReferences: Map, - id: string, - referenceKey: string, - exportNames: Iterable, -): void { - removeOwnedServerReferenceMetadata(manager, ownedReferences, id); - const metadata = { - importId: id, - referenceKey, - exportNames: [...new Set(exportNames)], - }; - ownedReferences.set(id, metadata); - mergeServerReferenceMetadata(manager, id, referenceKey, metadata.exportNames); -} +const SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME = "vinext:server-function-directives"; +const USE_SERVER_PLUGIN_NAME = "rsc:use-server"; function resolvePluginRscModule(projectRoot: string, specifier: string): string { try { @@ -136,7 +83,13 @@ function matchDirective(value: string, directive: string | RegExp): RegExpMatchA } function isStringLiteral(value: unknown): value is StringDirective { - return isUnknownRecord(value) && value.type === "Literal" && typeof value.value === "string"; + return ( + isUnknownRecord(value) && + value.type === "Literal" && + typeof value.value === "string" && + typeof value.start === "number" && + typeof value.end === "number" + ); } function isExpressionStatement( @@ -206,24 +159,6 @@ function findInlineDirective( return result; } -function hashString(value: string): string { - return createHash("sha256").update(value).digest("hex").slice(0, 12); -} - -function normalizeViteImportAnalysisUrl( - environment: ViteDevServer["environments"][string], - id: string, -): string { - const root = environment.config.root; - const rootPrefix = root.endsWith("/") ? root : `${root}/`; - if (id.startsWith(rootPrefix)) return id.slice(root.length); - - const cleanId = id.split("?", 1)[0] ?? id; - if (path.isAbsolute(cleanId) && fs.existsSync(cleanId)) return path.posix.join("/@fs/", id); - if (id.startsWith(".") || id.startsWith("/")) return id; - return `/@id/${id.replace("\0", "__x00__")}`; -} - async function expandExportAll( transforms: RscTransforms, context: Rollup.TransformPluginContext, @@ -244,7 +179,7 @@ async function expandExportAll( }); } -export async function createServerFunctionDirectivePlugins(options: Options): Promise { +export async function createServerFunctionDirectivePlugin(options: Options): Promise { const rscModulePath = resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc"); const transformsPath = resolvePluginRscModule( options.projectRoot, @@ -268,11 +203,9 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr const transforms: RscTransforms = await import(pathToFileURL(transformsPath).href); const { getPluginApi } = rscModule; let manager: RscPluginManager | undefined; - const ownedReferences = new Map(); - const serverReferenceOwnership = new Map(); const transformPlugin: Plugin = { - name: "vinext:server-function-directives", + name: SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, configResolved(config) { manager = getPluginApi(config)?.manager; @@ -292,24 +225,17 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr throw new Error("vinext: failed to access @vitejs/plugin-rsc through getPluginApi()."); } if (active.length === 0) { - if (isServer) { - serverReferenceOwnership.set(id, false); - removeOwnedServerReferenceMetadata(manager, ownedReferences, id); - } + manager.serverReferences.deleteClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id); return; } let ast = await parseProgram(code); const useServerBoundary = transforms.hasDirective(ast.body, "use server"); - if (!isServer && useServerBoundary) return; - - const normalizedId = - manager.config.command === "build" - ? hashString(manager.toRelativeId(id)) - : normalizeViteImportAnalysisUrl( - manager.server.environments[options.serverEnvironmentName], - id, - ); + if (!isServer && useServerBoundary) { + manager.serverReferences.deleteClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id); + return; + } + const reference = manager.serverReferences.resolve(id, options.serverEnvironmentName); if (!isServer) { for (const definition of active) { @@ -327,7 +253,10 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr const moduleDirective = findModuleDirective(ast, definition.directive); if (moduleDirective) matches.push([definition, moduleDirective]); } - if (matches.length === 0) return; + if (matches.length === 0) { + manager.serverReferences.deleteClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id); + return; + } if (matches.length > 1) { throw Object.assign( new Error("Multiple server function directives match this module."), @@ -344,18 +273,17 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr code, directive: moduleDirective.value, runtime: (name) => - `$$ReactClient.createServerReference(${JSON.stringify(`${normalizedId}#${name}`)},$$ReactClient.callServer,undefined,${this.environment.mode === "dev" ? "$$ReactClient.findSourceMapURL" : "undefined"},${JSON.stringify(name)})`, + `$$ReactClient.createServerReference(${JSON.stringify(`${reference.referenceKey}#${name}`)},$$ReactClient.callServer,undefined,${this.environment.mode === "dev" ? "$$ReactClient.findSourceMapURL" : "undefined"},${JSON.stringify(name)})`, }); - if (!result?.output.hasChanged()) return; - if (serverReferenceOwnership.get(id) !== false) { - setOwnedServerReferenceMetadata( - manager, - ownedReferences, - id, - normalizedId, - result.exportNames, - ); + if (!result?.output.hasChanged()) { + manager.serverReferences.deleteClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id); + return; } + manager.serverReferences.deleteClaim(USE_SERVER_PLUGIN_NAME, id); + manager.serverReferences.replaceClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id, { + ...reference, + exportNames: result.exportNames, + }); result.output.prepend( `${SERVER_FUNCTION_DIRECTIVE_MARKER}\nimport * as $$ReactClient from ${JSON.stringify(this.environment.name === options.browserEnvironmentName ? browserRuntime : ssrRuntime)};\n`, ); @@ -370,9 +298,9 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr let needsEncryptionRuntime = false; let outputMap: SourceMap | undefined; - for (const definition of active) { + for (const [definitionIndex, definition] of active.entries()) { const runtimeName = definition.runtime - ? `$$server_function_directive_${hashString(definition.runtime)}` + ? `$$server_function_directive_${definitionIndex}` : undefined; let runtimeUsed = false; const getRuntime = () => { @@ -405,55 +333,61 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr definition.validate?.({ id, directive: moduleMatch[0], location: "module" }); } - const result = transforms.transformServerActionServer(code, ast, { - runtime: (value, name) => - `$$ReactServer.registerServerReference(${value}, ${JSON.stringify(normalizedId)}, ${JSON.stringify(name)})`, - directive: definition.directive, - moduleDirective, - moduleRuntime: (value, name, meta) => { - if (!moduleMatch) return value; - needsReactRuntime = true; - return `$$ReactServer.registerServerReference(${definition.wrap({ value, name, id, directiveMatch: moduleMatch, location: "module", hasBoundArgs: false, parameters: meta.parameters, runtime: getRuntime(), meta })}, ${JSON.stringify(normalizedId)}, ${JSON.stringify(name)})`; - }, - inlineRuntime: (value, name, meta) => { - definition.validate?.({ - id, - directive: meta.directiveMatch[0], - location: "inline", + const result = moduleMatch + ? transforms.transformWrapExport(code, ast, { + runtime: (value, name, meta) => { + needsReactRuntime = true; + return `$$VinextReactServer.registerServerReference(${definition.wrap({ value, name, id, directiveMatch: moduleMatch, location: "module", hasBoundArgs: false, parameters: meta.parameters, runtime: getRuntime(), meta })}, ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; + }, + filter: (name, meta) => definition.filterExport?.({ name, id, meta }) ?? true, + rejectNonAsyncFunction: definition.rejectNonAsyncModule, + }) + : transforms.transformHoistInlineDirective(code, ast, { + directive: definition.directive, + runtime: (value, name, meta) => { + definition.validate?.({ + id, + directive: meta.directiveMatch[0], + location: "inline", + }); + const wrapped = definition.wrap({ + value, + name, + id, + directiveMatch: meta.directiveMatch, + location: "inline", + hasBoundArgs: meta.hasBoundArgs, + parameters: meta.parameters, + runtime: getRuntime(), + }); + if (useServerBoundary) return wrapped; + + needsReactRuntime = true; + if (meta.hasBoundArgs) { + needsEncryptionRuntime = true; + return `$$VinextReactServer.registerServerReference((($$wrapped) => async ($$encoded, ...$$args) => $$wrapped(...await __vite_rsc_encryption_runtime.decryptActionBoundArgs($$encoded), ...$$args))(${wrapped}), ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; + } + return `$$VinextReactServer.registerServerReference(${wrapped}, ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; + }, + rejectNonAsyncFunction: definition.rejectNonAsyncFunction, + encode: (value) => { + needsEncryptionRuntime = true; + return `__vite_rsc_encryption_runtime.encryptActionBoundArgs(${value})`; + }, + stableName: true, + exportWrappedHoist: !useServerBoundary, + rejectForbiddenExpressions: true, }); - const wrapped = definition.wrap({ - value, - name, - id, - directiveMatch: meta.directiveMatch, - location: "inline", - hasBoundArgs: meta.hasBoundArgs, - parameters: meta.parameters, - runtime: getRuntime(), - }); - if (useServerBoundary) return wrapped; - - needsReactRuntime = true; - if (meta.hasBoundArgs) { - needsEncryptionRuntime = true; - return `$$ReactServer.registerServerReference((($$wrapped) => async ($$encoded, ...$$args) => $$wrapped(...await __vite_rsc_encryption_runtime.decryptActionBoundArgs($$encoded), ...$$args))(${wrapped}), ${JSON.stringify(normalizedId)}, ${JSON.stringify(name)})`; - } - return `$$ReactServer.registerServerReference(${wrapped}, ${JSON.stringify(normalizedId)}, ${JSON.stringify(name)})`; - }, - filter: (name, meta) => definition.filterExport?.({ name, id, meta }) ?? true, - rejectNonAsyncFunction: definition.rejectNonAsyncFunction, - rejectNonAsyncModule: definition.rejectNonAsyncModule, - encode: (value) => { - needsEncryptionRuntime = true; - return `__vite_rsc_encryption_runtime.encryptActionBoundArgs(${value})`; - }, - stableName: true, - exportWrappedHoist: !useServerBoundary, - detectUseServerModule: false, - rejectForbiddenExpressions: true, - }); if (!result.output.hasChanged()) continue; + if (moduleDirective) { + result.output.overwrite( + moduleDirective.start, + moduleDirective.end, + `/* ${JSON.stringify(moduleDirective.value)} */`, + ); + } + if (runtimeUsed && definition.runtime && runtimeName) { result.output.prepend( `import * as ${runtimeName} from ${JSON.stringify(definition.runtime)};\n`, @@ -468,15 +402,18 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr } if (!useServerBoundary && exportNames.size > 0) { - serverReferenceOwnership.set(id, true); - setOwnedServerReferenceMetadata(manager, ownedReferences, id, normalizedId, exportNames); - } else if (isServer) { - serverReferenceOwnership.set(id, false); - removeOwnedServerReferenceMetadata(manager, ownedReferences, id); + manager.serverReferences.deleteClaim(USE_SERVER_PLUGIN_NAME, id); + manager.serverReferences.replaceClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id, { + ...reference, + exportNames: [...exportNames], + }); + } else { + manager.serverReferences.deleteClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id); } const imports = [ - needsReactRuntime && `import * as $$ReactServer from ${JSON.stringify(rscRuntime)};`, + needsReactRuntime && + `import * as $$VinextReactServer from ${JSON.stringify(rscRuntime)};`, needsEncryptionRuntime && `import * as __vite_rsc_encryption_runtime from ${JSON.stringify(encryptionRuntime)};`, ].filter(Boolean); @@ -488,17 +425,5 @@ export async function createServerFunctionDirectivePlugins(options: Options): Pr }, }; - const metadataPlugin: Plugin = { - name: "vinext:server-function-directive-metadata", - transform: { - handler(_code, id) { - if (!manager) return; - const owned = ownedReferences.get(id); - if (!owned) return; - mergeServerReferenceMetadata(manager, id, owned.referenceKey, owned.exportNames); - }, - }, - }; - - return [transformPlugin, metadataPlugin]; + return transformPlugin; } diff --git a/packages/vinext/src/shims/cache-runtime.ts b/packages/vinext/src/shims/cache-runtime.ts index 99575ab39b..f0940241f3 100644 --- a/packages/vinext/src/shims/cache-runtime.ts +++ b/packages/vinext/src/shims/cache-runtime.ts @@ -166,18 +166,7 @@ export function getCacheContext(): CacheContext | null { * (they depend on virtual modules set up by @vitejs/plugin-rsc). * In test environments, the import fails and we fall back to JSON. */ -type RscModule = { - renderToReadableStream: (data: unknown, options?: object) => ReadableStream; - createFromReadableStream: ( - stream: ReadableStream, - options?: object, - extraOptions?: { preserveServerReferences?: boolean }, - ) => Promise; - encodeReply: (v: unknown[], options?: unknown) => Promise; - createTemporaryReferenceSet: () => unknown; - createClientTemporaryReferenceSet: () => unknown; - decodeReply: (body: string | FormData, options?: unknown) => Promise; -}; +type RscModule = typeof import("@vitejs/plugin-rsc/react/rsc"); function getUseCacheDeploymentIdDefine(): string | undefined { try { @@ -216,7 +205,7 @@ let _rscModule: RscModule | null | typeof NOT_LOADED = NOT_LOADED; async function getRscModule(): Promise { if (_rscModule !== NOT_LOADED) return _rscModule; try { - _rscModule = (await import("@vitejs/plugin-rsc/react/rsc")) as RscModule; + _rscModule = await import("@vitejs/plugin-rsc/react/rsc"); } catch { _rscModule = null; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84e22e533e..0269913e1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,8 +91,8 @@ catalogs: specifier: https://pkg.pr.new/@vitejs/plugin-react@82d2c578 version: 6.0.2 '@vitejs/plugin-rsc': - specifier: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75 - version: 0.5.28 + specifier: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476 + version: 0.5.30 '@vitest/coverage-istanbul': specifier: 4.1.6 version: 4.1.6 @@ -277,7 +277,7 @@ importers: version: 7.0.0-dev.20260217.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.6(@voidzero-dev/vite-plus-test@0.1.24) @@ -365,7 +365,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) drizzle-kit: specifier: 'catalog:' version: 0.31.10 @@ -408,7 +408,7 @@ importers: devDependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) vite: specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' @@ -426,7 +426,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -558,7 +558,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -597,7 +597,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -680,7 +680,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -713,7 +713,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) ms: specifier: 'catalog:' version: 3.0.0-canary.1 @@ -784,7 +784,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) typescript: specifier: 'catalog:' version: 5.9.3 @@ -876,7 +876,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -922,7 +922,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1008,7 +1008,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react-server-dom-webpack: specifier: 'catalog:' version: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1020,7 +1020,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) fake-context-lib: specifier: file:./__test_packages__/fake-context-lib version: file:tests/fixtures/app-basic/__test_packages__/fake-context-lib @@ -1054,7 +1054,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1079,7 +1079,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1104,7 +1104,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1135,7 +1135,7 @@ importers: version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1170,7 +1170,7 @@ importers: version: 1.9.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) better-auth: specifier: 'catalog:' version: 1.5.6(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-istanbul@4.1.6(@voidzero-dev/vite-plus-test@0.1.24))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(better-sqlite3@12.6.2)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.6.2)(kysely@0.28.15))(esbuild@0.27.3)(jiti@2.7.0)(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0) @@ -1201,7 +1201,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-intl: specifier: 'catalog:' version: 4.11.1(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react@19.2.7)(typescript@5.9.3) @@ -1229,7 +1229,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1257,7 +1257,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-view-transitions: specifier: 'catalog:' version: 0.3.5(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1285,7 +1285,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) nuqs: specifier: 'catalog:' version: 2.8.8(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react@19.2.7) @@ -1322,7 +1322,7 @@ importers: version: 1.2.4(@types/react@19.2.16)(react@19.2.7) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) class-variance-authority: specifier: 'catalog:' version: 0.7.1 @@ -1378,7 +1378,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1403,7 +1403,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1428,7 +1428,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1472,7 +1472,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1516,7 +1516,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -4394,9 +4394,9 @@ packages: babel-plugin-react-compiler: optional: true - '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75': - resolution: {integrity: sha512-Q4cHFFyarDlR6ZcMqlIg5XbGdxYfxgyee77hTPgq4RmT2fbu9sB503cgZ1A3DSzsykufBQB0Ix69fquE2CSRMA==, tarball: https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75} - version: 0.5.28 + '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476': + resolution: {integrity: sha512-hJz+rxnLJ6zSlHNJBZr097gF+3uzoOYLYgYVkwIpdl7ttO5UUjVOLXaomF4ZF0OVFde17uAfUyW2eX1e9s/+hQ==, tarball: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476} + version: 0.5.30 peerDependencies: react: '*' react-dom: '*' @@ -9414,7 +9414,7 @@ snapshots: '@rolldown/pluginutils': 1.0.1 vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' - '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': + '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': dependencies: '@rolldown/pluginutils': 1.0.1 es-module-lexer: 2.3.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 90ee02a165..94ba817b31 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -43,7 +43,7 @@ catalog: "@next/mdx": 16.2.7 recma-codehike: 0.0.1 remark-codehike: 0.0.1 - "@vitejs/plugin-rsc": https://pkg.pr.new/@vitejs/plugin-rsc@5a2fd75 + "@vitejs/plugin-rsc": https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476 "@vitest/coverage-istanbul": 4.1.6 better-auth: ^1.5.6 better-sqlite3: ^12.0.0 diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index 9a4b72d81b..d5d961ceb1 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -1,8 +1,8 @@ /** * Tests the vinext user-land server function directive integration used for function-level * "use cache" directives. Vinext owns the directive plugin while plugin-rsc - * provides directive transforms and the shared reference metadata map. Vinext - * owns directive orchestration and merges its references after rsc:use-server. + * provides directive transforms and aggregates independently owned server + * reference claims. */ import path from "node:path"; import { createHash } from "node:crypto"; @@ -56,18 +56,14 @@ async function configurePluginRsc(plugins: Plugin[]) { } describe("plugin-rsc inline use-cache references", () => { - it("restores user-land reference metadata after rsc:use-server", async () => { + it("keeps the vinext claim when rsc:use-server removes its own claim", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const useCacheIndex = plugins.findIndex( (candidate) => candidate.name === "vinext:server-function-directives", ); const useServerIndex = plugins.findIndex((candidate) => candidate.name === "rsc:use-server"); - const metadataIndex = plugins.findIndex( - (candidate) => candidate.name === "vinext:server-function-directive-metadata", - ); expect(useCacheIndex).toBeLessThan(useServerIndex); - expect(metadataIndex).toBeGreaterThan(useServerIndex); const context = { environment: { name: "rsc", mode: "build" } }; const transformed = await unwrapHook(plugins[useCacheIndex]!.transform)!.call( @@ -75,17 +71,14 @@ describe("plugin-rsc inline use-cache references", () => { inlineCacheCode, moduleId, ); - expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); + expect(manager.serverReferences.metaMap.get(moduleId)).toBeDefined(); await unwrapHook(plugins[useServerIndex]!.transform)!.call( context, transformed!.code, moduleId, ); - expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); - - unwrapHook(plugins[metadataIndex]!.transform)!.call(context, transformed!.code, moduleId); - expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); + expect(manager.serverReferences.metaMap.get(moduleId)).toBeDefined(); const ssrContext = { environment: { name: "ssr", mode: "build" } }; const proxied = await unwrapHook(plugins[useCacheIndex]!.transform)!.call( @@ -94,23 +87,19 @@ describe("plugin-rsc inline use-cache references", () => { moduleId, ); await unwrapHook(plugins[useServerIndex]!.transform)!.call(ssrContext, proxied!.code, moduleId); - unwrapHook(plugins[metadataIndex]!.transform)!.call(ssrContext, proxied!.code, moduleId); - expect(manager.serverReferenceMetaMap[moduleId]).toMatchObject({ + expect(manager.serverReferences.metaMap.get(moduleId)).toMatchObject({ importId: moduleId, exportNames: expect.arrayContaining(["getData"]), }); }); - it("merges and deduplicates use-server and user-land reference metadata", async () => { + it("aggregates use-server and vinext claims", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const useCachePlugin = plugins.find( (candidate) => candidate.name === "vinext:server-function-directives", )!; const useServerPlugin = plugins.find((candidate) => candidate.name === "rsc:use-server")!; - const metadataPlugin = plugins.find( - (candidate) => candidate.name === "vinext:server-function-directive-metadata", - )!; const context = { environment: { name: "rsc", mode: "build" } }; const source = [ `export async function action() {`, @@ -132,79 +121,98 @@ describe("plugin-rsc inline use-cache references", () => { useCacheResult!.code, moduleId, ); - const upstreamMetadata = manager.serverReferenceMetaMap[moduleId]; - const upstreamExport = upstreamMetadata.exportNames[0]!; - upstreamMetadata.exportNames.push(upstreamExport); - - unwrapHook(metadataPlugin.transform)!.call(context, useServerResult!.code, moduleId); - - const merged = manager.serverReferenceMetaMap[moduleId]; - expect(merged.importId).toBe(upstreamMetadata.importId); - expect(merged.referenceKey).toBe(upstreamMetadata.referenceKey); - expect(merged.exportNames).toContain(upstreamExport); + expect(useServerResult!.code).toContain("$$VinextReactServer.registerServerReference"); + expect(() => parseAst(useServerResult!.code)).not.toThrow(); + const claims = manager.serverReferences.claimMap.get(moduleId); + expect([...claims.keys()]).toEqual(["vinext:server-function-directives", "rsc:use-server"]); + + const merged = manager.serverReferences.metaMap.get(moduleId)!; + expect(merged.importId).toBe(moduleId); + expect(merged.exportNames).toContainEqual(expect.stringMatching(/action/)); expect(merged.exportNames).toContainEqual(expect.stringMatching(/getData/)); expect(merged.exportNames).toHaveLength(new Set(merged.exportNames).size); }); - it("restores RSC-owned metadata after a non-owning proxy pass", async () => { + it("preserves the vinext claim when transformed code enters a proxy graph", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const useCachePlugin = plugins.find( (candidate) => candidate.name === "vinext:server-function-directives", )!; const useServerPlugin = plugins.find((candidate) => candidate.name === "rsc:use-server")!; - const metadataPlugin = plugins.find( - (candidate) => candidate.name === "vinext:server-function-directive-metadata", - )!; const rscContext = { environment: { name: "rsc", mode: "build" } }; const transformed = await unwrapHook(useCachePlugin.transform)!.call( rscContext, inlineCacheCode, moduleId, ); - const ownedExportNames = manager.serverReferenceMetaMap[moduleId].exportNames; + const ownedExportNames = manager.serverReferences.metaMap.get(moduleId)!.exportNames; const ssrContext = { environment: { name: "ssr", mode: "build" } }; await unwrapHook(useCachePlugin.transform)!.call(ssrContext, transformed!.code, moduleId); await unwrapHook(useServerPlugin.transform)!.call(ssrContext, transformed!.code, moduleId); - expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); - - unwrapHook(metadataPlugin.transform)!.call(ssrContext, transformed!.code, moduleId); - expect(manager.serverReferenceMetaMap[moduleId].exportNames).toEqual(ownedExportNames); + expect(manager.serverReferences.metaMap.get(moduleId)!.exportNames).toEqual(ownedExportNames); }); - it("does not let a stale proxy transform resurrect RSC-removed metadata", async () => { + it("removes the vinext claim when the directive is removed", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const useCachePlugin = plugins.find( (candidate) => candidate.name === "vinext:server-function-directives", )!; - const metadataPlugin = plugins.find( - (candidate) => candidate.name === "vinext:server-function-directive-metadata", - )!; const rscContext = { environment: { name: "rsc", mode: "build" } }; const ssrContext = { environment: { name: "ssr", mode: "build" } }; await unwrapHook(useCachePlugin.transform)!.call(rscContext, fileCacheCode, moduleId); - expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); + await unwrapHook(useCachePlugin.transform)!.call(ssrContext, fileCacheCode, moduleId); + expect(manager.serverReferences.metaMap.get(moduleId)).toBeDefined(); - await unwrapHook(useCachePlugin.transform)!.call( - rscContext, - `export async function getData() { return 1; }`, - moduleId, - ); - expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); + const source = `export async function getData() { return 1; }`; + await unwrapHook(useCachePlugin.transform)!.call(rscContext, source, moduleId); + await unwrapHook(useCachePlugin.transform)!.call(ssrContext, source, moduleId); + expect(manager.serverReferences.metaMap.get(moduleId)).toBeUndefined(); + }); - const staleProxy = await unwrapHook(useCachePlugin.transform)!.call( - ssrContext, - fileCacheCode, - moduleId, - ); - unwrapHook(metadataPlugin.transform)!.call(ssrContext, staleProxy!.code, moduleId); - expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); + it("hands a file-level reference between vinext and rsc:use-server", async () => { + const plugins = await getPlugins(); + const manager = await configurePluginRsc(plugins); + const useCachePlugin = plugins.find( + (candidate) => candidate.name === "vinext:server-function-directives", + )!; + const useServerPlugin = plugins.find((candidate) => candidate.name === "rsc:use-server")!; + const context = { environment: { name: "rsc", mode: "build" } }; + const useServerCode = [ + `"use server";`, + `export async function getData() {`, + ` return 1;`, + `}`, + ].join("\n"); - await unwrapHook(useCachePlugin.transform)!.call(rscContext, fileCacheCode, moduleId); - expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); + const transform = async (source: string) => { + const useCacheResult = await unwrapHook(useCachePlugin.transform)!.call( + context, + source, + moduleId, + ); + await unwrapHook(useServerPlugin.transform)!.call( + context, + useCacheResult?.code ?? source, + moduleId, + ); + }; + + await transform(fileCacheCode); + expect([...manager.serverReferences.claimMap.get(moduleId).keys()]).toEqual([ + "vinext:server-function-directives", + ]); + + await transform(useServerCode); + expect([...manager.serverReferences.claimMap.get(moduleId).keys()]).toEqual(["rsc:use-server"]); + + await transform(fileCacheCode); + expect([...manager.serverReferences.claimMap.get(moduleId).keys()]).toEqual([ + "vinext:server-function-directives", + ]); }); it("matches Vite's dev reference key for files outside the project root", async () => { @@ -230,7 +238,7 @@ describe("plugin-rsc inline use-cache references", () => { ); const expectedKey = path.posix.join("/@fs/", externalId); expect(result!.code).toContain(JSON.stringify(expectedKey)); - expect(manager.serverReferenceMetaMap[externalId].referenceKey).toBe(expectedKey); + expect(manager.serverReferences.metaMap.get(externalId)!.referenceKey).toBe(expectedKey); }); it("wraps and registers inline cache functions with plugin-rsc's build reference key", async () => { @@ -251,10 +259,10 @@ describe("plugin-rsc inline use-cache references", () => { .update(manager.toRelativeId(moduleId)) .digest("hex") .slice(0, 12); - expect(result!.code).toContain("$$ReactServer.registerServerReference"); + expect(result!.code).toContain("$$VinextReactServer.registerServerReference"); expect(result!.code).toContain("registerCachedFunction"); expect(result!.code).toContain(JSON.stringify(expectedKey)); - expect(manager.serverReferenceMetaMap[moduleId]).toEqual({ + expect(manager.serverReferences.metaMap.get(moduleId)).toEqual({ importId: moduleId, referenceKey: expectedKey, exportNames: [expect.stringMatching(/^\$\$hoist_[a-z0-9]+_0_getData$/)], @@ -286,7 +294,7 @@ describe("plugin-rsc inline use-cache references", () => { expect(getDataName(withUnrelated!.code)).toBe(getDataName(original!.code)); }); - it("leaves directive removal cleanup to the preceding use-server pass", async () => { + it("removes its claim when the directive is removed", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const plugin = plugins.find( @@ -296,7 +304,7 @@ describe("plugin-rsc inline use-cache references", () => { const useServerPlugin = plugins.find((candidate) => candidate.name === "rsc:use-server")!; const context = { environment: { name: "rsc", mode: "build" } }; await transform.call(context, inlineCacheCode, moduleId); - expect(manager.serverReferenceMetaMap[moduleId]).toBeDefined(); + expect(manager.serverReferences.metaMap.get(moduleId)).toBeDefined(); const source = `export async function getData() { return 1; }`; const useServerResult = await unwrapHook(useServerPlugin.transform)!.call( context, @@ -304,7 +312,7 @@ describe("plugin-rsc inline use-cache references", () => { moduleId, ); await transform.call(context, useServerResult?.code ?? source, moduleId); - expect(manager.serverReferenceMetaMap[moduleId]).toBeUndefined(); + expect(manager.serverReferences.metaMap.get(moduleId)).toBeUndefined(); }); it("encrypts closure captures and reports bound-argument metadata to vinext", async () => { @@ -428,7 +436,7 @@ describe("plugin-rsc inline use-cache references", () => { expect(result!.code).toContain("registerCachedFunction(alias"); expect(result!.code).toContain("registerCachedFunction(named"); expect(result!.code).toContain("registerCachedFunction(imported"); - expect(manager.serverReferenceMetaMap[moduleId].exportNames).toEqual( + expect(manager.serverReferences.metaMap.get(moduleId)!.exportNames).toEqual( expect.arrayContaining(["direct", "alias", "named", "renamed", "default"]), ); }); @@ -588,10 +596,10 @@ describe("plugin-rsc inline use-cache references", () => { moduleId, ); expect(result).not.toBeNull(); - expect(result!.code).toContain("$$ReactServer.registerServerReference"); + expect(result!.code).toContain("$$VinextReactServer.registerServerReference"); expect(result!.code).toContain("registerCachedFunction"); expect(result!.code).not.toContain('"use cache";'); - expect(manager.serverReferenceMetaMap[moduleId].exportNames).toEqual(["getData"]); + expect(manager.serverReferences.metaMap.get(moduleId)!.exportNames).toEqual(["getData"]); }); it.each(["ssr", "client"])( From a1c2e6740a78e80f3d126c77f94f37a8bd5e3bd6 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 24 Jul 2026 12:21:29 +0100 Subject: [PATCH 23/33] fix(init): install required plugin-rsc prerelease --- packages/vinext/src/deploy.ts | 5 ++++- packages/vinext/src/init.ts | 8 +++++++- .../vinext/src/plugins/server-function-directives.ts | 5 +++++ tests/deploy.test.ts | 5 ++++- tests/init.test.ts | 9 ++++++++- 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/vinext/src/deploy.ts b/packages/vinext/src/deploy.ts index b74f98cb84..a11e5cc283 100644 --- a/packages/vinext/src/deploy.ts +++ b/packages/vinext/src/deploy.ts @@ -811,7 +811,10 @@ export function getMissingDeps( missing.push({ name: "@vitejs/plugin-react", version: "latest" }); } if (info.isAppRouter && !info.hasRscPlugin) { - missing.push({ name: "@vitejs/plugin-rsc", version: "latest" }); + missing.push({ + name: "@vitejs/plugin-rsc", + version: "https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476", + }); } if (info.isAppRouter) { // react-server-dom-webpack must be resolvable from the project root for Vite. diff --git a/packages/vinext/src/init.ts b/packages/vinext/src/init.ts index 937705022e..3cc4332ea9 100644 --- a/packages/vinext/src/init.ts +++ b/packages/vinext/src/init.ts @@ -121,6 +121,8 @@ export function addScripts(root: string, port: number): string[] { // ─── Dependency Installation ───────────────────────────────────────────────── +const PLUGIN_RSC_INSTALL_SPEC = "@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476"; + export function getInitDeps(isAppRouter: boolean): string[] { const deps = ["vinext", "vite", "@vitejs/plugin-react"]; if (isAppRouter) { @@ -308,7 +310,11 @@ export async function init(options: InitOptions): Promise { if (missingDeps.length > 0) { console.log(` Installing ${missingDeps.join(", ")}...`); - installDeps(root, missingDeps, exec); + installDeps( + root, + missingDeps.map((dep) => (dep === "@vitejs/plugin-rsc" ? PLUGIN_RSC_INSTALL_SPEC : dep)), + exec, + ); console.log(); } diff --git a/packages/vinext/src/plugins/server-function-directives.ts b/packages/vinext/src/plugins/server-function-directives.ts index 2dd6982b21..d9129ef89b 100644 --- a/packages/vinext/src/plugins/server-function-directives.ts +++ b/packages/vinext/src/plugins/server-function-directives.ts @@ -224,6 +224,11 @@ export async function createServerFunctionDirectivePlugin(options: Options): Pro if (!manager) { throw new Error("vinext: failed to access @vitejs/plugin-rsc through getPluginApi()."); } + if (!manager.serverReferences) { + throw new Error( + "vinext: Installed @vitejs/plugin-rsc does not support user-land server reference claims.", + ); + } if (active.length === 0) { manager.serverReferences.deleteClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id); return; diff --git a/tests/deploy.test.ts b/tests/deploy.test.ts index 704954c1c7..d94d41a5c8 100644 --- a/tests/deploy.test.ts +++ b/tests/deploy.test.ts @@ -1273,7 +1273,10 @@ describe("getMissingDeps", () => { info.hasRscPlugin = false; const missing = getMissingDeps(info); - expect(missing).toContainEqual(expect.objectContaining({ name: "@vitejs/plugin-rsc" })); + expect(missing).toContainEqual({ + name: "@vitejs/plugin-rsc", + version: "https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476", + }); }); it("does not require @vitejs/plugin-rsc for Pages Router", () => { diff --git a/tests/init.test.ts b/tests/init.test.ts index a55b64b51c..5b0edd2e04 100644 --- a/tests/init.test.ts +++ b/tests/init.test.ts @@ -517,10 +517,17 @@ describe("init — dependency installation", () => { it("detects missing @vitejs/plugin-rsc for App Router", async () => { setupProject(tmpDir, { router: "app" }); - const { result } = await runInit(tmpDir); + const { result, execCalls } = await runInit(tmpDir); expect(result.installedDeps).toContain("@vitejs/plugin-react"); expect(result.installedDeps).toContain("@vitejs/plugin-rsc"); + expect(execCalls).toContainEqual( + expect.objectContaining({ + cmd: expect.stringContaining( + "@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476", + ), + }), + ); }); it("treats src/app projects as App Router", async () => { From 442d760d66b8cfd1e8a50f4cbba5bf1a0ae623ac Mon Sep 17 00:00:00 2001 From: James Date: Mon, 27 Jul 2026 20:26:02 +0100 Subject: [PATCH 24/33] feat(rsc): harden use cache server functions --- packages/vinext/src/index.ts | 3 +- .../src/plugins/server-function-directives.ts | 5 +- tests/e2e/app-router-prod/use-cache.spec.ts | 68 +++++++++++- tests/e2e/app-router/use-cache.spec.ts | 102 +++++++++++++++++- .../app/use-cache-client-import/form.tsx | 26 +++-- .../app/use-cache-mixed-ownership/actions.ts | 11 ++ .../app/use-cache-mixed-ownership/client.tsx | 50 +++++++++ .../app/use-cache-mixed-ownership/page.tsx | 11 ++ .../app/use-cache-nested-fn-props/form.tsx | 18 ++-- .../app/use-cache-nested-fn-props/page.tsx | 14 ++- .../server-boundary-client.tsx | 9 +- .../server-boundary.ts | 2 +- tests/use-cache-transform.test.ts | 6 ++ 13 files changed, 298 insertions(+), 27 deletions(-) create mode 100644 tests/fixtures/app-basic/app/use-cache-mixed-ownership/actions.ts create mode 100644 tests/fixtures/app-basic/app/use-cache-mixed-ownership/client.tsx create mode 100644 tests/fixtures/app-basic/app/use-cache-mixed-ownership/page.tsx diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 298e25bf26..703a116d96 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -976,6 +976,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { id, directiveMatch, location, + hasBoundArgs, parameters, runtime, }: ServerFunctionDirectiveContext) => { @@ -991,7 +992,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { fileMatcher.extensionRegex.test(moduleFileName); const runtimeOptions = { ...(isAppPageDefault ? { appPageDefaultExport: true } : {}), - ...(parameters ? { parameters } : {}), + ...(parameters && !hasBoundArgs ? { parameters } : {}), }; const pageOptions = Object.keys(runtimeOptions).length > 0 diff --git a/packages/vinext/src/plugins/server-function-directives.ts b/packages/vinext/src/plugins/server-function-directives.ts index d9129ef89b..5920a1a27e 100644 --- a/packages/vinext/src/plugins/server-function-directives.ts +++ b/packages/vinext/src/plugins/server-function-directives.ts @@ -370,7 +370,10 @@ export async function createServerFunctionDirectivePlugin(options: Options): Pro needsReactRuntime = true; if (meta.hasBoundArgs) { needsEncryptionRuntime = true; - return `$$VinextReactServer.registerServerReference((($$wrapped) => async ($$encoded, ...$$args) => $$wrapped(...await __vite_rsc_encryption_runtime.decryptActionBoundArgs($$encoded), ...$$args))(${wrapped}), ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; + const forwardedArgs = meta.parameters.hasRest + ? "$$args" + : `$$args.slice(0, ${meta.parameters.count})`; + return `$$VinextReactServer.registerServerReference((($$wrapped) => async ($$encoded, ...$$args) => $$wrapped(...await __vite_rsc_encryption_runtime.decryptActionBoundArgs($$encoded), ...${forwardedArgs}))(${wrapped}), ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; } return `$$VinextReactServer.registerServerReference(${wrapped}, ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; }, diff --git a/tests/e2e/app-router-prod/use-cache.spec.ts b/tests/e2e/app-router-prod/use-cache.spec.ts index ef915601d8..ec2f6ed48a 100644 --- a/tests/e2e/app-router-prod/use-cache.spec.ts +++ b/tests/e2e/app-router-prod/use-cache.spec.ts @@ -1,12 +1,69 @@ import { expect, test } from "@playwright/test"; test.describe('production "use cache" server function references', () => { - test("invokes file-level cached exports imported by a Client Component", async ({ page }) => { + test("separates arguments for file-level cached exports imported by a Client Component", async ({ + page, + }) => { await page.goto("/use-cache-client-import"); + await page.locator("#call-client-imported-cache").click(); + await expect(page.getByTestId("client-imported-cache-call-count")).toHaveText("1"); await expect(page.getByTestId("client-imported-cache-result")).toHaveText( /^client-cache:direct:[0-9.e+-]+$/, ); + const directResult = await page.getByTestId("client-imported-cache-result").innerText(); + + await page.locator("#call-client-imported-cache-other").click(); + await expect(page.getByTestId("client-imported-cache-call-count")).toHaveText("2"); + await expect(page.getByTestId("client-imported-cache-result")).toHaveText( + /^client-cache:other:[0-9.e+-]+$/, + ); + const otherResult = await page.getByTestId("client-imported-cache-result").innerText(); + expect(otherResult).not.toBe(directResult); + + await page.locator("#call-client-imported-cache").click(); + await expect(page.getByTestId("client-imported-cache-call-count")).toHaveText("3"); + await expect(page.getByTestId("client-imported-cache-result")).toHaveText(directResult); + }); + + test("runs inline use-server and use-cache exports owned by different plugins", async ({ + page, + }) => { + await page.goto("/use-cache-mixed-ownership"); + await expect(page.getByTestId("use-cache-mixed-ownership-page")).toBeVisible(); + + await page.locator("#call-mixed-builtin").click(); + await expect(page.getByTestId("mixed-builtin-call-count")).toHaveText("1"); + await expect(page.getByTestId("mixed-builtin-result")).toHaveText("builtin"); + + await page.locator("#call-mixed-flexible").click(); + await expect(page.getByTestId("mixed-flexible-call-count")).toHaveText("1"); + const cachedResult = await page.getByTestId("mixed-flexible-result").innerText(); + expect(cachedResult).toMatch(/^cached:[0-9.e+-]+$/); + + await page.locator("#call-mixed-flexible").click(); + await expect(page.getByTestId("mixed-flexible-call-count")).toHaveText("2"); + await expect(page.getByTestId("mixed-flexible-result")).toHaveText(cachedResult); + }); + + test('caches an inline "use cache" function inside a file-level "use server" module', async ({ + page, + }) => { + await page.goto("/use-cache-transform-coverage"); + + const aggregateResult = await page.getByTestId("use-cache-transform-coverage").innerText(); + const serverResult = aggregateResult.split("|")[4]; + if (!serverResult) throw new Error("Missing server-boundary result"); + expect(serverResult).toMatch(/^server-boundary:[0-9.e+-]+$/); + + await page.locator("#call-cached-server-boundary").click(); + await expect(page.getByTestId("cached-server-boundary-call-count")).toHaveText("1"); + const firstActionResult = await page.getByTestId("cached-server-boundary-result").innerText(); + expect(firstActionResult).toMatch(/^server-boundary:[0-9.e+-]+$/); + + await page.locator("#call-cached-server-boundary").click(); + await expect(page.getByTestId("cached-server-boundary-call-count")).toHaveText("2"); + await expect(page.getByTestId("cached-server-boundary-result")).toHaveText(firstActionResult); }); test("replays cached RSC through SSR and invokes nested functions from the browser", async ({ @@ -34,5 +91,14 @@ test.describe('production "use cache" server function references', () => { const firstMessage = await page.locator("#message").textContent(); await page.locator("#submit-button-message").click(); await expect(page.locator("#message")).toHaveText(firstMessage!); + + await page.locator("#submit-button-message-other").click(); + await expect(page.locator("#message-other")).toHaveText( + /^message:closure-captured-bound-arg-other:[0-9.e+-]+$/, + ); + const otherMessage = await page.locator("#message-other").textContent(); + expect(otherMessage).not.toBe(firstMessage); + await page.locator("#submit-button-message-other").click(); + await expect(page.locator("#message-other")).toHaveText(otherMessage!); }); }); diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index 44fd145187..03c984c545 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -7,6 +7,10 @@ const USE_CACHE_HMR_ACTIONS_FILE = path.join( process.cwd(), "tests/fixtures/app-basic/app/use-cache-hmr/actions.ts", ); +const USE_CACHE_MIXED_HMR_ACTIONS_FILE = path.join( + process.cwd(), + "tests/fixtures/app-basic/app/use-cache-mixed-ownership/actions.ts", +); const USE_CACHE_HMR_CACHED = `"use cache"; export async function getMode() { @@ -19,6 +23,34 @@ export async function getMode() { return "plain"; } `; +const USE_CACHE_MIXED_HMR_CACHED = [ + `export const ownershipLabel = "cache";`, + ``, + `export async function builtinAction() {`, + ` "use server";`, + ` return "builtin";`, + `}`, + ``, + `export async function flexibleAction() {`, + ` "use cache";`, + ` return \`cached:\${Math.random()}\`;`, + `}`, + ``, +].join("\n"); +const USE_CACHE_MIXED_HMR_SERVER = [ + `export const ownershipLabel = "server";`, + ``, + `export async function builtinAction() {`, + ` "use server";`, + ` return "builtin";`, + `}`, + ``, + `export async function flexibleAction() {`, + ` "use server";`, + ` return "server";`, + `}`, + ``, +].join("\n"); async function writeUseCacheHmrActions(content: string, forceUpdate = false) { const nextContent = forceUpdate ? `${content}// hmr-update:${Date.now()}\n` : content; @@ -36,6 +68,13 @@ async function waitForUseCacheHmrTransform(request: APIRequestContext) { .toBe(true); } +async function writeUseCacheMixedHmrActions(content: string, forceUpdate = false) { + const nextContent = forceUpdate ? `${content}// hmr-update:${Date.now()}\n` : content; + if ((await readFile(USE_CACHE_MIXED_HMR_ACTIONS_FILE, "utf8")) !== nextContent) { + await writeFile(USE_CACHE_MIXED_HMR_ACTIONS_FILE, nextContent); + } +} + test.describe('"use cache" file-level directive', () => { test("use-cache page renders correctly", async ({ page }) => { await page.goto(`${BASE}/use-cache-test`); @@ -150,12 +189,12 @@ test.describe('"use cache" transform coverage', () => { test("supports advanced function and export forms", async ({ page }) => { await page.goto(`${BASE}/use-cache-transform-coverage`); await expect(page.getByTestId("use-cache-transform-coverage")).toHaveText( - "destructured|export-star|object-method|static-method|server-boundary|custom-kind", + /^destructured\|export-star\|object-method\|static-method\|server-boundary:[0-9.e+-]+\|custom-kind$/, ); await expect(async () => { await page.locator("#call-cached-server-boundary").click(); await expect(page.getByTestId("cached-server-boundary-result")).toHaveText( - "server-boundary", + /^server-boundary:[0-9.e+-]+$/, { timeout: 2000 }, ); }).toPass({ timeout: 15_000 }); @@ -195,6 +234,65 @@ test.describe('"use cache" transform coverage', () => { await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED); } }); + + test("moves one export between use-cache and use-server ownership without reloading", async ({ + page, + }) => { + await writeUseCacheMixedHmrActions(USE_CACHE_MIXED_HMR_CACHED, true); + try { + await page.goto(`${BASE}/use-cache-mixed-ownership`); + await expect(page.getByTestId("mixed-ownership-label")).toHaveText("cache"); + await expect(async () => { + await page.locator("#call-mixed-builtin").click(); + await expect(page.getByTestId("mixed-builtin-result")).toHaveText("builtin", { + timeout: 2000, + }); + await page.locator("#call-mixed-flexible").click(); + await expect(page.getByTestId("mixed-flexible-result")).toHaveText(/^cached:[0-9.e+-]+$/, { + timeout: 2000, + }); + }).toPass({ timeout: 15_000 }); + await page.evaluate(() => Reflect.set(window, "__vinextMixedOwnershipHmr", true)); + + await writeUseCacheMixedHmrActions(USE_CACHE_MIXED_HMR_SERVER, true); + await expect(page.getByTestId("mixed-ownership-label")).toHaveText("server", { + timeout: 15_000, + }); + expect(await page.evaluate(() => Reflect.get(window, "__vinextMixedOwnershipHmr"))).toBe( + true, + ); + await expect(async () => { + await page.locator("#call-mixed-builtin").click(); + await expect(page.getByTestId("mixed-builtin-result")).toHaveText("builtin", { + timeout: 2000, + }); + await page.locator("#call-mixed-flexible").click(); + await expect(page.getByTestId("mixed-flexible-result")).toHaveText("server", { + timeout: 2000, + }); + }).toPass({ timeout: 15_000 }); + + await writeUseCacheMixedHmrActions(USE_CACHE_MIXED_HMR_CACHED, true); + await expect(page.getByTestId("mixed-ownership-label")).toHaveText("cache", { + timeout: 15_000, + }); + expect(await page.evaluate(() => Reflect.get(window, "__vinextMixedOwnershipHmr"))).toBe( + true, + ); + await expect(async () => { + await page.locator("#call-mixed-builtin").click(); + await expect(page.getByTestId("mixed-builtin-result")).toHaveText("builtin", { + timeout: 2000, + }); + await page.locator("#call-mixed-flexible").click(); + await expect(page.getByTestId("mixed-flexible-result")).toHaveText(/^cached:[0-9.e+-]+$/, { + timeout: 2000, + }); + }).toPass({ timeout: 15_000 }); + } finally { + await writeUseCacheMixedHmrActions(USE_CACHE_MIXED_HMR_CACHED); + } + }); }); test.describe('"use cache" nested cache functions as props', () => { diff --git a/tests/fixtures/app-basic/app/use-cache-client-import/form.tsx b/tests/fixtures/app-basic/app/use-cache-client-import/form.tsx index a80152df09..874bb7c6d5 100644 --- a/tests/fixtures/app-basic/app/use-cache-client-import/form.tsx +++ b/tests/fixtures/app-basic/app/use-cache-client-import/form.tsx @@ -5,20 +5,30 @@ import { getCachedMessage } from "./actions"; export function ClientCacheCaller() { const [message, setMessage] = useState(""); + const [completedCalls, setCompletedCalls] = useState(0); + + function callCachedMessage(value: string) { + void getCachedMessage(value).then( + (result) => { + setMessage(result); + setCompletedCalls((count) => count + 1); + }, + (error) => { + setMessage(`error:${error instanceof Error ? error.message : String(error)}`); + }, + ); + } return (
- + {message} + {completedCalls}
); } diff --git a/tests/fixtures/app-basic/app/use-cache-mixed-ownership/actions.ts b/tests/fixtures/app-basic/app/use-cache-mixed-ownership/actions.ts new file mode 100644 index 0000000000..172b10eec9 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-mixed-ownership/actions.ts @@ -0,0 +1,11 @@ +export const ownershipLabel = "cache"; + +export async function builtinAction() { + "use server"; + return "builtin"; +} + +export async function flexibleAction() { + "use cache"; + return `cached:${Math.random()}`; +} diff --git a/tests/fixtures/app-basic/app/use-cache-mixed-ownership/client.tsx b/tests/fixtures/app-basic/app/use-cache-mixed-ownership/client.tsx new file mode 100644 index 0000000000..ec9840ba0c --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-mixed-ownership/client.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useState } from "react"; + +type ServerFunction = () => Promise; + +export function MixedOwnershipClient({ + builtinAction, + flexibleAction, +}: { + builtinAction: ServerFunction; + flexibleAction: ServerFunction; +}) { + const [builtinResult, setBuiltinResult] = useState(""); + const [builtinCalls, setBuiltinCalls] = useState(0); + const [flexibleResult, setFlexibleResult] = useState(""); + const [flexibleCalls, setFlexibleCalls] = useState(0); + + return ( +
+ + {builtinResult} + {builtinCalls} + + + {flexibleResult} + {flexibleCalls} +
+ ); +} diff --git a/tests/fixtures/app-basic/app/use-cache-mixed-ownership/page.tsx b/tests/fixtures/app-basic/app/use-cache-mixed-ownership/page.tsx new file mode 100644 index 0000000000..3475ce14bd --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-mixed-ownership/page.tsx @@ -0,0 +1,11 @@ +import { builtinAction, flexibleAction, ownershipLabel } from "./actions"; +import { MixedOwnershipClient } from "./client"; + +export default function UseCacheMixedOwnershipPage() { + return ( +
+ {ownershipLabel} + +
+ ); +} diff --git a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/form.tsx b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/form.tsx index f6a8662e43..1b98660d66 100644 --- a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/form.tsx +++ b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/form.tsx @@ -9,13 +9,15 @@ export function Form({ getDate, getRandom, getMessage, + idSuffix, }: { getDate: () => Promise; getRandom: () => Promise; - // Closure-capturing cached function — exercises bound-arg serialization - // (the captured value travels as an unencrypted `.bind(null, ...)` arg). + // Closure-capturing cached function — exercises bound-arg serialization. getMessage: () => Promise; + idSuffix?: string; }) { + const suffix = idSuffix ? `-${idSuffix}` : ""; const [date, formAction, isDatePending] = useActionState(getDate, null); const [random, buttonAction, isRandomPending] = useActionState(getRandom, null); @@ -24,16 +26,16 @@ export function Form({ return ( - {" "} - {" "} + {" "} - -

{isDatePending ? "loading..." : date}

-

{isRandomPending ? "loading..." : random}

-

{isMessagePending ? "loading..." : message}

+

{isDatePending ? "loading..." : date}

+

{isRandomPending ? "loading..." : random}

+

{isMessagePending ? "loading..." : message}

); } diff --git a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx index 894d59acc1..ccd4af92b4 100644 --- a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx +++ b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx @@ -16,22 +16,28 @@ export default function UseCacheNestedFnPropsPage() { Loading...}> - + + ); } -async function CachedForm() { +async function CachedForm({ + capturedScopeValue, + idSuffix, +}: { + capturedScopeValue: string; + idSuffix?: string; +}) { "use cache"; // Closure-captured by getMessage below. The hoist transform lifts the // capture into a `.bind(null, capturedScopeValue)` bound argument on the // server reference. The binding is encrypted before RSC serialization and // decrypted before the cached wrapper builds its argument-based cache key. - const capturedScopeValue = "closure-captured-bound-arg-vinext"; - return (
{ "use cache"; return new Date().toISOString(); diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary-client.tsx b/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary-client.tsx index f197377fab..b449b5d17f 100644 --- a/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary-client.tsx +++ b/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary-client.tsx @@ -5,16 +5,23 @@ import { fromServerBoundary } from "./server-boundary"; export function ServerBoundaryClientCaller() { const [value, setValue] = useState(""); + const [completedCalls, setCompletedCalls] = useState(0); return (
{value} + {completedCalls}
); } diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary.ts b/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary.ts index 3ac3300496..aefd9fe73e 100644 --- a/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary.ts +++ b/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary.ts @@ -2,5 +2,5 @@ export async function fromServerBoundary() { "use cache"; - return "server-boundary"; + return `server-boundary:${Math.random()}`; } diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index d5d961ceb1..4cdfa56004 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -345,6 +345,12 @@ describe("plugin-rsc inline use-cache references", () => { ); expect(result!.code).not.toMatch(/\.bind\(null,\s*capturedSecret\)/); expect(result!.code).toContain("decryptActionBoundArgs($$encoded)"); + expect(result!.code).toContain("...$$args.slice(0, 0)"); + const boundRegistration = result!.code.match( + /registerCachedFunction\(\$\$hoist_[^,]+_getMessage\$\$impl,[^)]*\)/, + )?.[0]; + expect(boundRegistration).toBeDefined(); + expect(boundRegistration).not.toContain('"parameters"'); }); it.each(["ssr", "client"])( From 8a737248cb850e256c0bc37305b0d1b1d16964c0 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 6 Aug 2026 17:17:28 +0100 Subject: [PATCH 25/33] feat(cache): adopt plugin-rsc transform primitives --- packages/vinext/package.json | 2 +- packages/vinext/src/index.ts | 89 +-- .../src/plugins/server-function-directives.ts | 584 +++++++----------- .../src/shims/cache-callable-runtime.ts | 56 ++ packages/vinext/src/shims/cache-runtime.ts | 35 +- pnpm-lock.yaml | 106 ++-- pnpm-workspace.yaml | 4 +- tests/e2e/app-router-prod/use-cache.spec.ts | 15 +- tests/e2e/app-router/use-cache.spec.ts | 11 +- .../use-cache-transform-coverage/methods.ts | 13 - .../app/use-cache-transform-coverage/page.tsx | 19 +- .../server-boundary-client.tsx | 27 - .../star-source.ts | 3 - .../app/use-cache-transform-coverage/star.ts | 3 - tests/shims.test.ts | 16 +- tests/use-cache-transform.test.ts | 117 +--- 16 files changed, 380 insertions(+), 720 deletions(-) create mode 100644 packages/vinext/src/shims/cache-callable-runtime.ts delete mode 100644 tests/fixtures/app-basic/app/use-cache-transform-coverage/methods.ts delete mode 100644 tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary-client.tsx delete mode 100644 tests/fixtures/app-basic/app/use-cache-transform-coverage/star-source.ts delete mode 100644 tests/fixtures/app-basic/app/use-cache-transform-coverage/star.ts diff --git a/packages/vinext/package.json b/packages/vinext/package.json index b9d522d28f..c4f0dfaa44 100644 --- a/packages/vinext/package.json +++ b/packages/vinext/package.json @@ -111,7 +111,7 @@ "peerDependencies": { "@mdx-js/rollup": "^3.0.0", "@vitejs/plugin-react": "^5.1.4 || ^6.0.0", - "@vitejs/plugin-rsc": "^0.5.26", + "@vitejs/plugin-rsc": "^0.5.33", "react": "^19.2.6", "react-dom": "^19.2.6", "react-server-dom-webpack": "^19.2.6", diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 703a116d96..7ddfe1589f 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -117,10 +117,7 @@ import { createMiddlewareServerOnlyPlugin } from "./plugins/middleware-server-on import { createOptimizeImportsPlugin } from "./plugins/optimize-imports.js"; import { createDynamicPreloadMetadataPlugin } from "./plugins/dynamic-preload-metadata.js"; import { createOgInlineFetchAssetsPlugin, createOgAssetsPlugin } from "./plugins/og-assets.js"; -import { - createServerFunctionDirectivePlugin, - type ServerFunctionDirectiveContext, -} from "./plugins/server-function-directives.js"; +import { createUseCacheCallablePlugin } from "./plugins/server-function-directives.js"; import { generateRouteTypes } from "./typegen.js"; import { mergeOptimizeDepsExclude, @@ -189,13 +186,7 @@ import { createRequire } from "node:module"; import fs from "node:fs"; import { randomBytes, randomUUID } from "node:crypto"; import commonjs from "vite-plugin-commonjs"; -import { normalizePathSeparators, stripViteModuleQuery } from "./utils/path.js"; - -function parseUseCacheVariant(directive: string): string { - return directive === "use cache" - ? "" - : directive.replace("use cache:", "").replace("use cache: ", "").trim(); -} +import { normalizePathSeparators } from "./utils/path.js"; // Install the process-level peer-disconnect backstop at module load. // Vite plugin lifecycle hooks (config / configureServer) proved @@ -207,11 +198,6 @@ installSocketErrorBackstop(); type ASTNode = ReturnType["body"][number]["parent"]; -function isInsideDirectory(dir: string, filePath: string): boolean { - const relativePath = path.relative(dir, filePath); - return relativePath !== "" && !relativePath.startsWith("..") && !path.isAbsolute(relativePath); -} - // Detect a module-level `"use server"` directive (a Server Actions module). // Directives form the leading prologue of string-literal ExpressionStatements, // so we only scan until the first non-directive statement. @@ -946,77 +932,18 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { client: VIRTUAL_APP_BROWSER_ENTRY, }, }); - const serverFunctionPlugin = await createServerFunctionDirectivePlugin({ + const useCachePlugin = await createUseCacheCallablePlugin({ projectRoot: earlyBaseDir, - definitions: [ - { - directive: /^use cache.*$/, - test: (code: string) => code.includes("use cache"), - filter: (id: string) => - /\.(tsx?|jsx?|mjs)$/.test(id) && !id.includes("/node_modules/"), - rejectNonAsyncFunction: true, - rejectNonAsyncModule: false, - runtime: pathToFileURL(resolveShimModulePath(shimsDir, "cache-runtime")).href, - validate: ({ directive }: { directive: string }) => { - if (!/^use cache(?:: ([^\s].*))?$/.test(directive)) { - const cacheKind = directive.includes(":") - ? directive.slice(directive.indexOf(":") + 1).trim() - : directive.slice("use cache".length).trim(); - const expected = cacheKind ? `use cache: ${cacheKind}` : "use cache"; - throw new Error( - `Invalid cache directive ${JSON.stringify(directive)}. Did you mean ${JSON.stringify(expected)}?`, - ); - } - }, - clientError: ({ id, environment }: { id: string; environment: string }) => - `It is not allowed to define inline "use cache" annotated functions in Client Components. Export them from a separate file with a module-level "use cache" or "use server" directive, or pass them down through props from a Server Component. (${environment}: ${id})`, - wrap: ({ - value, - name, - id, - directiveMatch, - location, - hasBoundArgs, - parameters, - runtime, - }: ServerFunctionDirectiveContext) => { - const variant = parseUseCacheVariant(directiveMatch[0]); - const modulePath = stripViteModuleQuery(id); - const moduleFileName = path.basename(modulePath); - const isAppPageDefault = - location === "module" && - name === "default" && - hasAppDir && - isInsideDirectory(appDir, modulePath) && - path.parse(moduleFileName).name === "page" && - fileMatcher.extensionRegex.test(moduleFileName); - const runtimeOptions = { - ...(isAppPageDefault ? { appPageDefaultExport: true } : {}), - ...(parameters && !hasBoundArgs ? { parameters } : {}), - }; - const pageOptions = - Object.keys(runtimeOptions).length > 0 - ? `, ${JSON.stringify(runtimeOptions)}` - : ""; - return `${runtime}.registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)}${pageOptions})`; - }, - filterExport: ({ name, id, meta }) => { - if (meta.isFunction === false) return false; - if (/\/(layout|template)\.(tsx?|jsx?|mjs)$/.test(id) && name === "default") { - return false; - } - return true; - }, - }, - ], - serverEnvironmentName: "rsc", - browserEnvironmentName: "client", + cacheRuntime: pathToFileURL(resolveShimModulePath(shimsDir, "cache-callable-runtime")) + .href, + appDir, + matchesPageExtension: (fileName) => fileMatcher.extensionRegex.test(fileName), }); const useServerIndex = plugins.findIndex((plugin) => plugin.name === "rsc:use-server"); if (useServerIndex === -1) { throw new Error("vinext: Failed to locate @vitejs/plugin-rsc use-server plugin."); } - plugins.splice(useServerIndex, 0, serverFunctionPlugin); + plugins.splice(useServerIndex, 0, useCachePlugin); return plugins; }) .catch((cause) => { diff --git a/packages/vinext/src/plugins/server-function-directives.ts b/packages/vinext/src/plugins/server-function-directives.ts index 5920a1a27e..6ac7814079 100644 --- a/packages/vinext/src/plugins/server-function-directives.ts +++ b/packages/vinext/src/plugins/server-function-directives.ts @@ -1,61 +1,33 @@ -import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { pathToFileURL } from "node:url"; import type { RscPluginManager } from "@vitejs/plugin-rsc"; -import type { SourceMap } from "magic-string"; -import type { Plugin, Rollup } from "vite"; -import { parseAstAsync, transformWithOxc } from "vite"; -import { isUnknownRecord } from "../utils/record.js"; -import { escapeRegExp } from "../utils/regex.js"; +import type { + ModuleExportMeta, + TransformHoistInlineDirectiveMeta, +} from "@vitejs/plugin-rsc/transforms"; +import { parseAstAsync, type Plugin } from "vite"; +import { stripViteModuleQuery } from "../utils/path.js"; type RscTransforms = typeof import("@vitejs/plugin-rsc/transforms"); -type Program = Parameters[0]; -type ProgramExpressionStatement = Extract; -type StringDirective = Extract & { - value: string; - start: number; - end: number; -}; -type ExportFilter = NonNullable[2]["filter"]>; -type ExportMeta = Parameters[1]; -type FunctionParameters = NonNullable; - -export type ServerFunctionDirectiveContext = { - value: string; - name: string; - id: string; - directiveMatch: RegExpMatchArray; - location: "inline" | "module"; - hasBoundArgs: boolean; - parameters?: FunctionParameters; - runtime?: string; - meta?: ExportMeta; -}; - -type ServerFunctionDirective = { - directive: string | RegExp; - test?: (code: string) => boolean; - filter?: (id: string) => boolean; - validate?: (context: { id: string; directive: string; location: "inline" | "module" }) => void; - rejectNonAsyncFunction?: boolean; - rejectNonAsyncModule?: boolean; - runtime?: string; - wrap: (context: ServerFunctionDirectiveContext) => string; - filterExport?: (context: { name: string; id: string; meta: ExportMeta }) => boolean; - clientError?: (context: { id: string; environment: string }) => string; -}; +type Program = Awaited>; type Options = { projectRoot: string; - definitions: ServerFunctionDirective[]; - serverEnvironmentName: string; - browserEnvironmentName: string; + cacheRuntime: string; + appDir: string | undefined; + matchesPageExtension: (fileName: string) => boolean; }; -const SERVER_FUNCTION_DIRECTIVE_MARKER = "/* __vinext_server_function_directives__ */"; -const SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME = "vinext:server-function-directives"; +type CacheWrapperOptions = { + appPageDefaultExport?: boolean; + argumentCount?: number; +}; + +const PLUGIN_NAME = "vinext:server-function-directives"; const USE_SERVER_PLUGIN_NAME = "rsc:use-server"; +const USE_CACHE_DIRECTIVE = /^use cache(?:: ([^\s].*))?$/; +const USE_CACHE_DIRECTIVE_CANDIDATE = /^use cache.*$/; function resolvePluginRscModule(projectRoot: string, specifier: string): string { try { @@ -69,369 +41,247 @@ function resolvePluginRscModule(projectRoot: string, specifier: string): string } } -async function parseProgram(code: string): Promise { - return (await parseAstAsync(code)) as unknown as Program; -} - -function matchDirective(value: string, directive: string | RegExp): RegExpMatchArray | undefined { - const pattern = - typeof directive === "string" - ? new RegExp(`^${escapeRegExp(directive)}$`) - : new RegExp(directive.source, directive.flags); - pattern.lastIndex = 0; - return value.match(pattern) ?? undefined; -} +function matchUseCacheDirective(directive: string): RegExpMatchArray { + const match = directive.match(USE_CACHE_DIRECTIVE); + if (match) return match; -function isStringLiteral(value: unknown): value is StringDirective { - return ( - isUnknownRecord(value) && - value.type === "Literal" && - typeof value.value === "string" && - typeof value.start === "number" && - typeof value.end === "number" + const cacheKind = directive.includes(":") + ? directive.slice(directive.indexOf(":") + 1).trim() + : directive.slice("use cache".length).trim(); + const expected = cacheKind ? `use cache: ${cacheKind}` : "use cache"; + throw new Error( + `Invalid cache directive ${JSON.stringify(directive)}. Did you mean ${JSON.stringify(expected)}?`, ); } -function isExpressionStatement( - value: unknown, -): value is Record & { type: "ExpressionStatement"; expression: unknown } { - return isUnknownRecord(value) && value.type === "ExpressionStatement" && "expression" in value; -} - -function isBlockStatement( - value: unknown, -): value is Record & { type: "BlockStatement"; body: unknown[] } { - return isUnknownRecord(value) && value.type === "BlockStatement" && Array.isArray(value.body); -} - -function findModuleDirective( - ast: Program, - directive: string | RegExp, -): StringDirective | undefined { - for (const node of ast.body) { - if (node.type !== "ExpressionStatement") continue; - if (isStringLiteral(node.expression) && matchDirective(node.expression.value, directive)) { - return node.expression; +function findModuleUseCacheDirective(ast: Program): string | undefined { + for (const statement of ast.body) { + if ( + statement.type !== "ExpressionStatement" || + !("directive" in statement) || + typeof statement.directive !== "string" + ) { + break; + } + if (statement.directive.startsWith("use cache")) { + return matchUseCacheDirective(statement.directive)[0]; } } } -function findInlineDirective( - ast: Program, - directive: string | RegExp, -): StringDirective | undefined { - let result: StringDirective | undefined; - - function visit(value: unknown): void { - if (result) return; - if (Array.isArray(value)) { - for (const child of value) visit(child); - return; - } - if (!isUnknownRecord(value)) return; +function getArgumentCount( + meta: Pick | TransformHoistInlineDirectiveMeta, +): number | undefined { + const node = meta.valueNode; + if ( + node?.type !== "FunctionDeclaration" && + node?.type !== "FunctionExpression" && + node?.type !== "ArrowFunctionExpression" + ) { + return; + } + return node.params.at(-1)?.type === "RestElement" ? undefined : node.params.length; +} - const nodeType = typeof value.type === "string" ? value.type : undefined; - if ( - (nodeType === "FunctionDeclaration" || - nodeType === "FunctionExpression" || - nodeType === "ArrowFunctionExpression") && - isBlockStatement(value.body) - ) { - for (const statement of value.body.body) { - if ( - isExpressionStatement(statement) && - isStringLiteral(statement.expression) && - matchDirective(statement.expression.value, directive) - ) { - result = statement.expression; - return; - } - } - } +function isInsideDirectory(directory: string, filePath: string): boolean { + const relativePath = path.relative(directory, filePath); + return relativePath !== "" && !relativePath.startsWith("..") && !path.isAbsolute(relativePath); +} - for (const [key, child] of Object.entries(value)) { - if (key === "parent" || key === "loc" || key === "start" || key === "end") continue; - visit(child); - } - } +function isAppPageDefaultExport( + options: Options, + id: string, + name: string, + isModuleDirective: boolean, +): boolean { + if (!isModuleDirective || name !== "default" || !options.appDir) return false; + const modulePath = stripViteModuleQuery(id); + const moduleFileName = path.basename(modulePath); + return ( + isInsideDirectory(options.appDir, modulePath) && + path.parse(moduleFileName).name === "page" && + options.matchesPageExtension(moduleFileName) + ); +} - visit(ast); - return result; +function shouldTransformModuleExport(name: string, id: string, meta: ModuleExportMeta): boolean { + if (meta.isFunction === false) return false; + if (/\/(layout|template)\.(tsx?|jsx?|mjs)$/.test(id) && name === "default") return false; + return true; } -async function expandExportAll( - transforms: RscTransforms, - context: Rollup.TransformPluginContext, - code: string, - ast: Program, +function getCacheWrapperOptions( + options: Options, id: string, -): Promise<{ code: string } | undefined> { - return transforms.transformExpandExportAll({ - code, - ast, - importer: id, - resolve: async (source, importer) => (await context.resolve(source, importer))?.id, - load: async (resolvedId) => { - const source = await fs.promises.readFile(resolvedId, "utf8"); - const transformed = await transformWithOxc(source, resolvedId, { sourcemap: false }); - return parseProgram(transformed.code); - }, - }); + name: string, + isModuleDirective: boolean, + meta: Pick | TransformHoistInlineDirectiveMeta, +): CacheWrapperOptions { + const argumentCount = getArgumentCount(meta); + return { + ...(isAppPageDefaultExport(options, id, name, isModuleDirective) + ? { appPageDefaultExport: true } + : {}), + ...(argumentCount === undefined ? {} : { argumentCount }), + }; } -export async function createServerFunctionDirectivePlugin(options: Options): Promise { +export async function createUseCacheCallablePlugin(options: Options): Promise { const rscModulePath = resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc"); const transformsPath = resolvePluginRscModule( options.projectRoot, "@vitejs/plugin-rsc/transforms", ); - const rscRuntime = pathToFileURL( - resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc/react/rsc/server"), - ).href; - const browserRuntime = pathToFileURL( - resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc/react/browser"), - ).href; - const ssrRuntime = pathToFileURL( - resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc/react/ssr"), - ).href; - const encryptionRuntime = pathToFileURL( - resolvePluginRscModule(options.projectRoot, "@vitejs/plugin-rsc/utils/encryption-runtime"), - ).href; const rscModule: typeof import("@vitejs/plugin-rsc") = await import( pathToFileURL(rscModulePath).href ); const transforms: RscTransforms = await import(pathToFileURL(transformsPath).href); - const { getPluginApi } = rscModule; let manager: RscPluginManager | undefined; - const transformPlugin: Plugin = { - name: SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, - + return { + name: PLUGIN_NAME, configResolved(config) { - manager = getPluginApi(config)?.manager; + manager = rscModule.getPluginApi(config)?.manager; }, + async transform(code, id) { + if (!manager) { + throw new Error("vinext: failed to access @vitejs/plugin-rsc through getPluginApi()."); + } + if ( + !/\.(tsx?|jsx?|mjs)$/.test(id) || + id.includes("/node_modules/") || + !code.includes("use cache") + ) { + manager.serverReferences.deleteClaim(PLUGIN_NAME, id); + return; + } - transform: { - async handler(code, id) { - if (code.includes(SERVER_FUNCTION_DIRECTIVE_MARKER)) return; - - const active = options.definitions.filter( - (definition) => - (definition.test?.(code) ?? code.includes("use ")) && - (!definition.filter || definition.filter(id)), + const ast = await parseAstAsync(code); + const moduleDirective = findModuleUseCacheDirective(ast); + const useServerBoundary = transforms.hasDirective(ast.body, "use server"); + if (moduleDirective && useServerBoundary) { + throw new Error( + `A module cannot contain both ${JSON.stringify(moduleDirective)} and "use server" directives.`, ); - const isServer = this.environment.name === options.serverEnvironmentName; - if (!manager) { - throw new Error("vinext: failed to access @vitejs/plugin-rsc through getPluginApi()."); - } - if (!manager.serverReferences) { - throw new Error( - "vinext: Installed @vitejs/plugin-rsc does not support user-land server reference claims.", - ); - } - if (active.length === 0) { - manager.serverReferences.deleteClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id); - return; - } + } - let ast = await parseProgram(code); - const useServerBoundary = transforms.hasDirective(ast.body, "use server"); - if (!isServer && useServerBoundary) { - manager.serverReferences.deleteClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id); + const reference = manager.serverReferences.resolve(id, "rsc"); + const isRsc = this.environment.name === "rsc"; + + if (!isRsc) { + if (useServerBoundary) { + manager.serverReferences.deleteClaim(PLUGIN_NAME, id); return; } - const reference = manager.serverReferences.resolve(id, options.serverEnvironmentName); - - if (!isServer) { - for (const definition of active) { - const inlineDirective = findInlineDirective(ast, definition.directive); - if (inlineDirective && definition.clientError) { - throw Object.assign( - new Error(definition.clientError({ id, environment: this.environment.name })), - { pos: inlineDirective.start }, + if (!moduleDirective) { + transforms.transformHoistInlineDirective(code, ast, { + directive: USE_CACHE_DIRECTIVE_CANDIDATE, + rejectNonAsyncFunction: true, + runtime: (_value, _name, meta) => { + matchUseCacheDirective(meta.directiveMatch[0]); + throw new Error( + `It is not allowed to define inline "use cache" annotated functions in Client Components. Export them from a separate file with a module-level "use cache" or "use server" directive, or pass them down through props from a Server Component. (${this.environment.name}: ${id})`, ); - } - } - - const matches: Array = []; - for (const definition of active) { - const moduleDirective = findModuleDirective(ast, definition.directive); - if (moduleDirective) matches.push([definition, moduleDirective]); - } - if (matches.length === 0) { - manager.serverReferences.deleteClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id); - return; - } - if (matches.length > 1) { - throw Object.assign( - new Error("Multiple server function directives match this module."), - { - pos: matches[1]?.[1].start, - }, - ); - } - - const match = matches[0]; - if (!match) return; - const [, moduleDirective] = match; - const result = transforms.transformDirectiveProxyExport(ast, { - code, - directive: moduleDirective.value, - runtime: (name) => - `$$ReactClient.createServerReference(${JSON.stringify(`${reference.referenceKey}#${name}`)},$$ReactClient.callServer,undefined,${this.environment.mode === "dev" ? "$$ReactClient.findSourceMapURL" : "undefined"},${JSON.stringify(name)})`, - }); - if (!result?.output.hasChanged()) { - manager.serverReferences.deleteClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id); - return; - } - manager.serverReferences.deleteClaim(USE_SERVER_PLUGIN_NAME, id); - manager.serverReferences.replaceClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id, { - ...reference, - exportNames: result.exportNames, + }, }); - result.output.prepend( - `${SERVER_FUNCTION_DIRECTIVE_MARKER}\nimport * as $$ReactClient from ${JSON.stringify(this.environment.name === options.browserEnvironmentName ? browserRuntime : ssrRuntime)};\n`, - ); - return { - code: result.output.toString(), - map: result.output.generateMap({ hires: "boundary", source: id }), - }; - } - - const exportNames = new Set(); - let needsReactRuntime = false; - let needsEncryptionRuntime = false; - let outputMap: SourceMap | undefined; - - for (const [definitionIndex, definition] of active.entries()) { - const runtimeName = definition.runtime - ? `$$server_function_directive_${definitionIndex}` - : undefined; - let runtimeUsed = false; - const getRuntime = () => { - if (runtimeName) runtimeUsed = true; - return runtimeName; - }; - - let moduleDirective = findModuleDirective(ast, definition.directive); - if (moduleDirective) { - if (useServerBoundary) { - throw Object.assign( - new Error( - `A module cannot contain both ${JSON.stringify(moduleDirective.value)} and "use server" directives.`, - ), - { pos: moduleDirective.start }, - ); - } - const expanded = await expandExportAll(transforms, this, code, ast, id); - if (expanded) { - code = expanded.code; - ast = await parseProgram(code); - moduleDirective = findModuleDirective(ast, definition.directive); - } - } - - const moduleMatch = moduleDirective - ? matchDirective(moduleDirective.value, definition.directive) - : undefined; - if (moduleMatch) { - definition.validate?.({ id, directive: moduleMatch[0], location: "module" }); - } - - const result = moduleMatch - ? transforms.transformWrapExport(code, ast, { - runtime: (value, name, meta) => { - needsReactRuntime = true; - return `$$VinextReactServer.registerServerReference(${definition.wrap({ value, name, id, directiveMatch: moduleMatch, location: "module", hasBoundArgs: false, parameters: meta.parameters, runtime: getRuntime(), meta })}, ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; - }, - filter: (name, meta) => definition.filterExport?.({ name, id, meta }) ?? true, - rejectNonAsyncFunction: definition.rejectNonAsyncModule, - }) - : transforms.transformHoistInlineDirective(code, ast, { - directive: definition.directive, - runtime: (value, name, meta) => { - definition.validate?.({ - id, - directive: meta.directiveMatch[0], - location: "inline", - }); - const wrapped = definition.wrap({ - value, - name, - id, - directiveMatch: meta.directiveMatch, - location: "inline", - hasBoundArgs: meta.hasBoundArgs, - parameters: meta.parameters, - runtime: getRuntime(), - }); - if (useServerBoundary) return wrapped; - - needsReactRuntime = true; - if (meta.hasBoundArgs) { - needsEncryptionRuntime = true; - const forwardedArgs = meta.parameters.hasRest - ? "$$args" - : `$$args.slice(0, ${meta.parameters.count})`; - return `$$VinextReactServer.registerServerReference((($$wrapped) => async ($$encoded, ...$$args) => $$wrapped(...await __vite_rsc_encryption_runtime.decryptActionBoundArgs($$encoded), ...${forwardedArgs}))(${wrapped}), ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; - } - return `$$VinextReactServer.registerServerReference(${wrapped}, ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; - }, - rejectNonAsyncFunction: definition.rejectNonAsyncFunction, - encode: (value) => { - needsEncryptionRuntime = true; - return `__vite_rsc_encryption_runtime.encryptActionBoundArgs(${value})`; - }, - stableName: true, - exportWrappedHoist: !useServerBoundary, - rejectForbiddenExpressions: true, - }); - if (!result.output.hasChanged()) continue; - - if (moduleDirective) { - result.output.overwrite( - moduleDirective.start, - moduleDirective.end, - `/* ${JSON.stringify(moduleDirective.value)} */`, - ); - } - - if (runtimeUsed && definition.runtime && runtimeName) { - result.output.prepend( - `import * as ${runtimeName} from ${JSON.stringify(definition.runtime)};\n`, - ); - } - - const transformedNames = "names" in result ? result.names : result.exportNames; - transformedNames.forEach((name) => exportNames.add(name)); - outputMap = result.output.generateMap({ hires: "boundary", source: id }); - code = result.output.toString(); - ast = await parseProgram(code); + manager.serverReferences.deleteClaim(PLUGIN_NAME, id); + return; } - if (!useServerBoundary && exportNames.size > 0) { - manager.serverReferences.deleteClaim(USE_SERVER_PLUGIN_NAME, id); - manager.serverReferences.replaceClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id, { - ...reference, - exportNames: [...exportNames], - }); - } else { - manager.serverReferences.deleteClaim(SERVER_FUNCTION_DIRECTIVE_PLUGIN_NAME, id); + const result = transforms.transformDirectiveProxyExport(ast, { + code, + directive: moduleDirective, + filter: (name, meta) => shouldTransformModuleExport(name, id, meta), + runtime: (name) => + `$$ReactClient.createServerReference(${JSON.stringify(`${reference.referenceKey}#${name}`)},$$ReactClient.callServer,undefined,${this.environment.mode === "dev" ? "$$ReactClient.findSourceMapURL" : "undefined"},${JSON.stringify(name)})`, + }); + if (!result?.output.hasChanged()) { + manager.serverReferences.deleteClaim(PLUGIN_NAME, id); + return; } - const imports = [ - needsReactRuntime && - `import * as $$VinextReactServer from ${JSON.stringify(rscRuntime)};`, - needsEncryptionRuntime && - `import * as __vite_rsc_encryption_runtime from ${JSON.stringify(encryptionRuntime)};`, - ].filter(Boolean); + manager.serverReferences.deleteClaim(USE_SERVER_PLUGIN_NAME, id); + manager.serverReferences.replaceClaim(PLUGIN_NAME, id, { + ...reference, + exportNames: result.exportNames, + }); + const runtimeEnvironment = this.environment.name === "client" ? "browser" : "ssr"; + result.output.prepend( + `import * as $$ReactClient from "@vitejs/plugin-rsc/react/${runtimeEnvironment}";\n`, + ); return { - code: `${SERVER_FUNCTION_DIRECTIVE_MARKER}\n${imports.join("\n")}\n${code}`, - map: outputMap, + code: result.output.toString(), + map: result.output.generateMap({ hires: "boundary", source: id }), }; - }, + } + + const wrap = ( + value: string, + name: string, + directiveMatch: RegExpMatchArray, + meta: Pick | TransformHoistInlineDirectiveMeta, + isModuleDirective: boolean, + ) => { + const variant = directiveMatch[1] ?? ""; + const wrapperOptions = getCacheWrapperOptions(options, id, name, isModuleDirective, meta); + return `$$cacheRuntime.registerCachedFunction(${value}, ${JSON.stringify(`${id}:${name}`)}, ${JSON.stringify(variant)}, ${JSON.stringify(wrapperOptions)})`; + }; + let needsReactServer = false; + const runtime = ( + value: string, + name: string, + directiveMatch: RegExpMatchArray, + meta: Pick | TransformHoistInlineDirectiveMeta, + isModuleDirective: boolean, + ) => { + const cached = wrap(value, name, directiveMatch, meta, isModuleDirective); + if (useServerBoundary) return cached; + needsReactServer = true; + return `$$VinextReactServer.registerServerReference(${cached}, ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; + }; + + const result = moduleDirective + ? transforms.transformWrapExport(code, ast, { + filter: (name, meta) => shouldTransformModuleExport(name, id, meta), + runtime: (value, name, meta) => + runtime(value, name, matchUseCacheDirective(moduleDirective), meta, true), + }) + : transforms.transformHoistInlineDirective(code, ast, { + directive: USE_CACHE_DIRECTIVE_CANDIDATE, + rejectNonAsyncFunction: true, + hoistRuntime: true, + runtime: (value, name, meta) => + runtime(value, name, matchUseCacheDirective(meta.directiveMatch[0]), meta, false), + encode: (value) => `$$cacheRuntime.encryptCacheCaptures(${value})`, + decode: (value) => value, + }); + if (!result.output.hasChanged()) { + manager.serverReferences.deleteClaim(PLUGIN_NAME, id); + return; + } + + if (useServerBoundary) { + manager.serverReferences.deleteClaim(PLUGIN_NAME, id); + } else { + manager.serverReferences.deleteClaim(USE_SERVER_PLUGIN_NAME, id); + manager.serverReferences.replaceClaim(PLUGIN_NAME, id, { + ...reference, + exportNames: "names" in result ? result.names : result.exportNames, + }); + } + result.output.prepend( + [ + `import * as $$cacheRuntime from ${JSON.stringify(options.cacheRuntime)};`, + needsReactServer && + `import * as $$VinextReactServer from "@vitejs/plugin-rsc/react/rsc/server";`, + ] + .filter(Boolean) + .join("\n") + "\n", + ); + return { + code: result.output.toString(), + map: result.output.generateMap({ hires: "boundary", source: id }), + }; }, }; - - return transformPlugin; } diff --git a/packages/vinext/src/shims/cache-callable-runtime.ts b/packages/vinext/src/shims/cache-callable-runtime.ts new file mode 100644 index 0000000000..28cb3f9d33 --- /dev/null +++ b/packages/vinext/src/shims/cache-callable-runtime.ts @@ -0,0 +1,56 @@ +import { + decryptActionBoundArgs, + encryptActionBoundArgs, +} from "@vitejs/plugin-rsc/utils/encryption-runtime"; +import { + registerCachedFunction as registerCachedFunctionBase, + type RegisterCachedFunctionOptions, +} from "./cache-runtime.js"; + +const CACHE_CAPTURE_TYPE = "use-cache-captures"; + +type CacheCaptureEnvelope = { + type: typeof CACHE_CAPTURE_TYPE; + encrypted: string | PromiseLike; +}; + +export function encryptCacheCaptures(captures: unknown[]): CacheCaptureEnvelope { + return { + type: CACHE_CAPTURE_TYPE, + encrypted: encryptActionBoundArgs(captures), + }; +} + +async function decryptCacheCaptures(value: unknown): Promise { + if (!isCacheCaptureEnvelope(value)) return; + const captures = await decryptActionBoundArgs(Promise.resolve(value.encrypted)); + if (!Array.isArray(captures)) throw new Error("Invalid cache capture payload"); + return captures; +} + +function isCacheCaptureEnvelope(value: unknown): value is CacheCaptureEnvelope { + if (typeof value !== "object" || value === null) return false; + if (!("type" in value) || value.type !== CACHE_CAPTURE_TYPE || !("encrypted" in value)) { + return false; + } + const encrypted = value.encrypted; + return ( + typeof encrypted === "string" || + (typeof encrypted === "object" && + encrypted !== null && + "then" in encrypted && + typeof encrypted.then === "function") + ); +} + +export function registerCachedFunction( + fn: (...args: TArgs) => Promise, + id: string, + variant: string, + options: RegisterCachedFunctionOptions, +): (...args: TArgs) => Promise { + return registerCachedFunctionBase(fn, id, variant, { + ...options, + decryptCaptures: decryptCacheCaptures, + }); +} diff --git a/packages/vinext/src/shims/cache-runtime.ts b/packages/vinext/src/shims/cache-runtime.ts index f0940241f3..09cb1c16ae 100644 --- a/packages/vinext/src/shims/cache-runtime.ts +++ b/packages/vinext/src/shims/cache-runtime.ts @@ -410,7 +410,7 @@ export function markAppPagePropsForUseCache(props: T): T { // Core runtime: registerCachedFunction // --------------------------------------------------------------------------- -type RegisterCachedFunctionOptions = { +export type RegisterCachedFunctionOptions = { /** * Internal transform metadata for file-level `"use cache"` default exports * in App Router `page.*` files. Page components receive framework-owned @@ -419,8 +419,9 @@ type RegisterCachedFunctionOptions = { * rather than on the intermediate createElement config object. */ appPageDefaultExport?: boolean; - /** Declared function parameter shape supplied by the directive transform. */ - parameters?: { count: number; hasRest: boolean }; + /** Number of declared arguments supplied by the directive transform. */ + argumentCount?: number; + decryptCaptures?: (value: unknown) => Promise; }; /** @@ -452,21 +453,27 @@ export function registerCachedFunction( const cachedFn = async (...args: TArgs): Promise => { const rsc = await getRscModule(); const keySeed = getUseCacheKeySeed(); + const captures = options.decryptCaptures ? await options.decryptCaptures(args[0]) : undefined; + const hasCaptureEnvelope = captures !== undefined; + const admittedArgs = + options.argumentCount === undefined + ? args + : hasCaptureEnvelope + ? [args[0], ...args.slice(1, 1 + options.argumentCount)] + : args.slice(0, options.argumentCount); + const executionArgs = hasCaptureEnvelope ? [captures, ...admittedArgs.slice(1)] : admittedArgs; + const callArgs = executionArgs as TArgs; // Build the cache key. Use encodeReply (RSC protocol) when available — // it correctly handles React elements as temporary references (excluded // from key). Falls back to stableStringify when RSC is unavailable. let cacheKey: string; try { - const keyArgs = - options.parameters && !options.parameters.hasRest - ? args.slice(0, options.parameters.count) - : args; const processedArgs = - keyArgs.length > 0 - ? unwrapThenableObjectArray(keyArgs, { omitAppPageSearchParamsFromFirstArg }) + executionArgs.length > 0 + ? unwrapThenableObjectArray(executionArgs, { omitAppPageSearchParamsFromFirstArg }) : []; - if (rsc && keyArgs.length > 0) { + if (rsc && executionArgs.length > 0) { // Temporary references let encodeReply handle non-serializable values // (like React elements in args) by excluding them from the key. const tempRefs = rsc.createClientTemporaryReferenceSet(); @@ -488,7 +495,7 @@ export function registerCachedFunction( } } catch { // Non-serializable arguments — run without caching - return fn(...args); + return fn(...callArgs); } // "use cache: private" uses per-request in-memory cache @@ -513,7 +520,7 @@ export function registerCachedFunction( return privateHit as TResult; } - const result = await executeWithContext(fn, args, cacheVariant); + const result = await executeWithContext(fn, callArgs, cacheVariant); privateCache.set(cacheKey, result); return result; } @@ -521,7 +528,7 @@ export function registerCachedFunction( // In dev mode, always execute fresh — skip shared cache lookup/storage. // This ensures HMR changes are reflected immediately. if (isDev) { - return executeWithContext(fn, args, cacheVariant); + return executeWithContext(fn, callArgs, cacheVariant); } // Shared cache ("use cache" / "use cache: remote") @@ -565,7 +572,7 @@ export function registerCachedFunction( // Cache miss (or stale) — execute with context const { result, ctx, effectiveLife } = await runCachedFunctionWithContext( fn, - args, + callArgs, cacheVariant, ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0269913e1e..e06caa87f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,11 +88,11 @@ catalogs: specifier: ^0.8.6 version: 0.8.6 '@vitejs/plugin-react': - specifier: https://pkg.pr.new/@vitejs/plugin-react@82d2c578 + specifier: ^6.0.1 version: 6.0.2 '@vitejs/plugin-rsc': - specifier: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476 - version: 0.5.30 + specifier: ^0.5.33 + version: 0.5.33 '@vitest/coverage-istanbul': specifier: 4.1.6 version: 4.1.6 @@ -277,7 +277,7 @@ importers: version: 7.0.0-dev.20260217.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.6(@voidzero-dev/vite-plus-test@0.1.24) @@ -362,10 +362,10 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) drizzle-kit: specifier: 'catalog:' version: 0.31.10 @@ -392,7 +392,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -408,7 +408,7 @@ importers: devDependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) vite: specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' @@ -423,10 +423,10 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -457,7 +457,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) nitro: specifier: 'catalog:' version: nitro-nightly@3.0.1-20260512-093145-0498ce70(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(better-sqlite3@12.6.2)(chokidar@5.0.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.6.2)(kysely@0.28.15))(jiti@2.7.0)(miniflare@4.20260401.0) @@ -494,7 +494,7 @@ importers: version: 2.0.13 '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) clsx: specifier: 'catalog:' version: 2.1.1 @@ -558,7 +558,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -594,10 +594,10 @@ importers: version: 4.2.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -677,10 +677,10 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -710,10 +710,10 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) ms: specifier: 'catalog:' version: 3.0.0-canary.1 @@ -753,7 +753,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -784,7 +784,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) typescript: specifier: 'catalog:' version: 5.9.3 @@ -805,7 +805,7 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -830,7 +830,7 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) react: specifier: 'catalog:' version: 19.2.7 @@ -873,10 +873,10 @@ importers: dependencies: '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -919,10 +919,10 @@ importers: version: link:../../packages/cloudflare '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1005,10 +1005,10 @@ importers: version: link:../cloudflare '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react-server-dom-webpack: specifier: 'catalog:' version: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1020,7 +1020,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) fake-context-lib: specifier: file:./__test_packages__/fake-context-lib version: file:tests/fixtures/app-basic/__test_packages__/fake-context-lib @@ -1054,7 +1054,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1079,7 +1079,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1104,7 +1104,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1132,10 +1132,10 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-react': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1170,7 +1170,7 @@ importers: version: 1.9.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) better-auth: specifier: 'catalog:' version: 1.5.6(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-istanbul@4.1.6(@voidzero-dev/vite-plus-test@0.1.24))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(better-sqlite3@12.6.2)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.6.2)(kysely@0.28.15))(esbuild@0.27.3)(jiti@2.7.0)(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0) @@ -1201,7 +1201,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-intl: specifier: 'catalog:' version: 4.11.1(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react@19.2.7)(typescript@5.9.3) @@ -1229,7 +1229,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1257,7 +1257,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-view-transitions: specifier: 'catalog:' version: 0.3.5(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1285,7 +1285,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) nuqs: specifier: 'catalog:' version: 2.8.8(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react@19.2.7) @@ -1322,7 +1322,7 @@ importers: version: 1.2.4(@types/react@19.2.16)(react@19.2.7) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) class-variance-authority: specifier: 'catalog:' version: 0.7.1 @@ -1378,7 +1378,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1403,7 +1403,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1428,7 +1428,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1472,7 +1472,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1516,7 +1516,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -4380,9 +4380,8 @@ packages: resolution: {integrity: sha512-hBcWIOppZV14bi+eAmCZj8Elj8hVSUZJTpf1lgGBhVD85pervzQ1poM/qYfFUlPraYSZYP+ASg6To5BwYmUSGQ==} engines: {node: '>=16'} - '@vitejs/plugin-react@https://pkg.pr.new/@vitejs/plugin-react@82d2c578': - resolution: {integrity: sha512-0KNIsFan88aSruEH1bs+Bk3n3solO57k7AvvtTNCg0rb2Yb4W1Ci+5PrAkp41A/6JXldWvZVBeyQEKCsR0j9iQ==, tarball: https://pkg.pr.new/@vitejs/plugin-react@82d2c578} - version: 6.0.2 + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 @@ -4394,9 +4393,8 @@ packages: babel-plugin-react-compiler: optional: true - '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476': - resolution: {integrity: sha512-hJz+rxnLJ6zSlHNJBZr097gF+3uzoOYLYgYVkwIpdl7ttO5UUjVOLXaomF4ZF0OVFde17uAfUyW2eX1e9s/+hQ==, tarball: https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476} - version: 0.5.30 + '@vitejs/plugin-rsc@0.5.33': + resolution: {integrity: sha512-3q4F9yyJQwOqUGHw60ZwdfmWTRERtd8iDiwRgQyudss5wGbNeQP1L4fDDQPAFSKdyRQlXfZiAt8naxvIOmiinQ==} peerDependencies: react: '*' react-dom: '*' @@ -9409,12 +9407,12 @@ snapshots: '@resvg/resvg-wasm': 2.4.0 satori: 0.16.0 - '@vitejs/plugin-react@https://pkg.pr.new/@vitejs/plugin-react@82d2c578(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0)' - '@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': + '@vitejs/plugin-rsc@0.5.33(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@5.9.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': dependencies: '@rolldown/pluginutils': 1.0.1 es-module-lexer: 2.3.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 94ba817b31..c0b26b04da 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,11 +39,11 @@ catalog: "@typescript/native-preview": ^7.0.0-dev.20260213.1 "@unpic/react": ^1.0.2 "@vercel/og": ^0.8.6 - "@vitejs/plugin-react": https://pkg.pr.new/@vitejs/plugin-react@82d2c578 + "@vitejs/plugin-react": ^6.0.1 "@next/mdx": 16.2.7 recma-codehike: 0.0.1 remark-codehike: 0.0.1 - "@vitejs/plugin-rsc": https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476 + "@vitejs/plugin-rsc": ^0.5.33 "@vitest/coverage-istanbul": 4.1.6 better-auth: ^1.5.6 better-sqlite3: ^12.0.0 diff --git a/tests/e2e/app-router-prod/use-cache.spec.ts b/tests/e2e/app-router-prod/use-cache.spec.ts index ec2f6ed48a..447592d2f2 100644 --- a/tests/e2e/app-router-prod/use-cache.spec.ts +++ b/tests/e2e/app-router-prod/use-cache.spec.ts @@ -46,24 +46,19 @@ test.describe('production "use cache" server function references', () => { await expect(page.getByTestId("mixed-flexible-result")).toHaveText(cachedResult); }); - test('caches an inline "use cache" function inside a file-level "use server" module', async ({ + test('caches an inline "use cache" function during server render inside a file-level "use server" module', async ({ page, }) => { await page.goto("/use-cache-transform-coverage"); const aggregateResult = await page.getByTestId("use-cache-transform-coverage").innerText(); - const serverResult = aggregateResult.split("|")[4]; + const serverResult = aggregateResult.split("|")[1]; if (!serverResult) throw new Error("Missing server-boundary result"); expect(serverResult).toMatch(/^server-boundary:[0-9.e+-]+$/); - await page.locator("#call-cached-server-boundary").click(); - await expect(page.getByTestId("cached-server-boundary-call-count")).toHaveText("1"); - const firstActionResult = await page.getByTestId("cached-server-boundary-result").innerText(); - expect(firstActionResult).toMatch(/^server-boundary:[0-9.e+-]+$/); - - await page.locator("#call-cached-server-boundary").click(); - await expect(page.getByTestId("cached-server-boundary-call-count")).toHaveText("2"); - await expect(page.getByTestId("cached-server-boundary-result")).toHaveText(firstActionResult); + await page.reload(); + const repeatedResult = await page.getByTestId("use-cache-transform-coverage").innerText(); + expect(repeatedResult.split("|")[1]).toBe(serverResult); }); test("replays cached RSC through SSR and invokes nested functions from the browser", async ({ diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index 03c984c545..b8f2279d6c 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -186,18 +186,11 @@ test.describe('"use cache" direct client imports', () => { }); test.describe('"use cache" transform coverage', () => { - test("supports advanced function and export forms", async ({ page }) => { + test("supports released transform forms and use-server boundaries", async ({ page }) => { await page.goto(`${BASE}/use-cache-transform-coverage`); await expect(page.getByTestId("use-cache-transform-coverage")).toHaveText( - /^destructured\|export-star\|object-method\|static-method\|server-boundary:[0-9.e+-]+\|custom-kind$/, + /^destructured\|server-boundary:[0-9.e+-]+\|custom-kind$/, ); - await expect(async () => { - await page.locator("#call-cached-server-boundary").click(); - await expect(page.getByTestId("cached-server-boundary-result")).toHaveText( - /^server-boundary:[0-9.e+-]+$/, - { timeout: 2000 }, - ); - }).toPass({ timeout: 15_000 }); }); test("removes and restores directive metadata during HMR", async ({ page, request }) => { diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/methods.ts b/tests/fixtures/app-basic/app/use-cache-transform-coverage/methods.ts deleted file mode 100644 index affede44bd..0000000000 --- a/tests/fixtures/app-basic/app/use-cache-transform-coverage/methods.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const objectMethods = { - async getValue() { - "use cache"; - return "object-method"; - }, -}; - -export class StaticMethods { - static async getValue() { - "use cache"; - return "static-method"; - } -} diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/page.tsx b/tests/fixtures/app-basic/app/use-cache-transform-coverage/page.tsx index cb962ecc1c..d34755bdca 100644 --- a/tests/fixtures/app-basic/app/use-cache-transform-coverage/page.tsx +++ b/tests/fixtures/app-basic/app/use-cache-transform-coverage/page.tsx @@ -1,24 +1,9 @@ import { destructured } from "./destructured"; -import { fromStar } from "./star"; -import { objectMethods, StaticMethods } from "./methods"; import { fromServerBoundary } from "./server-boundary"; import { customKind } from "./custom-kind"; -import { ServerBoundaryClientCaller } from "./server-boundary-client"; export default async function UseCacheTransformCoveragePage() { - const values = await Promise.all([ - destructured(), - fromStar(), - objectMethods.getValue(), - StaticMethods.getValue(), - fromServerBoundary(), - customKind(), - ]); + const values = await Promise.all([destructured(), fromServerBoundary(), customKind()]); - return ( - <> - {values.join("|")} - - - ); + return {values.join("|")}; } diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary-client.tsx b/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary-client.tsx deleted file mode 100644 index b449b5d17f..0000000000 --- a/tests/fixtures/app-basic/app/use-cache-transform-coverage/server-boundary-client.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { fromServerBoundary } from "./server-boundary"; - -export function ServerBoundaryClientCaller() { - const [value, setValue] = useState(""); - const [completedCalls, setCompletedCalls] = useState(0); - - return ( -
- - {value} - {completedCalls} -
- ); -} diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/star-source.ts b/tests/fixtures/app-basic/app/use-cache-transform-coverage/star-source.ts deleted file mode 100644 index 4cca5d9349..0000000000 --- a/tests/fixtures/app-basic/app/use-cache-transform-coverage/star-source.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function fromStar() { - return "export-star"; -} diff --git a/tests/fixtures/app-basic/app/use-cache-transform-coverage/star.ts b/tests/fixtures/app-basic/app/use-cache-transform-coverage/star.ts deleted file mode 100644 index 43173a5147..0000000000 --- a/tests/fixtures/app-basic/app/use-cache-transform-coverage/star.ts +++ /dev/null @@ -1,3 +0,0 @@ -"use cache"; - -export * from "./star-source"; diff --git a/tests/shims.test.ts b/tests/shims.test.ts index 861afade06..b3271881a6 100644 --- a/tests/shims.test.ts +++ b/tests/shims.test.ts @@ -4566,7 +4566,7 @@ describe('"use cache" runtime', () => { expect(callCount).toBe(2); }); - it("registerCachedFunction excludes arguments beyond declared arity from cache keys", async () => { + it("registerCachedFunction excludes arguments beyond declared arity", async () => { const { registerCachedFunction } = await import("../packages/vinext/src/shims/cache-runtime.js"); let calls = 0; @@ -4577,11 +4577,11 @@ describe('"use cache" runtime', () => { }, "test:declared-arity", "", - { parameters: { count: 1, hasRest: false } }, + { argumentCount: 1 }, ); - expect(await cached(1, "first")).toEqual({ value: 1, extra: ["first"] }); - expect(await cached(1, "second")).toEqual({ value: 1, extra: ["first"] }); + expect(await cached(1, "first")).toEqual({ value: 1, extra: [] }); + expect(await cached(1, "second")).toEqual({ value: 1, extra: [] }); expect(calls).toBe(1); }); @@ -4596,11 +4596,11 @@ describe('"use cache" runtime', () => { }, "test:zero-arity", "", - { parameters: { count: 0, hasRest: false } }, + { argumentCount: 0 }, ); - expect(await cached("first")).toEqual(["first"]); - expect(await cached("second")).toEqual(["first"]); + expect(await cached("first")).toEqual([]); + expect(await cached("second")).toEqual([]); expect(calls).toBe(1); }); @@ -4615,7 +4615,7 @@ describe('"use cache" runtime', () => { }, "test:rest-args", "", - { parameters: { count: 1, hasRest: true } }, + {}, ); expect(await cached(1, 2)).toEqual([1, 2]); diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index 4cdfa56004..0400a15ee1 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -133,27 +133,6 @@ describe("plugin-rsc inline use-cache references", () => { expect(merged.exportNames).toHaveLength(new Set(merged.exportNames).size); }); - it("preserves the vinext claim when transformed code enters a proxy graph", async () => { - const plugins = await getPlugins(); - const manager = await configurePluginRsc(plugins); - const useCachePlugin = plugins.find( - (candidate) => candidate.name === "vinext:server-function-directives", - )!; - const useServerPlugin = plugins.find((candidate) => candidate.name === "rsc:use-server")!; - const rscContext = { environment: { name: "rsc", mode: "build" } }; - const transformed = await unwrapHook(useCachePlugin.transform)!.call( - rscContext, - inlineCacheCode, - moduleId, - ); - const ownedExportNames = manager.serverReferences.metaMap.get(moduleId)!.exportNames; - - const ssrContext = { environment: { name: "ssr", mode: "build" } }; - await unwrapHook(useCachePlugin.transform)!.call(ssrContext, transformed!.code, moduleId); - await unwrapHook(useServerPlugin.transform)!.call(ssrContext, transformed!.code, moduleId); - expect(manager.serverReferences.metaMap.get(moduleId)!.exportNames).toEqual(ownedExportNames); - }); - it("removes the vinext claim when the directive is removed", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); @@ -265,35 +244,10 @@ describe("plugin-rsc inline use-cache references", () => { expect(manager.serverReferences.metaMap.get(moduleId)).toEqual({ importId: moduleId, referenceKey: expectedKey, - exportNames: [expect.stringMatching(/^\$\$hoist_[a-z0-9]+_0_getData$/)], + exportNames: ["$$hoist_0_getData"], }); }); - it("keeps hoist names stable when unrelated cached functions are inserted", async () => { - const plugins = await getPlugins(); - await configurePluginRsc(plugins); - const plugin = plugins.find( - (candidate) => candidate.name === "vinext:server-function-directives", - )!; - const transform = unwrapHook(plugin.transform)!; - const original = await transform.call( - { environment: { name: "rsc", mode: "build" } }, - inlineCacheCode, - moduleId, - ); - const withUnrelated = await transform.call( - { environment: { name: "rsc", mode: "build" } }, - [`async function unrelated() {`, ` "use cache";`, ` return 0;`, `}`, inlineCacheCode].join( - "\n", - ), - moduleId, - ); - const getDataName = (code: string) => - code.match(/function (\$\$hoist_[a-z0-9]+_0_getData)/)?.[1]; - expect(getDataName(original!.code)).toBeDefined(); - expect(getDataName(withUnrelated!.code)).toBe(getDataName(original!.code)); - }); - it("removes its claim when the directive is removed", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); @@ -315,7 +269,7 @@ describe("plugin-rsc inline use-cache references", () => { expect(manager.serverReferences.metaMap.get(moduleId)).toBeUndefined(); }); - it("encrypts closure captures and reports bound-argument metadata to vinext", async () => { + it("encrypts closure captures through the cache runtime envelope", async () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( @@ -341,16 +295,15 @@ describe("plugin-rsc inline use-cache references", () => { ); expect(result).not.toBeNull(); expect(result!.code).toMatch( - /\.bind\(null,\s*__vite_rsc_encryption_runtime\.encryptActionBoundArgs\(\[capturedSecret\]\)\)/, + /\.bind\(null,\s*\$\$cacheRuntime\.encryptCacheCaptures\(\[capturedSecret\]\)\)/, ); expect(result!.code).not.toMatch(/\.bind\(null,\s*capturedSecret\)/); - expect(result!.code).toContain("decryptActionBoundArgs($$encoded)"); - expect(result!.code).toContain("...$$args.slice(0, 0)"); + expect(result!.code).toContain("const [capturedSecret] = $$hoist_encoded"); const boundRegistration = result!.code.match( /registerCachedFunction\(\$\$hoist_[^,]+_getMessage\$\$impl,[^)]*\)/, )?.[0]; expect(boundRegistration).toBeDefined(); - expect(boundRegistration).not.toContain('"parameters"'); + expect(boundRegistration).toContain('"argumentCount":0'); }); it.each(["ssr", "client"])( @@ -482,64 +435,6 @@ describe("plugin-rsc inline use-cache references", () => { }, ); - it.each([ - [ - "object method", - [ - `const object = {`, - ` async getData() {`, - ` "use cache";`, - ` return 1;`, - ` },`, - `};`, - `export { object };`, - ].join("\n"), - ], - [ - "static class method", - [ - `export class CacheClass {`, - ` static async getData() {`, - ` "use cache";`, - ` return 1;`, - ` }`, - `}`, - ].join("\n"), - ], - ])("handles inline directives in %s syntax", async (_label, code) => { - const plugins = await getPlugins(); - await configurePluginRsc(plugins); - const plugin = plugins.find( - (candidate) => candidate.name === "vinext:server-function-directives", - )!; - const transform = unwrapHook(plugin.transform)!; - const result = await transform.call( - { environment: { name: "rsc", mode: "build" } }, - code, - moduleId, - ); - expect(result?.code).toContain("registerCachedFunction"); - expect(() => parseAst(result!.code)).not.toThrow(); - }); - - it("rejects inline directives in class instance methods", async () => { - const plugins = await getPlugins(); - await configurePluginRsc(plugins); - const plugin = plugins.find( - (candidate) => candidate.name === "vinext:server-function-directives", - )!; - const transform = unwrapHook(plugin.transform)!; - await expect( - transform.call( - { environment: { name: "rsc", mode: "build" } }, - [`export class CacheClass {`, ` async getData() {`, ` "use cache";`, ` }`, `}`].join( - "\n", - ), - moduleId, - ), - ).rejects.toThrow(/class instance methods/); - }); - it("preserves inline cache semantics inside a module-level use-server boundary", async () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); @@ -604,7 +499,7 @@ describe("plugin-rsc inline use-cache references", () => { expect(result).not.toBeNull(); expect(result!.code).toContain("$$VinextReactServer.registerServerReference"); expect(result!.code).toContain("registerCachedFunction"); - expect(result!.code).not.toContain('"use cache";'); + expect(result!.code).toContain('"use cache";'); expect(manager.serverReferences.metaMap.get(moduleId)!.exportNames).toEqual(["getData"]); }); From e841cd9206b91ebd912269ae710a8a435d220a5a Mon Sep 17 00:00:00 2001 From: James Date: Thu, 6 Aug 2026 17:17:48 +0100 Subject: [PATCH 26/33] refactor(cache): rename callable plugin --- packages/vinext/src/index.ts | 2 +- .../{server-function-directives.ts => use-cache-callable.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/vinext/src/plugins/{server-function-directives.ts => use-cache-callable.ts} (100%) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 7ddfe1589f..7e829cb407 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -117,7 +117,7 @@ import { createMiddlewareServerOnlyPlugin } from "./plugins/middleware-server-on import { createOptimizeImportsPlugin } from "./plugins/optimize-imports.js"; import { createDynamicPreloadMetadataPlugin } from "./plugins/dynamic-preload-metadata.js"; import { createOgInlineFetchAssetsPlugin, createOgAssetsPlugin } from "./plugins/og-assets.js"; -import { createUseCacheCallablePlugin } from "./plugins/server-function-directives.js"; +import { createUseCacheCallablePlugin } from "./plugins/use-cache-callable.js"; import { generateRouteTypes } from "./typegen.js"; import { mergeOptimizeDepsExclude, diff --git a/packages/vinext/src/plugins/server-function-directives.ts b/packages/vinext/src/plugins/use-cache-callable.ts similarity index 100% rename from packages/vinext/src/plugins/server-function-directives.ts rename to packages/vinext/src/plugins/use-cache-callable.ts From 7078ed9eba16a2e9a684ffdffa39e5eff77dbac1 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 6 Aug 2026 18:02:24 +0100 Subject: [PATCH 27/33] test(init): update plugin-rsc install expectations --- tests/deploy.test.ts | 5 +---- tests/init.test.ts | 9 +-------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/deploy.test.ts b/tests/deploy.test.ts index 7f94e418db..d91688b198 100644 --- a/tests/deploy.test.ts +++ b/tests/deploy.test.ts @@ -2109,10 +2109,7 @@ describe("getMissingDeps", () => { info.hasRscPlugin = false; const missing = getMissingDeps(info); - expect(missing).toContainEqual({ - name: "@vitejs/plugin-rsc", - version: "https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476", - }); + expect(missing).toContainEqual(expect.objectContaining({ name: "@vitejs/plugin-rsc" })); }); it("does not require @vitejs/plugin-rsc for Pages Router", () => { diff --git a/tests/init.test.ts b/tests/init.test.ts index 9ee4ea1820..306011364a 100644 --- a/tests/init.test.ts +++ b/tests/init.test.ts @@ -1177,17 +1177,10 @@ describe("init — dependency installation", () => { it("detects missing @vitejs/plugin-rsc for App Router", async () => { setupProject(tmpDir, { router: "app" }); - const { result, execCalls } = await runInit(tmpDir); + const { result } = await runInit(tmpDir); expect(result.installedDeps).toContain("@vitejs/plugin-react"); expect(result.installedDeps).toContain("@vitejs/plugin-rsc"); - expect(execCalls).toContainEqual( - expect.objectContaining({ - cmd: expect.stringContaining( - "@vitejs/plugin-rsc@https://pkg.pr.new/@vitejs/plugin-rsc@50eaf476", - ), - }), - ); }); it("treats src/app projects as App Router", async () => { From f83386529ee9ff803de7994a40d1d9c13b434725 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 6 Aug 2026 18:37:45 +0100 Subject: [PATCH 28/33] test(cache): avoid reloading during HMR retries --- tests/e2e/app-router/use-cache.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index b8f2279d6c..8dc1725f29 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -206,8 +206,8 @@ test.describe('"use cache" transform coverage', () => { await writeUseCacheHmrActions(USE_CACHE_HMR_PLAIN, true); await waitForUseCacheHmrTransform(request); + await page.reload(); await expect(async () => { - await page.reload(); await page.locator("#call-use-cache-hmr").click(); await expect(page.getByTestId("use-cache-hmr-result")).toHaveText("plain", { timeout: 2000, @@ -216,8 +216,8 @@ test.describe('"use cache" transform coverage', () => { await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED, true); await waitForUseCacheHmrTransform(request); + await page.reload(); await expect(async () => { - await page.reload(); await page.locator("#call-use-cache-hmr").click(); await expect(page.getByTestId("use-cache-hmr-result")).toHaveText("cached", { timeout: 2000, From 2b26266619a5f3b978c234dec8629d8929296003 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 6 Aug 2026 18:52:07 +0100 Subject: [PATCH 29/33] test(cache): align callable references with plugin-rsc --- tests/app-router-production-server.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/app-router-production-server.test.ts b/tests/app-router-production-server.test.ts index cbd6e22d2a..b167385c7d 100644 --- a/tests/app-router-production-server.test.ts +++ b/tests/app-router-production-server.test.ts @@ -2195,9 +2195,7 @@ describe("App Router Production server (startProdServer)", () => { // The flight payload embeds each cached function prop as a server // reference whose id is "<12-hex normalised key>#". - const refIds = [ - ...new Set(html.match(/[0-9a-f]{12}#\$\$hoist_[a-z0-9]+_\d+_[A-Za-z0-9_$]+/g) ?? []), - ]; + const refIds = [...new Set(html.match(/[0-9a-f]{12}#\$\$hoist_\d+_[A-Za-z0-9_$]+/g) ?? [])]; expect(refIds.length).toBe(3); const [getDateRefId, getRandomRefId, getMessageRefId] = refIds; @@ -2208,6 +2206,10 @@ describe("App Router Production server (startProdServer)", () => { expect(html).not.toContain(capturedScopeValue); const encryptedBoundArg = html.match(/rsc\.push\("[0-9a-f]+:\\"([A-Za-z0-9+/=]{64,})\\"/)?.[1]; expect(encryptedBoundArg).toBeDefined(); + const encryptedCaptureEnvelope = { + type: "use-cache-captures", + encrypted: encryptedBoundArg, + }; const invokeAction = async (actionId: string, args: unknown[] = []): Promise => { const actionRes = await fetch(`${baseUrl}/use-cache-nested-fn-props.rsc`, { @@ -2243,7 +2245,7 @@ describe("App Router Production server (startProdServer)", () => { // cached function, so plaintext values still determine the cache key. const messageRegExpFor = (boundArg: string): RegExp => new RegExp(`message:${boundArg}:[0-9.e+-]+`); - const message1 = (await invokeAction(getMessageRefId, [encryptedBoundArg])).match( + const message1 = (await invokeAction(getMessageRefId, [encryptedCaptureEnvelope])).match( messageRegExpFor(capturedScopeValue), )?.[0]; expect(message1).toBeDefined(); @@ -2254,7 +2256,7 @@ describe("App Router Production server (startProdServer)", () => { // Math.random() suffix, so a second invocation with the same bound arg // can only return the identical value if the bound arg produced the same // cache key and the entry was hit (a recompute would change the suffix). - const message2 = (await invokeAction(getMessageRefId, [encryptedBoundArg])).match( + const message2 = (await invokeAction(getMessageRefId, [encryptedCaptureEnvelope])).match( messageRegExpFor(capturedScopeValue), )?.[0]; expect(message2).toBe(message1); From c31ae144008d6dc9dc4c5a0f03825fe936d3cf13 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 7 Aug 2026 10:11:28 +0100 Subject: [PATCH 30/33] fix(cache): align mixed directives with plugin-rsc 0.5.34 --- packages/vinext/package.json | 2 +- .../vinext/src/plugins/use-cache-callable.ts | 45 +++++++---- pnpm-lock.yaml | 81 ++++++++++--------- pnpm-workspace.yaml | 2 +- tests/e2e/app-router-prod/use-cache.spec.ts | 13 +++ .../app/use-cache-client-import/actions.ts | 5 ++ .../app/use-cache-client-import/form.tsx | 17 +++- tests/use-cache-transform.test.ts | 61 +++++++++++++- 8 files changed, 168 insertions(+), 58 deletions(-) diff --git a/packages/vinext/package.json b/packages/vinext/package.json index 05a723ef7f..9062847cd0 100644 --- a/packages/vinext/package.json +++ b/packages/vinext/package.json @@ -198,7 +198,7 @@ "peerDependencies": { "@mdx-js/rollup": "^3.0.0", "@vitejs/plugin-react": "^5.1.4 || ^6.0.0", - "@vitejs/plugin-rsc": "^0.5.33", + "@vitejs/plugin-rsc": "^0.5.34", "react": "^19.2.6", "react-dom": "^19.2.6", "react-server-dom-webpack": "^19.2.6", diff --git a/packages/vinext/src/plugins/use-cache-callable.ts b/packages/vinext/src/plugins/use-cache-callable.ts index c95c479460..9b718d7781 100644 --- a/packages/vinext/src/plugins/use-cache-callable.ts +++ b/packages/vinext/src/plugins/use-cache-callable.ts @@ -26,7 +26,6 @@ type CacheWrapperOptions = { }; const PLUGIN_NAME = "vinext:server-function-directives"; -const USE_SERVER_PLUGIN_NAME = "rsc:use-server"; const USE_CACHE_DIRECTIVE = /^use cache(?:: ([^\s].*))?$/; const USE_CACHE_DIRECTIVE_CANDIDATE = /^use cache.*$/; @@ -127,6 +126,27 @@ function shouldTransformModuleExport(name: string, id: string, meta: ModuleExpor return true; } +function hasFunctionDirective( + meta: Pick, + directive: string, +): boolean { + const node = meta.valueNode; + if ( + (node?.type !== "FunctionDeclaration" && + node?.type !== "FunctionExpression" && + node?.type !== "ArrowFunctionExpression") || + node.body.type !== "BlockStatement" + ) { + return false; + } + return node.body.body.some( + (statement) => + statement.type === "ExpressionStatement" && + "directive" in statement && + statement.directive === directive, + ); +} + function getCacheWrapperOptions( options: Options, id: string, @@ -218,7 +238,6 @@ export async function createUseCacheCallablePlugin(options: Options): Promise { const cached = wrap(value, name, directiveMatch, meta, isModuleDirective); - if (useServerBoundary) return cached; needsReactServer = true; return `$$VinextReactServer.registerServerReference(${cached}, ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; }; const result = moduleDirective ? transforms.transformWrapExport(code, ast, { - filter: (name, meta) => shouldTransformModuleExport(name, id, meta), + filter: (name, meta) => + !hasFunctionDirective(meta, "use server") && + shouldTransformModuleExport(name, id, meta), runtime: (value, name, meta) => runtime(value, name, matchUseCacheDirective(moduleDirective), meta, true), }) @@ -278,16 +298,13 @@ export async function createUseCacheCallablePlugin(options: Options): Promise !("directive" in node))?.start ?? code.length; + result.output.prependLeft( + importPosition, [ `import * as $$cacheRuntime from ${JSON.stringify(options.cacheRuntime)};`, needsReactServer && diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84a5ff232a..5f39e594a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,8 +91,8 @@ catalogs: specifier: ^6.0.1 version: 6.0.1 '@vitejs/plugin-rsc': - specifier: ^0.5.33 - version: 0.5.33 + specifier: ^0.5.34 + version: 0.5.34 '@vitest/coverage-istanbul': specifier: 4.1.10 version: 4.1.10 @@ -283,7 +283,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.10(vitest@4.1.10) @@ -377,7 +377,7 @@ importers: version: 6.0.1(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) drizzle-kit: specifier: 'catalog:' version: 0.31.10 @@ -420,7 +420,7 @@ importers: devDependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) vite: specifier: npm:@voidzero-dev/vite-plus-core@0.2.6 version: '@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)' @@ -441,7 +441,7 @@ importers: version: 6.0.1(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -576,7 +576,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -615,7 +615,7 @@ importers: version: 6.0.1(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -698,7 +698,7 @@ importers: version: 6.0.1(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) postcss: specifier: 'catalog:' version: 8.5.10 @@ -731,7 +731,7 @@ importers: version: 6.0.1(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) ms: specifier: 'catalog:' version: 3.0.0-canary.1 @@ -802,7 +802,7 @@ importers: version: 19.2.3(@types/react@19.2.16) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) typescript: specifier: 'catalog:' version: 7.0.2 @@ -943,7 +943,7 @@ importers: version: 6.0.1(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -989,7 +989,7 @@ importers: version: 6.0.1(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1095,7 +1095,7 @@ importers: version: 6.0.1(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) am-i-vibing: specifier: 'catalog:' version: 0.5.0 @@ -1113,7 +1113,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) fake-context-lib: specifier: file:./__test_packages__/fake-context-lib version: file:tests/fixtures/app-basic/__test_packages__/fake-context-lib @@ -1147,7 +1147,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1172,7 +1172,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1205,7 +1205,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1239,7 +1239,7 @@ importers: version: 6.0.1(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1276,7 +1276,7 @@ importers: version: 6.0.1(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1342,7 +1342,7 @@ importers: version: 1.9.1 '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) better-auth: specifier: 'catalog:' version: 1.5.6(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.6.2)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.6.2)(kysely@0.28.15))(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.10) @@ -1373,7 +1373,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-intl: specifier: 'catalog:' version: 4.11.1(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react@19.2.7)(typescript@7.0.2) @@ -1401,7 +1401,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1429,7 +1429,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-view-transitions: specifier: 'catalog:' version: 0.3.5(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1457,7 +1457,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) nuqs: specifier: 'catalog:' version: 2.8.8(next@16.2.7(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.100.0))(react@19.2.7) @@ -1494,7 +1494,7 @@ importers: version: 1.2.4(@types/react@19.2.16)(react@19.2.7) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) class-variance-authority: specifier: 'catalog:' version: 0.7.1 @@ -1550,7 +1550,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1575,7 +1575,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1600,7 +1600,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1693,7 +1693,7 @@ importers: version: 1.31.0(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(workerd@1.20260401.1)(wrangler@4.80.0) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1726,7 +1726,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1773,7 +1773,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -1794,7 +1794,7 @@ importers: dependencies: '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react: specifier: 'catalog:' version: 19.2.7 @@ -5111,8 +5111,8 @@ packages: babel-plugin-react-compiler: optional: true - '@vitejs/plugin-rsc@0.5.33': - resolution: {integrity: sha512-3q4F9yyJQwOqUGHw60ZwdfmWTRERtd8iDiwRgQyudss5wGbNeQP1L4fDDQPAFSKdyRQlXfZiAt8naxvIOmiinQ==} + '@vitejs/plugin-rsc@0.5.34': + resolution: {integrity: sha512-95V6fyGQklQMYIWTr5qwwNmpDYxsHkTunuzq8i/keIcgdckL9zb6nyEgTnb1CEl1IPJWVxGgORMkTFHrct7XJg==} peerDependencies: react: '*' react-dom: '*' @@ -7769,6 +7769,11 @@ packages: engines: {node: '>=20.16.0'} hasBin: true + srvx@0.12.5: + resolution: {integrity: sha512-IuvtDNQg5EIwv3c6dleyau7u8hCyGQ7D6+V/QM799Aud07z0wCUcurKLTRfyG33C8oUY+UWcVBFkfHMcbtmRLA==} + engines: {node: '>=20.16.0'} + hasBin: true + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -10977,7 +10982,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: '@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)' - '@vitejs/plugin-rsc@0.5.33(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': + '@vitejs/plugin-rsc@0.5.34(@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': dependencies: '@rolldown/pluginutils': 1.0.1 es-module-lexer: 2.3.1 @@ -10985,7 +10990,7 @@ snapshots: magic-string: 0.30.21 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - srvx: 0.11.13 + srvx: 0.12.5 strip-literal: 3.1.0 turbo-stream: 3.2.0 vite: '@voidzero-dev/vite-plus-core@0.2.6(@types/node@25.9.2)(esbuild@0.27.3)(jiti@2.7.0)(sass@1.100.0)(tsx@4.21.1)(typescript@7.0.2)(yaml@2.9.0)' @@ -13831,6 +13836,8 @@ snapshots: srvx@0.11.13: {} + srvx@0.12.5: {} + stackback@0.0.2: {} stacktrace-parser@0.1.11: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 804047fe45..971ebc9980 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -44,7 +44,7 @@ catalog: "@next/mdx": 16.2.7 recma-codehike: 0.0.1 remark-codehike: 0.0.1 - "@vitejs/plugin-rsc": ^0.5.33 + "@vitejs/plugin-rsc": ^0.5.34 "@vitest/coverage-istanbul": 4.1.10 better-auth: ^1.5.6 better-sqlite3: ^12.0.0 diff --git a/tests/e2e/app-router-prod/use-cache.spec.ts b/tests/e2e/app-router-prod/use-cache.spec.ts index 447592d2f2..a2e2493825 100644 --- a/tests/e2e/app-router-prod/use-cache.spec.ts +++ b/tests/e2e/app-router-prod/use-cache.spec.ts @@ -24,6 +24,19 @@ test.describe('production "use cache" server function references', () => { await page.locator("#call-client-imported-cache").click(); await expect(page.getByTestId("client-imported-cache-call-count")).toHaveText("3"); await expect(page.getByTestId("client-imported-cache-result")).toHaveText(directResult); + + await page.locator("#call-client-imported-server").click(); + await expect(page.getByTestId("client-imported-cache-call-count")).toHaveText("4"); + await expect(page.getByTestId("client-imported-cache-result")).toHaveText( + /^client-server:direct:[0-9.e+-]+$/, + ); + const firstServerResult = await page.getByTestId("client-imported-cache-result").innerText(); + + await page.locator("#call-client-imported-server").click(); + await expect(page.getByTestId("client-imported-cache-call-count")).toHaveText("5"); + await expect(page.getByTestId("client-imported-cache-result")).not.toHaveText( + firstServerResult, + ); }); test("runs inline use-server and use-cache exports owned by different plugins", async ({ diff --git a/tests/fixtures/app-basic/app/use-cache-client-import/actions.ts b/tests/fixtures/app-basic/app/use-cache-client-import/actions.ts index e27f4713d3..32b47d1e31 100644 --- a/tests/fixtures/app-basic/app/use-cache-client-import/actions.ts +++ b/tests/fixtures/app-basic/app/use-cache-client-import/actions.ts @@ -3,3 +3,8 @@ export async function getCachedMessage(value: string) { return `client-cache:${value}:${Math.random()}`; } + +export async function getUncachedMessage(value: string) { + "use server"; + return `client-server:${value}:${Math.random()}`; +} diff --git a/tests/fixtures/app-basic/app/use-cache-client-import/form.tsx b/tests/fixtures/app-basic/app/use-cache-client-import/form.tsx index 874bb7c6d5..3cc1381227 100644 --- a/tests/fixtures/app-basic/app/use-cache-client-import/form.tsx +++ b/tests/fixtures/app-basic/app/use-cache-client-import/form.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { getCachedMessage } from "./actions"; +import { getCachedMessage, getUncachedMessage } from "./actions"; export function ClientCacheCaller() { const [message, setMessage] = useState(""); @@ -19,6 +19,18 @@ export function ClientCacheCaller() { ); } + function callUncachedMessage(value: string) { + void getUncachedMessage(value).then( + (result) => { + setMessage(result); + setCompletedCalls((count) => count + 1); + }, + (error) => { + setMessage(`error:${error instanceof Error ? error.message : String(error)}`); + }, + ); + } + return (
+ {message} {completedCalls}
diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index 73a86190c7..29570cd3dd 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -70,7 +70,7 @@ async function transformRsc(source: string): Promise { } describe("plugin-rsc inline use-cache references", () => { - it("keeps the vinext claim when rsc:use-server removes its own claim", async () => { + it("keeps the vinext claim through the built-in use-server transform", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); const useCacheIndex = plugins.findIndex( @@ -449,12 +449,13 @@ describe("plugin-rsc inline use-cache references", () => { }, ); - it("preserves inline cache semantics inside a module-level use-server boundary", async () => { + it("composes inline cache semantics with a module-level use-server boundary", async () => { const plugins = await getPlugins(); - await configurePluginRsc(plugins); + const manager = await configurePluginRsc(plugins); const plugin = plugins.find( (candidate) => candidate.name === "vinext:server-function-directives", )!; + const useServerPlugin = plugins.find((candidate) => candidate.name === "rsc:use-server")!; const context = { environment: { name: "rsc", mode: "build" } }; const source = [ `"use server";`, @@ -465,7 +466,59 @@ describe("plugin-rsc inline use-cache references", () => { ].join("\n"); const result = await unwrapHook(plugin.transform)!.call(context, source, moduleId); expect(result?.code).toContain("registerCachedFunction"); - expect(result?.code).not.toContain("registerServerReference"); + expect(result?.code).toMatch(/^"use server";\nimport /); + + const useServerResult = await unwrapHook(useServerPlugin.transform)!.call( + context, + result!.code, + moduleId, + ); + expect(useServerResult?.code).toContain("$$VinextReactServer.registerServerReference"); + expect(() => parseAst(useServerResult!.code)).not.toThrow(); + expect([...manager.serverReferences.claimMap.get(moduleId).keys()]).toEqual([ + "vinext:server-function-directives", + "rsc:use-server", + ]); + const exportNames = manager.serverReferences.metaMap.get(moduleId)!.exportNames; + expect(exportNames).toContainEqual(expect.stringMatching(/getData/)); + expect(exportNames).toHaveLength(new Set(exportNames).size); + }); + + it("leaves inline use-server exports to plugin-rsc inside a file-level cache boundary", async () => { + const plugins = await getPlugins(); + const manager = await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "vinext:server-function-directives", + )!; + const useServerPlugin = plugins.find((candidate) => candidate.name === "rsc:use-server")!; + const context = { environment: { name: "rsc", mode: "build" } }; + const source = [ + `"use cache";`, + `export async function cached() {}`, + `export async function uncached() {`, + ` "use server";`, + `}`, + ].join("\n"); + + const result = await unwrapHook(plugin.transform)!.call(context, source, moduleId); + expect(result?.code).toContain("registerCachedFunction(cached"); + expect(result?.code).not.toContain("registerCachedFunction(uncached"); + expect(result?.code).toMatch(/^"use cache";\nimport /); + + const useServerResult = await unwrapHook(useServerPlugin.transform)!.call( + context, + result!.code, + moduleId, + ); + expect(() => parseAst(useServerResult!.code)).not.toThrow(); + expect([...manager.serverReferences.claimMap.get(moduleId).keys()]).toEqual([ + "vinext:server-function-directives", + "rsc:use-server", + ]); + const exportNames = manager.serverReferences.metaMap.get(moduleId)!.exportNames; + expect(exportNames).toContain("cached"); + expect(exportNames).toContainEqual(expect.stringMatching(/uncached/)); + expect(exportNames).toHaveLength(new Set(exportNames).size); }); it("rejects conflicting file-level cache and use-server directives", async () => { From 286c6eca3c4c51ec702257433d882634f16fb12c Mon Sep 17 00:00:00 2001 From: James Date: Fri, 7 Aug 2026 11:16:51 +0100 Subject: [PATCH 31/33] fix(cache): harden callable use cache transforms --- packages/vinext/src/index.ts | 2 +- .../vinext/src/plugins/use-cache-callable.ts | 284 ++++++++++-------- packages/vinext/src/shims/cache-runtime.ts | 12 +- tests/e2e/app-router-prod/use-cache.spec.ts | 5 + tests/e2e/app-router/use-cache.spec.ts | 43 +++ .../app/prerender-cache-life-only/page.tsx | 2 +- .../app/prerender-cache-life/page.tsx | 4 +- .../app-basic/app/use-cache-hmr/dependency.ts | 1 + tests/use-cache-transform.test.ts | 67 ++++- 9 files changed, 290 insertions(+), 130 deletions(-) create mode 100644 tests/fixtures/app-basic/app/use-cache-hmr/dependency.ts diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index fe87f767d5..51c592e9ad 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -1532,7 +1532,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { projectRoot: earlyBaseDir, cacheRuntime: pathToFileURL(resolveShimModulePath(shimsDir, "cache-callable-runtime")) .href, - appDir, + getAppDir: () => appDir, matchesPageExtension: (fileName) => fileMatcher.extensionRegex.test(fileName), }); const useServerIndex = plugins.findIndex((plugin) => plugin.name === "rsc:use-server"); diff --git a/packages/vinext/src/plugins/use-cache-callable.ts b/packages/vinext/src/plugins/use-cache-callable.ts index 9b718d7781..f22a5db8e5 100644 --- a/packages/vinext/src/plugins/use-cache-callable.ts +++ b/packages/vinext/src/plugins/use-cache-callable.ts @@ -15,7 +15,7 @@ type Program = Awaited>; type Options = { projectRoot: string; cacheRuntime: string; - appDir: string | undefined; + getAppDir: () => string | undefined; matchesPageExtension: (fileName: string) => boolean; }; @@ -26,6 +26,9 @@ type CacheWrapperOptions = { }; const PLUGIN_NAME = "vinext:server-function-directives"; +const SOURCE_MODULE_ID_RE = /\.(?:tsx?|jsx?|mjs)(?:\?.*)?$/; +const DEPENDENCY_MODULE_ID_RE = /[\\/]node_modules[\\/]/; +const RESOLVED_VIRTUAL_MODULE_ID_RE = new RegExp(`^${String.fromCharCode(0)}`); const USE_CACHE_DIRECTIVE = /^use cache(?:: ([^\s].*))?$/; const USE_CACHE_DIRECTIVE_CANDIDATE = /^use cache.*$/; @@ -110,22 +113,39 @@ function isAppPageDefaultExport( name: string, isModuleDirective: boolean, ): boolean { - if (!isModuleDirective || name !== "default" || !options.appDir) return false; + const appDir = options.getAppDir(); + if (!isModuleDirective || name !== "default" || !appDir) return false; const modulePath = stripViteModuleQuery(id); const moduleFileName = path.basename(modulePath); return ( - isInsideDirectory(options.appDir, modulePath) && + isInsideDirectory(appDir, modulePath) && path.parse(moduleFileName).name === "page" && options.matchesPageExtension(moduleFileName) ); } function shouldTransformModuleExport(name: string, id: string, meta: ModuleExportMeta): boolean { - if (meta.isFunction === false) return false; + if ( + meta.isFunction === false && + (meta.valueNode?.type === "ObjectExpression" || meta.valueNode?.type === "ArrayExpression") + ) { + return false; + } if (/\/(layout|template)\.(tsx?|jsx?|mjs)$/.test(id) && name === "default") return false; return true; } +function validateModuleExport(transforms: RscTransforms, meta: ModuleExportMeta): void { + if (!meta.valueNode) return; + if ( + meta.isFunction !== false && + (meta.valueNode.type === "ObjectExpression" || meta.valueNode.type === "ArrayExpression") + ) { + return; + } + transforms.validateNonAsyncFunction({ rejectNonAsyncFunction: true }, meta.valueNode); +} + function hasFunctionDirective( meta: Pick, directive: string, @@ -179,144 +199,164 @@ export async function createUseCacheCallablePlugin(options: Options): Promise { - matchUseCacheDirective(meta.directiveMatch[0]); - throw new Error( - `It is not allowed to define inline "use cache" annotated functions in Client Components. Export them from a separate file with a module-level "use cache" or "use server" directive, or pass them down through props from a Server Component. (${this.environment.name}: ${id})`, - ); + + const ast = await parseAstAsync(code); + const moduleDirective = findModuleUseCacheDirective(ast); + const useServerBoundary = transforms.hasDirective(ast.body, "use server"); + if (moduleDirective && useServerBoundary) { + throw new Error( + `A module cannot contain both ${JSON.stringify(moduleDirective)} and "use server" directives.`, + ); + } + + const reference = manager.serverReferences.resolve(id, "rsc"); + const isRsc = this.environment.name === "rsc"; + + if (!isRsc) { + if (useServerBoundary) { + manager.serverReferences.deleteClaim(PLUGIN_NAME, id); + return; + } + if (!moduleDirective) { + transforms.transformHoistInlineDirective(code, ast, { + directive: USE_CACHE_DIRECTIVE_CANDIDATE, + rejectNonAsyncFunction: true, + runtime: (_value, _name, meta) => { + matchUseCacheDirective(meta.directiveMatch[0]); + throw new Error( + `It is not allowed to define inline "use cache" annotated functions in Client Components. Export them from a separate file with a module-level "use cache" or "use server" directive, or pass them down through props from a Server Component. (${this.environment.name}: ${id})`, + ); + }, + }); + manager.serverReferences.deleteClaim(PLUGIN_NAME, id); + return; + } + + const result = transforms.transformDirectiveProxyExport(ast, { + code, + directive: moduleDirective, + filter: (name, meta) => { + if (!shouldTransformModuleExport(name, id, meta)) return false; + validateModuleExport(transforms, meta); + return true; }, + runtime: (name) => + `$$ReactClient.createServerReference(${JSON.stringify(`${reference.referenceKey}#${name}`)},$$ReactClient.callServer,undefined,${this.environment.mode === "dev" ? "$$ReactClient.findSourceMapURL" : "undefined"},${JSON.stringify(name)})`, }); - manager.serverReferences.deleteClaim(PLUGIN_NAME, id); - return; + if (!result?.output.hasChanged()) { + manager.serverReferences.deleteClaim(PLUGIN_NAME, id); + return; + } + + manager.serverReferences.replaceClaim(PLUGIN_NAME, id, { + ...reference, + exportNames: result.exportNames, + }); + const runtimeEnvironment = this.environment.name === "client" ? "browser" : "ssr"; + result.output.prepend( + `import * as $$ReactClient from "@vitejs/plugin-rsc/react/${runtimeEnvironment}";\n`, + ); + return { + code: result.output.toString(), + map: result.output.generateMap({ hires: "boundary", source: id }), + }; } - const result = transforms.transformDirectiveProxyExport(ast, { - code, - directive: moduleDirective, - filter: (name, meta) => shouldTransformModuleExport(name, id, meta), - runtime: (name) => - `$$ReactClient.createServerReference(${JSON.stringify(`${reference.referenceKey}#${name}`)},$$ReactClient.callServer,undefined,${this.environment.mode === "dev" ? "$$ReactClient.findSourceMapURL" : "undefined"},${JSON.stringify(name)})`, - }); - if (!result?.output.hasChanged()) { + const wrap = ( + value: string, + name: string, + directiveMatch: RegExpMatchArray, + meta: Pick | TransformHoistInlineDirectiveMeta, + isModuleDirective: boolean, + ) => { + const variant = directiveMatch[1] ?? ""; + const wrapperOptions = getCacheWrapperOptions(options, id, name, isModuleDirective, meta); + return `$$cacheRuntime.registerCachedFunction(${value}, ${JSON.stringify(`${id}:${name}`)}, ${JSON.stringify(variant)}, ${JSON.stringify(wrapperOptions)})`; + }; + let needsReactServer = false; + const runtime = ( + value: string, + name: string, + directiveMatch: RegExpMatchArray, + meta: Pick | TransformHoistInlineDirectiveMeta, + isModuleDirective: boolean, + ) => { + const cached = wrap(value, name, directiveMatch, meta, isModuleDirective); + needsReactServer = true; + return `$$VinextReactServer.registerServerReference(${cached}, ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; + }; + + const result = moduleDirective + ? transforms.transformWrapExport(code, ast, { + filter: (name, meta) => { + if ( + hasFunctionDirective(meta, "use server") || + !shouldTransformModuleExport(name, id, meta) + ) { + return false; + } + validateModuleExport(transforms, meta); + return true; + }, + runtime: (value, name, meta) => + runtime(value, name, matchUseCacheDirective(moduleDirective), meta, true), + }) + : transforms.transformHoistInlineDirective(code, ast, { + directive: USE_CACHE_DIRECTIVE_CANDIDATE, + rejectNonAsyncFunction: true, + hoistRuntime: true, + runtime: (value, name, meta) => + runtime(value, name, matchUseCacheDirective(meta.directiveMatch[0]), meta, false), + encode: (value) => `$$cacheRuntime.encryptCacheCaptures(${value})`, + decode: (value) => value, + }); + if (!result.output.hasChanged()) { manager.serverReferences.deleteClaim(PLUGIN_NAME, id); return; } manager.serverReferences.replaceClaim(PLUGIN_NAME, id, { ...reference, - exportNames: result.exportNames, + exportNames: "names" in result ? result.names : result.exportNames, }); - const runtimeEnvironment = this.environment.name === "client" ? "browser" : "ssr"; - result.output.prepend( - `import * as $$ReactClient from "@vitejs/plugin-rsc/react/${runtimeEnvironment}";\n`, + const importPosition = + ast.body.find((node) => !("directive" in node))?.start ?? code.length; + result.output.prependLeft( + importPosition, + [ + `import * as $$cacheRuntime from ${JSON.stringify(options.cacheRuntime)};`, + needsReactServer && + `import * as $$VinextReactServer from "@vitejs/plugin-rsc/react/rsc/server";`, + ] + .filter(Boolean) + .join("\n") + "\n", ); return { code: result.output.toString(), map: result.output.generateMap({ hires: "boundary", source: id }), }; - } - - const wrap = ( - value: string, - name: string, - directiveMatch: RegExpMatchArray, - meta: Pick | TransformHoistInlineDirectiveMeta, - isModuleDirective: boolean, - ) => { - const variant = directiveMatch[1] ?? ""; - const wrapperOptions = getCacheWrapperOptions(options, id, name, isModuleDirective, meta); - return `$$cacheRuntime.registerCachedFunction(${value}, ${JSON.stringify(`${id}:${name}`)}, ${JSON.stringify(variant)}, ${JSON.stringify(wrapperOptions)})`; - }; - let needsReactServer = false; - const runtime = ( - value: string, - name: string, - directiveMatch: RegExpMatchArray, - meta: Pick | TransformHoistInlineDirectiveMeta, - isModuleDirective: boolean, - ) => { - const cached = wrap(value, name, directiveMatch, meta, isModuleDirective); - needsReactServer = true; - return `$$VinextReactServer.registerServerReference(${cached}, ${JSON.stringify(reference.referenceKey)}, ${JSON.stringify(name)})`; - }; - - const result = moduleDirective - ? transforms.transformWrapExport(code, ast, { - filter: (name, meta) => - !hasFunctionDirective(meta, "use server") && - shouldTransformModuleExport(name, id, meta), - runtime: (value, name, meta) => - runtime(value, name, matchUseCacheDirective(moduleDirective), meta, true), - }) - : transforms.transformHoistInlineDirective(code, ast, { - directive: USE_CACHE_DIRECTIVE_CANDIDATE, - rejectNonAsyncFunction: true, - hoistRuntime: true, - runtime: (value, name, meta) => - runtime(value, name, matchUseCacheDirective(meta.directiveMatch[0]), meta, false), - encode: (value) => `$$cacheRuntime.encryptCacheCaptures(${value})`, - decode: (value) => value, - }); - if (!result.output.hasChanged()) { - manager.serverReferences.deleteClaim(PLUGIN_NAME, id); - return; - } - - manager.serverReferences.replaceClaim(PLUGIN_NAME, id, { - ...reference, - exportNames: "names" in result ? result.names : result.exportNames, - }); - const importPosition = ast.body.find((node) => !("directive" in node))?.start ?? code.length; - result.output.prependLeft( - importPosition, - [ - `import * as $$cacheRuntime from ${JSON.stringify(options.cacheRuntime)};`, - needsReactServer && - `import * as $$VinextReactServer from "@vitejs/plugin-rsc/react/rsc/server";`, - ] - .filter(Boolean) - .join("\n") + "\n", - ); - return { - code: result.output.toString(), - map: result.output.generateMap({ hires: "boundary", source: id }), - }; + }, }, }; } diff --git a/packages/vinext/src/shims/cache-runtime.ts b/packages/vinext/src/shims/cache-runtime.ts index 98dc9c60e9..dbc23f4700 100644 --- a/packages/vinext/src/shims/cache-runtime.ts +++ b/packages/vinext/src/shims/cache-runtime.ts @@ -174,7 +174,11 @@ export function getCacheContext(): CacheContext | null { */ type RscModule = { renderToReadableStream: (data: unknown, options?: object) => ReadableStream; - createFromReadableStream: (stream: ReadableStream, options?: object) => Promise; + createFromReadableStream: ( + stream: ReadableStream, + options?: object, + context?: { preserveServerReferences?: boolean }, + ) => Promise; encodeReply: (v: unknown[], options?: unknown) => Promise; createTemporaryReferenceSet: () => unknown; createClientTemporaryReferenceSet: () => unknown; @@ -597,7 +601,11 @@ export function registerCachedFunction( // RSC-serialized entry: base64 → bytes → stream → deserialize const bytes = base64ToUint8(existing.value.data.body); const stream = uint8ToStream(bytes); - const result = await rsc.createFromReadableStream(stream); + const result = await rsc.createFromReadableStream( + stream, + {}, + { preserveServerReferences: true }, + ); recordRequestScopedCacheControl(existing.cacheControl); return result; } diff --git a/tests/e2e/app-router-prod/use-cache.spec.ts b/tests/e2e/app-router-prod/use-cache.spec.ts index a2e2493825..a2301c6944 100644 --- a/tests/e2e/app-router-prod/use-cache.spec.ts +++ b/tests/e2e/app-router-prod/use-cache.spec.ts @@ -80,6 +80,11 @@ test.describe('production "use cache" server function references', () => { await page.goto("/use-cache-nested-fn-props"); await expect(page.getByTestId("use-cache-nested-fn-props-page")).toBeVisible(); + // Force a second server request so this test exercises the cache-hit Flight + // replay path before invoking the nested references in the browser. + await page.reload(); + await expect(page.getByTestId("use-cache-nested-fn-props-page")).toBeVisible(); + await page.locator("#submit-button-date").click(); await expect(page.locator("#date")).toHaveText(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); const firstDate = await page.locator("#date").textContent(); diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index 8dc1725f29..b3cde1bcb9 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -11,6 +11,10 @@ const USE_CACHE_MIXED_HMR_ACTIONS_FILE = path.join( process.cwd(), "tests/fixtures/app-basic/app/use-cache-mixed-ownership/actions.ts", ); +const USE_CACHE_HMR_DEPENDENCY_FILE = path.join( + process.cwd(), + "tests/fixtures/app-basic/app/use-cache-hmr/dependency.ts", +); const USE_CACHE_HMR_CACHED = `"use cache"; export async function getMode() { @@ -23,6 +27,16 @@ export async function getMode() { return "plain"; } `; +const USE_CACHE_HMR_IMPORTED = `"use cache"; + +import { dependencyMode } from "./dependency"; + +export async function getMode() { + return dependencyMode; +} +`; +const USE_CACHE_HMR_DEPENDENCY_INITIAL = `export const dependencyMode = "dependency-initial";\n`; +const USE_CACHE_HMR_DEPENDENCY_UPDATED = `export const dependencyMode = "dependency-updated";\n`; const USE_CACHE_MIXED_HMR_CACHED = [ `export const ownershipLabel = "cache";`, ``, @@ -228,6 +242,35 @@ test.describe('"use cache" transform coverage', () => { } }); + test("refreshes cached functions when only an imported dependency changes", async ({ + page, + request, + }) => { + await writeFile(USE_CACHE_HMR_DEPENDENCY_FILE, USE_CACHE_HMR_DEPENDENCY_INITIAL); + await writeUseCacheHmrActions(USE_CACHE_HMR_IMPORTED, true); + try { + await waitForUseCacheHmrTransform(request); + await page.goto(`${BASE}/use-cache-hmr`); + await expect(async () => { + await page.locator("#call-use-cache-hmr").click(); + await expect(page.getByTestId("use-cache-hmr-result")).toHaveText("dependency-initial", { + timeout: 2000, + }); + }).toPass({ timeout: 15_000 }); + + await writeFile(USE_CACHE_HMR_DEPENDENCY_FILE, USE_CACHE_HMR_DEPENDENCY_UPDATED); + await expect(async () => { + await page.locator("#call-use-cache-hmr").click(); + await expect(page.getByTestId("use-cache-hmr-result")).toHaveText("dependency-updated", { + timeout: 2000, + }); + }).toPass({ timeout: 15_000 }); + } finally { + await writeFile(USE_CACHE_HMR_DEPENDENCY_FILE, USE_CACHE_HMR_DEPENDENCY_INITIAL); + await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED); + } + }); + test("moves one export between use-cache and use-server ownership without reloading", async ({ page, }) => { diff --git a/tests/fixtures/app-basic/app/prerender-cache-life-only/page.tsx b/tests/fixtures/app-basic/app/prerender-cache-life-only/page.tsx index abf7d1c072..ad32ea519c 100644 --- a/tests/fixtures/app-basic/app/prerender-cache-life-only/page.tsx +++ b/tests/fixtures/app-basic/app/prerender-cache-life-only/page.tsx @@ -2,7 +2,7 @@ import { cacheLife } from "next/cache"; -export default function PrerenderCacheLifeOnlyPage() { +export default async function PrerenderCacheLifeOnlyPage() { cacheLife({ revalidate: 1, expire: 3 }); return ( diff --git a/tests/fixtures/app-basic/app/prerender-cache-life/page.tsx b/tests/fixtures/app-basic/app/prerender-cache-life/page.tsx index 6be56c10e5..934d305504 100644 --- a/tests/fixtures/app-basic/app/prerender-cache-life/page.tsx +++ b/tests/fixtures/app-basic/app/prerender-cache-life/page.tsx @@ -2,9 +2,7 @@ import { cacheLife } from "next/cache"; -export const revalidate = 1; - -export default function PrerenderCacheLifePage() { +export default async function PrerenderCacheLifePage() { cacheLife({ revalidate: 1, expire: 3 }); return ( diff --git a/tests/fixtures/app-basic/app/use-cache-hmr/dependency.ts b/tests/fixtures/app-basic/app/use-cache-hmr/dependency.ts new file mode 100644 index 0000000000..d5a451ecf6 --- /dev/null +++ b/tests/fixtures/app-basic/app/use-cache-hmr/dependency.ts @@ -0,0 +1 @@ +export const dependencyMode = "dependency-initial"; diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index 29570cd3dd..43c76393b3 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -55,6 +55,15 @@ async function configurePluginRsc(plugins: Plugin[]) { return (minimal as any).api.manager; } +async function configureVinext(plugins: Plugin[]) { + const configPlugin = plugins.find((plugin) => plugin.name === "vinext:config")!; + await unwrapHook(configPlugin.config)!.call( + configPlugin, + { root: APP_FIXTURE_DIR }, + { command: "build", mode: "test" }, + ); +} + async function transformRsc(source: string): Promise { const plugins = await getPlugins(); await configurePluginRsc(plugins); @@ -414,7 +423,7 @@ describe("plugin-rsc inline use-cache references", () => { ); }); - it("rejects statically known synchronous cached functions", async () => { + it("rejects statically known synchronous inline cached functions", async () => { const plugins = await getPlugins(); await configurePluginRsc(plugins); const plugin = plugins.find( @@ -430,6 +439,45 @@ describe("plugin-rsc inline use-cache references", () => { ).rejects.toThrow(/non async function/); }); + it.each(["rsc", "ssr", "client"])( + "rejects synchronous exports from file-level cache modules in the %s graph", + async (environmentName) => { + // Ported from Next.js: crates/next-custom-transforms/tests/errors/server-actions/server-graph/14/input.js + // https://github.com/vercel/next.js/blob/canary/crates/next-custom-transforms/tests/errors/server-actions/server-graph/14/input.js + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "vinext:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + await expect( + transform.call( + { environment: { name: environmentName, mode: "build" } }, + [`"use cache";`, `export function getData() { return 1; }`].join("\n"), + moduleId, + ), + ).rejects.toThrow(/non async function/); + }, + ); + + it("rejects primitive exports from file-level cache modules", async () => { + // Ported from Next.js: crates/next-custom-transforms/tests/errors/server-actions/server-graph/14/input.js + // https://github.com/vercel/next.js/blob/canary/crates/next-custom-transforms/tests/errors/server-actions/server-graph/14/input.js + const plugins = await getPlugins(); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "vinext:server-function-directives", + )!; + const transform = unwrapHook(plugin.transform)!; + await expect( + transform.call( + { environment: { name: "rsc", mode: "build" } }, + [`"use cache";`, `export const value = 1;`].join("\n"), + moduleId, + ), + ).rejects.toThrow(/non async function/); + }); + it.each(["use cache:remote", "use cache remote", "use cache : remote"])( "rejects malformed cache directive %s", async (directive) => { @@ -570,6 +618,23 @@ describe("plugin-rsc inline use-cache references", () => { expect(manager.serverReferences.metaMap.get(moduleId)!.exportNames).toEqual(["getData"]); }); + it("marks file-level App Page default exports after Vinext resolves the app directory", async () => { + const plugins = await getPlugins(); + await configureVinext(plugins); + await configurePluginRsc(plugins); + const plugin = plugins.find( + (candidate) => candidate.name === "vinext:server-function-directives", + )!; + const pageId = path.join(APP_FIXTURE_DIR, "app", "page.tsx"); + const result = await unwrapHook(plugin.transform)!.call( + { environment: { name: "rsc", mode: "build" } }, + [`"use cache";`, `export default async function Page() { return null; }`].join("\n"), + pageId, + ); + + expect(result?.code).toContain('"appPageDefaultExport":true'); + }); + it.each(["ssr", "client"])( "emits server-reference proxies for file-level cache exports in the %s graph", async (environmentName) => { From ad542dadd877894a0839bf5b5355e1182ba3060f Mon Sep 17 00:00:00 2001 From: James Date: Fri, 7 Aug 2026 11:31:22 +0100 Subject: [PATCH 32/33] fix(cache): support manually configured RSC --- packages/vinext/src/index.ts | 12 +++++ .../vinext/src/plugins/use-cache-callable.ts | 7 +-- tests/e2e/app-router-prod/use-cache.spec.ts | 6 +++ tests/e2e/app-router/use-cache.spec.ts | 4 ++ .../app/use-cache-nested-fn-props/page.tsx | 47 +++++++++++-------- tests/use-cache-transform.test.ts | 31 ++++++++++-- 6 files changed, 81 insertions(+), 26 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index f2f311db53..0fe2f3e973 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -1568,6 +1568,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // resolves to the configured RSC plugin array. Vite's asyncFlatten // will resolve this before processing the plugin list. let rscPluginPromise: Promise | null = null; + let manualUseCachePluginPromise: Promise | null = null; if (earlyAppDirExists && autoRsc) { if (!resolvedRscPath) { throw new Error( @@ -1608,6 +1609,15 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { cause, }); }); + } else if (earlyAppDirExists && resolvedRscPath) { + rscPluginModulePromise = import(pathToFileURL(resolvedRscPath).href); + manualUseCachePluginPromise = createUseCacheCallablePlugin({ + projectRoot: earlyBaseDir, + cacheRuntime: pathToFileURL(resolveShimModulePath(shimsDir, "cache-callable-runtime")).href, + getAppDir: () => appDir, + matchesPageExtension: (fileName) => fileMatcher.extensionRegex.test(fileName), + allowMissingRsc: true, + }); } async function resolveHasServerActions( @@ -6805,6 +6815,8 @@ export const loadServerActionClient = ${ plugins.push(rscPluginPromise); plugins.push(createRscReferenceValidationNormalizerPlugin()); plugins.push(createRscClientReferenceLoadersPlugin()); + } else if (manualUseCachePluginPromise) { + plugins.push(manualUseCachePluginPromise); } return plugins; diff --git a/packages/vinext/src/plugins/use-cache-callable.ts b/packages/vinext/src/plugins/use-cache-callable.ts index f22a5db8e5..f7c6e7439e 100644 --- a/packages/vinext/src/plugins/use-cache-callable.ts +++ b/packages/vinext/src/plugins/use-cache-callable.ts @@ -17,6 +17,7 @@ type Options = { cacheRuntime: string; getAppDir: () => string | undefined; matchesPageExtension: (fileName: string) => boolean; + allowMissingRsc?: boolean; }; type CacheWrapperOptions = { @@ -200,6 +201,8 @@ export async function createUseCacheCallablePlugin(options: Options): Promise plugin.name === "rsc"); + if (!pluginApi && options.allowMissingRsc && !hasRscPlugin) return; if (!pluginApi?.manager.serverReferences) { throw new Error("vinext: callable use cache requires @vitejs/plugin-rsc 0.5.34 or newer."); } @@ -213,9 +216,7 @@ export async function createUseCacheCallablePlugin(options: Options): Promise { test("separates arguments for file-level cached exports imported by a Client Component", async ({ page, }) => { await page.goto("/use-cache-client-import"); + await waitForAppRouterHydration(page); await page.locator("#call-client-imported-cache").click(); await expect(page.getByTestId("client-imported-cache-call-count")).toHaveText("1"); @@ -44,6 +46,7 @@ test.describe('production "use cache" server function references', () => { }) => { await page.goto("/use-cache-mixed-ownership"); await expect(page.getByTestId("use-cache-mixed-ownership-page")).toBeVisible(); + await waitForAppRouterHydration(page); await page.locator("#call-mixed-builtin").click(); await expect(page.getByTestId("mixed-builtin-call-count")).toHaveText("1"); @@ -79,11 +82,14 @@ test.describe('production "use cache" server function references', () => { }) => { await page.goto("/use-cache-nested-fn-props"); await expect(page.getByTestId("use-cache-nested-fn-props-page")).toBeVisible(); + const cachedRender = await page.getByTestId("nested-cache-render").textContent(); // Force a second server request so this test exercises the cache-hit Flight // replay path before invoking the nested references in the browser. await page.reload(); await expect(page.getByTestId("use-cache-nested-fn-props-page")).toBeVisible(); + await expect(page.getByTestId("nested-cache-render")).toHaveText(cachedRender!); + await waitForAppRouterHydration(page); await page.locator("#submit-button-date").click(); await expect(page.locator("#date")).toHaveText(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index b3cde1bcb9..c161a70c49 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -257,6 +257,7 @@ test.describe('"use cache" transform coverage', () => { timeout: 2000, }); }).toPass({ timeout: 15_000 }); + await page.evaluate(() => Reflect.set(window, "__vinextUseCacheDependencyHmr", true)); await writeFile(USE_CACHE_HMR_DEPENDENCY_FILE, USE_CACHE_HMR_DEPENDENCY_UPDATED); await expect(async () => { @@ -265,6 +266,9 @@ test.describe('"use cache" transform coverage', () => { timeout: 2000, }); }).toPass({ timeout: 15_000 }); + expect(await page.evaluate(() => Reflect.get(window, "__vinextUseCacheDependencyHmr"))).toBe( + true, + ); } finally { await writeFile(USE_CACHE_HMR_DEPENDENCY_FILE, USE_CACHE_HMR_DEPENDENCY_INITIAL); await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED); diff --git a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx index ccd4af92b4..a8b1b51c5b 100644 --- a/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx +++ b/tests/fixtures/app-basic/app/use-cache-nested-fn-props/page.tsx @@ -31,31 +31,38 @@ async function CachedForm({ }) { "use cache"; + const suffix = idSuffix ? `-${idSuffix}` : ""; + // Closure-captured by getMessage below. The hoist transform lifts the // capture into a `.bind(null, capturedScopeValue)` bound argument on the // server reference. The binding is encrypted before RSC serialization and // decrypted before the cached wrapper builds its argument-based cache key. return ( - { - "use cache"; - return new Date().toISOString(); - }} - getRandom={async function getRandom() { - "use cache"; - return Math.random(); - }} - getMessage={async () => { - "use cache"; - // The Math.random() suffix makes cache hits on the closure-BOUND path - // observable: only a cache hit can repeat the suffix, so the - // production-server test can assert that two invocations with the - // same bound arg return the identical cached value (and that a - // different bound arg misses instead of reusing the entry). - return `message:${capturedScopeValue}:${Math.random()}`; - }} - /> + <> + + { + "use cache"; + return new Date().toISOString(); + }} + getRandom={async function getRandom() { + "use cache"; + return Math.random(); + }} + getMessage={async () => { + "use cache"; + // The Math.random() suffix makes cache hits on the closure-BOUND path + // observable: only a cache hit can repeat the suffix, so the + // production-server test can assert that two invocations with the + // same bound arg return the identical cached value (and that a + // different bound arg misses instead of reusing the entry). + return `message:${capturedScopeValue}:${Math.random()}`; + }} + /> + ); } diff --git a/tests/use-cache-transform.test.ts b/tests/use-cache-transform.test.ts index 43c76393b3..d0c8a3239a 100644 --- a/tests/use-cache-transform.test.ts +++ b/tests/use-cache-transform.test.ts @@ -9,16 +9,22 @@ import { createHash } from "node:crypto"; import { describe, expect, it } from "vite-plus/test"; import { parseAst, type Plugin } from "vite"; import vinext from "../packages/vinext/src/index.js"; -import { APP_FIXTURE_DIR } from "./helpers.js"; +import { APP_FIXTURE_DIR, RSC_ENTRIES } from "./helpers.js"; // oxlint-disable-next-line typescript/no-explicit-any function unwrapHook(hook: any): ((...args: any[]) => any) | undefined { return typeof hook === "function" ? hook : hook?.handler; } -async function getPlugins(): Promise { +async function getPlugins(options: { manualRsc?: boolean } = {}): Promise { // oxlint-disable-next-line typescript/no-explicit-any - const rawPlugins = (vinext({ appDir: APP_FIXTURE_DIR }) as any[]).flat(Infinity); + const rawPlugins = ( + vinext({ appDir: APP_FIXTURE_DIR, rsc: options.manualRsc ? false : undefined }) as any[] + ).flat(Infinity); + if (options.manualRsc) { + const rsc = (await import("@vitejs/plugin-rsc")).default; + rawPlugins.push(rsc({ entries: RSC_ENTRIES })); + } const resolved = await Promise.all(rawPlugins.map((plugin) => Promise.resolve(plugin))); return resolved.flat(Infinity).filter(Boolean) as Plugin[]; } @@ -79,6 +85,25 @@ async function transformRsc(source: string): Promise { } describe("plugin-rsc inline use-cache references", () => { + it("supports an explicitly registered RSC plugin", async () => { + const plugins = await getPlugins({ manualRsc: true }); + const manager = await configurePluginRsc(plugins); + const useCacheIndex = plugins.findIndex( + (candidate) => candidate.name === "vinext:server-function-directives", + ); + const useServerIndex = plugins.findIndex((candidate) => candidate.name === "rsc:use-server"); + expect(useCacheIndex).toBeGreaterThanOrEqual(0); + expect(useCacheIndex).toBeLessThan(useServerIndex); + + const transformed = await unwrapHook(plugins[useCacheIndex]!.transform)!.call( + { environment: { name: "rsc", mode: "build" } }, + inlineCacheCode, + moduleId, + ); + expect(transformed!.code).toContain("registerCachedFunction"); + expect(manager.serverReferences.metaMap.get(moduleId)).toBeDefined(); + }); + it("keeps the vinext claim through the built-in use-server transform", async () => { const plugins = await getPlugins(); const manager = await configurePluginRsc(plugins); From 517415525aa3c68de681a235c043ed521c174f86 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 7 Aug 2026 11:42:08 +0100 Subject: [PATCH 33/33] fix(cache): harden manual RSC ordering --- .../vinext/src/plugins/use-cache-callable.ts | 11 +++++++++ tests/app-router-rsc-plugin.test.ts | 15 ++++++++++++ tests/e2e/app-router/use-cache.spec.ts | 23 +++++++++++-------- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/vinext/src/plugins/use-cache-callable.ts b/packages/vinext/src/plugins/use-cache-callable.ts index f7c6e7439e..bea55e69d4 100644 --- a/packages/vinext/src/plugins/use-cache-callable.ts +++ b/packages/vinext/src/plugins/use-cache-callable.ts @@ -206,6 +206,17 @@ export async function createUseCacheCallablePlugin(options: Options): Promise plugin.name === PLUGIN_NAME); + const useServerIndex = config.plugins.findIndex( + (plugin) => plugin.name === "rsc:use-server", + ); + if (useServerIndex !== -1 && useCacheIndex > useServerIndex) { + throw new Error( + "vinext: when configuring @vitejs/plugin-rsc manually, vinext({ rsc: false }) must appear before rsc() in the Vite plugins array.", + ); + } + } manager = pluginApi.manager; }, transform: { diff --git a/tests/app-router-rsc-plugin.test.ts b/tests/app-router-rsc-plugin.test.ts index 2d9f01e256..92b768e7fa 100644 --- a/tests/app-router-rsc-plugin.test.ts +++ b/tests/app-router-rsc-plugin.test.ts @@ -111,6 +111,21 @@ describe("RSC plugin auto-registration", () => { } }, 30000); + it("rejects a manually registered RSC plugin placed before vinext", async () => { + const { createServer } = await import("vite"); + const rsc = (await import("@vitejs/plugin-rsc")).default; + await expect( + createServer({ + root: APP_FIXTURE_DIR, + configFile: false, + plugins: [rsc({ entries: RSC_ENTRIES }), vinext({ appDir: APP_FIXTURE_DIR, rsc: false })], + optimizeDeps: { holdUntilCrawlEnd: true }, + server: { port: 0, cors: false }, + logLevel: "silent", + }), + ).rejects.toThrow("vinext({ rsc: false }) must appear before rsc()"); + }, 30000); + it("throws an error when user double-registers rsc() alongside auto-registration", async () => { const { createBuilder } = await import("vite"); const rsc = (await import("@vitejs/plugin-rsc")).default; diff --git a/tests/e2e/app-router/use-cache.spec.ts b/tests/e2e/app-router/use-cache.spec.ts index c161a70c49..68ac1b9bf2 100644 --- a/tests/e2e/app-router/use-cache.spec.ts +++ b/tests/e2e/app-router/use-cache.spec.ts @@ -67,17 +67,19 @@ const USE_CACHE_MIXED_HMR_SERVER = [ ].join("\n"); async function writeUseCacheHmrActions(content: string, forceUpdate = false) { - const nextContent = forceUpdate ? `${content}// hmr-update:${Date.now()}\n` : content; + const updateMarker = forceUpdate ? `// hmr-update:${Date.now()}` : undefined; + const nextContent = updateMarker ? `${content}${updateMarker}\n` : content; if ((await readFile(USE_CACHE_HMR_ACTIONS_FILE, "utf8")) !== nextContent) { await writeFile(USE_CACHE_HMR_ACTIONS_FILE, nextContent); } + return updateMarker; } -async function waitForUseCacheHmrTransform(request: APIRequestContext) { +async function waitForUseCacheHmrTransform(request: APIRequestContext, updateMarker?: string) { await expect .poll(async () => { const response = await request.get(`${BASE}/app/use-cache-hmr/actions.ts?t=${Date.now()}`); - return response.ok(); + return response.ok() && (!updateMarker || (await response.text()).includes(updateMarker)); }) .toBe(true); } @@ -208,8 +210,9 @@ test.describe('"use cache" transform coverage', () => { }); test("removes and restores directive metadata during HMR", async ({ page, request }) => { - await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED, true); + const initialUpdate = await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED, true); try { + await waitForUseCacheHmrTransform(request, initialUpdate); await page.goto(`${BASE}/use-cache-hmr`); await expect(async () => { await page.locator("#call-use-cache-hmr").click(); @@ -218,8 +221,8 @@ test.describe('"use cache" transform coverage', () => { }); }).toPass({ timeout: 15_000 }); - await writeUseCacheHmrActions(USE_CACHE_HMR_PLAIN, true); - await waitForUseCacheHmrTransform(request); + const plainUpdate = await writeUseCacheHmrActions(USE_CACHE_HMR_PLAIN, true); + await waitForUseCacheHmrTransform(request, plainUpdate); await page.reload(); await expect(async () => { await page.locator("#call-use-cache-hmr").click(); @@ -228,8 +231,8 @@ test.describe('"use cache" transform coverage', () => { }); }).toPass({ timeout: 15_000 }); - await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED, true); - await waitForUseCacheHmrTransform(request); + const cachedUpdate = await writeUseCacheHmrActions(USE_CACHE_HMR_CACHED, true); + await waitForUseCacheHmrTransform(request, cachedUpdate); await page.reload(); await expect(async () => { await page.locator("#call-use-cache-hmr").click(); @@ -247,9 +250,9 @@ test.describe('"use cache" transform coverage', () => { request, }) => { await writeFile(USE_CACHE_HMR_DEPENDENCY_FILE, USE_CACHE_HMR_DEPENDENCY_INITIAL); - await writeUseCacheHmrActions(USE_CACHE_HMR_IMPORTED, true); + const importedUpdate = await writeUseCacheHmrActions(USE_CACHE_HMR_IMPORTED, true); try { - await waitForUseCacheHmrTransform(request); + await waitForUseCacheHmrTransform(request, importedUpdate); await page.goto(`${BASE}/use-cache-hmr`); await expect(async () => { await page.locator("#call-use-cache-hmr").click();