akanjs 3.0.0-alpha.73 → 3.0.0-alpha.75

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.
@@ -98,7 +98,7 @@ export class FetchClient {
98
98
  private ErrorCls?: ErrorConstructor,
99
99
  ) {
100
100
  this.origin = origin;
101
- this.http = new HttpClient(origin, ErrorCls);
101
+ this.http = new HttpClient(origin, { ErrorCls });
102
102
  this.ws = new WsClient(FetchClient.#makeWsUri(origin), ErrorCls);
103
103
  Object.assign(this.#handlerStore, handler);
104
104
  this.handler = this.#makeHandlerProxy();
@@ -22,11 +22,15 @@ export interface ErrorConstructor {
22
22
  fromJSON: (payload: ErrorResponsePayload) => RestoredError;
23
23
  }
24
24
 
25
- interface FetchOptions {
25
+ export interface HttpClientOptions {
26
26
  headers?: Record<string, string>;
27
- baseUrl?: string;
28
27
  /** Milliseconds before the request is abandoned; `false` waits as long as the browser will. */
29
28
  timeout?: number | false;
29
+ ErrorCls?: ErrorConstructor;
30
+ }
31
+
32
+ interface FetchOptions extends Pick<HttpClientOptions, "headers" | "timeout"> {
33
+ baseUrl?: string;
30
34
  }
31
35
 
32
36
  const jsonContentType = /^application\/(?:[\w.+-]+\+)?json\b/i;
@@ -53,11 +57,14 @@ const DEFAULT_TIMEOUT_MS = 30_000;
53
57
 
54
58
  export class HttpClient {
55
59
  readonly baseUrl: string;
56
- constructor(
57
- baseUrl: string,
58
- private ErrorCls?: ErrorConstructor,
59
- ) {
60
+ #headers: Record<string, string>;
61
+ #timeout?: number | false;
62
+ private ErrorCls?: ErrorConstructor;
63
+ constructor(baseUrl: string, options: HttpClientOptions = {}) {
60
64
  this.baseUrl = baseUrl;
65
+ this.#headers = options.headers ?? {};
66
+ this.#timeout = options.timeout;
67
+ this.ErrorCls = options.ErrorCls;
61
68
  }
62
69
 
63
70
  setErrorConstructor(ErrorCls?: ErrorConstructor) {
@@ -70,13 +77,13 @@ export class HttpClient {
70
77
  return `${this.#resolveBaseUrl(options.baseUrl)}${url}`;
71
78
  }
72
79
 
73
- static #makeHeaders(headers: Record<string, string>, options: FetchOptions) {
74
- return { Accept: "application/json", ...headers, ...options.headers };
80
+ #makeHeaders(headers: Record<string, string>, options: FetchOptions) {
81
+ return { Accept: "application/json", ...this.#headers, ...headers, ...options.headers };
75
82
  }
76
83
  async get<Returns = unknown>(url: string, options: FetchOptions = {}): Promise<Returns> {
77
84
  return await this.#request<Returns>(
78
85
  this.#resolveUrl(url, options),
79
- { headers: HttpClient.#makeHeaders({ "Content-Type": "application/json" }, options) },
86
+ { headers: this.#makeHeaders({ "Content-Type": "application/json" }, options) },
80
87
  options,
81
88
  );
82
89
  }
@@ -94,7 +101,7 @@ export class HttpClient {
94
101
  const { body, headers } = this.#makeReqContent(data);
95
102
  return await this.#request<Returns>(
96
103
  this.#resolveUrl(url, options),
97
- { method, body, headers: HttpClient.#makeHeaders(headers, options) },
104
+ { method, body, headers: this.#makeHeaders(headers, options) },
98
105
  options,
99
106
  );
100
107
  }
@@ -115,13 +122,13 @@ export class HttpClient {
115
122
  async delete<Returns = unknown>(url: string, options: FetchOptions = {}): Promise<Returns> {
116
123
  return await this.#request<Returns>(
117
124
  this.#resolveUrl(url, options),
118
- { method: "DELETE", headers: HttpClient.#makeHeaders({ "Content-Type": "application/json" }, options) },
125
+ { method: "DELETE", headers: this.#makeHeaders({ "Content-Type": "application/json" }, options) },
119
126
  options,
120
127
  );
121
128
  }
122
129
 
123
130
  async #request<Returns>(url: string, init: RequestInit, options: FetchOptions = {}): Promise<Returns> {
124
- const res = await this.#fetch(url, HttpClient.#withTimeout(init, options));
131
+ const res = await this.#fetch(url, this.#withTimeout(init, options));
125
132
  return await this.#readJsonResponse<Returns>(res);
126
133
  }
127
134
 
@@ -130,8 +137,8 @@ export class HttpClient {
130
137
  * the request body is the only thing this side can tell that from. Everything else takes the default unless
131
138
  * the caller named one.
132
139
  */
133
- static #withTimeout(init: RequestInit, options: FetchOptions): RequestInit {
134
- const timeout = options.timeout ?? (init.body instanceof FormData ? false : DEFAULT_TIMEOUT_MS);
140
+ #withTimeout(init: RequestInit, options: FetchOptions): RequestInit {
141
+ const timeout = options.timeout ?? (init.body instanceof FormData ? false : (this.#timeout ?? DEFAULT_TIMEOUT_MS));
135
142
  if (timeout === false || !Number.isFinite(timeout) || timeout <= 0) return init;
136
143
  return { ...init, signal: AbortSignal.timeout(timeout) };
137
144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.73",
3
+ "version": "3.0.0-alpha.75",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/server/akanApp.ts CHANGED
@@ -832,6 +832,13 @@ export class AkanApp {
832
832
  this.#scheduleChildRestart(child, child.proc, "upstream-open-failed");
833
833
  return AkanApp.#unavailableResponse(req, "Federation child upstream is unreachable; restarting");
834
834
  }
835
+ if (req.signal.aborted) return AkanApp.#clientClosedResponse();
836
+ if (AkanApp.#isUpstreamMidFlightClose(error)) {
837
+ this.logger.warn(
838
+ `Child ${child.idx}/${child.role} closed the connection mid-request (${req.method} ${new URL(req.url).pathname})`,
839
+ );
840
+ return AkanApp.#badGatewayResponse(req);
841
+ }
835
842
  throw error;
836
843
  } finally {
837
844
  child.metrics.activeRequests = Math.max(0, (child.metrics.activeRequests ?? 1) - 1);
@@ -845,6 +852,43 @@ export class AkanApp {
845
852
  return candidate.code === "FailedToOpenSocket" || String(candidate.message ?? "").includes("FailedToOpenSocket");
846
853
  }
847
854
 
855
+ /**
856
+ * The socket opened and then died under us: the child crashed, or was recycled, while its response was still
857
+ * in flight. Distinct from `FailedToOpenSocket` — that one means the child was already gone when we dialled,
858
+ * and only that one implies the socket needs a restart, which the child's own exit handler schedules anyway.
859
+ * Rethrowing this instead reaches Bun as an unhandled route error, which answers 500 and prints the raw
860
+ * `TypeError` against whatever URL happened to be in flight — usually a browser background probe, which is
861
+ * how a dead child gets misread as a bug in the probed route.
862
+ */
863
+ static #isUpstreamMidFlightClose(error: unknown): boolean {
864
+ if (!error || typeof error !== "object") return false;
865
+ const candidate = error as { code?: unknown; message?: unknown };
866
+ if (candidate.code === "ECONNRESET" || candidate.code === "ConnectionClosed") return true;
867
+ return String(candidate.message ?? "").includes("connection was closed");
868
+ }
869
+
870
+ static #clientClosedResponse(): Response {
871
+ return new Response(null, { status: 499, headers: { "cache-control": "no-store" } });
872
+ }
873
+
874
+ static #badGatewayResponse(req: Request): Response {
875
+ const detail = "The replica closed the connection before it finished answering.";
876
+ if (req.headers.get("accept")?.includes("text/html")) {
877
+ return new Response(
878
+ AkanApp.#statusPageHtml({
879
+ heading: "Backend dropped the connection",
880
+ detail,
881
+ note: "The replica is restarting — this page reloads itself as soon as it answers.",
882
+ }),
883
+ { status: 502, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } },
884
+ );
885
+ }
886
+ return new Response(detail, {
887
+ status: 502,
888
+ headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" },
889
+ });
890
+ }
891
+
848
892
  /**
849
893
  * A replica that is booting or restarting is normally back within a second or two, and a browser
850
894
  * handed a 503 stays on that dead page until somebody reloads by hand — so a request waits for the
@@ -4,7 +4,8 @@ export class ShutdownManager {
4
4
  /**
5
5
  * Registers process-level handlers that drive `onShutdown` on:
6
6
  * - SIGTERM / SIGINT (graceful stop → exit(0) on success, exit(1) on error)
7
- * - uncaughtException / unhandledRejection (best-effort stop → exit(1))
7
+ * - uncaughtException (best-effort stop → exit(1))
8
+ * - unhandledRejection (logged at error, process kept alive; see `#isFatalUnhandledRejection`)
8
9
  *
9
10
  * Kept separate from `AkanServer` so the server class doesn't have to know about
10
11
  * Node-level process events, and so tests can opt out by not calling it.
@@ -37,6 +38,7 @@ export class ShutdownManager {
37
38
 
38
39
  process.on("unhandledRejection", async (reason) => {
39
40
  logger.error(`Unhandled rejection: ${ShutdownManager.#formatError(reason)}`);
41
+ if (!ShutdownManager.#isFatalUnhandledRejection()) return;
40
42
  try {
41
43
  await onShutdown();
42
44
  process.exit(1);
@@ -46,6 +48,19 @@ export class ShutdownManager {
46
48
  });
47
49
  }
48
50
 
51
+ /**
52
+ * A rejected promise nobody awaited is scoped to whatever built it, so it cannot leave the process in the
53
+ * unknown state an `uncaughtException` does — while exiting over one drops every other in-flight request,
54
+ * every websocket, and (under federation) restarts the replica. React's streaming SSR is the case that forced
55
+ * this: it rejects `stream.allReady` for a post-shell render failure and holds no handler of its own, so one
56
+ * page's boundary error used to kill a whole child. Set `AKAN_FATAL_UNHANDLED_REJECTION=1` to restore the
57
+ * exit for a deployment that would rather crash than serve on.
58
+ */
59
+ static #isFatalUnhandledRejection(): boolean {
60
+ const flag = process.env.AKAN_FATAL_UNHANDLED_REJECTION;
61
+ return flag === "1" || flag === "true";
62
+ }
63
+
49
64
  static #formatError(error: unknown): string {
50
65
  return error instanceof Error ? (error.stack ?? error.message) : String(error);
51
66
  }
@@ -850,6 +850,32 @@ export class RscRenderer {
850
850
  this.#send({ type: "metrics", metrics });
851
851
  }
852
852
 
853
+ /**
854
+ * An error raised after the first Flight chunk has left the worker can no longer become a status code or a
855
+ * system error page — `sendLateRedirect` is the only control the host can still act on, so the render
856
+ * control is dropped. Logging here is the only record that the request failed at all; without it a page
857
+ * whose boundary died mid-stream is indistinguishable from one that rendered.
858
+ */
859
+ #reportRenderError(error: unknown, pathname?: string): void {
860
+ const description = error instanceof Error ? (error.stack ?? error.message) : String(error);
861
+ const scope = pathname ? ` path=${pathname}` : "";
862
+ if (RscRenderer.#isExpectedRequestAbort(error)) {
863
+ this.#logger.debug(`[rsc] render aborted${scope}: ${description}`);
864
+ return;
865
+ }
866
+ this.#logger.error(`[rsc] render failed${scope}: ${description}`);
867
+ }
868
+
869
+ /** A client that navigated away, or a stream the host cancelled: expected, and not the replica's problem. */
870
+ static #isExpectedRequestAbort(error: unknown): boolean {
871
+ if (!(error instanceof Error)) return false;
872
+ return (
873
+ error.name === "AbortError" ||
874
+ error.message === "Connection closed." ||
875
+ error.message.includes("The connection was closed")
876
+ );
877
+ }
878
+
853
879
  async #renderFlightElement(
854
880
  element: ReactNode,
855
881
  clientManifest: ClientManifest,
@@ -888,6 +914,7 @@ export class RscRenderer {
888
914
  return error.digest;
889
915
  }
890
916
  controlRef.current = { type: "error", error };
917
+ this.#reportRenderError(error, options.trace?.pathname);
891
918
  return error instanceof Error ? error.message : String(error);
892
919
  },
893
920
  });
