@fictjs/ssr 0.26.0 → 0.27.0

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.
Files changed (41) hide show
  1. package/README.md +169 -52
  2. package/dist/experimental.cjs +1 -1
  3. package/dist/experimental.d.cts +1 -1
  4. package/dist/experimental.d.ts +1 -1
  5. package/dist/experimental.js +1 -1
  6. package/dist/experimental.node.cjs +5 -0
  7. package/dist/experimental.node.d.cts +2 -0
  8. package/dist/experimental.node.d.ts +2 -0
  9. package/dist/experimental.node.js +4 -0
  10. package/dist/fict-stream-runtime.js +1 -1
  11. package/dist/index.cjs +1 -1
  12. package/dist/index.d.cts +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.node.cjs +10 -0
  16. package/dist/index.node.d.cts +2 -0
  17. package/dist/index.node.d.ts +2 -0
  18. package/dist/index.node.js +4 -0
  19. package/dist/node-session-carrier-CTo-1Awq.cjs +8 -0
  20. package/dist/node-session-carrier-ClqRbdoQ.js +12 -0
  21. package/dist/node-session-carrier-ClqRbdoQ.js.map +1 -0
  22. package/dist/render-core-D0nOT_Pe.js +1824 -0
  23. package/dist/render-core-D0nOT_Pe.js.map +1 -0
  24. package/dist/render-core-D_EheVwh.cjs +1863 -0
  25. package/dist/{render-core-BCYvLfHF.d.ts → render-core-KCiJV_SK.d.cts} +22 -1
  26. package/dist/render-core-KCiJV_SK.d.cts.map +1 -0
  27. package/dist/{render-core-BCYvLfHF.d.cts → render-core-KCiJV_SK.d.ts} +22 -1
  28. package/dist/render-core-KCiJV_SK.d.ts.map +1 -0
  29. package/dist/stream-runtime.cjs +1 -1
  30. package/dist/stream-runtime.d.cts +2 -0
  31. package/dist/stream-runtime.d.cts.map +1 -1
  32. package/dist/stream-runtime.d.ts +2 -0
  33. package/dist/stream-runtime.d.ts.map +1 -1
  34. package/dist/stream-runtime.js +1 -1
  35. package/dist/stream-runtime.js.map +1 -1
  36. package/package.json +15 -5
  37. package/dist/render-core-BCYvLfHF.d.cts.map +0 -1
  38. package/dist/render-core-BCYvLfHF.d.ts.map +0 -1
  39. package/dist/render-core-BLLUhkYy.js +0 -969
  40. package/dist/render-core-BLLUhkYy.js.map +0 -1
  41. package/dist/render-core-CWudV9yi.cjs +0 -1008
