@vertz/ui-server 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,453 +2,441 @@
2
2
 
3
3
  Server-side rendering (SSR) for `@vertz/ui`.
4
4
 
5
- ## Features
6
-
7
- - **Streaming HTML** — `renderToStream()` returns a `ReadableStream<Uint8Array>`
8
- - **Out-of-order streaming** — Suspense boundaries emit placeholders immediately, resolved content later
9
- - **Atomic hydration markers** — Interactive components get `data-v-id` attributes; static components ship zero JS
10
- - **Head management** — Collect `<title>`, `<meta>`, and `<link>` tags during render
11
- - **Asset injection** — Script and stylesheet helpers for the HTML head
12
- - **Critical CSS inlining** — Inline above-the-fold CSS with injection prevention
13
- - **CSP nonce support** — All inline scripts support Content Security Policy nonces
14
-
15
5
  ## Installation
16
6
 
17
7
  ```bash
18
8
  bun add @vertz/ui-server
19
9
  ```
20
10
 
21
- ## Usage
11
+ `vite` is a peer dependency (required for the dev server):
12
+
13
+ ```bash
14
+ bun add -d vite
15
+ ```
16
+
17
+ ## Quick Start
22
18
 
23
- ### Basic SSR
19
+ ### Render to HTML
20
+
21
+ The simplest way to server-render a Vertz app:
24
22
 
25
23
  ```typescript
26
- import { renderToStream } from '@vertz/ui-server';
27
- import type { VNode } from '@vertz/ui-server';
24
+ import { renderToHTML } from '@vertz/ui-server';
28
25
 
29
- const tree: VNode = {
30
- tag: 'html',
31
- attrs: { lang: 'en' },
32
- children: [
33
- {
34
- tag: 'head',
35
- attrs: {},
36
- children: [{ tag: 'title', attrs: {}, children: ['My App'] }],
37
- },
38
- {
39
- tag: 'body',
40
- attrs: {},
41
- children: [
42
- {
43
- tag: 'div',
44
- attrs: { id: 'app' },
45
- children: ['Hello, SSR!'],
46
- },
47
- ],
48
- },
49
- ],
50
- };
26
+ function App() {
27
+ return <h1>Hello, SSR!</h1>;
28
+ }
51
29
 
52
- const stream = renderToStream(tree);
30
+ const html = await renderToHTML(App, {
31
+ url: '/',
32
+ head: { title: 'My App' },
33
+ });
53
34
 
54
- // Return as HTTP response
55
- return new Response(stream, {
35
+ return new Response(html, {
56
36
  headers: { 'content-type': 'text/html; charset=utf-8' },
57
37
  });
58
38
  ```
59
39
 
60
- ### Streaming with Suspense
40
+ `renderToHTML` handles the DOM shim, theme compilation, styles, and head management automatically.
61
41
 
62
- ```typescript
63
- import { renderToStream } from '@vertz/ui-server';
64
- import type { VNode } from '@vertz/ui-server';
42
+ ### Dev Server
65
43
 
66
- // Create a Suspense boundary
67
- const suspenseNode = {
68
- tag: '__suspense',
69
- attrs: {},
70
- children: [],
71
- _fallback: { tag: 'div', attrs: { class: 'skeleton' }, children: ['Loading...'] },
72
- _resolve: fetchUserData().then((user) => ({
73
- tag: 'div',
74
- attrs: { class: 'user-profile' },
75
- children: [user.name],
76
- })),
77
- };
44
+ For local development with Vite HMR:
78
45
 
79
- const tree: VNode = {
80
- tag: 'div',
81
- attrs: { id: 'app' },
82
- children: [
83
- { tag: 'h1', attrs: {}, children: ['User Profile'] },
84
- suspenseNode as VNode,
85
- ],
86
- };
46
+ ```typescript
47
+ import { createDevServer } from '@vertz/ui-server';
87
48
 