@@ -1,4 +1,5 @@
1
1
  import { Readable } from "node:stream";
2
+ import { Logger } from "akanjs/common";
2
3
  import { type AkanRequestStore, type AkanTheme, pushRequestFallback, requestStorage } from "akanjs/fetch";
3
4
  import type { ReactNode } from "react";
4
5
  import { renderToReadableStream } from "react-dom/server.browser";
@@ -575,6 +576,8 @@ export class SsrFromRscRenderer {
575
576
  SsrFromRscRenderer.#installWebpackShims();
576
577
  }
577
578
 
579
+ static readonly #logger = new Logger("SsrFromRscRenderer");
580
+
578
581
  static getChunkRegistryStats(): SsrChunkRegistryStats {
579
582
  return { ...SsrFromRscRenderer.#chunkRegistryStats };
580
583
  }
@@ -599,8 +602,14 @@ export class SsrFromRscRenderer {
599
602
  const root = await thenable;
600
603
  const stream = await renderToReadableStream(root, {
601
604
  bootstrapScriptContent: bootstrap,
605
+ onError: (error) => SsrFromRscRenderer.#reportRenderError(error, input),
602
606
  });
603
607
  if (waitForAllReady) await stream.allReady;
608
+ else {
609
+ SsrFromRscRenderer.holdPostShellErrors(stream, (error) =>
610
+ SsrFromRscRenderer.#reportRenderError(error, input, "post-shell"),
611
+ );
612
+ }
604
613
  return stream;
605
614
  };
606
615
  const requestContext = input.requestStore ?? input.request;
@@ -657,6 +666,47 @@ export class SsrFromRscRenderer {
657
666
  };
658
667
  }
