@bgub/fig-server 0.0.1 → 0.1.0-alpha.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/README.md +99 -72
- package/dist/escaping-DAot8sFu.js +26 -0
- package/dist/escaping-DAot8sFu.js.map +1 -0
- package/dist/html-entry.d.ts +7 -0
- package/dist/html-entry.js +2 -0
- package/dist/index.d.ts +3 -7
- package/dist/index.js +671 -395
- package/dist/index.js.map +1 -1
- package/dist/payload.d.ts +13 -236
- package/dist/payload.js +235 -1363
- package/dist/payload.js.map +1 -1
- package/dist/{shared-CQn6Dje6.js → shared-Bna74_aZ.js} +22 -4
- package/dist/shared-Bna74_aZ.js.map +1 -0
- package/dist/{types-4X0JT0wt.d.ts → types-CimwEFsf.d.ts} +32 -10
- package/package.json +8 -3
- package/dist/shared-CQn6Dje6.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @bgub/fig-server
|
|
2
2
|
|
|
3
|
-
Fig server renderer.
|
|
3
|
+
Fig server renderer for HTML and Payload component trees.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -109,18 +109,65 @@ Pass `identifierPrefix` when multiple streaming renders share a document. Fig
|
|
|
109
109
|
uses it in generated Suspense marker and script identifiers, and it defaults to
|
|
110
110
|
an empty string.
|
|
111
111
|
|
|
112
|
+
Streams respect consumer backpressure: once the stream's internal queue holds
|
|
113
|
+
`highWaterMark` encoded bytes (default 65536), completed Suspense content
|
|
114
|
+
waits in segment form and flushes as the consumer reads. Rendering itself
|
|
115
|
+
never pauses, so `shellReady`/`allReady` settle at the same time regardless of
|
|
116
|
+
how fast the stream is consumed.
|
|
117
|
+
|
|
118
|
+
## Content Security Policy
|
|
119
|
+
|
|
120
|
+
Every script Fig streams is inline: the Suspense reveal runtime, the
|
|
121
|
+
per-boundary reveal ops, and the early event-capture script that opens a
|
|
122
|
+
document `<head>`. Under a CSP that restricts `script-src`, generate a
|
|
123
|
+
per-request nonce, pass it as the `nonce` option, and allow it in the header —
|
|
124
|
+
Fig adds it to every inline script and to emitted `<script>`/`<link>` resource
|
|
125
|
+
tags:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const nonce = crypto.randomUUID();
|
|
129
|
+
const result = renderToDocumentStream(<App />, { nonce });
|
|
130
|
+
|
|
131
|
+
return new Response(result.stream, {
|
|
132
|
+
headers: {
|
|
133
|
+
"content-type": result.contentType,
|
|
134
|
+
"content-security-policy": `script-src 'self' 'nonce-${nonce}'`,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The nonce is the whole CSP story. Fig intentionally ships no external-runtime
|
|
140
|
+
alternative for nonce-less strict CSP (per-render op scripts also rule out
|
|
141
|
+
static `script-src` hashes), so streamed Suspense requires the nonce. If you
|
|
142
|
+
cannot attach one, use `prerender`: fragment mode emits no scripts at all, and
|
|
143
|
+
document mode emits only the nonce-carrying early event-capture script.
|
|
144
|
+
|
|
145
|
+
Framework adapters that add companion markup use the focused HTML helpers:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import {
|
|
149
|
+
escapeAttribute,
|
|
150
|
+
escapeScriptJson,
|
|
151
|
+
escapeScriptText,
|
|
152
|
+
escapeText,
|
|
153
|
+
} from "@bgub/fig-server/html";
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`escapeScriptText` preserves raw script content while preventing a literal
|
|
157
|
+
`</script>` terminator; `escapeScriptJson` serializes a value and applies the
|
|
158
|
+
same protection.
|
|
159
|
+
|
|
112
160
|
## Data Resources
|
|
113
161
|
|
|
114
|
-
Server render requests have their own data-resource store. Reads
|
|
115
|
-
`@bgub/fig` dedupe by key within the request and fulfilled entries are
|
|
162
|
+
Server render requests have their own data-resource store by default. Reads
|
|
163
|
+
through `@bgub/fig` dedupe by key within the request and fulfilled entries are
|
|
116
164
|
available through `result.getData()`:
|
|
117
165
|
|
|
118
166
|
```tsx
|
|
119
|
-
import { readData } from "@bgub/fig";
|
|
120
|
-
import { serverDataResource } from "@bgub/fig/server";
|
|
167
|
+
import { dataResource, readData } from "@bgub/fig";
|
|
121
168
|
import { renderToStream } from "@bgub/fig-server";
|
|
122
169
|
|
|
123
|
-
const userResource =
|
|
170
|
+
const userResource = dataResource<[string], { name: string }>({
|
|
124
171
|
key: (id) => ["user", id],
|
|
125
172
|
load: async (id, { signal }) => fetchUser(id, signal),
|
|
126
173
|
});
|
|
@@ -136,6 +183,22 @@ await result.allReady;
|
|
|
136
183
|
const initialData = result.getData();
|
|
137
184
|
```
|
|
138
185
|
|
|
186
|
+
Adapters that run loaders before rendering can create the store themselves.
|
|
187
|
+
The renderer adopts that exact store instead of copying its entries:
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
import { createDataStore } from "@bgub/fig";
|
|
191
|
+
import { renderToStream } from "@bgub/fig-server";
|
|
192
|
+
|
|
193
|
+
const dataStore = createDataStore();
|
|
194
|
+
await dataStore.ensureData(userResource, "one");
|
|
195
|
+
|
|
196
|
+
const result = renderToStream(<Profile id="one" />, { dataStore });
|
|
197
|
+
result.data === dataStore; // true
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
One renderer may adopt a store, and that renderer owns its lifetime.
|
|
201
|
+
|
|
139
202
|
Pass `initialData` to `createRoot(...)` or `hydrateRoot(...)` on the client to
|
|
140
203
|
hydrate those values by key. Client imports can use a loader-less resource with
|
|
141
204
|
the same key; if no client loader exists, `refreshData(...)` reports
|
|
@@ -162,10 +225,10 @@ and injects `title()` and `meta()` before `</head>`. With the lower-level
|
|
|
162
225
|
`renderToStream`, those tags are available through `result.getHead()`
|
|
163
226
|
after `headReady`; they are never emitted into segment HTML. `headReady`
|
|
164
227
|
resolves with the shell and seals the initial document head; `getHead()` keeps
|
|
165
|
-
returning that sealed snapshot afterward.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
228
|
+
returning that sealed snapshot afterward. Metadata discovered later stays staged
|
|
229
|
+
with its Suspense content. When that boundary reveals, Fig replaces the visible
|
|
230
|
+
fallback metadata with the completed branch's title/meta in the same reveal
|
|
231
|
+
operation; failed or abandoned branches never mutate the document head.
|
|
169
232
|
|
|
170
233
|
```ts
|
|
171
234
|
const result = renderToStream(<Page />);
|
|
@@ -190,12 +253,11 @@ function Page() {
|
|
|
190
253
|
}
|
|
191
254
|
```
|
|
192
255
|
|
|
193
|
-
Metadata
|
|
194
|
-
head. Metadata discovered
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
unless `{ blocking: "none" }` opts out.
|
|
256
|
+
Metadata for the shell's visible branch is injected into the initial document
|
|
257
|
+
head. Metadata discovered in pending Suspense content is carried by its final
|
|
258
|
+
boundary reveal; partial segment fills do not publish it. Stylesheets discovered
|
|
259
|
+
for later Suspense segments remain stream-safe: Fig emits them near the segment
|
|
260
|
+
and gates reveal until they load unless `{ blocking: "none" }` opts out.
|
|
199
261
|
|
|
200
262
|
Stylesheets, preloads, fonts, preconnects, and scripts are body-stream-safe
|
|
201
263
|
resources. Fig hoists them before the HTML segment that depends on them and
|
|
@@ -203,13 +265,13 @@ dedupes identical resources. Stylesheets block streamed Suspense reveals by
|
|
|
203
265
|
default; pass `{ blocking: "none" }` to opt out for non-critical styles.
|
|
204
266
|
|
|
205
267
|
Resource duplicates are checked by key and behavior. Identical duplicates dedupe
|
|
206
|
-
silently. Conflicting
|
|
207
|
-
with a different `media
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
resources.
|
|
268
|
+
silently. Conflicting delivery assets throw: for example, the same stylesheet
|
|
269
|
+
`href` with a different `media`. Title/meta instead use visible-owner claims, so
|
|
270
|
+
a revealed primary branch can replace its fallback and releasing a claim can
|
|
271
|
+
restore the previous value. Preloads are keyed by `href` plus `as`, so the same
|
|
272
|
+
URL can be preloaded for distinct targets, but behavior fields such as `type`,
|
|
273
|
+
`crossorigin`, and `fetchpriority` must match for duplicates. Conflict errors
|
|
274
|
+
include the resource key plus the existing and incoming resources.
|
|
213
275
|
|
|
214
276
|
The `assets` option can attach asset resources to component modules without
|
|
215
277
|
wrapping the component tree. It is a string-keyed record intended for
|
|
@@ -228,59 +290,24 @@ renderToStream(<Page />, {
|
|
|
228
290
|
Manifest assets use the same registry, destination rules, dedupe checks, and
|
|
229
291
|
Suspense reveal gating as explicit `assets(...)` wrappers.
|
|
230
292
|
|
|
231
|
-
## Payload
|
|
293
|
+
## Payload components
|
|
232
294
|
|
|
233
295
|
```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" });
|
|
296
|
+
import { renderToPayloadStream } from "@bgub/fig-server/payload";
|
|
261
297
|
```
|
|
262
298
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
`
|
|
274
|
-
|
|
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.
|
|
299
|
+
`renderToPayloadStream(node, options?)` renders a component tree to Payload. The
|
|
300
|
+
result exposes `stream`, `contentType`, and `allReady`; cancellation is
|
|
301
|
+
signal-only — aborting `options.signal` (or cancelling the stream) cancels the
|
|
302
|
+
render and rejects `allReady`. Pass the stream and content type directly to a `Response`. Browser
|
|
303
|
+
code decodes it with `decodePayloadStream` from `@bgub/fig/payload`, normally
|
|
304
|
+
through fig-dom's `createPayloadComponent`. Rows, codecs, value encoding, and
|
|
305
|
+
framework document transports are internal implementation details.
|
|
306
|
+
|
|
307
|
+
The public options cover error sanitization (`onError`), bundler-provided
|
|
308
|
+
Payload component assets (`componentAssets`), manifest-provided client assets
|
|
309
|
+
(`clientReferenceAssets`), data-store partitioning, byte backpressure
|
|
310
|
+
(`highWaterMark`), and cancellation (`signal`).
|
|
284
311
|
|
|
285
312
|
The server renderer supports function components, fragments, context providers,
|
|
286
313
|
`useState` initial values, `useSyncExternalStore` server snapshots, no-op server
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/escaping.ts
|
|
2
|
+
function escapeText(value) {
|
|
3
|
+
return value.replace(/[&<>]/g, (character) => {
|
|
4
|
+
if (character === "&") return "&";
|
|
5
|
+
if (character === "<") return "<";
|
|
6
|
+
return ">";
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
function escapeAttribute(value) {
|
|
10
|
+
return value.replace(/[&"<>]/g, (character) => {
|
|
11
|
+
if (character === "&") return "&";
|
|
12
|
+
if (character === "\"") return """;
|
|
13
|
+
if (character === "<") return "<";
|
|
14
|
+
return ">";
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function escapeScriptText(value) {
|
|
18
|
+
return value.replaceAll("<", "\\u003C").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
|
|
19
|
+
}
|
|
20
|
+
function escapeScriptJson(value) {
|
|
21
|
+
return escapeScriptText(JSON.stringify(value));
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { escapeText as i, escapeScriptJson as n, escapeScriptText as r, escapeAttribute as t };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=escaping-DAot8sFu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escaping-DAot8sFu.js","names":[],"sources":["../src/escaping.ts"],"sourcesContent":["export function escapeText(value: string): string {\n return value.replace(/[&<>]/g, (character) => {\n if (character === \"&\") return \"&\";\n if (character === \"<\") return \"<\";\n return \">\";\n });\n}\n\nexport function escapeAttribute(value: string): string {\n return value.replace(/[&\"<>]/g, (character) => {\n if (character === \"&\") return \"&\";\n if (character === '\"') return \""\";\n if (character === \"<\") return \"<\";\n return \">\";\n });\n}\n\n// Script elements are raw-text elements: HTML entities are not decoded in\n// their contents, so escape only characters that can terminate the element\n// or executable source.\nexport function escapeScriptText(value: string): string {\n return value\n .replaceAll(\"<\", \"\\\\u003C\")\n .replaceAll(\"\\u2028\", \"\\\\u2028\")\n .replaceAll(\"\\u2029\", \"\\\\u2029\");\n}\n\nexport function escapeScriptJson(value: unknown): string {\n return escapeScriptText(JSON.stringify(value));\n}\n"],"mappings":";AAAA,SAAgB,WAAW,OAAuB;CAChD,OAAO,MAAM,QAAQ,WAAW,cAAc;EAC5C,IAAI,cAAc,KAAK,OAAO;EAC9B,IAAI,cAAc,KAAK,OAAO;EAC9B,OAAO;CACT,CAAC;AACH;AAEA,SAAgB,gBAAgB,OAAuB;CACrD,OAAO,MAAM,QAAQ,YAAY,cAAc;EAC7C,IAAI,cAAc,KAAK,OAAO;EAC9B,IAAI,cAAc,MAAK,OAAO;EAC9B,IAAI,cAAc,KAAK,OAAO;EAC9B,OAAO;CACT,CAAC;AACH;AAKA,SAAgB,iBAAiB,OAAuB;CACtD,OAAO,MACJ,WAAW,KAAK,SAAS,CAAC,CAC1B,WAAW,UAAU,SAAS,CAAC,CAC/B,WAAW,UAAU,SAAS;AACnC;AAEA,SAAgB,iBAAiB,OAAwB;CACvD,OAAO,iBAAiB,KAAK,UAAU,KAAK,CAAC;AAC/C"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/escaping.d.ts
|
|
2
|
+
declare function escapeText(value: string): string;
|
|
3
|
+
declare function escapeAttribute(value: string): string;
|
|
4
|
+
declare function escapeScriptText(value: string): string;
|
|
5
|
+
declare function escapeScriptJson(value: unknown): string;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { escapeAttribute, escapeScriptJson, escapeScriptText, escapeText };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { FigNode
|
|
3
|
-
//#region src/html.d.ts
|
|
4
|
-
declare function escapeText(value: string): string;
|
|
5
|
-
declare function escapeAttribute(value: string): string;
|
|
6
|
-
//#endregion
|
|
1
|
+
import { a as ServerPreloadHeaderOptions, c as ServerPrerenderResult, d as RenderTreeKind, f as RenderTreeNode, i as ServerFragmentRenderResult, l as ServerRenderOptions, n as ServerErrorInfo, o as ServerPreloadHeaderResource, p as createRenderTreeCollector, r as ServerErrorPayload, s as ServerPrerenderOptions, t as ServerDocumentRenderResult, u as RenderTreeCollector } from "./types-CimwEFsf.js";
|
|
2
|
+
import { FigNode } from "@bgub/fig";
|
|
7
3
|
//#region src/index.d.ts
|
|
8
4
|
declare function renderToStream(node: FigNode, options?: ServerRenderOptions): ServerFragmentRenderResult;
|
|
9
5
|
declare function renderToDocumentStream(node: FigNode, options?: ServerRenderOptions): ServerDocumentRenderResult;
|
|
@@ -24,4 +20,4 @@ declare function renderToDocumentHtml(node: FigNode, options?: ServerRenderOptio
|
|
|
24
20
|
*/
|
|
25
21
|
declare function prerender(node: FigNode, options?: ServerPrerenderOptions): Promise<ServerPrerenderResult>;
|
|
26
22
|
//#endregion
|
|
27
|
-
export { type RenderTreeCollector, type RenderTreeKind, type RenderTreeNode, type
|
|
23
|
+
export { type RenderTreeCollector, type RenderTreeKind, type RenderTreeNode, type ServerDocumentRenderResult, type ServerErrorInfo, type ServerErrorPayload, type ServerFragmentRenderResult, type ServerPreloadHeaderOptions, type ServerPreloadHeaderResource, type ServerPrerenderOptions, type ServerPrerenderResult, type ServerRenderOptions, createRenderTreeCollector, prerender, renderToDocumentHtml, renderToDocumentStream, renderToHtml, renderToStream };
|