-
-
Notifications
You must be signed in to change notification settings - Fork 266
feat(rsc): support hoisting with runtime wrapper in transformHoistInlineDirective + add callable use cache example
#1330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
e69bc30
feat(rsc): hoist inline directive runtimes
hi-ogawa 9913cee
Merge branch 'main' into callable-inline-hoist
hi-ogawa 6833638
fix: lockfile
hi-ogawa adb9b37
Merge branch 'main' into callable-inline-hoist
hi-ogawa f3ff93e
test(rsc): use fixtures for hoisted runtimes
hi-ogawa f27b72c
test(rsc): simplify hoisted runtime fixture
hi-ogawa 39d3893
refactor(rsc): place runtime hoists before imports
hi-ogawa a0413a3
test(rsc): cover callable cache file directives
hi-ogawa-agent bbbae0d
refactor(rsc): align callable cache example framework
hi-ogawa 2a5ddef
refactor(rsc): route callable cache examples
hi-ogawa 018f110
test(rsc): cover callable progressive enhancement
hi-ogawa 4104915
Merge branch 'main' into callable-inline-hoist
hi-ogawa a60e1b7
chore(rsc): align callable cache example framework
hi-ogawa 019c955
refactor(rsc): simplify example FormData cache keys
hi-ogawa 9b26887
test(rsc): clarify callable cache scenarios
hi-ogawa 677b4c5
test(rsc): explain callable submit synchronization
hi-ogawa a2ea49c
test(rsc): cover callable cache progressive forms
hi-ogawa 6643232
test(rsc): align callable cache scenarios
hi-ogawa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { expect, test } from '@playwright/test' | ||
| import { type Fixture, useFixture } from './fixture' | ||
| import { expectNoPageError, waitForHydration } from './helper' | ||
|
|
||
| test.describe('dev', () => { | ||
| const f = useFixture({ root: 'examples/use-cache-callable', mode: 'dev' }) | ||
| defineTests(f) | ||
| }) | ||
|
|
||
| test.describe('build', () => { | ||
| const f = useFixture({ root: 'examples/use-cache-callable', mode: 'build' }) | ||
| defineTests(f) | ||
| }) | ||
|
|
||
| function defineTests(f: Fixture) { | ||
| test('calls the exported cache wrapper', async ({ page }) => { | ||
| using _errors = expectNoPageError(page) | ||
| await page.goto(f.url()) | ||
| await waitForHydration(page) | ||
|
|
||
| const example = page.getByTestId('callable-cache') | ||
| await expect(example.locator('span')).toHaveText( | ||
| 'requests: 0; result: none', | ||
| ) | ||
| await example.getByRole('button', { name: 'same' }).click() | ||
| await expect(example.locator('span')).toHaveText( | ||
| 'requests: 1; result: captured:same:1', | ||
| ) | ||
| await example.getByRole('button', { name: 'same' }).click() | ||
| await expect(example.locator('span')).toHaveText( | ||
| 'requests: 2; result: captured:same:1', | ||
| ) | ||
| await example.getByRole('button', { name: 'different' }).click() | ||
| await expect(example.locator('span')).toHaveText( | ||
| 'requests: 3; result: captured:different:2', | ||
| ) | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| node_modules | ||
| dist |
23 changes: 23 additions & 0 deletions
23
packages/plugin-rsc/examples/use-cache-callable/package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "name": "@vitejs/plugin-rsc-examples-use-cache-callable", | ||
| "private": true, | ||
| "license": "MIT", | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "vite build", | ||
| "preview": "vite preview" | ||
| }, | ||
| "dependencies": { | ||
| "react": "^19.2.8", | ||
| "react-dom": "^19.2.8" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/react": "^19.2.17", | ||
| "@types/react-dom": "^19.2.3", | ||
| "@vitejs/plugin-react": "latest", | ||
| "@vitejs/plugin-rsc": "latest", | ||
| "rsc-html-stream": "^0.0.7", | ||
| "vite": "^8.1.4" | ||
| } | ||
| } |
17 changes: 17 additions & 0 deletions
17
packages/plugin-rsc/examples/use-cache-callable/src/cache-runtime.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| type CacheFunction = (...args: string[]) => Promise<string> | ||
|
|
||
| export default function cacheWrapper( | ||
| implementation: CacheFunction, | ||
| ): CacheFunction { | ||
| const entries = new Map<string, Promise<string>>() | ||
|
|
||
| return (...args) => { | ||
| const key = JSON.stringify(args) | ||
| let result = entries.get(key) | ||
| if (!result) { | ||
| result = implementation(...args) | ||
| entries.set(key, result) | ||
| } | ||
| return result | ||
| } | ||
| } |
25 changes: 25 additions & 0 deletions
25
packages/plugin-rsc/examples/use-cache-callable/src/client.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| 'use client' | ||
|
|
||
| import { useState } from 'react' | ||
|
|
||
| export function CallableCacheClient(props: { | ||
| action: (argument: string) => Promise<string> | ||
| }) { | ||
| const [requests, setRequests] = useState(0) | ||
| const [result, setResult] = useState('none') | ||
|
|
||
| async function call(argument: string) { | ||
| setRequests((value) => value + 1) | ||
| setResult(await props.action(argument)) | ||
| } | ||
|
|
||
| return ( | ||
| <div data-testid="callable-cache"> | ||
| <button onClick={() => void call('same')}>same</button> | ||
| <button onClick={() => void call('different')}>different</button> | ||
| <span> | ||
| requests: {requests}; result: {result} | ||
| </span> | ||
| </div> | ||
| ) | ||
| } |
44 changes: 44 additions & 0 deletions
44
packages/plugin-rsc/examples/use-cache-callable/src/framework/entry.browser.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { | ||
| createFromFetch, | ||
| createFromReadableStream, | ||
| createTemporaryReferenceSet, | ||
| encodeReply, | ||
| setServerCallback, | ||
| } from '@vitejs/plugin-rsc/browser' | ||
| import { useEffect, useState } from 'react' | ||
| import { hydrateRoot } from 'react-dom/client' | ||
| import { rscStream } from 'rsc-html-stream/client' | ||
| import type { RscPayload } from './entry.rsc' | ||
| import { createRscRenderRequest } from './request' | ||
|
|
||
| async function main() { | ||
| const initialPayload = await createFromReadableStream<RscPayload>(rscStream) | ||
| let updatePayload: (payload: RscPayload) => void | ||
|
|
||
| function BrowserRoot() { | ||
| const [payload, setPayload] = useState(initialPayload) | ||
| useEffect(() => { | ||
| updatePayload = setPayload | ||
| }, []) | ||
| return payload.root | ||
| } | ||
|
|
||
| setServerCallback(async (id, args) => { | ||
| const temporaryReferences = createTemporaryReferenceSet() | ||
| const request = createRscRenderRequest(window.location.href, { | ||
| id, | ||
| body: await encodeReply(args, { temporaryReferences }), | ||
| }) | ||
| const payload = await createFromFetch<RscPayload>(fetch(request), { | ||
| temporaryReferences, | ||
| }) | ||
| updatePayload(payload) | ||
| const { ok, data } = payload.returnValue! | ||
| if (!ok) throw data | ||
| return data | ||
| }) | ||
|
|
||
| hydrateRoot(document, <BrowserRoot />) | ||
| } | ||
|
|
||
| main() |
61 changes: 61 additions & 0 deletions
61
packages/plugin-rsc/examples/use-cache-callable/src/framework/entry.rsc.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import { | ||
| createTemporaryReferenceSet, | ||
| decodeReply, | ||
| loadServerAction, | ||
| renderToReadableStream, | ||
| } from '@vitejs/plugin-rsc/rsc' | ||
| import { Root } from '../root' | ||
| import { parseRenderRequest } from './request' | ||
|
|
||
| export type RscPayload = { | ||
| root: React.ReactNode | ||
| returnValue?: { ok: boolean; data: unknown } | ||
| } | ||
|
|
||
| export default { fetch: handler } | ||
|
|
||
| async function handler(request: Request): Promise<Response> { | ||
| const renderRequest = parseRenderRequest(request) | ||
| let returnValue: RscPayload['returnValue'] | ||
| let temporaryReferences: unknown | ||
| let status: number | undefined | ||
|
|
||
| if (renderRequest.actionId) { | ||
| temporaryReferences = createTemporaryReferenceSet() | ||
| const args = await decodeReply(await renderRequest.request.text(), { | ||
| temporaryReferences, | ||
| }) | ||
| const action = await loadServerAction(renderRequest.actionId) | ||
| try { | ||
| returnValue = { ok: true, data: await action.apply(null, args) } | ||
| } catch (error) { | ||
| returnValue = { ok: false, data: error } | ||
| status = 500 | ||
| } | ||
| } | ||
|
|
||
| const payload: RscPayload = { | ||
| root: <Root />, | ||
| returnValue, | ||
| } | ||
| const rscStream = renderToReadableStream<RscPayload>(payload, { | ||
| temporaryReferences, | ||
| }) | ||
| if (renderRequest.isRsc) { | ||
| return new Response(rscStream, { | ||
| status, | ||
| headers: { 'content-type': 'text/x-component;charset=utf-8' }, | ||
| }) | ||
| } | ||
|
|
||
| const ssr = await import.meta.viteRsc.loadModule< | ||
| typeof import('./entry.ssr') | ||
| >('ssr', 'index') | ||
| const htmlStream = await ssr.renderHTML(rscStream) | ||
| return new Response(htmlStream, { | ||
| status, | ||
| headers: { 'content-type': 'text/html' }, | ||
| }) | ||
| } | ||
|
|
||
| if (import.meta.hot) import.meta.hot.accept() |
22 changes: 22 additions & 0 deletions
22
packages/plugin-rsc/examples/use-cache-callable/src/framework/entry.ssr.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr' | ||
| import { use } from 'react' | ||
| import { renderToReadableStream } from 'react-dom/server.edge' | ||
| import { injectRSCPayload } from 'rsc-html-stream/server' | ||
| import type { RscPayload } from './entry.rsc' | ||
|
|
||
| export async function renderHTML(rscStream: ReadableStream<Uint8Array>) { | ||
| const [ssrStream, browserStream] = rscStream.tee() | ||
| let payload: Promise<RscPayload> | undefined | ||
|
|
||
| function SsrRoot() { | ||
| payload ??= createFromReadableStream<RscPayload>(ssrStream) | ||
| return use(payload).root | ||
| } | ||
|
|
||
| const bootstrapScriptContent = | ||
| await import.meta.viteRsc.loadBootstrapScriptContent('index') | ||
| const htmlStream = await renderToReadableStream(<SsrRoot />, { | ||
| bootstrapScriptContent, | ||
| }) | ||
| return htmlStream.pipeThrough(injectRSCPayload(browserStream)) | ||
| } |
29 changes: 29 additions & 0 deletions
29
packages/plugin-rsc/examples/use-cache-callable/src/framework/request.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| const URL_POSTFIX = '_.rsc' | ||
| const HEADER_ACTION_ID = 'x-rsc-action' | ||
|
|
||
| export function createRscRenderRequest( | ||
| urlString: string, | ||
| action?: { id: string; body: BodyInit }, | ||
| ): Request { | ||
| const url = new URL(urlString) | ||
| url.pathname += URL_POSTFIX | ||
| const headers = new Headers() | ||
| if (action) headers.set(HEADER_ACTION_ID, action.id) | ||
| return new Request(url, { | ||
| method: action ? 'POST' : 'GET', | ||
| headers, | ||
| body: action?.body, | ||
| }) | ||
| } | ||
|
|
||
| export function parseRenderRequest(request: Request) { | ||
| const url = new URL(request.url) | ||
| const isRsc = url.pathname.endsWith(URL_POSTFIX) | ||
| if (isRsc) url.pathname = url.pathname.slice(0, -URL_POSTFIX.length) | ||
| return { | ||
| isRsc, | ||
| actionId: request.headers.get(HEADER_ACTION_ID) || undefined, | ||
| request: new Request(url, request), | ||
| url, | ||
| } | ||
| } |
26 changes: 26 additions & 0 deletions
26
packages/plugin-rsc/examples/use-cache-callable/src/root.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { CallableCacheClient } from './client' | ||
|
|
||
| let implementationCalls = 0 | ||
|
|
||
| export function Root() { | ||
| const captured = 'captured' | ||
|
|
||
| async function cachedAction(argument: string) { | ||
| 'use cache' | ||
|
hi-ogawa marked this conversation as resolved.
Outdated
|
||
| implementationCalls++ | ||
| return `${captured}:${argument}:${implementationCalls}` | ||
| } | ||
|
|
||
| return ( | ||
| <html> | ||
| <head> | ||
| <meta charSet="utf-8" /> | ||
| <title>RSC callable use cache</title> | ||
| </head> | ||
| <body> | ||
| <h1>RSC callable use cache</h1> | ||
| <CallableCacheClient action={cachedAction} /> | ||
| </body> | ||
| </html> | ||
| ) | ||
| } | ||
16 changes: 16 additions & 0 deletions
16
packages/plugin-rsc/examples/use-cache-callable/tsconfig.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "allowImportingTsExtensions": true, | ||
| "noUnusedLocals": true, | ||
| "noUnusedParameters": true, | ||
| "skipLibCheck": true, | ||
| "verbatimModuleSyntax": true, | ||
| "noEmit": true, | ||
| "moduleResolution": "Bundler", | ||
| "module": "ESNext", | ||
| "target": "ESNext", | ||
| "lib": ["ESNext", "DOM"], | ||
| "types": ["vite/client", "@vitejs/plugin-rsc/types"], | ||
| "jsx": "react-jsx" | ||
| } | ||
| } |
71 changes: 71 additions & 0 deletions
71
packages/plugin-rsc/examples/use-cache-callable/vite.config.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import react from '@vitejs/plugin-react' | ||
| import rsc, { getPluginApi, type RscPluginManager } from '@vitejs/plugin-rsc' | ||
| import { transformHoistInlineDirective } from '@vitejs/plugin-rsc/transforms' | ||
| import { defineConfig, parseAstAsync, type Plugin } from 'vite' | ||
|
|
||
| const directive = 'use cache' | ||
| const pluginName = 'example:use-cache-callable' | ||
|
|
||
| export default defineConfig({ | ||
| plugins: [ | ||
| react(), | ||
| callableCachePlugin(), | ||
| rsc({ | ||
| entries: { | ||
| client: './src/framework/entry.browser.tsx', | ||
| ssr: './src/framework/entry.ssr.tsx', | ||
| rsc: './src/framework/entry.rsc.tsx', | ||
| }, | ||
| }), | ||
| ], | ||
| }) | ||
|
|
||
| function callableCachePlugin(): Plugin { | ||
| let manager: RscPluginManager | ||
|
|
||
| return { | ||
| name: pluginName, | ||
| configResolved(config) { | ||
| manager = getPluginApi(config)!.manager | ||
| }, | ||
| async transform(code, id) { | ||
| if (this.environment.name !== 'rsc') return | ||
| if (!code.includes(directive)) { | ||
| manager.serverReferences.deleteClaim(pluginName, id) | ||
| return | ||
| } | ||
|
|
||
| const reference = manager.serverReferences.resolve(id, 'rsc') | ||
| const ast = (await parseAstAsync(code)) as unknown as Parameters< | ||
| typeof transformHoistInlineDirective | ||
| >[1] | ||
| const result = transformHoistInlineDirective(code, ast, { | ||
| directive, | ||
| rejectNonAsyncFunction: true, | ||
| hoistRuntime: true, | ||
| runtime: (value, name) => | ||
| `$$ReactServer.registerServerReference(` + | ||
| `$$cacheWrapper(${value}),` + | ||
| `${JSON.stringify(reference.referenceKey)},` + | ||
| `${JSON.stringify(name)})`, | ||
| }) | ||
| if (!result.output.hasChanged()) { | ||
| manager.serverReferences.deleteClaim(pluginName, id) | ||
| return | ||
| } | ||
|
|
||
| manager.serverReferences.replaceClaim(pluginName, id, { | ||
| ...reference, | ||
| exportNames: result.names, | ||
| }) | ||
| result.output.prepend( | ||
| `import $$cacheWrapper from "/src/cache-runtime";\n` + | ||
| `import * as $$ReactServer from "@vitejs/plugin-rsc/react/rsc/server";\n`, | ||
| ) | ||
| return { | ||
| code: result.output.toString(), | ||
| map: result.output.generateMap({ hires: 'boundary' }), | ||
| } | ||
| }, | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.