diff --git a/README.md b/README.md
index f71c7aa..01230ea 100644
--- a/README.md
+++ b/README.md
@@ -3,332 +3,16 @@
SSRx provides the missing pieces required to create SSR apps with Vite and your third party libraries of choice. It is
framework agnostic on the client and the server - use React, Solid, Hono, H3, Cloudflare, Bun, you name it.
-SSRx is split into two parts that can be used independently, or together:
+SSRx is split into two parts that can be used independently, or together.
-1. `@ssrx/vite` - a Vite plugin to improve the DX of developing SSR apps (can be used on it's own).
-2. `@ssrx/renderer` - establishes some patterns to hook into the lifecycle of streaming SSR apps in a framework/library
- agnostic way. A handful of renderer plugins for common libraries are maintained in this repo.
+**[`@ssrx/vite`](/packages/vite/README.md)**
-## `@ssrx/vite`
+A Vite plugin to improve the DX of developing SSR apps.
-> ❗ Remix is transitioning to Vite, so for Vite + React Router projects I now recommend Remix as the best-in-class
-> option.
+**[`@ssrx/renderer`](/packages/renderer/README.md)**
-The SSRx Vite plugin is barebones and (mostly) unopinionated by design. It can be used standalone, see the
-[`bun-react-router`](examples/bun-react-router/README.md),
-[`react-router-simple`](examples/react-router-simple/README.md),
-[`tanstack-router-simple`](examples/tanstack-router-simple/README.md), and
-[`solid-router-simple`](examples/solid-router-simple/README.md) examples.
+Establishes some patterns to hook into the lifecycle of streaming SSR apps in a framework/library agnostic way. A
+handful of renderer plugins for common libraries are maintained in this repo.
-The goal of `@ssrx/vite` is to close the small gaps that prevent Vite from being a delightful building block for modern
-SSR apps, not to provide solutions for routing, deployment, etc.
-
-**It is:**
-
-- ✅ Framework agnostic on the client (use react, solid, etc)
-- ✅ Framework agnostic on the server (use node 18+, hono, h3, cloudflare, bun, deno, etc)
-- ✅ Simple "native" Vite - continue using `vite dev`, `vite build`, etc
-
-**It enables:**
-
-- Route based code-spliting with asset pre-loading
-- Typescript + HMR support on the client AND server
-- Elimates FOUC css issues during development
-- Generates a `ssr-manifest.json` file during build that maps client route urls -> assets
-- Provides a `assetsForRequest(url: string)` function on the server that returns a list of assets critical to the given
- request (along with preload links, etc)
-
-> ❗ A small disclaimer... SSRx intentionally does not try to do everything and is intended for a specific audience. If
-> you're looking for a full-fledged framework, SSRx might not be for you. If you are looking to build a modern SSR app
-> with your choice of 3rd party libraries for routing, head management, etc, then SSRx might be right for you.
-
-### Usage
-
-First, install deps via yarn, npm, etc, along these lines:
-
-```
-yarn add @ssrx/vite
-yarn add -D vite@5
-```
-
-`@ssrx/vite` is mostly unopinionated, but does require 3 things:
-
-#### Requirement 1 - a client entry file
-
-This file should mount your application in the browser. For React it might look something like this:
-
-```tsx
-import { hydrateRoot } from 'react-dom/client';
-
-import { App } from '~/app.tsx';
-
-hydrateRoot(document, );
-```
-
-#### Requirement 2 - a server entry file
-
-A server entry who's default export includes a `fetch` function that accepts a
-[Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and returns a
-[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object with your rendered or streamed app.
-
-> `@ssrx/vite` is focused on supporting the WinterCG standard. Modern node frameworks such as `Hono` and `h3`, as well
-> as alternative runtimes such as `bun`, `deno`, `cloudflare`, and more should all work well with this pattern.
-
-For React, it might look something like this:
-
-```tsx
-import { renderToString } from 'react-dom/server';
-
-import { App } from '~/app.tsx';
-
-export default {
- fetch(req: Request) {
- const html = renderToString();
-
- return new Response(html, {
- headers: {
- 'Content-Type': 'text/html',
- },
- });
- },
-};
-```
-
-#### Requirement 3 - a routes file
-
-Your routes file should export a `routes` object. By default `@ssrx/vite` expects the `routes` object to conform to the
-following shape:
-
-```ts
-type Route = {
- // path must adhere to the path-to-regex syntax
- path?: string;
- children?: Route[];
-
- // If lazy or component.preload point to a dynamic import, that route will be code split
- lazy?: () => Promise;
- component?: {
- preload?: () => Promise;
- };
-};
-```
-
-`react-router` and `solid-router` both conform to this shape out of the box. You can provide your own `routerAdapter` if
-your routes config does not - see [plugin-tanstack-router](packages/plugin-tanstack-router/README.md) for an example.
-
-#### Finally, update your vite.config.js
-
-Example:
-
-```ts
-import { ssrx } from '@ssrx/vite/plugin';
-import { defineConfig } from 'vite';
-
-export default defineConfig({
- plugins: [
- // ... your other plugins
-
- // The plugin, with all of it's defaults.
- // You only need to set these options if they deviate from the defaults.
- ssrx({
- routesFile: 'src/routes.tsx',
- clientEntry: 'src/entry.client.tsx',
- serverFile: 'src/server.ts',
- clientOutDir: 'dist/public',
- serverOutDir: 'dist',
- runtime: 'node',
- routerAdapter: defaultRouterAdapter,
- }),
- ],
-});
-```
-
-See [`bun-react-router`](examples/bun-react-router/README.md),
-[`react-router-simple`](examples/react-router-simple/README.md),
-[`tanstack-router-simple`](examples/tanstack-router-simple/README.md), and
-[`solid-router-simple`](examples/solid-router-simple/README.md) for more concrete examples.
-
-### Runtimes
-
-The `ssrx` vite plugin accepts a `runtime` option.
-
-Setting the value to `edge` will adjust vite to bundle the server output into a single file, and set resolve conditions
-more appropriate for ssr / server rendering in popular edge environments.
-
-Setting the value to `cf-pages` will adjust the output to be suitable for deployment to Cloudflare Pages, including
-generating sane `_routes.json` and `_headers` defaults.
-
-## `@ssrx/renderer`
-
-The SSRx renderer provides building blocks that make it easier to develop streaming SSR apps. It is client and server
-framework agnostic, so long as the server runtime supports web streams and AsyncLocalStorage (node 18+, bun, deno,
-cloudflare, vercel, etc).
-
-See the [react-router-kitchen-sink](examples/react-router-kitchen-sink/README.md) and
-[remix-vite](examples/remix-vite/README.md) examples for a look at how everything can work together in practice.
-
-### Directory
-
-| Package | Release Notes |
-| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| [@ssrx/renderer](packages/renderer) | [](packages/renderer/CHANGELOG.md) |
-| [@ssrx/react](packages/react) | [](packages/react/CHANGELOG.md) |
-| [@ssrx/remix](packages/remix) | [](packages/remix/CHANGELOG.md) |
-| [@ssrx/solid](packages/solid) | [](packages/solid/CHANGELOG.md) |
-| [@ssrx/streaming](packages/streaming) | [](packages/streaming/CHANGELOG.md) |
-| [@ssrx/trpc-react-query](packages/trpc-react-query) | [](packages/trpc-react-query/CHANGELOG.md) |
-| [@ssrx/plugin-react-router](packages/plugin-react-router) | [](packages/solid/CHANGELOG.md) |
-| [@ssrx/plugin-solid-router](packages/plugin-solid-router) | [](packages/plugin-solid-router/CHANGELOG.md) |
-| [@ssrx/plugin-tanstack-query](packages/plugin-tanstack-query) | [](packages/plugin-tanstack-query/CHANGELOG.md) |
-| [@ssrx/plugin-tanstack-router](packages/plugin-tanstack-router) | [](packages/plugin-tanstack-router/CHANGELOG.md) |
-| [@ssrx/plugin-trpc-react](packages/plugin-trpc-react) | [](packages/plugin-trpc-react/CHANGELOG.md) |
-| [@ssrx/plugin-unhead](packages/plugin-unhead) | [](packages/plugin-unhead/CHANGELOG.md) |
-
-### Usage
-
-`@ssrx/renderer` exports a `createApp` function that allows you to compose all the pieces necessary to render a SSR
-streamed application. For example:
-
-**app.tsx**
-
-```tsx
-// In this case we're using the `react` renderer, which simply wraps @ssrx/renderer with a react specific stream function
-import { createApp } from '@ssrx/react';
-import { assetsPlugin } from '@ssrx/renderer/assets';
-
-export const { clientHandler, serverHandler, ctx } = createApp({
- // Usually a router plugin will provide the appRenderer, but you can always provide your own if needed
- appRenderer:
- ({ req }) =>
- () =>
-
My App
,
-
- plugins: [
- // If you are also using `@ssrx/vite`, this plugin automatically injects js/css assets into your html stream
- assetsPlugin(),
-
- // ... your plugins, or 3rd party plugins. More on the plugin shape below
- ],
-});
-```
-
-**entry.client.tsx**
-
-```tsx
-import { hydrateRoot } from 'react-dom/client';
-
-import { clientHandler } from './app.tsx';
-
-void hydrate();
-
-async function hydrate() {
- const app = await clientHandler();
-
- hydrateRoot(document, app());
-}
-```
-
-**server.ts**
-
-```tsx
-import { serverHandler } from '~/app.tsx';
-
-export default {
- fetch(req: Request) {
- const { stream, statusCode } = await serverHandler({ req });
-
- return new Response(stream, { status: statusCode(), headers: { 'Content-Type': 'text/html' } });
- },
-};
-```
-
-With the above steps you get a streaming react app with support for lazy asset preloading. However, plugins are where
-`@ssrx/renderer` really shines.
-
-### Plugins
-
-Plugins can:
-
-- Hook into the client and server rendering in a standardized way
-- Extend a typesafe `ctx` object that is made available on the client and the server, even outside of the rendering tree
- (for example in router loader functions). This is accomplished via a proxy that is exposed on the window in the client
- context, and via async local storage on the server.
-
-**Plugin Shape**
-
-See the [renderer types](packages/renderer/src/types.ts) file for the full plugin signature.
-
-```ts
-export type RenderPlugin = {
- id: string;
-
- /**
- * Called once per request.
- */
- hooksForReq: (props: {
- req: Request;
- meta?: SSRx.ReqMeta;
- renderProps: SSRx.RenderProps;
- ctx: Record;
- }) => {
- // Called on the client and the server
- common?: {
- /**
- * Extend the app ctx object with additional properties. Consider this "external" context - it is made available
- * to the end application on the server and the client.
- */
- extendAppCtx?: () => Record;
-
- /**
- * Wrap the app component with a higher-order component. This is useful for wrapping the app with providers, etc.
- */
- wrapApp?: (props: { children: () => Config['jsxElement'] }) => Config['jsxElement'];
-
- /**
- * Render the final inner-most app component. Only one plugin may do this - usually a routing plugin.
- */
- renderApp?: () => (() => Config['jsxElement']) | Promise<() => Config['jsxElement']>;
- };
-
- // Only called on the server
- server?: {
- /**
- * Return a string to emit some HTML into the SSR stream just before the document's closing tag.
- *
- * Triggers once per request.
- */
- emitToDocumentHead?: Promise | string | undefined;
-
- /**
- * Return a string to emit into the SSR stream just before the rendering
- * framework (react, solid, etc) emits a chunk of the page.
- *
- * Triggers one or more times per request.
- */
- emitBeforeStreamChunk?: Promise | string | undefined;
-
- /**
- * Return a string to emit some HTML to the document body, after the client renderer's first flush.
- *
- * Triggers once per request.
- */
- emitToDocumentBody?: Promise | string | undefined;
-
- /**
- * Runs when the stream is done processing.
- */
- onStreamComplete?: Promise | void;
- };
- };
-};
-```
-
-## Inspiration
-
-Many thanks to these awesome libraries! Please check them out - they provided inspiration as I navigated my first Vite
-plugin.
-
-- https://github.com/Lomray-Software/vite-ssr-boost
-- https://github.com/nksaraf/vinxi
-- https://github.com/fastify/fastify-vite
-- https://github.com/honojs/vite-plugins/tree/main/packages/dev-server
+Use this if you are looking for ways to integrate libraries like Tanstack Query, Unhead, tRPC, etc, into your streaming
+SSR application.
diff --git a/packages/renderer/README.md b/packages/renderer/README.md
new file mode 100644
index 0000000..4e31fbd
--- /dev/null
+++ b/packages/renderer/README.md
@@ -0,0 +1,176 @@
+# `@ssrx/renderer`
+
+The SSRx renderer establishes some patterns to hook into the lifecycle of streaming SSR apps in a framework/library
+agnostic way. It is client and server framework agnostic, so long as the server runtime supports web streams and
+AsyncLocalStorage (node 18+, bun, deno, cloudflare, vercel, etc). A handful of renderer plugins for common libraries are
+maintained in this repo.
+
+See the [react-router-kitchen-sink](/examples/react-router-kitchen-sink/README.md) and
+[remix-vite](/examples/remix-vite/README.md) examples for a look at how everything can work together in practice.
+
+## Usage
+
+`@ssrx/renderer` exports a `createApp` function that allows you to compose all the pieces necessary to render a SSR
+streamed application.
+
+An example with React (Solid works almost exactly the same):
+
+
+src/app.tsx
+
+```tsx
+// In this case we're using the `react` renderer, which simply wraps @ssrx/renderer with a react specific stream function
+import { createApp } from '@ssrx/react';
+import { assetsPlugin } from '@ssrx/renderer/assets';
+
+export const { clientHandler, serverHandler, ctx } = createApp({
+ // Usually a router plugin will provide the appRenderer, but you can always provide your own if needed
+ appRenderer:
+ ({ req }) =>
+ () =>
+
My App
,
+
+ plugins: [
+ // If you are also using `@ssrx/vite`, this plugin automatically injects js/css assets into your html stream
+ assetsPlugin(),
+
+ // ... your plugins, or 3rd party plugins. More on the plugin shape below
+ ],
+});
+```
+
+
+
+
+src/entry.client.tsx
+
+```tsx
+import { hydrateRoot } from 'react-dom/client';
+
+import { clientHandler } from './app.tsx';
+
+void hydrate();
+
+async function hydrate() {
+ const app = await clientHandler();
+
+ hydrateRoot(document, app());
+}
+```
+
+
+
+
+src/server.ts
+
+```tsx
+import { serverHandler } from '~/app.tsx';
+
+export default {
+ fetch(req: Request) {
+ const { stream, statusCode } = await serverHandler({ req });
+
+ return new Response(stream, { status: statusCode(), headers: { 'Content-Type': 'text/html' } });
+ },
+};
+```
+
+
+
+With the above steps you get a streaming react app with support for lazy asset preloading. However, plugins are where
+`@ssrx/renderer` really shines.
+
+## Plugins
+
+Plugins can:
+
+- Hook into the client and server rendering in a standardized way
+- Extend a typesafe `ctx` object that is made available on the client and the server, even outside of the rendering tree
+ (for example in router loader functions). This is accomplished via a proxy that is exposed on the window in the client
+ context, and via async local storage on the server.
+
+**Plugin Shape**
+
+See the [renderer types](/packages/renderer/src/types.ts) file for the full plugin signature.
+
+```ts
+export type RenderPlugin = {
+ id: string;
+
+ /**
+ * Called once per request.
+ */
+ hooksForReq: (props: {
+ req: Request;
+ meta?: SSRx.ReqMeta;
+ renderProps: SSRx.RenderProps;
+ ctx: Record;
+ }) => {
+ // Called on the client and the server
+ common?: {
+ /**
+ * Extend the app ctx object with additional properties. Consider this "external" context - it is made available
+ * to the end application on the server and the client.
+ */
+ extendAppCtx?: () => Record;
+
+ /**
+ * Wrap the app component with a higher-order component. This is useful for wrapping the app with providers, etc.
+ */
+ wrapApp?: (props: { children: () => Config['jsxElement'] }) => Config['jsxElement'];
+
+ /**
+ * Render the final inner-most app component. Only one plugin may do this - usually a routing plugin.
+ */
+ renderApp?: () => (() => Config['jsxElement']) | Promise<() => Config['jsxElement']>;
+ };
+
+ // Only called on the server
+ server?: {
+ /**
+ * Return a string to emit some HTML into the SSR stream just before the document's closing tag.
+ *
+ * Triggers once per request.
+ */
+ emitToDocumentHead?: Promise | string | undefined;
+
+ /**
+ * Return a string to emit into the SSR stream just before the rendering
+ * framework (react, solid, etc) emits a chunk of the page.
+ *
+ * Triggers one or more times per request.
+ */
+ emitBeforeStreamChunk?: Promise | string | undefined;
+
+ /**
+ * Return a string to emit some HTML to the document body, after the client renderer's first flush.
+ *
+ * Triggers once per request.
+ */
+ emitToDocumentBody?: Promise | string | undefined;
+
+ /**
+ * Runs when the stream is done processing.
+ */
+ onStreamComplete?: Promise | void;
+ };
+ };
+};
+```
+
+## Directory
+
+| Package | Release Notes |
+| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [@ssrx/renderer](/packages/renderer) | [](/packages/renderer/CHANGELOG.md) |
+| [@ssrx/react](/packages/react) | [](/packages/react/CHANGELOG.md) |
+| [@ssrx/remix](/packages/remix) | [](/packages/remix/CHANGELOG.md) |
+| [@ssrx/solid](/packages/solid) | [](/packages/solid/CHANGELOG.md) |
+| [@ssrx/streaming](/packages/streaming) | [](/packages/streaming/CHANGELOG.md) |
+| [@ssrx/trpc-react-query](/packages/trpc-react-query) | [](/packages/trpc-react-query/CHANGELOG.md) |
+| [@ssrx/plugin-react-router](/packages/plugin-react-router) | [](/packages/solid/CHANGELOG.md) |
+| [@ssrx/plugin-solid-router](/packages/plugin-solid-router) | [](/packages/plugin-solid-router/CHANGELOG.md) |
+| [@ssrx/plugin-tanstack-query](/packages/plugin-tanstack-query) | [](/packages/plugin-tanstack-query/CHANGELOG.md) |
+| [@ssrx/plugin-tanstack-router](/packages/plugin-tanstack-router) | [](/packages/plugin-tanstack-router/CHANGELOG.md) |
+| [@ssrx/plugin-trpc-react](/packages/plugin-trpc-react) | [](/packages/plugin-trpc-react/CHANGELOG.md) |
+| [@ssrx/plugin-unhead](/packages/plugin-unhead) | [](/packages/plugin-unhead/CHANGELOG.md) |
diff --git a/packages/streaming/README.md b/packages/streaming/README.md
new file mode 100644
index 0000000..98962a7
--- /dev/null
+++ b/packages/streaming/README.md
@@ -0,0 +1,59 @@
+# `@ssrx/streaming`
+
+Exports a `injectToStream` function that gives the consumer several convenient hooks into the stream lifecycle.
+
+Expects web streams, not node streams (however, can pass a node web stream in).
+
+```tsx
+import { injectIntoStream } from '@ssrx/streaming';
+import { renderToReadableStream } from 'react-dom';
+
+import { MyApp } from './app.tsx';
+
+export const handler = (req: Request) => {
+ const originalStream = renderToReadableStream();
+
+ let counter = 0;
+
+ const stream = injectIntoStream(req, originalStream, {
+ /**
+ * Return a string to emit some HTML into the SSR stream just before the document's closing tag.
+ *
+ * Called once per request.
+ */
+ emitToDocumentHead: async () => {
+ return ``
+ };
+
+ /**
+ * Return a string to emit into the SSR stream just before the rendering
+ * framework (react, solid, etc) emits a chunk of the page.
+ *
+ * Called one or more times per request.
+ */
+ emitToDocumentBody: async () => {
+ return ``
+ };
+
+ /**
+ * Return a string to emit some HTML to the document body, after the client renderer's first flush.
+ *
+ * Called once per request.
+ */
+ emitToDocumentBody: async () => {
+ counter += 1;
+
+ return ``
+ };
+
+ /**
+ * Runs when the stream is done processing.
+ */
+ onStreamComplete: () => {
+ // cleanup, etc
+ };
+ })
+
+ return new Response(stream, { headers: { 'content-type': 'text/html' } });
+}
+```
diff --git a/packages/streaming/src/stream-injector.ts b/packages/streaming/src/stream-injector.ts
index ae355f2..bf0dc47 100644
--- a/packages/streaming/src/stream-injector.ts
+++ b/packages/streaming/src/stream-injector.ts
@@ -8,9 +8,31 @@ import {
} from './stream-utils.ts';
export type StreamInjectorHooks = {
+ /**
+ * Return a string to emit some HTML into the SSR stream just before the document's closing tag.
+ *
+ * Called once per request.
+ */
emitToDocumentHead?: () => Promise | string | undefined;
+
+ /**
+ * Return a string to emit into the SSR stream just before the rendering
+ * framework (react, solid, etc) emits a chunk of the page.
+ *
+ * Called one or more times per request.
+ */
emitBeforeStreamChunk?: () => Promise | string | undefined;
+
+ /**
+ * Return a string to emit some HTML to the document body, after the client renderer's first flush.
+ *
+ * Called once per request.
+ */
emitToDocumentBody?: () => Promise | string | undefined;
+
+ /**
+ * Runs when the stream is done processing.
+ */
onStreamComplete?: () => Promise | void;
};
diff --git a/packages/trpc-react-query/README.md b/packages/trpc-react-query/README.md
index 0bf893b..f74ae22 100644
--- a/packages/trpc-react-query/README.md
+++ b/packages/trpc-react-query/README.md
@@ -1,4 +1,4 @@
-# @ssrx/trpc-react
+# @ssrx/trpc-react-query
Similar https://github.com/trpc/trpc/tree/main/packages/react-query, with the following major changes:
diff --git a/packages/vite/README.md b/packages/vite/README.md
new file mode 100644
index 0000000..9803ba3
--- /dev/null
+++ b/packages/vite/README.md
@@ -0,0 +1,173 @@
+# `@ssrx/vite`
+
+A Vite plugin that improves the DX of developing SSR apps.
+
+**It is:**
+
+- ✅ Framework agnostic on the client (use react, solid, etc)
+- ✅ Framework agnostic on the server (use node 18+, hono, h3, cloudflare, bun, deno, etc)
+- ✅ Simple "native" Vite - continue using `vite dev`, `vite build`, etc
+
+**It enables:**
+
+- Route based code-spliting with asset pre-loading
+- Typescript + HMR support on the client AND server
+- Elimates FOUC css issues during development
+- Generates a `ssr-manifest.json` file during build that maps client route urls -> assets
+- Provides a `assetsForRequest(url: string)` function on the server that returns a list of assets critical to the given
+ request (along with preload links, etc). You can use this to inject the appropriate asset tags.
+
+> ❗ A small disclaimer... SSRx intentionally does not try to do everything and is intended for a specific audience. If
+> you're looking for a full-fledged framework, SSRx might not be for you. If you are looking to build a modern SSR app
+> with your choice of 3rd party libraries for routing, head management, etc, then SSRx might be right for you.
+
+> ❗ Remix is transitioning to Vite, so for Vite + React Router projects I now recommend Remix as the best-in-class
+> option.
+
+## Examples
+
+The SSRx Vite plugin is barebones and (mostly) unopinionated by design, and can be used standalone. See the
+[`bun-react-router`](/examples/bun-react-router/README.md),
+[`react-router-simple`](/examples/react-router-simple/README.md),
+[`tanstack-router-simple`](/examples/tanstack-router-simple/README.md), and
+[`solid-router-simple`](/examples/solid-router-simple/README.md) examples.
+
+## Usage
+
+`@ssrx/vite` is mostly unopinionated, but does require 3 things (the file locations are configurable, defaults below):
+
+
+Install deps via yarn, npm, etc
+
+```
+yarn add @ssrx/vite
+yarn add -D vite@5
+```
+
+
+
+
+Requirement #1 - a client entry file, `src/entry.client.tsx`
+
+This file should mount your application in the browser. For React it might look something like this:
+
+```tsx
+// src/entry.client.tsx
+
+import { hydrateRoot } from 'react-dom/client';
+
+import { App } from '~/app.tsx';
+
+hydrateRoot(document, );
+```
+
+
+
+
+Requirement #2 - a server file, `src/server.ts`
+
+A server entry who's default export includes a `fetch` function that accepts a
+[Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and returns a
+[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object with your rendered or streamed app.
+
+> `@ssrx/vite` is focused on supporting the WinterCG standard. Modern node frameworks such as `Hono` and `h3`, as well
+> as alternative runtimes such as `bun`, `deno`, `cloudflare`, and more should all work well with this pattern.
+
+For React, it might look something like this:
+
+```tsx
+// src/server.ts
+
+import { renderToString } from 'react-dom/server';
+
+import { App } from '~/app.tsx';
+
+export default {
+ fetch(req: Request) {
+ const html = renderToString();
+
+ return new Response(html, {
+ headers: {
+ 'Content-Type': 'text/html',
+ },
+ });
+ },
+};
+```
+
+
+
+
+Requirement #3 - a routes file, `src/routes.tsx`
+
+Your routes file should export a `routes` object. By default `@ssrx/vite` expects the `routes` object to conform to the
+following shape:
+
+```ts
+type Route = {
+ // path must adhere to the path-to-regex syntax
+ path?: string;
+ children?: Route[];
+
+ // If lazy or component.preload point to a dynamic import, that route will be code split
+ lazy?: () => Promise;
+ component?: {
+ preload?: () => Promise;
+ };
+};
+```
+
+`react-router` and `solid-router` both conform to this shape out of the box. You can provide your own `routerAdapter` if
+your routes config does not - see [plugin-tanstack-router](/packages/plugin-tanstack-router/README.md) for an example.
+
+
+
+
+Finally, update your vite.config.js
+
+Example:
+
+```ts
+import { ssrx } from '@ssrx/vite/plugin';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [
+ // ... your other plugins
+
+ // The plugin, with all of it's defaults.
+ // You only need to set these options if they deviate from the defaults.
+ ssrx({
+ routesFile: 'src/routes.tsx',
+ clientEntry: 'src/entry.client.tsx',
+ serverFile: 'src/server.ts',
+ clientOutDir: 'dist/public',
+ serverOutDir: 'dist',
+ runtime: 'node',
+ routerAdapter: defaultRouterAdapter,
+ }),
+ ],
+});
+```
+
+
+
+## Runtimes
+
+The `ssrx` vite plugin accepts a `runtime` option. The available values are:
+
+- `node` (default)
+- `edge`: adjusts Vite to bundle the server output into a single file, and sets resolve conditions that are more
+ appropriate for ssr / server rendering in popular edge environments.
+- `cf-pages`: adjust the output to be suitable for deployment to Cloudflare Pages, including generating sane
+ `_routes.json` and `_headers` defaults.
+
+## Inspiration
+
+Many thanks to these awesome libraries! Please check them out - they provided inspiration as I navigated my first Vite
+plugin.
+
+- https://github.com/Lomray-Software/vite-ssr-boost
+- https://github.com/nksaraf/vinxi
+- https://github.com/fastify/fastify-vite
+- https://github.com/honojs/vite-plugins/tree/main/packages/dev-server