akanjs 2.1.1 → 2.1.2-rc.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/base/baseEnv.ts +2 -1
- package/client/csrTypes.ts +22 -0
- package/client/translator.ts +25 -11
- package/package.json +1 -1
- package/server/akanApp.ts +1 -1
- package/server/artifact/routeSeedIndexStore.ts +34 -0
- package/server/routeElementComposer.tsx +101 -1
- package/server/routeTreeBuilder.ts +82 -1
- package/server/rscClient.tsx +9 -1
- package/server/rscWorker.tsx +308 -66
- package/server/rscWorkerHost.ts +10 -5
- package/server/systemPages.tsx +165 -0
- package/server/webRouter.ts +116 -18
- package/service/predefinedAdaptor/database.adaptor.ts +2 -0
- package/service/predefinedAdaptor/solidSqlite.ts +1 -0
- package/service/predefinedAdaptor/sqlitePath.ts +4 -1
- package/service/predefinedAdaptor/storage.adaptor.ts +2 -2
- package/types/client/csrTypes.d.ts +21 -0
- package/types/client/translator.d.ts +1 -0
- package/types/server/artifact/routeSeedIndexStore.d.ts +1 -0
- package/types/server/routeElementComposer.d.ts +15 -1
- package/types/server/routeTreeBuilder.d.ts +6 -1
- package/types/server/rscWorkerHost.d.ts +1 -0
- package/types/server/systemPages.d.ts +27 -0
- package/types/service/predefinedAdaptor/sqlitePath.d.ts +2 -1
- package/types/ui/System/Client.d.ts +5 -9
- package/types/ui/System/Common.d.ts +2 -0
- package/types/ui/System/SSR.d.ts +2 -2
- package/ui/InfiniteScroll.tsx +0 -1
- package/ui/System/Client.tsx +78 -20
- package/ui/System/Common.tsx +11 -2
- package/ui/System/SSR.tsx +58 -11
- package/webkit/bootCsr.tsx +13 -2
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { AkanI18nConfig } from "akanjs/common";
|
|
2
|
+
import { DEFAULT_AKAN_I18N, getBasePathFromPathname } from "akanjs/common";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export type SystemPageKind = "not-found" | "error";
|
|
6
|
+
|
|
7
|
+
export interface SystemPageOptions {
|
|
8
|
+
kind: SystemPageKind;
|
|
9
|
+
pathname: string;
|
|
10
|
+
homeHref: string;
|
|
11
|
+
lang?: string;
|
|
12
|
+
stylesheetHref?: string | null;
|
|
13
|
+
showDetails?: boolean;
|
|
14
|
+
error?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SystemPageResponseOptions extends SystemPageOptions {
|
|
18
|
+
method?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SystemPageHomeHrefOptions {
|
|
22
|
+
pathname: string;
|
|
23
|
+
i18n?: AkanI18nConfig;
|
|
24
|
+
basePaths?: Iterable<string>;
|
|
25
|
+
headerBasePath?: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const STATUS_COPY = {
|
|
29
|
+
"not-found": {
|
|
30
|
+
status: 404,
|
|
31
|
+
eyebrow: "Page not found",
|
|
32
|
+
title: "This page is off the flight path.",
|
|
33
|
+
description: "The route you requested does not exist, or it may have moved to a different address.",
|
|
34
|
+
actionLabel: "Go home",
|
|
35
|
+
},
|
|
36
|
+
error: {
|
|
37
|
+
status: 500,
|
|
38
|
+
eyebrow: "Server error",
|
|
39
|
+
title: "Something broke on the server.",
|
|
40
|
+
description: "The app could not finish rendering this page. Please try again in a moment.",
|
|
41
|
+
actionLabel: "Back to safety",
|
|
42
|
+
},
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
const FALLBACK_STYLE = `
|
|
46
|
+
:root { color-scheme: dark; --akan-primary: #ff493b; --akan-secondary: #2b2e33; --akan-accent: #d1a23b; --akan-base-content: #ffffff; --akan-base-100: #1a1a1a; --akan-base-200: #2a2a2a; --akan-error: #f02020; }
|
|
47
|
+
body { margin: 0; min-height: 100vh; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--akan-base-100); color: var(--akan-base-content); }
|
|
48
|
+
a { color: inherit; }
|
|
49
|
+
.akan-system-page { min-height: 100vh; display: grid; place-items: center; padding: 32px 18px; box-sizing: border-box; background: radial-gradient(circle at top left, rgba(255, 73, 59, 0.16), transparent 34%), linear-gradient(135deg, var(--akan-base-100), #111); }
|
|
50
|
+
.akan-system-card { width: min(720px, 100%); border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 32px; background: linear-gradient(145deg, rgba(42, 42, 42, 0.96), rgba(43, 46, 51, 0.78)); box-shadow: 0 24px 80px rgba(0, 0, 0, 0.36); padding: clamp(28px, 6vw, 56px); }
|
|
51
|
+
.akan-system-status { margin: 0 0 18px; font-weight: 800; font-size: clamp(4rem, 18vw, 8rem); line-height: 0.85; letter-spacing: -0.08em; color: var(--akan-primary); }
|
|
52
|
+
.akan-system-eyebrow { margin: 0 0 10px; color: var(--akan-accent); font-size: 0.78rem; font-weight: 800; letter-spacing: 0.18em; text-transform: uppercase; }
|
|
53
|
+
.akan-system-title { margin: 0; max-width: 12ch; font-size: clamp(2rem, 7vw, 4rem); line-height: 0.95; letter-spacing: -0.055em; color: var(--akan-base-content); }
|
|
54
|
+
.akan-system-description { margin: 22px 0 0; max-width: 56ch; color: rgba(255, 255, 255, 0.76); font-size: 1.05rem; line-height: 1.75; }
|
|
55
|
+
.akan-system-path { margin: 22px 0 0; overflow-wrap: anywhere; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 16px; background: rgba(26, 26, 26, 0.72); padding: 12px 14px; color: rgba(255, 255, 255, 0.72); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.88rem; }
|
|
56
|
+
.akan-system-actions { margin-top: 30px; display: flex; flex-wrap: wrap; gap: 12px; }
|
|
57
|
+
.akan-system-action { display: inline-flex; min-height: 44px; align-items: center; justify-content: center; border: 1px solid var(--akan-primary); border-radius: 999px; background: var(--akan-primary); box-shadow: 0 12px 34px rgba(255, 73, 59, 0.26); color: var(--akan-base-content); padding: 0 18px; font-weight: 800; text-decoration: none; }
|
|
58
|
+
.akan-system-secondary { border: 1px solid rgba(255, 255, 255, 0.12); background: var(--akan-secondary); color: var(--akan-base-content); }
|
|
59
|
+
.akan-system-details { margin-top: 28px; max-height: 260px; overflow: auto; border-radius: 18px; background: rgba(26, 26, 26, 0.82); border: 1px solid rgba(240, 32, 32, 0.22); padding: 16px; color: rgba(255, 255, 255, 0.78); font-size: 0.82rem; line-height: 1.55; white-space: pre-wrap; }
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
export function createSystemPageDocument(options: SystemPageOptions): ReactNode {
|
|
63
|
+
const copy = STATUS_COPY[options.kind];
|
|
64
|
+
const details = options.showDetails ? getSystemPageErrorDetails(options.error) : null;
|
|
65
|
+
const title = `${copy.status} - ${copy.eyebrow}`;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<html lang={options.lang ?? DEFAULT_AKAN_I18N.defaultLocale}>
|
|
69
|
+
<head>
|
|
70
|
+
<meta charSet="utf-8" />
|
|
71
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
72
|
+
<meta name="robots" content="noindex" />
|
|
73
|
+
<title>{title}</title>
|
|
74
|
+
{options.stylesheetHref ? (
|
|
75
|
+
<link rel="stylesheet" href={options.stylesheetHref} precedence="default" data-akan-css="active" />
|
|
76
|
+
) : null}
|
|
77
|
+
<style data-akan-system-page>{FALLBACK_STYLE}</style>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
<main className="akan-system-page min-h-screen bg-base-100 text-base-content">
|
|
81
|
+
<section
|
|
82
|
+
className="akan-system-card rounded-3xl border border-base-content/10 bg-base-content/4 p-8 shadow-2xl backdrop-blur-xl"
|
|
83
|
+
aria-labelledby="akan-system-title"
|
|
84
|
+
>
|
|
85
|
+
<p className="akan-system-status text-primary">{copy.status}</p>
|
|
86
|
+
<p className="akan-system-eyebrow text-primary">{copy.eyebrow}</p>
|
|
87
|
+
<h1 id="akan-system-title" className="akan-system-title font-black">
|
|
88
|
+
{copy.title}
|
|
89
|
+
</h1>
|
|
90
|
+
<p className="akan-system-description text-base-content/70">{copy.description}</p>
|
|
91
|
+
<p className="akan-system-path border border-base-content/10 bg-base-200/50" aria-label="Requested path">
|
|
92
|
+
{options.pathname}
|
|
93
|
+
</p>
|
|
94
|
+
<div className="akan-system-actions">
|
|
95
|
+
<a className="akan-system-action btn btn-primary" href={options.homeHref}>
|
|
96
|
+
{copy.actionLabel}
|
|
97
|
+
</a>
|
|
98
|
+
<a className="akan-system-action akan-system-secondary btn" href={options.pathname}>
|
|
99
|
+
Try again
|
|
100
|
+
</a>
|
|
101
|
+
</div>
|
|
102
|
+
{details ? (
|
|
103
|
+
<pre className="akan-system-details" aria-label="Development error details">
|
|
104
|
+
{details}
|
|
105
|
+
</pre>
|
|
106
|
+
) : null}
|
|
107
|
+
</section>
|
|
108
|
+
</main>
|
|
109
|
+
</body>
|
|
110
|
+
</html>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function createSystemPageResponse(options: SystemPageResponseOptions): Promise<Response> {
|
|
115
|
+
const status = STATUS_COPY[options.kind].status;
|
|
116
|
+
const headers = createSystemPageHeaders();
|
|
117
|
+
if (options.method === "HEAD") return new Response(null, { status, headers });
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const { renderToReadableStream } = await import("react-dom/server.browser");
|
|
121
|
+
const stream = await renderToReadableStream(createSystemPageDocument(options));
|
|
122
|
+
return new Response(stream, { status, headers });
|
|
123
|
+
} catch {
|
|
124
|
+
return new Response(createSystemPageFallbackText(options.kind), {
|
|
125
|
+
status,
|
|
126
|
+
headers: {
|
|
127
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
128
|
+
"Cache-Control": "no-store",
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createSystemPageHeaders(): Headers {
|
|
135
|
+
return new Headers({
|
|
136
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
137
|
+
"Cache-Control": "no-store",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function createSystemPageFallbackText(kind: SystemPageKind): string {
|
|
142
|
+
const copy = STATUS_COPY[kind];
|
|
143
|
+
return `${copy.status} ${copy.eyebrow}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getSystemPageErrorDetails(error: unknown): string {
|
|
147
|
+
if (error instanceof Error) return error.stack ?? error.message;
|
|
148
|
+
return String(error);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getSystemPageHomeHref({
|
|
152
|
+
pathname,
|
|
153
|
+
i18n = DEFAULT_AKAN_I18N,
|
|
154
|
+
basePaths = [],
|
|
155
|
+
headerBasePath,
|
|
156
|
+
}: SystemPageHomeHrefOptions): string {
|
|
157
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
158
|
+
const locale = i18n.locales.includes(segments[0] ?? "") ? segments[0] : i18n.defaultLocale;
|
|
159
|
+
const basePath = getBasePathFromPathname(pathname, {
|
|
160
|
+
basePaths,
|
|
161
|
+
i18n,
|
|
162
|
+
headerBasePath,
|
|
163
|
+
});
|
|
164
|
+
return `/${[locale, basePath].filter(Boolean).join("/")}`;
|
|
165
|
+
}
|
package/server/webRouter.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { createDefaultRobotsTxt } from "./robots";
|
|
|
25
25
|
import { RscWorker } from "./rscWorkerHost";
|
|
26
26
|
import { createDefaultSitemapXml, getSitemapBasePath } from "./sitemap";
|
|
27
27
|
import { SsrFromRscRenderer } from "./ssrFromRscRenderer";
|
|
28
|
+
import { createSystemPageResponse, getSystemPageHomeHref } from "./systemPages";
|
|
28
29
|
import type { BaseBuildArtifact, HttpRoutes, RenderState } from "./types";
|
|
29
30
|
|
|
30
31
|
const RESERVED_BASE_PATHS = new Set(["admin"]);
|
|
@@ -176,7 +177,9 @@ export class WebRouter {
|
|
|
176
177
|
<script type="module" src="/csr.js"></script>
|
|
177
178
|
</body>
|
|
178
179
|
</html>`;
|
|
179
|
-
return new Response(this.#withCsrHmr(htmlText), {
|
|
180
|
+
return new Response(this.#withCsrHmr(htmlText), {
|
|
181
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
182
|
+
});
|
|
180
183
|
},
|
|
181
184
|
[`${clientServePrefix}/*`]: async (req) => {
|
|
182
185
|
this.#requestStats.staticAsset += 1;
|
|
@@ -252,15 +255,26 @@ export class WebRouter {
|
|
|
252
255
|
const manifest = await this.#ensureRoute(targetUrl);
|
|
253
256
|
const rscHeaders = new Headers(req.headers);
|
|
254
257
|
if (normalizedTarget.basePath) rscHeaders.set("x-base-path", normalizedTarget.basePath);
|
|
255
|
-
const rscReq = new Request(targetUrl, {
|
|
256
|
-
|
|
258
|
+
const rscReq = new Request(targetUrl, {
|
|
259
|
+
method: "GET",
|
|
260
|
+
headers: rscHeaders,
|
|
261
|
+
});
|
|
262
|
+
const result = await this.#rsc.renderWithMeta(rscReq, {
|
|
263
|
+
clientManifest: manifest.clientManifest,
|
|
264
|
+
});
|
|
257
265
|
if (result.type === "redirect") return WebRouter.#rscRedirectResponse(result.location, result.method);
|
|
258
266
|
if (result.type === "not-found") return WebRouter.#rscRedirectResponse("/404", "replace");
|
|
267
|
+
if (result.status === 404) return WebRouter.#rscRedirectResponse("/404", "replace");
|
|
268
|
+
if (result.status && result.status >= 500)
|
|
269
|
+
return this.#renderRscErrorResponse("__rsc", "Internal Server Error");
|
|
259
270
|
return new Response(result.stream, {
|
|
260
|
-
headers: {
|
|
271
|
+
headers: {
|
|
272
|
+
"Content-Type": "text/x-component; charset=utf-8",
|
|
273
|
+
"Cache-Control": "no-store",
|
|
274
|
+
},
|
|
261
275
|
});
|
|
262
276
|
} catch (err) {
|
|
263
|
-
return this.#
|
|
277
|
+
return this.#renderRscErrorResponse("__rsc", err);
|
|
264
278
|
}
|
|
265
279
|
},
|
|
266
280
|
"/__rsc/manifest": () =>
|
|
@@ -280,7 +294,9 @@ export class WebRouter {
|
|
|
280
294
|
const csrHtml = WebRouter.#resolveCsrHtmlPath(csrOutputDir, url.pathname, this.#artifact);
|
|
281
295
|
if (!csrHtml) return new Response("Not Found", { status: 404 });
|
|
282
296
|
const html = await Bun.file(csrHtml).text();
|
|
283
|
-
return new Response(this.#withCsrHmr(html), {
|
|
297
|
+
return new Response(this.#withCsrHmr(html), {
|
|
298
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
299
|
+
});
|
|
284
300
|
}
|
|
285
301
|
|
|
286
302
|
const csrAssetPath = path.extname(url.pathname) ? WebRouter.#safeResolve(csrOutputDir, url.pathname) : null;
|
|
@@ -300,7 +316,7 @@ export class WebRouter {
|
|
|
300
316
|
this.#requestStats.staticAsset += 1;
|
|
301
317
|
return WebRouter.#fileResponse(req, filePath, {
|
|
302
318
|
contentType: Bun.file(filePath).type || "application/octet-stream",
|
|
303
|
-
cacheControl: this.#prodMode ? "public, max-age=
|
|
319
|
+
cacheControl: this.#prodMode ? "public, max-age=300" : "no-store",
|
|
304
320
|
});
|
|
305
321
|
}
|
|
306
322
|
}
|
|
@@ -343,12 +359,17 @@ export class WebRouter {
|
|
|
343
359
|
const cachedHtml = htmlCacheKey ? this.#getCachedHtml(htmlCacheKey) : null;
|
|
344
360
|
if (cachedHtml) {
|
|
345
361
|
return new Response(cachedHtml, {
|
|
346
|
-
headers: {
|
|
362
|
+
headers: {
|
|
363
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
364
|
+
"X-Akan-Cache": "HIT",
|
|
365
|
+
},
|
|
347
366
|
});
|
|
348
367
|
}
|
|
349
|
-
const rscResult = await this.#rsc.renderWithMeta(req, {
|
|
368
|
+
const rscResult = await this.#rsc.renderWithMeta(req, {
|
|
369
|
+
clientManifest: manifest.clientManifest,
|
|
370
|
+
});
|
|
350
371
|
if (rscResult.type === "redirect") return Response.redirect(new URL(rscResult.location, url.origin), 307);
|
|
351
|
-
if (rscResult.type === "not-found") return
|
|
372
|
+
if (rscResult.type === "not-found") return this.#renderNotFoundResponse(req, url);
|
|
352
373
|
const themeCookieExists = WebRouter.#hasCookie(req, "theme");
|
|
353
374
|
const htmlStream = await new SsrFromRscRenderer().render({
|
|
354
375
|
request: req,
|
|
@@ -359,16 +380,24 @@ export class WebRouter {
|
|
|
359
380
|
importmap: this.#artifact.vendorMap,
|
|
360
381
|
theme: themeCookieExists ? undefined : (rscResult.theme ?? "system"),
|
|
361
382
|
});
|
|
362
|
-
|
|
383
|
+
const responseStatus = rscResult.status ?? 200;
|
|
384
|
+
const responseHeaders = WebRouter.#htmlResponseHeaders(responseStatus);
|
|
385
|
+
if (htmlCacheKey && responseStatus === 200) {
|
|
363
386
|
const html = await new Response(htmlStream).text();
|
|
364
387
|
this.#setCachedHtml(htmlCacheKey, html);
|
|
365
388
|
return new Response(html, {
|
|
366
|
-
headers: {
|
|
389
|
+
headers: {
|
|
390
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
391
|
+
"X-Akan-Cache": "MISS",
|
|
392
|
+
},
|
|
367
393
|
});
|
|
368
394
|
}
|
|
369
|
-
return new Response(
|
|
395
|
+
return new Response(req.method === "HEAD" ? null : htmlStream, {
|
|
396
|
+
status: responseStatus,
|
|
397
|
+
headers: responseHeaders,
|
|
398
|
+
});
|
|
370
399
|
} catch (err) {
|
|
371
|
-
return this.#renderErrorResponse(url.pathname, err);
|
|
400
|
+
return this.#renderErrorResponse(req, url.pathname, err);
|
|
372
401
|
}
|
|
373
402
|
},
|
|
374
403
|
};
|
|
@@ -535,18 +564,81 @@ export class WebRouter {
|
|
|
535
564
|
|
|
536
565
|
async #ensureRoute(url: URL) {
|
|
537
566
|
const started = Date.now();
|
|
538
|
-
const matched =
|
|
567
|
+
const matched =
|
|
568
|
+
RouteSeedIndexStore.match(url.pathname, this.#seedIndex.entries) ??
|
|
569
|
+
RouteSeedIndexStore.matchPrefix(url.pathname, this.#seedIndex.entries);
|
|
539
570
|
if (matched) await this.#routeCache.ensure(matched.entry.routeId, matched.entry.seeds);
|
|
540
571
|
this.#logger.verbose(
|
|
541
572
|
`[route-cache] ensure pathname=${url.pathname} routeId=${matched?.entry.routeId ?? "(none)"} in ${Date.now() - started}ms`,
|
|
542
573
|
);
|
|
543
574
|
return this.#routeCache.snapshot();
|
|
544
575
|
}
|
|
545
|
-
#
|
|
576
|
+
#renderNotFoundResponse(req: Request, url: URL): Promise<Response> {
|
|
577
|
+
return createSystemPageResponse({
|
|
578
|
+
kind: "not-found",
|
|
579
|
+
method: req.method,
|
|
580
|
+
pathname: url.pathname,
|
|
581
|
+
lang: WebRouter.#getLocale(url.pathname, this.#artifact.i18n),
|
|
582
|
+
homeHref: this.#getSystemPageHomeHref(req, url.pathname),
|
|
583
|
+
stylesheetHref: this.#getStylesheetHref(req, url.pathname),
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#renderErrorResponse(req: Request, scope: string, err: unknown): Promise<Response> {
|
|
588
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
589
|
+
this.#logger.error(`[SSR] render failed scope=${scope}: ${message}`);
|
|
590
|
+
this.#hub?.broadcast({ type: "error", message });
|
|
591
|
+
return createSystemPageResponse({
|
|
592
|
+
kind: "error",
|
|
593
|
+
method: req.method,
|
|
594
|
+
pathname: scope,
|
|
595
|
+
lang: WebRouter.#getLocale(new URL(req.url).pathname, this.#artifact.i18n),
|
|
596
|
+
homeHref: this.#getSystemPageHomeHref(req, new URL(req.url).pathname),
|
|
597
|
+
stylesheetHref: this.#getStylesheetHref(req, new URL(req.url).pathname),
|
|
598
|
+
showDetails: !this.#prodMode,
|
|
599
|
+
error: err,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#renderRscErrorResponse(scope: string, err: unknown): Response {
|
|
546
604
|
const message = err instanceof Error ? err.message : String(err);
|
|
547
605
|
this.#logger.error(`[SSR] render failed scope=${scope}: ${message}`);
|
|
548
606
|
this.#hub?.broadcast({ type: "error", message });
|
|
549
|
-
return new Response("Internal Server Error", {
|
|
607
|
+
return new Response("Internal Server Error", {
|
|
608
|
+
status: 500,
|
|
609
|
+
headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" },
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#getSystemPageHomeHref(req: Request, pathname: string): string {
|
|
614
|
+
return getSystemPageHomeHref({
|
|
615
|
+
pathname,
|
|
616
|
+
i18n: this.#artifact.i18n,
|
|
617
|
+
basePaths: this.#artifact.basePaths,
|
|
618
|
+
headerBasePath:
|
|
619
|
+
req.headers.get("x-base-path") ?? WebRouter.#basePathForRequestHost(req, this.#artifact.subRoutes),
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
#getStylesheetHref(req: Request, pathname: string): string | null {
|
|
624
|
+
const basePath = getBasePathFromPathname(pathname, {
|
|
625
|
+
basePaths: Object.keys(this.renderState.cssAssets),
|
|
626
|
+
i18n: this.#artifact.i18n,
|
|
627
|
+
headerBasePath:
|
|
628
|
+
req.headers.get("x-base-path") ?? WebRouter.#basePathForRequestHost(req, this.#artifact.subRoutes),
|
|
629
|
+
});
|
|
630
|
+
return this.renderState.cssAssets[basePath ?? ""]?.cssUrl ?? null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
static #getLocale(pathname: string, i18n: AkanI18nConfig): string {
|
|
634
|
+
const [segment] = pathname.split("/").filter(Boolean);
|
|
635
|
+
return segment && i18n.locales.includes(segment) ? segment : i18n.defaultLocale;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
static #htmlResponseHeaders(status: number): Headers {
|
|
639
|
+
const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
|
|
640
|
+
if (status >= 400) headers.set("Cache-Control", "no-store");
|
|
641
|
+
return headers;
|
|
550
642
|
}
|
|
551
643
|
#withCsrHmr(html: string): string {
|
|
552
644
|
if (this.#prodMode) return html;
|
|
@@ -589,7 +681,13 @@ export class WebRouter {
|
|
|
589
681
|
const rsc = new RscWorker(artifact);
|
|
590
682
|
await rsc.ready;
|
|
591
683
|
const seedIndex = await RouteSeedIndexStore.load(artifactDir);
|
|
592
|
-
return new WebRouter({
|
|
684
|
+
return new WebRouter({
|
|
685
|
+
artifact,
|
|
686
|
+
cssBytesByUrl,
|
|
687
|
+
rsc,
|
|
688
|
+
seedIndex,
|
|
689
|
+
upgradeHmrWs,
|
|
690
|
+
});
|
|
593
691
|
}
|
|
594
692
|
|
|
595
693
|
static #resolveArtifactDir() {
|
|
@@ -1094,6 +1094,7 @@ export class SqliteDatabase
|
|
|
1094
1094
|
appName,
|
|
1095
1095
|
fileName: `${appName}-${environment}.db`,
|
|
1096
1096
|
isProduction: process.env.NODE_ENV === "production",
|
|
1097
|
+
operationMode: env.operationMode,
|
|
1097
1098
|
workspaceRoot: env.workspaceRoot,
|
|
1098
1099
|
});
|
|
1099
1100
|
return {
|
|
@@ -1204,6 +1205,7 @@ export class LibsqlDatabase
|
|
|
1204
1205
|
appName,
|
|
1205
1206
|
fileName: `${appName}-${environment}.db`,
|
|
1206
1207
|
isProduction: process.env.NODE_ENV === "production",
|
|
1208
|
+
operationMode: env.operationMode,
|
|
1207
1209
|
workspaceRoot: env.workspaceRoot,
|
|
1208
1210
|
});
|
|
1209
1211
|
return {
|
|
@@ -30,6 +30,7 @@ export const getSolidConfig = (env: SolidEnv): Required<SolidConfig> => {
|
|
|
30
30
|
appName,
|
|
31
31
|
fileName: `${appName}-${environment}_solid.db`,
|
|
32
32
|
isProduction: process.env.NODE_ENV === "production",
|
|
33
|
+
operationMode: env.operationMode,
|
|
33
34
|
workspaceRoot: env.workspaceRoot,
|
|
34
35
|
});
|
|
35
36
|
return {
|
|
@@ -4,6 +4,7 @@ interface ResolveDefaultSqliteFileOptions {
|
|
|
4
4
|
appName: string;
|
|
5
5
|
fileName: string;
|
|
6
6
|
isProduction: boolean;
|
|
7
|
+
operationMode?: string;
|
|
7
8
|
workspaceRoot?: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -11,10 +12,12 @@ export const resolveDefaultSqliteFile = ({
|
|
|
11
12
|
appName,
|
|
12
13
|
fileName,
|
|
13
14
|
isProduction,
|
|
15
|
+
operationMode,
|
|
14
16
|
workspaceRoot,
|
|
15
17
|
}: ResolveDefaultSqliteFileOptions) => {
|
|
16
18
|
const sqliteDir = process.env.AKAN_SQLITE_DIR;
|
|
17
19
|
if (sqliteDir) return path.join(sqliteDir, fileName);
|
|
18
|
-
|
|
20
|
+
const isLocalOperation = (operationMode ?? process.env.AKAN_PUBLIC_OPERATION_MODE) === "local";
|
|
21
|
+
if (isProduction && !isLocalOperation) return path.join(process.cwd(), "sqlite", fileName);
|
|
19
22
|
return path.join(workspaceRoot ?? process.cwd(), "local", "apps", appName, fileName);
|
|
20
23
|
};
|
|
@@ -59,7 +59,7 @@ export interface BlobStorageOptions extends BaseEnv {
|
|
|
59
59
|
export class BlobStorage
|
|
60
60
|
extends adapt("blobStorage", ({ env }) => ({
|
|
61
61
|
root: env(
|
|
62
|
-
({ appName, blobStorage = { baseDir: "local", urlPrefix: "/
|
|
62
|
+
({ appName, blobStorage = { baseDir: "local", urlPrefix: "/api/localFile/getBlob" } }: BlobStorageOptions) =>
|
|
63
63
|
`${process.env.AKAN_WORKSPACE_ROOT ?? "."}/${blobStorage.baseDir ?? "local"}/${appName}/backend`,
|
|
64
64
|
),
|
|
65
65
|
privateRoot: env(
|
|
@@ -67,7 +67,7 @@ export class BlobStorage
|
|
|
67
67
|
`${process.env.AKAN_WORKSPACE_ROOT ?? "."}/${blobStorage.privateBaseDir ?? "local"}/${appName}/server-private`,
|
|
68
68
|
),
|
|
69
69
|
urlPrefix: env(
|
|
70
|
-
({ blobStorage = { urlPrefix: "/
|
|
70
|
+
({ blobStorage = { urlPrefix: "/api/localFile/getBlob" } }: BlobStorageOptions) => blobStorage.urlPrefix,
|
|
71
71
|
),
|
|
72
72
|
}))
|
|
73
73
|
implements StorageAdaptor
|
|
@@ -54,6 +54,13 @@ export interface PageLoadingProps {
|
|
|
54
54
|
export interface LayoutLoadingProps extends PageLoadingProps {
|
|
55
55
|
children: ReactNode;
|
|
56
56
|
}
|
|
57
|
+
export interface LayoutNotFoundProps extends PageProps {
|
|
58
|
+
pathname: string;
|
|
59
|
+
}
|
|
60
|
+
export interface LayoutErrorProps extends LayoutNotFoundProps {
|
|
61
|
+
error?: unknown;
|
|
62
|
+
digest?: string;
|
|
63
|
+
}
|
|
57
64
|
export type Head = ReactNode;
|
|
58
65
|
export type GenerateHead = (props: PageProps) => PromiseOrObject<Head | null | undefined>;
|
|
59
66
|
export type ResolveHead = (props: PageProps) => PromiseOrObject<Head | null | undefined>;
|
|
@@ -62,9 +69,15 @@ export type PageRender = (props: PageProps) => PromiseOrObject<ReactNode>;
|
|
|
62
69
|
export type LayoutRender = (props: LayoutProps) => PromiseOrObject<ReactNode>;
|
|
63
70
|
export type PageLoadingRender = (props: PageLoadingProps) => PromiseOrObject<ReactNode>;
|
|
64
71
|
export type LayoutLoadingRender = (props: LayoutLoadingProps) => PromiseOrObject<ReactNode>;
|
|
72
|
+
export type LayoutNotFoundRender = (props: LayoutNotFoundProps) => PromiseOrObject<ReactNode>;
|
|
73
|
+
export type LayoutErrorRender = (props: LayoutErrorProps) => PromiseOrObject<ReactNode>;
|
|
65
74
|
export interface RouteRender {
|
|
66
75
|
render: LayoutRender | PageRender;
|
|
67
76
|
Loading?: LayoutLoadingRender | PageLoadingRender;
|
|
77
|
+
NotFound?: LayoutNotFoundRender;
|
|
78
|
+
Error?: LayoutErrorRender;
|
|
79
|
+
resolveNotFound?: () => PromiseOrObject<LayoutNotFoundRender | undefined>;
|
|
80
|
+
resolveError?: () => PromiseOrObject<LayoutErrorRender | undefined>;
|
|
68
81
|
resolveHead?: ResolveHead;
|
|
69
82
|
getPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
|
|
70
83
|
}
|
|
@@ -111,6 +124,8 @@ export interface LayoutModule {
|
|
|
111
124
|
layoutStyle?: "mobile" | "web";
|
|
112
125
|
gaTrackingId?: string;
|
|
113
126
|
Loading?: LayoutLoadingRender;
|
|
127
|
+
NotFound?: LayoutNotFoundRender;
|
|
128
|
+
Error?: LayoutErrorRender;
|
|
114
129
|
}
|
|
115
130
|
export type RouteModule = PageModule | LayoutModule;
|
|
116
131
|
export interface Route {
|
|
@@ -231,6 +246,12 @@ export interface PathRoute {
|
|
|
231
246
|
resolveHead?: ResolveHead;
|
|
232
247
|
isSpecialRoute?: boolean;
|
|
233
248
|
}
|
|
249
|
+
export interface LayoutFallbackRoute {
|
|
250
|
+
path: string;
|
|
251
|
+
pathSegments: string[];
|
|
252
|
+
renderRootLayouts: RouteRender[];
|
|
253
|
+
renderLayouts: RouteRender[];
|
|
254
|
+
}
|
|
234
255
|
export interface RouteGuide {
|
|
235
256
|
pathSegment: string;
|
|
236
257
|
pathRoute?: PathRoute;
|
|
@@ -10,6 +10,7 @@ export declare class Translator {
|
|
|
10
10
|
#private;
|
|
11
11
|
constructor(dictionary: Record<string, Record<string, Record<string, unknown>>>);
|
|
12
12
|
hasDictionary(lang: string): boolean;
|
|
13
|
+
static seed(lang: string, dict: Dictionary | undefined): void;
|
|
13
14
|
translate(lang: string, key: string, param?: Record<string, string | number>): string;
|
|
14
15
|
getDictionary(lang: string): Promise<Dictionary>;
|
|
15
16
|
}
|
|
@@ -38,5 +38,6 @@ export declare class RouteSeedIndexStore {
|
|
|
38
38
|
* matches, along with the extracted parameters.
|
|
39
39
|
*/
|
|
40
40
|
static match(pathname: string, entries: RouteSeedEntry[]): MatchedRoute | null;
|
|
41
|
+
static matchPrefix(pathname: string, entries: RouteSeedEntry[]): MatchedRoute | null;
|
|
41
42
|
}
|
|
42
43
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Head, PathRoute, RouteRender } from "akanjs/client";
|
|
1
|
+
import type { Head, LayoutFallbackRoute, PathRoute, RouteRender } from "akanjs/client";
|
|
2
2
|
import { type ReactElement, type ReactNode } from "react";
|
|
3
3
|
export declare class RouteElementComposer {
|
|
4
4
|
#private;
|
|
@@ -12,6 +12,20 @@ export declare class RouteElementComposer {
|
|
|
12
12
|
params: Record<string, string>;
|
|
13
13
|
searchParams: Record<string, string | string[]>;
|
|
14
14
|
}): Promise<Head | null | undefined>;
|
|
15
|
+
static composeFallback({ kind, route, params, searchParams, pathname, error, digest, }: {
|
|
16
|
+
kind: "not-found" | "error";
|
|
17
|
+
route: PathRoute | LayoutFallbackRoute;
|
|
18
|
+
params: Record<string, string>;
|
|
19
|
+
searchParams: Record<string, string | string[]>;
|
|
20
|
+
pathname: string;
|
|
21
|
+
error?: unknown;
|
|
22
|
+
digest?: string;
|
|
23
|
+
}): Promise<ReactNode | null>;
|
|
24
|
+
static composeRenders({ renders, params, searchParams, }: {
|
|
25
|
+
renders: RouteRender[];
|
|
26
|
+
params: Record<string, string>;
|
|
27
|
+
searchParams: Record<string, string | string[]>;
|
|
28
|
+
}): ReactNode;
|
|
15
29
|
static renderAsync({ routeRender, children, params, searchParams, }: {
|
|
16
30
|
routeRender: RouteRender;
|
|
17
31
|
children: ReactNode;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PageState, PathRoute, RouteModule } from "akanjs/client";
|
|
1
|
+
import type { LayoutFallbackRoute, PageState, PathRoute, RouteModule } from "akanjs/client";
|
|
2
2
|
export type PagesContext = Record<string, () => Promise<RouteModule>>;
|
|
3
3
|
export declare const defaultPageState: PageState;
|
|
4
4
|
export interface RouteModuleCacheStats {
|
|
@@ -13,11 +13,16 @@ export declare class RouteTreeBuilder {
|
|
|
13
13
|
#private;
|
|
14
14
|
constructor(context: PagesContext);
|
|
15
15
|
build(): PathRoute[];
|
|
16
|
+
getFallbackRoutes(): LayoutFallbackRoute[];
|
|
16
17
|
static getCacheStats(): RouteModuleCacheStats;
|
|
17
18
|
static resetCacheStats(): void;
|
|
18
19
|
static match(pathname: string, pathRoutes: PathRoute[]): {
|
|
19
20
|
pathRoute: PathRoute;
|
|
20
21
|
params: Record<string, string>;
|
|
21
22
|
} | null;
|
|
23
|
+
static matchFallback(pathname: string, fallbackRoutes: LayoutFallbackRoute[]): {
|
|
24
|
+
fallbackRoute: LayoutFallbackRoute;
|
|
25
|
+
params: Record<string, string>;
|
|
26
|
+
} | null;
|
|
22
27
|
static parseSearchParams(search: string): Record<string, string | string[]>;
|
|
23
28
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AkanI18nConfig } from "akanjs/common";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type SystemPageKind = "not-found" | "error";
|
|
4
|
+
export interface SystemPageOptions {
|
|
5
|
+
kind: SystemPageKind;
|
|
6
|
+
pathname: string;
|
|
7
|
+
homeHref: string;
|
|
8
|
+
lang?: string;
|
|
9
|
+
stylesheetHref?: string | null;
|
|
10
|
+
showDetails?: boolean;
|
|
11
|
+
error?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface SystemPageResponseOptions extends SystemPageOptions {
|
|
14
|
+
method?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SystemPageHomeHrefOptions {
|
|
17
|
+
pathname: string;
|
|
18
|
+
i18n?: AkanI18nConfig;
|
|
19
|
+
basePaths?: Iterable<string>;
|
|
20
|
+
headerBasePath?: string | null;
|
|
21
|
+
}
|
|
22
|
+
export declare function createSystemPageDocument(options: SystemPageOptions): ReactNode;
|
|
23
|
+
export declare function createSystemPageResponse(options: SystemPageResponseOptions): Promise<Response>;
|
|
24
|
+
export declare function createSystemPageHeaders(): Headers;
|
|
25
|
+
export declare function createSystemPageFallbackText(kind: SystemPageKind): string;
|
|
26
|
+
export declare function getSystemPageErrorDetails(error: unknown): string;
|
|
27
|
+
export declare function getSystemPageHomeHref({ pathname, i18n, basePaths, headerBasePath, }: SystemPageHomeHrefOptions): string;
|
|
@@ -2,7 +2,8 @@ interface ResolveDefaultSqliteFileOptions {
|
|
|
2
2
|
appName: string;
|
|
3
3
|
fileName: string;
|
|
4
4
|
isProduction: boolean;
|
|
5
|
+
operationMode?: string;
|
|
5
6
|
workspaceRoot?: string;
|
|
6
7
|
}
|
|
7
|
-
export declare const resolveDefaultSqliteFile: ({ appName, fileName, isProduction, workspaceRoot, }: ResolveDefaultSqliteFileOptions) => string;
|
|
8
|
+
export declare const resolveDefaultSqliteFile: ({ appName, fileName, isProduction, operationMode, workspaceRoot, }: ResolveDefaultSqliteFileOptions) => string;
|
|
8
9
|
export {};
|
|
@@ -6,19 +6,15 @@ import { type HTMLAttributes, type ReactNode, type RefObject } from "react";
|
|
|
6
6
|
export declare const Client: {
|
|
7
7
|
(): import("react/jsx-runtime").JSX.Element;
|
|
8
8
|
Wrapper: ({ children, theme, lang, dictionary, signals, reconnect, }: ClientWrapperProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
-
Bridge: ({ env, lang, theme, prefix, gaTrackingId }: ClientBridgeProps) => "" | import("react/jsx-runtime").JSX.Element | undefined;
|
|
9
|
+
Bridge: ({ env, lang, theme, prefix, gaTrackingId, }: ClientBridgeProps) => "" | import("react/jsx-runtime").JSX.Element | undefined;
|
|
10
10
|
Inner: () => import("react/jsx-runtime").JSX.Element;
|
|
11
|
-
SsrBridge: ({ lang, prefix }: ClientSsrBridgeProps) => null;
|
|
11
|
+
SsrBridge: ({ lang, prefix, }: ClientSsrBridgeProps) => null;
|
|
12
12
|
};
|
|
13
13
|
interface ClientWrapperProps {
|
|
14
14
|
children: ReactNode;
|
|
15
15
|
theme?: AkanTheme;
|
|
16
16
|
lang?: string;
|
|
17
|
-
dictionary?:
|
|
18
|
-
[key: string]: {
|
|
19
|
-
[key: string]: string;
|
|
20
|
-
};
|
|
21
|
-
};
|
|
17
|
+
dictionary?: Record<string, Record<string, unknown>>;
|
|
22
18
|
signals?: SerializedSignal[];
|
|
23
19
|
reconnect?: boolean;
|
|
24
20
|
}
|
|
@@ -41,11 +37,11 @@ interface ClientBridgeProps {
|
|
|
41
37
|
prefix?: string;
|
|
42
38
|
gaTrackingId?: string;
|
|
43
39
|
}
|
|
44
|
-
export declare const ClientBridge: ({ env, lang, theme, prefix, gaTrackingId }: ClientBridgeProps) => "" | import("react/jsx-runtime").JSX.Element | undefined;
|
|
40
|
+
export declare const ClientBridge: ({ env, lang, theme, prefix, gaTrackingId, }: ClientBridgeProps) => "" | import("react/jsx-runtime").JSX.Element | undefined;
|
|
45
41
|
export declare const ClientInner: () => import("react/jsx-runtime").JSX.Element;
|
|
46
42
|
interface ClientSsrBridgeProps {
|
|
47
43
|
lang: string;
|
|
48
44
|
prefix?: string;
|
|
49
45
|
}
|
|
50
|
-
export declare const ClientSsrBridge: ({ lang, prefix }: ClientSsrBridgeProps) => null;
|
|
46
|
+
export declare const ClientSsrBridge: ({ lang, prefix, }: ClientSsrBridgeProps) => null;
|
|
51
47
|
export {};
|