88
- const stream = renderToStream(tree);
49
+ const server = createDevServer({
50
+ entry: './src/entry-server.ts',
51
+ port: 5173,
52
+ });
53
+
54
+ await server.listen();
89
55
  ```
90
56
 
91
- **How out-of-order streaming works:**
92
-
93
- 1. The initial stream contains the placeholder: `<div id="v-slot-0"><div class="skeleton">Loading...</div></div>`
94
- 2. When `_resolve` completes, a replacement chunk is streamed:
95
- ```html
96
- <template id="v-tmpl-0"><div class="user-profile">Alice</div></template>
97
- <script>
98
- (function(){
99
- var s=document.getElementById("v-slot-0");
100
- var t=document.getElementById("v-tmpl-0");
101
- if(s&&t){s.replaceWith(t.content.cloneNode(true));t.remove()}
102
- })()
103
- </script>
104
- ```
57
+ ---
105
58
 
106
- ### CSP Nonce Support
59
+ ## Rendering APIs
107
60
 
108
- For sites with strict Content Security Policies:
61
+ ### `renderToHTML(app, options)`
109
62
 
110
- ```typescript
111
- import { renderToStream } from '@vertz/ui-server';
63
+ Renders a component to a complete HTML document string. Handles DOM shim setup/teardown, theme compilation, style injection, and head management automatically.
112
64
 
113
- const nonce = crypto.randomUUID();
65
+ ```typescript
66
+ import { renderToHTML } from '@vertz/ui-server';
67
+ import { defineTheme } from '@vertz/ui';
114
68
 
115
- const stream = renderToStream(tree, { nonce });
69
+ const theme = defineTheme({
70
+ colors: { primary: { DEFAULT: '#3b82f6' } },
71
+ });
116
72
 
117
- return new Response(stream, {
118
- headers: {
119
- 'content-type': 'text/html; charset=utf-8',
120
- 'content-security-policy': `script-src 'nonce-${nonce}'`,
73
+ const html = await renderToHTML(App, {
74
+ url: '/dashboard',
75
+ theme,
76
+ styles: ['body { margin: 0; }'],
77
+ head: {
78
+ title: 'Dashboard',
79
+ meta: [{ name: 'description', content: 'App dashboard' }],
80
+ links: [{ rel: 'stylesheet', href: '/styles.css' }],
121
81
  },
82
+ container: '#app',
122
83
  });
123
84
  ```
124
85
 
125
- All inline `<script>` tags (Suspense replacement scripts) will include `nonce="..."`.
86
+ **Options:**
126
87
 
127
- ### Hydration Markers
88
+ | Option | Type | Description |
89
+ |---|---|---|
90
+ | `app` | `() => VNode` | App component function |
91
+ | `url` | `string` | Request URL for SSR routing |
92
+ | `theme` | `Theme` | Theme definition for CSS vars |
93
+ | `styles` | `string[]` | Global CSS strings to inject |
94
+ | `head` | `object` | Head config: `title`, `meta[]`, `links[]` |
95
+ | `container` | `string` | Container selector (default `'#app'`) |
128
96
 
129
- Interactive components get hydration markers so the client knows where to attach event handlers:
97
+ ### `renderPage(vnode, options?)`
98
+
99
+ Renders a VNode to a full HTML `Response` with doctype, head (meta, OG, Twitter, favicon, styles), body, and scripts.
130
100
 
