From 3bb990e0a4ea33b6c8b08a6edf2121ef59c43ca0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:18:29 +0900 Subject: [PATCH 01/10] test(rsc): cover mixed server and cache directives Co-authored-by: OpenCode --- .../plugin-rsc/e2e/use-cache-callable.test.ts | 58 +++++++++++++++++++ .../examples/use-cache-callable/README.md | 2 + .../callable-cache-plugin.ts | 12 ++-- .../use-cache-in-use-server/actions.ts | 19 ++++++ .../use-cache-in-use-server/client.tsx | 33 +++++++++++ .../use-cache-in-use-server/server.tsx | 11 ++++ .../features/use-cache-in-use-server/state.ts | 4 ++ .../use-server-in-use-cache/actions.ts | 14 +++++ .../use-server-in-use-cache/client.tsx | 34 +++++++++++ .../features/use-server-in-use-cache/reset.ts | 10 ++++ .../use-server-in-use-cache/server.tsx | 11 ++++ .../features/use-server-in-use-cache/state.ts | 4 ++ .../examples/use-cache-callable/src/root.tsx | 16 +++++ 13 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/actions.ts create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/client.tsx create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/server.tsx create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/state.ts create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/actions.ts create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/client.tsx create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/reset.ts create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/server.tsx create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/state.ts diff --git a/packages/plugin-rsc/e2e/use-cache-callable.test.ts b/packages/plugin-rsc/e2e/use-cache-callable.test.ts index 74f0457ce..1563e7c03 100644 --- a/packages/plugin-rsc/e2e/use-cache-callable.test.ts +++ b/packages/plugin-rsc/e2e/use-cache-callable.test.ts @@ -368,6 +368,52 @@ function defineTests(f: Fixture) { await expect(result).toHaveText('arguments: 0') }) + test('use cache in use server', async ({ page }) => { + using _errors = expectNoPageError(page) + await page.goto(f.url('/use-cache-in-use-server')) + await waitForHydration(page) + + const example = page.getByTestId('use-cache-in-use-server') + const defaultExecutions = example.getByTestId('default-executions') + const overrideExecutions = example.getByTestId('override-executions') + await example.getByRole('button', { name: 'Reset' }).click() + await expect(defaultExecutions).toHaveText('0') + await expect(overrideExecutions).toHaveText('0') + + // The file directive supplies ordinary server behavior by default. + await callAction(page, example, 'Call default action') + await callAction(page, example, 'Call default action') + await expect(defaultExecutions).toHaveText('2') + + // The inline cache wrapper runs once for the same argument. + await callAction(page, example, 'Call override action') + await callAction(page, example, 'Call override action') + await expect(overrideExecutions).toHaveText('1') + }) + + test('use server in use cache', async ({ page }) => { + using _errors = expectNoPageError(page) + await page.goto(f.url('/use-server-in-use-cache')) + await waitForHydration(page) + + const example = page.getByTestId('use-server-in-use-cache') + const defaultExecutions = example.getByTestId('default-executions') + const overrideExecutions = example.getByTestId('override-executions') + await example.getByRole('button', { name: 'Reset' }).click() + await expect(defaultExecutions).toHaveText('0') + await expect(overrideExecutions).toHaveText('0') + + await callAction(page, example, 'Call default action') + await callAction(page, example, 'Call default action') + await expect(defaultExecutions).toHaveText('1') + + // This characterizes the current transform order: the file-level cache + // wrapper is applied before the built-in transform sees "use server". + await callAction(page, example, 'Call override action') + await callAction(page, example, 'Call override action') + await expect(overrideExecutions).toHaveText('1') + }) + test('protected captures', async ({ page }) => { // verify captured value is encoded and thus doesn't appear in raw response const rscResponse = await page.request.get( @@ -425,6 +471,18 @@ function defineTests(f: Fixture) { }) } +async function callAction(page: Page, example: Locator, name: string) { + const [response] = await Promise.all([ + page.waitForResponse( + (response) => + response.request().method() === 'POST' && + response.url().includes('_.rsc'), + ), + example.getByRole('button', { name }).click(), + ]) + expect(response.ok()).toBe(true) +} + async function submit(page: Page, form: Locator) { // `submissionCount` updates immediately on the client, while a cache hit leaves // the server-rendered execution count and result unchanged. Those assertions do diff --git a/packages/plugin-rsc/examples/use-cache-callable/README.md b/packages/plugin-rsc/examples/use-cache-callable/README.md index f61a05947..53ce273a2 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/README.md +++ b/packages/plugin-rsc/examples/use-cache-callable/README.md @@ -53,6 +53,8 @@ Arguments are serialized with React's `encodeReply()` so values supported by the | `/file-directive-from-client` | A Client Component imports a cached export through its generated proxy. | | `/file-directive-extra-arguments` | A zero-parameter module export excludes React-supplied caller arguments. | | `/inline-directive-extra-arguments` | A zero-parameter inline function uses transform metadata to exclude React-supplied caller arguments. | +| `/use-cache-in-use-server` | An inline cached export overrides the default behavior of a `"use server"` module. | +| `/use-server-in-use-cache` | Characterizes the current handling of an inline server export in a `"use cache"` module. | | `/protected-captures` | Inline captures cross the client boundary encrypted while decoded values define cache identity. | Each route displays submission and execution counts. Every form submission calls the server reference, while the function body runs only on a cache miss. diff --git a/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts b/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts index 5825c3a5a..464e048cf 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts @@ -36,7 +36,7 @@ export function callableCachePlugin(): Plugin { name: string, options: CacheWrapperOptions, ) => - `$$ReactServer.registerServerReference(` + + `$$CacheReactServer.registerServerReference(` + `$$cacheWrapper(${value}, ${JSON.stringify(options)}),` + `${JSON.stringify(reference.referenceKey)},` + `${JSON.stringify(name)})` @@ -75,7 +75,7 @@ export function callableCachePlugin(): Plugin { }) result.output.prepend( `import $$cacheWrapper, { encryptCacheCaptures as $$encryptCacheCaptures } from "/src/framework/use-cache-runtime";\n` + - `import * as $$ReactServer from "@vitejs/plugin-rsc/react/rsc/server";\n`, + `import * as $$CacheReactServer from "@vitejs/plugin-rsc/react/rsc/server";\n`, ) return { code: result.output.toString(), @@ -91,12 +91,12 @@ export function callableCachePlugin(): Plugin { meta.valueNode?.type !== 'ArrayExpression', rejectNonAsyncFunction: true, runtime: (name) => - `$$ReactClient.createServerReference(` + + `$$CacheReactClient.createServerReference(` + `${JSON.stringify(reference.referenceKey + '#' + name)},` + - `$$ReactClient.callServer,` + + `$$CacheReactClient.callServer,` + `undefined,` + (this.environment.mode === 'dev' - ? `$$ReactClient.findSourceMapURL,` + ? `$$CacheReactClient.findSourceMapURL,` : `undefined,`) + `${JSON.stringify(name)})`, }) @@ -112,7 +112,7 @@ export function callableCachePlugin(): Plugin { const runtimeEnvironment = environmentName === 'client' ? 'browser' : 'ssr' result.output.prepend( - `import * as $$ReactClient from "@vitejs/plugin-rsc/react/${runtimeEnvironment}";\n`, + `import * as $$CacheReactClient from "@vitejs/plugin-rsc/react/${runtimeEnvironment}";\n`, ) return { code: result.output.toString(), diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/actions.ts new file mode 100644 index 000000000..311ccf487 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/actions.ts @@ -0,0 +1,19 @@ +'use server' + +import { resetCache } from '../../framework/use-cache-runtime' +import { state } from './state' + +export async function defaultAction(_key: string) { + state.defaultExecutions++ +} + +export async function overrideAction(_key: string) { + 'use cache' + state.overrideExecutions++ +} + +export async function resetAction() { + resetCache() + state.defaultExecutions = 0 + state.overrideExecutions = 0 +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/client.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/client.tsx new file mode 100644 index 000000000..d2ce893b3 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/client.tsx @@ -0,0 +1,33 @@ +'use client' + +import { defaultAction, overrideAction, resetAction } from './actions' + +export function UseCacheInUseServerClient(props: { + defaultExecutions: number + overrideExecutions: number +}) { + return ( +
+
+ +
+
+ +
+

+ Default executions:{' '} + + {props.defaultExecutions} + +
+ Override executions:{' '} + + {props.overrideExecutions} + +

+
+ +
+
+ ) +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/server.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/server.tsx new file mode 100644 index 000000000..b56584944 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/server.tsx @@ -0,0 +1,11 @@ +import { UseCacheInUseServerClient } from './client' +import { state } from './state' + +export function UseCacheInUseServer() { + return ( + + ) +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/state.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/state.ts new file mode 100644 index 000000000..8cbac1600 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/state.ts @@ -0,0 +1,4 @@ +export const state = { + defaultExecutions: 0, + overrideExecutions: 0, +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/actions.ts new file mode 100644 index 000000000..67028ecd8 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/actions.ts @@ -0,0 +1,14 @@ +'use cache' + +import { state } from './state' + +// Variable exports stay assignable when the file-level cache wrapper runs +// before the inline server transform. +export const defaultAction = async (_key: string) => { + state.defaultExecutions++ +} + +export const overrideAction = async (_key: string) => { + 'use server' + state.overrideExecutions++ +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/client.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/client.tsx new file mode 100644 index 000000000..f8c4547ba --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/client.tsx @@ -0,0 +1,34 @@ +'use client' + +import { defaultAction, overrideAction } from './actions' +import { resetAction } from './reset' + +export function UseServerInUseCacheClient(props: { + defaultExecutions: number + overrideExecutions: number +}) { + return ( +
+
+ +
+
+ +
+

+ Default executions:{' '} + + {props.defaultExecutions} + +
+ Override executions:{' '} + + {props.overrideExecutions} + +

+
+ +
+
+ ) +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/reset.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/reset.ts new file mode 100644 index 000000000..16911e798 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/reset.ts @@ -0,0 +1,10 @@ +'use server' + +import { resetCache } from '../../framework/use-cache-runtime' +import { state } from './state' + +export async function resetAction() { + resetCache() + state.defaultExecutions = 0 + state.overrideExecutions = 0 +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/server.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/server.tsx new file mode 100644 index 000000000..5b36f649c --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/server.tsx @@ -0,0 +1,11 @@ +import { UseServerInUseCacheClient } from './client' +import { state } from './state' + +export function UseServerInUseCache() { + return ( + + ) +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/state.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/state.ts new file mode 100644 index 000000000..8cbac1600 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/state.ts @@ -0,0 +1,4 @@ +export const state = { + defaultExecutions: 0, + overrideExecutions: 0, +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx index 2e1aa3c76..04d75eed4 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx @@ -4,6 +4,8 @@ import { FileDirectiveFromServer } from './features/file-directive-from-server/s import { InlineDirectiveExtraArgumentsServer } from './features/inline-directive-extra-arguments/server' import { InlineDirective } from './features/inline-directive/server' import { ProtectedCaptures } from './features/protected-captures/server' +import { UseCacheInUseServer } from './features/use-cache-in-use-server/server' +import { UseServerInUseCache } from './features/use-server-in-use-cache/server' const routes = [ { @@ -41,6 +43,20 @@ const routes = [ 'A zero-parameter inline cached function ignores FormData supplied by React.', Component: InlineDirectiveExtraArgumentsServer, }, + { + path: '/use-cache-in-use-server', + title: 'Use cache in use server', + description: + 'An inline cached export overrides the default behavior of a server function module.', + Component: UseCacheInUseServer, + }, + { + path: '/use-server-in-use-cache', + title: 'Use server in use cache', + description: + 'An inline server export currently retains the default behavior of a cached function module.', + Component: UseServerInUseCache, + }, { path: '/protected-captures', title: 'Protected captures', From 85dd899f316ac0b9b184cb28e053022592ab95da Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:28:17 +0900 Subject: [PATCH 02/10] test(rsc): cover mixed directive reachability Co-authored-by: OpenCode --- .../plugin-rsc/e2e/use-cache-callable.test.ts | 102 ++++++++++-------- .../examples/use-cache-callable/README.md | 22 ++-- .../actions.ts | 0 .../client.tsx | 4 +- .../server.tsx | 6 +- .../state.ts | 0 .../actions.ts | 19 ++++ .../client.tsx | 36 +++++++ .../server.tsx | 15 +++ .../state.ts | 0 .../actions.ts | 0 .../client.tsx | 4 +- .../reset.ts | 0 .../server.tsx | 6 +- .../state.ts | 4 + .../actions.ts | 14 +++ .../client.tsx | 36 +++++++ .../reset.ts | 10 ++ .../server.tsx | 16 +++ .../state.ts | 4 + .../examples/use-cache-callable/src/root.tsx | 36 +++++-- 21 files changed, 260 insertions(+), 74 deletions(-) rename packages/plugin-rsc/examples/use-cache-callable/src/features/{use-cache-in-use-server => use-cache-in-use-server-from-client}/actions.ts (100%) rename packages/plugin-rsc/examples/use-cache-callable/src/features/{use-cache-in-use-server => use-cache-in-use-server-from-client}/client.tsx (87%) rename packages/plugin-rsc/examples/use-cache-callable/src/features/{use-cache-in-use-server => use-cache-in-use-server-from-client}/server.tsx (51%) rename packages/plugin-rsc/examples/use-cache-callable/src/features/{use-cache-in-use-server => use-cache-in-use-server-from-client}/state.ts (100%) create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/actions.ts create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/client.tsx create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/server.tsx rename packages/plugin-rsc/examples/use-cache-callable/src/features/{use-server-in-use-cache => use-cache-in-use-server-from-server}/state.ts (100%) rename packages/plugin-rsc/examples/use-cache-callable/src/features/{use-server-in-use-cache => use-server-in-use-cache-from-client}/actions.ts (100%) rename packages/plugin-rsc/examples/use-cache-callable/src/features/{use-server-in-use-cache => use-server-in-use-cache-from-client}/client.tsx (87%) rename packages/plugin-rsc/examples/use-cache-callable/src/features/{use-server-in-use-cache => use-server-in-use-cache-from-client}/reset.ts (100%) rename packages/plugin-rsc/examples/use-cache-callable/src/features/{use-server-in-use-cache => use-server-in-use-cache-from-client}/server.tsx (51%) create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/state.ts create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/actions.ts create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/client.tsx create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/reset.ts create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/server.tsx create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/state.ts diff --git a/packages/plugin-rsc/e2e/use-cache-callable.test.ts b/packages/plugin-rsc/e2e/use-cache-callable.test.ts index 1563e7c03..dd5c12d28 100644 --- a/packages/plugin-rsc/e2e/use-cache-callable.test.ts +++ b/packages/plugin-rsc/e2e/use-cache-callable.test.ts @@ -368,50 +368,64 @@ function defineTests(f: Fixture) { await expect(result).toHaveText('arguments: 0') }) - test('use cache in use server', async ({ page }) => { - using _errors = expectNoPageError(page) - await page.goto(f.url('/use-cache-in-use-server')) - await waitForHydration(page) - - const example = page.getByTestId('use-cache-in-use-server') - const defaultExecutions = example.getByTestId('default-executions') - const overrideExecutions = example.getByTestId('override-executions') - await example.getByRole('button', { name: 'Reset' }).click() - await expect(defaultExecutions).toHaveText('0') - await expect(overrideExecutions).toHaveText('0') - - // The file directive supplies ordinary server behavior by default. - await callAction(page, example, 'Call default action') - await callAction(page, example, 'Call default action') - await expect(defaultExecutions).toHaveText('2') - - // The inline cache wrapper runs once for the same argument. - await callAction(page, example, 'Call override action') - await callAction(page, example, 'Call override action') - await expect(overrideExecutions).toHaveText('1') - }) - - test('use server in use cache', async ({ page }) => { - using _errors = expectNoPageError(page) - await page.goto(f.url('/use-server-in-use-cache')) - await waitForHydration(page) - - const example = page.getByTestId('use-server-in-use-cache') - const defaultExecutions = example.getByTestId('default-executions') - const overrideExecutions = example.getByTestId('override-executions') - await example.getByRole('button', { name: 'Reset' }).click() - await expect(defaultExecutions).toHaveText('0') - await expect(overrideExecutions).toHaveText('0') - - await callAction(page, example, 'Call default action') - await callAction(page, example, 'Call default action') - await expect(defaultExecutions).toHaveText('1') - - // This characterizes the current transform order: the file-level cache - // wrapper is applied before the built-in transform sees "use server". - await callAction(page, example, 'Call override action') - await callAction(page, example, 'Call override action') - await expect(overrideExecutions).toHaveText('1') + for (const testCase of [ + { + route: 'use-cache-in-use-server-from-client', + defaultExecutions: 2, + overrideExecutions: 1, + }, + { + route: 'use-server-in-use-cache-from-client', + defaultExecutions: 1, + overrideExecutions: 1, + }, + { + route: 'use-server-in-use-cache-from-server', + defaultExecutions: 1, + overrideExecutions: 1, + }, + ]) { + test(testCase.route.replaceAll('-', ' '), async ({ page }) => { + using _errors = expectNoPageError(page) + await page.goto(f.url('/' + testCase.route)) + await waitForHydration(page) + + const example = page.getByTestId(testCase.route) + const defaultExecutions = example.getByTestId('default-executions') + const overrideExecutions = example.getByTestId('override-executions') + await example.getByRole('button', { name: 'Reset' }).click() + await expect(defaultExecutions).toHaveText('0') + await expect(overrideExecutions).toHaveText('0') + + await callAction(page, example, 'Call default action') + await callAction(page, example, 'Call default action') + await expect(defaultExecutions).toHaveText( + String(testCase.defaultExecutions), + ) + + // "use server" inside a cached module currently retains the file-level + // cache wrapper, while "use cache" inside a server module overrides it. + await callAction(page, example, 'Call override action') + await callAction(page, example, 'Call override action') + await expect(overrideExecutions).toHaveText( + String(testCase.overrideExecutions), + ) + }) + } + + test('use cache in use server from server', async ({ page }) => { + await page.goto(f.url('/use-cache-in-use-server-from-server')) + + // The source-facing exports currently reach Flight without server reference + // registration when this mixed module stays out of the client graph. + await expect(page.getByText('Caught an unexpected error')).toBeVisible() + if (f.mode === 'dev') { + await expect( + page.getByText( + /Functions cannot be passed directly to Client Components/, + ), + ).toBeVisible() + } }) test('protected captures', async ({ page }) => { diff --git a/packages/plugin-rsc/examples/use-cache-callable/README.md b/packages/plugin-rsc/examples/use-cache-callable/README.md index 53ce273a2..2779bb9ba 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/README.md +++ b/packages/plugin-rsc/examples/use-cache-callable/README.md @@ -46,16 +46,18 @@ Arguments are serialized with React's `encodeReply()` so values supported by the ## Examples -| Route | Demonstrates | -| ----------------------------------- | ----------------------------------------------------------------------------------------------------- | -| `/inline-directive` | An inline cached function captures a Server Component value and is passed to a Client Component form. | -| `/file-directive-from-server` | A module-level cached export is imported on the server and passed to a Client Component. | -| `/file-directive-from-client` | A Client Component imports a cached export through its generated proxy. | -| `/file-directive-extra-arguments` | A zero-parameter module export excludes React-supplied caller arguments. | -| `/inline-directive-extra-arguments` | A zero-parameter inline function uses transform metadata to exclude React-supplied caller arguments. | -| `/use-cache-in-use-server` | An inline cached export overrides the default behavior of a `"use server"` module. | -| `/use-server-in-use-cache` | Characterizes the current handling of an inline server export in a `"use cache"` module. | -| `/protected-captures` | Inline captures cross the client boundary encrypted while decoded values define cache identity. | +| Route | Demonstrates | +| -------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `/inline-directive` | An inline cached function captures a Server Component value and is passed to a Client Component form. | +| `/file-directive-from-server` | A module-level cached export is imported on the server and passed to a Client Component. | +| `/file-directive-from-client` | A Client Component imports a cached export through its generated proxy. | +| `/file-directive-extra-arguments` | A zero-parameter module export excludes React-supplied caller arguments. | +| `/inline-directive-extra-arguments` | A zero-parameter inline function uses transform metadata to exclude React-supplied caller arguments. | +| `/use-cache-in-use-server-from-client` | A Client Component imports an inline cached export from a `"use server"` module. | +| `/use-cache-in-use-server-from-server` | Characterizes the current serialization error for server-passed exports from the mixed module. | +| `/use-server-in-use-cache-from-client` | A Client Component imports an inline server export from a `"use cache"` module. | +| `/use-server-in-use-cache-from-server` | A Server Component passes an inline server export from a `"use cache"` module. | +| `/protected-captures` | Inline captures cross the client boundary encrypted while decoded values define cache identity. | Each route displays submission and execution counts. Every form submission calls the server reference, while the function body runs only on a cache miss. diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/actions.ts similarity index 100% rename from packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/actions.ts rename to packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/actions.ts diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/client.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/client.tsx similarity index 87% rename from packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/client.tsx rename to packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/client.tsx index d2ce893b3..3d36241d0 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/client.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/client.tsx @@ -2,12 +2,12 @@ import { defaultAction, overrideAction, resetAction } from './actions' -export function UseCacheInUseServerClient(props: { +export function UseCacheInUseServerFromClient(props: { defaultExecutions: number overrideExecutions: number }) { return ( -
+
diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/server.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/server.tsx similarity index 51% rename from packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/server.tsx rename to packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/server.tsx index b56584944..6ca011847 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/server.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/server.tsx @@ -1,9 +1,9 @@ -import { UseCacheInUseServerClient } from './client' +import { UseCacheInUseServerFromClient } from './client' import { state } from './state' -export function UseCacheInUseServer() { +export function UseCacheInUseServerFromClientServer() { return ( - diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/state.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/state.ts similarity index 100% rename from packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server/state.ts rename to packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/state.ts diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/actions.ts new file mode 100644 index 000000000..311ccf487 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/actions.ts @@ -0,0 +1,19 @@ +'use server' + +import { resetCache } from '../../framework/use-cache-runtime' +import { state } from './state' + +export async function defaultAction(_key: string) { + state.defaultExecutions++ +} + +export async function overrideAction(_key: string) { + 'use cache' + state.overrideExecutions++ +} + +export async function resetAction() { + resetCache() + state.defaultExecutions = 0 + state.overrideExecutions = 0 +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/client.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/client.tsx new file mode 100644 index 000000000..a7ce68968 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/client.tsx @@ -0,0 +1,36 @@ +'use client' + +type Action = (key: string) => Promise + +export function UseCacheInUseServerFromServer(props: { + defaultAction: Action + defaultExecutions: number + overrideAction: Action + overrideExecutions: number + resetAction: () => Promise +}) { + return ( +
+
+ +
+
+ +
+

+ Default executions:{' '} + + {props.defaultExecutions} + +
+ Override executions:{' '} + + {props.overrideExecutions} + +

+
+ +
+
+ ) +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/server.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/server.tsx new file mode 100644 index 000000000..64cfb1a8e --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/server.tsx @@ -0,0 +1,15 @@ +import { defaultAction, overrideAction, resetAction } from './actions' +import { UseCacheInUseServerFromServer } from './client' +import { state } from './state' + +export function UseCacheInUseServerFromServerServer() { + return ( + + ) +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/state.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/state.ts similarity index 100% rename from packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/state.ts rename to packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/state.ts diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/actions.ts similarity index 100% rename from packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/actions.ts rename to packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/actions.ts diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/client.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/client.tsx similarity index 87% rename from packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/client.tsx rename to packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/client.tsx index f8c4547ba..8c6107c29 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/client.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/client.tsx @@ -3,12 +3,12 @@ import { defaultAction, overrideAction } from './actions' import { resetAction } from './reset' -export function UseServerInUseCacheClient(props: { +export function UseServerInUseCacheFromClient(props: { defaultExecutions: number overrideExecutions: number }) { return ( -
+
diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/reset.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/reset.ts similarity index 100% rename from packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/reset.ts rename to packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/reset.ts diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/server.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/server.tsx similarity index 51% rename from packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/server.tsx rename to packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/server.tsx index 5b36f649c..c3dced99d 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache/server.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/server.tsx @@ -1,9 +1,9 @@ -import { UseServerInUseCacheClient } from './client' +import { UseServerInUseCacheFromClient } from './client' import { state } from './state' -export function UseServerInUseCache() { +export function UseServerInUseCacheFromClientServer() { return ( - diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/state.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/state.ts new file mode 100644 index 000000000..8cbac1600 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/state.ts @@ -0,0 +1,4 @@ +export const state = { + defaultExecutions: 0, + overrideExecutions: 0, +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/actions.ts new file mode 100644 index 000000000..67028ecd8 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/actions.ts @@ -0,0 +1,14 @@ +'use cache' + +import { state } from './state' + +// Variable exports stay assignable when the file-level cache wrapper runs +// before the inline server transform. +export const defaultAction = async (_key: string) => { + state.defaultExecutions++ +} + +export const overrideAction = async (_key: string) => { + 'use server' + state.overrideExecutions++ +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/client.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/client.tsx new file mode 100644 index 000000000..5c1ed50d1 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/client.tsx @@ -0,0 +1,36 @@ +'use client' + +type Action = (key: string) => Promise + +export function UseServerInUseCacheFromServer(props: { + defaultAction: Action + defaultExecutions: number + overrideAction: Action + overrideExecutions: number + resetAction: () => Promise +}) { + return ( +
+
+ +
+
+ +
+

+ Default executions:{' '} + + {props.defaultExecutions} + +
+ Override executions:{' '} + + {props.overrideExecutions} + +

+
+ +
+
+ ) +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/reset.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/reset.ts new file mode 100644 index 000000000..16911e798 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/reset.ts @@ -0,0 +1,10 @@ +'use server' + +import { resetCache } from '../../framework/use-cache-runtime' +import { state } from './state' + +export async function resetAction() { + resetCache() + state.defaultExecutions = 0 + state.overrideExecutions = 0 +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/server.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/server.tsx new file mode 100644 index 000000000..b5aa8011e --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/server.tsx @@ -0,0 +1,16 @@ +import { defaultAction, overrideAction } from './actions' +import { UseServerInUseCacheFromServer } from './client' +import { resetAction } from './reset' +import { state } from './state' + +export function UseServerInUseCacheFromServerServer() { + return ( + + ) +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/state.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/state.ts new file mode 100644 index 000000000..8cbac1600 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/state.ts @@ -0,0 +1,4 @@ +export const state = { + defaultExecutions: 0, + overrideExecutions: 0, +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx index 04d75eed4..69d879389 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx @@ -4,8 +4,10 @@ import { FileDirectiveFromServer } from './features/file-directive-from-server/s import { InlineDirectiveExtraArgumentsServer } from './features/inline-directive-extra-arguments/server' import { InlineDirective } from './features/inline-directive/server' import { ProtectedCaptures } from './features/protected-captures/server' -import { UseCacheInUseServer } from './features/use-cache-in-use-server/server' -import { UseServerInUseCache } from './features/use-server-in-use-cache/server' +import { UseCacheInUseServerFromClientServer } from './features/use-cache-in-use-server-from-client/server' +import { UseCacheInUseServerFromServerServer } from './features/use-cache-in-use-server-from-server/server' +import { UseServerInUseCacheFromClientServer } from './features/use-server-in-use-cache-from-client/server' +import { UseServerInUseCacheFromServerServer } from './features/use-server-in-use-cache-from-server/server' const routes = [ { @@ -44,18 +46,32 @@ const routes = [ Component: InlineDirectiveExtraArgumentsServer, }, { - path: '/use-cache-in-use-server', - title: 'Use cache in use server', + path: '/use-cache-in-use-server-from-client', + title: 'Use cache in use server from client', description: - 'An inline cached export overrides the default behavior of a server function module.', - Component: UseCacheInUseServer, + 'A Client Component imports an inline cached export from a server function module.', + Component: UseCacheInUseServerFromClientServer, }, { - path: '/use-server-in-use-cache', - title: 'Use server in use cache', + path: '/use-cache-in-use-server-from-server', + title: 'Use cache in use server from server', description: - 'An inline server export currently retains the default behavior of a cached function module.', - Component: UseServerInUseCache, + 'A Server Component passes exports from the mixed module, currently exposing missing server reference registration.', + Component: UseCacheInUseServerFromServerServer, + }, + { + path: '/use-server-in-use-cache-from-client', + title: 'Use server in use cache from client', + description: + 'A Client Component imports an inline server export from a cached function module.', + Component: UseServerInUseCacheFromClientServer, + }, + { + path: '/use-server-in-use-cache-from-server', + title: 'Use server in use cache from server', + description: + 'A Server Component passes an inline server export from a cached function module to a Client Component.', + Component: UseServerInUseCacheFromServerServer, }, { path: '/protected-captures', From 9c00c7b876affc0f8752784c9096bdc9df604b5d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:31:33 +0900 Subject: [PATCH 03/10] test(rsc): exclude server overrides from cache wrapping Co-authored-by: OpenCode --- .../plugin-rsc/e2e/use-cache-callable.test.ts | 7 +++--- .../examples/use-cache-callable/README.md | 4 ++-- .../callable-cache-plugin.ts | 22 +++++++++++++++++++ .../examples/use-cache-callable/src/root.tsx | 4 ++-- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/packages/plugin-rsc/e2e/use-cache-callable.test.ts b/packages/plugin-rsc/e2e/use-cache-callable.test.ts index dd5c12d28..a49861a10 100644 --- a/packages/plugin-rsc/e2e/use-cache-callable.test.ts +++ b/packages/plugin-rsc/e2e/use-cache-callable.test.ts @@ -377,12 +377,12 @@ function defineTests(f: Fixture) { { route: 'use-server-in-use-cache-from-client', defaultExecutions: 1, - overrideExecutions: 1, + overrideExecutions: 2, }, { route: 'use-server-in-use-cache-from-server', defaultExecutions: 1, - overrideExecutions: 1, + overrideExecutions: 2, }, ]) { test(testCase.route.replaceAll('-', ' '), async ({ page }) => { @@ -403,8 +403,7 @@ function defineTests(f: Fixture) { String(testCase.defaultExecutions), ) - // "use server" inside a cached module currently retains the file-level - // cache wrapper, while "use cache" inside a server module overrides it. + // An inline directive overrides the behavior inherited from the file. await callAction(page, example, 'Call override action') await callAction(page, example, 'Call override action') await expect(overrideExecutions).toHaveText( diff --git a/packages/plugin-rsc/examples/use-cache-callable/README.md b/packages/plugin-rsc/examples/use-cache-callable/README.md index 2779bb9ba..65d2752a5 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/README.md +++ b/packages/plugin-rsc/examples/use-cache-callable/README.md @@ -55,8 +55,8 @@ Arguments are serialized with React's `encodeReply()` so values supported by the | `/inline-directive-extra-arguments` | A zero-parameter inline function uses transform metadata to exclude React-supplied caller arguments. | | `/use-cache-in-use-server-from-client` | A Client Component imports an inline cached export from a `"use server"` module. | | `/use-cache-in-use-server-from-server` | Characterizes the current serialization error for server-passed exports from the mixed module. | -| `/use-server-in-use-cache-from-client` | A Client Component imports an inline server export from a `"use cache"` module. | -| `/use-server-in-use-cache-from-server` | A Server Component passes an inline server export from a `"use cache"` module. | +| `/use-server-in-use-cache-from-client` | A Client Component imports an uncached inline server export from a `"use cache"` module. | +| `/use-server-in-use-cache-from-server` | A Server Component passes an uncached inline server export from a `"use cache"` module. | | `/protected-captures` | Inline captures cross the client boundary encrypted while decoded values define cache identity. | Each route displays submission and execution counts. Every form submission calls the server reference, while the function body runs only on a cache miss. diff --git a/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts b/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts index 464e048cf..73ac8c270 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts @@ -49,6 +49,7 @@ export function callableCachePlugin(): Plugin { // and viewport exports. // https://github.com/vercel/next.js/blob/aae4179ac628e55483b62cd023a7e1827dcef122/crates/next-custom-transforms/src/transforms/server_actions.rs#L1914-L1919 filter: (_name, meta) => + !hasFunctionDirective(meta, 'use server') && meta.valueNode?.type !== 'ObjectExpression' && meta.valueNode?.type !== 'ArrayExpression', rejectNonAsyncFunction: true, @@ -122,6 +123,27 @@ export function callableCachePlugin(): Plugin { } } +function hasFunctionDirective( + meta: Pick, + directive: string, +): boolean { + const node = meta.valueNode + if ( + (node?.type !== 'FunctionDeclaration' && + node?.type !== 'FunctionExpression' && + node?.type !== 'ArrowFunctionExpression') || + node.body.type !== 'BlockStatement' + ) { + return false + } + return node.body.body.some( + (statement) => + statement.type === 'ExpressionStatement' && + 'directive' in statement && + statement.directive === directive, + ) +} + function getCacheWrapperOptions( meta: Pick, ): CacheWrapperOptions { diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx index 69d879389..4c3c74650 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx @@ -63,14 +63,14 @@ const routes = [ path: '/use-server-in-use-cache-from-client', title: 'Use server in use cache from client', description: - 'A Client Component imports an inline server export from a cached function module.', + 'A Client Component imports an uncached inline server export from a cached function module.', Component: UseServerInUseCacheFromClientServer, }, { path: '/use-server-in-use-cache-from-server', title: 'Use server in use cache from server', description: - 'A Server Component passes an inline server export from a cached function module to a Client Component.', + 'A Server Component passes an uncached inline server export from a cached function module to a Client Component.', Component: UseServerInUseCacheFromServerServer, }, { From c705a17522875fb0ff59606ea9b2d195b0d540c7 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:35:52 +0900 Subject: [PATCH 04/10] test(rsc): isolate mixed directive actions Co-authored-by: OpenCode --- .../use-cache-in-use-server-from-client/actions.ts | 7 ------- .../use-cache-in-use-server-from-client/client.tsx | 3 ++- .../use-cache-in-use-server-from-client/reset.ts | 10 ++++++++++ .../use-cache-in-use-server-from-server/actions.ts | 7 ------- .../use-cache-in-use-server-from-server/reset.ts | 10 ++++++++++ .../use-cache-in-use-server-from-server/server.tsx | 3 ++- 6 files changed, 24 insertions(+), 16 deletions(-) create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/reset.ts create mode 100644 packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/reset.ts diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/actions.ts index 311ccf487..5338933ac 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/actions.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/actions.ts @@ -1,6 +1,5 @@ 'use server' -import { resetCache } from '../../framework/use-cache-runtime' import { state } from './state' export async function defaultAction(_key: string) { @@ -11,9 +10,3 @@ export async function overrideAction(_key: string) { 'use cache' state.overrideExecutions++ } - -export async function resetAction() { - resetCache() - state.defaultExecutions = 0 - state.overrideExecutions = 0 -} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/client.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/client.tsx index 3d36241d0..7b402cb01 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/client.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/client.tsx @@ -1,6 +1,7 @@ 'use client' -import { defaultAction, overrideAction, resetAction } from './actions' +import { defaultAction, overrideAction } from './actions' +import { resetAction } from './reset' export function UseCacheInUseServerFromClient(props: { defaultExecutions: number diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/reset.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/reset.ts new file mode 100644 index 000000000..16911e798 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/reset.ts @@ -0,0 +1,10 @@ +'use server' + +import { resetCache } from '../../framework/use-cache-runtime' +import { state } from './state' + +export async function resetAction() { + resetCache() + state.defaultExecutions = 0 + state.overrideExecutions = 0 +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/actions.ts index 311ccf487..5338933ac 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/actions.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/actions.ts @@ -1,6 +1,5 @@ 'use server' -import { resetCache } from '../../framework/use-cache-runtime' import { state } from './state' export async function defaultAction(_key: string) { @@ -11,9 +10,3 @@ export async function overrideAction(_key: string) { 'use cache' state.overrideExecutions++ } - -export async function resetAction() { - resetCache() - state.defaultExecutions = 0 - state.overrideExecutions = 0 -} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/reset.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/reset.ts new file mode 100644 index 000000000..16911e798 --- /dev/null +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/reset.ts @@ -0,0 +1,10 @@ +'use server' + +import { resetCache } from '../../framework/use-cache-runtime' +import { state } from './state' + +export async function resetAction() { + resetCache() + state.defaultExecutions = 0 + state.overrideExecutions = 0 +} diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/server.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/server.tsx index 64cfb1a8e..d72130552 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/server.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/server.tsx @@ -1,5 +1,6 @@ -import { defaultAction, overrideAction, resetAction } from './actions' +import { defaultAction, overrideAction } from './actions' import { UseCacheInUseServerFromServer } from './client' +import { resetAction } from './reset' import { state } from './state' export function UseCacheInUseServerFromServerServer() { From ebc7982f3d18c58c80acea4d57bb74da182fd80a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:44:26 +0900 Subject: [PATCH 05/10] feat(rsc): compose cache and server reference claims Co-authored-by: OpenCode --- .../plugin-rsc/e2e/use-cache-callable.test.ts | 20 +++++-------------- .../examples/use-cache-callable/README.md | 2 +- .../callable-cache-plugin.ts | 5 ++++- .../examples/use-cache-callable/src/root.tsx | 2 +- .../src/plugins/server-reference.ts | 11 +++++----- 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/packages/plugin-rsc/e2e/use-cache-callable.test.ts b/packages/plugin-rsc/e2e/use-cache-callable.test.ts index a49861a10..936bf609a 100644 --- a/packages/plugin-rsc/e2e/use-cache-callable.test.ts +++ b/packages/plugin-rsc/e2e/use-cache-callable.test.ts @@ -374,6 +374,11 @@ function defineTests(f: Fixture) { defaultExecutions: 2, overrideExecutions: 1, }, + { + route: 'use-cache-in-use-server-from-server', + defaultExecutions: 2, + overrideExecutions: 1, + }, { route: 'use-server-in-use-cache-from-client', defaultExecutions: 1, @@ -412,21 +417,6 @@ function defineTests(f: Fixture) { }) } - test('use cache in use server from server', async ({ page }) => { - await page.goto(f.url('/use-cache-in-use-server-from-server')) - - // The source-facing exports currently reach Flight without server reference - // registration when this mixed module stays out of the client graph. - await expect(page.getByText('Caught an unexpected error')).toBeVisible() - if (f.mode === 'dev') { - await expect( - page.getByText( - /Functions cannot be passed directly to Client Components/, - ), - ).toBeVisible() - } - }) - test('protected captures', async ({ page }) => { // verify captured value is encoded and thus doesn't appear in raw response const rscResponse = await page.request.get( diff --git a/packages/plugin-rsc/examples/use-cache-callable/README.md b/packages/plugin-rsc/examples/use-cache-callable/README.md index 65d2752a5..6800257d8 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/README.md +++ b/packages/plugin-rsc/examples/use-cache-callable/README.md @@ -54,7 +54,7 @@ Arguments are serialized with React's `encodeReply()` so values supported by the | `/file-directive-extra-arguments` | A zero-parameter module export excludes React-supplied caller arguments. | | `/inline-directive-extra-arguments` | A zero-parameter inline function uses transform metadata to exclude React-supplied caller arguments. | | `/use-cache-in-use-server-from-client` | A Client Component imports an inline cached export from a `"use server"` module. | -| `/use-cache-in-use-server-from-server` | Characterizes the current serialization error for server-passed exports from the mixed module. | +| `/use-cache-in-use-server-from-server` | A Server Component passes an inline cached export from a `"use server"` module. | | `/use-server-in-use-cache-from-client` | A Client Component imports an uncached inline server export from a `"use cache"` module. | | `/use-server-in-use-cache-from-server` | A Server Component passes an uncached inline server export from a `"use cache"` module. | | `/protected-captures` | Inline captures cross the client boundary encrypted while decoded values define cache identity. | diff --git a/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts b/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts index 73ac8c270..8bf611322 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts @@ -74,7 +74,10 @@ export function callableCachePlugin(): Plugin { ...reference, exportNames: 'names' in result ? result.names : result.exportNames, }) - result.output.prepend( + const importPosition = + ast.body.find((node) => !('directive' in node))?.start ?? code.length + result.output.prependLeft( + importPosition, `import $$cacheWrapper, { encryptCacheCaptures as $$encryptCacheCaptures } from "/src/framework/use-cache-runtime";\n` + `import * as $$CacheReactServer from "@vitejs/plugin-rsc/react/rsc/server";\n`, ) diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx index 4c3c74650..a12af492c 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/root.tsx @@ -56,7 +56,7 @@ const routes = [ path: '/use-cache-in-use-server-from-server', title: 'Use cache in use server from server', description: - 'A Server Component passes exports from the mixed module, currently exposing missing server reference registration.', + 'A Server Component passes an inline cached export from a server function module to a Client Component.', Component: UseCacheInUseServerFromServerServer, }, { diff --git a/packages/plugin-rsc/src/plugins/server-reference.ts b/packages/plugin-rsc/src/plugins/server-reference.ts index e428246b4..486ff7d26 100644 --- a/packages/plugin-rsc/src/plugins/server-reference.ts +++ b/packages/plugin-rsc/src/plugins/server-reference.ts @@ -110,13 +110,14 @@ function aggregateClaims( ) } for (const name of claim.exportNames) { + // An inline "use cache" transform followed by a file-level "use server" + // transform can intentionally claim the same generated export. The + // identity checks above ensure they contribute to one manifest entry, so + // only the export name needs to be retained. const existingOwner = exportOwners.get(name) - if (existingOwner && existingOwner !== claimOwner) { - throw new Error( - `[vite-rsc] server reference '${claim.referenceKey}#${name}' is claimed by both '${existingOwner}' and '${claimOwner}'`, - ) + if (!existingOwner) { + exportOwners.set(name, claimOwner) } - exportOwners.set(name, claimOwner) } } assert(aggregate) From 18bb6f23ad45de526a9267569fcaea76793e8f64 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:47:17 +0900 Subject: [PATCH 06/10] docs(rsc): explain mixed directive handling Co-authored-by: OpenCode --- .../examples/use-cache-callable/callable-cache-plugin.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts b/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts index 8bf611322..53fbd762f 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts @@ -49,6 +49,8 @@ export function callableCachePlugin(): Plugin { // and viewport exports. // https://github.com/vercel/next.js/blob/aae4179ac628e55483b62cd023a7e1827dcef122/crates/next-custom-transforms/src/transforms/server_actions.rs#L1914-L1919 filter: (_name, meta) => + // Inline "use server" overrides the file-level cache role and + // is left for the built-in transform. !hasFunctionDirective(meta, 'use server') && meta.valueNode?.type !== 'ObjectExpression' && meta.valueNode?.type !== 'ArrayExpression', @@ -74,6 +76,8 @@ export function callableCachePlugin(): Plugin { ...reference, exportNames: 'names' in result ? result.names : result.exportNames, }) + // Preserve leading directives so a later transform can still recognize + // a file-level role such as "use server". const importPosition = ast.body.find((node) => !('directive' in node))?.start ?? code.length result.output.prependLeft( From 539fed85b22fcd311fc4d18ab53c9873f28b11f5 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:52:00 +0900 Subject: [PATCH 07/10] test(rsc): simplify mixed directive calls Co-authored-by: OpenCode --- .../actions.ts | 4 ++-- .../client.tsx | 12 +++--------- .../actions.ts | 4 ++-- .../client.tsx | 16 ++++++---------- .../actions.ts | 4 ++-- .../client.tsx | 12 +++--------- .../actions.ts | 4 ++-- .../client.tsx | 16 ++++++---------- 8 files changed, 26 insertions(+), 46 deletions(-) diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/actions.ts index 5338933ac..ad3847b4e 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/actions.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/actions.ts @@ -2,11 +2,11 @@ import { state } from './state' -export async function defaultAction(_key: string) { +export async function defaultAction() { state.defaultExecutions++ } -export async function overrideAction(_key: string) { +export async function overrideAction() { 'use cache' state.overrideExecutions++ } diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/client.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/client.tsx index 7b402cb01..687c2a8c5 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/client.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-client/client.tsx @@ -9,12 +9,8 @@ export function UseCacheInUseServerFromClient(props: { }) { return (
-
- -
-
- -
+ +

Default executions:{' '} @@ -26,9 +22,7 @@ export function UseCacheInUseServerFromClient(props: { {props.overrideExecutions}

-
- -
+
) } diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/actions.ts index 5338933ac..ad3847b4e 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/actions.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/actions.ts @@ -2,11 +2,11 @@ import { state } from './state' -export async function defaultAction(_key: string) { +export async function defaultAction() { state.defaultExecutions++ } -export async function overrideAction(_key: string) { +export async function overrideAction() { 'use cache' state.overrideExecutions++ } diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/client.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/client.tsx index a7ce68968..b52dc6d16 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/client.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-cache-in-use-server-from-server/client.tsx @@ -1,6 +1,6 @@ 'use client' -type Action = (key: string) => Promise +type Action = () => Promise export function UseCacheInUseServerFromServer(props: { defaultAction: Action @@ -11,12 +11,10 @@ export function UseCacheInUseServerFromServer(props: { }) { return (
-
- -
-
- -
+ +

Default executions:{' '} @@ -28,9 +26,7 @@ export function UseCacheInUseServerFromServer(props: { {props.overrideExecutions}

-
- -
+
) } diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/actions.ts index 67028ecd8..d987efc76 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/actions.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/actions.ts @@ -4,11 +4,11 @@ import { state } from './state' // Variable exports stay assignable when the file-level cache wrapper runs // before the inline server transform. -export const defaultAction = async (_key: string) => { +export const defaultAction = async () => { state.defaultExecutions++ } -export const overrideAction = async (_key: string) => { +export const overrideAction = async () => { 'use server' state.overrideExecutions++ } diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/client.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/client.tsx index 8c6107c29..2030bd860 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/client.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/client.tsx @@ -9,12 +9,8 @@ export function UseServerInUseCacheFromClient(props: { }) { return (
-
- -
-
- -
+ +

Default executions:{' '} @@ -26,9 +22,7 @@ export function UseServerInUseCacheFromClient(props: { {props.overrideExecutions}

-
- -
+
) } diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/actions.ts index 67028ecd8..d987efc76 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/actions.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/actions.ts @@ -4,11 +4,11 @@ import { state } from './state' // Variable exports stay assignable when the file-level cache wrapper runs // before the inline server transform. -export const defaultAction = async (_key: string) => { +export const defaultAction = async () => { state.defaultExecutions++ } -export const overrideAction = async (_key: string) => { +export const overrideAction = async () => { 'use server' state.overrideExecutions++ } diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/client.tsx b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/client.tsx index 5c1ed50d1..82f9248fb 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/client.tsx +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/client.tsx @@ -1,6 +1,6 @@ 'use client' -type Action = (key: string) => Promise +type Action = () => Promise export function UseServerInUseCacheFromServer(props: { defaultAction: Action @@ -11,12 +11,10 @@ export function UseServerInUseCacheFromServer(props: { }) { return (
-
- -
-
- -
+ +

Default executions:{' '} @@ -28,9 +26,7 @@ export function UseServerInUseCacheFromServer(props: { {props.overrideExecutions}

-
- -
+
) } From d0eb1a537514dd0d7a2de63ce0967aa42a53bed8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:53:10 +0900 Subject: [PATCH 08/10] test(rsc): use declarations in mixed cache modules Co-authored-by: OpenCode --- .../features/use-server-in-use-cache-from-client/actions.ts | 6 ++---- .../features/use-server-in-use-cache-from-server/actions.ts | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/actions.ts index d987efc76..86e21f1b8 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/actions.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-client/actions.ts @@ -2,13 +2,11 @@ import { state } from './state' -// Variable exports stay assignable when the file-level cache wrapper runs -// before the inline server transform. -export const defaultAction = async () => { +export async function defaultAction() { state.defaultExecutions++ } -export const overrideAction = async () => { +export async function overrideAction() { 'use server' state.overrideExecutions++ } diff --git a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/actions.ts b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/actions.ts index d987efc76..86e21f1b8 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/actions.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/src/features/use-server-in-use-cache-from-server/actions.ts @@ -2,13 +2,11 @@ import { state } from './state' -// Variable exports stay assignable when the file-level cache wrapper runs -// before the inline server transform. -export const defaultAction = async () => { +export async function defaultAction() { state.defaultExecutions++ } -export const overrideAction = async () => { +export async function overrideAction() { 'use server' state.overrideExecutions++ } From 3ec9403aeb3bced0f3bb35be95025554784a72de Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:58:36 +0900 Subject: [PATCH 09/10] test(rsc): snapshot mixed directive output Co-authored-by: OpenCode --- .../use-cache-in-use-server.js | 7 ++ .../use-cache-in-use-server.js.snap.md | 78 ++++++++++++ .../use-server-in-use-cache.js | 7 ++ .../use-server-in-use-cache.js.snap.md | 68 +++++++++++ .../src/transforms/mixed-directives.test.ts | 114 ++++++++++++++++++ 5 files changed, 274 insertions(+) create mode 100644 packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-cache-in-use-server.js create mode 100644 packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-cache-in-use-server.js.snap.md create mode 100644 packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-server-in-use-cache.js create mode 100644 packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-server-in-use-cache.js.snap.md create mode 100644 packages/plugin-rsc/src/transforms/mixed-directives.test.ts diff --git a/packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-cache-in-use-server.js b/packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-cache-in-use-server.js new file mode 100644 index 000000000..1a375419c --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-cache-in-use-server.js @@ -0,0 +1,7 @@ +'use server' + +export async function defaultAction() {} + +export async function overrideAction() { + 'use cache' +} diff --git a/packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-cache-in-use-server.js.snap.md b/packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-cache-in-use-server.js.snap.md new file mode 100644 index 000000000..a092a970b --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-cache-in-use-server.js.snap.md @@ -0,0 +1,78 @@ +## Input + +```js +'use server' + +export async function defaultAction() {} + +export async function overrideAction() { + 'use cache' +} +``` + +## framework cache RSC transform + +**Status:** transformed + +**References:** $$hoist_0_overrideAction + +```js +'use server' + +import { cache as $cache, register as $registerCache } from "cache-runtime"; +export const $$hoist_0_overrideAction = /* #__PURE__ */ $registerCache($cache($$hoist_0_overrideAction$$impl), "$$hoist_0_overrideAction"); +export async function defaultAction() {} + +export const overrideAction = $$hoist_0_overrideAction; + +;async function $$hoist_0_overrideAction$$impl() { + 'use cache' +}; +/* #__PURE__ */ Object.defineProperty($$hoist_0_overrideAction$$impl, "name", { value: "overrideAction" }); +``` + +## final RSC transform + +**Status:** transformed + +**References:** $$hoist_0_overrideAction, defaultAction, overrideAction + +```js +'use server' + +import { cache as $cache, register as $registerCache } from "cache-runtime"; +const $$hoist_0_overrideAction = /* #__PURE__ */ $registerCache($cache($$hoist_0_overrideAction$$impl), "$$hoist_0_overrideAction"); +async function defaultAction() {} + +const overrideAction = $$hoist_0_overrideAction; + +;async function $$hoist_0_overrideAction$$impl() { + 'use cache' +}; +/* #__PURE__ */ Object.defineProperty($$hoist_0_overrideAction$$impl, "name", { value: "overrideAction" }); + +$registerServer($$hoist_0_overrideAction, "$$hoist_0_overrideAction"); +export { $$hoist_0_overrideAction }; + +$registerServer(defaultAction, "defaultAction"); +export { defaultAction }; + +$registerServer(overrideAction, "overrideAction"); +export { overrideAction }; +``` + +## browser and SSR proxy transform + +**Status:** transformed + +**References:** defaultAction, overrideAction + +```js + + +export const defaultAction = /* #__PURE__ */ $serverProxy("defaultAction"); + + +export const overrideAction = /* #__PURE__ */ $serverProxy("overrideAction"); + +``` diff --git a/packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-server-in-use-cache.js b/packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-server-in-use-cache.js new file mode 100644 index 000000000..b4303db27 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-server-in-use-cache.js @@ -0,0 +1,7 @@ +'use cache' + +export async function defaultAction() {} + +export async function overrideAction() { + 'use server' +} diff --git a/packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-server-in-use-cache.js.snap.md b/packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-server-in-use-cache.js.snap.md new file mode 100644 index 000000000..359544053 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/mixed-directives/use-server-in-use-cache.js.snap.md @@ -0,0 +1,68 @@ +## Input + +```js +'use cache' + +export async function defaultAction() {} + +export async function overrideAction() { + 'use server' +} +``` + +## framework cache RSC transform + +**Status:** transformed + +**References:** defaultAction + +```js +'use cache' + +import { cache as $cache, register as $registerCache } from "cache-runtime"; +async function defaultAction() {} + +export async function overrideAction() { + 'use server' +} +defaultAction = /* #__PURE__ */ $registerCache($cache(defaultAction), "defaultAction"); +export { defaultAction }; +``` + +## final RSC transform + +**Status:** transformed + +**References:** $$hoist_0_overrideAction + +```js +'use cache' + +import { cache as $cache, register as $registerCache } from "cache-runtime"; +async function defaultAction() {} + +export const overrideAction = /* #__PURE__ */ $registerServer($$hoist_0_overrideAction, "$$hoist_0_overrideAction"); +defaultAction = /* #__PURE__ */ $registerCache($cache(defaultAction), "defaultAction"); +export { defaultAction }; + +;export async function $$hoist_0_overrideAction() { + 'use server' +}; +/* #__PURE__ */ Object.defineProperty($$hoist_0_overrideAction, "name", { value: "overrideAction" }); +``` + +## browser and SSR proxy transform + +**Status:** transformed + +**References:** defaultAction, overrideAction + +```js + + +export const defaultAction = /* #__PURE__ */ $cacheProxy("defaultAction"); + + +export const overrideAction = /* #__PURE__ */ $cacheProxy("overrideAction"); + +``` diff --git a/packages/plugin-rsc/src/transforms/mixed-directives.test.ts b/packages/plugin-rsc/src/transforms/mixed-directives.test.ts new file mode 100644 index 000000000..6e26d2718 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/mixed-directives.test.ts @@ -0,0 +1,114 @@ +import path from 'node:path' +import { parseAstAsync } from 'vite' +import { expect, test } from 'vitest' +import { transformHoistInlineDirective } from './hoist' +import type { ModuleExportMeta } from './module-export-scan' +import { transformDirectiveProxyExport } from './proxy-export' +import { transformServerActionServer } from './server-action' +import { formatTransformMarkdownFixture } from './test-utils' +import { hasDirective } from './utils' +import { transformWrapExport } from './wrap-export' + +const fixtures = import.meta.glob( + ['./fixtures/mixed-directives/*.js', '!**/*.snap.*'], + { query: 'raw' }, +) + +for (const [file, load] of Object.entries(fixtures)) { + test(path.basename(file), async () => { + const input = ((await load()) as any).default as string + const ast = await parseAstAsync(input) + const cacheResult = hasDirective(ast.body, 'use cache') + ? transformWrapExport(input, ast, { + runtime: cacheRuntime, + filter: (_name, meta) => !hasFunctionDirective(meta, 'use server'), + }) + : transformHoistInlineDirective(input, ast, { + directive: 'use cache', + hoistRuntime: true, + runtime: cacheRuntime, + }) + + const importPosition = + ast.body.find((node) => !('directive' in node))?.start ?? input.length + cacheResult.output.prependLeft( + importPosition, + `import { cache as $cache, register as $registerCache } from "cache-runtime";\n`, + ) + const cacheCode = cacheResult.output.toString() + const cacheReferences = + 'names' in cacheResult ? cacheResult.names : cacheResult.exportNames + + const rscAst = await parseAstAsync(cacheCode) + const rscResult = transformServerActionServer(cacheCode, rscAst, { + runtime: (value, name) => + `$registerServer(${value}, ${JSON.stringify(name)})`, + }) + + let proxyAst = ast + let proxyCode = input + let proxyResult = transformDirectiveProxyExport(proxyAst, { + code: proxyCode, + directive: 'use cache', + runtime: (name) => `$cacheProxy(${JSON.stringify(name)})`, + }) + if (proxyResult) { + proxyCode = proxyResult.output.toString() + proxyAst = await parseAstAsync(proxyCode) + } + proxyResult ??= transformDirectiveProxyExport(proxyAst, { + code: proxyCode, + directive: 'use server', + runtime: (name) => `$serverProxy(${JSON.stringify(name)})`, + }) + + await parseAstAsync(rscResult.output.toString()) + if (proxyResult) { + await parseAstAsync(proxyResult.output.toString()) + } + await expect( + formatTransformMarkdownFixture(input, [ + { + name: 'framework cache RSC transform', + output: cacheResult.output, + references: cacheReferences, + }, + { + name: 'final RSC transform', + output: rscResult.output, + references: rscResult.referenceNames, + }, + { + name: 'browser and SSR proxy transform', + output: proxyResult?.output, + references: proxyResult?.exportNames, + }, + ]), + ).toMatchFileSnapshot(file + '.snap.md') + }) +} + +function cacheRuntime(value: string, name: string): string { + return `$registerCache($cache(${value}), ${JSON.stringify(name)})` +} + +function hasFunctionDirective( + meta: Pick, + directive: string, +): boolean { + const node = meta.valueNode + if ( + (node?.type !== 'FunctionDeclaration' && + node?.type !== 'FunctionExpression' && + node?.type !== 'ArrowFunctionExpression') || + node.body.type !== 'BlockStatement' + ) { + return false + } + return node.body.body.some( + (statement) => + statement.type === 'ExpressionStatement' && + 'directive' in statement && + statement.directive === directive, + ) +} From 6b457f702e24afa0b2f49a5060ece58c6fde935b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:07:03 +0900 Subject: [PATCH 10/10] test(rsc): clarify mixed directive fixtures Co-authored-by: OpenCode --- .../src/transforms/mixed-directives.test.ts | 120 ++++++++++-------- 1 file changed, 69 insertions(+), 51 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/mixed-directives.test.ts b/packages/plugin-rsc/src/transforms/mixed-directives.test.ts index 6e26d2718..ba9bdf103 100644 --- a/packages/plugin-rsc/src/transforms/mixed-directives.test.ts +++ b/packages/plugin-rsc/src/transforms/mixed-directives.test.ts @@ -1,4 +1,5 @@ import path from 'node:path' +import type MagicString from 'magic-string' import { parseAstAsync } from 'vite' import { expect, test } from 'vitest' import { transformHoistInlineDirective } from './hoist' @@ -17,77 +18,94 @@ const fixtures = import.meta.glob( for (const [file, load] of Object.entries(fixtures)) { test(path.basename(file), async () => { const input = ((await load()) as any).default as string - const ast = await parseAstAsync(input) - const cacheResult = hasDirective(ast.body, 'use cache') - ? transformWrapExport(input, ast, { - runtime: cacheRuntime, - filter: (_name, meta) => !hasFunctionDirective(meta, 'use server'), - }) - : transformHoistInlineDirective(input, ast, { - directive: 'use cache', - hoistRuntime: true, - runtime: cacheRuntime, - }) - - const importPosition = - ast.body.find((node) => !('directive' in node))?.start ?? input.length - cacheResult.output.prependLeft( - importPosition, - `import { cache as $cache, register as $registerCache } from "cache-runtime";\n`, - ) - const cacheCode = cacheResult.output.toString() - const cacheReferences = - 'names' in cacheResult ? cacheResult.names : cacheResult.exportNames - - const rscAst = await parseAstAsync(cacheCode) - const rscResult = transformServerActionServer(cacheCode, rscAst, { - runtime: (value, name) => - `$registerServer(${value}, ${JSON.stringify(name)})`, - }) - - let proxyAst = ast - let proxyCode = input - let proxyResult = transformDirectiveProxyExport(proxyAst, { - code: proxyCode, - directive: 'use cache', - runtime: (name) => `$cacheProxy(${JSON.stringify(name)})`, - }) - if (proxyResult) { - proxyCode = proxyResult.output.toString() - proxyAst = await parseAstAsync(proxyCode) - } - proxyResult ??= transformDirectiveProxyExport(proxyAst, { - code: proxyCode, - directive: 'use server', - runtime: (name) => `$serverProxy(${JSON.stringify(name)})`, - }) + const cacheResult = await transformUseCache(input) + const rscResult = await transformUseServer(cacheResult.output.toString()) + const proxyResult = await transformMixedDirectiveProxy(input) await parseAstAsync(rscResult.output.toString()) - if (proxyResult) { - await parseAstAsync(proxyResult.output.toString()) - } + await parseAstAsync(proxyResult.output.toString()) await expect( formatTransformMarkdownFixture(input, [ { name: 'framework cache RSC transform', output: cacheResult.output, - references: cacheReferences, + references: cacheResult.references, }, { name: 'final RSC transform', output: rscResult.output, - references: rscResult.referenceNames, + references: rscResult.references, }, { name: 'browser and SSR proxy transform', - output: proxyResult?.output, - references: proxyResult?.exportNames, + output: proxyResult.output, + references: proxyResult.references, }, ]), ).toMatchFileSnapshot(file + '.snap.md') }) } +type TransformResult = { + output: MagicString + references: string[] +} + +async function transformUseCache(input: string): Promise { + const ast = await parseAstAsync(input) + const result = hasDirective(ast.body, 'use cache') + ? transformWrapExport(input, ast, { + runtime: cacheRuntime, + filter: (_name, meta) => !hasFunctionDirective(meta, 'use server'), + }) + : transformHoistInlineDirective(input, ast, { + directive: 'use cache', + hoistRuntime: true, + runtime: cacheRuntime, + }) + + const importPosition = + ast.body.find((node) => !('directive' in node))?.start ?? input.length + result.output.prependLeft( + importPosition, + `import { cache as $cache, register as $registerCache } from "cache-runtime";\n`, + ) + return { + output: result.output, + references: 'names' in result ? result.names : result.exportNames, + } +} + +async function transformUseServer(input: string): Promise { + const ast = await parseAstAsync(input) + const result = transformServerActionServer(input, ast, { + runtime: (value, name) => + `$registerServer(${value}, ${JSON.stringify(name)})`, + }) + return { output: result.output, references: result.referenceNames } +} + +async function transformMixedDirectiveProxy( + input: string, +): Promise { + const ast = await parseAstAsync(input) + const result = + transformDirectiveProxyExport(ast, { + code: input, + directive: 'use cache', + runtime: (name) => `$cacheProxy(${JSON.stringify(name)})`, + }) ?? + transformDirectiveProxyExport(ast, { + code: input, + directive: 'use server', + runtime: (name) => `$serverProxy(${JSON.stringify(name)})`, + }) + if (!result) { + throw new Error('expected a file directive') + } + return { output: result.output, references: result.exportNames } +} + function cacheRuntime(value: string, name: string): string { return `$registerCache($cache(${value}), ${JSON.stringify(name)})` }