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/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 = 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,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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}</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
- async function* renderToStream(vnode) {
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