@fictjs/ssr 0.20.0 → 0.21.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/README.md CHANGED
@@ -250,11 +250,17 @@ interface RenderToStringOptions {
250
250
  snapshotTarget?: 'container' | 'body' | 'head'
251
251
 
252
252
  // Runtime Configuration
253
- exposeGlobals?: boolean // Default: true
253
+ exposeGlobals?: boolean // Default: false; opt-in compatibility mode
254
254
  manifest?: Record<string, string> | string
255
255
  }
256
256
  ```
257
257
 
258
+ By default SSR does not write `window`, `document`, `Node`, or related DOM constructors to
259
+ `globalThis`. This keeps concurrent renders from racing over process-global DOM state. Components
260
+ should use Fict's render-provided document/ownerDocument paths; set `exposeGlobals: true` only for
261
+ legacy code that still reads DOM globals during server render. That compatibility mode is restored
262
+ on `dispose()`, but it is not concurrency-safe while overlapping renders are active.
263
+
258
264
  ### renderToDocument
259
265
 
260
266
  ```typescript
@@ -306,6 +312,10 @@ The asset is published as `@fictjs/ssr/fict-stream-runtime.js`. Build tools that
306
312
  need to materialize it themselves can import `createStreamRuntimeCode` from
307
313
  `@fictjs/ssr/stream-runtime`.
308
314
 
315
+ Trusted Types deployments should use this external observer runtime. The patch
316
+ runtime moves `<template>` content with DOM APIs (`content` + `insertBefore`) and
317
+ does not call `innerHTML`, `insertAdjacentHTML`, `eval`, or `Function`.
318
+
309
319
  ### renderToPipeableStream
310
320
 
311
321
  Node.js-style stream variant (compatible with `pipe()`).
@@ -324,7 +334,7 @@ await allReady
324
334
  ### renderToPartial
325
335
 
326
336
  Generate a complete shell HTML plus a deferred patch stream for Partial Prerendering workflows.
327
- This is an advanced API and currently considered **Preview** in v1.0.
337
+ This is an advanced API and currently considered **Experimental Preview** in v1.0; do not treat the return shape as frozen.
328
338
 
