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