131
101
  ```typescript
132
- import { wrapWithHydrationMarkers } from '@vertz/ui-server';
102
+ import { renderPage } from '@vertz/ui-server';
103
+
104
+ return renderPage(<App />, {
105
+ title: 'My App',
106
+ description: 'Built with Vertz',
107
+ og: { image: '/og.png', url: 'https://example.com' },
108
+ twitter: { card: 'summary_large_image' },
109
+ scripts: ['/app.js'],
110
+ styles: ['/app.css'],
111
+ });
112
+ ```
113
+
114
+ **Options:**
115
+
116
+ | Option | Type | Description |
117
+ |---|---|---|
118
+ | `status` | `number` | HTTP status code (default `200`) |
119
+ | `title` | `string` | Page title |
120
+ | `description` | `string` | Meta description |
121
+ | `lang` | `string` | HTML lang attribute (default `'en'`) |
122
+ | `favicon` | `string` | Favicon URL |
123
+ | `og` | `object` | Open Graph: `title`, `description`, `image`, `url`, `type` |
124
+ | `twitter` | `object` | Twitter card: `card`, `site` |
125
+ | `scripts` | `string[]` | Script URLs for end of body |
126
+ | `styles` | `string[]` | Stylesheet URLs for head |
127
+ | `head` | `string` | Raw HTML escape hatch for head |
128
+
129
+ ### `renderToStream(tree, options?)`
130
+
131
+ Low-level streaming renderer. Returns a `ReadableStream<Uint8Array>` that emits HTML as it's generated, including out-of-order Suspense resolution.
132
+
133
+ ```typescript
134
+ import { renderToStream } from '@vertz/ui-server';
133
135
  import type { VNode } from '@vertz/ui-server';
134
136
 
135
- const counterNode: VNode = {
137
+ const tree: VNode = {
136
138
  tag: 'div',
137
- attrs: {},
138
- children: [
139
- { tag: 'span', attrs: {}, children: ['Count: 0'] },
140
- { tag: 'button', attrs: {}, children: ['+'] },
141
- ],
139
+ attrs: { id: 'app' },
140
+ children: ['Hello, SSR!'],
142
141
  };
143
142
 
144
- const hydratedNode = wrapWithHydrationMarkers(counterNode, {
145
- componentName: 'Counter',
146
- key: 'counter-0',
147
- props: { initial: 0 },
148
- });
143
+ const stream = renderToStream(tree);
149
144
 
150
- const stream = renderToStream(hydratedNode);
145
+ return new Response(stream, {
146
+ headers: { 'content-type': 'text/html; charset=utf-8' },
147
+ });
151
148
  ```
152
149
 
153
- **Output:**
150
+ **Options:**
151
+ - `nonce?: string` — CSP nonce for inline scripts
154
152
 
155
- ```html
156
- <div data-v-id="Counter" data-v-key="counter-0">
157
- <span>Count: 0</span>
158
- <button>+</button>
159
- <script type="application/json">{"initial":0}</script>
160
- </div>
161
- ```
153
+ ### `serializeToHtml(node)`
162
154
 
163
- The hydration runtime on the client reads `data-v-id` and `data-v-key` to restore interactivity.
155
+ Synchronously serialize a VNode tree to an HTML string:
164
156
 
165
- ### Head Management
157
+ ```typescript
158
+ import { serializeToHtml } from '@vertz/ui-server';
166
159
 
167
- Collect `<title>`, `<meta>`, and `<link>` tags during render:
160
+ const html = serializeToHtml({
161
+ tag: 'div',
162
+ attrs: { class: 'card' },
163
+ children: ['Hello'],
164
+ });
165
+ // '<div class="card">Hello</div>'
166
+ ```
167
+
168
+ ### `rawHtml(html)`
169
+
170
+ Create a raw HTML string that bypasses escaping:
168
171
 
169
172
  ```typescript
170
- import { HeadCollector, renderHeadToHtml, rawHtml } from '@vertz/ui-server';
173
+ import { rawHtml } from '@vertz/ui-server';
171
174
 
172
- const headCollector = new HeadCollector();
173
- headCollector.addTitle('My SSR App');
174
- headCollector.addMeta({ charset: 'utf-8' });
175
- headCollector.addMeta({ name: 'viewport', content: 'width=device-width, initial-scale=1' });
176
- headCollector.addLink({ rel: 'stylesheet', href: '/styles.css' });
175
+ const node = rawHtml('<p>This HTML is <strong>not</strong> escaped.</p>');
176
+ ```
177
177
 
178
- const headHtml = renderHeadToHtml(headCollector.getEntries());
178
+ **Warning:** Only use `rawHtml()` with trusted content.
179
179
 
180
- const tree: VNode = {
181
- tag: 'html',
182
- attrs: { lang: 'en' },
183
- children: [
184
- {
185
- tag: 'head',
186
- attrs: {},
187
- children: [rawHtml(headHtml)],
188
- },
189
- {
190
- tag: 'body',
191
- attrs: {},
192
- children: [{ tag: 'div', attrs: { id: 'app' }, children: ['Content'] }],
193
- },
194
- ],
195
- };
180
+ ---
196
181
 
197
- const stream = renderToStream(tree);
198
- ```
182
+ ## DOM Shim
199
183
 
200
- ### Asset Injection
184
+ Import from `@vertz/ui-server/dom-shim`:
201
185
 
202
- Inject scripts and stylesheets into the HTML head:
186
+ The DOM shim provides `document.createElement`, `createTextNode`, etc. for SSR — allowing `@vertz/ui` components to work on the server without modification.
203
187
 
204
188
  ```typescript
