@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.
- package/README.md +25 -7
- package/dist/experimental.cjs +1 -1
- package/dist/experimental.d.cts +1 -1
- package/dist/experimental.d.ts +1 -1
- package/dist/experimental.js +1 -1
- package/dist/experimental.node.cjs +1 -1
- package/dist/experimental.node.d.cts +1 -1
- package/dist/experimental.node.d.ts +1 -1
- package/dist/experimental.node.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.node.cjs +1 -1
- package/dist/index.node.d.cts +1 -1
- package/dist/index.node.d.ts +1 -1
- package/dist/index.node.js +1 -1
- package/dist/{render-core-D_EheVwh.cjs → render-core-BDbPxAeQ.cjs} +215 -31
- package/dist/{render-core-D0nOT_Pe.js → render-core-BgW-UZfD.js} +216 -32
- package/dist/render-core-BgW-UZfD.js.map +1 -0
- package/dist/{render-core-KCiJV_SK.d.cts → render-core-FDu-zTWN.d.cts} +18 -3
- package/dist/render-core-FDu-zTWN.d.cts.map +1 -0
- package/dist/{render-core-KCiJV_SK.d.ts → render-core-FDu-zTWN.d.ts} +18 -3
- package/dist/render-core-FDu-zTWN.d.ts.map +1 -0
- package/package.json +3 -3
- package/dist/render-core-D0nOT_Pe.js.map +0 -1
- package/dist/render-core-KCiJV_SK.d.cts.map +0 -1
- package/dist/render-core-KCiJV_SK.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# @fictjs/ssr
|
|
2
2
|
|
|
3
|
-
Fict's Server-Side Rendering (SSR) package, providing
|
|
3
|
+
Fict's Satellite Server-Side Rendering (SSR) package, providing supported
|
|
4
|
+
string and streaming rendering plus opt-in client resumability.
|
|
5
|
+
|
|
6
|
+
> **Preview** — resumability, snapshot schema v2, and partial prerendering have no
|
|
7
|
+
> semver guarantee and are excluded from Core 1.0. They require
|
|
8
|
+
> `includeSnapshot: true`, compiler `resumable: true`, and the
|
|
9
|
+
> `fict/experimental/loader` entrypoint. Basic string and streaming SSR does not
|
|
10
|
+
> enable them by default.
|
|
4
11
|
|
|
5
12
|
## Table of Contents
|
|
6
13
|
|
|
@@ -20,7 +27,9 @@ Fict's Server-Side Rendering (SSR) package, providing high-performance server-si
|
|
|
20
27
|
|
|
21
28
|
## Overview
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
Supported SSR produces HTML without a resumability snapshot by default. The
|
|
31
|
+
optional **Preview Resumability** architecture is fundamentally different from
|
|
32
|
+
traditional hydration:
|
|
24
33
|
|
|
25
34
|
| Feature | Traditional Hydration | Fict Resumability |
|
|
26
35
|
| ------------------------- | --------------------------------- | --------------------------------- |
|
|
@@ -29,7 +38,7 @@ Fict SSR adopts a **Resumability** architecture, which is fundamentally differen
|
|
|
29
38
|
| Handler Loading | All preloaded | Lazy loaded on demand |
|
|
30
39
|
| State Restoration | Re-calculated | Restored from serialized snapshot |
|
|
31
40
|
|
|
32
|
-
### How
|
|
41
|
+
### How Preview Resumability Works
|
|
33
42
|
|
|
34
43
|
```
|
|
35
44
|
┌─────────────────────────────────────────────────────────────────┐
|
|
@@ -79,7 +88,7 @@ import { renderToString } from '@fictjs/ssr'
|
|
|
79
88
|
import { App } from './App'
|
|
80
89
|
|
|
81
90
|
const html = renderToString(() => <App />, {
|
|
82
|
-
includeSnapshot: true,
|
|
91
|
+
includeSnapshot: true, // Explicit Preview snapshot opt-in (default false)
|
|
83
92
|
containerId: 'app',
|
|
84
93
|
manifest: './dist/client/fict.manifest.json',
|
|
85
94
|
})
|
|
@@ -90,7 +99,7 @@ const html = renderToString(() => <App />, {
|
|
|
90
99
|
```typescript
|
|
91
100
|
// entry-client.tsx
|
|
92
101
|
import { render } from 'fict'
|
|
93
|
-
import { installResumableLoader } from 'fict/loader'
|
|
102
|
+
import { installResumableLoader } from 'fict/experimental/loader'
|
|
94
103
|
import { App } from './App'
|
|
95
104
|
|
|
96
105
|
// Load manifest (production)
|
|
@@ -285,7 +294,7 @@ interface RenderToStringOptions {
|
|
|
285
294
|
doctype?: string | null
|
|
286
295
|
|
|
287
296
|
// Resumability Configuration
|
|
288
|
-
includeSnapshot?: boolean //
|
|
297
|
+
includeSnapshot?: boolean // Preview opt-in; default: false
|
|
289
298
|
snapshotScriptId?: string // Default: '__FICT_SNAPSHOT__'
|
|
290
299
|
snapshotTarget?: 'container' | 'body' | 'head'
|
|
291
300
|
|
|
@@ -299,7 +308,11 @@ By default SSR does not write `window`, `document`, `Node`, or related DOM const
|
|
|
299
308
|
`globalThis`. This keeps concurrent renders from racing over process-global DOM state. Components
|
|
300
309
|
should use Fict's render-provided document/ownerDocument paths; set `exposeGlobals: true` only for
|
|
301
310
|
legacy code that still reads DOM globals during server render. That compatibility mode is restored
|
|
302
|
-
on `dispose()
|
|
311
|
+
on `dispose()`. Default renders may overlap each other, but a compatibility render is exclusive
|
|
312
|
+
with every other SSR render so no request can observe another request's process-global DOM. On a
|
|
313
|
+
non-extensible hardened global object, ordinary render-local SSR remains available without a
|
|
314
|
+
process marker; compatibility mode remains unavailable because it cannot acquire its exclusive
|
|
315
|
+
lease or install DOM globals.
|
|
303
316
|
|
|
304
317
|
### renderToStringAsync
|
|
305
318
|
|
|
@@ -367,6 +380,11 @@ Trusted Types deployments should use this external observer runtime. The patch
|
|
|
367
380
|
runtime moves `<template>` content with DOM APIs (`content` + `insertBefore`) and
|
|
368
381
|
does not call `innerHTML`, `insertAdjacentHTML`, `eval`, or `Function`.
|
|
369
382
|
|
|
383
|
+
When Preview snapshots are enabled on a nonce-free strict-CSP route, keep
|
|
384
|
+
`snapshotTarget` at `container` or `body`. Incremental `head` snapshots require a
|
|
385
|
+
small executable mover; external runtime mode rejects that combination unless
|
|
386
|
+
`scriptNonce` is non-empty.
|
|
387
|
+
|
|
370
388
|
Each shell stream receives an automatic unique namespace for its Suspense patch
|
|
371
389
|
identifiers. When independently cached fragments or streams from multiple
|
|
372
390
|
services can be composed into one document, pass a stable
|
package/dist/experimental.cjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_render_core = require("./render-core-
|
|
2
|
+
const require_render_core = require("./render-core-BDbPxAeQ.cjs");
|
|
3
3
|
exports.renderToPartial = require_render_core.renderToPartial;
|
package/dist/experimental.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-
|
|
1
|
+
import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-FDu-zTWN.cjs";
|
|
2
2
|
export { type PartialPrerenderResult, renderToPartial };
|
package/dist/experimental.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-
|
|
1
|
+
import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-FDu-zTWN.js";
|
|
2
2
|
export { type PartialPrerenderResult, renderToPartial };
|
package/dist/experimental.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { r as renderToPartial } from "./render-core-
|
|
1
|
+
import { r as renderToPartial } from "./render-core-BgW-UZfD.js";
|
|
2
2
|
export { renderToPartial };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_render_core = require("./render-core-
|
|
2
|
+
const require_render_core = require("./render-core-BDbPxAeQ.cjs");
|
|
3
3
|
require("./node-session-carrier-CTo-1Awq.cjs");
|
|
4
4
|
require("./experimental.cjs");
|
|
5
5
|
exports.renderToPartial = require_render_core.renderToPartial;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-
|
|
1
|
+
import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-FDu-zTWN.cjs";
|
|
2
2
|
export { type PartialPrerenderResult, renderToPartial };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-
|
|
1
|
+
import { l as renderToPartial, t as PartialPrerenderResult } from "./render-core-FDu-zTWN.js";
|
|
2
2
|
export { type PartialPrerenderResult, renderToPartial };
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_render_core = require("./render-core-
|
|
2
|
+
const require_render_core = require("./render-core-BDbPxAeQ.cjs");
|
|
3
3
|
exports.createSSRDocument = require_render_core.createSSRDocument;
|
|
4
4
|
exports.renderToDocument = require_render_core.renderToDocument;
|
|
5
5
|
exports.renderToPipeableStream = require_render_core.renderToPipeableStream;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-
|
|
1
|
+
import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-FDu-zTWN.cjs";
|
|
2
2
|
export { type PipeableStream, type RenderToDocumentResult, type RenderToStreamOptions, type RenderToStringOptions, type SSRDom, createSSRDocument, renderToDocument, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-
|
|
1
|
+
import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-FDu-zTWN.js";
|
|
2
2
|
export { type PipeableStream, type RenderToDocumentResult, type RenderToStreamOptions, type RenderToStringOptions, type SSRDom, createSSRDocument, renderToDocument, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as renderToStream, i as renderToPipeableStream, n as renderToDocument, o as renderToString, s as renderToStringAsync, t as createSSRDocument } from "./render-core-
|
|
1
|
+
import { a as renderToStream, i as renderToPipeableStream, n as renderToDocument, o as renderToString, s as renderToStringAsync, t as createSSRDocument } from "./render-core-BgW-UZfD.js";
|
|
2
2
|
export { createSSRDocument, renderToDocument, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
|
package/dist/index.node.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_render_core = require("./render-core-
|
|
2
|
+
const require_render_core = require("./render-core-BDbPxAeQ.cjs");
|
|
3
3
|
require("./index.cjs");
|
|
4
4
|
require("./node-session-carrier-CTo-1Awq.cjs");
|
|
5
5
|
exports.createSSRDocument = require_render_core.createSSRDocument;
|
package/dist/index.node.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-
|
|
1
|
+
import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-FDu-zTWN.cjs";
|
|
2
2
|
export { type PipeableStream, type RenderToDocumentResult, type RenderToStreamOptions, type RenderToStringOptions, type SSRDom, createSSRDocument, renderToDocument, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
|
package/dist/index.node.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-
|
|
1
|
+
import { a as RenderToStringOptions, c as renderToDocument, d as renderToStream, f as renderToString, i as RenderToStreamOptions, n as PipeableStream, o as SSRDom, p as renderToStringAsync, r as RenderToDocumentResult, s as createSSRDocument, u as renderToPipeableStream } from "./render-core-FDu-zTWN.js";
|
|
2
2
|
export { type PipeableStream, type RenderToDocumentResult, type RenderToStreamOptions, type RenderToStringOptions, type SSRDom, createSSRDocument, renderToDocument, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
|
package/dist/index.node.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as renderToStream, i as renderToPipeableStream, n as renderToDocument, o as renderToString, s as renderToStringAsync, t as createSSRDocument } from "./render-core-
|
|
1
|
+
import { a as renderToStream, i as renderToPipeableStream, n as renderToDocument, o as renderToString, s as renderToStringAsync, t as createSSRDocument } from "./render-core-BgW-UZfD.js";
|
|
2
2
|
import "./index.js";
|
|
3
3
|
import "./node-session-carrier-ClqRbdoQ.js";
|
|
4
4
|
export { createSSRDocument, renderToDocument, renderToPipeableStream, renderToStream, renderToString, renderToStringAsync };
|
|
@@ -22,7 +22,55 @@ function getNodeRequire() {
|
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region src/globals.ts
|
|
25
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
1172
|
+
const includeSnapshot = options.includeSnapshot === true;
|
|
1000
1173
|
(0, _fictjs_runtime_internal.__fictEnableSSR)();
|
|
1001
1174
|
(0, _fictjs_runtime_internal.__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 =
|
|
1186
|
+
restoreGlobals = shouldExpose ? installGlobals(window, document) : () => {};
|
|
1011
1187
|
restoreManifest = installManifest(options.manifest);
|
|
1012
1188
|
container = resolveContainer(document, options);
|
|
1013
1189
|
teardown = (0, _fictjs_runtime.render)(view, container);
|
|
@@ -1017,7 +1193,7 @@ function renderToDocumentInSession(view, options) {
|
|
|
1017
1193
|
}
|
|
1018
1194
|
} catch (error) {
|
|
1019
1195
|
(0, _fictjs_runtime_internal.__fictDisableSSR)();
|
|
1020
|
-
cleanupRenderResources(teardown, restoreGlobals, restoreManifest, true);
|
|
1196
|
+
cleanupRenderResources(teardown, restoreGlobals, restoreManifest, releaseRenderLease, true);
|
|
1021
1197
|
throw error;
|
|
1022
1198
|
}
|
|
1023
1199
|
(0, _fictjs_runtime_internal.__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
|
|
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
|
(0, _fictjs_runtime_internal.__fictEnableSSR)();
|
|
1557
1736
|
(0, _fictjs_runtime_internal.__fictSetSSRScopeIdentifierPrefix)(scopeIdentifierPrefix);
|
|
1558
1737
|
(0, _fictjs_runtime_internal.__fictSetSSRStreamHooks)(hooks);
|
|
1738
|
+
const shouldExpose = resolvedOptions.exposeGlobals === true;
|
|
1739
|
+
releaseRenderLease = shouldExpose ? () => {} : acquireSharedGlobalTarget();
|
|
1559
1740
|
dom = resolveDom(resolvedOptions);
|
|
1560
|
-
restoreGlobals =
|
|
1741
|
+
restoreGlobals = shouldExpose ? installGlobals(dom.window, dom.document) : () => {};
|
|
1561
1742
|
restoreManifest = installManifest(resolvedOptions.manifest);
|
|
1562
1743
|
container = resolveContainer(dom.document, resolvedOptions);
|
|
1563
1744
|
teardown = (0, _fictjs_runtime.render)(view, container);
|
|
@@ -1632,6 +1813,9 @@ function buildStreamRuntimeScript(options) {
|
|
|
1632
1813
|
}
|
|
1633
1814
|
return `<script${nonce}>${require_stream_runtime.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;
|