@fictjs/ssr 0.14.0 → 0.15.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 (3) hide show
  1. package/dist/index.cjs +334 -316
  2. package/dist/index.js +334 -316
  3. package/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -32,174 +32,120 @@ module.exports = __toCommonJS(index_exports);
32
32
  var import_runtime = require("@fictjs/runtime");
33
33
  var import_internal = require("@fictjs/runtime/internal");
34
34
  var import_linkedom = require("linkedom");
35
- var DEFAULT_HTML = "<!doctype html><html><head></head><body></body></html>";
36
- function createSSRDocument(html = DEFAULT_HTML) {
37
- const window = (0, import_linkedom.parseHTML)(html);
38
- const document = window.document;
39
- if (!window || !document) {
40
- throw new Error("[fict/ssr] Failed to create DOM. Missing window or document.");
41
- }
42
- return { window, document };
43
- }
44
- function renderToDocument(view, options = {}) {
45
- const includeSnapshot = options.includeSnapshot !== false;
46
- (0, import_internal.__fictEnableSSR)();
47
- let dom;
48
- let restoreGlobals2 = () => {
49
- };
50
- let restoreManifest = () => {
35
+
36
+ // src/globals.ts
37
+ function installGlobals(window, document) {
38
+ const win = window;
39
+ const required = {
40
+ window: win,
41
+ document,
42
+ self: win,
43
+ Node: win.Node,
44
+ Element: win.Element,
45
+ HTMLElement: win.HTMLElement,
46
+ SVGElement: win.SVGElement,
47
+ Document: win.Document,
48
+ DocumentFragment: win.DocumentFragment,
49
+ Text: win.Text,
50
+ Comment: win.Comment
51
51
  };
52
- let container;
53
- let teardown = () => {
52
+ const optional = {
53
+ Range: win.Range,
54
+ Event: win.Event,
55
+ CustomEvent: win.CustomEvent,
56
+ MutationObserver: win.MutationObserver,
57
+ DOMParser: win.DOMParser,
58
+ getComputedStyle: win.getComputedStyle?.bind(win)
54
59
  };
55
- try {
56
- dom = resolveDom(options);
57
- const { document, window } = dom;
58
- const shouldExpose = options.exposeGlobals !== false;
59
- restoreGlobals2 = shouldExpose ? installGlobals(window, document) : () => {
60
- };
61
- restoreManifest = installManifest(options.manifest);
62
- container = resolveContainer(document, options);
63
- teardown = (0, import_runtime.render)(view, container);
64
- if (includeSnapshot) {
65
- const state = (0, import_internal.__fictSerializeSSRState)();
66
- injectSnapshot(document, container, state, options);
67
- }
68
- } catch (error) {
69
- (0, import_internal.__fictDisableSSR)();
70
- restoreGlobals2();
71
- restoreManifest();
72
- throw error;
60
+ const missing = Object.entries(required).filter(([, value]) => value === void 0).map(([key]) => key);
61
+ if (missing.length) {
62
+ throw new Error(`[fict/ssr] Missing DOM globals: ${missing.join(", ")}`);
73
63
  }
74
- (0, import_internal.__fictDisableSSR)();
75
- const html = serializeOutput(dom.document, container, options);
76
- const dispose = () => {
77
- try {
78
- teardown();
79
- } finally {
80
- restoreGlobals2();
81
- restoreManifest();
82
- }
83
- };
84
- return { html, document: dom.document, window: dom.window, container, dispose };
85
- }
86
- function renderToString(view, options = {}) {
87
- const result = renderToDocument(view, options);
88
- const html = result.html;
89
- result.dispose();
90
- return html;
91
- }
92
- async function renderToStringAsync(view, options = {}) {
93
- return renderToString(view, options);
94
- }
95
- function renderToStream(view, options = {}) {
96
- const encoder = new TextEncoder();
97
- let controller = null;
98
- const stream = new ReadableStream({
99
- start(ctrl) {
100
- controller = ctrl;
101
- const started = startStreamingRender(view, options, {
102
- write(chunk) {
103
- if (!controller) return;
104
- controller.enqueue(encoder.encode(chunk));
105
- },
106
- close() {
107
- controller?.close();
108
- },
109
- abort(reason) {
110
- controller?.error(reason);
111
- }
112
- });
113
- started.allReady.catch(() => void 0);
64
+ const globals = { ...required, ...optional };
65
+ const keys = Object.keys(globals);
66
+ const snapshot = captureGlobals(keys);
67
+ for (const key of keys) {
68
+ const value = globals[key];
69
+ if (value !== void 0) {
70
+ ;
71
+ globalThis[key] = value;
114
72
  }
115
- });
116
- return stream;
73
+ }
74
+ return () => restoreGlobals(snapshot);
117
75
  }
118
- function renderToPipeableStream(view, options = {}) {
119
- const bridge = createPipeBridge();
120
- const { shellReady, allReady, abort } = startStreamingRender(view, options, {
121
- write(chunk) {
122
- bridge.write(chunk);
123
- },
124
- close() {
125
- bridge.close();
126
- },
127
- abort(reason) {
128
- bridge.abort(reason);
129
- }
130
- });
131
- return {
132
- pipe(writable) {
133
- bridge.pipe(writable);
134
- },
135
- abort,
136
- shellReady,
137
- allReady
76
+ function installManifest(manifest) {
77
+ if (!manifest) return () => {
138
78
  };
139
- }
140
- function renderToPartial(view, options = {}) {
141
- const partialOptions = {
142
- ...options,
143
- mode: "shell",
144
- fullDocument: options.fullDocument ?? true
79
+ let resolved;
80
+ if (typeof manifest === "string") {
81
+ const raw = readTextFileFromPath(manifest);
82
+ resolved = JSON.parse(raw);
83
+ } else {
84
+ resolved = manifest;
85
+ }
86
+ const key = "__FICT_MANIFEST__";
87
+ const snapshot = {
88
+ exists: Object.prototype.hasOwnProperty.call(globalThis, key),
89
+ value: globalThis[key]
145
90
  };
146
- let shell = "";
147
- let shellPhase = true;
148
- const queued = createQueuedTextStream();
149
- const { shellReady, allReady, abort } = startStreamingRender(
150
- view,
151
- partialOptions,
152
- {
153
- write(chunk) {
154
- if (shellPhase) {
155
- shell += chunk;
156
- return;
157
- }
158
- queued.writer.write(chunk);
159
- },
160
- close() {
161
- queued.writer.close();
162
- },
163
- abort(reason) {
164
- queued.writer.abort(reason);
165
- }
166
- },
167
- {
168
- includeTailInShell: true,
169
- onShellFlushed() {
170
- shellPhase = false;
171
- }
91
+ globalThis[key] = resolved;
92
+ return () => {
93
+ if (snapshot.exists) {
94
+ ;
95
+ globalThis[key] = snapshot.value;
96
+ } else {
97
+ delete globalThis[key];
172
98
  }
173
- );
174
- return {
175
- shell,
176
- stream: queued.stream,
177
- shellReady,
178
- allReady,
179
- abort
180
99
  };
181
100
  }
182
- function resolveDom(options) {
183
- if (options.dom) {
184
- return options.dom;
185
- }
186
- if (options.document && options.window) {
187
- return { document: options.document, window: options.window };
101
+ function captureGlobals(keys) {
102
+ const snapshot = [];
103
+ for (const key of keys) {
104
+ const exists = Object.prototype.hasOwnProperty.call(globalThis, key);
105
+ const value = globalThis[key];
106
+ snapshot.push({ key, exists, value });
188
107
  }
189
- if (options.document) {
190
- const window = options.window ?? options.document.defaultView ?? options.document.defaultView ?? void 0;
191
- if (!window) {
192
- throw new Error(
193
- "[fict/ssr] A window is required when providing a document without defaultView."
194
- );
108
+ return snapshot;
109
+ }
110
+ function restoreGlobals(snapshot) {
111
+ for (const entry of snapshot) {
112
+ if (entry.exists) {
113
+ ;
114
+ globalThis[entry.key] = entry.value;
115
+ } else {
116
+ delete globalThis[entry.key];
195
117
  }
196
- return { document: options.document, window };
197
118
  }
198
- if (options.window) {
199
- return { document: options.window.document, window: options.window };
119
+ }
120
+ function readTextFileFromPath(path) {
121
+ const g = globalThis;
122
+ const deno = g.Deno;
123
+ if (deno && typeof deno.readTextFileSync === "function") {
124
+ return deno.readTextFileSync(path);
125
+ }
126
+ const nodeRequire = getNodeRequire();
127
+ if (nodeRequire) {
128
+ const fs = nodeRequire("node:fs");
129
+ return fs.readFileSync(path, "utf8");
130
+ }
131
+ throw new Error(
132
+ "[fict/ssr] `manifest` as file path is only supported in Node.js or Deno. Pass a manifest object in edge runtimes."
133
+ );
134
+ }
135
+ function getNodeRequire() {
136
+ const g = globalThis;
137
+ const direct = g.require;
138
+ if (typeof direct === "function") {
139
+ return direct;
140
+ }
141
+ try {
142
+ return Function('return typeof require === "function" ? require : null')();
143
+ } catch {
144
+ return null;
200
145
  }
201
- return createSSRDocument(options.html);
202
146
  }
147
+
148
+ // src/stream-bridge.ts
203
149
  function createQueuedTextStream() {
204
150
  const encoder = new TextEncoder();
205
151
  const queue = [];
@@ -282,74 +228,256 @@ function createPipeBridge() {
282
228
  for (const chunk of buffer) {
283
229
  safeWrite(writable, chunk);
284
230
  }
285
- buffer.length = 0;
286
- }
287
- if (state === "closed") {
288
- safeEnd(writable);
289
- } else if (state === "aborted") {
290
- safeDestroy(writable, abortReason ?? new Error("Stream aborted"));
291
- }
292
- },
231
+ buffer.length = 0;
232
+ }
233
+ if (state === "closed") {
234
+ safeEnd(writable);
235
+ } else if (state === "aborted") {
236
+ safeDestroy(writable, abortReason ?? new Error("Stream aborted"));
237
+ }
238
+ },
239
+ write(chunk) {
240
+ if (state !== "open") return;
241
+ if (targets.size === 0) {
242
+ buffer.push(chunk);
243
+ return;
244
+ }
245
+ for (const target of targets) {
246
+ safeWrite(target, chunk);
247
+ }
248
+ },
249
+ close() {
250
+ if (state !== "open") return;
251
+ state = "closed";
252
+ for (const target of targets) {
253
+ safeEnd(target);
254
+ }
255
+ if (targets.size > 0) {
256
+ buffer.length = 0;
257
+ }
258
+ },
259
+ abort(reason) {
260
+ if (state !== "open") return;
261
+ state = "aborted";
262
+ abortReason = reason instanceof Error ? reason : new Error("Stream aborted");
263
+ for (const target of targets) {
264
+ safeDestroy(target, abortReason);
265
+ }
266
+ buffer.length = 0;
267
+ }
268
+ };
269
+ }
270
+ function createNodePipeBridge() {
271
+ const nodeRequire = getNodeRequire2();
272
+ if (!nodeRequire) return null;
273
+ try {
274
+ const streamModule = nodeRequire("node:stream");
275
+ if (!streamModule.PassThrough) return null;
276
+ const passThrough = new streamModule.PassThrough();
277
+ return {
278
+ pipe(writable) {
279
+ passThrough.pipe(writable);
280
+ },
281
+ write(chunk) {
282
+ passThrough.write(chunk);
283
+ },
284
+ close() {
285
+ passThrough.end();
286
+ },
287
+ abort(reason) {
288
+ const error = reason instanceof Error ? reason : new Error("Stream aborted");
289
+ if (typeof passThrough.destroy === "function") {
290
+ passThrough.destroy(error);
291
+ } else {
292
+ passThrough.end();
293
+ }
294
+ }
295
+ };
296
+ } catch {
297
+ return null;
298
+ }
299
+ }
300
+ function getNodeRequire2() {
301
+ const g = globalThis;
302
+ const direct = g.require;
303
+ if (typeof direct === "function") {
304
+ return direct;
305
+ }
306
+ try {
307
+ return Function('return typeof require === "function" ? require : null')();
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+
313
+ // src/index.ts
314
+ var DEFAULT_HTML = "<!doctype html><html><head></head><body></body></html>";
315
+ function createSSRDocument(html = DEFAULT_HTML) {
316
+ const window = (0, import_linkedom.parseHTML)(html);
317
+ const document = window.document;
318
+ if (!window || !document) {
319
+ throw new Error("[fict/ssr] Failed to create DOM. Missing window or document.");
320
+ }
321
+ return { window, document };
322
+ }
323
+ function renderToDocument(view, options = {}) {
324
+ const includeSnapshot = options.includeSnapshot !== false;
325
+ (0, import_internal.__fictEnableSSR)();
326
+ let dom;
327
+ let restoreGlobals2 = () => {
328
+ };
329
+ let restoreManifest = () => {
330
+ };
331
+ let container;
332
+ let teardown = () => {
333
+ };
334
+ try {
335
+ dom = resolveDom(options);
336
+ const { document, window } = dom;
337
+ const shouldExpose = options.exposeGlobals !== false;
338
+ restoreGlobals2 = shouldExpose ? installGlobals(window, document) : () => {
339
+ };
340
+ restoreManifest = installManifest(options.manifest);
341
+ container = resolveContainer(document, options);
342
+ teardown = (0, import_runtime.render)(view, container);
343
+ if (includeSnapshot) {
344
+ const state = (0, import_internal.__fictSerializeSSRState)();
345
+ injectSnapshot(document, container, state, options);
346
+ }
347
+ } catch (error) {
348
+ (0, import_internal.__fictDisableSSR)();
349
+ restoreGlobals2();
350
+ restoreManifest();
351
+ throw error;
352
+ }
353
+ (0, import_internal.__fictDisableSSR)();
354
+ const html = serializeOutput(dom.document, container, options);
355
+ const dispose = () => {
356
+ try {
357
+ teardown();
358
+ } finally {
359
+ restoreGlobals2();
360
+ restoreManifest();
361
+ }
362
+ };
363
+ return { html, document: dom.document, window: dom.window, container, dispose };
364
+ }
365
+ function renderToString(view, options = {}) {
366
+ const result = renderToDocument(view, options);
367
+ const html = result.html;
368
+ result.dispose();
369
+ return html;
370
+ }
371
+ async function renderToStringAsync(view, options = {}) {
372
+ return renderToString(view, options);
373
+ }
374
+ function renderToStream(view, options = {}) {
375
+ const encoder = new TextEncoder();
376
+ let controller = null;
377
+ const stream = new ReadableStream({
378
+ start(ctrl) {
379
+ controller = ctrl;
380
+ const started = startStreamingRender(view, options, {
381
+ write(chunk) {
382
+ if (!controller) return;
383
+ controller.enqueue(encoder.encode(chunk));
384
+ },
385
+ close() {
386
+ controller?.close();
387
+ },
388
+ abort(reason) {
389
+ controller?.error(reason);
390
+ }
391
+ });
392
+ started.allReady.catch(() => void 0);
393
+ }
394
+ });
395
+ return stream;
396
+ }
397
+ function renderToPipeableStream(view, options = {}) {
398
+ const bridge = createPipeBridge();
399
+ const { shellReady, allReady, abort } = startStreamingRender(view, options, {
293
400
  write(chunk) {
294
- if (state !== "open") return;
295
- if (targets.size === 0) {
296
- buffer.push(chunk);
297
- return;
298
- }
299
- for (const target of targets) {
300
- safeWrite(target, chunk);
301
- }
401
+ bridge.write(chunk);
302
402
  },
303
403
  close() {
304
- if (state !== "open") return;
305
- state = "closed";
306
- for (const target of targets) {
307
- safeEnd(target);
308
- }
309
- if (targets.size > 0) {
310
- buffer.length = 0;
311
- }
404
+ bridge.close();
312
405
  },
313
406
  abort(reason) {
314
- if (state !== "open") return;
315
- state = "aborted";
316
- abortReason = reason instanceof Error ? reason : new Error("Stream aborted");
317
- for (const target of targets) {
318
- safeDestroy(target, abortReason);
319
- }
320
- buffer.length = 0;
407
+ bridge.abort(reason);
321
408
  }
409
+ });
410
+ return {
411
+ pipe(writable) {
412
+ bridge.pipe(writable);
413
+ },
414
+ abort,
415
+ shellReady,
416
+ allReady
322
417
  };
323
418
  }
324
- function createNodePipeBridge() {
325
- const nodeRequire = getNodeRequire();
326
- if (!nodeRequire) return null;
327
- try {
328
- const streamModule = nodeRequire("node:stream");
329
- if (!streamModule.PassThrough) return null;
330
- const passThrough = new streamModule.PassThrough();
331
- return {
332
- pipe(writable) {
333
- passThrough.pipe(writable);
334
- },
419
+ function renderToPartial(view, options = {}) {
420
+ const partialOptions = {
421
+ ...options,
422
+ mode: "shell",
423
+ fullDocument: options.fullDocument ?? true
424
+ };
425
+ let shell = "";
426
+ let shellPhase = true;
427
+ const queued = createQueuedTextStream();
428
+ const { shellReady, allReady, abort } = startStreamingRender(
429
+ view,
430
+ partialOptions,
431
+ {
335
432
  write(chunk) {
336
- passThrough.write(chunk);
433
+ if (shellPhase) {
434
+ shell += chunk;
435
+ return;
436
+ }
437
+ queued.writer.write(chunk);
337
438
  },
338
439
  close() {
339
- passThrough.end();
440
+ queued.writer.close();
340
441
  },
341
442
  abort(reason) {
342
- const error = reason instanceof Error ? reason : new Error("Stream aborted");
343
- if (typeof passThrough.destroy === "function") {
344
- passThrough.destroy(error);
345
- } else {
346
- passThrough.end();
347
- }
443
+ queued.writer.abort(reason);
348
444
  }
349
- };
350
- } catch {
351
- return null;
445
+ },
446
+ {
447
+ includeTailInShell: true,
448
+ onShellFlushed() {
449
+ shellPhase = false;
450
+ }
451
+ }
452
+ );
453
+ return {
454
+ shell,
455
+ stream: queued.stream,
456
+ shellReady,
457
+ allReady,
458
+ abort
459
+ };
460
+ }
461
+ function resolveDom(options) {
462
+ if (options.dom) {
463
+ return options.dom;
464
+ }
465
+ if (options.document && options.window) {
466
+ return { document: options.document, window: options.window };
467
+ }
468
+ if (options.document) {
469
+ const window = options.window ?? options.document.defaultView ?? options.document.defaultView ?? void 0;
470
+ if (!window) {
471
+ throw new Error(
472
+ "[fict/ssr] A window is required when providing a document without defaultView."
473
+ );
474
+ }
475
+ return { document: options.document, window };
476
+ }
477
+ if (options.window) {
478
+ return { document: options.window.document, window: options.window };
352
479
  }
480
+ return createSSRDocument(options.html);
353
481
  }
354
482
  function startStreamingRender(view, options, writer, control = {}) {
355
483
  const resolvedOptions = {
@@ -659,116 +787,6 @@ function serializeDoctype(document, override) {
659
787
  }
660
788
  return `<!DOCTYPE ${name}${id}>`;
661
789
  }
662
- function installGlobals(window, document) {
663
- const win = window;
664
- const required = {
665
- window: win,
666
- document,
667
- self: win,
668
- Node: win.Node,
669
- Element: win.Element,
670
- HTMLElement: win.HTMLElement,
671
- SVGElement: win.SVGElement,
672
- Document: win.Document,
673
- DocumentFragment: win.DocumentFragment,
674
- Text: win.Text,
675
- Comment: win.Comment
676
- };
677
- const optional = {
678
- Range: win.Range,
679
- Event: win.Event,
680
- CustomEvent: win.CustomEvent,
681
- MutationObserver: win.MutationObserver,
682
- DOMParser: win.DOMParser,
683
- getComputedStyle: win.getComputedStyle?.bind(win)
684
- };
685
- const missing = Object.entries(required).filter(([, value]) => value === void 0).map(([key]) => key);
686
- if (missing.length) {
687
- throw new Error(`[fict/ssr] Missing DOM globals: ${missing.join(", ")}`);
688
- }
689
- const globals = { ...required, ...optional };
690
- const keys = Object.keys(globals);
691
- const snapshot = captureGlobals(keys);
692
- for (const key of keys) {
693
- const value = globals[key];
694
- if (value !== void 0) {
695
- ;
696
- globalThis[key] = value;
697
- }
698
- }
699
- return () => restoreGlobals(snapshot);
700
- }
701
- function captureGlobals(keys) {
702
- const snapshot = [];
703
- for (const key of keys) {
704
- const exists = Object.prototype.hasOwnProperty.call(globalThis, key);
705
- const value = globalThis[key];
706
- snapshot.push({ key, exists, value });
707
- }
708
- return snapshot;
709
- }
710
- function restoreGlobals(snapshot) {
711
- for (const entry of snapshot) {
712
- if (entry.exists) {
713
- ;
714
- globalThis[entry.key] = entry.value;
715
- } else {
716
- delete globalThis[entry.key];
717
- }
718
- }
719
- }
720
- function readTextFileFromPath(path) {
721
- const g = globalThis;
722
- const deno = g.Deno;
723
- if (deno && typeof deno.readTextFileSync === "function") {
724
- return deno.readTextFileSync(path);
725
- }
726
- const nodeRequire = getNodeRequire();
727
- if (nodeRequire) {
728
- const fs = nodeRequire("node:fs");
729
- return fs.readFileSync(path, "utf8");
730
- }
731
- throw new Error(
732
- "[fict/ssr] `manifest` as file path is only supported in Node.js or Deno. Pass a manifest object in edge runtimes."
733
- );
734
- }
735
- function getNodeRequire() {
736
- const g = globalThis;
737
- const direct = g.require;
738
- if (typeof direct === "function") {
739
- return direct;
740
- }
741
- try {
742
- return Function('return typeof require === "function" ? require : null')();
743
- } catch {
744
- return null;
745
- }
746
- }
747
- function installManifest(manifest) {
748
- if (!manifest) return () => {
749
- };
750
- let resolved;
751
- if (typeof manifest === "string") {
752
- const raw = readTextFileFromPath(manifest);
753
- resolved = JSON.parse(raw);
754
- } else {
755
- resolved = manifest;
756
- }
757
- const key = "__FICT_MANIFEST__";
758
- const snapshot = {
759
- exists: Object.prototype.hasOwnProperty.call(globalThis, key),
760
- value: globalThis[key]
761
- };
762
- globalThis[key] = resolved;
763
- return () => {
764
- if (snapshot.exists) {
765
- ;
766
- globalThis[key] = snapshot.value;
767
- } else {
768
- delete globalThis[key];
769
- }
770
- };
771
- }
772
790
  // Annotate the CommonJS export names for ESM import in node:
773
791
  0 && (module.exports = {
774
792
  createSSRDocument,
package/dist/index.js CHANGED
@@ -10,174 +10,120 @@ import {
10
10
  __fictSetSSRStreamHooks
11
11
  } from "@fictjs/runtime/internal";
12
12
  import { parseHTML } from "linkedom";
13
- var DEFAULT_HTML = "<!doctype html><html><head></head><body></body></html>";
14
- function createSSRDocument(html = DEFAULT_HTML) {
15
- const window = parseHTML(html);
16
- const document = window.document;
17
- if (!window || !document) {
18
- throw new Error("[fict/ssr] Failed to create DOM. Missing window or document.");
19
- }
20
- return { window, document };
21
- }
22
- function renderToDocument(view, options = {}) {
23
- const includeSnapshot = options.includeSnapshot !== false;
24
- __fictEnableSSR();
25
- let dom;
26
- let restoreGlobals2 = () => {
27
- };
28
- let restoreManifest = () => {
13
+
14
+ // src/globals.ts
15
+ function installGlobals(window, document) {
16
+ const win = window;
17
+ const required = {
18
+ window: win,
19
+ document,
20
+ self: win,
21
+ Node: win.Node,
22
+ Element: win.Element,
23
+ HTMLElement: win.HTMLElement,
24
+ SVGElement: win.SVGElement,
25
+ Document: win.Document,
26
+ DocumentFragment: win.DocumentFragment,
27
+ Text: win.Text,
28
+ Comment: win.Comment
29
29
  };
30
- let container;
31
- let teardown = () => {
30
+ const optional = {
31
+ Range: win.Range,
32
+ Event: win.Event,
33
+ CustomEvent: win.CustomEvent,
34
+ MutationObserver: win.MutationObserver,
35
+ DOMParser: win.DOMParser,
36
+ getComputedStyle: win.getComputedStyle?.bind(win)
32
37
  };
33
- try {
34
- dom = resolveDom(options);
35
- const { document, window } = dom;
36
- const shouldExpose = options.exposeGlobals !== false;
37
- restoreGlobals2 = shouldExpose ? installGlobals(window, document) : () => {
38
- };
39
- restoreManifest = installManifest(options.manifest);
40
- container = resolveContainer(document, options);
41
- teardown = render(view, container);
42
- if (includeSnapshot) {
43
- const state = __fictSerializeSSRState();
44
- injectSnapshot(document, container, state, options);
45
- }
46
- } catch (error) {
47
- __fictDisableSSR();
48
- restoreGlobals2();
49
- restoreManifest();
50
- throw error;
38
+ const missing = Object.entries(required).filter(([, value]) => value === void 0).map(([key]) => key);
39
+ if (missing.length) {
40
+ throw new Error(`[fict/ssr] Missing DOM globals: ${missing.join(", ")}`);
51
41
  }
52
- __fictDisableSSR();
53
- const html = serializeOutput(dom.document, container, options);
54
- const dispose = () => {
55
- try {
56
- teardown();
57
- } finally {
58
- restoreGlobals2();
59
- restoreManifest();
60
- }
61
- };
62
- return { html, document: dom.document, window: dom.window, container, dispose };
63
- }
64
- function renderToString(view, options = {}) {
65
- const result = renderToDocument(view, options);
66
- const html = result.html;
67
- result.dispose();
68
- return html;
69
- }
70
- async function renderToStringAsync(view, options = {}) {
71
- return renderToString(view, options);
72
- }
73
- function renderToStream(view, options = {}) {
74
- const encoder = new TextEncoder();
75
- let controller = null;
76
- const stream = new ReadableStream({
77
- start(ctrl) {
78
- controller = ctrl;
79
- const started = startStreamingRender(view, options, {
80
- write(chunk) {
81
- if (!controller) return;
82
- controller.enqueue(encoder.encode(chunk));
83
- },
84
- close() {
85
- controller?.close();
86
- },
87
- abort(reason) {
88
- controller?.error(reason);
89
- }
90
- });
91
- started.allReady.catch(() => void 0);
42
+ const globals = { ...required, ...optional };
43
+ const keys = Object.keys(globals);
44
+ const snapshot = captureGlobals(keys);
45
+ for (const key of keys) {
46
+ const value = globals[key];
47
+ if (value !== void 0) {
48
+ ;
49
+ globalThis[key] = value;
92
50
  }
93
- });
94
- return stream;
51
+ }
52
+ return () => restoreGlobals(snapshot);
95
53
  }
96
- function renderToPipeableStream(view, options = {}) {
97
- const bridge = createPipeBridge();
98
- const { shellReady, allReady, abort } = startStreamingRender(view, options, {
99
- write(chunk) {
100
- bridge.write(chunk);
101
- },
102
- close() {
103
- bridge.close();
104
- },
105
- abort(reason) {
106
- bridge.abort(reason);
107
- }
108
- });
109
- return {
110
- pipe(writable) {
111
- bridge.pipe(writable);
112
- },
113
- abort,
114
- shellReady,
115
- allReady
54
+ function installManifest(manifest) {
55
+ if (!manifest) return () => {
116
56
  };
117
- }
118
- function renderToPartial(view, options = {}) {
119
- const partialOptions = {
120
- ...options,
121
- mode: "shell",
122
- fullDocument: options.fullDocument ?? true
57
+ let resolved;
58
+ if (typeof manifest === "string") {
59
+ const raw = readTextFileFromPath(manifest);
60
+ resolved = JSON.parse(raw);
61
+ } else {
62
+ resolved = manifest;
63
+ }
64
+ const key = "__FICT_MANIFEST__";
65
+ const snapshot = {
66
+ exists: Object.prototype.hasOwnProperty.call(globalThis, key),
67
+ value: globalThis[key]
123
68
  };
124
- let shell = "";
125
- let shellPhase = true;
126
- const queued = createQueuedTextStream();
127
- const { shellReady, allReady, abort } = startStreamingRender(
128
- view,
129
- partialOptions,
130
- {
131
- write(chunk) {
132
- if (shellPhase) {
133
- shell += chunk;
134
- return;
135
- }
136
- queued.writer.write(chunk);
137
- },
138
- close() {
139
- queued.writer.close();
140
- },
141
- abort(reason) {
142
- queued.writer.abort(reason);
143
- }
144
- },
145
- {
146
- includeTailInShell: true,
147
- onShellFlushed() {
148
- shellPhase = false;
149
- }
69
+ globalThis[key] = resolved;
70
+ return () => {
71
+ if (snapshot.exists) {
72
+ ;
73
+ globalThis[key] = snapshot.value;
74
+ } else {
75
+ delete globalThis[key];
150
76
  }
151
- );
152
- return {
153
- shell,
154
- stream: queued.stream,
155
- shellReady,
156
- allReady,
157
- abort
158
77
  };
159
78
  }
160
- function resolveDom(options) {
161
- if (options.dom) {
162
- return options.dom;
163
- }
164
- if (options.document && options.window) {
165
- return { document: options.document, window: options.window };
79
+ function captureGlobals(keys) {
80
+ const snapshot = [];
81
+ for (const key of keys) {
82
+ const exists = Object.prototype.hasOwnProperty.call(globalThis, key);
83
+ const value = globalThis[key];
84
+ snapshot.push({ key, exists, value });
166
85
  }
167
- if (options.document) {
168
- const window = options.window ?? options.document.defaultView ?? options.document.defaultView ?? void 0;
169
- if (!window) {
170
- throw new Error(
171
- "[fict/ssr] A window is required when providing a document without defaultView."
172
- );
86
+ return snapshot;
87
+ }
88
+ function restoreGlobals(snapshot) {
89
+ for (const entry of snapshot) {
90
+ if (entry.exists) {
91
+ ;
92
+ globalThis[entry.key] = entry.value;
93
+ } else {
94
+ delete globalThis[entry.key];
173
95
  }
174
- return { document: options.document, window };
175
96
  }
176
- if (options.window) {
177
- return { document: options.window.document, window: options.window };
97
+ }
98
+ function readTextFileFromPath(path) {
99
+ const g = globalThis;
100
+ const deno = g.Deno;
101
+ if (deno && typeof deno.readTextFileSync === "function") {
102
+ return deno.readTextFileSync(path);
178
103
  }
179
- return createSSRDocument(options.html);
104
+ const nodeRequire = getNodeRequire();
105
+ if (nodeRequire) {
106
+ const fs = nodeRequire("node:fs");
107
+ return fs.readFileSync(path, "utf8");
108
+ }
109
+ throw new Error(
110
+ "[fict/ssr] `manifest` as file path is only supported in Node.js or Deno. Pass a manifest object in edge runtimes."
111
+ );
180
112
  }
113
+ function getNodeRequire() {
114
+ const g = globalThis;
115
+ const direct = g.require;
116
+ if (typeof direct === "function") {
117
+ return direct;
118
+ }
119
+ try {
120
+ return Function('return typeof require === "function" ? require : null')();
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ // src/stream-bridge.ts
181
127
  function createQueuedTextStream() {
182
128
  const encoder = new TextEncoder();
183
129
  const queue = [];
@@ -260,74 +206,256 @@ function createPipeBridge() {
260
206
  for (const chunk of buffer) {
261
207
  safeWrite(writable, chunk);
262
208
  }
263
- buffer.length = 0;
264
- }
265
- if (state === "closed") {
266
- safeEnd(writable);
267
- } else if (state === "aborted") {
268
- safeDestroy(writable, abortReason ?? new Error("Stream aborted"));
269
- }
270
- },
209
+ buffer.length = 0;
210
+ }
211
+ if (state === "closed") {
212
+ safeEnd(writable);
213
+ } else if (state === "aborted") {
214
+ safeDestroy(writable, abortReason ?? new Error("Stream aborted"));
215
+ }
216
+ },
217
+ write(chunk) {
218
+ if (state !== "open") return;
219
+ if (targets.size === 0) {
220
+ buffer.push(chunk);
221
+ return;
222
+ }
223
+ for (const target of targets) {
224
+ safeWrite(target, chunk);
225
+ }
226
+ },
227
+ close() {
228
+ if (state !== "open") return;
229
+ state = "closed";
230
+ for (const target of targets) {
231
+ safeEnd(target);
232
+ }
233
+ if (targets.size > 0) {
234
+ buffer.length = 0;
235
+ }
236
+ },
237
+ abort(reason) {
238
+ if (state !== "open") return;
239
+ state = "aborted";
240
+ abortReason = reason instanceof Error ? reason : new Error("Stream aborted");
241
+ for (const target of targets) {
242
+ safeDestroy(target, abortReason);
243
+ }
244
+ buffer.length = 0;
245
+ }
246
+ };
247
+ }
248
+ function createNodePipeBridge() {
249
+ const nodeRequire = getNodeRequire2();
250
+ if (!nodeRequire) return null;
251
+ try {
252
+ const streamModule = nodeRequire("node:stream");
253
+ if (!streamModule.PassThrough) return null;
254
+ const passThrough = new streamModule.PassThrough();
255
+ return {
256
+ pipe(writable) {
257
+ passThrough.pipe(writable);
258
+ },
259
+ write(chunk) {
260
+ passThrough.write(chunk);
261
+ },
262
+ close() {
263
+ passThrough.end();
264
+ },
265
+ abort(reason) {
266
+ const error = reason instanceof Error ? reason : new Error("Stream aborted");
267
+ if (typeof passThrough.destroy === "function") {
268
+ passThrough.destroy(error);
269
+ } else {
270
+ passThrough.end();
271
+ }
272
+ }
273
+ };
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+ function getNodeRequire2() {
279
+ const g = globalThis;
280
+ const direct = g.require;
281
+ if (typeof direct === "function") {
282
+ return direct;
283
+ }
284
+ try {
285
+ return Function('return typeof require === "function" ? require : null')();
286
+ } catch {
287
+ return null;
288
+ }
289
+ }
290
+
291
+ // src/index.ts
292
+ var DEFAULT_HTML = "<!doctype html><html><head></head><body></body></html>";
293
+ function createSSRDocument(html = DEFAULT_HTML) {
294
+ const window = parseHTML(html);
295
+ const document = window.document;
296
+ if (!window || !document) {
297
+ throw new Error("[fict/ssr] Failed to create DOM. Missing window or document.");
298
+ }
299
+ return { window, document };
300
+ }
301
+ function renderToDocument(view, options = {}) {
302
+ const includeSnapshot = options.includeSnapshot !== false;
303
+ __fictEnableSSR();
304
+ let dom;
305
+ let restoreGlobals2 = () => {
306
+ };
307
+ let restoreManifest = () => {
308
+ };
309
+ let container;
310
+ let teardown = () => {
311
+ };
312
+ try {
313
+ dom = resolveDom(options);
314
+ const { document, window } = dom;
315
+ const shouldExpose = options.exposeGlobals !== false;
316
+ restoreGlobals2 = shouldExpose ? installGlobals(window, document) : () => {
317
+ };
318
+ restoreManifest = installManifest(options.manifest);
319
+ container = resolveContainer(document, options);
320
+ teardown = render(view, container);
321
+ if (includeSnapshot) {
322
+ const state = __fictSerializeSSRState();
323
+ injectSnapshot(document, container, state, options);
324
+ }
325
+ } catch (error) {
326
+ __fictDisableSSR();
327
+ restoreGlobals2();
328
+ restoreManifest();
329
+ throw error;
330
+ }
331
+ __fictDisableSSR();
332
+ const html = serializeOutput(dom.document, container, options);
333
+ const dispose = () => {
334
+ try {
335
+ teardown();
336
+ } finally {
337
+ restoreGlobals2();
338
+ restoreManifest();
339
+ }
340
+ };
341
+ return { html, document: dom.document, window: dom.window, container, dispose };
342
+ }
343
+ function renderToString(view, options = {}) {
344
+ const result = renderToDocument(view, options);
345
+ const html = result.html;
346
+ result.dispose();
347
+ return html;
348
+ }
349
+ async function renderToStringAsync(view, options = {}) {
350
+ return renderToString(view, options);
351
+ }
352
+ function renderToStream(view, options = {}) {
353
+ const encoder = new TextEncoder();
354
+ let controller = null;
355
+ const stream = new ReadableStream({
356
+ start(ctrl) {
357
+ controller = ctrl;
358
+ const started = startStreamingRender(view, options, {
359
+ write(chunk) {
360
+ if (!controller) return;
361
+ controller.enqueue(encoder.encode(chunk));
362
+ },
363
+ close() {
364
+ controller?.close();
365
+ },
366
+ abort(reason) {
367
+ controller?.error(reason);
368
+ }
369
+ });
370
+ started.allReady.catch(() => void 0);
371
+ }
372
+ });
373
+ return stream;
374
+ }
375
+ function renderToPipeableStream(view, options = {}) {
376
+ const bridge = createPipeBridge();
377
+ const { shellReady, allReady, abort } = startStreamingRender(view, options, {
271
378
  write(chunk) {
272
- if (state !== "open") return;
273
- if (targets.size === 0) {
274
- buffer.push(chunk);
275
- return;
276
- }
277
- for (const target of targets) {
278
- safeWrite(target, chunk);
279
- }
379
+ bridge.write(chunk);
280
380
  },
281
381
  close() {
282
- if (state !== "open") return;
283
- state = "closed";
284
- for (const target of targets) {
285
- safeEnd(target);
286
- }
287
- if (targets.size > 0) {
288
- buffer.length = 0;
289
- }
382
+ bridge.close();
290
383
  },
291
384
  abort(reason) {
292
- if (state !== "open") return;
293
- state = "aborted";
294
- abortReason = reason instanceof Error ? reason : new Error("Stream aborted");
295
- for (const target of targets) {
296
- safeDestroy(target, abortReason);
297
- }
298
- buffer.length = 0;
385
+ bridge.abort(reason);
299
386
  }
387
+ });
388
+ return {
389
+ pipe(writable) {
390
+ bridge.pipe(writable);
391
+ },
392
+ abort,
393
+ shellReady,
394
+ allReady
300
395
  };
301
396
  }
302
- function createNodePipeBridge() {
303
- const nodeRequire = getNodeRequire();
304
- if (!nodeRequire) return null;
305
- try {
306
- const streamModule = nodeRequire("node:stream");
307
- if (!streamModule.PassThrough) return null;
308
- const passThrough = new streamModule.PassThrough();
309
- return {
310
- pipe(writable) {
311
- passThrough.pipe(writable);
312
- },
397
+ function renderToPartial(view, options = {}) {
398
+ const partialOptions = {
399
+ ...options,
400
+ mode: "shell",
401
+ fullDocument: options.fullDocument ?? true
402
+ };
403
+ let shell = "";
404
+ let shellPhase = true;
405
+ const queued = createQueuedTextStream();
406
+ const { shellReady, allReady, abort } = startStreamingRender(
407
+ view,
408
+ partialOptions,
409
+ {
313
410
  write(chunk) {
314
- passThrough.write(chunk);
411
+ if (shellPhase) {
412
+ shell += chunk;
413
+ return;
414
+ }
415
+ queued.writer.write(chunk);
315
416
  },
316
417
  close() {
317
- passThrough.end();
418
+ queued.writer.close();
318
419
  },
319
420
  abort(reason) {
320
- const error = reason instanceof Error ? reason : new Error("Stream aborted");
321
- if (typeof passThrough.destroy === "function") {
322
- passThrough.destroy(error);
323
- } else {
324
- passThrough.end();
325
- }
421
+ queued.writer.abort(reason);
326
422
  }
327
- };
328
- } catch {
329
- return null;
423
+ },
424
+ {
425
+ includeTailInShell: true,
426
+ onShellFlushed() {
427
+ shellPhase = false;
428
+ }
429
+ }
430
+ );
431
+ return {
432
+ shell,
433
+ stream: queued.stream,
434
+ shellReady,
435
+ allReady,
436
+ abort
437
+ };
438
+ }
439
+ function resolveDom(options) {
440
+ if (options.dom) {
441
+ return options.dom;
442
+ }
443
+ if (options.document && options.window) {
444
+ return { document: options.document, window: options.window };
445
+ }
446
+ if (options.document) {
447
+ const window = options.window ?? options.document.defaultView ?? options.document.defaultView ?? void 0;
448
+ if (!window) {
449
+ throw new Error(
450
+ "[fict/ssr] A window is required when providing a document without defaultView."
451
+ );
452
+ }
453
+ return { document: options.document, window };
454
+ }
455
+ if (options.window) {
456
+ return { document: options.window.document, window: options.window };
330
457
  }
458
+ return createSSRDocument(options.html);
331
459
  }
332
460
  function startStreamingRender(view, options, writer, control = {}) {
333
461
  const resolvedOptions = {
@@ -637,116 +765,6 @@ function serializeDoctype(document, override) {
637
765
  }
638
766
  return `<!DOCTYPE ${name}${id}>`;
639
767
  }
640
- function installGlobals(window, document) {
641
- const win = window;
642
- const required = {
643
- window: win,
644
- document,
645
- self: win,
646
- Node: win.Node,
647
- Element: win.Element,
648
- HTMLElement: win.HTMLElement,
649
- SVGElement: win.SVGElement,
650
- Document: win.Document,
651
- DocumentFragment: win.DocumentFragment,
652
- Text: win.Text,
653
- Comment: win.Comment
654
- };
655
- const optional = {
656
- Range: win.Range,
657
- Event: win.Event,
658
- CustomEvent: win.CustomEvent,
659
- MutationObserver: win.MutationObserver,
660
- DOMParser: win.DOMParser,
661
- getComputedStyle: win.getComputedStyle?.bind(win)
662
- };
663
- const missing = Object.entries(required).filter(([, value]) => value === void 0).map(([key]) => key);
664
- if (missing.length) {
665
- throw new Error(`[fict/ssr] Missing DOM globals: ${missing.join(", ")}`);
666
- }
667
- const globals = { ...required, ...optional };
668
- const keys = Object.keys(globals);
669
- const snapshot = captureGlobals(keys);
670
- for (const key of keys) {
671
- const value = globals[key];
672
- if (value !== void 0) {
673
- ;
674
- globalThis[key] = value;
675
- }
676
- }
677
- return () => restoreGlobals(snapshot);
678
- }
679
- function captureGlobals(keys) {
680
- const snapshot = [];
681
- for (const key of keys) {
682
- const exists = Object.prototype.hasOwnProperty.call(globalThis, key);
683
- const value = globalThis[key];
684
- snapshot.push({ key, exists, value });
685
- }
686
- return snapshot;
687
- }
688
- function restoreGlobals(snapshot) {
689
- for (const entry of snapshot) {
690
- if (entry.exists) {
691
- ;
692
- globalThis[entry.key] = entry.value;
693
- } else {
694
- delete globalThis[entry.key];
695
- }
696
- }
697
- }
698
- function readTextFileFromPath(path) {
699
- const g = globalThis;
700
- const deno = g.Deno;
701
- if (deno && typeof deno.readTextFileSync === "function") {
702
- return deno.readTextFileSync(path);
703
- }
704
- const nodeRequire = getNodeRequire();
705
- if (nodeRequire) {
706
- const fs = nodeRequire("node:fs");
707
- return fs.readFileSync(path, "utf8");
708
- }
709
- throw new Error(
710
- "[fict/ssr] `manifest` as file path is only supported in Node.js or Deno. Pass a manifest object in edge runtimes."
711
- );
712
- }
713
- function getNodeRequire() {
714
- const g = globalThis;
715
- const direct = g.require;
716
- if (typeof direct === "function") {
717
- return direct;
718
- }
719
- try {
720
- return Function('return typeof require === "function" ? require : null')();
721
- } catch {
722
- return null;
723
- }
724
- }
725
- function installManifest(manifest) {
726
- if (!manifest) return () => {
727
- };
728
- let resolved;
729
- if (typeof manifest === "string") {
730
- const raw = readTextFileFromPath(manifest);
731
- resolved = JSON.parse(raw);
732
- } else {
733
- resolved = manifest;
734
- }
735
- const key = "__FICT_MANIFEST__";
736
- const snapshot = {
737
- exists: Object.prototype.hasOwnProperty.call(globalThis, key),
738
- value: globalThis[key]
739
- };
740
- globalThis[key] = resolved;
741
- return () => {
742
- if (snapshot.exists) {
743
- ;
744
- globalThis[key] = snapshot.value;
745
- } else {
746
- delete globalThis[key];
747
- }
748
- };
749
- }
750
768
  export {
751
769
  createSSRDocument,
752
770
  renderToDocument,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fictjs/ssr",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Fict server-side rendering",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "dependencies": {
29
29
  "linkedom": "^0.18.12",
30
- "@fictjs/runtime": "0.14.0"
30
+ "@fictjs/runtime": "0.15.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "tsup": "^8.5.1"