what-server 0.8.4 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -0
- package/dist/actions.js +32 -1
- package/dist/actions.js.map +3 -3
- package/dist/actions.min.js +1 -1
- package/dist/actions.min.js.map +4 -4
- package/dist/index.js +743 -23
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +10 -10
- package/dist/index.min.js.map +4 -4
- package/dist/islands.js +23 -1
- package/dist/islands.js.map +3 -3
- package/dist/islands.min.js +1 -1
- package/dist/islands.min.js.map +4 -4
- package/package.json +8 -2
- package/src/action-handler.js +331 -0
- package/src/actions.js +13 -1
- package/src/adapter/cloudflare.js +18 -0
- package/src/adapter/core.js +203 -0
- package/src/adapter/node.js +77 -0
- package/src/adapter/static.js +62 -0
- package/src/adapter/vercel.js +93 -0
- package/src/index.js +203 -9
- package/src/islands.js +12 -2
- package/src/revalidation-registry.js +37 -0
- package/src/serialize.js +34 -0
package/dist/index.js
CHANGED
|
@@ -1,8 +1,63 @@
|
|
|
1
1
|
// packages/server/src/index.js
|
|
2
|
-
import { h } from "what-core";
|
|
2
|
+
import { h, runWithServerContext, beginHeadCollection, endHeadCollection } from "what-core";
|
|
3
|
+
|
|
4
|
+
// packages/server/src/serialize.js
|
|
5
|
+
var SCRIPT_UNSAFE = new RegExp("[<>&\\u2028\\u2029]", "g");
|
|
6
|
+
var ESCAPES = {
|
|
7
|
+
60: "\\u003c",
|
|
8
|
+
// <
|
|
9
|
+
62: "\\u003e",
|
|
10
|
+
// >
|
|
11
|
+
38: "\\u0026",
|
|
12
|
+
// &
|
|
13
|
+
8232: "\\u2028",
|
|
14
|
+
8233: "\\u2029"
|
|
15
|
+
};
|
|
16
|
+
function serializeState(value) {
|
|
17
|
+
return JSON.stringify(value).replace(SCRIPT_UNSAFE, (c) => ESCAPES[c.charCodeAt(0)]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// packages/server/src/islands.js
|
|
21
|
+
import { mount, hydrate, signal, batch } from "what-core";
|
|
22
|
+
var sharedStores = /* @__PURE__ */ new Map();
|
|
23
|
+
function getIslandStoresSnapshot() {
|
|
24
|
+
const data = {};
|
|
25
|
+
for (const [name, store] of sharedStores) {
|
|
26
|
+
data[name] = store._getSnapshot();
|
|
27
|
+
}
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// packages/server/src/actions.js
|
|
32
|
+
import { signal as signal2, batch as batch2 } from "what-core";
|
|
33
|
+
|
|
34
|
+
// packages/server/src/revalidation-registry.js
|
|
35
|
+
var _handler = null;
|
|
36
|
+
var isDev = typeof process !== "undefined" ? true : true;
|
|
37
|
+
function setRevalidationHandler(handler) {
|
|
38
|
+
_handler = handler;
|
|
39
|
+
}
|
|
40
|
+
function getRevalidationHandler() {
|
|
41
|
+
return _handler;
|
|
42
|
+
}
|
|
43
|
+
async function revalidatePath(path, options) {
|
|
44
|
+
if (_handler && _handler.revalidatePath) return _handler.revalidatePath(path, options);
|
|
45
|
+
if (isDev) {
|
|
46
|
+
console.warn(
|
|
47
|
+
`[what] revalidatePath('${path}') had no effect: no cache engine is bound. Create a what-isr engine and bind it in your adapter (setRevalidationHandler).`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function revalidateTag(tag, options) {
|
|
52
|
+
if (_handler && _handler.revalidateTag) return _handler.revalidateTag(tag, options);
|
|
53
|
+
if (isDev) {
|
|
54
|
+
console.warn(
|
|
55
|
+
`[what] revalidateTag('${tag}') had no effect: no cache engine is bound.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
3
59
|
|
|
4
60
|
// packages/server/src/actions.js
|
|
5
|
-
import { signal, batch } from "what-core";
|
|
6
61
|
var actionRegistry = /* @__PURE__ */ new Map();
|
|
7
62
|
function getCsrfToken() {
|
|
8
63
|
if (typeof document !== "undefined") {
|
|
@@ -142,9 +197,9 @@ function formAction(actionFn, options = {}) {
|
|
|
142
197
|
};
|
|
143
198
|
}
|
|
144
199
|
function useAction(actionFn) {
|
|
145
|
-
const isPending =
|
|
146
|
-
const error =
|
|
147
|
-
const data =
|
|
200
|
+
const isPending = signal2(false);
|
|
201
|
+
const error = signal2(null);
|
|
202
|
+
const data = signal2(null);
|
|
148
203
|
async function trigger(...args) {
|
|
149
204
|
isPending.set(true);
|
|
150
205
|
error.set(null);
|
|
@@ -187,18 +242,18 @@ function useFormAction(actionFn, options = {}) {
|
|
|
187
242
|
};
|
|
188
243
|
}
|
|
189
244
|
function useOptimistic(initialValue, reducer) {
|
|
190
|
-
const value =
|
|
191
|
-
const pending =
|
|
192
|
-
const baseValue =
|
|
245
|
+
const value = signal2(initialValue);
|
|
246
|
+
const pending = signal2([]);
|
|
247
|
+
const baseValue = signal2(initialValue);
|
|
193
248
|
function addOptimistic(action2) {
|
|
194
249
|
const optimisticValue = reducer(value.peek(), action2);
|
|
195
|
-
|
|
250
|
+
batch2(() => {
|
|
196
251
|
pending.set([...pending.peek(), action2]);
|
|
197
252
|
value.set(optimisticValue);
|
|
198
253
|
});
|
|
199
254
|
}
|
|
200
255
|
function resolve(action2, serverValue) {
|
|
201
|
-
|
|
256
|
+
batch2(() => {
|
|
202
257
|
pending.set(pending.peek().filter((a) => a !== action2));
|
|
203
258
|
if (serverValue !== void 0) {
|
|
204
259
|
baseValue.set(serverValue);
|
|
@@ -211,7 +266,7 @@ function useOptimistic(initialValue, reducer) {
|
|
|
211
266
|
});
|
|
212
267
|
}
|
|
213
268
|
function rollback(action2, realValue) {
|
|
214
|
-
|
|
269
|
+
batch2(() => {
|
|
215
270
|
const newPending = pending.peek().filter((a) => a !== action2);
|
|
216
271
|
pending.set(newPending);
|
|
217
272
|
const base = realValue !== void 0 ? realValue : baseValue.peek();
|
|
@@ -292,7 +347,16 @@ function handleActionRequest(req, actionId, args, options = {}) {
|
|
|
292
347
|
if (!Array.isArray(args)) {
|
|
293
348
|
return Promise.resolve({ status: 400, body: { message: "Invalid action arguments" } });
|
|
294
349
|
}
|
|
295
|
-
return action2.fn(...args).then(
|
|
350
|
+
return action2.fn(...args).then(async (result) => {
|
|
351
|
+
const opts = action2.options || {};
|
|
352
|
+
if (Array.isArray(opts.revalidate)) {
|
|
353
|
+
for (const p of opts.revalidate) await revalidatePath(p);
|
|
354
|
+
}
|
|
355
|
+
if (Array.isArray(opts.revalidateTags)) {
|
|
356
|
+
for (const t of opts.revalidateTags) await revalidateTag(t);
|
|
357
|
+
}
|
|
358
|
+
return { status: 200, body: result };
|
|
359
|
+
}).catch((error) => {
|
|
296
360
|
console.error(`[what] Action "${actionId}" error:`, error);
|
|
297
361
|
return {
|
|
298
362
|
status: 500,
|
|
@@ -306,9 +370,9 @@ function getRegisteredActions() {
|
|
|
306
370
|
function useMutation(mutationFn, options = {}) {
|
|
307
371
|
const { onSuccess, onError, onSettled } = options;
|
|
308
372
|
const state = {
|
|
309
|
-
isPending:
|
|
310
|
-
error:
|
|
311
|
-
data:
|
|
373
|
+
isPending: signal2(false),
|
|
374
|
+
error: signal2(null),
|
|
375
|
+
data: signal2(null)
|
|
312
376
|
};
|
|
313
377
|
async function mutate(...args) {
|
|
314
378
|
state.isPending.set(true);
|
|
@@ -339,7 +403,549 @@ function useMutation(mutationFn, options = {}) {
|
|
|
339
403
|
};
|
|
340
404
|
}
|
|
341
405
|
|
|
406
|
+
// packages/server/src/action-handler.js
|
|
407
|
+
var DEFAULT_BASE_PATH = "/__what_action";
|
|
408
|
+
var MAX_BODY_BYTES = 1024 * 1024;
|
|
409
|
+
function lowerHeaders(headers) {
|
|
410
|
+
if (!headers) return {};
|
|
411
|
+
if (typeof headers.forEach === "function" && typeof headers.get === "function") {
|
|
412
|
+
const out2 = {};
|
|
413
|
+
headers.forEach((v, k) => {
|
|
414
|
+
out2[k.toLowerCase()] = v;
|
|
415
|
+
});
|
|
416
|
+
return out2;
|
|
417
|
+
}
|
|
418
|
+
const out = {};
|
|
419
|
+
for (const k in headers) out[k.toLowerCase()] = headers[k];
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
function jsonResponse(status, bodyObj) {
|
|
423
|
+
return {
|
|
424
|
+
status,
|
|
425
|
+
headers: { "content-type": "application/json" },
|
|
426
|
+
body: JSON.stringify(bodyObj)
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function htmlResponse(status, message) {
|
|
430
|
+
return {
|
|
431
|
+
status,
|
|
432
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
433
|
+
body: `<!DOCTYPE html><html><body><h1>${status}</h1><p>${String(message).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")}</p></body></html>`
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function safeLocalPath(value) {
|
|
437
|
+
if (typeof value !== "string" || !value.startsWith("/")) return null;
|
|
438
|
+
if (/^[/\\]{2}/.test(value) || value.includes("\\")) return null;
|
|
439
|
+
try {
|
|
440
|
+
const u = new URL(value, "http://localhost");
|
|
441
|
+
if (u.origin !== "http://localhost") return null;
|
|
442
|
+
return u.pathname + u.search;
|
|
443
|
+
} catch {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function safeRedirectTarget(form, headers) {
|
|
448
|
+
const explicit = safeLocalPath(form && form._redirect);
|
|
449
|
+
if (explicit) return explicit;
|
|
450
|
+
const referer = headers.referer || headers.referrer;
|
|
451
|
+
if (referer) {
|
|
452
|
+
try {
|
|
453
|
+
const u = new URL(referer, "http://localhost");
|
|
454
|
+
const path = safeLocalPath(u.pathname + u.search);
|
|
455
|
+
if (path) return path;
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return "/";
|
|
460
|
+
}
|
|
461
|
+
var RESERVED_FORM_FIELDS = /* @__PURE__ */ new Set(["_action", "data-action", "_csrf", "_redirect"]);
|
|
462
|
+
function createActionHandler(options = {}) {
|
|
463
|
+
const { getCsrfToken: getCsrfToken2, skipCsrf = false } = options;
|
|
464
|
+
return async function handle(reqLike) {
|
|
465
|
+
const method = (reqLike.method || "POST").toUpperCase();
|
|
466
|
+
if (method !== "POST") {
|
|
467
|
+
return jsonResponse(405, { message: "Method Not Allowed" });
|
|
468
|
+
}
|
|
469
|
+
const headers = lowerHeaders(reqLike.headers);
|
|
470
|
+
const headerActionId = headers["x-what-action"];
|
|
471
|
+
const contentType = headers["content-type"] || "";
|
|
472
|
+
const isFormPost = !headerActionId && contentType.includes("application/x-www-form-urlencoded");
|
|
473
|
+
const sessionCsrfToken = skipCsrf ? void 0 : getCsrfToken2 ? await getCsrfToken2(reqLike) : void 0;
|
|
474
|
+
if (isFormPost) {
|
|
475
|
+
const form = reqLike.body || {};
|
|
476
|
+
const actionId = form._action || form["data-action"] || reqLike.query && reqLike.query.action;
|
|
477
|
+
if (!actionId) {
|
|
478
|
+
return htmlResponse(400, 'Missing action name (add a hidden "_action" field or ?action= query param)');
|
|
479
|
+
}
|
|
480
|
+
const formHeaders = { ...headers };
|
|
481
|
+
if (form._csrf && !formHeaders["x-csrf-token"]) formHeaders["x-csrf-token"] = String(form._csrf);
|
|
482
|
+
if (!skipCsrf && getCsrfToken2 && !sessionCsrfToken) {
|
|
483
|
+
return htmlResponse(403, "Missing CSRF token");
|
|
484
|
+
}
|
|
485
|
+
const data = {};
|
|
486
|
+
for (const [k, v] of Object.entries(form)) {
|
|
487
|
+
if (!RESERVED_FORM_FIELDS.has(k)) data[k] = v;
|
|
488
|
+
}
|
|
489
|
+
const result2 = await handleActionRequest(
|
|
490
|
+
{ headers: formHeaders },
|
|
491
|
+
actionId,
|
|
492
|
+
[data],
|
|
493
|
+
{ csrfToken: sessionCsrfToken, skipCsrf }
|
|
494
|
+
);
|
|
495
|
+
if (result2.status === 200) {
|
|
496
|
+
return {
|
|
497
|
+
status: 303,
|
|
498
|
+
headers: { location: safeRedirectTarget(form, headers) },
|
|
499
|
+
body: ""
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return htmlResponse(result2.status, result2.body && result2.body.message || "Action failed");
|
|
503
|
+
}
|
|
504
|
+
if (!headerActionId) {
|
|
505
|
+
return jsonResponse(400, { message: "Missing X-What-Action header" });
|
|
506
|
+
}
|
|
507
|
+
if (!skipCsrf && getCsrfToken2 && !sessionCsrfToken) {
|
|
508
|
+
return jsonResponse(403, { message: "Missing CSRF token" });
|
|
509
|
+
}
|
|
510
|
+
const body = reqLike.body || {};
|
|
511
|
+
const args = body.args;
|
|
512
|
+
const result = await handleActionRequest(
|
|
513
|
+
{ headers },
|
|
514
|
+
headerActionId,
|
|
515
|
+
args,
|
|
516
|
+
{ csrfToken: sessionCsrfToken, skipCsrf }
|
|
517
|
+
);
|
|
518
|
+
return jsonResponse(result.status, result.body);
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function nodeActionMiddleware(options = {}) {
|
|
522
|
+
const basePath = options.basePath || DEFAULT_BASE_PATH;
|
|
523
|
+
const handle = createActionHandler(options);
|
|
524
|
+
return async function middleware(req, res, next) {
|
|
525
|
+
const [url, search] = (req.url || "").split("?");
|
|
526
|
+
if (url !== basePath || (req.method || "").toUpperCase() !== "POST") {
|
|
527
|
+
return next ? next() : void 0;
|
|
528
|
+
}
|
|
529
|
+
let body;
|
|
530
|
+
try {
|
|
531
|
+
const raw = await readRawBody(req);
|
|
532
|
+
body = parseActionBody(raw, req.headers["content-type"] || "");
|
|
533
|
+
} catch (err) {
|
|
534
|
+
res.writeHead(err.code === "BODY_TOO_LARGE" ? 413 : 400, { "content-type": "application/json" });
|
|
535
|
+
res.end(JSON.stringify({ message: err.code === "BODY_TOO_LARGE" ? "Payload too large" : "Invalid request body" }));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const query = Object.fromEntries(new URLSearchParams(search || ""));
|
|
539
|
+
const out = await handle({ method: req.method, headers: req.headers, body, query });
|
|
540
|
+
res.writeHead(out.status, out.headers);
|
|
541
|
+
res.end(out.body);
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function parseActionBody(raw, contentType) {
|
|
545
|
+
if ((contentType || "").includes("application/x-www-form-urlencoded")) {
|
|
546
|
+
const fields = {};
|
|
547
|
+
for (const [k, v] of new URLSearchParams(String(raw))) {
|
|
548
|
+
if (fields[k] === void 0) fields[k] = v;
|
|
549
|
+
else if (Array.isArray(fields[k])) fields[k].push(v);
|
|
550
|
+
else fields[k] = [fields[k], v];
|
|
551
|
+
}
|
|
552
|
+
return fields;
|
|
553
|
+
}
|
|
554
|
+
if (raw == null || raw === "") return {};
|
|
555
|
+
return JSON.parse(String(raw));
|
|
556
|
+
}
|
|
557
|
+
async function readFetchBodyCapped(request, limit = MAX_BODY_BYTES) {
|
|
558
|
+
const declared = Number(request.headers.get("content-length"));
|
|
559
|
+
if (Number.isFinite(declared) && declared > limit) {
|
|
560
|
+
return { tooLarge: true };
|
|
561
|
+
}
|
|
562
|
+
const body = request.body;
|
|
563
|
+
if (!body || typeof body.getReader !== "function") {
|
|
564
|
+
const raw = await request.text();
|
|
565
|
+
if (Buffer.byteLength(raw, "utf8") > limit) return { tooLarge: true };
|
|
566
|
+
return { raw };
|
|
567
|
+
}
|
|
568
|
+
const reader = body.getReader();
|
|
569
|
+
const chunks = [];
|
|
570
|
+
let size = 0;
|
|
571
|
+
while (true) {
|
|
572
|
+
const { done, value } = await reader.read();
|
|
573
|
+
if (done) break;
|
|
574
|
+
if (value) {
|
|
575
|
+
size += value.byteLength;
|
|
576
|
+
if (size > limit) {
|
|
577
|
+
try {
|
|
578
|
+
await reader.cancel();
|
|
579
|
+
} catch {
|
|
580
|
+
}
|
|
581
|
+
return { tooLarge: true };
|
|
582
|
+
}
|
|
583
|
+
chunks.push(value);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return { raw: Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8") };
|
|
587
|
+
}
|
|
588
|
+
function readRawBody(req) {
|
|
589
|
+
return new Promise((resolve, reject) => {
|
|
590
|
+
let size = 0;
|
|
591
|
+
const chunks = [];
|
|
592
|
+
req.on("data", (chunk) => {
|
|
593
|
+
size += chunk.length;
|
|
594
|
+
if (size > MAX_BODY_BYTES) {
|
|
595
|
+
const e = new Error("Body too large");
|
|
596
|
+
e.code = "BODY_TOO_LARGE";
|
|
597
|
+
reject(e);
|
|
598
|
+
req.destroy?.();
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
chunks.push(chunk);
|
|
602
|
+
});
|
|
603
|
+
req.on("end", () => {
|
|
604
|
+
if (chunks.length === 0) return resolve("");
|
|
605
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
606
|
+
});
|
|
607
|
+
req.on("error", reject);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
function fetchActionHandler(options = {}) {
|
|
611
|
+
const handle = createActionHandler(options);
|
|
612
|
+
return async function(request) {
|
|
613
|
+
let body = {};
|
|
614
|
+
try {
|
|
615
|
+
const read = await readFetchBodyCapped(request);
|
|
616
|
+
if (read.tooLarge) {
|
|
617
|
+
return new Response(JSON.stringify({ message: "Payload too large" }), {
|
|
618
|
+
status: 413,
|
|
619
|
+
headers: { "content-type": "application/json" }
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
body = parseActionBody(read.raw, request.headers.get("content-type") || "");
|
|
623
|
+
} catch {
|
|
624
|
+
body = {};
|
|
625
|
+
}
|
|
626
|
+
let query = {};
|
|
627
|
+
try {
|
|
628
|
+
query = Object.fromEntries(new URL(request.url, "http://localhost").searchParams);
|
|
629
|
+
} catch {
|
|
630
|
+
}
|
|
631
|
+
const out = await handle({ method: request.method, headers: request.headers, body, query });
|
|
632
|
+
return new Response(out.body, { status: out.status, headers: out.headers });
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// packages/server/src/adapter/core.js
|
|
637
|
+
import { matchRoute, parseQuery } from "what-router/match";
|
|
638
|
+
var ACTION_PATH = "/__what_action";
|
|
639
|
+
var REVALIDATE_PATH = "/__what_revalidate";
|
|
640
|
+
var CSRF_COOKIE = "what-csrf";
|
|
641
|
+
function headersToObject(headers) {
|
|
642
|
+
const out = {};
|
|
643
|
+
if (headers && typeof headers.forEach === "function") headers.forEach((v, k) => {
|
|
644
|
+
out[k.toLowerCase()] = v;
|
|
645
|
+
});
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
function readCookie(cookieHeader, name) {
|
|
649
|
+
if (!cookieHeader) return null;
|
|
650
|
+
const match = String(cookieHeader).match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
|
|
651
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
652
|
+
}
|
|
653
|
+
async function readActionBody(request) {
|
|
654
|
+
try {
|
|
655
|
+
const read = await readFetchBodyCapped(request);
|
|
656
|
+
if (read.tooLarge) return { tooLarge: true };
|
|
657
|
+
return parseActionBody(read.raw, request.headers.get("content-type") || "");
|
|
658
|
+
} catch {
|
|
659
|
+
return {};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async function readJsonBody(request) {
|
|
663
|
+
try {
|
|
664
|
+
return await request.json();
|
|
665
|
+
} catch {
|
|
666
|
+
return {};
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function defaultRenderRoute(documentOptions) {
|
|
670
|
+
return async function renderRoute(routeMatch) {
|
|
671
|
+
const { route, params, query, request } = routeMatch;
|
|
672
|
+
const pageModule = { default: route.component, loader: route.loader };
|
|
673
|
+
const opts = routeMatch.csrfToken ? { ...documentOptions, csrfToken: routeMatch.csrfToken } : documentOptions;
|
|
674
|
+
const html = await renderDocument(pageModule, { params, query, request }, opts);
|
|
675
|
+
return {
|
|
676
|
+
html,
|
|
677
|
+
status: 200,
|
|
678
|
+
tags: routeMatch.config && routeMatch.config.tags || [],
|
|
679
|
+
path: routeMatch.path
|
|
680
|
+
};
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function createRequestHandler(options = {}) {
|
|
684
|
+
const {
|
|
685
|
+
routes = [],
|
|
686
|
+
cache,
|
|
687
|
+
render,
|
|
688
|
+
revalidateWebhook,
|
|
689
|
+
document: documentOptions = {},
|
|
690
|
+
notFound,
|
|
691
|
+
basePath = "",
|
|
692
|
+
csrf = true
|
|
693
|
+
} = options;
|
|
694
|
+
const autoCsrf = csrf !== false && !options.actionHandler;
|
|
695
|
+
const actionHandler = options.actionHandler || createActionHandler(
|
|
696
|
+
autoCsrf ? { getCsrfToken: (reqLike) => readCookie(reqLike.headers && reqLike.headers.cookie, CSRF_COOKIE) } : { skipCsrf: true }
|
|
697
|
+
);
|
|
698
|
+
const renderRoute = render || defaultRenderRoute(documentOptions);
|
|
699
|
+
if (cache && (cache.revalidatePath || cache.revalidateTag)) {
|
|
700
|
+
setRevalidationHandler({
|
|
701
|
+
revalidatePath: cache.revalidatePath,
|
|
702
|
+
revalidateTag: cache.revalidateTag
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
return async function handle(request) {
|
|
706
|
+
const url = new URL(request.url, "http://localhost");
|
|
707
|
+
let pathname = url.pathname;
|
|
708
|
+
if (basePath && pathname.startsWith(basePath)) pathname = pathname.slice(basePath.length) || "/";
|
|
709
|
+
if (request.method === "POST" && pathname === ACTION_PATH) {
|
|
710
|
+
const body = await readActionBody(request);
|
|
711
|
+
if (body && body.tooLarge) {
|
|
712
|
+
return new Response(JSON.stringify({ message: "Payload too large" }), {
|
|
713
|
+
status: 413,
|
|
714
|
+
headers: { "content-type": "application/json" }
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
const out2 = await actionHandler({
|
|
718
|
+
method: "POST",
|
|
719
|
+
headers: headersToObject(request.headers),
|
|
720
|
+
body,
|
|
721
|
+
query: Object.fromEntries(url.searchParams)
|
|
722
|
+
});
|
|
723
|
+
return new Response(out2.body, { status: out2.status, headers: out2.headers });
|
|
724
|
+
}
|
|
725
|
+
let csrfToken = null;
|
|
726
|
+
let csrfSetCookie = null;
|
|
727
|
+
if (autoCsrf) {
|
|
728
|
+
csrfToken = readCookie(headersToObject(request.headers).cookie, CSRF_COOKIE);
|
|
729
|
+
if (!csrfToken) {
|
|
730
|
+
csrfToken = generateCsrfToken();
|
|
731
|
+
const reqHeaders = headersToObject(request.headers);
|
|
732
|
+
const isHttps = reqHeaders["x-forwarded-proto"] === "https" || url.protocol === "https:" || false;
|
|
733
|
+
csrfSetCookie = `${CSRF_COOKIE}=${encodeURIComponent(csrfToken)}; Path=/; SameSite=Lax` + (isHttps ? "; Secure" : "");
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const withCsrfCookie = (headers2) => {
|
|
737
|
+
if (csrfSetCookie) headers2["set-cookie"] = csrfSetCookie;
|
|
738
|
+
return headers2;
|
|
739
|
+
};
|
|
740
|
+
if (request.method === "POST" && pathname === REVALIDATE_PATH && revalidateWebhook) {
|
|
741
|
+
const body = await readJsonBody(request);
|
|
742
|
+
const out2 = await revalidateWebhook({ headers: headersToObject(request.headers), body });
|
|
743
|
+
return new Response(JSON.stringify(out2.body), {
|
|
744
|
+
status: out2.status,
|
|
745
|
+
headers: { "content-type": "application/json" }
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
const matched = matchRoute(pathname, routes);
|
|
749
|
+
if (!matched) {
|
|
750
|
+
const html = notFound ? notFound() : "<!DOCTYPE html><html><body><h1>404 \u2014 Not Found</h1></body></html>";
|
|
751
|
+
return new Response(html, { status: 404, headers: withCsrfCookie({ "content-type": "text/html; charset=utf-8" }) });
|
|
752
|
+
}
|
|
753
|
+
const { route, params } = matched;
|
|
754
|
+
const config = route.page || { mode: route.mode || "client" };
|
|
755
|
+
const routeMatch = { path: pathname, query: parseQuery(url.search), config, route, params, request };
|
|
756
|
+
if (cache && config.mode !== "server") {
|
|
757
|
+
const result = await cache.handle(routeMatch, () => renderRoute(routeMatch));
|
|
758
|
+
return new Response(result.html, {
|
|
759
|
+
status: result.status || 200,
|
|
760
|
+
headers: withCsrfCookie({ "content-type": "text/html; charset=utf-8", ...result.headers || {} })
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
if (csrfToken) routeMatch.csrfToken = csrfToken;
|
|
764
|
+
const out = await renderRoute(routeMatch);
|
|
765
|
+
const headers = withCsrfCookie({ "content-type": "text/html; charset=utf-8" });
|
|
766
|
+
if (config.mode === "server") headers["Cache-Control"] = "private, no-store";
|
|
767
|
+
return new Response(out.html, { status: out.status || 200, headers });
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// packages/server/src/adapter/node.js
|
|
772
|
+
import http from "node:http";
|
|
773
|
+
async function nodeToWebRequest(req) {
|
|
774
|
+
const host = req.headers.host || "localhost";
|
|
775
|
+
const url = `http://${host}${req.url}`;
|
|
776
|
+
const headers = new Headers();
|
|
777
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
778
|
+
if (v != null) headers.set(k, Array.isArray(v) ? v.join(", ") : String(v));
|
|
779
|
+
}
|
|
780
|
+
let body;
|
|
781
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
782
|
+
const chunks = [];
|
|
783
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
784
|
+
if (chunks.length) body = Buffer.concat(chunks);
|
|
785
|
+
}
|
|
786
|
+
return new Request(url, { method: req.method, headers, body });
|
|
787
|
+
}
|
|
788
|
+
async function sendWebResponse(res, webRes) {
|
|
789
|
+
res.statusCode = webRes.status;
|
|
790
|
+
webRes.headers.forEach((value, key) => res.setHeader(key, value));
|
|
791
|
+
const text = await webRes.text();
|
|
792
|
+
res.end(text);
|
|
793
|
+
}
|
|
794
|
+
function toNodeListener(handler) {
|
|
795
|
+
return async function listener(req, res) {
|
|
796
|
+
try {
|
|
797
|
+
const webReq = await nodeToWebRequest(req);
|
|
798
|
+
const webRes = await handler(webReq);
|
|
799
|
+
await sendWebResponse(res, webRes);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
if (!res.headersSent) res.writeHead(500, { "content-type": "text/html; charset=utf-8" });
|
|
802
|
+
res.end("<!DOCTYPE html><html><body><h1>500 \u2014 Server Error</h1></body></html>");
|
|
803
|
+
console.error("[what-server] request error:", err);
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function whatMiddleware(options = {}) {
|
|
808
|
+
const handler = createRequestHandler(options);
|
|
809
|
+
return async function middleware(req, res, next) {
|
|
810
|
+
const webReq = await nodeToWebRequest(req);
|
|
811
|
+
const webRes = await handler(webReq);
|
|
812
|
+
if (webRes.status === 404 && typeof next === "function") return next();
|
|
813
|
+
await sendWebResponse(res, webRes);
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function createServer(options = {}) {
|
|
817
|
+
const handler = createRequestHandler(options);
|
|
818
|
+
const server2 = http.createServer(toNodeListener(handler));
|
|
819
|
+
const { scheduler } = options;
|
|
820
|
+
if (scheduler) {
|
|
821
|
+
scheduler.start();
|
|
822
|
+
const stop = () => {
|
|
823
|
+
try {
|
|
824
|
+
scheduler.stop();
|
|
825
|
+
} catch {
|
|
826
|
+
}
|
|
827
|
+
server2.close();
|
|
828
|
+
};
|
|
829
|
+
process.once("SIGTERM", stop);
|
|
830
|
+
process.once("SIGINT", stop);
|
|
831
|
+
}
|
|
832
|
+
return server2;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// packages/server/src/adapter/static.js
|
|
836
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
837
|
+
import { join } from "node:path";
|
|
838
|
+
import { matchRoute as matchRoute2 } from "what-router/match";
|
|
839
|
+
function isDynamic(path) {
|
|
840
|
+
return path.includes(":") || path.includes("*") || path.includes("[");
|
|
841
|
+
}
|
|
842
|
+
function buildConcretePath(pattern, params) {
|
|
843
|
+
return pattern.replace(/\[\.\.\.(\w+)\]/g, (_, n) => params[n] ?? "").replace(/\[(\w+)\]/g, (_, n) => params[n] ?? "").replace(/[:*](\w+)/g, (_, n) => params[n] ?? "");
|
|
844
|
+
}
|
|
845
|
+
async function exportStatic({ routes = [], outDir, render, documentOptions = {} } = {}) {
|
|
846
|
+
const written = [];
|
|
847
|
+
for (const route of routes) {
|
|
848
|
+
const mode = route.page && route.page.mode || route.mode;
|
|
849
|
+
if (mode !== "static" && mode !== "hybrid") continue;
|
|
850
|
+
const pageModule = { default: route.component, loader: route.loader };
|
|
851
|
+
let concrete = [route.path];
|
|
852
|
+
if (isDynamic(route.path)) {
|
|
853
|
+
if (typeof route.getStaticPaths !== "function") continue;
|
|
854
|
+
const result = await route.getStaticPaths();
|
|
855
|
+
concrete = (result.paths || []).map((p) => buildConcretePath(route.path, p.params || {}));
|
|
856
|
+
}
|
|
857
|
+
for (const urlPath of concrete) {
|
|
858
|
+
const matched = matchRoute2(urlPath, [route]);
|
|
859
|
+
const params = matched ? matched.params : {};
|
|
860
|
+
const reqCtx = { params, query: {} };
|
|
861
|
+
const html = render ? await render(pageModule, reqCtx) : await renderDocument(pageModule, reqCtx, documentOptions);
|
|
862
|
+
const dirPath = join(outDir, urlPath === "/" ? "" : urlPath);
|
|
863
|
+
await mkdir(dirPath, { recursive: true });
|
|
864
|
+
await writeFile(join(dirPath, "index.html"), html);
|
|
865
|
+
if (typeof route.loader === "function") {
|
|
866
|
+
const data = await route.loader(reqCtx);
|
|
867
|
+
await writeFile(join(dirPath, "__what_data.json"), serializeState({ loaderData: data }));
|
|
868
|
+
}
|
|
869
|
+
written.push(urlPath);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return { pages: written };
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// packages/server/src/adapter/cloudflare.js
|
|
876
|
+
function createCloudflareHandler(options = {}) {
|
|
877
|
+
const handle = createRequestHandler(options);
|
|
878
|
+
return {
|
|
879
|
+
async fetch(request, env, ctx) {
|
|
880
|
+
if (env) request.__env = env;
|
|
881
|
+
if (ctx) request.__ctx = ctx;
|
|
882
|
+
return handle(request);
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// packages/server/src/adapter/vercel.js
|
|
888
|
+
function createVercelHandler(options = {}) {
|
|
889
|
+
return createRequestHandler(options);
|
|
890
|
+
}
|
|
891
|
+
async function buildVercelOutput({
|
|
892
|
+
outDir = ".vercel/output",
|
|
893
|
+
functionName = "render",
|
|
894
|
+
runtime = "nodejs22.x",
|
|
895
|
+
files = null,
|
|
896
|
+
handler = "index.mjs",
|
|
897
|
+
staticDir = null
|
|
898
|
+
} = {}) {
|
|
899
|
+
const { mkdir: mkdir2, writeFile: writeFile2, cp } = await import("node:fs/promises");
|
|
900
|
+
const { join: join2, dirname } = await import("node:path");
|
|
901
|
+
await mkdir2(outDir, { recursive: true });
|
|
902
|
+
const config = {
|
|
903
|
+
version: 3,
|
|
904
|
+
routes: [
|
|
905
|
+
// CDN-served static assets win before the render function runs.
|
|
906
|
+
{ handle: "filesystem" },
|
|
907
|
+
{ src: "/.*", dest: `/${functionName}` }
|
|
908
|
+
]
|
|
909
|
+
};
|
|
910
|
+
await writeFile2(join2(outDir, "config.json"), JSON.stringify(config, null, 2));
|
|
911
|
+
if (staticDir) {
|
|
912
|
+
await cp(staticDir, join2(outDir, "static"), { recursive: true });
|
|
913
|
+
}
|
|
914
|
+
let functionDir = null;
|
|
915
|
+
if (files && typeof files === "object") {
|
|
916
|
+
functionDir = join2(outDir, "functions", `${functionName}.func`);
|
|
917
|
+
await mkdir2(functionDir, { recursive: true });
|
|
918
|
+
const vcConfig = {
|
|
919
|
+
runtime,
|
|
920
|
+
handler,
|
|
921
|
+
launcherType: "Nodejs",
|
|
922
|
+
shouldAddHelpers: false,
|
|
923
|
+
supportsResponseStreaming: true
|
|
924
|
+
};
|
|
925
|
+
await writeFile2(join2(functionDir, ".vc-config.json"), JSON.stringify(vcConfig, null, 2));
|
|
926
|
+
for (const [rel, contents] of Object.entries(files)) {
|
|
927
|
+
const dest = join2(functionDir, rel);
|
|
928
|
+
await mkdir2(dirname(dest), { recursive: true });
|
|
929
|
+
await writeFile2(dest, contents);
|
|
930
|
+
}
|
|
931
|
+
if (!(handler in files)) {
|
|
932
|
+
console.warn(`[what-server] buildVercelOutput: files does not include the handler entry "${handler}" \u2014 the deploy will 500 until your build emits it.`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return { config, outDir, functionDir };
|
|
936
|
+
}
|
|
937
|
+
|
|
342
938
|
// packages/server/src/index.js
|
|
939
|
+
function createRenderContext(loaderData) {
|
|
940
|
+
return {
|
|
941
|
+
head: beginHeadCollection(),
|
|
942
|
+
loaderData,
|
|
943
|
+
resources: /* @__PURE__ */ new Map(),
|
|
944
|
+
resourceCounter: 0,
|
|
945
|
+
boundaryCounter: 0,
|
|
946
|
+
suspended: []
|
|
947
|
+
};
|
|
948
|
+
}
|
|
343
949
|
var _hydrationIdCounter = 0;
|
|
344
950
|
function resetHydrationId() {
|
|
345
951
|
_hydrationIdCounter = 0;
|
|
@@ -417,6 +1023,16 @@ function renderToString(vnode) {
|
|
|
417
1023
|
if (Array.isArray(vnode)) {
|
|
418
1024
|
return vnode.map(renderToString).join("");
|
|
419
1025
|
}
|
|
1026
|
+
if (vnode.tag === "__suspense") {
|
|
1027
|
+
try {
|
|
1028
|
+
return (vnode.children || []).map(renderToString).join("");
|
|
1029
|
+
} catch (e) {
|
|
1030
|
+
if (e && typeof e.then === "function") {
|
|
1031
|
+
return renderToString(vnode.props && vnode.props.fallback);
|
|
1032
|
+
}
|
|
1033
|
+
throw e;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
420
1036
|
if (typeof vnode.tag === "function") {
|
|
421
1037
|
const result = vnode.tag({ ...vnode.props, children: vnode.children });
|
|
422
1038
|
return renderToString(result);
|
|
@@ -429,19 +1045,75 @@ function renderToString(vnode) {
|
|
|
429
1045
|
const inner = rawInner != null ? String(rawInner) : children.map(renderToString).join("");
|
|
430
1046
|
return `${open}${inner}</${tag}>`;
|
|
431
1047
|
}
|
|
432
|
-
|
|
1048
|
+
function renderToStringWithHead(vnode) {
|
|
1049
|
+
const ctx = createRenderContext(void 0);
|
|
1050
|
+
const body = runWithServerContext(ctx, () => renderToString(vnode));
|
|
1051
|
+
return { body, head: endHeadCollection(ctx.head) };
|
|
1052
|
+
}
|
|
1053
|
+
async function renderPage(pageModule, reqCtx = {}) {
|
|
1054
|
+
const Component = pageModule.default || pageModule;
|
|
1055
|
+
const loaderData = typeof pageModule.loader === "function" ? await pageModule.loader(reqCtx) : void 0;
|
|
1056
|
+
const ctx = createRenderContext(loaderData);
|
|
1057
|
+
const params = reqCtx.params || {};
|
|
1058
|
+
const body = runWithServerContext(
|
|
1059
|
+
ctx,
|
|
1060
|
+
() => renderToString(h(Component, { ...params, loaderData }))
|
|
1061
|
+
);
|
|
1062
|
+
return { body, head: endHeadCollection(ctx.head), loaderData };
|
|
1063
|
+
}
|
|
1064
|
+
var MAX_RESOLVE_PASSES = 12;
|
|
1065
|
+
async function renderToStringAsync(vnode, ctx) {
|
|
1066
|
+
if (!ctx) ctx = createRenderContext(void 0);
|
|
1067
|
+
let body = "";
|
|
1068
|
+
for (let pass = 0; pass < MAX_RESOLVE_PASSES; pass++) {
|
|
1069
|
+
body = runWithServerContext(ctx, () => renderToString(vnode));
|
|
1070
|
+
const pending = [...ctx.resources.values()].filter((r) => r.status === "pending").map((r) => r.promise);
|
|
1071
|
+
if (pending.length === 0) break;
|
|
1072
|
+
await Promise.all(pending);
|
|
1073
|
+
}
|
|
1074
|
+
const resources = {};
|
|
1075
|
+
for (const [k, v] of ctx.resources) if (v.status === "ready") resources[k] = v.value;
|
|
1076
|
+
return { body, head: endHeadCollection(ctx.head), loaderData: ctx.loaderData, resources, ctx };
|
|
1077
|
+
}
|
|
1078
|
+
async function renderDocument(pageModule, reqCtx = {}, options = {}) {
|
|
1079
|
+
const Component = pageModule.default || pageModule;
|
|
1080
|
+
const loaderData = typeof pageModule.loader === "function" ? await pageModule.loader(reqCtx) : void 0;
|
|
1081
|
+
const ctx = createRenderContext(loaderData);
|
|
1082
|
+
const params = reqCtx.params || {};
|
|
1083
|
+
const { body, head, resources } = await renderToStringAsync(
|
|
1084
|
+
h(Component, { ...params, loaderData }),
|
|
1085
|
+
ctx
|
|
1086
|
+
);
|
|
1087
|
+
const payload = {
|
|
1088
|
+
loaderData: loaderData ?? null,
|
|
1089
|
+
resources,
|
|
1090
|
+
islandStores: getIslandStoresSnapshot()
|
|
1091
|
+
};
|
|
1092
|
+
return wrapHtmlDocument({ body, head, payload, options });
|
|
1093
|
+
}
|
|
1094
|
+
function wrapHtmlDocument({ body, head, payload, options = {} }) {
|
|
1095
|
+
const lang = options.lang || "en";
|
|
1096
|
+
const dataScript = `<script id="__what_data" type="application/json">${serializeState(payload)}<\/script>`;
|
|
1097
|
+
const clientScript = options.clientEntry ? `<script type="module" src="${escapeHtml(options.clientEntry)}"><\/script>` : "";
|
|
1098
|
+
const csrfHead = options.csrfToken ? csrfMetaTag(options.csrfToken) : "";
|
|
1099
|
+
const extraHead = csrfHead + (options.head || "");
|
|
1100
|
+
const bodyClass = options.bodyClass ? ` class="${escapeHtml(options.bodyClass)}"` : "";
|
|
1101
|
+
return `<!DOCTYPE html><html lang="${escapeHtml(lang)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${head || ""}${extraHead}</head><body${bodyClass}>${body}${dataScript}${clientScript}</body></html>`;
|
|
1102
|
+
}
|
|
1103
|
+
async function* renderToStream(vnode, ctx) {
|
|
1104
|
+
if (ctx === void 0) ctx = createRenderContext(void 0);
|
|
433
1105
|
if (vnode == null || vnode === false || vnode === true) return;
|
|
434
1106
|
if (typeof vnode === "string" || typeof vnode === "number") {
|
|
435
1107
|
yield escapeHtml(String(vnode));
|
|
436
1108
|
return;
|
|
437
1109
|
}
|
|
438
1110
|
if (typeof vnode === "function" && vnode._signal) {
|
|
439
|
-
yield* renderToStream(vnode());
|
|
1111
|
+
yield* renderToStream(vnode(), ctx);
|
|
440
1112
|
return;
|
|
441
1113
|
}
|
|
442
1114
|
if (typeof vnode === "function") {
|
|
443
1115
|
try {
|
|
444
|
-
yield* renderToStream(vnode());
|
|
1116
|
+
yield* renderToStream(vnode(), ctx);
|
|
445
1117
|
} catch (e) {
|
|
446
1118
|
if (typeof process !== "undefined" && true) {
|
|
447
1119
|
console.warn("[what-server] Error rendering reactive function in stream SSR:", e.message);
|
|
@@ -451,15 +1123,36 @@ async function* renderToStream(vnode) {
|
|
|
451
1123
|
}
|
|
452
1124
|
if (Array.isArray(vnode)) {
|
|
453
1125
|
for (const child of vnode) {
|
|
454
|
-
yield* renderToStream(child);
|
|
1126
|
+
yield* renderToStream(child, ctx);
|
|
455
1127
|
}
|
|
456
1128
|
return;
|
|
457
1129
|
}
|
|
1130
|
+
if (vnode.tag === "__suspense") {
|
|
1131
|
+
let html = null;
|
|
1132
|
+
for (let attempt = 0; attempt < MAX_RESOLVE_PASSES && html === null; attempt++) {
|
|
1133
|
+
let suspended = null;
|
|
1134
|
+
try {
|
|
1135
|
+
html = runWithServerContext(ctx, () => (vnode.children || []).map(renderToString).join(""));
|
|
1136
|
+
} catch (e) {
|
|
1137
|
+
if (e && typeof e.then === "function") suspended = e;
|
|
1138
|
+
else throw e;
|
|
1139
|
+
}
|
|
1140
|
+
if (html === null) {
|
|
1141
|
+
const pending = [...ctx.resources.values()].filter((r) => r.status === "pending").map((r) => r.promise);
|
|
1142
|
+
await Promise.all([suspended, ...pending].filter(Boolean));
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (html === null) {
|
|
1146
|
+
html = runWithServerContext(ctx, () => renderToString(vnode.props && vnode.props.fallback));
|
|
1147
|
+
}
|
|
1148
|
+
yield html;
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
458
1151
|
if (typeof vnode.tag === "function") {
|
|
459
1152
|
try {
|
|
460
1153
|
const result = vnode.tag({ ...vnode.props, children: vnode.children });
|
|
461
1154
|
const resolved = result instanceof Promise ? await result : result;
|
|
462
|
-
yield* renderToStream(resolved);
|
|
1155
|
+
yield* renderToStream(resolved, ctx);
|
|
463
1156
|
} catch (e) {
|
|
464
1157
|
if (typeof process !== "undefined" && true) {
|
|
465
1158
|
console.warn("[what-server] Error rendering component in stream SSR:", e.message);
|
|
@@ -477,7 +1170,7 @@ async function* renderToStream(vnode) {
|
|
|
477
1170
|
yield String(rawInner);
|
|
478
1171
|
} else {
|
|
479
1172
|
for (const child of children) {
|
|
480
|
-
yield* renderToStream(child);
|
|
1173
|
+
yield* renderToStream(child, ctx);
|
|
481
1174
|
}
|
|
482
1175
|
}
|
|
483
1176
|
yield `</${tag}>`;
|
|
@@ -558,12 +1251,19 @@ function _resolveInnerHTML(props) {
|
|
|
558
1251
|
}
|
|
559
1252
|
return null;
|
|
560
1253
|
}
|
|
1254
|
+
var SAFE_ATTR_NAME = /^[a-zA-Z_:][a-zA-Z0-9:._-]*$/;
|
|
561
1255
|
function renderAttrs(props) {
|
|
562
1256
|
let out = "";
|
|
563
1257
|
for (const [key, val] of Object.entries(props)) {
|
|
564
1258
|
if (key === "key" || key === "ref" || key === "children" || key === "dangerouslySetInnerHTML" || key === "innerHTML") continue;
|
|
565
1259
|
if (key.startsWith("on") && key.length > 2) continue;
|
|
566
1260
|
if (val === false || val == null) continue;
|
|
1261
|
+
if (!SAFE_ATTR_NAME.test(key)) {
|
|
1262
|
+
if (_isDevMode) {
|
|
1263
|
+
console.warn(`[what-server] Skipping invalid attribute name in SSR: ${JSON.stringify(key)}`);
|
|
1264
|
+
}
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
567
1267
|
if (key === "className" || key === "class") {
|
|
568
1268
|
out += ` class="${escapeHtml(String(val))}"`;
|
|
569
1269
|
} else if (key === "style" && typeof val === "object") {
|
|
@@ -586,7 +1286,7 @@ function isUnsafeUrlAttribute(key, val) {
|
|
|
586
1286
|
const normalizedKey = key.toLowerCase();
|
|
587
1287
|
if (!URL_ATTRS.has(normalizedKey)) return false;
|
|
588
1288
|
const normalizedValue = String(val).trim().replace(/[\u0000-\u001f\u007f\s]+/g, "").toLowerCase();
|
|
589
|
-
return normalizedValue.startsWith("javascript:") || normalizedValue.startsWith("vbscript:");
|
|
1289
|
+
return normalizedValue.startsWith("javascript:") || normalizedValue.startsWith("vbscript:") || normalizedValue.startsWith("data:");
|
|
590
1290
|
}
|
|
591
1291
|
var URL_ATTRS = /* @__PURE__ */ new Set([
|
|
592
1292
|
"href",
|
|
@@ -620,23 +1320,43 @@ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
|
620
1320
|
]);
|
|
621
1321
|
export {
|
|
622
1322
|
action,
|
|
1323
|
+
buildVercelOutput,
|
|
1324
|
+
createActionHandler,
|
|
1325
|
+
createCloudflareHandler,
|
|
1326
|
+
createRequestHandler,
|
|
1327
|
+
createServer,
|
|
1328
|
+
createVercelHandler,
|
|
623
1329
|
csrfMetaTag,
|
|
624
1330
|
definePage,
|
|
1331
|
+
exportStatic,
|
|
1332
|
+
fetchActionHandler,
|
|
625
1333
|
formAction,
|
|
626
1334
|
generateCsrfToken,
|
|
627
1335
|
generateStaticPage,
|
|
628
1336
|
getRegisteredActions,
|
|
1337
|
+
getRevalidationHandler,
|
|
629
1338
|
handleActionRequest,
|
|
630
1339
|
invalidatePath,
|
|
1340
|
+
nodeActionMiddleware,
|
|
631
1341
|
onRevalidate,
|
|
1342
|
+
renderDocument,
|
|
1343
|
+
renderPage,
|
|
632
1344
|
renderToHydratableString,
|
|
633
1345
|
renderToStream,
|
|
634
1346
|
renderToString,
|
|
1347
|
+
renderToStringAsync,
|
|
1348
|
+
renderToStringWithHead,
|
|
1349
|
+
revalidatePath,
|
|
1350
|
+
revalidateTag,
|
|
1351
|
+
serializeState,
|
|
635
1352
|
server,
|
|
1353
|
+
setRevalidationHandler,
|
|
1354
|
+
toNodeListener,
|
|
636
1355
|
useAction,
|
|
637
1356
|
useFormAction,
|
|
638
1357
|
useMutation,
|
|
639
1358
|
useOptimistic,
|
|
640
|
-
validateCsrfToken
|
|
1359
|
+
validateCsrfToken,
|
|
1360
|
+
whatMiddleware
|
|
641
1361
|
};
|
|
642
1362
|
//# sourceMappingURL=index.js.map
|