what-server 0.8.3 → 0.10.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.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-cache 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 = signal(false);
146
- const error = signal(null);
147
- const data = signal(null);
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 = signal(initialValue);
191
- const pending = signal([]);
192
- const baseValue = signal(initialValue);
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
- batch(() => {
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
- batch(() => {
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
- batch(() => {
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((result) => ({ status: 200, body: result })).catch((error) => {
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: signal(false),
310
- error: signal(null),
311
- data: signal(null)
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,345 @@ 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 createActionHandler(options = {}) {
430
+ const { getCsrfToken: getCsrfToken2, skipCsrf = false } = options;
431
+ return async function handle(reqLike) {
432
+ const method = (reqLike.method || "POST").toUpperCase();
433
+ if (method !== "POST") {
434
+ return jsonResponse(405, { message: "Method Not Allowed" });
435
+ }
436
+ const headers = lowerHeaders(reqLike.headers);
437
+ const actionId = headers["x-what-action"];
438
+ if (!actionId) {
439
+ return jsonResponse(400, { message: "Missing X-What-Action header" });
440
+ }
441
+ const body = reqLike.body || {};
442
+ const args = body.args;
443
+ const sessionCsrfToken = skipCsrf ? void 0 : getCsrfToken2 ? await getCsrfToken2(reqLike) : void 0;
444
+ const result = await handleActionRequest(
445
+ { headers },
446
+ actionId,
447
+ args,
448
+ { csrfToken: sessionCsrfToken, skipCsrf }
449
+ );
450
+ return jsonResponse(result.status, result.body);
451
+ };
452
+ }
453
+ function nodeActionMiddleware(options = {}) {
454
+ const basePath = options.basePath || DEFAULT_BASE_PATH;
455
+ const handle = createActionHandler(options);
456
+ return async function middleware(req, res, next) {
457
+ const url = (req.url || "").split("?")[0];
458
+ if (url !== basePath || (req.method || "").toUpperCase() !== "POST") {
459
+ return next ? next() : void 0;
460
+ }
461
+ let body;
462
+ try {
463
+ body = await readJsonBody(req);
464
+ } catch (err) {
465
+ res.writeHead(err.code === "BODY_TOO_LARGE" ? 413 : 400, { "content-type": "application/json" });
466
+ res.end(JSON.stringify({ message: err.code === "BODY_TOO_LARGE" ? "Payload too large" : "Invalid JSON body" }));
467
+ return;
468
+ }
469
+ const out = await handle({ method: req.method, headers: req.headers, body });
470
+ res.writeHead(out.status, out.headers);
471
+ res.end(out.body);
472
+ };
473
+ }
474
+ function readJsonBody(req) {
475
+ return new Promise((resolve, reject) => {
476
+ let size = 0;
477
+ const chunks = [];
478
+ req.on("data", (chunk) => {
479
+ size += chunk.length;
480
+ if (size > MAX_BODY_BYTES) {
481
+ const e = new Error("Body too large");
482
+ e.code = "BODY_TOO_LARGE";
483
+ reject(e);
484
+ req.destroy?.();
485
+ return;
486
+ }
487
+ chunks.push(chunk);
488
+ });
489
+ req.on("end", () => {
490
+ if (chunks.length === 0) return resolve({});
491
+ try {
492
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
493
+ } catch (e) {
494
+ reject(e);
495
+ }
496
+ });
497
+ req.on("error", reject);
498
+ });
499
+ }
500
+ function fetchActionHandler(options = {}) {
501
+ const handle = createActionHandler(options);
502
+ return async function(request) {
503
+ let body = {};
504
+ try {
505
+ body = await request.json();
506
+ } catch {
507
+ body = {};
508
+ }
509
+ const out = await handle({ method: request.method, headers: request.headers, body });
510
+ return new Response(out.body, { status: out.status, headers: out.headers });
511
+ };
512
+ }
513
+
514
+ // packages/server/src/adapter/core.js
515
+ import { matchRoute, parseQuery } from "what-router/match";
516
+ var ACTION_PATH = "/__what_action";
517
+ var REVALIDATE_PATH = "/__what_revalidate";
518
+ function headersToObject(headers) {
519
+ const out = {};
520
+ if (headers && typeof headers.forEach === "function") headers.forEach((v, k) => {
521
+ out[k.toLowerCase()] = v;
522
+ });
523
+ return out;
524
+ }
525
+ async function readJsonBody2(request) {
526
+ try {
527
+ return await request.json();
528
+ } catch {
529
+ return {};
530
+ }
531
+ }
532
+ function defaultRenderRoute(documentOptions) {
533
+ return async function renderRoute(routeMatch) {
534
+ const { route, params, query, request } = routeMatch;
535
+ const pageModule = { default: route.component, loader: route.loader };
536
+ const html = await renderDocument(pageModule, { params, query, request }, documentOptions);
537
+ return {
538
+ html,
539
+ status: 200,
540
+ tags: routeMatch.config && routeMatch.config.tags || [],
541
+ path: routeMatch.path
542
+ };
543
+ };
544
+ }
545
+ function createRequestHandler(options = {}) {
546
+ const {
547
+ routes = [],
548
+ cache,
549
+ render,
550
+ actionHandler = createActionHandler({ skipCsrf: true }),
551
+ revalidateWebhook,
552
+ document: documentOptions = {},
553
+ notFound,
554
+ basePath = ""
555
+ } = options;
556
+ const renderRoute = render || defaultRenderRoute(documentOptions);
557
+ if (cache && (cache.revalidatePath || cache.revalidateTag)) {
558
+ setRevalidationHandler({
559
+ revalidatePath: cache.revalidatePath,
560
+ revalidateTag: cache.revalidateTag
561
+ });
562
+ }
563
+ return async function handle(request) {
564
+ const url = new URL(request.url, "http://localhost");
565
+ let pathname = url.pathname;
566
+ if (basePath && pathname.startsWith(basePath)) pathname = pathname.slice(basePath.length) || "/";
567
+ if (request.method === "POST" && pathname === ACTION_PATH) {
568
+ const body = await readJsonBody2(request);
569
+ const out2 = await actionHandler({ method: "POST", headers: headersToObject(request.headers), body });
570
+ return new Response(out2.body, { status: out2.status, headers: out2.headers });
571
+ }
572
+ if (request.method === "POST" && pathname === REVALIDATE_PATH && revalidateWebhook) {
573
+ const body = await readJsonBody2(request);
574
+ const out2 = await revalidateWebhook({ headers: headersToObject(request.headers), body });
575
+ return new Response(JSON.stringify(out2.body), {
576
+ status: out2.status,
577
+ headers: { "content-type": "application/json" }
578
+ });
579
+ }
580
+ const matched = matchRoute(pathname, routes);
581
+ if (!matched) {
582
+ const html = notFound ? notFound() : "<!DOCTYPE html><html><body><h1>404 \u2014 Not Found</h1></body></html>";
583
+ return new Response(html, { status: 404, headers: { "content-type": "text/html; charset=utf-8" } });
584
+ }
585
+ const { route, params } = matched;
586
+ const config = route.page || { mode: route.mode || "client" };
587
+ const routeMatch = { path: pathname, query: parseQuery(url.search), config, route, params, request };
588
+ if (cache && config.mode !== "server") {
589
+ const result = await cache.handle(routeMatch, () => renderRoute(routeMatch));
590
+ return new Response(result.html, {
591
+ status: result.status || 200,
592
+ headers: { "content-type": "text/html; charset=utf-8", ...result.headers || {} }
593
+ });
594
+ }
595
+ const out = await renderRoute(routeMatch);
596
+ const headers = { "content-type": "text/html; charset=utf-8" };
597
+ if (config.mode === "server") headers["Cache-Control"] = "private, no-store";
598
+ return new Response(out.html, { status: out.status || 200, headers });
599
+ };
600
+ }
601
+
602
+ // packages/server/src/adapter/node.js
603
+ import http from "node:http";
604
+ async function nodeToWebRequest(req) {
605
+ const host = req.headers.host || "localhost";
606
+ const url = `http://${host}${req.url}`;
607
+ const headers = new Headers();
608
+ for (const [k, v] of Object.entries(req.headers)) {
609
+ if (v != null) headers.set(k, Array.isArray(v) ? v.join(", ") : String(v));
610
+ }
611
+ let body;
612
+ if (req.method !== "GET" && req.method !== "HEAD") {
613
+ const chunks = [];
614
+ for await (const chunk of req) chunks.push(chunk);
615
+ if (chunks.length) body = Buffer.concat(chunks);
616
+ }
617
+ return new Request(url, { method: req.method, headers, body });
618
+ }
619
+ async function sendWebResponse(res, webRes) {
620
+ res.statusCode = webRes.status;
621
+ webRes.headers.forEach((value, key) => res.setHeader(key, value));
622
+ const text = await webRes.text();
623
+ res.end(text);
624
+ }
625
+ function toNodeListener(handler) {
626
+ return async function listener(req, res) {
627
+ try {
628
+ const webReq = await nodeToWebRequest(req);
629
+ const webRes = await handler(webReq);
630
+ await sendWebResponse(res, webRes);
631
+ } catch (err) {
632
+ if (!res.headersSent) res.writeHead(500, { "content-type": "text/html; charset=utf-8" });
633
+ res.end("<!DOCTYPE html><html><body><h1>500 \u2014 Server Error</h1></body></html>");
634
+ console.error("[what-server] request error:", err);
635
+ }
636
+ };
637
+ }
638
+ function whatMiddleware(options = {}) {
639
+ const handler = createRequestHandler(options);
640
+ return async function middleware(req, res, next) {
641
+ const webReq = await nodeToWebRequest(req);
642
+ const webRes = await handler(webReq);
643
+ if (webRes.status === 404 && typeof next === "function") return next();
644
+ await sendWebResponse(res, webRes);
645
+ };
646
+ }
647
+ function createServer(options = {}) {
648
+ const handler = createRequestHandler(options);
649
+ const server2 = http.createServer(toNodeListener(handler));
650
+ const { scheduler } = options;
651
+ if (scheduler) {
652
+ scheduler.start();
653
+ const stop = () => {
654
+ try {
655
+ scheduler.stop();
656
+ } catch {
657
+ }
658
+ server2.close();
659
+ };
660
+ process.once("SIGTERM", stop);
661
+ process.once("SIGINT", stop);
662
+ }
663
+ return server2;
664
+ }
665
+
666
+ // packages/server/src/adapter/static.js
667
+ import { mkdir, writeFile } from "node:fs/promises";
668
+ import { join } from "node:path";
669
+ import { matchRoute as matchRoute2 } from "what-router/match";
670
+ function isDynamic(path) {
671
+ return path.includes(":") || path.includes("*") || path.includes("[");
672
+ }
673
+ function buildConcretePath(pattern, params) {
674
+ return pattern.replace(/\[\.\.\.(\w+)\]/g, (_, n) => params[n] ?? "").replace(/\[(\w+)\]/g, (_, n) => params[n] ?? "").replace(/[:*](\w+)/g, (_, n) => params[n] ?? "");
675
+ }
676
+ async function exportStatic({ routes = [], outDir, render, documentOptions = {} } = {}) {
677
+ const written = [];
678
+ for (const route of routes) {
679
+ const mode = route.page && route.page.mode || route.mode;
680
+ if (mode !== "static" && mode !== "hybrid") continue;
681
+ const pageModule = { default: route.component, loader: route.loader };
682
+ let concrete = [route.path];
683
+ if (isDynamic(route.path)) {
684
+ if (typeof route.getStaticPaths !== "function") continue;
685
+ const result = await route.getStaticPaths();
686
+ concrete = (result.paths || []).map((p) => buildConcretePath(route.path, p.params || {}));
687
+ }
688
+ for (const urlPath of concrete) {
689
+ const matched = matchRoute2(urlPath, [route]);
690
+ const params = matched ? matched.params : {};
691
+ const reqCtx = { params, query: {} };
692
+ const html = render ? await render(pageModule, reqCtx) : await renderDocument(pageModule, reqCtx, documentOptions);
693
+ const dirPath = join(outDir, urlPath === "/" ? "" : urlPath);
694
+ await mkdir(dirPath, { recursive: true });
695
+ await writeFile(join(dirPath, "index.html"), html);
696
+ if (typeof route.loader === "function") {
697
+ const data = await route.loader(reqCtx);
698
+ await writeFile(join(dirPath, "__what_data.json"), serializeState({ loaderData: data }));
699
+ }
700
+ written.push(urlPath);
701
+ }
702
+ }
703
+ return { pages: written };
704
+ }
705
+
706
+ // packages/server/src/adapter/cloudflare.js
707
+ function createCloudflareHandler(options = {}) {
708
+ const handle = createRequestHandler(options);
709
+ return {
710
+ async fetch(request, env, ctx) {
711
+ if (env) request.__env = env;
712
+ if (ctx) request.__ctx = ctx;
713
+ return handle(request);
714
+ }
715
+ };
716
+ }
717
+
718
+ // packages/server/src/adapter/vercel.js
719
+ function createVercelHandler(options = {}) {
720
+ return createRequestHandler(options);
721
+ }
722
+ async function buildVercelOutput({ outDir = ".vercel/output", functionName = "render" } = {}) {
723
+ const { mkdir: mkdir2, writeFile: writeFile2 } = await import("node:fs/promises");
724
+ const { join: join2 } = await import("node:path");
725
+ await mkdir2(outDir, { recursive: true });
726
+ const config = {
727
+ version: 3,
728
+ routes: [{ src: "/.*", dest: `/${functionName}` }]
729
+ };
730
+ await writeFile2(join2(outDir, "config.json"), JSON.stringify(config, null, 2));
731
+ return { config, outDir };
732
+ }
733
+
342
734
  // packages/server/src/index.js
735
+ function createRenderContext(loaderData) {
736
+ return {
737
+ head: beginHeadCollection(),
738
+ loaderData,
739
+ resources: /* @__PURE__ */ new Map(),
740
+ resourceCounter: 0,
741
+ boundaryCounter: 0,
742
+ suspended: []
743
+ };
744
+ }
343
745
  var _hydrationIdCounter = 0;
344
746
  function resetHydrationId() {
345
747
  _hydrationIdCounter = 0;
@@ -417,6 +819,16 @@ function renderToString(vnode) {
417
819
  if (Array.isArray(vnode)) {
418
820
  return vnode.map(renderToString).join("");
419
821
  }
822
+ if (vnode.tag === "__suspense") {
823
+ try {
824
+ return (vnode.children || []).map(renderToString).join("");
825
+ } catch (e) {
826
+ if (e && typeof e.then === "function") {
827
+ return renderToString(vnode.props && vnode.props.fallback);
828
+ }
829
+ throw e;
830
+ }
831
+ }
420
832
  if (typeof vnode.tag === "function") {
421
833
  const result = vnode.tag({ ...vnode.props, children: vnode.children });
422
834
  return renderToString(result);
@@ -429,19 +841,74 @@ function renderToString(vnode) {
429
841
  const inner = rawInner != null ? String(rawInner) : children.map(renderToString).join("");
430
842
  return `${open}${inner}</${tag}>`;
431
843
  }
432
- async function* renderToStream(vnode) {
844
+ function renderToStringWithHead(vnode) {
845
+ const ctx = createRenderContext(void 0);
846
+ const body = runWithServerContext(ctx, () => renderToString(vnode));
847
+ return { body, head: endHeadCollection(ctx.head) };
848
+ }
849
+ async function renderPage(pageModule, reqCtx = {}) {
850
+ const Component = pageModule.default || pageModule;
851
+ const loaderData = typeof pageModule.loader === "function" ? await pageModule.loader(reqCtx) : void 0;
852
+ const ctx = createRenderContext(loaderData);
853
+ const params = reqCtx.params || {};
854
+ const body = runWithServerContext(
855
+ ctx,
856
+ () => renderToString(h(Component, { ...params, loaderData }))
857
+ );
858
+ return { body, head: endHeadCollection(ctx.head), loaderData };
859
+ }
860
+ var MAX_RESOLVE_PASSES = 12;
861
+ async function renderToStringAsync(vnode, ctx) {
862
+ if (!ctx) ctx = createRenderContext(void 0);
863
+ let body = "";
864
+ for (let pass = 0; pass < MAX_RESOLVE_PASSES; pass++) {
865
+ body = runWithServerContext(ctx, () => renderToString(vnode));
866
+ const pending = [...ctx.resources.values()].filter((r) => r.status === "pending").map((r) => r.promise);
867
+ if (pending.length === 0) break;
868
+ await Promise.all(pending);
869
+ }
870
+ const resources = {};
871
+ for (const [k, v] of ctx.resources) if (v.status === "ready") resources[k] = v.value;
872
+ return { body, head: endHeadCollection(ctx.head), loaderData: ctx.loaderData, resources, ctx };
873
+ }
874
+ async function renderDocument(pageModule, reqCtx = {}, options = {}) {
875
+ const Component = pageModule.default || pageModule;
876
+ const loaderData = typeof pageModule.loader === "function" ? await pageModule.loader(reqCtx) : void 0;
877
+ const ctx = createRenderContext(loaderData);
878
+ const params = reqCtx.params || {};
879
+ const { body, head, resources } = await renderToStringAsync(
880
+ h(Component, { ...params, loaderData }),
881
+ ctx
882
+ );
883
+ const payload = {
884
+ loaderData: loaderData ?? null,
885
+ resources,
886
+ islandStores: getIslandStoresSnapshot()
887
+ };
888
+ return wrapHtmlDocument({ body, head, payload, options });
889
+ }
890
+ function wrapHtmlDocument({ body, head, payload, options = {} }) {
891
+ const lang = options.lang || "en";
892
+ const dataScript = `<script id="__what_data" type="application/json">${serializeState(payload)}<\/script>`;
893
+ const clientScript = options.clientEntry ? `<script type="module" src="${escapeHtml(options.clientEntry)}"><\/script>` : "";
894
+ const extraHead = options.head || "";
895
+ const bodyClass = options.bodyClass ? ` class="${escapeHtml(options.bodyClass)}"` : "";
896
+ 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>`;
897
+ }
898
+ async function* renderToStream(vnode, ctx) {
899
+ if (ctx === void 0) ctx = createRenderContext(void 0);
433
900
  if (vnode == null || vnode === false || vnode === true) return;
434
901
  if (typeof vnode === "string" || typeof vnode === "number") {
435
902
  yield escapeHtml(String(vnode));
436
903
  return;
437
904
  }
438
905
  if (typeof vnode === "function" && vnode._signal) {
439
- yield* renderToStream(vnode());
906
+ yield* renderToStream(vnode(), ctx);
440
907
  return;
441
908
  }
442
909
  if (typeof vnode === "function") {
443
910
  try {
444
- yield* renderToStream(vnode());
911
+ yield* renderToStream(vnode(), ctx);
445
912
  } catch (e) {
446
913
  if (typeof process !== "undefined" && true) {
447
914
  console.warn("[what-server] Error rendering reactive function in stream SSR:", e.message);
@@ -451,15 +918,36 @@ async function* renderToStream(vnode) {
451
918
  }
452
919
  if (Array.isArray(vnode)) {
453
920
  for (const child of vnode) {
454
- yield* renderToStream(child);
921
+ yield* renderToStream(child, ctx);
922
+ }
923
+ return;
924
+ }
925
+ if (vnode.tag === "__suspense") {
926
+ let html = null;
927
+ for (let attempt = 0; attempt < MAX_RESOLVE_PASSES && html === null; attempt++) {
928
+ let suspended = null;
929
+ try {
930
+ html = runWithServerContext(ctx, () => (vnode.children || []).map(renderToString).join(""));
931
+ } catch (e) {
932
+ if (e && typeof e.then === "function") suspended = e;
933
+ else throw e;
934
+ }
935
+ if (html === null) {
936
+ const pending = [...ctx.resources.values()].filter((r) => r.status === "pending").map((r) => r.promise);
937
+ await Promise.all([suspended, ...pending].filter(Boolean));
938
+ }
939
+ }
940
+ if (html === null) {
941
+ html = runWithServerContext(ctx, () => renderToString(vnode.props && vnode.props.fallback));
455
942
  }
943
+ yield html;
456
944
  return;
457
945
  }
458
946
  if (typeof vnode.tag === "function") {
459
947
  try {
460
948
  const result = vnode.tag({ ...vnode.props, children: vnode.children });
461
949
  const resolved = result instanceof Promise ? await result : result;
462
- yield* renderToStream(resolved);
950
+ yield* renderToStream(resolved, ctx);
463
951
  } catch (e) {
464
952
  if (typeof process !== "undefined" && true) {
465
953
  console.warn("[what-server] Error rendering component in stream SSR:", e.message);
@@ -477,7 +965,7 @@ async function* renderToStream(vnode) {
477
965
  yield String(rawInner);
478
966
  } else {
479
967
  for (const child of children) {
480
- yield* renderToStream(child);
968
+ yield* renderToStream(child, ctx);
481
969
  }
482
970
  }
483
971
  yield `</${tag}>`;
@@ -586,7 +1074,7 @@ function isUnsafeUrlAttribute(key, val) {
586
1074
  const normalizedKey = key.toLowerCase();
587
1075
  if (!URL_ATTRS.has(normalizedKey)) return false;
588
1076
  const normalizedValue = String(val).trim().replace(/[\u0000-\u001f\u007f\s]+/g, "").toLowerCase();
589
- return normalizedValue.startsWith("javascript:") || normalizedValue.startsWith("vbscript:");
1077
+ return normalizedValue.startsWith("javascript:") || normalizedValue.startsWith("vbscript:") || normalizedValue.startsWith("data:");
590
1078
  }
591
1079
  var URL_ATTRS = /* @__PURE__ */ new Set([
592
1080
  "href",
@@ -620,23 +1108,43 @@ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
620
1108
  ]);
621
1109
  export {
622
1110
  action,
1111
+ buildVercelOutput,
1112
+ createActionHandler,
1113
+ createCloudflareHandler,
1114
+ createRequestHandler,
1115
+ createServer,
1116
+ createVercelHandler,
623
1117
  csrfMetaTag,
624
1118
  definePage,
1119
+ exportStatic,
1120
+ fetchActionHandler,
625
1121
  formAction,
626
1122
  generateCsrfToken,
627
1123
  generateStaticPage,
628
1124
  getRegisteredActions,
1125
+ getRevalidationHandler,
629
1126
  handleActionRequest,
630
1127
  invalidatePath,
1128
+ nodeActionMiddleware,
631
1129
  onRevalidate,
1130
+ renderDocument,
1131
+ renderPage,
632
1132
  renderToHydratableString,
633
1133
  renderToStream,
634
1134
  renderToString,
1135
+ renderToStringAsync,
1136
+ renderToStringWithHead,
1137
+ revalidatePath,
1138
+ revalidateTag,
1139
+ serializeState,
635
1140
  server,
1141
+ setRevalidationHandler,
1142
+ toNodeListener,
636
1143
  useAction,
637
1144
  useFormAction,
638
1145
  useMutation,
639
1146
  useOptimistic,
640
- validateCsrfToken
1147
+ validateCsrfToken,
1148
+ whatMiddleware
641
1149
  };
642
1150
  //# sourceMappingURL=index.js.map