329
339
  ```typescript
330
340
  import { renderToPartial } from '@fictjs/ssr'
@@ -469,7 +479,7 @@ Generated detailed `fict.manifest.json` during production build, mapping virtual
469
479
 
470
480
  ## Partial Prerendering
471
481
 
472
- `renderToPartial()` enables a PPR-style split:
482
+ `renderToPartial()` is an **Experimental Preview** API that enables a PPR-style split:
473
483
 
474
484
  1. **Shell phase**: serve/cache `shell` as static-first HTML.
475
485
  2. **Deferred phase**: deliver `stream` patches for resolved Suspense boundaries.
package/dist/index.cjs CHANGED
@@ -30,10 +30,11 @@ __export(index_exports, {
30
30
  });
31
31
  module.exports = __toCommonJS(index_exports);
32
32
  var import_runtime = require("@fictjs/runtime");
33
- var import_internal = require("@fictjs/runtime/internal");
33
+ var import_internal2 = require("@fictjs/runtime/internal");
34
34
  var import_linkedom = require("linkedom");
35
35
 
36
36
  // src/globals.ts
37
+ var import_internal = require("@fictjs/runtime/internal");
37
38
  function installGlobals(window, document) {
38
39
  const win = window;
39
40
  const required = {
@@ -83,6 +84,14 @@ function installManifest(manifest) {
83
84
  } else {
84
85
  resolved = manifest;
85
86
  }
87
+ const session = (0, import_internal.__fictGetCurrentSSRSession)();
88
+ if (session) {
89
+ const previous = session.manifest;
90
+ session.manifest = resolved;
91
+ return () => {
92
+ session.manifest = previous;
93
+ };
94
+ }
86
95
  const key = "__FICT_MANIFEST__";
87
96
  const snapshot = {
88
97
  exists: Object.prototype.hasOwnProperty.call(globalThis, key),
@@ -431,12 +440,12 @@ function createSSRDocument(html = DEFAULT_HTML) {
431
440
  return { window, document };
432
441
  }
433
442
  function renderToDocument(view, options = {}) {
434
- const session = (0, import_internal.__fictCreateSSRSession)();
435
- return (0, import_internal.__fictRunWithSSRSession)(session, () => renderToDocumentInSession(view, options));
443
+ const session = (0, import_internal2.__fictCreateSSRSession)();
444
+ return (0, import_internal2.__fictRunWithSSRSession)(session, () => renderToDocumentInSession(view, options));
436
445
  }
437
446
  function renderToDocumentInSession(view, options) {
438
447
  const includeSnapshot = options.includeSnapshot !== false;
439
- (0, import_internal.__fictEnableSSR)();
448
+ (0, import_internal2.__fictEnableSSR)();
440
449
  let dom;
441
450
  let restoreGlobals2 = () => {
442
451
  };
@@ -448,23 +457,23 @@ function renderToDocumentInSession(view, options) {
448
457
  try {
449
458
  dom = resolveDom(options);
450
459
  const { document, window } = dom;
451
- const shouldExpose = options.exposeGlobals !== false;
460
+ const shouldExpose = options.exposeGlobals === true;
452
461
  restoreGlobals2 = shouldExpose ? installGlobals(window, document) : () => {
453
462
  };
454
463
  restoreManifest = installManifest(options.manifest);
455
464
  container = resolveContainer(document, options);
456
465
  teardown = (0, import_runtime.render)(view, container);
457
466
  if (includeSnapshot) {
458
- const state = (0, import_internal.__fictSerializeSSRState)();
467
+ const state = (0, import_internal2.__fictSerializeSSRState)();
459
468
  injectSnapshot(document, container, state, options);
460
469
  }
461
470
  } catch (error) {
462
- (0, import_internal.__fictDisableSSR)();
471
+ (0, import_internal2.__fictDisableSSR)();
463
472
  restoreGlobals2();
464
473
  restoreManifest();
465
474
  throw error;
466
475
  }
467
- (0, import_internal.__fictDisableSSR)();
476
+ (0, import_internal2.__fictDisableSSR)();
468
477
  const html = serializeOutput(dom.document, container, options);
469
478
  const dispose = () => {
470
479
  try {
@@ -651,14 +660,14 @@ function isPromiseLike(value) {
651
660
  return typeof value === "object" && value !== null && typeof value.then === "function";
652
661
  }
653
662
  function startStreamingRender(view, options, writer, control = {}) {
654
- const session = (0, import_internal.__fictCreateSSRSession)();
655
- return (0, import_internal.__fictRunWithSSRSession)(
663
+ const session = (0, import_internal2.__fictCreateSSRSession)();
664
+ return (0, import_internal2.__fictRunWithSSRSession)(
656
665
  session,
657
666
  () => startStreamingRenderInSession(session, view, options, writer, control)
658
667
  );
659
668
  }
660
669
  function startStreamingRenderInSession(session, view, options, writer, control = {}) {
661
- const runInSession = (fn) => (0, import_internal.__fictRunWithSSRSession)(session, fn);
670
+ const runInSession = (fn) => (0, import_internal2.__fictRunWithSSRSession)(session, fn);
662
671
  const resolvedOptions = {
663
672
  ...options,
664
673
  // Streaming requires a real document; default to fullDocument when unspecified.
@@ -779,10 +788,10 @@ function startStreamingRenderInSession(session, view, options, writer, control =
779
788
  const writeSnapshotForScopes = (scopeIds) => {
780
789
  runInSession(() => {
781
790
  if (!includeSnapshot || scopeIds.length === 0) return;
782
- const registry = (0, import_internal.__fictGetScopeRegistry)();
791
+ const registry = (0, import_internal2.__fictGetScopeRegistry)();
783
792
  const pending = scopeIds.filter((id) => registry.has(id) && !sentScopes.has(id));
784
793
  if (pending.length === 0) return;
785
- const snapshot = (0, import_internal.__fictSerializeSSRStateForScopes)(pending);
794
+ const snapshot = (0, import_internal2.__fictSerializeSSRStateForScopes)(pending);
786
795
  const ids = Object.keys(snapshot.scopes);
787
796
  if (ids.length === 0) return;
788
797
  const chunk = buildIncrementalSnapshotChunk(snapshot, resolvedOptions);
@@ -796,13 +805,13 @@ function startStreamingRenderInSession(session, view, options, writer, control =
796
805
  };
797
806
  const writeSnapshotForBoundary = (boundary) => {
798
807
  runInSession(() => {
799
- const scopes = (0, import_internal.__fictGetScopesForBoundary)(boundary);
808
+ const scopes = (0, import_internal2.__fictGetScopesForBoundary)(boundary);
800
809
  writeSnapshotForScopes(scopes);
801
810
  });
802
811
  };
803
812
  const writeRemainingSnapshots = () => {
804
813
  runInSession(() => {
805
- const scopes = Array.from((0, import_internal.__fictGetScopeRegistry)().keys());
814
+ const scopes = Array.from((0, import_internal2.__fictGetScopeRegistry)().keys());
806
815
  writeSnapshotForScopes(scopes);
807
816
  });
808
817
  };
@@ -811,14 +820,15 @@ function startStreamingRenderInSession(session, view, options, writer, control =
811
820
  removeAbortListener = () => {
812
821
  };
813
822
  runInSession(() => {
814
- (0, import_internal.__fictSetSSRStreamHooks)(null);
815
- (0, import_internal.__fictDisableSSR)();
823
+ (0, import_internal2.__fictSetSSRStreamHooks)(null);
824
+ (0, import_internal2.__fictDisableSSR)();
816
825
  });
817
- restoreGlobals2();
818
- restoreManifest();
819
826
  try {
820
827
  teardown();
821
828
  } catch {
829
+ } finally {
830
+ restoreGlobals2();
831
+ restoreManifest();
822
832
  }
823
833
  };
824
834
  const finalize = () => {
@@ -826,7 +836,7 @@ function startStreamingRenderInSession(session, view, options, writer, control =
826
836
  closed = true;
827
837
  if (mode === "all" && dom && container && !wroteShell) {
828
838
  if (includeSnapshot) {
829
- const snapshot = (0, import_internal.__fictSerializeSSRState)();
839
+ const snapshot = (0, import_internal2.__fictSerializeSSRState)();
830
840
  injectSnapshot(dom.document, container, snapshot, resolvedOptions);
831
841
  }
832
842
  const fullHtml = serializeOutput(dom.document, container, resolvedOptions);
@@ -911,10 +921,10 @@ function startStreamingRenderInSession(session, view, options, writer, control =
911
921
  }
912
922
  }
913
923
  try {
914
- (0, import_internal.__fictEnableSSR)();
915
- (0, import_internal.__fictSetSSRStreamHooks)(hooks);
924
+ (0, import_internal2.__fictEnableSSR)();
925
+ (0, import_internal2.__fictSetSSRStreamHooks)(hooks);
916
926
  dom = resolveDom(resolvedOptions);
917
- restoreGlobals2 = resolvedOptions.exposeGlobals !== false ? installGlobals(dom.window, dom.document) : () => {
927
+ restoreGlobals2 = resolvedOptions.exposeGlobals === true ? installGlobals(dom.window, dom.document) : () => {
918
928
  };
919
929
  restoreManifest = installManifest(resolvedOptions.manifest);
920
930
  container = resolveContainer(dom.document, resolvedOptions);
@@ -944,7 +954,7 @@ function startStreamingRenderInSession(session, view, options, writer, control =
944
954
  enqueueWrite(shellHtml + streamRuntime);
945
955
  }
946
956
  wroteShell = true;
947
- writeSnapshotForScopes(Array.from((0, import_internal.__fictGetScopeRegistry)().keys()));
957
+ writeSnapshotForScopes(Array.from((0, import_internal2.__fictGetScopeRegistry)().keys()));
948
958
  if (shellCarriesTail && tailHtml) {
949
959
  enqueueWrite(tailHtml);
950
960
  tailHtml = "";
package/dist/index.d.cts CHANGED
@@ -53,7 +53,8 @@ interface RenderToStringOptions {
53
53
  doctype?: string | null;
54
54
  /**
55
55
  * Expose DOM globals (window/document/Node/Element/etc) during render.
56
- * Defaults to true.
56
+ * Defaults to false. Set to true only for compatibility with components
57
+ * that still read process-global DOM objects during server rendering.
57
58
  */
58
59
  exposeGlobals?: boolean;
59
60
  /**
@@ -128,6 +129,9 @@ interface PipeableStream {
128
129
  interface PartialPrerenderResult {
129
130
  /**
130
131
  * Complete shell HTML (fallbacks + markers + initial snapshot scripts).
132
+ *
133
+ * @experimental Preview API for v1.0; the access pattern may change before
134
+ * this becomes stable.
131
135
  */
132
136
  shell: string;
133
137
  /**
@@ -149,6 +153,10 @@ declare function renderToString(view: () => FictNode, options?: RenderToStringOp
149
153
  declare function renderToStringAsync(view: () => FictNode, options?: RenderToStringOptions): Promise<string>;
150
154
  declare function renderToStream(view: () => FictNode, options?: RenderToStreamOptions): ReadableStream<Uint8Array>;
151
155
  declare function renderToPipeableStream(view: () => FictNode, options?: RenderToStreamOptions): PipeableStream;
156
+ /**
157
+ * @experimental Preview API for v1.0; the return shape may change before this
158
+ * becomes stable.
159
+ */
152
160
  declare function renderToPartial(view: () => FictNode, options?: RenderToStreamOptions): PartialPrerenderResult;
153
161
 
154
162
  export { type PartialPrerenderResult, type PipeableStream, type RenderToDocumentResult, type RenderToStreamOptions, type RenderToStringOptions, type SSRDom, createSSRDocument, renderToDocument, renderToPartial, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
package/dist/index.d.ts CHANGED
@@ -53,7 +53,8 @@ interface RenderToStringOptions {
53
53
  doctype?: string | null;
54
54
  /**
55
55
  * Expose DOM globals (window/document/Node/Element/etc) during render.
56
- * Defaults to true.
56
+ * Defaults to false. Set to true only for compatibility with components
57
+ * that still read process-global DOM objects during server rendering.
57
58
  */
58
59
  exposeGlobals?: boolean;
59
60
  /**
@@ -128,6 +129,9 @@ interface PipeableStream {
128
129
  interface PartialPrerenderResult {
129
130
  /**
130
131
  * Complete shell HTML (fallbacks + markers + initial snapshot scripts).
132
+ *
133
+ * @experimental Preview API for v1.0; the access pattern may change before
134
+ * this becomes stable.
131
135
  */
132
136
  shell: string;
133
137
  /**
@@ -149,6 +153,10 @@ declare function renderToString(view: () => FictNode, options?: RenderToStringOp
149
153
  declare function renderToStringAsync(view: () => FictNode, options?: RenderToStringOptions): Promise<string>;
150
154
  declare function renderToStream(view: () => FictNode, options?: RenderToStreamOptions): ReadableStream<Uint8Array>;
151
155
  declare function renderToPipeableStream(view: () => FictNode, options?: RenderToStreamOptions): PipeableStream;
156
+ /**
157
+ * @experimental Preview API for v1.0; the return shape may change before this
158
+ * becomes stable.
159
+ */
152
160
  declare function renderToPartial(view: () => FictNode, options?: RenderToStreamOptions): PartialPrerenderResult;
153
161
 
154
162
  export { type PartialPrerenderResult, type PipeableStream, type RenderToDocumentResult, type RenderToStreamOptions, type RenderToStringOptions, type SSRDom, createSSRDocument, renderToDocument, renderToPartial, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  import { parseHTML } from "linkedom";
19
19
 
20
20
  // src/globals.ts
21
+ import { __fictGetCurrentSSRSession } from "@fictjs/runtime/internal";
21
22
  function installGlobals(window, document) {
22
23
  const win = window;
23
24
  const required = {
@@ -67,6 +68,14 @@ function installManifest(manifest) {
67
68
  } else {
68
69
  resolved = manifest;
69
70
  }
71
+ const session = __fictGetCurrentSSRSession();
72
+ if (session) {
73
+ const previous = session.manifest;
74
+ session.manifest = resolved;
75
+ return () => {
76
+ session.manifest = previous;
77
+ };
78
+ }
70
79
  const key = "__FICT_MANIFEST__";
71
80
  const snapshot = {
72
81
  exists: Object.prototype.hasOwnProperty.call(globalThis, key),
@@ -425,7 +434,7 @@ function renderToDocumentInSession(view, options) {
425
434
  try {
426
435
  dom = resolveDom(options);
427
436
  const { document, window } = dom;
428
- const shouldExpose = options.exposeGlobals !== false;
437
+ const shouldExpose = options.exposeGlobals === true;
429
438
  restoreGlobals2 = shouldExpose ? installGlobals(window, document) : () => {
430
439
  };
431
440
  restoreManifest = installManifest(options.manifest);
@@ -791,11 +800,12 @@ function startStreamingRenderInSession(session, view, options, writer, control =
791
800
  __fictSetSSRStreamHooks(null);
792
801
  __fictDisableSSR();
793
802
  });
794
- restoreGlobals2();
795
- restoreManifest();
796
803
  try {
797
804
  teardown();
798
805
  } catch {
806
+ } finally {
807
+ restoreGlobals2();
808
+ restoreManifest();
799
809
  }
800
810
  };
801
811
  const finalize = () => {
@@ -891,7 +901,7 @@ function startStreamingRenderInSession(session, view, options, writer, control =
891
901
  __fictEnableSSR();
892
902
  __fictSetSSRStreamHooks(hooks);
893
903
  dom = resolveDom(resolvedOptions);
894
- restoreGlobals2 = resolvedOptions.exposeGlobals !== false ? installGlobals(dom.window, dom.document) : () => {
904
+ restoreGlobals2 = resolvedOptions.exposeGlobals === true ? installGlobals(dom.window, dom.document) : () => {
895
905
  };
896
906
  restoreManifest = installManifest(resolvedOptions.manifest);
897
907
  container = resolveContainer(dom.document, resolvedOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fictjs/ssr",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Fict server-side rendering",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -33,11 +33,11 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "linkedom": "^0.18.12",
36
- "@fictjs/runtime": "0.20.0"
36
+ "@fictjs/runtime": "0.21.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "tsup": "^8.5.1",
40
- "fict": "0.20.0"
40
+ "fict": "0.21.0"
41
41
  },
42
42
  "keywords": [
43
43
  "fict",