205
- import { renderAssetTags } from '@vertz/ui-server';
206
- import type { AssetDescriptor } from '@vertz/ui-server';
189
+ import { installDomShim, removeDomShim, toVNode } from '@vertz/ui-server/dom-shim';
207
190
 
208
- const assets: AssetDescriptor[] = [
209
- { type: 'stylesheet', src: '/styles/main.css' },
210
- { type: 'script', src: '/js/runtime.js', defer: true },
211
- { type: 'script', src: '/js/app.js', defer: true },
212
- ];
191
+ // Install before rendering
192
+ installDomShim();
213
193
 
214
- const assetHtml = renderAssetTags(assets);
215
- // <link rel="stylesheet" href="/styles/main.css">
216
- // <script src="/js/runtime.js" defer></script>
217
- // <script src="/js/app.js" defer></script>
194
+ // Your component code can use document.createElement, etc.
195
+ const element = App();
196
+
197
+ // Convert SSR elements to VNodes for serialization
198
+ const vnode = toVNode(element);
199
+
200
+ // Clean up after rendering
201
+ removeDomShim();
218
202
  ```
219
203
 
220
- ### Critical CSS Inlining
204
+ **Note:** `renderToHTML` handles DOM shim setup and teardown automatically. You only need these when using lower-level rendering APIs.
221
205
 
222
- Inline above-the-fold CSS for faster First Contentful Paint:
206
+ | Export | Description |
207
+ |---|---|
208
+ | `installDomShim()` | Install the minimal DOM shim on `globalThis` |
209
+ | `removeDomShim()` | Remove the DOM shim from `globalThis` |
210
+ | `toVNode(element)` | Convert an SSR element to a VNode |
223
211
 
224
- ```typescript
225
- import { inlineCriticalCss, rawHtml } from '@vertz/ui-server';
212
+ ---
226
213
 
227
- const criticalCss = `
228
- body { margin: 0; font-family: system-ui, sans-serif; }
229
- .hero { padding: 2rem; background: linear-gradient(to right, #667eea, #764ba2); }
230
- `;
214
+ ## Head Management
231
215
 
232
- const styleTag = inlineCriticalCss(criticalCss);
233
- // <style>body { margin: 0; font-family: system-ui, sans-serif; } ...</style>
216
+ Collect `<title>`, `<meta>`, and `<link>` tags during render:
234
217
 
235
- const tree: VNode = {
236
- tag: 'html',
237
- attrs: {},
238
- children: [
239
- {
240
- tag: 'head',
241
- attrs: {},
242
- children: [rawHtml(styleTag)],
243
- },
244
- {
245
- tag: 'body',
246
- attrs: {},
247
- children: [{ tag: 'div', attrs: { class: 'hero' }, children: ['Hero Section'] }],
248
- },
249
- ],
250
- };
218
+ ```typescript
219
+ import { HeadCollector, renderHeadToHtml, rawHtml } from '@vertz/ui-server';
251
220
 
252
- const stream = renderToStream(tree);
221
+ const headCollector = new HeadCollector();
222
+ headCollector.addTitle('My SSR App');
223
+ headCollector.addMeta({ charset: 'utf-8' });
224
+ headCollector.addMeta({ name: 'viewport', content: 'width=device-width, initial-scale=1' });
225
+ headCollector.addLink({ rel: 'stylesheet', href: '/styles.css' });
226
+
227
+ const headHtml = renderHeadToHtml(headCollector.getEntries());
253
228
  ```
254
229
 
255
- The `inlineCriticalCss()` function escapes any `</style>` sequences in the CSS to prevent injection attacks.
230
+ **`HeadCollector` methods:**
231
+ - `addTitle(text)` — Add a `<title>` tag
232
+ - `addMeta(attrs)` — Add a `<meta>` tag
233
+ - `addLink(attrs)` — Add a `<link>` tag
234
+ - `getEntries()` — Get all collected `HeadEntry[]`
235
+ - `clear()` — Clear all entries
236
+
237
+ ---
256
238
 
257
- ### Raw HTML
239
+ ## Hydration Markers
258
240
 
259
- Embed pre-rendered HTML without escaping:
241
+ Interactive components get hydration markers so the client knows where to attach event handlers:
260
242
 