659
668
 
669
+ /**
670
+ * React rejects `stream.allReady` for any fatal error raised after the shell flushed — an abort landing
671
+ * mid-flush, a `flushCompletedQueues` invariant — and it attaches a handler to that promise itself only on
672
+ * the shell-error path, because `completeShell` replaces `onShellError` with a noop. Shell-first streaming
673
+ * never reads `allReady`, so an unheld rejection reaches `process.on("unhandledRejection")`, and under
674
+ * federation that took the whole replica down over one request's render.
675
+ *
676
+ * Holding it keeps the failure scoped to the stream that raised it. The response cannot become a 5xx — the
677
+ * shell's headers and markup are already on the wire — so the client gets a truncated document and the stack
678
+ * goes to the log; every other request and socket on the replica survives.
679
+ */
680
+ static holdPostShellErrors(stream: { allReady: Promise<void> }, report: (error: unknown) => void): void {
681
+ void stream.allReady.catch(report);
682
+ }
683
+
684
+ static #reportRenderError(error: unknown, input: SsrFromRscInput, phase = "render"): void {
685
+ const description = SsrFromRscRenderer.#describeError(error);
686
+ if (SsrFromRscRenderer.#isExpectedRequestAbort(error)) {
687
+ SsrFromRscRenderer.#logger.debug(`[SSR] ${phase} aborted: ${description}`);
688
+ return;
689
+ }
690
+ const path = input.request ? new URL(input.request.url).pathname : "unknown";
691
+ SsrFromRscRenderer.#logger.error(`[SSR] ${phase} failed path=${path}: ${description}`);
692
+ }
693
+
694
+ static #describeError(error: unknown): string {
695
+ if (!(error instanceof Error)) return String(error);
696
+ return error.stack ?? error.message;
697
+ }
698
+
699
+ /** A client that navigated away, or a stream we cancelled ourselves: expected, and not the replica's problem. */
700
+ static #isExpectedRequestAbort(error: unknown): boolean {
701
+ if (!(error instanceof Error)) return false;
702
+ return (
703
+ error.name === "AbortError" ||
704
+ error.name === "AkanRedirectError" ||
705
+ error.message === "Connection closed." ||
706
+ error.message.includes("The connection was closed")
707
+ );
708
+ }
709
+
660
710
  static #suppressExpectedLateRedirectError(
