@fictjs/ssr 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +334 -316
- package/dist/index.js +334 -316
- 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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
73
|
+
}
|
|
74
|
+
return () => restoreGlobals(snapshot);
|
|
117
75
|
}
|
|
118
|
-
function
|
|
119
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
433
|
+
if (shellPhase) {
|
|
434
|
+
shell += chunk;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
queued.writer.write(chunk);
|
|
337
438
|
},
|
|
338
439
|
close() {
|
|
339
|
-
|
|
440
|
+
queued.writer.close();
|
|
340
441
|
},
|
|
341
442
|
abort(reason) {
|
|
342
|
-
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
51
|
+
}
|
|
52
|
+
return () => restoreGlobals(snapshot);
|
|
95
53
|
}
|
|
96
|
-
function
|
|
97
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
411
|
+
if (shellPhase) {
|
|
412
|
+
shell += chunk;
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
queued.writer.write(chunk);
|
|
315
416
|
},
|
|
316
417
|
close() {
|
|
317
|
-
|
|
418
|
+
queued.writer.close();
|
|
318
419
|
},
|
|
319
420
|
abort(reason) {
|
|
320
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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.
|
|
3
|
+
"version": "0.16.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.
|
|
30
|
+
"@fictjs/runtime": "0.16.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"tsup": "^8.5.1"
|