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/actions.js +32 -1
- package/dist/actions.js.map +3 -3
- package/dist/actions.min.js +1 -1
- package/dist/actions.min.js.map +4 -4
- package/dist/index.js +531 -23
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +10 -10
- package/dist/index.min.js.map +4 -4
- package/dist/islands.js +23 -1
- package/dist/islands.js.map +3 -3
- package/dist/islands.min.js +1 -1
- package/dist/islands.min.js.map +4 -4
- package/package.json +8 -2
- package/src/action-handler.js +149 -0
- package/src/actions.js +13 -1
- package/src/adapter/cloudflare.js +18 -0
- package/src/adapter/core.js +112 -0
- package/src/adapter/node.js +77 -0
- package/src/adapter/static.js +62 -0
- package/src/adapter/vercel.js +29 -0
- package/src/index.js +184 -9
- package/src/islands.js +12 -2
- package/src/revalidation-registry.js +37 -0
- package/src/serialize.js +34 -0
package/dist/index.js
CHANGED
|
@@ -1,8 +1,63 @@
|
|
|
1
1
|
// packages/server/src/index.js
|
|
2
|
-
import { h } from "what-core";
|
|
2
|
+
import { h, runWithServerContext, beginHeadCollection, endHeadCollection } from "what-core";
|
|
3
|
+
|
|
4
|
+
// packages/server/src/serialize.js
|
|
5
|
+
var SCRIPT_UNSAFE = new RegExp("[<>&\\u2028\\u2029]", "g");
|
|
6
|
+
var ESCAPES = {
|
|
7
|
+
60: "\\u003c",
|
|
8
|
+
// <
|
|
9
|
+
62: "\\u003e",
|
|
10
|
+
// >
|
|
11
|
+
38: "\\u0026",
|
|
12
|
+
// &
|
|
13
|
+
8232: "\\u2028",
|
|
14
|
+
8233: "\\u2029"
|
|
15
|
+
};
|
|
16
|
+
function serializeState(value) {
|
|
17
|
+
return JSON.stringify(value).replace(SCRIPT_UNSAFE, (c) => ESCAPES[c.charCodeAt(0)]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// packages/server/src/islands.js
|
|
21
|
+
import { mount, hydrate, signal, batch } from "what-core";
|
|
22
|
+
var sharedStores = /* @__PURE__ */ new Map();
|
|
23
|
+
function getIslandStoresSnapshot() {
|
|
24
|
+
const data = {};
|
|
25
|
+
for (const [name, store] of sharedStores) {
|
|
26
|
+
data[name] = store._getSnapshot();
|
|
27
|
+
}
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// packages/server/src/actions.js
|
|
32
|
+
import { signal as signal2, batch as batch2 } from "what-core";
|
|
33
|
+
|
|
34
|
+
// packages/server/src/revalidation-registry.js
|
|
35
|
+
var _handler = null;
|
|
36
|
+
var isDev = typeof process !== "undefined" ? true : true;
|
|
37
|
+
function setRevalidationHandler(handler) {
|
|
38
|
+
_handler = handler;
|
|
39
|
+
}
|
|
40
|
+
function getRevalidationHandler() {
|
|
41
|
+
return _handler;
|
|
42
|
+
}
|
|
43
|
+
async function revalidatePath(path, options) {
|
|
44
|
+
if (_handler && _handler.revalidatePath) return _handler.revalidatePath(path, options);
|
|
45
|
+
if (isDev) {
|
|
46
|
+
console.warn(
|
|
47
|
+
`[what] revalidatePath('${path}') had no effect: no cache engine is bound. Create a what-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 =
|
|
146
|
-
const error =
|
|
147
|
-
const data =
|
|
200
|
+
const isPending = signal2(false);
|
|
201
|
+
const error = signal2(null);
|
|
202
|
+
const data = signal2(null);
|
|
148
203
|
async function trigger(...args) {
|
|
149
204
|
isPending.set(true);
|
|
150
205
|
error.set(null);
|
|
@@ -187,18 +242,18 @@ function useFormAction(actionFn, options = {}) {
|
|
|
187
242
|
};
|
|
188
243
|
}
|
|
189
244
|
function useOptimistic(initialValue, reducer) {
|
|
190
|
-
const value =
|
|
191
|
-
const pending =
|
|
192
|
-
const baseValue =
|
|
245
|
+
const value = signal2(initialValue);
|
|
246
|
+
const pending = signal2([]);
|
|
247
|
+
const baseValue = signal2(initialValue);
|
|
193
248
|
function addOptimistic(action2) {
|
|
194
249
|
const optimisticValue = reducer(value.peek(), action2);
|
|
195
|
-
|
|
250
|
+
batch2(() => {
|
|
196
251
|
pending.set([...pending.peek(), action2]);
|
|
197
252
|
value.set(optimisticValue);
|
|
198
253
|
});
|
|
199
254
|
}
|
|
200
255
|
function resolve(action2, serverValue) {
|
|
201
|
-
|
|
256
|
+
batch2(() => {
|
|
202
257
|
pending.set(pending.peek().filter((a) => a !== action2));
|
|
203
258
|
if (serverValue !== void 0) {
|
|
204
259
|
baseValue.set(serverValue);
|
|
@@ -211,7 +266,7 @@ function useOptimistic(initialValue, reducer) {
|
|
|
211
266
|
});
|
|
212
267
|
}
|
|
213
268
|
function rollback(action2, realValue) {
|
|
214
|
-
|
|
269
|
+
batch2(() => {
|
|
215
270
|
const newPending = pending.peek().filter((a) => a !== action2);
|
|
216
271
|
pending.set(newPending);
|
|
217
272
|
const base = realValue !== void 0 ? realValue : baseValue.peek();
|
|
@@ -292,7 +347,16 @@ function handleActionRequest(req, actionId, args, options = {}) {
|
|
|
292
347
|
if (!Array.isArray(args)) {
|
|
293
348
|
return Promise.resolve({ status: 400, body: { message: "Invalid action arguments" } });
|
|
294
349
|
}
|
|
295
|
-
return action2.fn(...args).then(
|
|
350
|
+
return action2.fn(...args).then(async (result) => {
|
|
351
|
+
const opts = action2.options || {};
|
|
352
|
+
if (Array.isArray(opts.revalidate)) {
|
|
353
|
+
for (const p of opts.revalidate) await revalidatePath(p);
|
|
354
|
+
}
|
|
355
|
+
if (Array.isArray(opts.revalidateTags)) {
|
|
356
|
+
for (const t of opts.revalidateTags) await revalidateTag(t);
|
|
357
|
+
}
|
|
358
|
+
return { status: 200, body: result };
|
|
359
|
+
}).catch((error) => {
|
|
296
360
|
console.error(`[what] Action "${actionId}" error:`, error);
|
|
297
361
|
return {
|
|
298
362
|
status: 500,
|
|
@@ -306,9 +370,9 @@ function getRegisteredActions() {
|
|
|
306
370
|
function useMutation(mutationFn, options = {}) {
|
|
307
371
|
const { onSuccess, onError, onSettled } = options;
|
|
308
372
|
const state = {
|
|
309
|
-
isPending:
|
|
310
|
-
error:
|
|
311
|
-
data:
|
|
373
|
+
isPending: signal2(false),
|
|
374
|
+
error: signal2(null),
|
|
375
|
+
data: signal2(null)
|
|
312
376
|
};
|
|
313
377
|
async function mutate(...args) {
|
|
314
378
|
state.isPending.set(true);
|
|
@@ -339,7 +403,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
|
-
|
|
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
|