261
243
  ```typescript
262
- import { rawHtml } from '@vertz/ui-server';
244
+ import { wrapWithHydrationMarkers } from '@vertz/ui-server';
263
245
  import type { VNode } from '@vertz/ui-server';
264
246
 
265
- const tree: VNode = {
247
+ const counterNode: VNode = {
266
248
  tag: 'div',
267
249
  attrs: {},
268
250
  children: [
269
- rawHtml('<p>This HTML is <strong>not</strong> escaped.</p>'),
270
- 'This text is escaped.',
251
+ { tag: 'span', attrs: {}, children: ['Count: 0'] },
252
+ { tag: 'button', attrs: {}, children: ['+'] },
271
253
  ],
272
254
  };
273
255
 
274
- const stream = renderToStream(tree);
275
- // <div><p>This HTML is <strong>not</strong> escaped.</p>This text is escaped.</div>
256
+ const hydratedNode = wrapWithHydrationMarkers(counterNode, {
257
+ componentName: 'Counter',
258
+ key: 'counter-0',
259
+ props: { initial: 0 },
260
+ });
276
261
  ```
277
262
 
278
- **Warning:** Only use `rawHtml()` with trusted content. Embedding user-generated content without escaping is a security risk.
279
-
280
- ## API Reference
281
-
282
- ### `renderToStream(tree, options?)`
263
+ **Output:**
283
264
 
284
- Render a VNode tree to a `ReadableStream<Uint8Array>`.
265
+ ```html
266
+ <div data-v-id="Counter" data-v-key="counter-0">
267
+ <span>Count: 0</span>
268
+ <button>+</button>
269
+ <script type="application/json">{"initial":0}</script>
270
+ </div>
271
+ ```
285
272
 
286
- - **Parameters:**
287
- - `tree: VNode | string | RawHtml` — The virtual tree to render
288
- - `options?: RenderToStreamOptions` — Optional configuration
289
- - `nonce?: string` — CSP nonce for inline scripts
290
- - **Returns:** `ReadableStream<Uint8Array>`
273
+ ---
291
274
 
292
- ### `wrapWithHydrationMarkers(node, options)`
275
+ ## Assets
293
276
 
294
- Wrap a VNode with hydration markers for interactive components.
277
+ ### `renderAssetTags(assets)`
295
278
 
296
- - **Parameters:**
297
- - `node: VNode` — The component's root VNode
298
- - `options: HydrationOptions`
299
- - `componentName: string` — Component name for `data-v-id`
300
- - `key: string` — Unique key for `data-v-key`
301
- - `props?: Record<string, unknown>` — Serialized props
302
- - **Returns:** `VNode` — A new VNode with hydration attributes
279
+ Render asset descriptors to HTML tags:
303
280
 
304
- ### `HeadCollector`
281
+ ```typescript
282
+ import { renderAssetTags } from '@vertz/ui-server';
283
+ import type { AssetDescriptor } from '@vertz/ui-server';
305
284
 
306
- Collects `<head>` metadata during SSR.
285
+ const assets: AssetDescriptor[] = [
286
+ { type: 'stylesheet', src: '/styles/main.css' },
287
+ { type: 'script', src: '/js/runtime.js', defer: true },
288
+ { type: 'script', src: '/js/app.js', defer: true },
289
+ ];
307
290
 
308
- - **Methods:**
309
- - `addTitle(text: string)` — Add a `<title>` tag
310
- - `addMeta(attrs: Record<string, string>)` — Add a `<meta>` tag
311
- - `addLink(attrs: Record<string, string>)` — Add a `<link>` tag
312
- - `getEntries(): HeadEntry[]` — Get all collected entries
313
- - `clear()` — Clear all entries
291
+ const html = renderAssetTags(assets);
292
+ ```
314
293
 
315
- ### `renderHeadToHtml(entries)`
294
+ ### `inlineCriticalCss(css)`
316
295
 
317
- Render head entries to an HTML string.
296
+ Inline above-the-fold CSS as a `<style>` tag with injection prevention:
318
297
 
