what-server 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js DELETED
@@ -1,1150 +0,0 @@
1
- // packages/server/src/index.js
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
- }
59
-
60
- // packages/server/src/actions.js
61
- var actionRegistry = /* @__PURE__ */ new Map();
62
- function getCsrfToken() {
63
- if (typeof document !== "undefined") {
64
- const meta = document.querySelector('meta[name="what-csrf-token"]');
65
- if (meta) {
66
- return meta.getAttribute("content");
67
- }
68
- const match = document.cookie.match(/(?:^|;\s*)what-csrf=([^;]+)/);
69
- if (match) {
70
- return decodeURIComponent(match[1]);
71
- }
72
- }
73
- return null;
74
- }
75
- function generateCsrfToken() {
76
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
77
- return crypto.randomUUID();
78
- }
79
- if (typeof crypto !== "undefined" && crypto.getRandomValues) {
80
- const arr = new Uint8Array(16);
81
- crypto.getRandomValues(arr);
82
- return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
83
- }
84
- throw new Error("[what] No secure random source available for CSRF token generation");
85
- }
86
- function validateCsrfToken(requestToken, sessionToken) {
87
- if (!requestToken || !sessionToken) return false;
88
- if (requestToken.length !== sessionToken.length) return false;
89
- let result = 0;
90
- for (let i = 0; i < requestToken.length; i++) {
91
- result |= requestToken.charCodeAt(i) ^ sessionToken.charCodeAt(i);
92
- }
93
- return result === 0;
94
- }
95
- function csrfMetaTag(token) {
96
- const escaped = String(token).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
97
- return `<meta name="what-csrf-token" content="${escaped}">`;
98
- }
99
- var _actionCounter = 0;
100
- function generateActionId() {
101
- const rand = typeof crypto !== "undefined" && crypto.getRandomValues ? Array.from(crypto.getRandomValues(new Uint8Array(6)), (b) => b.toString(16).padStart(2, "0")).join("") : `c${(++_actionCounter).toString(36)}_${Date.now().toString(36)}`;
102
- return `a_${rand}`;
103
- }
104
- function action(fn, options = {}) {
105
- const id = options.id || generateActionId();
106
- const { onError, onSuccess, revalidate } = options;
107
- if (typeof window === "undefined") {
108
- actionRegistry.set(id, { fn, options });
109
- }
110
- async function callAction(...args) {
111
- if (typeof window === "undefined") {
112
- return fn(...args);
113
- }
114
- const timeout = options.timeout || 3e4;
115
- const controller = new AbortController();
116
- const timeoutId = setTimeout(() => controller.abort(), timeout);
117
- try {
118
- const csrfToken = getCsrfToken();
119
- const headers = {
120
- "Content-Type": "application/json",
121
- "X-What-Action": id
122
- };
123
- if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
124
- const response = await fetch("/__what_action", {
125
- method: "POST",
126
- headers,
127
- credentials: "same-origin",
128
- signal: controller.signal,
129
- body: JSON.stringify({ args })
130
- });
131
- if (!response.ok) {
132
- const error = await response.json().catch(() => ({ message: "Action failed" }));
133
- throw new Error(error.message || "Action failed");
134
- }
135
- const result = await response.json();
136
- if (onSuccess) onSuccess(result);
137
- if (revalidate) {
138
- for (const path of revalidate) {
139
- invalidatePath(path);
140
- }
141
- }
142
- return result;
143
- } catch (error) {
144
- if (error.name === "AbortError") {
145
- const timeoutError = new Error(`Action "${id}" timed out after ${timeout}ms`);
146
- timeoutError.code = "TIMEOUT";
147
- if (onError) onError(timeoutError);
148
- throw timeoutError;
149
- }
150
- if (onError) onError(error);
151
- throw error;
152
- } finally {
153
- clearTimeout(timeoutId);
154
- }
155
- }
156
- callAction._actionId = id;
157
- callAction._isAction = true;
158
- return callAction;
159
- }
160
- function formAction(actionFn, options = {}) {
161
- const { onSuccess, onError, resetOnSuccess = true } = options;
162
- return async (formDataOrEvent) => {
163
- let formData;
164
- let form;
165
- if (formDataOrEvent instanceof Event) {
166
- formDataOrEvent.preventDefault();
167
- form = formDataOrEvent.target;
168
- formData = new FormData(form);
169
- } else {
170
- formData = formDataOrEvent;
171
- }
172
- const data = {};
173
- let hasFiles = false;
174
- for (const [key, value] of formData.entries()) {
175
- if (typeof File !== "undefined" && value instanceof File) {
176
- hasFiles = true;
177
- }
178
- if (data[key]) {
179
- if (Array.isArray(data[key])) {
180
- data[key].push(value);
181
- } else {
182
- data[key] = [data[key], value];
183
- }
184
- } else {
185
- data[key] = value;
186
- }
187
- }
188
- try {
189
- const result = hasFiles ? await actionFn(data, formData) : await actionFn(data);
190
- if (onSuccess) onSuccess(result, form);
191
- if (resetOnSuccess && form) form.reset();
192
- return result;
193
- } catch (error) {
194
- if (onError) onError(error, form);
195
- throw error;
196
- }
197
- };
198
- }
199
- function useAction(actionFn) {
200
- const isPending = signal2(false);
201
- const error = signal2(null);
202
- const data = signal2(null);
203
- async function trigger(...args) {
204
- isPending.set(true);
205
- error.set(null);
206
- try {
207
- const result = await actionFn(...args);
208
- data.set(result);
209
- return result;
210
- } catch (e) {
211
- error.set(e);
212
- throw e;
213
- } finally {
214
- isPending.set(false);
215
- }
216
- }
217
- return {
218
- trigger,
219
- isPending: () => isPending(),
220
- error: () => error(),
221
- data: () => data(),
222
- reset: () => {
223
- error.set(null);
224
- data.set(null);
225
- }
226
- };
227
- }
228
- function useFormAction(actionFn, options = {}) {
229
- const { resetOnSuccess = true } = options;
230
- const formRef = { current: null };
231
- const actionState = useAction(formAction(actionFn, { resetOnSuccess }));
232
- function handleSubmit(e) {
233
- e.preventDefault();
234
- const formData = new FormData(e.target);
235
- formRef.current = e.target;
236
- return actionState.trigger(formData);
237
- }
238
- return {
239
- ...actionState,
240
- handleSubmit,
241
- formRef
242
- };
243
- }
244
- function useOptimistic(initialValue, reducer) {
245
- const value = signal2(initialValue);
246
- const pending = signal2([]);
247
- const baseValue = signal2(initialValue);
248
- function addOptimistic(action2) {
249
- const optimisticValue = reducer(value.peek(), action2);
250
- batch2(() => {
251
- pending.set([...pending.peek(), action2]);
252
- value.set(optimisticValue);
253
- });
254
- }
255
- function resolve(action2, serverValue) {
256
- batch2(() => {
257
- pending.set(pending.peek().filter((a) => a !== action2));
258
- if (serverValue !== void 0) {
259
- baseValue.set(serverValue);
260
- let current = serverValue;
261
- for (const a of pending.peek()) {
262
- current = reducer(current, a);
263
- }
264
- value.set(current);
265
- }
266
- });
267
- }
268
- function rollback(action2, realValue) {
269
- batch2(() => {
270
- const newPending = pending.peek().filter((a) => a !== action2);
271
- pending.set(newPending);
272
- const base = realValue !== void 0 ? realValue : baseValue.peek();
273
- baseValue.set(base);
274
- let current = base;
275
- for (const a of newPending) {
276
- current = reducer(current, a);
277
- }
278
- value.set(current);
279
- });
280
- }
281
- async function withOptimistic(action2, asyncFn) {
282
- addOptimistic(action2);
283
- try {
284
- const result = await asyncFn();
285
- resolve(action2, result);
286
- return result;
287
- } catch (e) {
288
- rollback(action2);
289
- throw e;
290
- }
291
- }
292
- return {
293
- value: () => value(),
294
- isPending: () => pending().length > 0,
295
- addOptimistic,
296
- resolve,
297
- rollback,
298
- withOptimistic,
299
- set: (v) => {
300
- value.set(v);
301
- baseValue.set(v);
302
- }
303
- };
304
- }
305
- var revalidationCallbacks = /* @__PURE__ */ new Map();
306
- function onRevalidate(path, callback) {
307
- if (!revalidationCallbacks.has(path)) {
308
- revalidationCallbacks.set(path, /* @__PURE__ */ new Set());
309
- }
310
- revalidationCallbacks.get(path).add(callback);
311
- return () => {
312
- revalidationCallbacks.get(path)?.delete(callback);
313
- };
314
- }
315
- function invalidatePath(path) {
316
- const callbacks = revalidationCallbacks.get(path);
317
- if (callbacks) {
318
- for (const cb of callbacks) {
319
- try {
320
- cb();
321
- } catch (e) {
322
- console.error("[what] Revalidation error:", e);
323
- }
324
- }
325
- }
326
- }
327
- function handleActionRequest(req, actionId, args, options = {}) {
328
- const { csrfToken: sessionCsrfToken, skipCsrf = false } = options;
329
- if (!skipCsrf) {
330
- if (!sessionCsrfToken) {
331
- return Promise.resolve({
332
- status: 500,
333
- body: {
334
- message: "[what] CSRF token not configured. Pass { csrfToken: sessionToken } to handleActionRequest, or { skipCsrf: true } to explicitly opt out."
335
- }
336
- });
337
- }
338
- const requestCsrfToken = req?.headers?.["x-csrf-token"] || req?.headers?.["X-CSRF-Token"];
339
- if (!validateCsrfToken(requestCsrfToken, sessionCsrfToken)) {
340
- return Promise.resolve({ status: 403, body: { message: "Invalid CSRF token" } });
341
- }
342
- }
343
- const action2 = actionRegistry.get(actionId);
344
- if (!action2) {
345
- return Promise.resolve({ status: 404, body: { message: "Action not found" } });
346
- }
347
- if (!Array.isArray(args)) {
348
- return Promise.resolve({ status: 400, body: { message: "Invalid action arguments" } });
349
- }
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) => {
360
- console.error(`[what] Action "${actionId}" error:`, error);
361
- return {
362
- status: 500,
363
- body: { message: "Action failed" }
364
- };
365
- });
366
- }
367
- function getRegisteredActions() {
368
- return [...actionRegistry.keys()];
369
- }
370
- function useMutation(mutationFn, options = {}) {
371
- const { onSuccess, onError, onSettled } = options;
372
- const state = {
373
- isPending: signal2(false),
374
- error: signal2(null),
375
- data: signal2(null)
376
- };
377
- async function mutate(...args) {
378
- state.isPending.set(true);
379
- state.error.set(null);
380
- try {
381
- const result = await mutationFn(...args);
382
- state.data.set(result);
383
- if (onSuccess) onSuccess(result, ...args);
384
- return result;
385
- } catch (error) {
386
- state.error.set(error);
387
- if (onError) onError(error, ...args);
388
- throw error;
389
- } finally {
390
- state.isPending.set(false);
391
- if (onSettled) onSettled(state.data.peek(), state.error.peek(), ...args);
392
- }
393
- }
394
- return {
395
- mutate,
396
- isPending: () => state.isPending(),
397
- error: () => state.error(),
398
- data: () => state.data(),
399
- reset: () => {
400
- state.error.set(null);
401
- state.data.set(null);
402
- }
403
- };
404
- }
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
-
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
- }
745
- var _hydrationIdCounter = 0;
746
- function resetHydrationId() {
747
- _hydrationIdCounter = 0;
748
- }
749
- function nextHydrationId() {
750
- return "h" + _hydrationIdCounter++;
751
- }
752
- function renderToHydratableString(vnode) {
753
- resetHydrationId();
754
- return _renderHydratable(vnode);
755
- }
756
- function _renderHydratable(vnode) {
757
- if (vnode == null || vnode === false || vnode === true) return "";
758
- if (typeof vnode === "string" || typeof vnode === "number") {
759
- return escapeHtml(String(vnode));
760
- }
761
- if (typeof vnode === "function" && vnode._signal) {
762
- return `<!--$-->${_renderHydratable(vnode())}<!--/$-->`;
763
- }
764
- if (typeof vnode === "function") {
765
- try {
766
- return `<!--$-->${_renderHydratable(vnode())}<!--/$-->`;
767
- } catch (e) {
768
- if (typeof process !== "undefined" && true) {
769
- console.warn("[what-server] Error rendering reactive function in SSR:", e.message);
770
- }
771
- return "<!--$--><!--/$-->";
772
- }
773
- }
774
- if (Array.isArray(vnode)) {
775
- return `<!--[]-->${vnode.map(_renderHydratable).join("")}<!--/[]-->`;
776
- }
777
- if (typeof vnode.tag === "function") {
778
- const hkId = nextHydrationId();
779
- const result = vnode.tag({ ...vnode.props, children: vnode.children });
780
- const html = _renderHydratable(result);
781
- return injectHydrationKey(html, hkId);
782
- }
783
- const { tag, props, children } = vnode;
784
- const attrs = renderAttrs(props || {});
785
- const open = `<${tag}${attrs}>`;
786
- if (VOID_ELEMENTS.has(tag)) return open;
787
- const rawInner = _resolveInnerHTML(props);
788
- const inner = rawInner != null ? String(rawInner) : children.map(_renderHydratable).join("");
789
- return `${open}${inner}</${tag}>`;
790
- }
791
- function injectHydrationKey(html, hkId) {
792
- const match = html.match(/^((?:<!--.*?-->)*)<([a-zA-Z][a-zA-Z0-9-]*)/);
793
- if (match) {
794
- const prefix = match[1];
795
- const tagName = match[2];
796
- const insertAt = prefix.length + 1 + tagName.length;
797
- return html.slice(0, insertAt) + ` data-hk="${hkId}"` + html.slice(insertAt);
798
- }
799
- return html;
800
- }
801
- function renderToString(vnode) {
802
- if (vnode == null || vnode === false || vnode === true) return "";
803
- if (typeof vnode === "string" || typeof vnode === "number") {
804
- return escapeHtml(String(vnode));
805
- }
806
- if (typeof vnode === "function" && vnode._signal) {
807
- return renderToString(vnode());
808
- }
809
- if (typeof vnode === "function") {
810
- try {
811
- return renderToString(vnode());
812
- } catch (e) {
813
- if (typeof process !== "undefined" && true) {
814
- console.warn("[what-server] Error rendering reactive function in SSR:", e.message);
815
- }
816
- return "";
817
- }
818
- }
819
- if (Array.isArray(vnode)) {
820
- return vnode.map(renderToString).join("");
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
- }
832
- if (typeof vnode.tag === "function") {
833
- const result = vnode.tag({ ...vnode.props, children: vnode.children });
834
- return renderToString(result);
835
- }
836
- const { tag, props, children } = vnode;
837
- const attrs = renderAttrs(props || {});
838
- const open = `<${tag}${attrs}>`;
839
- if (VOID_ELEMENTS.has(tag)) return open;
840
- const rawInner = _resolveInnerHTML(props);
841
- const inner = rawInner != null ? String(rawInner) : children.map(renderToString).join("");
842
- return `${open}${inner}</${tag}>`;
843
- }
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);
900
- if (vnode == null || vnode === false || vnode === true) return;
901
- if (typeof vnode === "string" || typeof vnode === "number") {
902
- yield escapeHtml(String(vnode));
903
- return;
904
- }
905
- if (typeof vnode === "function" && vnode._signal) {
906
- yield* renderToStream(vnode(), ctx);
907
- return;
908
- }
909
- if (typeof vnode === "function") {
910
- try {
911
- yield* renderToStream(vnode(), ctx);
912
- } catch (e) {
913
- if (typeof process !== "undefined" && true) {
914
- console.warn("[what-server] Error rendering reactive function in stream SSR:", e.message);
915
- }
916
- }
917
- return;
918
- }
919
- if (Array.isArray(vnode)) {
920
- for (const child of vnode) {
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));
942
- }
943
- yield html;
944
- return;
945
- }
946
- if (typeof vnode.tag === "function") {
947
- try {
948
- const result = vnode.tag({ ...vnode.props, children: vnode.children });
949
- const resolved = result instanceof Promise ? await result : result;
950
- yield* renderToStream(resolved, ctx);
951
- } catch (e) {
952
- if (typeof process !== "undefined" && true) {
953
- console.warn("[what-server] Error rendering component in stream SSR:", e.message);
954
- }
955
- yield _isDevMode ? `<!-- SSR Error: ${escapeHtml(e.message || "Component error")} -->` : `<!-- SSR Error -->`;
956
- }
957
- return;
958
- }
959
- const { tag, props, children } = vnode;
960
- const attrs = renderAttrs(props || {});
961
- yield `<${tag}${attrs}>`;
962
- if (!VOID_ELEMENTS.has(tag)) {
963
- const rawInner = _resolveInnerHTML(props);
964
- if (rawInner != null) {
965
- yield String(rawInner);
966
- } else {
967
- for (const child of children) {
968
- yield* renderToStream(child, ctx);
969
- }
970
- }
971
- yield `</${tag}>`;
972
- }
973
- }
974
- function definePage(config) {
975
- return {
976
- // 'static' = pre-render at build time (default)
977
- // 'server' = render on each request
978
- // 'client' = render in browser (SPA)
979
- // 'hybrid' = static shell + islands
980
- mode: "static",
981
- ...config
982
- };
983
- }
984
- function generateStaticPage(page, data = {}) {
985
- const vnode = page.component(data);
986
- const html = renderToString(vnode);
987
- const islands = page.islands || [];
988
- return wrapDocument({
989
- title: page.title || "",
990
- meta: page.meta || {},
991
- body: html,
992
- islands,
993
- scripts: page.mode === "static" ? [] : page.scripts || [],
994
- styles: page.styles || [],
995
- mode: page.mode
996
- });
997
- }
998
- function wrapDocument({ title, meta, body, islands, scripts, styles, mode }) {
999
- const metaTags = Object.entries(meta).map(([name, content]) => `<meta name="${escapeHtml(name)}" content="${escapeHtml(content)}">`).join("\n ");
1000
- const styleTags = styles.map((href) => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join("\n ");
1001
- const islandScript = islands.length > 0 ? `
1002
- <script type="module">
1003
- import { hydrateIslands } from '/@what/islands.js';
1004
- hydrateIslands();
1005
- <\/script>` : "";
1006
- const scriptTags = scripts.map((src) => `<script type="module" src="${escapeHtml(src)}"><\/script>`).join("\n ");
1007
- const clientScript = mode === "client" ? `
1008
- <script type="module" src="/@what/client.js"><\/script>` : "";
1009
- return `<!DOCTYPE html>
1010
- <html lang="en">
1011
- <head>
1012
- <meta charset="UTF-8">
1013
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1014
- ${metaTags}
1015
- <title>${escapeHtml(title)}</title>
1016
- ${styleTags}
1017
- </head>
1018
- <body>
1019
- <div id="app">${body}</div>
1020
- ${islandScript}
1021
- ${scriptTags}
1022
- ${clientScript}
1023
- </body>
1024
- </html>`;
1025
- }
1026
- function server(Component) {
1027
- Component._server = true;
1028
- return Component;
1029
- }
1030
- var _isDevMode = typeof process !== "undefined" ? true : true;
1031
- function _resolveInnerHTML(props) {
1032
- if (!props) return null;
1033
- if (props.dangerouslySetInnerHTML) {
1034
- return props.dangerouslySetInnerHTML.__html ?? null;
1035
- }
1036
- if (props.innerHTML && typeof props.innerHTML === "object" && "__html" in props.innerHTML) {
1037
- return props.innerHTML.__html ?? null;
1038
- }
1039
- if (props.innerHTML != null && typeof props.innerHTML === "string") {
1040
- if (_isDevMode) {
1041
- console.warn(
1042
- "[what-server] innerHTML received a raw string. This is a security risk (XSS). Use innerHTML={{ __html: trustedString }} or dangerouslySetInnerHTML={{ __html: trustedString }} instead."
1043
- );
1044
- }
1045
- return null;
1046
- }
1047
- return null;
1048
- }
1049
- function renderAttrs(props) {
1050
- let out = "";
1051
- for (const [key, val] of Object.entries(props)) {
1052
- if (key === "key" || key === "ref" || key === "children" || key === "dangerouslySetInnerHTML" || key === "innerHTML") continue;
1053
- if (key.startsWith("on") && key.length > 2) continue;
1054
- if (val === false || val == null) continue;
1055
- if (key === "className" || key === "class") {
1056
- out += ` class="${escapeHtml(String(val))}"`;
1057
- } else if (key === "style" && typeof val === "object") {
1058
- const css = Object.entries(val).map(([p, v]) => `${camelToKebab(p)}:${v}`).join(";");
1059
- out += ` style="${escapeHtml(css)}"`;
1060
- } else if (val === true) {
1061
- if (key.startsWith("aria-") || key === "role") {
1062
- out += ` ${key}="true"`;
1063
- } else {
1064
- out += ` ${key}`;
1065
- }
1066
- } else {
1067
- if (isUnsafeUrlAttribute(key, val)) continue;
1068
- out += ` ${key}="${escapeHtml(String(val))}"`;
1069
- }
1070
- }
1071
- return out;
1072
- }
1073
- function isUnsafeUrlAttribute(key, val) {
1074
- const normalizedKey = key.toLowerCase();
1075
- if (!URL_ATTRS.has(normalizedKey)) return false;
1076
- const normalizedValue = String(val).trim().replace(/[\u0000-\u001f\u007f\s]+/g, "").toLowerCase();
1077
- return normalizedValue.startsWith("javascript:") || normalizedValue.startsWith("vbscript:") || normalizedValue.startsWith("data:");
1078
- }
1079
- var URL_ATTRS = /* @__PURE__ */ new Set([
1080
- "href",
1081
- "src",
1082
- "action",
1083
- "formaction",
1084
- "xlink:href"
1085
- ]);
1086
- function escapeHtml(str) {
1087
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1088
- }
1089
- function camelToKebab(str) {
1090
- if (str.startsWith("--")) return str;
1091
- return str.replace(/([A-Z])/g, "-$1").toLowerCase();
1092
- }
1093
- var VOID_ELEMENTS = /* @__PURE__ */ new Set([
1094
- "area",
1095
- "base",
1096
- "br",
1097
- "col",
1098
- "embed",
1099
- "hr",
1100
- "img",
1101
- "input",
1102
- "link",
1103
- "meta",
1104
- "param",
1105
- "source",
1106
- "track",
1107
- "wbr"
1108
- ]);
1109
- export {
1110
- action,
1111
- buildVercelOutput,
1112
- createActionHandler,
1113
- createCloudflareHandler,
1114
- createRequestHandler,
1115
- createServer,
1116
- createVercelHandler,
1117
- csrfMetaTag,
1118
- definePage,
1119
- exportStatic,
1120
- fetchActionHandler,
1121
- formAction,
1122
- generateCsrfToken,
1123
- generateStaticPage,
1124
- getRegisteredActions,
1125
- getRevalidationHandler,
1126
- handleActionRequest,
1127
- invalidatePath,
1128
- nodeActionMiddleware,
1129
- onRevalidate,
1130
- renderDocument,
1131
- renderPage,
1132
- renderToHydratableString,
1133
- renderToStream,
1134
- renderToString,
1135
- renderToStringAsync,
1136
- renderToStringWithHead,
1137
- revalidatePath,
1138
- revalidateTag,
1139
- serializeState,
1140
- server,
1141
- setRevalidationHandler,
1142
- toNodeListener,
1143
- useAction,
1144
- useFormAction,
1145
- useMutation,
1146
- useOptimistic,
1147
- validateCsrfToken,
1148
- whatMiddleware
1149
- };
1150
- //# sourceMappingURL=index.js.map