From ea50d9dcd032df586ec64c0810e0166a9197a8bf Mon Sep 17 00:00:00 2001 From: James Date: Tue, 9 Jun 2026 14:07:21 +0100 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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);