@@ -1,969 +0,0 @@
1
- import { createStreamRuntimeCode } from "./stream-runtime.js";
2
- import { render } from "@fictjs/runtime";
3
- import { __fictCreateSSRSession, __fictDisableSSR, __fictEnableSSR, __fictGetCurrentSSRSession, __fictGetScopeRegistry, __fictGetScopesForBoundary, __fictRunWithSSRSession, __fictSerializeSSRState, __fictSerializeSSRStateForScopes, __fictSetSSRStreamHooks } from "@fictjs/runtime/internal";
4
- import { parseHTML } from "linkedom";
5
- //#region src/globals.ts
6
- function installGlobals(window, document) {
7
- const win = window;
8
- const required = {
9
- window: win,
10
- document,
11
- self: win,
12
- Node: win.Node,
13
- Element: win.Element,
14
- HTMLElement: win.HTMLElement,
15
- SVGElement: win.SVGElement,
16
- Document: win.Document,
17
- DocumentFragment: win.DocumentFragment,
18
- Text: win.Text,
19
- Comment: win.Comment
20
- };
21
- const optional = {
22
- Range: win.Range,
23
- Event: win.Event,
24
- CustomEvent: win.CustomEvent,
25
- MutationObserver: win.MutationObserver,
26
- DOMParser: win.DOMParser,
27
- getComputedStyle: win.getComputedStyle?.bind(win)
28
- };
29
- const missing = Object.entries(required).filter(([, value]) => value === void 0).map(([key]) => key);
30
- if (missing.length) throw new Error(`[fict/ssr] Missing DOM globals: ${missing.join(", ")}`);
31
- const globals = {
32
- ...required,
33
- ...optional
34
- };
35
- const keys = Object.keys(globals);
36
- const snapshot = captureGlobals(keys);
37
- for (const key of keys) {
38
- const value = globals[key];
39
- if (value !== void 0) globalThis[key] = value;
40
- }
41
- return () => restoreGlobals(snapshot);
42
- }
43
- function installManifest(manifest) {
44
- if (!manifest) return () => {};
45
- let resolved;
46
- if (typeof manifest === "string") {
47
- const raw = readTextFileFromPath(manifest);
48
- resolved = JSON.parse(raw);
49
- } else resolved = manifest;
50
- const session = __fictGetCurrentSSRSession();
51
- if (session) {
52
- const previous = session.manifest;
53
- session.manifest = resolved;
54
- return () => {
55
- session.manifest = previous;
56
- };
57
- }
58
- const key = "__FICT_MANIFEST__";
59
- const snapshot = {
60
- exists: Object.prototype.hasOwnProperty.call(globalThis, key),
61
- value: globalThis[key]
62
- };
63
- globalThis[key] = resolved;
64
- return () => {
65
- if (snapshot.exists) globalThis[key] = snapshot.value;
66
- else delete globalThis[key];
67
- };
68
- }
69
- function captureGlobals(keys) {
70
- const snapshot = [];
71
- for (const key of keys) {
72
- const exists = Object.prototype.hasOwnProperty.call(globalThis, key);
73
- const value = globalThis[key];
74
- snapshot.push({
75
- key,
76
- exists,
77
- value
78
- });
79
- }
80
- return snapshot;
81
- }
82
- function restoreGlobals(snapshot) {
83
- for (const entry of snapshot) if (entry.exists) globalThis[entry.key] = entry.value;
84
- else delete globalThis[entry.key];
85
- }
86
- function readTextFileFromPath(path) {
87
- const deno = globalThis.Deno;
88
- if (deno && typeof deno.readTextFileSync === "function") return deno.readTextFileSync(path);
89
- const nodeRequire = getNodeRequire$1();
90
- if (nodeRequire) return nodeRequire("node:fs").readFileSync(path, "utf8");
91
- throw new Error("[fict/ssr] `manifest` as file path is only supported when Deno.readTextFileSync or CommonJS require is available. Pass a manifest object in Node ESM or edge runtimes.");
92
- }
93
- function getNodeRequire$1() {
94
- const direct = globalThis.require;
95
- if (typeof direct === "function") return direct;
96
- try {
97
- return Function("return typeof require === \"function\" ? require : null")();
98
- } catch {
99
- return null;
100
- }
101
- }
102
- //#endregion
103
- //#region src/stream-bridge.ts
104
- function createQueuedTextStream(options = {}) {
105
- const encoder = new TextEncoder();
106
- const queue = [];
107
- let controller = null;
108
- let closed = false;
109
- let aborted;
110
- const readyResolvers = [];
111
- const resolveReady = () => {
112
- if (!controller || (controller.desiredSize ?? 1) <= 0) return;
113
- while (readyResolvers.length > 0) readyResolvers.shift()?.();
114
- };
115
- const drainReady = () => {
116
- while (readyResolvers.length > 0) readyResolvers.shift()?.();
117
- };
118
- const abortQueue = (reason, notifyController = true) => {
119
- if (closed || aborted !== void 0) return;
120
- aborted = reason ?? /* @__PURE__ */ new Error("Stream aborted");
121
- queue.length = 0;
122
- drainReady();
123
- if (notifyController) controller?.error(aborted);
124
- };
125
- return {
126
- stream: new ReadableStream({
127
- start(ctrl) {
128
- controller = ctrl;
129
- for (const chunk of queue) ctrl.enqueue(chunk);
130
- queue.length = 0;
131
- if (aborted !== void 0) {
132
- ctrl.error(aborted);
133
- return;
134
- }
135
- if (closed) ctrl.close();
136
- },
137
- pull() {
138
- resolveReady();
139
- },
140
- cancel(reason) {
141
- abortQueue(reason, false);
142
- options.onCancel?.(reason);
143
- }
144
- }),
145
- writer: {
146
- write(chunk) {
147
- if (closed || aborted !== void 0) return;
148
- const data = encoder.encode(chunk);
149
- if (controller) {
150
- controller.enqueue(data);
151
- if ((controller.desiredSize ?? 1) <= 0) return new Promise((resolve) => {
152
- readyResolvers.push(resolve);
153
- });
154
- } else queue.push(data);
155
- },
156
- close() {
157
- if (closed || aborted !== void 0) return;
158
- closed = true;
159
- drainReady();
160
- controller?.close();
161
- },
162
- abort(reason) {
163
- abortQueue(reason);
164
- }
165
- }
166
- };
167
- }
168
- function createPipeBridge() {
169
- const nodeBridge = createNodePipeBridge();
170
- if (nodeBridge) return nodeBridge;
171
- const targets = /* @__PURE__ */ new Set();
172
- const buffer = [];
173
- let state = "open";
174
- let abortReason = null;
175
- let sinkErrorHandler;
176
- const safeWrite = (target, chunk) => {
177
- try {
178
- if (target.write(chunk) === false) return new Promise((resolve) => {
179
- const withOnce = target;
180
- if (typeof withOnce.once === "function") withOnce.once("drain", resolve);
181
- else resolve();
182
- });
183
- } catch (error) {
184
- sinkErrorHandler?.(error);
185
- }
186
- };
187
- const safeEnd = (target) => {
188
- try {
189
- target.end();
190
- } catch {}
191
- };
192
- const safeDestroy = (target, reason) => {
193
- const withDestroy = target;
194
- if (typeof withDestroy.destroy === "function") {
195
- try {
196
- withDestroy.destroy(reason);
197
- } catch {}
198
- return;
199
- }
200
- safeEnd(target);
201
- };
202
- return {
203
- pipe(writable, options) {
204
- targets.add(writable);
205
- if (options?.onError) {
206
- sinkErrorHandler = options.onError;
207
- if (typeof writable.on === "function") writable.on("error", options.onError);
208
- }
209
- if (buffer.length > 0) {
210
- for (const chunk of buffer) safeWrite(writable, chunk);
211
- buffer.length = 0;
212
- }
213
- if (state === "closed") safeEnd(writable);
214
- else if (state === "aborted") safeDestroy(writable, abortReason ?? /* @__PURE__ */ new Error("Stream aborted"));
215
- },
216
- write(chunk) {
217
- if (state !== "open") return;
218
- if (targets.size === 0) {
219
- buffer.push(chunk);
220
- return;
221
- }
222
- const pending = [];
223
- for (const target of targets) {
224
- const result = safeWrite(target, chunk);
225
- if (result) pending.push(result);
226
- }
227
- return pending.length > 0 ? Promise.all(pending).then(() => void 0) : void 0;
228
- },
229
- close() {
230
- if (state !== "open") return;
231
- state = "closed";
232
- for (const target of targets) safeEnd(target);
233
- if (targets.size > 0) buffer.length = 0;
234
- },
235
- abort(reason) {
236
- if (state !== "open") return;
237
- state = "aborted";
238
- abortReason = reason instanceof Error ? reason : /* @__PURE__ */ new Error("Stream aborted");
239
- for (const target of targets) safeDestroy(target, abortReason);
240
- buffer.length = 0;
241
- }
242
- };
243
- }
244
- function createNodePipeBridge() {
245
- const nodeRequire = getNodeRequire();
246
- if (!nodeRequire) return null;
247
- try {
248
- const streamModule = nodeRequire("node:stream");
249
- if (!streamModule.PassThrough) return null;
250
- const passThrough = new streamModule.PassThrough();
251
- const buffer = [];
252
- let piped = false;
253
- let state = "open";
254
- let abortReason = null;
255
- const pendingDrains = /* @__PURE__ */ new Set();
256
- const flushDrains = () => {
257
- for (const resolve of pendingDrains) resolve();
258
- pendingDrains.clear();
259
- };
260
- const writeToPassThrough = (chunk) => {
261
- if (passThrough.write(chunk) === false) return new Promise((resolve) => {
262
- const settle = () => {
263
- pendingDrains.delete(settle);
264
- resolve();
265
- };
266
- pendingDrains.add(settle);
267
- const withOnce = passThrough;
268
- if (typeof withOnce.once === "function") withOnce.once("drain", settle);
269
- else settle();
270
- });
271
- };
272
- const flushBuffer = () => {
273
- if (buffer.length === 0) return void 0;
274
- const pending = [];
275
- for (const chunk of buffer) {
276
- const result = writeToPassThrough(chunk);
277
- if (result) pending.push(result);
278
- }
279
- buffer.length = 0;
280
- return pending.length > 0 ? Promise.all(pending).then(() => void 0) : void 0;
281
- };
282
- const destroyPassThrough = (error) => {
283
- if (typeof passThrough.destroy === "function") passThrough.destroy(error);
284
- else passThrough.end();
285
- };
286
- return {
287
- pipe(writable, options) {
288
- piped = true;
289
- passThrough.pipe(writable);
290
- const onError = options?.onError;
291
- if (onError && typeof writable.on === "function") writable.on("error", (err) => {
292
- flushDrains();
293
- onError(err);
294
- });
295
- if (state === "aborted") {
296
- destroyPassThrough(abortReason ?? /* @__PURE__ */ new Error("Stream aborted"));
297
- return;
298
- }
299
- const flushed = flushBuffer();
300
- if (state === "closed") if (flushed) flushed.then(() => passThrough.end());
301
- else passThrough.end();
302
- },
303
- write(chunk) {
304
- if (state !== "open") return;
305
- if (!piped) {
306
- buffer.push(chunk);
307
- return;
308
- }
309
- return writeToPassThrough(chunk);
310
- },
311
- close() {
312
- if (state !== "open") return;
313
- state = "closed";
314
- if (!piped) return;
315
- passThrough.end();
316
- },
317
- abort(reason) {
318
- if (state !== "open") return;
319
- state = "aborted";
320
- abortReason = reason instanceof Error ? reason : /* @__PURE__ */ new Error("Stream aborted");
321
- buffer.length = 0;
322
- flushDrains();
323
- if (piped) destroyPassThrough(abortReason);
324
- }
325
- };
326
- } catch {
327
- return null;
328
- }
329
- }
330
- function getNodeRequire() {
331
- const direct = globalThis.require;
332
- if (typeof direct === "function") return direct;
333
- try {
334
- return Function("return typeof require === \"function\" ? require : null")();
335
- } catch {
336
- return null;
337
- }
338
- }
339
- //#endregion
340
- //#region src/render-core.ts
341
- const DEFAULT_HTML = "<!doctype html><html><head></head><body></body></html>";
342
- function createSSRDocument(html = DEFAULT_HTML) {
343
- const window = parseHTML(html);
344
- const document = window.document;
345
- if (!window || !document) throw new Error("[fict/ssr] Failed to create DOM. Missing window or document.");
346
- return {
347
- window,
348
- document
349
- };
350
- }
351
- function renderToDocument(view, options = {}) {
352
- return __fictRunWithSSRSession(__fictCreateSSRSession(), () => renderToDocumentInSession(view, options));
353
- }
354
- function renderToDocumentInSession(view, options) {
355
- const includeSnapshot = options.includeSnapshot !== false;
356
- __fictEnableSSR();
357
- let dom;
358
- let restoreGlobals = () => {};
359
- let restoreManifest = () => {};
360
- let container;
361
- let teardown = () => {};
362
- try {
363
- dom = resolveDom(options);
364
- const { document, window } = dom;
365
- restoreGlobals = options.exposeGlobals === true ? installGlobals(window, document) : () => {};
366
- restoreManifest = installManifest(options.manifest);
367
- container = resolveContainer(document, options);
368
- teardown = render(view, container);
369
- if (includeSnapshot) {
370
- const state = __fictSerializeSSRState();
371
- injectSnapshot(document, container, state, options);
372
- }
373
- } catch (error) {
374
- __fictDisableSSR();
375
- restoreGlobals();
376
- restoreManifest();
377
- throw error;
378
- }
379
- __fictDisableSSR();
380
- const html = serializeOutput(dom.document, container, options);
381
- const dispose = () => {
382
- try {
383
- teardown();
384
- } finally {
385
- restoreGlobals();
386
- restoreManifest();
387
- }
388
- };
389
- return {
390
- html,
391
- document: dom.document,
392
- window: dom.window,
393
- container,
394
- dispose
395
- };
396
- }
397
- function renderToString(view, options = {}) {
398
- const result = renderToDocument(view, options);
399
- const html = result.html;
400
- result.dispose();
401
- return html;
402
- }
403
- async function renderToStringAsync(view, options = {}) {
404
- return renderToString(view, options);
405
- }
406
- function renderToStream(view, options = {}) {
407
- const encoder = new TextEncoder();
408
- let controller = null;
409
- let abortRender = null;
410
- const readyResolvers = [];
411
- const drainBackpressure = () => {
412
- while (readyResolvers.length > 0) readyResolvers.shift()?.();
413
- };
414
- const resolveBackpressure = () => {
415
- if (!controller || (controller.desiredSize ?? 1) <= 0) return;
416
- drainBackpressure();
417
- };
418
- const closeController = () => {
419
- abortRender = null;
420
- drainBackpressure();
421
- if (!controller) return;
422
- try {
423
- controller.close();
424
- } finally {
425
- controller = null;
426
- }
427
- };
428
- const errorController = (reason) => {
429
- abortRender = null;
430
- drainBackpressure();
431
- if (!controller) return;
432
- try {
433
- controller.error(reason);
434
- } finally {
435
- controller = null;
436
- }
437
- };
438
- return new ReadableStream({
439
- start(ctrl) {
440
- controller = ctrl;
441
- const started = startStreamingRender(view, options, {
442
- write(chunk) {
443
- if (!controller) return;
444
- controller.enqueue(encoder.encode(chunk));
445
- if ((controller.desiredSize ?? 1) <= 0) return new Promise((resolve) => {
446
- readyResolvers.push(resolve);
447
- });
448
- },
449
- close() {
450
- closeController();
451
- },
452
- abort(reason) {
453
- errorController(reason);
454
- }
455
- });
456
- abortRender = started.abort;
457
- started.shellReady.catch(() => void 0);
458
- started.allReady.catch(() => void 0);
459
- },
460
- pull() {
461
- resolveBackpressure();
462
- },
463
- cancel(reason) {
464
- const abort = abortRender;
465
- controller = null;
466
- drainBackpressure();
467
- abort?.(reason ?? /* @__PURE__ */ new Error("Stream canceled"));
468
- }
469
- });
470
- }
471
- function renderToPipeableStream(view, options = {}) {
472
- const bridge = createPipeBridge();
473
- const { shellReady, allReady, abort } = startStreamingRender(view, options, {
474
- write(chunk) {
475
- return bridge.write(chunk);
476
- },
477
- close() {
478
- bridge.close();
479
- },
480
- abort(reason) {
481
- bridge.abort(reason);
482
- }
483
- });
484
- return {
485
- pipe(writable) {
486
- bridge.pipe(writable, { onError: abort });
487
- },
488
- abort,
489
- shellReady,
490
- allReady
491
- };
492
- }
493
- /**
494
- * @experimental Preview API for v1.0; the return shape may change before this
495
- * becomes stable.
496
- */
497
- function renderToPartial(view, options = {}) {
498
- const partialOptions = {
499
- ...options,
500
- mode: "shell",
501
- fullDocument: options.fullDocument ?? true
502
- };
503
- let shell = "";
504
- let shellPhase = true;
505
- let abortPartial = null;
506
- const queued = createQueuedTextStream({ onCancel(reason) {
507
- abortPartial?.(reason ?? /* @__PURE__ */ new Error("Stream canceled"));
508
- } });
509
- const { shellReady, allReady, abort } = startStreamingRender(view, partialOptions, {
510
- write(chunk) {
511
- if (shellPhase) {
512
- shell += chunk;
513
- return;
514
- }
515
- return queued.writer.write(chunk);
516
- },
517
- close() {
518
- queued.writer.close();
519
- },
520
- abort(reason) {
521
- queued.writer.abort(reason);
522
- }
523
- }, {
524
- includeTailInShell: true,
525
- onShellFlushed() {
526
- shellPhase = false;
527
- }
528
- });
529
- abortPartial = abort;
530
- return {
531
- shell,
532
- stream: queued.stream,
533
- shellReady,
534
- allReady,
535
- abort
536
- };
537
- }
538
- function resolveDom(options) {
539
- if (options.dom) return options.dom;
540
- if (options.document && options.window) return {
541
- document: options.document,
542
- window: options.window
543
- };
544
- if (options.document) {
545
- const window = options.window ?? options.document.defaultView ?? options.document.defaultView ?? void 0;
546
- if (!window) throw new Error("[fict/ssr] A window is required when providing a document without defaultView.");
547
- return {
548
- document: options.document,
549
- window
550
- };
551
- }
552
- if (options.window) return {
553
- document: options.window.document,
554
- window: options.window
555
- };
556
- return createSSRDocument(options.html);
557
- }
558
- function isPromiseLike(value) {
559
- return typeof value === "object" && value !== null && typeof value.then === "function";
560
- }
561
- function startStreamingRender(view, options, writer, control = {}) {
562
- const session = __fictCreateSSRSession();
563
- return __fictRunWithSSRSession(session, () => startStreamingRenderInSession(session, view, options, writer, control));
564
- }
565
- function startStreamingRenderInSession(session, view, options, writer, control = {}) {
566
- const runInSession = (fn) => __fictRunWithSSRSession(session, fn);
567
- const resolvedOptions = {
568
- ...options,
569
- fullDocument: options.fullDocument ?? true
570
- };
571
- let resolveShell;
572
- let rejectShell;
573
- let resolveAll;
574
- let rejectAll;
575
- let shellSettled = false;
576
- const shellReady = new Promise((res, rej) => {
577
- resolveShell = () => {
578
- if (shellSettled) return;
579
- shellSettled = true;
580
- res();
581
- };
582
- rejectShell = (err) => {
583
- if (shellSettled) return;
584
- shellSettled = true;
585
- rej(err);
586
- };
587
- });
588
- const allReady = new Promise((res, rej) => {
589
- resolveAll = res;
590
- rejectAll = rej;
591
- });
592
- let dom = null;
593
- let restoreGlobals = () => {};
594
- let restoreManifest = () => {};
595
- let teardown = () => {};
596
- let container = null;
597
- let closed = false;
598
- let tailHtml = "";
599
- let wroteShell = false;
600
- let shellCarriesTail = false;
601
- let writeChain = null;
602
- let writeFailed = false;
603
- let removeAbortListener = () => {};
604
- const mode = options.mode ?? "shell";
605
- const includeSnapshot = options.includeSnapshot !== false;
606
- const sentScopes = /* @__PURE__ */ new Set();
607
- const boundaryMap = /* @__PURE__ */ new Map();
608
- let boundaryId = 0;
609
- let pendingCount = 0;
610
- const handleWriteError = (error) => {
611
- if (writeFailed) return;
612
- writeFailed = true;
613
- options.onError?.(error);
614
- writer.abort(error);
615
- cleanup();
616
- rejectShell(error);
617
- rejectAll(error);
618
- };
619
- const enqueueWrite = (chunk) => {
620
- if (writeFailed) return;
621
- const trackWrite = (promise) => {
622
- const tracked = promise.then(() => {
623
- if (writeChain === tracked) writeChain = null;
624
- }, (error) => {
625
- if (writeChain === tracked) writeChain = null;
626
- handleWriteError(error);
627
- });
628
- writeChain = tracked;
629
- };
630
- const writeAsync = () => Promise.resolve(writer.write(chunk)).then(() => void 0);
631
- if (writeChain) {
632
- trackWrite(writeChain.then(writeAsync));
633
- return;
634
- }
635
- try {
636
- const result = writer.write(chunk);
637
- if (isPromiseLike(result)) trackWrite(result.then(() => void 0, (error) => {
638
- throw error;
639
- }));
640
- } catch (error) {
641
- handleWriteError(error);
642
- }
643
- };
644
- const afterWrites = (fn) => {
645
- const pending = writeChain;
646
- if (!pending) {
647
- if (!writeFailed) fn();
648
- return;
649
- }
650
- pending.then(() => {
651
- if (!writeFailed) fn();
652
- });
653
- };
654
- const markShellReady = () => {
655
- afterWrites(() => {
656
- control.onShellFlushed?.();
657
- resolveShell();
658
- options.onShellReady?.();
659
- });
660
- };
661
- const writeSnapshotForScopes = (scopeIds) => {
662
- runInSession(() => {
663
- if (!includeSnapshot || scopeIds.length === 0) return;
664
- const registry = __fictGetScopeRegistry();
665
- const pending = scopeIds.filter((id) => registry.has(id) && !sentScopes.has(id));
666
- if (pending.length === 0) return;
667
- const snapshot = __fictSerializeSSRStateForScopes(pending);
668
- const ids = Object.keys(snapshot.scopes);
669
- if (ids.length === 0) return;
670
- const chunk = buildIncrementalSnapshotChunk(snapshot, resolvedOptions);
671
- if (chunk) enqueueWrite(chunk);
672
- for (const id of ids) sentScopes.add(id);
673
- });
674
- };
675
- const writeSnapshotForBoundary = (boundary) => {
676
- runInSession(() => {
677
- const scopes = __fictGetScopesForBoundary(boundary);
678
- writeSnapshotForScopes(scopes);
679
- });
680
- };
681
- const writeRemainingSnapshots = () => {
682
- runInSession(() => {
683
- const scopes = Array.from(__fictGetScopeRegistry().keys());
684
- writeSnapshotForScopes(scopes);
685
- });
686
- };
687
- const cleanup = () => {
688
- removeAbortListener();
689
- removeAbortListener = () => {};
690
- runInSession(() => {
691
- __fictSetSSRStreamHooks(null);
692
- __fictDisableSSR();
693
- });
694
- try {
695
- teardown();
696
- } catch {} finally {
697
- restoreGlobals();
698
- restoreManifest();
699
- }
700
- };
701
- const finalize = () => {
702
- if (closed) return;
703
- closed = true;
704
- if (mode === "all" && dom && container && !wroteShell) {
705
- if (includeSnapshot) {
706
- const snapshot = __fictSerializeSSRState();
707
- injectSnapshot(dom.document, container, snapshot, resolvedOptions);
708
- }
709
- const fullHtml = serializeOutput(dom.document, container, resolvedOptions);
710
- enqueueWrite(fullHtml);
711
- afterWrites(() => {
712
- writer.close();
713
- cleanup();
714
- resolveShell();
715
- resolveAll();
716
- options.onShellReady?.();
717
- options.onAllReady?.();
718
- });
719
- return;
720
- }
721
- writeRemainingSnapshots();
722
- if (tailHtml) enqueueWrite(tailHtml);
723
- afterWrites(() => {
724
- writer.close();
725
- cleanup();
726
- resolveAll();
727
- options.onAllReady?.();
728
- });
729
- };
730
- const maybeFinalize = () => {
731
- if (pendingCount === 0) finalize();
732
- };
733
- const hooks = {
734
- registerBoundary(start, end) {
735
- const id = `s${++boundaryId}`;
736
- boundaryMap.set(id, {
737
- start,
738
- end,
739
- pending: false
740
- });
741
- return id;
742
- },
743
- boundaryPending(id) {
744
- const entry = boundaryMap.get(id);
745
- if (!entry || entry.pending) return;
746
- entry.pending = true;
747
- pendingCount++;
748
- },
749
- boundaryResolved(id) {
750
- const entry = boundaryMap.get(id);
751
- if (!entry) return;
752
- if (entry.pending) {
753
- entry.pending = false;
754
- pendingCount = Math.max(0, pendingCount - 1);
755
- }
756
- if (mode === "shell") {
757
- writeSnapshotForBoundary(id);
758
- if (dom) {
759
- const html = serializeBetween(dom.document, entry.start, entry.end);
760
- enqueueWrite(buildPatchChunk(id, html, resolvedOptions));
761
- }
762
- }
763
- maybeFinalize();
764
- },
765
- onError(err) {
766
- options.onError?.(err);
767
- abort(err);
768
- }
769
- };
770
- const abort = (reason) => {
771
- const abortReason = reason ?? /* @__PURE__ */ new Error("Stream aborted");
772
- if (!closed) {
773
- closed = true;
774
- writeFailed = true;
775
- writer.abort(reason);
776
- cleanup();
777
- }
778
- rejectShell(abortReason);
779
- rejectAll(abortReason);
780
- };
781
- if (options.signal) if (options.signal.aborted) {
782
- abort(options.signal.reason);
783
- return {
784
- shellReady,
785
- allReady,
786
- abort
787
- };
788
- } else {
789
- const onAbort = () => abort(options.signal?.reason);
790
- options.signal.addEventListener("abort", onAbort, { once: true });
791
- removeAbortListener = () => options.signal?.removeEventListener("abort", onAbort);
792
- }
793
- try {
794
- __fictEnableSSR();
795
- __fictSetSSRStreamHooks(hooks);
796
- dom = resolveDom(resolvedOptions);
797
- restoreGlobals = resolvedOptions.exposeGlobals === true ? installGlobals(dom.window, dom.document) : () => {};
798
- restoreManifest = installManifest(resolvedOptions.manifest);
799
- container = resolveContainer(dom.document, resolvedOptions);
800
- teardown = render(view, container);
801
- if (mode === "all") {
802
- if (pendingCount === 0) finalize();
803
- return {
804
- shellReady,
805
- allReady,
806
- abort
807
- };
808
- }
809
- const shellHtml = serializeOutput(dom.document, container, resolvedOptions);
810
- const streamRuntime = boundaryMap.size > 0 ? buildStreamRuntimeScript(resolvedOptions) : "";
811
- if (resolvedOptions.fullDocument) {
812
- const split = splitDocumentHtml(shellHtml);
813
- if (!split) throw new Error("[fict/ssr] Failed to locate </body> for streaming output.");
814
- if (control.includeTailInShell) {
815
- enqueueWrite(split.head + streamRuntime);
816
- tailHtml = split.tail;
817
- shellCarriesTail = true;
818
- } else {
819
- enqueueWrite(split.head + streamRuntime);
820
- tailHtml = split.tail;
821
- }
822
- } else enqueueWrite(shellHtml + streamRuntime);
823
- wroteShell = true;
824
- writeSnapshotForScopes(Array.from(__fictGetScopeRegistry().keys()));
825
- if (shellCarriesTail && tailHtml) {
826
- enqueueWrite(tailHtml);
827
- tailHtml = "";
828
- shellCarriesTail = false;
829
- }
830
- markShellReady();
831
- maybeFinalize();
832
- } catch (err) {
833
- options.onError?.(err);
834
- abort(err);
835
- }
836
- return {
837
- shellReady,
838
- allReady,
839
- abort
840
- };
841
- }
842
- function resolveContainer(document, options) {
843
- if (options.container) {
844
- if (options.container.ownerDocument && options.container.ownerDocument !== document) throw new Error("[fict/ssr] Provided container belongs to a different document.");
845
- return options.container;
846
- }
847
- const tag = options.containerTag ?? "div";
848
- const container = document.createElement(tag);
849
- if (options.containerId) container.setAttribute("id", options.containerId);
850
- if (options.containerAttributes) for (const [name, value] of Object.entries(options.containerAttributes)) {
851
- if (value === null || value === void 0 || value === false) continue;
852
- container.setAttribute(name, value === true ? "" : String(value));
853
- }
854
- if (document.body) document.body.appendChild(container);
855
- return container;
856
- }
857
- function buildStreamRuntimeScript(options) {
858
- const nonce = renderNonceAttribute(options);
859
- if (options.streamRuntime === "external") {
860
- if (!options.streamRuntimeSrc) throw new Error("[fict/ssr] streamRuntimeSrc is required when streamRuntime is \"external\".");
861
- return `<script${nonce} src="${escapeAttribute(options.streamRuntimeSrc)}" data-fict-stream-runtime data-fict-stream-observer><\/script>`;
862
- }
863
- return `<script${nonce}>${createStreamRuntimeCode({ observerMode: resolveStreamPatchMode(options) === "observer" })}<\/script>`;
864
- }
865
- function buildPatchChunk(id, html, options) {
866
- const template = `<template data-fict-suspense="${escapeAttribute(id)}">${html}</template>`;
867
- if (resolveStreamPatchMode(options) === "observer") return template;
868
- return `${template}<script${renderNonceAttribute(options)}>__FICT_STREAM.apply("${escapeScriptString(id)}")<\/script>`;
869
- }
870
- function resolveStreamPatchMode(options) {
871
- if (options.streamPatchMode) return options.streamPatchMode;
872
- return options.streamRuntime === "external" ? "observer" : "inline";
873
- }
874
- function serializeBetween(document, start, end) {
875
- const wrapper = document.createElement("div");
876
- let node = start.nextSibling;
877
- while (node && node !== end) {
878
- wrapper.appendChild(node.cloneNode(true));
879
- node = node.nextSibling;
880
- }
881
- return wrapper.innerHTML;
882
- }
883
- function splitDocumentHtml(html) {
884
- const idx = html.toLowerCase().lastIndexOf("</body>");
885
- if (idx === -1) return null;
886
- return {
887
- head: html.slice(0, idx),
888
- tail: html.slice(idx)
889
- };
890
- }
891
- function buildIncrementalSnapshotChunk(state, options) {
892
- const json = serializeSnapshotForScript(state);
893
- const nonce = renderNonceAttribute(options);
894
- if (options.snapshotTarget === "head") {
895
- const jsonLiteral = JSON.stringify(json);
896
- return `<script${nonce}>(function(){var s=document.createElement('script');s.type='application/json';s.setAttribute('data-fict-snapshot','');${options.scriptNonce !== void 0 ? `s.setAttribute('nonce',${serializeScriptStringLiteral(options.scriptNonce)});` : ""}s.textContent=${jsonLiteral};(document.head||document.documentElement).appendChild(s);}())<\/script>`;
897
- }
898
- return `<script${nonce} type="application/json" data-fict-snapshot>${json}<\/script>`;
899
- }
900
- function serializeOutput(document, container, options) {
901
- if (options.fullDocument) {
902
- const doctype = serializeDoctype(document, options.doctype);
903
- const html = document.documentElement ? document.documentElement.outerHTML : container.outerHTML;
904
- return doctype ? `${doctype}${html}` : html;
905
- }
906
- if (options.includeContainer) return container.outerHTML;
907
- return container.innerHTML;
908
- }
909
- function injectSnapshot(document, container, state, options) {
910
- const script = document.createElement("script");
911
- script.type = "application/json";
912
- script.id = options.snapshotScriptId ?? "__FICT_SNAPSHOT__";
913
- if (options.scriptNonce !== void 0) script.setAttribute("nonce", options.scriptNonce);
914
- script.textContent = serializeSnapshotForScript(state);
915
- if (options.fullDocument) {
916
- if (options.snapshotTarget === "head" && document.head) {
917
- document.head.appendChild(script);
918
- return;
919
- }
920
- if (document.body) {
921
- document.body.appendChild(script);
922
- return;
923
- }
924
- }
925
- const target = options.snapshotTarget ?? "container";
926
- if (target === "body" && document.body) {
927
- document.body.appendChild(script);
928
- return;
929
- }
930
- if (target === "head" && document.head) {
931
- document.head.appendChild(script);
932
- return;
933
- }
934
- container.appendChild(script);
935
- }
936
- function serializeSnapshotForScript(state) {
937
- return JSON.stringify(state).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
938
- }
939
- function serializeScriptStringLiteral(value) {
940
- return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
941
- }
942
- function renderNonceAttribute(options) {
943
- return options.scriptNonce === void 0 ? "" : ` nonce="${escapeAttribute(options.scriptNonce)}"`;
944
- }
945
- function escapeAttribute(value) {
946
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
947
- }
948
- function escapeScriptString(value) {
949
- return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
950
- }
951
- function serializeDoctype(document, override) {
952
- if (override === null) return "";
953
- if (override !== void 0) return override;
954
- const doctype = document.doctype;
955
- if (!doctype) return "";
956
- const name = doctype.name || "html";
957
- const publicId = doctype.publicId;
958
- const systemId = doctype.systemId;
959
- let id = "";
960
- if (publicId) {
961
- id = ` PUBLIC "${publicId}"`;
962
- if (systemId) id += ` "${systemId}"`;
963
- } else if (systemId) id = ` SYSTEM "${systemId}"`;
964
- return `<!DOCTYPE ${name}${id}>`;
965
- }
966
- //#endregion
967
- export { renderToStream as a, renderToPipeableStream as i, renderToDocument as n, renderToString as o, renderToPartial as r, renderToStringAsync as s, createSSRDocument as t };
968
-
969
- //# sourceMappingURL=render-core-BLLUhkYy.js.map