@bgub/fig-server 0.0.1
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/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +1429 -0
- package/dist/index.js.map +1 -0
- package/dist/payload.d.ts +258 -0
- package/dist/payload.js +1700 -0
- package/dist/payload.js.map +1 -0
- package/dist/shared-CQn6Dje6.js +105 -0
- package/dist/shared-CQn6Dje6.js.map +1 -0
- package/dist/types-4X0JT0wt.d.ts +83 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Ben Gubler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# @bgub/fig-server
|
|
2
|
+
|
|
3
|
+
Fig server renderer.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @bgub/fig-server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Fig packages are ESM-only and require Node `^20.19.0 || >=22.12.0` for Node runtime entry
|
|
12
|
+
points.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import {
|
|
18
|
+
prerender,
|
|
19
|
+
renderToDocumentHtml,
|
|
20
|
+
renderToDocumentStream,
|
|
21
|
+
renderToStream,
|
|
22
|
+
renderToHtml,
|
|
23
|
+
} from "@bgub/fig-server";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const result = renderToDocumentStream(
|
|
28
|
+
<html>
|
|
29
|
+
<head>
|
|
30
|
+
<meta charset="utf-8" />
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<App />
|
|
34
|
+
</body>
|
|
35
|
+
</html>,
|
|
36
|
+
);
|
|
37
|
+
await result.shellReady;
|
|
38
|
+
|
|
39
|
+
return new Response(result.stream, {
|
|
40
|
+
headers: { "content-type": result.contentType },
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Fig server rendering intentionally uses Web `ReadableStream`s instead of
|
|
45
|
+
Node-specific streams. Web streams give Fig one streaming API for modern Node,
|
|
46
|
+
edge runtimes, Deno, Bun, and browser-like hosts; Node pipeable stream helpers
|
|
47
|
+
would be compatibility adapters rather than the primary SSR surface.
|
|
48
|
+
|
|
49
|
+
`renderToDocumentStream` is the primary SSR API when Fig owns the whole
|
|
50
|
+
document. The root must render an `<html>` element with a `<head>`. Fig prepends
|
|
51
|
+
`<!doctype html>`, injects collected head resources before `</head>`, and then
|
|
52
|
+
streams the body with Suspense updates.
|
|
53
|
+
|
|
54
|
+
`renderToStream` is the lower-level fragment/body API. It returns a
|
|
55
|
+
`ServerFragmentRenderResult` with a Web `ReadableStream`, `headReady`,
|
|
56
|
+
`shellReady`, and `allReady` promises, a `getHead()` method, a content type, and
|
|
57
|
+
an abort handle. Pending Suspense boundaries stream their fallback in the shell,
|
|
58
|
+
then receive completed content through nonce-compatible inline scripts. Aborting
|
|
59
|
+
after the shell flushes the affected boundaries as client-rendered fallbacks.
|
|
60
|
+
|
|
61
|
+
`renderToHtml` buffers the streamed output: it waits for `allReady` and
|
|
62
|
+
collects the readable stream:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const html = await renderToHtml(<App />);
|
|
66
|
+
const documentHtml = await renderToDocumentHtml(
|
|
67
|
+
<html>
|
|
68
|
+
<head />
|
|
69
|
+
<body>
|
|
70
|
+
<App />
|
|
71
|
+
</body>
|
|
72
|
+
</html>,
|
|
73
|
+
);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`prerender` is the settled static renderer for SSG, email, RSS, Open Graph
|
|
77
|
+
markup, and tests that should not record streaming protocol details. It waits
|
|
78
|
+
for all async work before emitting HTML, so fulfilled Suspense boundaries render
|
|
79
|
+
their completed content in logical position with no fallback, staging container,
|
|
80
|
+
or `__figSSR` reveal script. Suspense boundaries that fail on the server render
|
|
81
|
+
their fallback with a static client-render marker so hydratable pages can retry
|
|
82
|
+
that boundary on the client.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const result = await prerender(<App />, { signal });
|
|
86
|
+
|
|
87
|
+
result.html;
|
|
88
|
+
result.head; // fragment-mode head assets
|
|
89
|
+
result.data; // data-resource hydration entries
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For document output, pass `document: true`; the root must render an `<html>`
|
|
93
|
+
document with a `<head>`, and collected head assets are inlined into that
|
|
94
|
+
document head:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const result = await prerender(
|
|
98
|
+
<html>
|
|
99
|
+
<head />
|
|
100
|
+
<body>
|
|
101
|
+
<App />
|
|
102
|
+
</body>
|
|
103
|
+
</html>,
|
|
104
|
+
{ document: true },
|
|
105
|
+
);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Pass `identifierPrefix` when multiple streaming renders share a document. Fig
|
|
109
|
+
uses it in generated Suspense marker and script identifiers, and it defaults to
|
|
110
|
+
an empty string.
|
|
111
|
+
|
|
112
|
+
## Data Resources
|
|
113
|
+
|
|
114
|
+
Server render requests have their own data-resource store. Reads through
|
|
115
|
+
`@bgub/fig` dedupe by key within the request and fulfilled entries are
|
|
116
|
+
available through `result.getData()`:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { readData } from "@bgub/fig";
|
|
120
|
+
import { serverDataResource } from "@bgub/fig/server";
|
|
121
|
+
import { renderToStream } from "@bgub/fig-server";
|
|
122
|
+
|
|
123
|
+
const userResource = serverDataResource<[string], { name: string }>({
|
|
124
|
+
key: (id) => ["user", id],
|
|
125
|
+
load: async (id, { signal }) => fetchUser(id, signal),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
function Profile({ id }: { id: string }) {
|
|
129
|
+
const user = readData(userResource, id);
|
|
130
|
+
return <h1>{user.name}</h1>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const result = renderToStream(<Profile id="one" />);
|
|
134
|
+
await result.allReady;
|
|
135
|
+
|
|
136
|
+
const initialData = result.getData();
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Pass `initialData` to `createRoot(...)` or `hydrateRoot(...)` on the client to
|
|
140
|
+
hydrate those values by key. Client imports can use a loader-less resource with
|
|
141
|
+
the same key; if no client loader exists, `refreshData(...)` reports
|
|
142
|
+
`unsupported` and a framework/payload refresh path should revalidate the key.
|
|
143
|
+
|
|
144
|
+
## Resources
|
|
145
|
+
|
|
146
|
+
Use `assets([...], children)` from `@bgub/fig` to attach document resources to
|
|
147
|
+
a subtree:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
import { assets, stylesheet, title } from "@bgub/fig";
|
|
151
|
+
|
|
152
|
+
function Page() {
|
|
153
|
+
return assets(
|
|
154
|
+
[title("Fig"), stylesheet("/app.css", { precedence: "app" })],
|
|
155
|
+
<main>Ready</main>,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The document renderer keeps head-only metadata separate from body segment HTML
|
|
161
|
+
and injects `title()` and `meta()` before `</head>`. With the lower-level
|
|
162
|
+
`renderToStream`, those tags are available through `result.getHead()`
|
|
163
|
+
after `headReady`; they are never emitted into segment HTML. `headReady`
|
|
164
|
+
resolves with the shell and seals the initial document head; `getHead()` keeps
|
|
165
|
+
returning that sealed snapshot afterward. If a new head resource is found
|
|
166
|
+
later, such as behind pending Suspense, Fig reports it through `onAssetError`
|
|
167
|
+
instead of adding it to the already-flushed head, so required shell metadata
|
|
168
|
+
should render before `headReady`.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const result = renderToStream(<Page />);
|
|
172
|
+
await result.headReady;
|
|
173
|
+
|
|
174
|
+
response.write(`<!doctype html><html><head>${result.getHead()}</head><body>`);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Document-mode host resource tags lower to the same registry as helper-created
|
|
178
|
+
resources:
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
function Page() {
|
|
182
|
+
return (
|
|
183
|
+
<>
|
|
184
|
+
<title>Page</title>
|
|
185
|
+
<meta name="description" content="..." />
|
|
186
|
+
<link rel="stylesheet" href="/page.css" precedence="app" />
|
|
187
|
+
<script type="module" src="/page.js" />
|
|
188
|
+
</>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Metadata discovered before `headReady` is injected into the initial document
|
|
194
|
+
head. Metadata discovered after `headReady`, for example inside pending
|
|
195
|
+
Suspense content, is reported through `onAssetError` and is not added to the
|
|
196
|
+
already-flushed shell. Stylesheets discovered for later Suspense segments remain
|
|
197
|
+
stream-safe: Fig emits them near the segment and gates reveal until they load
|
|
198
|
+
unless `{ blocking: "none" }` opts out.
|
|
199
|
+
|
|
200
|
+
Stylesheets, preloads, fonts, preconnects, and scripts are body-stream-safe
|
|
201
|
+
resources. Fig hoists them before the HTML segment that depends on them and
|
|
202
|
+
dedupes identical resources. Stylesheets block streamed Suspense reveals by
|
|
203
|
+
default; pass `{ blocking: "none" }` to opt out for non-critical styles.
|
|
204
|
+
|
|
205
|
+
Resource duplicates are checked by key and behavior. Identical duplicates dedupe
|
|
206
|
+
silently. Conflicting duplicates throw: for example, the same stylesheet `href`
|
|
207
|
+
with a different `media`, the same `title` key with a different value, or the
|
|
208
|
+
same meta `name` with different `content`. Preloads are keyed by `href` plus
|
|
209
|
+
`as`, so the same URL can be preloaded for distinct targets, but behavior fields
|
|
210
|
+
such as `type`, `crossOrigin`, and `fetchPriority` must match for duplicates.
|
|
211
|
+
Conflict errors include the resource key plus the existing and incoming
|
|
212
|
+
resources.
|
|
213
|
+
|
|
214
|
+
The `assets` option can attach asset resources to component modules without
|
|
215
|
+
wrapping the component tree. It is a string-keyed record intended for
|
|
216
|
+
bundler/module ids. Use `resolveAssetKey` to map a component type to one of
|
|
217
|
+
those ids:
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
renderToStream(<Page />, {
|
|
221
|
+
resolveAssetKey: (type) => (type === Page ? "app/page.tsx" : undefined),
|
|
222
|
+
assets: {
|
|
223
|
+
"app/page.tsx": [title("Page"), stylesheet("/page.css")],
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Manifest assets use the same registry, destination rules, dedupe checks, and
|
|
229
|
+
Suspense reveal gating as explicit `assets(...)` wrappers.
|
|
230
|
+
|
|
231
|
+
## Payload (server components)
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import {
|
|
235
|
+
createPayloadResponse,
|
|
236
|
+
decodePayloadValue,
|
|
237
|
+
encodePayloadValue,
|
|
238
|
+
fetchPayload,
|
|
239
|
+
jsonPayloadCodec,
|
|
240
|
+
PAYLOAD_BOUNDARY_HEADER,
|
|
241
|
+
renderToPayloadStream,
|
|
242
|
+
PayloadBoundary,
|
|
243
|
+
PayloadFetchError,
|
|
244
|
+
type PayloadCodec,
|
|
245
|
+
} from "@bgub/fig-server/payload";
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`renderToPayloadStream(node, options?)` renders a server-component payload. The
|
|
249
|
+
result exposes `abort(reason?)`, and `options.signal` also cancels the render;
|
|
250
|
+
both paths reject `allReady`. The default `jsonPayloadCodec` writes one
|
|
251
|
+
readable JSON row per newline and identifies itself with
|
|
252
|
+
`text/x-fig-payload; codec=json; charset=utf-8`. Pass a custom `PayloadCodec`
|
|
253
|
+
to both `renderToPayloadStream(node, { codec })` and
|
|
254
|
+
`createPayloadResponse({ codec })` when both ends should use a different byte
|
|
255
|
+
encoding. Codec ids are implementation ids, not stable public wire formats.
|
|
256
|
+
|
|
257
|
+
Pass `refreshBoundary` to render a targeted boundary refresh:
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
renderToPayloadStream(<FeedItems />, { refreshBoundary: "feed" });
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The rendered node must be the replacement content for that boundary. Do not
|
|
264
|
+
include a nested `<PayloadBoundary id="feed">` wrapper in the refresh payload.
|
|
265
|
+
|
|
266
|
+
`createPayloadResponse()` decodes streamed rows on the client, and
|
|
267
|
+
`fetchPayload(response, input, options?)` fetches and processes a payload. Pass
|
|
268
|
+
`refreshBoundary` to `fetchPayload` to request and apply a boundary refresh.
|
|
269
|
+
`fetchPayload` sends the response codec in `Accept` and checks the response
|
|
270
|
+
`codec=` content-type parameter before decoding. Server integrations can read
|
|
271
|
+
the exported `PAYLOAD_BOUNDARY_HEADER` constant for targeted refresh requests.
|
|
272
|
+
Non-2xx responses reject with `PayloadFetchError`, which exposes `status` and
|
|
273
|
+
`response` and cancels the response body before throwing.
|
|
274
|
+
Decoded client references require `loadClientReference` or a matching
|
|
275
|
+
`resolveClientReference` before render; metadata-only decodes can still inspect
|
|
276
|
+
rows without configuring a loader.
|
|
277
|
+
|
|
278
|
+
`encodePayloadValue` / `decodePayloadValue` are low-level helpers for payload
|
|
279
|
+
integrations that need the same data-value fidelity as payload data rows:
|
|
280
|
+
`undefined`, `Date`, `Map`, `Set`, `BigInt`, non-finite numbers, `-0`, and
|
|
281
|
+
global `Symbol.for` symbols round-trip. Shared references and cycles across
|
|
282
|
+
arrays, plain objects, `Map`, and `Set` also round-trip; functions, class
|
|
283
|
+
instances, and non-global symbols are rejected.
|
|
284
|
+
|
|
285
|
+
The server renderer supports function components, fragments, context providers,
|
|
286
|
+
`useState` initial values, `useSyncExternalStore` server snapshots, no-op server
|
|
287
|
+
effects, host prop serialization, streaming Suspense, partial segments inside
|
|
288
|
+
Suspense boundaries, resource hoisting, abort fallback flushing, and
|
|
289
|
+
Suspense-only server error recovery. Host elements may render trusted raw content
|
|
290
|
+
with `unsafeHTML`, which is written without escaping and cannot be combined with
|
|
291
|
+
children. Error boundaries do not catch server render errors.
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { a as ServerErrorPayload, c as ServerPrerenderResult, d as RenderTreeKind, f as RenderTreeNode, i as ServerErrorInfo, l as ServerRenderOptions, n as ServerAssetErrorInfo, o as ServerFragmentRenderResult, p as createRenderTreeCollector, r as ServerDocumentRenderResult, s as ServerPrerenderOptions, t as ServerAssetDestination, u as RenderTreeCollector } from "./types-4X0JT0wt.js";
|
|
2
|
+
import { FigNode, Props } from "@bgub/fig";
|
|
3
|
+
//#region src/html.d.ts
|
|
4
|
+
declare function escapeText(value: string): string;
|
|
5
|
+
declare function escapeAttribute(value: string): string;
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/index.d.ts
|
|
8
|
+
declare function renderToStream(node: FigNode, options?: ServerRenderOptions): ServerFragmentRenderResult;
|
|
9
|
+
declare function renderToDocumentStream(node: FigNode, options?: ServerRenderOptions): ServerDocumentRenderResult;
|
|
10
|
+
/**
|
|
11
|
+
* The streamed output, buffered: awaits `allReady` and concatenates exactly
|
|
12
|
+
* the bytes a streaming client would have received — including the inline
|
|
13
|
+
* streaming runtime and boundary-reveal scripts when the tree suspends past
|
|
14
|
+
* the shell. Right for caching responses and snapshotting wire output; it is
|
|
15
|
+
* NOT React's `renderToString` (use `prerender` for settled, script-free
|
|
16
|
+
* static markup).
|
|
17
|
+
*/
|
|
18
|
+
declare function renderToHtml(node: FigNode, options?: ServerRenderOptions): Promise<string>;
|
|
19
|
+
/** Document-mode {@link renderToHtml}: the streamed document, buffered. */
|
|
20
|
+
declare function renderToDocumentHtml(node: FigNode, options?: ServerRenderOptions): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Settled static HTML: waits for all async work before flushing, so completed
|
|
23
|
+
* Suspense content is emitted in logical position without streaming scripts.
|
|
24
|
+
*/
|
|
25
|
+
declare function prerender(node: FigNode, options?: ServerPrerenderOptions): Promise<ServerPrerenderResult>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { type RenderTreeCollector, type RenderTreeKind, type RenderTreeNode, type ServerAssetDestination, type ServerAssetErrorInfo, type ServerDocumentRenderResult, type ServerErrorInfo, type ServerErrorPayload, type ServerFragmentRenderResult, type ServerPrerenderOptions, type ServerPrerenderResult, type ServerRenderOptions, createRenderTreeCollector, escapeAttribute, escapeText, prerender, renderToDocumentHtml, renderToDocumentStream, renderToHtml, renderToStream };
|