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.
@@ -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
+ }
@@ -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), { headers: { "Content-Type": "text/html; charset=utf-8" } });
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, { method: "GET", headers: rscHeaders });
256
- const result = await this.#rsc.renderWithMeta(rscReq, { clientManifest: manifest.clientManifest });
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: { "Content-Type": "text/x-component; charset=utf-8", "Cache-Control": "no-store" },
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.#renderErrorResponse("__rsc", err);
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), { headers: { "Content-Type": "text/html; charset=utf-8" } });
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=0" : "no-store",
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: { "Content-Type": "text/html; charset=utf-8", "X-Akan-Cache": "HIT" },
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, { clientManifest: manifest.clientManifest });
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 Response.redirect(new URL("/404", url.origin), 307);
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
- if (htmlCacheKey) {
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: { "Content-Type": "text/html; charset=utf-8", "X-Akan-Cache": "MISS" },
389
+ headers: {
390
+ "Content-Type": "text/html; charset=utf-8",
391
+ "X-Akan-Cache": "MISS",
392
+ },
367
393
  });
368
394
  }
369
- return new Response(htmlStream, { headers: { "Content-Type": "text/html; charset=utf-8" } });
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 = RouteSeedIndexStore.match(url.pathname, this.#seedIndex.entries);
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
- #renderErrorResponse(scope: string, err: unknown): Response {
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", { status: 500 });
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({ artifact, cssBytesByUrl, rsc, seedIndex, upgradeHmrWs });
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
- if (isProduction) return path.join(process.cwd(), "sqlite", fileName);
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: "/backend/localFile/getBlob" } }: BlobStorageOptions) =>
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: "/backend/localFile/getBlob" } }: BlobStorageOptions) => 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
  }
@@ -8,6 +8,7 @@ export type RscRenderResult = {
8
8
  type: "stream";
9
9
  stream: ReadableStream<Uint8Array>;
10
10
  theme?: AkanTheme;
11
+ status?: number;
11
12
  } | {
12
13
  type: "redirect";
13
14
  location: string;
@@ -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 {};