@fictjs/ssr 0.27.0 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,55 @@ function getNodeRequire() {
22
22
  }
23
23
  //#endregion
24
24
  //#region src/globals.ts
25
- function installGlobals(window, document) {
25
+ const activeGlobalTargets = /* @__PURE__ */ new WeakSet();
26
+ const activeSharedGlobalTargets = /* @__PURE__ */ new WeakMap();
27
+ const GLOBALS_LEASE = Symbol.for("@fictjs/ssr.exposeGlobalsLease");
28
+ const SHARED_RENDER_LEASE = Symbol.for("@fictjs/ssr.sharedRenderLease");
29
+ /**
30
+ * Reserve the process-global target for an SSR render that does not expose DOM
31
+ * globals. Shared reservations may overlap each other, but they prevent a
32
+ * compatibility-global render from starting until every owner has cleaned up.
33
+ */
34
+ function acquireSharedGlobalTarget(target = globalThis) {
35
+ const local = activeSharedGlobalTargets.get(target);
36
+ if (local) {
37
+ const descriptor = Object.getOwnPropertyDescriptor(target, GLOBALS_LEASE);
38
+ if (!descriptor || !("value" in descriptor) || descriptor.value !== local.state) throw poisonedSharedLeaseError();
39
+ setSharedLeaseCount(local.state, readSharedLeaseCount(local.state) + 1);
40
+ local.count += 1;
41
+ return createSharedLeaseRelease(target, local);
42
+ }
43
+ if (activeGlobalTargets.has(target)) throw renderOverlapError();
44
+ const descriptor = Object.getOwnPropertyDescriptor(target, GLOBALS_LEASE);
45
+ let state;
46
+ if (descriptor === void 0) {
47
+ if (isDefinitelyNonExtensible(target)) return () => {};
48
+ state = createSharedLeaseState();
49
+ try {
50
+ Object.defineProperty(target, GLOBALS_LEASE, {
51
+ configurable: true,
52
+ enumerable: false,
53
+ value: state,
54
+ writable: false
55
+ });
56
+ } catch (error) {
57
+ const failure = /* @__PURE__ */ new Error(`[fict/ssr] Failed to acquire the shared SSR render lease: ${formatError(error)}`);
58
+ failure.cause = error;
59
+ throw failure;
60
+ }
61
+ } else {
62
+ if (!("value" in descriptor) || !isSharedLeaseState(descriptor.value)) throw renderOverlapError();
63
+ state = descriptor.value;
64
+ setSharedLeaseCount(state, readSharedLeaseCount(state) + 1);
65
+ }
66
+ const localLease = {
67
+ state,
68
+ count: 1
69
+ };
70
+ activeSharedGlobalTargets.set(target, localLease);
71
+ return createSharedLeaseRelease(target, localLease);
72
+ }
73
+ function installGlobals(window, document, target = globalThis) {
26
74
  const win = window;
27
75
  const required = {
28
76
  window: win,
@@ -47,17 +95,44 @@ function installGlobals(window, document) {
47
95
  };
48
96
  const missing = Object.entries(required).filter(([, value]) => value === void 0).map(([key]) => key);
49
97
  if (missing.length) throw new Error(`[fict/ssr] Missing DOM globals: ${missing.join(", ")}`);
50
- const globals = {
98
+ const globals = Object.entries({
51
99
  ...required,
52
100
  ...optional
53
- };
54
- const keys = Object.keys(globals);
55
- const snapshot = captureGlobals(keys);
56
- for (const key of keys) {
57
- const value = globals[key];
58
- if (value !== void 0) globalThis[key] = value;
101
+ }).filter(([, value]) => value !== void 0);
102
+ const lease = acquireGlobalTarget(target);
103
+ let snapshot = [];
104
+ let installingKey = "<capture>";
105
+ try {
106
+ snapshot = captureGlobals(target, globals.map(([key]) => key));
107
+ for (const [key, value] of globals) {
108
+ installingKey = key;
109
+ const previous = snapshot.find((entry) => entry.key === key)?.descriptor;
110
+ Object.defineProperty(target, key, {
111
+ configurable: true,
112
+ enumerable: previous?.enumerable ?? true,
113
+ value,
114
+ writable: true
115
+ });
116
+ }
117
+ } catch (error) {
118
+ let rollbackError;
119
+ try {
120
+ restoreGlobals(target, snapshot);
121
+ releaseGlobalTarget(target, lease);
122
+ } catch (caught) {
123
+ rollbackError = caught;
124
+ }
125
+ const failure = /* @__PURE__ */ new Error(rollbackError === void 0 ? `[fict/ssr] Failed to install DOM global \`${installingKey}\`: ${formatError(error)}` : `[fict/ssr] Failed to install DOM global \`${installingKey}\` and roll back the partial installation: ${formatError(error)}; rollback: ${formatError(rollbackError)}`);
126
+ failure.cause = error;
127
+ throw failure;
59
128
  }
60
- return () => restoreGlobals(snapshot);
129
+ let active = true;
130
+ return () => {
131
+ if (!active) return;
132
+ restoreGlobals(target, snapshot);
133
+ releaseGlobalTarget(target, lease);
134
+ active = false;
135
+ };
61
136
  }
62
137
  function installManifest(manifest) {
63
138
  if (!manifest) return () => {};
@@ -85,22 +160,120 @@ function installManifest(manifest) {
85
160
  else delete globalThis[key];
86
161
  };
87
162
  }
88
- function captureGlobals(keys) {
163
+ function captureGlobals(target, keys) {
89
164
  const snapshot = [];
90
- for (const key of keys) {
91
- const exists = Object.prototype.hasOwnProperty.call(globalThis, key);
92
- const value = globalThis[key];
93
- snapshot.push({
94
- key,
95
- exists,
96
- value
165
+ for (const key of keys) snapshot.push({
166
+ key,
167
+ descriptor: Object.getOwnPropertyDescriptor(target, key)
168
+ });
169
+ return snapshot;
170
+ }
171
+ function restoreGlobals(target, snapshot) {
172
+ let firstError;
173
+ for (let index = snapshot.length - 1; index >= 0; index--) {
174
+ const entry = snapshot[index];
175
+ if (!entry) continue;
176
+ try {
177
+ if (entry.descriptor) Object.defineProperty(target, entry.key, entry.descriptor);
178
+ else if (!Reflect.deleteProperty(target, entry.key)) throw new Error(`Could not delete newly installed global \`${entry.key}\`.`);
179
+ } catch (error) {
180
+ firstError ??= error;
181
+ }
182
+ }
183
+ if (firstError !== void 0) throw firstError;
184
+ }
185
+ function acquireGlobalTarget(target) {
186
+ if (activeGlobalTargets.has(target) || activeSharedGlobalTargets.has(target) || Object.getOwnPropertyDescriptor(target, GLOBALS_LEASE) !== void 0) throw renderOverlapError();
187
+ const lease = {};
188
+ activeGlobalTargets.add(target);
189
+ try {
190
+ Object.defineProperty(target, GLOBALS_LEASE, {
191
+ configurable: true,
192
+ enumerable: false,
193
+ value: lease,
194
+ writable: false
97
195
  });
196
+ } catch (error) {
197
+ activeGlobalTargets.delete(target);
198
+ const failure = /* @__PURE__ */ new Error(`[fict/ssr] Failed to acquire the process DOM-global compatibility lease: ${formatError(error)}`);
199
+ failure.cause = error;
200
+ throw failure;
98
201
  }
99
- return snapshot;
202
+ return lease;
203
+ }
204
+ function releaseGlobalTarget(target, lease) {
205
+ const descriptor = Object.getOwnPropertyDescriptor(target, GLOBALS_LEASE);
206
+ if (!descriptor || !("value" in descriptor) || descriptor.value !== lease) throw new Error("[fict/ssr] The process DOM-global compatibility lease changed before cleanup.");
207
+ if (!Reflect.deleteProperty(target, GLOBALS_LEASE)) throw new Error("[fict/ssr] Could not release the process DOM-global compatibility lease.");
208
+ activeGlobalTargets.delete(target);
209
+ }
210
+ function formatError(error) {
211
+ return error instanceof Error ? error.message : String(error);
212
+ }
213
+ function isDefinitelyNonExtensible(target) {
214
+ try {
215
+ return !Object.isExtensible(target);
216
+ } catch {
217
+ return false;
218
+ }
219
+ }
220
+ function createSharedLeaseState() {
221
+ const state = {};
222
+ Object.defineProperties(state, {
223
+ [SHARED_RENDER_LEASE]: {
224
+ configurable: false,
225
+ enumerable: false,
226
+ value: true,
227
+ writable: false
228
+ },
229
+ count: {
230
+ configurable: false,
231
+ enumerable: false,
232
+ value: 1,
233
+ writable: true
234
+ }
235
+ });
236
+ return state;
237
+ }
238
+ function isSharedLeaseState(value) {
239
+ if (typeof value !== "object" || value === null) return false;
240
+ const marker = Object.getOwnPropertyDescriptor(value, SHARED_RENDER_LEASE);
241
+ const count = Object.getOwnPropertyDescriptor(value, "count");
242
+ return marker !== void 0 && "value" in marker && marker.value === true && count !== void 0 && "value" in count && Number.isSafeInteger(count.value) && count.value > 0 && count.writable === true;
243
+ }
244
+ function readSharedLeaseCount(state) {
245
+ const descriptor = Object.getOwnPropertyDescriptor(state, "count");
246
+ if (!descriptor || !("value" in descriptor) || !Number.isSafeInteger(descriptor.value) || descriptor.value <= 0 || descriptor.writable !== true) throw poisonedSharedLeaseError();
247
+ return descriptor.value;
100
248
  }
101
- function restoreGlobals(snapshot) {
102
- for (const entry of snapshot) if (entry.exists) globalThis[entry.key] = entry.value;
103
- else delete globalThis[entry.key];
249
+ function setSharedLeaseCount(state, count) {
250
+ try {
251
+ Object.defineProperty(state, "count", { value: count });
252
+ } catch (error) {
253
+ const failure = poisonedSharedLeaseError();
254
+ failure.cause = error;
255
+ throw failure;
256
+ }
257
+ }
258
+ function createSharedLeaseRelease(target, local) {
259
+ let active = true;
260
+ return () => {
261
+ if (!active) return;
262
+ const descriptor = Object.getOwnPropertyDescriptor(target, GLOBALS_LEASE);
263
+ if (!descriptor || !("value" in descriptor) || descriptor.value !== local.state) throw poisonedSharedLeaseError();
264
+ const count = readSharedLeaseCount(local.state);
265
+ if (count > 1) setSharedLeaseCount(local.state, count - 1);
266
+ else if (!Reflect.deleteProperty(target, GLOBALS_LEASE)) throw new Error("[fict/ssr] Could not release the shared SSR render lease.");
267
+ local.count -= 1;
268
+ if (local.count === 0) activeSharedGlobalTargets.delete(target);
269
+ active = false;
270
+ };
271
+ }
272
+ function renderOverlapError() {
273
+ return /* @__PURE__ */ new Error("[fict/ssr] `exposeGlobals: true` cannot be used by overlapping or nested renders, including renders that do not expose globals. Keep DOM access render-local, or wait for every active render to clean up.");
274
+ }
275
+ function poisonedSharedLeaseError() {
276
+ return /* @__PURE__ */ new Error("[fict/ssr] The shared SSR render lease changed before cleanup.");
104
277
  }
105
278
  function readTextFileFromPath(path) {
106
279
  const deno = globalThis.Deno;
@@ -996,18 +1169,21 @@ function renderToDocument(view, options = {}) {
996
1169
  function renderToDocumentInSession(view, options) {
997
1170
  validateScopeIdentifierPrefix(options.scopeIdentifierPrefix);
998
1171
  const scopeIdentifierPrefix = resolveScopeIdentifierPrefix(options.scopeIdentifierPrefix);
999
- const includeSnapshot = options.includeSnapshot !== false;
1172
+ const includeSnapshot = options.includeSnapshot === true;
1000
1173
  __fictEnableSSR();
1001
1174
  __fictSetSSRScopeIdentifierPrefix(scopeIdentifierPrefix);
1002
1175
  let dom;
1003
1176
  let restoreGlobals = () => {};
1004
1177
  let restoreManifest = () => {};
1178
+ let releaseRenderLease = () => {};
1005
1179
  let container;
1006
1180
  let teardown = () => {};
1007
1181
  try {
1182
+ const shouldExpose = options.exposeGlobals === true;
1183
+ releaseRenderLease = shouldExpose ? () => {} : acquireSharedGlobalTarget();
1008
1184
  dom = resolveDom(options);
1009
1185
  const { document, window } = dom;
1010
- restoreGlobals = options.exposeGlobals === true ? installGlobals(window, document) : () => {};
1186
+ restoreGlobals = shouldExpose ? installGlobals(window, document) : () => {};
1011
1187
  restoreManifest = installManifest(options.manifest);
1012
1188
  container = resolveContainer(document, options);
1013
1189
  teardown = render(view, container);
@@ -1017,7 +1193,7 @@ function renderToDocumentInSession(view, options) {
1017
1193
  }
1018
1194
  } catch (error) {
1019
1195
  __fictDisableSSR();
1020
- cleanupRenderResources(teardown, restoreGlobals, restoreManifest, true);
1196
+ cleanupRenderResources(teardown, restoreGlobals, restoreManifest, releaseRenderLease, true);
1021
1197
  throw error;
1022
1198
  }
1023
1199
  __fictDisableSSR();
@@ -1025,10 +1201,10 @@ function renderToDocumentInSession(view, options) {
1025
1201
  try {
1026
1202
  html = serializeOutput(dom.document, container, options);
1027
1203
  } catch (error) {
1028
- cleanupRenderResources(teardown, restoreGlobals, restoreManifest, true);
1204
+ cleanupRenderResources(teardown, restoreGlobals, restoreManifest, releaseRenderLease, true);
1029
1205
  throw error;
1030
1206
  }
1031
- const dispose = () => cleanupRenderResources(teardown, restoreGlobals, restoreManifest, false);
1207
+ const dispose = () => cleanupRenderResources(teardown, restoreGlobals, restoreManifest, releaseRenderLease, false);
1032
1208
  return {
1033
1209
  html,
1034
1210
  document: dom.document,
@@ -1216,13 +1392,14 @@ function resolveDom(options) {
1216
1392
  function isPromiseLike(value) {
1217
1393
  return typeof value === "object" && value !== null && typeof value.then === "function";
1218
1394
  }
1219
- function cleanupRenderResources(teardown, restoreGlobals, restoreManifest, suppressErrors) {
1395
+ function cleanupRenderResources(teardown, restoreGlobals, restoreManifest, releaseRenderLease, suppressErrors) {
1220
1396
  let failed = false;
1221
1397
  let firstError;
1222
1398
  for (const cleanup of [
1223
1399
  teardown,
1224
1400
  restoreGlobals,
1225
- restoreManifest
1401
+ restoreManifest,
1402
+ releaseRenderLease
1226
1403
  ]) try {
1227
1404
  cleanup();
1228
1405
  } catch (error) {
@@ -1273,6 +1450,7 @@ function startStreamingRenderInSession(session, view, options, writer, releaseSe
1273
1450
  let dom = null;
1274
1451
  let restoreGlobals = () => {};
1275
1452
  let restoreManifest = () => {};
1453
+ let releaseRenderLease = () => {};
1276
1454
  let teardown = () => {};
1277
1455
  let container = null;
1278
1456
  let closed = false;
@@ -1290,7 +1468,7 @@ function startStreamingRenderInSession(session, view, options, writer, releaseSe
1290
1468
  const mode = options.mode ?? "shell";
1291
1469
  const scopeIdentifierPrefix = resolveScopeIdentifierPrefix(options.scopeIdentifierPrefix);
1292
1470
  const streamIdentifierPrefix = mode === "shell" ? resolveStreamIdentifierPrefix(options.streamIdentifierPrefix) : null;
1293
- const includeSnapshot = options.includeSnapshot !== false;
1471
+ const includeSnapshot = options.includeSnapshot === true;
1294
1472
  const sentScopeSnapshots = /* @__PURE__ */ new Map();
1295
1473
  const boundaryMap = /* @__PURE__ */ new Map();
1296
1474
  let boundaryId = 0;
@@ -1397,7 +1575,7 @@ function startStreamingRenderInSession(session, view, options, writer, releaseSe
1397
1575
  });
1398
1576
  } catch {}
1399
1577
  try {
1400
- cleanupRenderResources(teardown, restoreGlobals, restoreManifest, true);
1578
+ cleanupRenderResources(teardown, restoreGlobals, restoreManifest, releaseRenderLease, true);
1401
1579
  } finally {
1402
1580
  releaseSession();
1403
1581
  }
@@ -1553,11 +1731,14 @@ function startStreamingRenderInSession(session, view, options, writer, releaseSe
1553
1731
  removeAbortListener = () => options.signal?.removeEventListener("abort", onAbort);
1554
1732
  }
1555
1733
  try {
1734
+ validateExternalHeadSnapshotCsp(resolvedOptions, includeSnapshot, mode);
1556
1735
  __fictEnableSSR();
1557
1736
  __fictSetSSRScopeIdentifierPrefix(scopeIdentifierPrefix);
1558
1737
  __fictSetSSRStreamHooks(hooks);
1738
+ const shouldExpose = resolvedOptions.exposeGlobals === true;
1739
+ releaseRenderLease = shouldExpose ? () => {} : acquireSharedGlobalTarget();
1559
1740
  dom = resolveDom(resolvedOptions);
1560
- restoreGlobals = resolvedOptions.exposeGlobals === true ? installGlobals(dom.window, dom.document) : () => {};
1741
+ restoreGlobals = shouldExpose ? installGlobals(dom.window, dom.document) : () => {};
1561
1742
  restoreManifest = installManifest(resolvedOptions.manifest);
1562
1743
  container = resolveContainer(dom.document, resolvedOptions);
1563
1744
  teardown = render(view, container);
@@ -1632,6 +1813,9 @@ function buildStreamRuntimeScript(options) {
1632
1813
  }
1633
1814
  return `<script${nonce}>${createStreamRuntimeCode({ observerMode: resolveStreamPatchMode(options) === "observer" })}<\/script>`;
1634
1815
  }
1816
+ function validateExternalHeadSnapshotCsp(options, includeSnapshot, mode) {
1817
+ if (includeSnapshot && mode === "shell" && options.snapshotTarget === "head" && options.streamRuntime === "external" && !options.scriptNonce) throw new Error("[fict/ssr] Streaming Preview snapshots with `snapshotTarget: \"head\"` and an external runtime require a non-empty `scriptNonce`, because incremental head insertion uses an inline executable mover. Use `snapshotTarget: \"container\"` or `\"body\"` for a nonce-free strict CSP route.");
1818
+ }
1635
1819
  function buildPatchChunk(id, content, options) {
1636
1820
  const namespaceAttribute = content.namespace ? ` data-fict-patch-namespace="${content.namespace}"` : "";
1637
1821
  const html = content.namespace === "svg" ? `<svg>${content.html}</svg>` : content.namespace === "mathml" ? `<math>${content.html}</math>` : content.html;
@@ -1821,4 +2005,4 @@ function serializeDoctype(document, override) {
1821
2005
  //#endregion
1822
2006
  export { renderToStream as a, renderToPipeableStream as i, renderToDocument as n, renderToString as o, renderToPartial as r, renderToStringAsync as s, createSSRDocument as t };
1823
2007
 
1824
- //# sourceMappingURL=render-core-D0nOT_Pe.js.map
2008
+ //# sourceMappingURL=render-core-BgW-UZfD.js.map