@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 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
@@ -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 };