@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 +298 -310
- package/dist/bun-dev-server.d.ts +115 -0
- package/dist/bun-dev-server.js +2032 -0
- package/dist/bun-plugin/fast-refresh-dom-state.d.ts +51 -0
- package/dist/bun-plugin/fast-refresh-dom-state.js +10 -0
- package/dist/bun-plugin/fast-refresh-runtime.d.ts +43 -0
- package/dist/bun-plugin/fast-refresh-runtime.js +150 -0
- package/dist/bun-plugin/index.d.ts +44 -0
- package/dist/bun-plugin/index.js +197 -0
- package/dist/dom-shim/index.d.ts +37 -6
- package/dist/dom-shim/index.js +12 -324
- package/dist/index.d.ts +331 -64
- package/dist/index.js +285 -292
- package/dist/jsx-runtime/index.js +15 -2
- package/dist/shared/chunk-2qsqp9xj.js +150 -0
- package/dist/shared/chunk-32688jav.js +564 -0
- package/dist/shared/chunk-4t0ekdyv.js +513 -0
- package/dist/shared/chunk-eb80r8e8.js +4 -0
- package/dist/ssr/index.d.ts +86 -0
- package/dist/ssr/index.js +11 -0
- package/package.json +35 -18
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
|
-
|
|
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
|
-
###
|
|
19
|
+
### Render to HTML
|
|
20
|
+
|
|
21
|
+
The simplest way to server-render a Vertz app:
|
|
24
22
|
|
|
25
23
|
```typescript
|
|
26
|
-
import {
|
|
27
|
-
import type { VNode } from '@vertz/ui-server';
|
|
24
|
+
import { renderToHTML } from '@vertz/ui-server';
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
30
|
+
const html = await renderToHTML(App, {
|
|
31
|
+
url: '/',
|
|
32
|
+
head: { title: 'My App' },
|
|
33
|
+
});
|
|
53
34
|
|
|
54
|
-
|
|
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
|
-
|
|
40
|
+
`renderToHTML` handles the DOM shim, theme compilation, styles, and head management automatically.
|
|
61
41
|
|
|
62
|
-
|
|
63
|
-
import { renderToStream } from '@vertz/ui-server';
|
|
64
|
-
import type { VNode } from '@vertz/ui-server';
|
|
42
|
+
### Dev Server
|
|
65
43
|
|
|
66
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
|
49
|
+
const server = createDevServer({
|
|
50
|
+
entry: './src/entry-server.ts',
|
|
51
|
+
port: 5173,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await server.listen();
|
|
89
55
|
```
|
|
90
56
|
|
|
91
|
-
|
|
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
|
-
|
|
59
|
+
## Rendering APIs
|
|
107
60
|
|
|
108
|
-
|
|
61
|
+
### `renderToHTML(app, options)`
|
|
109
62
|
|
|
110
|
-
|
|
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
|
-
|
|
65
|
+
```typescript
|
|
66
|
+
import { renderToHTML } from '@vertz/ui-server';
|
|
67
|
+
import { defineTheme } from '@vertz/ui';
|
|
114
68
|
|
|
115
|
-
const
|
|
69
|
+
const theme = defineTheme({
|
|
70
|
+
colors: { primary: { DEFAULT: '#3b82f6' } },
|
|
71
|
+
});
|
|
116
72
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
86
|
+
**Options:**
|
|
126
87
|
|
|
127
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
145
|
-
componentName: 'Counter',
|
|
146
|
-
key: 'counter-0',
|
|
147
|
-
props: { initial: 0 },
|
|
148
|
-
});
|
|
143
|
+
const stream = renderToStream(tree);
|
|
149
144
|
|
|
150
|
-
|
|
145
|
+
return new Response(stream, {
|
|
146
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
147
|
+
});
|
|
151
148
|
```
|
|
152
149
|
|
|
153
|
-
**
|
|
150
|
+
**Options:**
|
|
151
|
+
- `nonce?: string` — CSP nonce for inline scripts
|
|
154
152
|
|
|
155
|
-
|
|
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
|
-
|
|
155
|
+
Synchronously serialize a VNode tree to an HTML string:
|
|
164
156
|
|
|
165
|
-
|
|
157
|
+
```typescript
|
|
158
|
+
import { serializeToHtml } from '@vertz/ui-server';
|
|
166
159
|
|
|
167
|
-
|
|
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 {
|
|
173
|
+
import { rawHtml } from '@vertz/ui-server';
|
|
171
174
|
|
|
172
|
-
const
|
|
173
|
-
|
|
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
|
-
|
|
178
|
+
**Warning:** Only use `rawHtml()` with trusted content.
|
|
179
179
|
|
|
180
|
-
|
|
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
|
-
|
|
198
|
-
```
|
|
182
|
+
## DOM Shim
|
|
199
183
|
|
|
200
|
-
|
|
184
|
+
Import from `@vertz/ui-server/dom-shim`:
|
|
201
185
|
|
|
202
|
-
|
|
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 {
|
|
206
|
-
import type { AssetDescriptor } from '@vertz/ui-server';
|
|
189
|
+
import { installDomShim, removeDomShim, toVNode } from '@vertz/ui-server/dom-shim';
|
|
207
190
|
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
//
|
|
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
|
-
|
|
204
|
+
**Note:** `renderToHTML` handles DOM shim setup and teardown automatically. You only need these when using lower-level rendering APIs.
|
|
221
205
|
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
import { inlineCriticalCss, rawHtml } from '@vertz/ui-server';
|
|
212
|
+
---
|
|
226
213
|
|
|
227
|
-
|
|
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
|
-
|
|
233
|
-
// <style>body { margin: 0; font-family: system-ui, sans-serif; } ...</style>
|
|
216
|
+
Collect `<title>`, `<meta>`, and `<link>` tags during render:
|
|
234
217
|
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
239
|
+
## Hydration Markers
|
|
258
240
|
|
|
259
|
-
|
|
241
|
+
Interactive components get hydration markers so the client knows where to attach event handlers:
|
|
260
242
|
|
|
261
243
|
```typescript
|
|
262
|
-
import {
|
|
244
|
+
import { wrapWithHydrationMarkers } from '@vertz/ui-server';
|
|
263
245
|
import type { VNode } from '@vertz/ui-server';
|
|
264
246
|
|
|
265
|
-
const
|
|
247
|
+
const counterNode: VNode = {
|
|
266
248
|
tag: 'div',
|
|
267
249
|
attrs: {},
|
|
268
250
|
children: [
|
|
269
|
-
|
|
270
|
-
'
|
|
251
|
+
{ tag: 'span', attrs: {}, children: ['Count: 0'] },
|
|
252
|
+
{ tag: 'button', attrs: {}, children: ['+'] },
|
|
271
253
|
],
|
|
272
254
|
};
|
|
273
255
|
|
|
274
|
-
const
|
|
275
|
-
|
|
256
|
+
const hydratedNode = wrapWithHydrationMarkers(counterNode, {
|
|
257
|
+
componentName: 'Counter',
|
|
258
|
+
key: 'counter-0',
|
|
259
|
+
props: { initial: 0 },
|
|
260
|
+
});
|
|
276
261
|
```
|
|
277
262
|
|
|
278
|
-
**
|
|
279
|
-
|
|
280
|
-
## API Reference
|
|
281
|
-
|
|
282
|
-
### `renderToStream(tree, options?)`
|
|
263
|
+
**Output:**
|
|
283
264
|
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
+
## Assets
|
|
293
276
|
|
|
294
|
-
|
|
277
|
+
### `renderAssetTags(assets)`
|
|
295
278
|
|
|
296
|
-
|
|
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
|
-
|
|
281
|
+
```typescript
|
|
282
|
+
import { renderAssetTags } from '@vertz/ui-server';
|
|
283
|
+
import type { AssetDescriptor } from '@vertz/ui-server';
|
|
305
284
|
|
|
306
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
### `
|
|
294
|
+
### `inlineCriticalCss(css)`
|
|
316
295
|
|
|
317
|
-
|
|
296
|
+
Inline above-the-fold CSS as a `<style>` tag with injection prevention:
|
|
318
297
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
- **Returns:** `string` — HTML string
|
|
298
|
+
```typescript
|
|
299
|
+
import { inlineCriticalCss } from '@vertz/ui-server';
|
|
322
300
|
|
|
323
|
-
|
|
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
|
-
|
|
305
|
+
---
|
|
326
306
|
|
|
327
|
-
|
|
328
|
-
- `assets: AssetDescriptor[]` — Script/stylesheet descriptors
|
|
329
|
-
- **Returns:** `string` — HTML string
|
|
307
|
+
## Streaming & Suspense
|
|
330
308
|
|
|
331
|
-
###
|
|
309
|
+
### Out-of-Order Streaming
|
|
332
310
|
|
|
333
|
-
|
|
311
|
+
Suspense boundaries emit placeholders immediately. When the async content resolves, a replacement chunk is streamed:
|
|
334
312
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
326
|
+
const stream = renderToStream(suspenseNode as VNode);
|
|
327
|
+
```
|
|
340
328
|
|
|
341
|
-
|
|
329
|
+
The stream first emits the fallback, then streams a `<template>` + `<script>` that swaps in the resolved content.
|
|
342
330
|
|
|
343
|
-
|
|
344
|
-
- `html: string` — Pre-rendered HTML
|
|
345
|
-
- **Returns:** `RawHtml` — Raw HTML object
|
|
331
|
+
### CSP Nonce Support
|
|
346
332
|
|
|
347
|
-
|
|
333
|
+
All inline scripts support Content Security Policy nonces:
|
|
348
334
|
|
|
349
|
-
|
|
335
|
+
```typescript
|
|
336
|
+
const nonce = crypto.randomUUID();
|
|
337
|
+
const stream = renderToStream(tree, { nonce });
|
|
350
338
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
-
|
|
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
|
-
|
|
347
|
+
---
|
|
356
348
|
|
|
357
|
-
|
|
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
|
-
|
|
351
|
+
`createDevServer` provides a turnkey Vite SSR development server with HMR, module invalidation, and error stack fixing.
|
|
362
352
|
|
|
363
|
-
|
|
353
|
+
```typescript
|
|
354
|
+
import { createDevServer } from '@vertz/ui-server';
|
|
364
355
|
|
|
365
|
-
|
|
356
|
+
const server = createDevServer({
|
|
357
|
+
entry: './src/entry-server.ts',
|
|
358
|
+
port: 5173,
|
|
359
|
+
});
|
|
366
360
|
|
|
367
|
-
|
|
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
|
-
|
|
364
|
+
**Options:**
|
|
376
365
|
|
|
377
|
-
|
|
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
|
-
|
|
380
|
-
interface RawHtml {
|
|
381
|
-
__raw: true;
|
|
382
|
-
html: string;
|
|
383
|
-
}
|
|
384
|
-
```
|
|
376
|
+
**`DevServer` interface:**
|
|
385
377
|
|
|
386
|
-
|
|
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
|
-
|
|
385
|
+
The entry module should export a `renderToString(url: string)` function that returns HTML.
|
|
389
386
|
|
|
390
|
-
|
|
391
|
-
interface HydrationOptions {
|
|
392
|
-
componentName: string;
|
|
393
|
-
key: string;
|
|
394
|
-
props?: Record<string, unknown>;
|
|
395
|
-
}
|
|
396
|
-
```
|
|
387
|
+
---
|
|
397
388
|
|
|
398
|
-
|
|
389
|
+
## JSX Runtime
|
|
399
390
|
|
|
400
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
399
|
+
---
|
|
411
400
|
|
|
412
|
-
|
|
401
|
+
## Types
|
|
413
402
|
|
|
414
403
|
```typescript
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
defer?: boolean; // scripts only
|
|
420
|
-
}
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
### `RenderToStreamOptions`
|
|
404
|
+
import type {
|
|
405
|
+
// Core
|
|
406
|
+
VNode,
|
|
407
|
+
RawHtml,
|
|
424
408
|
|
|
425
|
-
|
|
409
|
+
// Rendering
|
|
410
|
+
RenderToHTMLOptions,
|
|
411
|
+
RenderToStreamOptions,
|
|
412
|
+
PageOptions,
|
|
426
413
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
```
|
|
414
|
+
// Dev Server
|
|
415
|
+
DevServerOptions,
|
|
416
|
+
DevServer,
|
|
432
417
|
|
|
433
|
-
|
|
418
|
+
// Head
|
|
419
|
+
HeadEntry,
|
|
434
420
|
|
|
435
|
-
|
|
421
|
+
// Hydration
|
|
422
|
+
HydrationOptions,
|
|
436
423
|
|
|
437
|
-
|
|
438
|
-
|
|
424
|
+
// Assets
|
|
425
|
+
AssetDescriptor,
|
|
426
|
+
} from '@vertz/ui-server';
|
|
439
427
|
```
|
|
440
428
|
|
|
441
|
-
|
|
429
|
+
---
|
|
442
430
|
|
|
443
|
-
|
|
444
|
-
bun run test:watch
|
|
445
|
-
```
|
|
431
|
+
## Utilities
|
|
446
432
|
|
|
447
|
-
|
|
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
|
-
|
|
450
|
-
bun run typecheck
|
|
451
|
-
```
|
|
439
|
+
---
|
|
452
440
|
|
|
453
441
|
## License
|
|
454
442
|
|