661
711
  thenable: PromiseLike<ReactNode>,
662
712
  lateControl?: Promise<SsrLateRedirect | null>,
@@ -519,6 +519,12 @@ export class WebRouter {
519
519
  WebRouter.#deepLinkAssociationResponse(ANDROID_ASSET_LINKS_PATH, this.#artifact, {
520
520
  cacheControl: this.#prodMode ? "public, max-age=3600" : "no-store",
521
521
  }) ?? new Response("Not Found", { status: 404 }),
522
+
523
+ "/.well-known/*": () =>
524
+ new Response("Not Found", {
525
+ status: 404,
526
+ headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" },
527
+ }),
522
528
  [FIREBASE_MESSAGING_SW_PATH]: async () => {
523
529
  this.#requestStats.staticAsset += 1;
524
530
  const firebaseConfig = await WebRouter.#resolveFirebaseClientConfig();
@@ -18,17 +18,20 @@ export interface RestoredError extends Error {
18
18
  export interface ErrorConstructor {
19
19
  fromJSON: (payload: ErrorResponsePayload) => RestoredError;
20
20
  }
21
- interface FetchOptions {
21
+ export interface HttpClientOptions {
22
22
  headers?: Record<string, string>;
23
- baseUrl?: string;
24
23
  /** Milliseconds before the request is abandoned; `false` waits as long as the browser will. */
25
24
  timeout?: number | false;
25
+ ErrorCls?: ErrorConstructor;
26
+ }
27
+ interface FetchOptions extends Pick<HttpClientOptions, "headers" | "timeout"> {
28
+ baseUrl?: string;
26
29
  }
27
30
  export declare class HttpClient {
28
31
  #private;
29
- private ErrorCls?;
30
32
  readonly baseUrl: string;
31
- constructor(baseUrl: string, ErrorCls?: ErrorConstructor | undefined);
33
+ private ErrorCls?;
34
+ constructor(baseUrl: string, options?: HttpClientOptions);
32
35
  setErrorConstructor(ErrorCls?: ErrorConstructor): void;
33
36
  get<Returns = unknown>(url: string, options?: FetchOptions): Promise<Returns>;
34
37
  send<Returns = unknown>(method: HttpMutationMethod, url: string, data: FormData | Record<string, unknown>, options?: FetchOptions): Promise<Returns>;
@@ -4,7 +4,8 @@ export declare class ShutdownManager {
4
4
  /**
5
5
  * Registers process-level handlers that drive `onShutdown` on:
6
6
  * - SIGTERM / SIGINT (graceful stop → exit(0) on success, exit(1) on error)
7
- * - uncaughtException / unhandledRejection (best-effort stop → exit(1))
7
+ * - uncaughtException (best-effort stop → exit(1))
8
+ * - unhandledRejection (logged at error, process kept alive; see `#isFatalUnhandledRejection`)
8
9
  *
9
10
  * Kept separate from `AkanServer` so the server class doesn't have to know about
10
11
  * Node-level process events, and so tests can opt out by not calling it.
@@ -36,4 +36,18 @@ export declare class SsrFromRscRenderer {
36
36
  #private;
37
37
  static getChunkRegistryStats(): SsrChunkRegistryStats;
38
38
  render(input: SsrFromRscInput): Promise<ReadableStream<Uint8Array>>;
39
+ /**
40
+ * React rejects `stream.allReady` for any fatal error raised after the shell flushed — an abort landing
41
+ * mid-flush, a `flushCompletedQueues` invariant — and it attaches a handler to that promise itself only on
42
+ * the shell-error path, because `completeShell` replaces `onShellError` with a noop. Shell-first streaming
43
+ * never reads `allReady`, so an unheld rejection reaches `process.on("unhandledRejection")`, and under
44
+ * federation that took the whole replica down over one request's render.
45
+ *
46
+ * Holding it keeps the failure scoped to the stream that raised it. The response cannot become a 5xx — the
47
+ * shell's headers and markup are already on the wire — so the client gets a truncated document and the stack
48
+ * goes to the log; every other request and socket on the replica survives.
49
+ */
50
+ static holdPostShellErrors(stream: {
51
+ allReady: Promise<void>;
52
+ }, report: (error: unknown) => void): void;
39
53
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * One named export per member, rather than the `Loading` namespace object itself, because the `"use client"`
3
+ * transform replaces a client module's *exports* with `registerClientReference` stubs. A namespace exported
4
+ * from here would reach a server component as one reference for the whole object, and `Loading.Skeleton` on it
5
+ * would read a property the stub does not have — `undefined`, which React reports as an invalid element type.
6
+ * The namespace is assembled in `index.ts`, which carries no directive, so each member crosses on its own.
7
+ */
8
+ export declare const LoadingArea: import("react").ComponentType<Record<string, never>>;
9
+ export declare const LoadingButton: import("react").ComponentType<import("./Button.d.ts").LoadingProps>;
10
+ export declare const LoadingInput: import("react").ComponentType<import("./Input.d.ts").LoadingProps>;
11
+ export declare const LoadingProgressBar: import("react").ComponentType<import("./ProgressBar.d.ts").ProgressBarProps>;
12
+ export declare const LoadingSkeleton: import("react").ComponentType<import("./Skeleton.d.ts").SkeletonProps>;
13
+ export declare const LoadingSpin: import("react").ComponentType<import("./Spin.d.ts").SpinProps>;
@@ -0,0 +1,15 @@
1
+ import { LoadingArea, LoadingButton, LoadingInput, LoadingProgressBar, LoadingSkeleton, LoadingSpin } from "./index_";
2
+
3
+ /**
4
+ * Loading indicators. Each member is an independent override slot (`LoadingSpin`, `LoadingSkeleton`,
5
+ * `LoadingProgressBar`, `LoadingButton`, `LoadingInput`, `LoadingArea`), resolved from the closest
6
+ * `page/**\/_overrides.tsx` in the route's ancestry, otherwise the shipped default.
7
+ */
8
+ export const Loading = {
9
+ Area: LoadingArea,
10
+ Button: LoadingButton,
11
+ Input: LoadingInput,
12
+ ProgressBar: LoadingProgressBar,
13
+ Skeleton: LoadingSkeleton,
14
+ Spin: LoadingSpin,
15
+ };
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ import { createOverridable } from "../UiOverride";
3
+ import { Area } from "./Area";
4
+ import { Button } from "./Button";
5
+ import { Input } from "./Input";
6
+ import { ProgressBar } from "./ProgressBar";
7
+ import { Skeleton } from "./Skeleton";
8
+ import { Spin } from "./Spin";
9
+
10
+ /**
11
+ * One named export per member, rather than the `Loading` namespace object itself, because the `"use client"`
12
+ * transform replaces a client module's *exports* with `registerClientReference` stubs. A namespace exported
13
+ * from here would reach a server component as one reference for the whole object, and `Loading.Skeleton` on it
14
+ * would read a property the stub does not have — `undefined`, which React reports as an invalid element type.
15
+ * The namespace is assembled in `index.ts`, which carries no directive, so each member crosses on its own.
16
+ */
17
+ export const LoadingArea = createOverridable("LoadingArea", Area);
18
+ export const LoadingButton = createOverridable("LoadingButton", Button);
19
+ export const LoadingInput = createOverridable("LoadingInput", Input);
20
+ export const LoadingProgressBar = createOverridable("LoadingProgressBar", ProgressBar);
21
+ export const LoadingSkeleton = createOverridable("LoadingSkeleton", Skeleton);
22
+ export const LoadingSpin = createOverridable("LoadingSpin", Spin);
@@ -1,22 +0,0 @@
1
- "use client";
2
- import { createOverridable } from "../UiOverride";
3
- import { Area } from "./Area";
4
- import { Button } from "./Button";
5
- import { Input } from "./Input";
6
- import { ProgressBar } from "./ProgressBar";
7
- import { Skeleton } from "./Skeleton";
8
- import { Spin } from "./Spin";
9
-
10
- /**
11
- * Loading indicators. Each member is an independent override slot (`LoadingSpin`, `LoadingSkeleton`,
12
- * `LoadingProgressBar`, `LoadingButton`, `LoadingInput`, `LoadingArea`), resolved from the closest
13
- * `page/**\/_overrides.tsx` in the route's ancestry, otherwise the shipped default.
14
- */
15
- export const Loading = {
16
- Area: createOverridable("LoadingArea", Area),
17
- Button: createOverridable("LoadingButton", Button),
18
- Input: createOverridable("LoadingInput", Input),
19
- ProgressBar: createOverridable("LoadingProgressBar", ProgressBar),
20
- Skeleton: createOverridable("LoadingSkeleton", Skeleton),
21
- Spin: createOverridable("LoadingSpin", Spin),
22
- };