diff --git a/packages/plugin-rsc/examples/hmr/README.md b/packages/plugin-rsc/examples/hmr/README.md new file mode 100644 index 00000000..bfb04fe6 --- /dev/null +++ b/packages/plugin-rsc/examples/hmr/README.md @@ -0,0 +1,7 @@ +The `App` just renders a client `Item` component passing in a key which It gets from 'lookup.ts'. The `Item` passes this key into 'lookup.ts' to get the value which it then displays. If you edit the key in 'lookup.ts' from 'a' to 'b', for example, then the hmr update throws. + +```ts +export const key = 'b' +``` + +The reason for the error is that the server response uses the latest 'lookup.ts' but the client uses the old 'lookup.ts'. So the rsc response has the key 'b' but the client still has the key 'a'. diff --git a/packages/plugin-rsc/examples/hmr/package.json b/packages/plugin-rsc/examples/hmr/package.json new file mode 100644 index 00000000..981def69 --- /dev/null +++ b/packages/plugin-rsc/examples/hmr/package.json @@ -0,0 +1,24 @@ +{ + "name": "@vitejs/plugin-rsc-examples-hmr", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@vitejs/plugin-rsc": "latest", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "latest", + "rsc-html-stream": "^0.0.7", + "vite": "^7.0.4", + "vite-plugin-inspect": "^11.3.0" + } +} diff --git a/packages/plugin-rsc/examples/hmr/public/favicon.ico b/packages/plugin-rsc/examples/hmr/public/favicon.ico new file mode 100644 index 00000000..4aff0766 Binary files /dev/null and b/packages/plugin-rsc/examples/hmr/public/favicon.ico differ diff --git a/packages/plugin-rsc/examples/hmr/src/App.tsx b/packages/plugin-rsc/examples/hmr/src/App.tsx new file mode 100644 index 00000000..08f56400 --- /dev/null +++ b/packages/plugin-rsc/examples/hmr/src/App.tsx @@ -0,0 +1,17 @@ +import { key } from './lookup' +import Item from './Item' + +const App = async () => { + return ( + + + Hmr + + + + + + ) +} + +export default App diff --git a/packages/plugin-rsc/examples/hmr/src/Item.tsx b/packages/plugin-rsc/examples/hmr/src/Item.tsx new file mode 100644 index 00000000..b9684269 --- /dev/null +++ b/packages/plugin-rsc/examples/hmr/src/Item.tsx @@ -0,0 +1,8 @@ +'use client' +import lookup from './lookup' + +const Item = ({ k }: { k: string }) => { + return lookup(k).hello +} + +export default Item diff --git a/packages/plugin-rsc/examples/hmr/src/client.tsx b/packages/plugin-rsc/examples/hmr/src/client.tsx new file mode 100644 index 00000000..72dc3eea --- /dev/null +++ b/packages/plugin-rsc/examples/hmr/src/client.tsx @@ -0,0 +1,30 @@ +import * as ReactClient from '@vitejs/plugin-rsc/browser' +import { useState, useEffect } from 'react' +import ReactDOM from 'react-dom/client' +import { rscStream } from 'rsc-html-stream/client' +import { createFromFetch } from '@vitejs/plugin-rsc/browser' + +async function fetchRSC(url: string, options: any) { + const payload = (await createFromFetch(fetch(url, options))) as any + return payload.root +} + +const initialPayload = await ReactClient.createFromReadableStream<{ + root: React.ReactNode +}>(rscStream) +function Shell() { + const [root, setRoot] = useState(initialPayload.root) + useEffect(() => { + const onHmrReload = () => { + const root = fetchRSC('/', { + method: 'post', + headers: { 'Content-Type': 'application/json' }, + }) + setRoot(root) + } + import.meta.hot?.on('rsc:update', onHmrReload) + return () => import.meta.hot?.off('rsc:update', onHmrReload) + }) + return root +} +ReactDOM.hydrateRoot(document, ) diff --git a/packages/plugin-rsc/examples/hmr/src/lookup.ts b/packages/plugin-rsc/examples/hmr/src/lookup.ts new file mode 100644 index 00000000..2e7da649 --- /dev/null +++ b/packages/plugin-rsc/examples/hmr/src/lookup.ts @@ -0,0 +1,11 @@ +export const key = 'a' + +const data = { + [key]: { hello: 'world ' }, +} as Record + +const lookup = (k: string) => { + return data[k] +} + +export default lookup diff --git a/packages/plugin-rsc/examples/hmr/src/server.ssr.tsx b/packages/plugin-rsc/examples/hmr/src/server.ssr.tsx new file mode 100644 index 00000000..790251b4 --- /dev/null +++ b/packages/plugin-rsc/examples/hmr/src/server.ssr.tsx @@ -0,0 +1,24 @@ +import * as ReactClient from '@vitejs/plugin-rsc/ssr' +import React from 'react' +import * as ReactDOMServer from 'react-dom/server.edge' +import { injectRSCPayload } from 'rsc-html-stream/server' + +type RscPayload = { + root: React.ReactNode +} +export async function renderHTML(rscStream: ReadableStream) { + const [rscStream1, rscStream2] = rscStream.tee() + let payload: Promise + function SsrRoot() { + payload ??= ReactClient.createFromReadableStream(rscStream1) + return React.use(payload).root + } + const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent('index') + const htmlStream = await ReactDOMServer.renderToReadableStream(, { + bootstrapScriptContent, + }) + let responseStream: ReadableStream = htmlStream + responseStream = responseStream.pipeThrough(injectRSCPayload(rscStream2)) + return responseStream +} diff --git a/packages/plugin-rsc/examples/hmr/src/server.tsx b/packages/plugin-rsc/examples/hmr/src/server.tsx new file mode 100644 index 00000000..2ce2c6e1 --- /dev/null +++ b/packages/plugin-rsc/examples/hmr/src/server.tsx @@ -0,0 +1,21 @@ +import * as ReactServer from '@vitejs/plugin-rsc/rsc' +import App from './App.tsx' + +export default async function handler(request: Request): Promise { + const root = + const rscStream = ReactServer.renderToReadableStream({ root }) + if (request.method !== 'GET') { + return new Response(rscStream, { + headers: { 'Content-type': 'text/x-component' }, + }) + } + const ssrEntryModule = await import.meta.viteRsc.loadModule< + typeof import('./server.ssr.tsx') + >('ssr', 'index') + const htmlStream = await ssrEntryModule.renderHTML(rscStream) + return new Response(htmlStream, { headers: { 'Content-type': 'text/html' } }) +} + +if (import.meta.hot) { + import.meta.hot.accept() +} diff --git a/packages/plugin-rsc/examples/hmr/tsconfig.json b/packages/plugin-rsc/examples/hmr/tsconfig.json new file mode 100644 index 00000000..77438d9d --- /dev/null +++ b/packages/plugin-rsc/examples/hmr/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["vite/client", "@vitejs/plugin-rsc/types"], + "jsx": "react-jsx" + } +} diff --git a/packages/plugin-rsc/examples/hmr/vite.config.ts b/packages/plugin-rsc/examples/hmr/vite.config.ts new file mode 100644 index 00000000..6c734151 --- /dev/null +++ b/packages/plugin-rsc/examples/hmr/vite.config.ts @@ -0,0 +1,22 @@ +import rsc from '@vitejs/plugin-rsc' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +export default defineConfig({ + clearScreen: false, + plugins: [ + react(), + rsc({ + entries: { + client: './src/client.tsx', + ssr: './src/server.ssr.tsx', + rsc: './src/server.tsx', + }, + ignoredPackageWarnings: ['navigation-react'], + }), + ], + optimizeDeps: { + include: ['navigation'], + exclude: ['navigation-react'], + }, +}) diff --git a/packages/plugin-rsc/examples/navigation/package.json b/packages/plugin-rsc/examples/navigation/package.json new file mode 100644 index 00000000..1f3cf25f --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/package.json @@ -0,0 +1,26 @@ +{ + "name": "@vitejs/plugin-rsc-examples-navigation", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@vitejs/plugin-rsc": "latest", + "navigation": "^6.3.0", + "navigation-react": "^4.13.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "latest", + "rsc-html-stream": "^0.0.7", + "vite": "^7.0.4", + "vite-plugin-inspect": "^11.3.0" + } +} diff --git a/packages/plugin-rsc/examples/navigation/public/favicon.ico b/packages/plugin-rsc/examples/navigation/public/favicon.ico new file mode 100644 index 00000000..4aff0766 Binary files /dev/null and b/packages/plugin-rsc/examples/navigation/public/favicon.ico differ diff --git a/packages/plugin-rsc/examples/navigation/src/App.tsx b/packages/plugin-rsc/examples/navigation/src/App.tsx new file mode 100644 index 00000000..99ee93aa --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/App.tsx @@ -0,0 +1,29 @@ +import { SceneView } from 'navigation-react' +import NavigationProvider from './NavigationProvider' +import HmrProvider from './HmrProvider' +import People from './People' +import Person from './Person' + +const App = async ({ url }: any) => { + return ( + + + Navigation React + + + + + + + + + + + + + + + ) +} + +export default App; diff --git a/packages/plugin-rsc/examples/navigation/src/Filter.tsx b/packages/plugin-rsc/examples/navigation/src/Filter.tsx new file mode 100644 index 00000000..904c1091 --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/Filter.tsx @@ -0,0 +1,38 @@ +'use client' +import { startTransition, useOptimistic } from 'react' +import { RefreshLink, useNavigationEvent } from 'navigation-react' + +const Filter = () => { + const { data, stateNavigator } = useNavigationEvent() + const { name } = data + const [optimisticName, setOptimisticName] = useOptimistic( + name || '', + (_, newName) => newName, + ) + return ( +
+
+ + { + startTransition(() => { + setOptimisticName(value) + stateNavigator.refresh({ ...data, name: value, page: null }) + }) + }} + /> +
+ Page size + + 5 + + + 10 + +
+ ) +} + +export default Filter diff --git a/packages/plugin-rsc/examples/navigation/src/Friends.tsx b/packages/plugin-rsc/examples/navigation/src/Friends.tsx new file mode 100644 index 00000000..7254cab2 --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/Friends.tsx @@ -0,0 +1,34 @@ +import { RefreshLink, useNavigationEvent } from 'navigation-react' +import { getFriends } from './data' +import Gender from './Gender' + +const Friends = async () => { + const { + data: { show, id, gender }, + } = useNavigationEvent() + const friends = show ? await getFriends(id, gender) : null + return ( + <> + {`${!show ? 'Show' : 'Hide'} Friends`} + {show && ( + <> + +
    + {friends?.map(({ id, name }) => ( +
  • + + {name} + +
  • + ))} +
+ + )} + + ) +} + +export default Friends diff --git a/packages/plugin-rsc/examples/navigation/src/Gender.tsx b/packages/plugin-rsc/examples/navigation/src/Gender.tsx new file mode 100644 index 00000000..a7cf044f --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/Gender.tsx @@ -0,0 +1,35 @@ +'use client' +import { startTransition } from 'react' +import { useNavigationEvent } from 'navigation-react' +import { useOptimistic } from 'react' + +const Gender = () => { + const { data, stateNavigator } = useNavigationEvent() + const { gender } = data + const [optimisticGender, setOptimisticGender] = useOptimistic( + gender || '', + (_, newGender) => newGender, + ) + return ( +
+ + +
+ ) +} + +export default Gender diff --git a/packages/plugin-rsc/examples/navigation/src/HmrProvider.tsx b/packages/plugin-rsc/examples/navigation/src/HmrProvider.tsx new file mode 100644 index 00000000..537229cd --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/HmrProvider.tsx @@ -0,0 +1,42 @@ +'use client' +import { useContext, useEffect } from 'react' +import { BundlerContext, useNavigationEvent } from 'navigation-react' + +const HmrProvider = ({ children }: any) => { + const { setRoot, deserialize } = useContext(BundlerContext) + const { stateNavigator } = useNavigationEvent() + useEffect(() => { + const onHmrReload = () => { + const { + stateContext: { + state, + data, + crumbs, + nextCrumb: { crumblessUrl }, + }, + } = stateNavigator + const root = deserialize( + stateNavigator.historyManager.getHref(crumblessUrl), + { + method: 'put', + headers: { 'Content-Type': 'application/json' }, + body: { + crumbs: crumbs.map(({ state, data }) => ({ + state: state.key, + data, + })), + state: state.key, + data, + }, + }, + ) + stateNavigator.historyManager.stop() + setRoot(root) + } + import.meta.hot?.on("rsc:update", onHmrReload); + return () => import.meta.hot?.off("rsc:update", onHmrReload); + }) + return children +} + +export default HmrProvider diff --git a/packages/plugin-rsc/examples/navigation/src/NavigationProvider.tsx b/packages/plugin-rsc/examples/navigation/src/NavigationProvider.tsx new file mode 100644 index 00000000..1e1c5ec9 --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/NavigationProvider.tsx @@ -0,0 +1,23 @@ +'use client' +import { useMemo } from 'react' +import { StateNavigator, HTML5HistoryManager } from 'navigation' +import { NavigationHandler } from 'navigation-react' +import stateNavigator from './stateNavigator' + +const historyManager = new HTML5HistoryManager() + +const NavigationProvider = ({ url, children }: any) => { + const clientNavigator = useMemo(() => { + historyManager.stop() + const clientNavigator = new StateNavigator(stateNavigator, historyManager) + clientNavigator.navigateLink(url) + return clientNavigator + }, []) + return ( + + {children} + + ) +} + +export default NavigationProvider diff --git a/packages/plugin-rsc/examples/navigation/src/Pager.tsx b/packages/plugin-rsc/examples/navigation/src/Pager.tsx new file mode 100644 index 00000000..bb8a7991 --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/Pager.tsx @@ -0,0 +1,64 @@ +import { RefreshLink, useNavigationEvent } from 'navigation-react' + +const Pager = ({ totalRowCount }: { totalRowCount: number }) => { + const { + data: { page, size }, + } = useNavigationEvent() + const lastPage = Math.ceil(totalRowCount / size) + return ( +
+
    + {totalRowCount ? ( + <> +
  • + + First + +
  • +
  • + + Previous + +
  • +
  • + + Next + +
  • +
  • + + Last + +
  • + + ) : ( + <> +
  • First
  • +
  • Previous
  • +
  • Next
  • +
  • Last
  • + + )} +
+ Total Count {totalRowCount} +
+ ) +} + +export default Pager diff --git a/packages/plugin-rsc/examples/navigation/src/People.tsx b/packages/plugin-rsc/examples/navigation/src/People.tsx new file mode 100644 index 00000000..6262ade3 --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/People.tsx @@ -0,0 +1,51 @@ +import { searchPeople } from './data' +import { + NavigationLink, + RefreshLink, + useNavigationEvent, +} from 'navigation-react' +import Filter from './Filter' +import Pager from './Pager' + +const People = async () => { + const { + data: { name, page, size, sort }, + } = useNavigationEvent() + const { people, count } = await searchPeople(name, page, size, sort) + return ( + <> +

People

+ + + + + + + + + + {people.map(({ id, name, dateOfBirth }) => ( + + + + + ))} + +
+ + Name + + Date of Birth
+ + {name} + + {dateOfBirth}
+ + + ) +} + +export default People diff --git a/packages/plugin-rsc/examples/navigation/src/Person.tsx b/packages/plugin-rsc/examples/navigation/src/Person.tsx new file mode 100644 index 00000000..f76e82da --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/Person.tsx @@ -0,0 +1,34 @@ +import { + SceneView, + NavigationBackLink, + useNavigationEvent, +} from 'navigation-react' +import { getPerson } from './data' +import Friends from './Friends' + +const Person = async () => { + const { data } = useNavigationEvent() + const { name, dateOfBirth, email, phone } = await getPerson(data.id) + return ( + <> +

Person

+
+ Person Search +
+

{name}

+
Date of Birth
+
{dateOfBirth}
+
Email
+
{email}
+
Phone
+
{phone}
+
+ + + +
+ + ) +} + +export default Person diff --git a/packages/plugin-rsc/examples/navigation/src/client.tsx b/packages/plugin-rsc/examples/navigation/src/client.tsx new file mode 100644 index 00000000..afe91f80 --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/client.tsx @@ -0,0 +1,23 @@ +import * as ReactClient from '@vitejs/plugin-rsc/browser' +import { useState, useMemo } from "react"; +import ReactDOM from "react-dom/client"; +import { rscStream } from 'rsc-html-stream/client' +import { createFromFetch } from "@vitejs/plugin-rsc/browser"; +import { BundlerContext } from 'navigation-react'; + +async function fetchRSC(url: string, {body, ...options}: any) { + const payload = await createFromFetch(fetch(url, {...options, body: JSON.stringify(body)})) as any; + return payload.root; +} + +const initialPayload = await ReactClient.createFromReadableStream<{root: React.ReactNode}>(rscStream) +function Shell() { + const [root, setRoot] = useState(initialPayload.root); + const bundler = useMemo(() => ({setRoot, deserialize: fetchRSC}), []); + return ( + + {root} + + ); +} +ReactDOM.hydrateRoot(document, ); diff --git a/packages/plugin-rsc/examples/navigation/src/data.ts b/packages/plugin-rsc/examples/navigation/src/data.ts new file mode 100644 index 00000000..01da4cba --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/data.ts @@ -0,0 +1,175 @@ +type Person = { + id: number + name: string + gender: 'male' | 'female' | 'other' + dateOfBirth: string + email: string + phone: string + friends: number[] +} + +var people: Person[] = [ + { + id: 1, + name: 'Bell Halvorson', + gender: 'female', + dateOfBirth: '01/01/1980', + email: 'bell@navigation.com', + phone: '555 0001', + friends: [2, 3, 4, 5], + }, + { + id: 2, + name: 'Aditya Larson', + gender: 'male', + dateOfBirth: '01/02/1980', + email: 'aditya@navigation.com', + phone: '555 0002', + friends: [3, 4, 5, 6], + }, + { + id: 3, + name: 'Rashawn Schamberger', + gender: 'male', + dateOfBirth: '01/03/1980', + email: 'rashawn@navigation.com', + phone: '555 0003', + friends: [4, 5, 6, 7], + }, + { + id: 4, + name: 'Rupert Grant', + gender: 'male', + dateOfBirth: '01/04/1980', + email: 'rupert@navigation.com', + phone: '555 0004', + friends: [5, 6, 7, 8], + }, + { + id: 5, + name: 'Opal Carter', + gender: 'female', + dateOfBirth: '01/05/1980', + email: 'opal@navigation.com', + phone: '555 0005', + friends: [6, 7, 8, 9], + }, + { + id: 6, + name: 'Candida Christiansen', + gender: 'female', + dateOfBirth: '01/06/1980', + email: 'candida@navigation.com', + phone: '555 0006', + friends: [7, 8, 9, 10], + }, + { + id: 7, + name: 'Haven Stroman', + gender: 'other', + dateOfBirth: '01/07/1980', + email: 'haven@navigation.com', + phone: '555 0007', + friends: [8, 9, 10, 11], + }, + { + id: 8, + name: 'Celine Leannon', + gender: 'female', + dateOfBirth: '01/08/1980', + email: 'celine@navigation.com', + phone: '555 0008', + friends: [9, 10, 11, 12], + }, + { + id: 9, + name: 'Ryan Ruecker', + gender: 'male', + dateOfBirth: '01/09/1980', + email: 'ryan@navigation.com', + phone: '555 0009', + friends: [10, 11, 12, 1], + }, + { + id: 10, + name: 'Kaci Hoppe', + gender: 'other', + dateOfBirth: '01/10/1980', + email: 'kaci@navigation.com', + phone: '555 0010', + friends: [11, 12, 1, 2], + }, + { + id: 11, + name: 'Fernando Dietrich', + gender: 'male', + dateOfBirth: '01/11/1980', + email: 'fernando@navigation.com', + phone: '555 0011', + friends: [12, 1, 2, 3], + }, + { + id: 12, + name: 'Emelie Lueilwitz', + gender: 'female', + dateOfBirth: '01/12/1980', + email: 'emelie@navigation.com', + phone: '555 0012', + friends: [1, 2, 3, 4], + }, +] + +const searchPeople = async ( + name: string, + pageNumber: number, + pageSize: number, + sortExpression: string, +) => { + return new Promise( + (res: (data: { people: Person[]; count: number }) => void) => { + var start = (pageNumber - 1) * pageSize + var filteredPeople = people + .sort((personA, personB) => { + var mult = sortExpression.indexOf('desc') === -1 ? -1 : 1 + return mult * (personA.name < personB.name ? 1 : -1) + }) + .filter( + (person) => + !name || + person.name.toUpperCase().indexOf(name.toUpperCase()) !== -1, + ) + setTimeout(() => { + res({ + people: filteredPeople.slice(start, start + pageSize), + count: filteredPeople.length, + }) + }, 10) + }, + ) +} + +const getPerson = async (id: number) => { + return new Promise((res: (person: Person) => void) => { + const person = people.find(({ id: personId }) => personId === id)! + setTimeout(() => { + res(person) + }, 10) + }) +} + +const getFriends = async (id: number, gender: 'male' | 'female' | 'other') => { + return new Promise((res: (friends: Person[]) => void) => { + const person = people.find(({ id: personId }) => personId == id)! + setTimeout(() => { + res( + person.friends + .map((id) => people.find(({ id: personId }) => personId === id)!) + .filter( + ({ gender: personGender }) => !gender || personGender === gender, + ), + ) + }, 10) + }) +} + +export { searchPeople, getPerson, getFriends } diff --git a/packages/plugin-rsc/examples/navigation/src/server.ssr.tsx b/packages/plugin-rsc/examples/navigation/src/server.ssr.tsx new file mode 100644 index 00000000..2dfe8785 --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/server.ssr.tsx @@ -0,0 +1,27 @@ +import * as ReactClient from '@vitejs/plugin-rsc/ssr'; +import React from 'react'; +import * as ReactDOMServer from 'react-dom/server.edge'; +import { injectRSCPayload } from 'rsc-html-stream/server'; + +type RscPayload = { + root: React.ReactNode +} +export async function renderHTML( + rscStream: ReadableStream) { + const [rscStream1, rscStream2] = rscStream.tee(); + let payload: Promise; + function SsrRoot() { + payload ??= ReactClient.createFromReadableStream(rscStream1); + return React.use(payload).root; + } + const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent('index'); + const htmlStream = await ReactDOMServer.renderToReadableStream(, { + bootstrapScriptContent, + }); + let responseStream: ReadableStream = htmlStream; + responseStream = responseStream.pipeThrough( + injectRSCPayload(rscStream2), + ); + return responseStream; +} diff --git a/packages/plugin-rsc/examples/navigation/src/server.tsx b/packages/plugin-rsc/examples/navigation/src/server.tsx new file mode 100644 index 00000000..8dd1dd77 --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/server.tsx @@ -0,0 +1,61 @@ +import * as ReactServer from '@vitejs/plugin-rsc/rsc'; +import { StateNavigator } from 'navigation'; +import stateNavigator from './stateNavigator.ts'; + +export default async function handler(request: Request): Promise { + let url: string = ''; + let view: any; + const serverNavigator = new StateNavigator(stateNavigator); + if (request.method === 'GET') { + let reqUrl = new URL(request.url); + url = `${reqUrl.pathname}${reqUrl.search}`; + const App = (await import('./App.tsx')).default; + view = ; + } + if (request.method === 'POST') { + const sceneViews: any = { + people: await import('./People.tsx'), + person: await import('./Person.tsx'), + friends: await import('./Friends.tsx') + }; + const {url: reqUrl, sceneViewKey} = await request.json(); + url = reqUrl; + const SceneView = sceneViews[sceneViewKey].default; + view = ; + } + if (request.method === 'PUT') { + const {state, data, crumbs} = await request.json(); + let fluentNavigator = serverNavigator.fluent(); + for (let i = 0; i < crumbs.length; i++) { + fluentNavigator = fluentNavigator.navigate(crumbs[i].state, crumbs[i].data); + } + fluentNavigator = fluentNavigator.navigate(state, data); + url = fluentNavigator.url; + const App = (await import('./App.tsx')).default; + view = ; + } + try { + serverNavigator.navigateLink(url); + } catch(e) { + return new Response('Not Found', { status: 404 }); + } + const {NavigationHandler} = await import('navigation-react'); + const root = ( + <> + + {view} + + + ); + const rscStream = ReactServer.renderToReadableStream({root}); + if (request.method !== 'GET') { + return new Response(rscStream, {headers: {'Content-type': 'text/x-component'}}); + } + const ssrEntryModule = await import.meta.viteRsc.loadModule('ssr', 'index'); + const htmlStream = await ssrEntryModule.renderHTML(rscStream); + return new Response(htmlStream, {headers: {'Content-type': 'text/html'}}); +} + +if (import.meta.hot) { + import.meta.hot.accept(); +} diff --git a/packages/plugin-rsc/examples/navigation/src/stateNavigator.ts b/packages/plugin-rsc/examples/navigation/src/stateNavigator.ts new file mode 100644 index 00000000..e33e2758 --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/src/stateNavigator.ts @@ -0,0 +1,18 @@ +import { StateNavigator } from 'navigation' + +const stateNavigator = new StateNavigator([ + { + key: 'people', + route: '{page?}', + defaults: { page: 1, sort: 'asc', size: 10 }, + }, + { + key: 'person', + route: 'person/{id}+/{show}', + defaults: { id: 0, show: false }, + trackCrumbTrail: true, + }, +]) +stateNavigator.historyManager.stop() + +export default stateNavigator diff --git a/packages/plugin-rsc/examples/navigation/tsconfig.json b/packages/plugin-rsc/examples/navigation/tsconfig.json new file mode 100644 index 00000000..77438d9d --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["vite/client", "@vitejs/plugin-rsc/types"], + "jsx": "react-jsx" + } +} diff --git a/packages/plugin-rsc/examples/navigation/vite.config.ts b/packages/plugin-rsc/examples/navigation/vite.config.ts new file mode 100644 index 00000000..6c734151 --- /dev/null +++ b/packages/plugin-rsc/examples/navigation/vite.config.ts @@ -0,0 +1,22 @@ +import rsc from '@vitejs/plugin-rsc' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +export default defineConfig({ + clearScreen: false, + plugins: [ + react(), + rsc({ + entries: { + client: './src/client.tsx', + ssr: './src/server.ssr.tsx', + rsc: './src/server.tsx', + }, + ignoredPackageWarnings: ['navigation-react'], + }), + ], + optimizeDeps: { + include: ['navigation'], + exclude: ['navigation-react'], + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 889b162a..341d1f67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -601,6 +601,37 @@ importers: specifier: 19.1.0-rc.2 version: 19.1.0-rc.2 + packages/plugin-rsc/examples/navigation: + dependencies: + '@vitejs/plugin-rsc': + specifier: latest + version: link:../.. + navigation: + specifier: ^6.3.0 + version: 6.3.0 + navigation-react: + specifier: ^4.12.0 + version: 4.12.0(navigation@6.3.0)(react@19.1.0) + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@types/react': + specifier: ^19.1.8 + version: 19.1.8 + '@types/react-dom': + specifier: ^19.1.6 + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react': + specifier: latest + version: link:../../../plugin-react + vite: + specifier: ^7.0.2 + version: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + packages/plugin-rsc/examples/no-ssr: dependencies: '@vitejs/plugin-rsc': @@ -3856,6 +3887,15 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + navigation-react@4.12.0: + resolution: {integrity: sha512-sYCWeqyB7Ev4nLcIs2REkA9WbL74XpFagto9VOVPloKwVb6zgSlRbt0CwQKtFVLU/iL4VncdSmlp6OnJaSSfYw==} + peerDependencies: + navigation: '*' + react: '*' + + navigation@6.3.0: + resolution: {integrity: sha512-FzpKHKcqn0c1Qcm6e6UHSJY2TvIWHHXMRYzSHXdCVOTnnMQO012gzsqtxu5aCD4qNK9ffAgn48wOn3umczgVaw==} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -7845,6 +7885,13 @@ snapshots: natural-compare@1.4.0: {} + navigation-react@4.12.0(navigation@6.3.0)(react@19.1.0): + dependencies: + navigation: 6.3.0 + react: 19.1.0 + + navigation@6.3.0: {} + neo-async@2.6.2: {} node-domexception@1.0.0: {}