319
- - **Parameters:**
320
- - `entries: HeadEntry[]` Collected head entries
321
- - **Returns:** `string` — HTML string
298
+ ```typescript
299
+ import { inlineCriticalCss } from '@vertz/ui-server';
322
300
 
323
- ### `renderAssetTags(assets)`
301
+ const styleTag = inlineCriticalCss('body { margin: 0; font-family: system-ui; }');
302
+ // '<style>body { margin: 0; font-family: system-ui; }</style>'
303
+ ```
324
304
 
325
- Render asset descriptors to HTML tags.
305
+ ---
326
306
 
327
- - **Parameters:**
328
- - `assets: AssetDescriptor[]` — Script/stylesheet descriptors
329
- - **Returns:** `string` — HTML string
307
+ ## Streaming & Suspense
330
308
 
331
- ### `inlineCriticalCss(css)`
309
+ ### Out-of-Order Streaming
332
310
 
333
- Inline critical CSS as a `<style>` tag.
311
+ Suspense boundaries emit placeholders immediately. When the async content resolves, a replacement chunk is streamed:
334
312
 
335
- - **Parameters:**
336
- - `css: string` — CSS content
337
- - **Returns:** `string` — `<style>...</style>` or empty string
313
+ ```typescript
314
+ const suspenseNode = {
315
+ tag: '__suspense',
316
+ attrs: {},
317
+ children: [],
318
+ _fallback: { tag: 'div', attrs: { class: 'skeleton' }, children: ['Loading...'] },
319
+ _resolve: fetchUserData().then((user) => ({
320
+ tag: 'div',
321
+ attrs: { class: 'user-profile' },
322
+ children: [user.name],
323
+ })),
324
+ };
338
325
 
339
- ### `rawHtml(html)`
326
+ const stream = renderToStream(suspenseNode as VNode);
327
+ ```
340
328
 
341
- Create a raw HTML string that bypasses escaping.
329
+ The stream first emits the fallback, then streams a `<template>` + `<script>` that swaps in the resolved content.
342
330
 
343
- - **Parameters:**
344
- - `html: string` — Pre-rendered HTML
345
- - **Returns:** `RawHtml` — Raw HTML object
331
+ ### CSP Nonce Support
346
332
 
347
- ### `serializeToHtml(node)`
333
+ All inline scripts support Content Security Policy nonces:
348
334
 
349
- Serialize a VNode tree to an HTML string (synchronous).
335
+ ```typescript
336
+ const nonce = crypto.randomUUID();
337
+ const stream = renderToStream(tree, { nonce });
350
338
 
351
- - **Parameters:**
352
- - `node: VNode | string | RawHtml` — The virtual tree to serialize
353
- - **Returns:** `string` — HTML string
339
+ return new Response(stream, {
340
+ headers: {
341
+ 'content-type': 'text/html; charset=utf-8',
342
+ 'content-security-policy': `script-src 'nonce-${nonce}'`,
343
+ },
344
+ });
345
+ ```
354
346
 
355
- ### Utilities
347
+ ---
356
348
 
357
- - `streamToString(stream: ReadableStream<Uint8Array>): Promise<string>` — Convert stream to string (for testing)
358
- - `collectStreamChunks(stream: ReadableStream<Uint8Array>): Promise<string[]>` — Collect stream chunks as array (for testing)
359
- - `encodeChunk(html: string): Uint8Array` — Encode string to UTF-8 chunk
349
+ ## Dev Server
360
350
 
361
- ## Types
351
+ `createDevServer` provides a turnkey Vite SSR development server with HMR, module invalidation, and error stack fixing.
362
352
 
363
- ### `VNode`
353
+ ```typescript
354
+ import { createDevServer } from '@vertz/ui-server';
364
355
 
365
- Virtual node representing an HTML element.
356
+ const server = createDevServer({
357
+ entry: './src/entry-server.ts',
358
+ port: 5173,
359
+ });
366
360
 
367
- ```typescript
368
- interface VNode {
369
- tag: string;
370
- attrs: Record<string, string>;
371
- children: (VNode | string | RawHtml)[];
372
- }
361
+ await server.listen();
373
362
  ```
374
363
 
375
- ### `RawHtml`
364
+ **Options:**
376
365
 
377
- Raw HTML that bypasses escaping.
366
+ | Option | Type | Default | Description |
367
+ |---|---|---|---|
368
+ | `entry` | `string` | (required) | Path to the SSR entry module |
369
+ | `port` | `number` | `5173` | Port to listen on |
370
+ | `host` | `string` | `'0.0.0.0'` | Host to bind to |
371
+ | `viteConfig` | `InlineConfig` | `{}` | Custom Vite configuration |
372
+ | `middleware` | `function` | — | Custom middleware before SSR handler |
373
+ | `skipModuleInvalidation` | `boolean` | `false` | Skip invalidating modules on each request |
374
+ | `logRequests` | `boolean` | `true` | Log requests to console |
378
375
 
379
- ```typescript
380
- interface RawHtml {
381
- __raw: true;
382
- html: string;
383
- }
384
- ```
376
+ **`DevServer` interface:**
385
377
 
386
- ### `HydrationOptions`
378
+ | Property/Method | Description |
379
+ |---|---|
380
+ | `listen()` | Start the server |
381
+ | `close()` | Stop the server |
382
+ | `vite` | The underlying `ViteDevServer` |
383
+ | `httpServer` | The underlying `http.Server` |
387
384
 
388
- Options for hydration marker generation.
385
+ The entry module should export a `renderToString(url: string)` function that returns HTML.
389
386
 
390
- ```typescript
391
- interface HydrationOptions {
392
- componentName: string;
393
- key: string;
394
- props?: Record<string, unknown>;
395
- }
396
- ```
387
+ ---
397
388
 
398
- ### `HeadEntry`
389
+ ## JSX Runtime
399
390
 
400
- Metadata entry for the HTML head.
391
+ The `@vertz/ui-server/jsx-runtime` subpath provides a server-side JSX factory that produces VNode trees compatible with `renderToStream`. During SSR, Vite's `ssrLoadModule` swaps this in automatically.
401
392
 
402
- ```typescript
403
- interface HeadEntry {
404
- tag: 'title' | 'meta' | 'link';
405
- attrs?: Record<string, string>;
406
- textContent?: string;
407
- }
408
- ```
393
+ | Export | Description |
394
+ |---|---|
395
+ | `jsx` | JSX factory for single-child elements |
396
+ | `jsxs` | JSX factory for multi-child elements |
397
+ | `Fragment` | Fragment component |
409
398
 
410
- ### `AssetDescriptor`
399
+ ---
411
400
 
412
- Asset descriptor for script/stylesheet injection.
401
+ ## Types
413
402
 
414
403
  ```typescript
415
- interface AssetDescriptor {
416
- type: 'script' | 'stylesheet';
417
- src: string;
418
- async?: boolean; // scripts only
419
- defer?: boolean; // scripts only
420
- }
421
- ```
422
-
423
- ### `RenderToStreamOptions`
404
+ import type {
405
+ // Core
406
+ VNode,
407
+ RawHtml,
424
408
 
425
- Options for `renderToStream()`.
409
+ // Rendering
410
+ RenderToHTMLOptions,
411
+ RenderToStreamOptions,
412
+ PageOptions,
426
413
 
427
- ```typescript
428
- interface RenderToStreamOptions {
429
- nonce?: string; // CSP nonce for inline scripts
430
- }
431
- ```
414
+ // Dev Server
415
+ DevServerOptions,
416
+ DevServer,
432
417
 
433
- ## Testing
418
+ // Head
419
+ HeadEntry,
434
420
 
435
- Run the test suite:
421
+ // Hydration
422
+ HydrationOptions,
436
423
 
437
- ```bash
438
- bun run test
424
+ // Assets
425
+ AssetDescriptor,
426
+ } from '@vertz/ui-server';
439
427
  ```
440
428
 
441
- Run tests in watch mode:
429
+ ---
442
430
 
443
- ```bash
444
- bun run test:watch
445
- ```
431
+ ## Utilities
446
432
 
447
- Type check:
433
+ | Export | Description |
434
+ |---|---|
435
+ | `streamToString(stream)` | Convert a `ReadableStream<Uint8Array>` to a string |
436
+ | `collectStreamChunks(stream)` | Collect stream chunks as a `string[]` |
437
+ | `encodeChunk(html)` | Encode a string to a `Uint8Array` chunk |
448
438
 
449
- ```bash
450
- bun run typecheck
451
- ```
439
+ ---
452
440
 
453
441
  ## License
454
442