@textjs/app 0.0.0-stage → 1.0.2
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/README.md +34 -2
- package/bin/text.js +29 -0
- package/bin/text.ts +13 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli/build.d.ts +39 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +157 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/config.d.ts +22 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +73 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/dev.d.ts +12 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +49 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/index.d.ts +26 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +192 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run.d.ts +31 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +111 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/start.d.ts +14 -0
- package/dist/cli/start.d.ts.map +1 -0
- package/dist/cli/start.js +183 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/ui.d.ts +35 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +73 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +93 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +143 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +44 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +26 -0
- package/dist/context.js.map +1 -0
- package/dist/dev-errors.d.ts +68 -0
- package/dist/dev-errors.d.ts.map +1 -0
- package/dist/dev-errors.js +121 -0
- package/dist/dev-errors.js.map +1 -0
- package/dist/devtools.d.ts +64 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +186 -0
- package/dist/devtools.js.map +1 -0
- package/dist/frame-mirror.d.ts +19 -0
- package/dist/frame-mirror.d.ts.map +1 -0
- package/dist/frame-mirror.js +141 -0
- package/dist/frame-mirror.js.map +1 -0
- package/dist/frame-protocol.d.ts +175 -0
- package/dist/frame-protocol.d.ts.map +1 -0
- package/dist/frame-protocol.js +369 -0
- package/dist/frame-protocol.js.map +1 -0
- package/dist/history.d.ts +54 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +152 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/link.d.ts +32 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +54 -0
- package/dist/link.js.map +1 -0
- package/dist/live-player.d.ts +83 -0
- package/dist/live-player.d.ts.map +1 -0
- package/dist/live-player.js +413 -0
- package/dist/live-player.js.map +1 -0
- package/dist/live-protocol.d.ts +132 -0
- package/dist/live-protocol.d.ts.map +1 -0
- package/dist/live-protocol.js +278 -0
- package/dist/live-protocol.js.map +1 -0
- package/dist/live-stub.d.ts +10 -0
- package/dist/live-stub.d.ts.map +1 -0
- package/dist/live-stub.js +32 -0
- package/dist/live-stub.js.map +1 -0
- package/dist/mcp/cli.d.ts +22 -0
- package/dist/mcp/cli.d.ts.map +1 -0
- package/dist/mcp/cli.js +177 -0
- package/dist/mcp/cli.js.map +1 -0
- package/dist/mcp/frame.d.ts +58 -0
- package/dist/mcp/frame.d.ts.map +1 -0
- package/dist/mcp/frame.js +169 -0
- package/dist/mcp/frame.js.map +1 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/inspect.d.ts +95 -0
- package/dist/mcp/inspect.d.ts.map +1 -0
- package/dist/mcp/inspect.js +386 -0
- package/dist/mcp/inspect.js.map +1 -0
- package/dist/mcp/keys.d.ts +20 -0
- package/dist/mcp/keys.d.ts.map +1 -0
- package/dist/mcp/keys.js +87 -0
- package/dist/mcp/keys.js.map +1 -0
- package/dist/mcp/logs.d.ts +63 -0
- package/dist/mcp/logs.d.ts.map +1 -0
- package/dist/mcp/logs.js +217 -0
- package/dist/mcp/logs.js.map +1 -0
- package/dist/mcp/server.d.ts +64 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +485 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +230 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +742 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/navigation-errors.d.ts +28 -0
- package/dist/navigation-errors.d.ts.map +1 -0
- package/dist/navigation-errors.js +43 -0
- package/dist/navigation-errors.js.map +1 -0
- package/dist/navigation.d.ts +21 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +33 -0
- package/dist/navigation.js.map +1 -0
- package/dist/router.d.ts +76 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +296 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.d.ts +101 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +286 -0
- package/dist/routing.js.map +1 -0
- package/dist/server/adapters/build-step.d.ts +34 -0
- package/dist/server/adapters/build-step.d.ts.map +1 -0
- package/dist/server/adapters/build-step.js +112 -0
- package/dist/server/adapters/build-step.js.map +1 -0
- package/dist/server/adapters/bun.d.ts +8 -0
- package/dist/server/adapters/bun.d.ts.map +1 -0
- package/dist/server/adapters/bun.js +89 -0
- package/dist/server/adapters/bun.js.map +1 -0
- package/dist/server/adapters/bundle.d.ts +21 -0
- package/dist/server/adapters/bundle.d.ts.map +1 -0
- package/dist/server/adapters/bundle.js +86 -0
- package/dist/server/adapters/bundle.js.map +1 -0
- package/dist/server/adapters/cloudflare.d.ts +29 -0
- package/dist/server/adapters/cloudflare.d.ts.map +1 -0
- package/dist/server/adapters/cloudflare.js +105 -0
- package/dist/server/adapters/cloudflare.js.map +1 -0
- package/dist/server/adapters/index.d.ts +10 -0
- package/dist/server/adapters/index.d.ts.map +1 -0
- package/dist/server/adapters/index.js +24 -0
- package/dist/server/adapters/index.js.map +1 -0
- package/dist/server/adapters/runtime-node.d.ts +16 -0
- package/dist/server/adapters/runtime-node.d.ts.map +1 -0
- package/dist/server/adapters/runtime-node.js +29 -0
- package/dist/server/adapters/runtime-node.js.map +1 -0
- package/dist/server/adapters/runtime.d.ts +42 -0
- package/dist/server/adapters/runtime.d.ts.map +1 -0
- package/dist/server/adapters/runtime.js +115 -0
- package/dist/server/adapters/runtime.js.map +1 -0
- package/dist/server/adapters/shared.d.ts +66 -0
- package/dist/server/adapters/shared.d.ts.map +1 -0
- package/dist/server/adapters/shared.js +124 -0
- package/dist/server/adapters/shared.js.map +1 -0
- package/dist/server/adapters/static.d.ts +9 -0
- package/dist/server/adapters/static.d.ts.map +1 -0
- package/dist/server/adapters/static.js +70 -0
- package/dist/server/adapters/static.js.map +1 -0
- package/dist/server/adapters/vercel.d.ts +43 -0
- package/dist/server/adapters/vercel.d.ts.map +1 -0
- package/dist/server/adapters/vercel.js +119 -0
- package/dist/server/adapters/vercel.js.map +1 -0
- package/dist/server/bun-jsx.d.ts +18 -0
- package/dist/server/bun-jsx.d.ts.map +1 -0
- package/dist/server/bun-jsx.js +56 -0
- package/dist/server/bun-jsx.js.map +1 -0
- package/dist/server/cache-route.d.ts +38 -0
- package/dist/server/cache-route.d.ts.map +1 -0
- package/dist/server/cache-route.js +109 -0
- package/dist/server/cache-route.js.map +1 -0
- package/dist/server/cache.d.ts +131 -0
- package/dist/server/cache.d.ts.map +1 -0
- package/dist/server/cache.js +409 -0
- package/dist/server/cache.js.map +1 -0
- package/dist/server/frames.d.ts +45 -0
- package/dist/server/frames.d.ts.map +1 -0
- package/dist/server/frames.js +294 -0
- package/dist/server/frames.js.map +1 -0
- package/dist/server/http.d.ts +80 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +453 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/jsx-dev-runtime.d.ts +5 -0
- package/dist/server/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/server/jsx-dev-runtime.js +6 -0
- package/dist/server/jsx-dev-runtime.js.map +1 -0
- package/dist/server/jsx-runtime.d.ts +10 -0
- package/dist/server/jsx-runtime.d.ts.map +1 -0
- package/dist/server/jsx-runtime.js +11 -0
- package/dist/server/jsx-runtime.js.map +1 -0
- package/dist/server/live-app.d.ts +87 -0
- package/dist/server/live-app.d.ts.map +1 -0
- package/dist/server/live-app.js +307 -0
- package/dist/server/live-app.js.map +1 -0
- package/dist/server/live-build.d.ts +70 -0
- package/dist/server/live-build.d.ts.map +1 -0
- package/dist/server/live-build.js +222 -0
- package/dist/server/live-build.js.map +1 -0
- package/dist/server/live-dev.d.ts +62 -0
- package/dist/server/live-dev.d.ts.map +1 -0
- package/dist/server/live-dev.js +313 -0
- package/dist/server/live-dev.js.map +1 -0
- package/dist/server/live-manifest.d.ts +23 -0
- package/dist/server/live-manifest.d.ts.map +1 -0
- package/dist/server/live-manifest.js +7 -0
- package/dist/server/live-manifest.js.map +1 -0
- package/dist/server/live-prod.d.ts +30 -0
- package/dist/server/live-prod.d.ts.map +1 -0
- package/dist/server/live-prod.js +97 -0
- package/dist/server/live-prod.js.map +1 -0
- package/dist/server/live-routes.d.ts +72 -0
- package/dist/server/live-routes.d.ts.map +1 -0
- package/dist/server/live-routes.js +274 -0
- package/dist/server/live-routes.js.map +1 -0
- package/dist/server/live-web.d.ts +16 -0
- package/dist/server/live-web.d.ts.map +1 -0
- package/dist/server/live-web.js +159 -0
- package/dist/server/live-web.js.map +1 -0
- package/dist/server/live.d.ts +138 -0
- package/dist/server/live.d.ts.map +1 -0
- package/dist/server/live.js +542 -0
- package/dist/server/live.js.map +1 -0
- package/dist/server/middleware-node.d.ts +48 -0
- package/dist/server/middleware-node.d.ts.map +1 -0
- package/dist/server/middleware-node.js +167 -0
- package/dist/server/middleware-node.js.map +1 -0
- package/dist/server/middleware-runtime.d.ts +97 -0
- package/dist/server/middleware-runtime.d.ts.map +1 -0
- package/dist/server/middleware-runtime.js +447 -0
- package/dist/server/middleware-runtime.js.map +1 -0
- package/dist/server/middleware.d.ts +145 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +365 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/ppr.d.ts +22 -0
- package/dist/server/ppr.d.ts.map +1 -0
- package/dist/server/ppr.js +58 -0
- package/dist/server/ppr.js.map +1 -0
- package/dist/server/scan.d.ts +48 -0
- package/dist/server/scan.d.ts.map +1 -0
- package/dist/server/scan.js +161 -0
- package/dist/server/scan.js.map +1 -0
- package/dist/server/server-components.d.ts +65 -0
- package/dist/server/server-components.d.ts.map +1 -0
- package/dist/server/server-components.js +215 -0
- package/dist/server/server-components.js.map +1 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +5 -0
- package/dist/terminal.js.map +1 -0
- package/dist/testing.d.ts +5 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +5 -0
- package/dist/testing.js.map +1 -0
- package/dist/vite-plugin.d.ts +51 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +403 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +130 -4
- package/src/cli/build.ts +182 -0
- package/src/cli/config.ts +72 -0
- package/src/cli/dev.ts +54 -0
- package/src/cli/index.ts +185 -0
- package/src/cli/run.ts +124 -0
- package/src/cli/start.ts +194 -0
- package/src/cli/ui.ts +93 -0
- package/src/client.tsx +122 -0
- package/src/config.ts +155 -0
- package/src/context.ts +69 -0
- package/src/dev-errors.ts +167 -0
- package/src/devtools.tsx +419 -0
- package/src/frame-mirror.ts +145 -0
- package/src/frame-protocol.ts +435 -0
- package/src/history.ts +201 -0
- package/src/index.ts +9 -0
- package/src/link.tsx +108 -0
- package/src/live-player.ts +449 -0
- package/src/live-protocol.ts +317 -0
- package/src/live-stub.tsx +32 -0
- package/src/mcp/cli.ts +188 -0
- package/src/mcp/frame.ts +224 -0
- package/src/mcp/index.ts +15 -0
- package/src/mcp/inspect.ts +445 -0
- package/src/mcp/keys.ts +86 -0
- package/src/mcp/logs.ts +225 -0
- package/src/mcp/server.ts +595 -0
- package/src/mcp/session.ts +821 -0
- package/src/navigation-errors.ts +60 -0
- package/src/navigation.ts +44 -0
- package/src/router.tsx +497 -0
- package/src/routing.ts +344 -0
- package/src/server/adapters/build-step.ts +135 -0
- package/src/server/adapters/bun.ts +92 -0
- package/src/server/adapters/bundle.ts +97 -0
- package/src/server/adapters/cloudflare.ts +111 -0
- package/src/server/adapters/index.ts +29 -0
- package/src/server/adapters/runtime-node.ts +34 -0
- package/src/server/adapters/runtime.ts +145 -0
- package/src/server/adapters/shared.ts +159 -0
- package/src/server/adapters/static.ts +68 -0
- package/src/server/adapters/vercel.ts +149 -0
- package/src/server/bun-jsx.ts +65 -0
- package/src/server/cache-route.ts +139 -0
- package/src/server/cache.ts +498 -0
- package/src/server/frames.ts +326 -0
- package/src/server/http.ts +467 -0
- package/src/server/jsx-dev-runtime.ts +9 -0
- package/src/server/jsx-runtime.ts +13 -0
- package/src/server/live-app.tsx +374 -0
- package/src/server/live-build.ts +258 -0
- package/src/server/live-dev.ts +357 -0
- package/src/server/live-manifest.ts +25 -0
- package/src/server/live-prod.ts +120 -0
- package/src/server/live-routes.ts +298 -0
- package/src/server/live-web.ts +170 -0
- package/src/server/live.ts +551 -0
- package/src/server/middleware-node.ts +168 -0
- package/src/server/middleware-runtime.ts +478 -0
- package/src/server/middleware.ts +451 -0
- package/src/server/ppr.ts +62 -0
- package/src/server/scan.ts +190 -0
- package/src/server/server-components.ts +224 -0
- package/src/terminal.ts +5 -0
- package/src/testing.ts +5 -0
- package/src/vite-plugin.ts +413 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `notFound()` and `redirect()` throw tagged errors that the App Router's
|
|
3
|
+
* boundaries intercept (they never reach `error.tsx`).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const NOT_FOUND_DIGEST = "TEXTJS_NOT_FOUND";
|
|
7
|
+
export const REDIRECT_DIGEST = "TEXTJS_REDIRECT";
|
|
8
|
+
|
|
9
|
+
export type RedirectType = "push" | "replace";
|
|
10
|
+
|
|
11
|
+
export interface NotFoundError extends Error {
|
|
12
|
+
digest: typeof NOT_FOUND_DIGEST;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RedirectError extends Error {
|
|
16
|
+
digest: `${typeof REDIRECT_DIGEST};${RedirectType};${string}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Render the nearest `not-found.tsx` for the current segment. */
|
|
20
|
+
export function notFound(): never {
|
|
21
|
+
const error = new Error(NOT_FOUND_DIGEST) as NotFoundError;
|
|
22
|
+
error.digest = NOT_FOUND_DIGEST;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Navigate to `href` (replacing the current history entry by default). */
|
|
27
|
+
export function redirect(href: string, type: RedirectType = "replace"): never {
|
|
28
|
+
const error = new Error(REDIRECT_DIGEST) as RedirectError;
|
|
29
|
+
error.digest = `${REDIRECT_DIGEST};${type};${href}`;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A permanent redirect: replaces the current history entry. */
|
|
34
|
+
export function permanentRedirect(href: string): never {
|
|
35
|
+
return redirect(href, "replace");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function digestOf(error: unknown): string | undefined {
|
|
39
|
+
if (typeof error !== "object" || error === null) return undefined;
|
|
40
|
+
const d = (error as { digest?: unknown }).digest;
|
|
41
|
+
return typeof d === "string" ? d : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isNotFoundError(error: unknown): error is NotFoundError {
|
|
45
|
+
return digestOf(error) === NOT_FOUND_DIGEST;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isRedirectError(error: unknown): error is RedirectError {
|
|
49
|
+
return digestOf(error)?.startsWith(`${REDIRECT_DIGEST};`) ?? false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `true` for `notFound()` / `redirect()` control-flow errors. */
|
|
53
|
+
export function isNavigationError(error: unknown): boolean {
|
|
54
|
+
return isNotFoundError(error) || isRedirectError(error);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getRedirectTarget(error: RedirectError): { href: string; type: RedirectType } {
|
|
58
|
+
const [, type, ...rest] = error.digest.split(";");
|
|
59
|
+
return { href: rest.join(";"), type: type === "push" ? "push" : "replace" };
|
|
60
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@textjs/app/navigation` — App Router hooks with Next.js signatures.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { useRouterState, type AppRouterInstance } from "./context";
|
|
7
|
+
import type { RouteParams } from "./routing";
|
|
8
|
+
|
|
9
|
+
export { notFound, redirect, permanentRedirect, isNotFoundError, isRedirectError, isNavigationError, type RedirectType } from "./navigation-errors";
|
|
10
|
+
export type { AppRouterInstance, NavigateOptions } from "./context";
|
|
11
|
+
|
|
12
|
+
/** Programmatic navigation: `push`, `replace`, `back`, `forward`, `refresh`, `prefetch`. */
|
|
13
|
+
export function useRouter(): AppRouterInstance {
|
|
14
|
+
return useRouterState().router;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The current pathname, e.g. `/deployments/dpl_8f2k`. */
|
|
18
|
+
export function usePathname(): string {
|
|
19
|
+
return useRouterState().location.pathname;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Dynamic route params, e.g. `{ id: "dpl_8f2k" }` for `/deployments/[id]`. */
|
|
23
|
+
export function useParams<T extends RouteParams = RouteParams>(): T {
|
|
24
|
+
return useRouterState().match.params as T;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ReadonlyURLSearchParams = Omit<URLSearchParams, "append" | "delete" | "set" | "sort">;
|
|
28
|
+
|
|
29
|
+
/** The current query string as read-only `URLSearchParams`. */
|
|
30
|
+
export function useSearchParams(): ReadonlyURLSearchParams {
|
|
31
|
+
const search = useRouterState().location.search;
|
|
32
|
+
return useMemo(() => new URLSearchParams(search), [search]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Active segments below the root, e.g. `["deployments", "dpl_8f2k"]`. */
|
|
36
|
+
export function useSelectedLayoutSegments(): string[] {
|
|
37
|
+
const pathname = usePathname();
|
|
38
|
+
return useMemo(() => pathname.split("/").filter(Boolean), [pathname]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The first active segment below the root (`null` on `/`). */
|
|
42
|
+
export function useSelectedLayoutSegment(): string | null {
|
|
43
|
+
return useSelectedLayoutSegments()[0] ?? null;
|
|
44
|
+
}
|
package/src/router.tsx
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Text.js App Router: nested layouts, dynamic segments, `loading.tsx`
|
|
3
|
+
* Suspense boundaries, `error.tsx` error boundaries and `not-found.tsx`,
|
|
4
|
+
* synced with `window.location` (or an in-memory history).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Component,
|
|
9
|
+
Fragment,
|
|
10
|
+
lazy,
|
|
11
|
+
Suspense,
|
|
12
|
+
useCallback,
|
|
13
|
+
useEffect,
|
|
14
|
+
useMemo,
|
|
15
|
+
useRef,
|
|
16
|
+
useState,
|
|
17
|
+
useSyncExternalStore,
|
|
18
|
+
type ComponentType,
|
|
19
|
+
type ErrorInfo,
|
|
20
|
+
type LazyExoticComponent,
|
|
21
|
+
type ReactNode,
|
|
22
|
+
} from "react";
|
|
23
|
+
import { Box, resolveTheme, Text, themes, ThemeProvider, useApp, useTheme, type TextTheme, type ThemeName } from "@textjs/react";
|
|
24
|
+
import { txError } from "@textjs/core";
|
|
25
|
+
import { AppShellContext, RouterContext, type AppRouterInstance, type AppShellState, type RouterState } from "./context";
|
|
26
|
+
import { devErrors, type DevErrorStore } from "./dev-errors";
|
|
27
|
+
import { TextDevOverlay } from "./devtools";
|
|
28
|
+
import { createBrowserHistory, createMemoryHistory, isExternalHref, parseHref, type RouterHistory } from "./history";
|
|
29
|
+
import { getRedirectTarget, isNavigationError, isNotFoundError, isRedirectError } from "./navigation-errors";
|
|
30
|
+
import {
|
|
31
|
+
isLazyModule,
|
|
32
|
+
loadModule,
|
|
33
|
+
matchAppRoutes,
|
|
34
|
+
parseSegment,
|
|
35
|
+
resolveMetadata,
|
|
36
|
+
resolveRoute,
|
|
37
|
+
searchParamsToRecord,
|
|
38
|
+
type RouteMatch,
|
|
39
|
+
type RouteModule,
|
|
40
|
+
type RouteModuleRef,
|
|
41
|
+
type RouteParams,
|
|
42
|
+
type RouteSegmentNode,
|
|
43
|
+
type SearchParamsRecord,
|
|
44
|
+
} from "./routing";
|
|
45
|
+
|
|
46
|
+
export * from "./routing";
|
|
47
|
+
export * from "./history";
|
|
48
|
+
export { useAppTheme, type AppRouterInstance, type AppShellState, type RouterState, type NavigateOptions } from "./context";
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Module components
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
const lazyComponents = new WeakMap<() => Promise<RouteModule>, LazyExoticComponent<ComponentType<any>>>();
|
|
55
|
+
|
|
56
|
+
function missingDefault(kind: string, file: string | undefined): ComponentType<any> {
|
|
57
|
+
const Missing = (): ReactNode => {
|
|
58
|
+
throw txError("TX015", file ?? kind);
|
|
59
|
+
};
|
|
60
|
+
return Missing;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function moduleComponent(ref: RouteModuleRef, kind: string, file: string | undefined, onLoad?: () => void): ComponentType<any> {
|
|
64
|
+
if (!isLazyModule(ref)) return ref.default ?? missingDefault(kind, file);
|
|
65
|
+
let comp = lazyComponents.get(ref);
|
|
66
|
+
if (!comp) {
|
|
67
|
+
comp = lazy(() =>
|
|
68
|
+
loadModule(ref).then((m) => {
|
|
69
|
+
onLoad?.();
|
|
70
|
+
return { default: m.default ?? missingDefault(kind, file) };
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
lazyComponents.set(ref, comp);
|
|
74
|
+
}
|
|
75
|
+
return comp;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Default UI
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/** Next.js-style default 404. */
|
|
83
|
+
export function DefaultNotFound(): ReactNode {
|
|
84
|
+
const theme = useTheme();
|
|
85
|
+
return (
|
|
86
|
+
<Box flexGrow={1} alignItems="center" justifyContent="center" flexDirection="row">
|
|
87
|
+
<Text color={theme.fg} bold>
|
|
88
|
+
404
|
|
89
|
+
</Text>
|
|
90
|
+
<Text color={theme.border}>{" │ "}</Text>
|
|
91
|
+
<Text color={theme.muted}>This page could not be found.</Text>
|
|
92
|
+
</Box>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Production fallback for uncaught render errors. */
|
|
97
|
+
export function DefaultAppError(): ReactNode {
|
|
98
|
+
const theme = useTheme();
|
|
99
|
+
return (
|
|
100
|
+
<Box flexGrow={1} alignItems="center" justifyContent="center" flexDirection="column" paddingX={2}>
|
|
101
|
+
<Text color={theme.fg} bold align="center">
|
|
102
|
+
Application error: a client-side exception has occurred
|
|
103
|
+
</Text>
|
|
104
|
+
<Text color={theme.muted} align="center">
|
|
105
|
+
(see the browser console for more information)
|
|
106
|
+
</Text>
|
|
107
|
+
</Box>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Boundaries
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
export interface ErrorComponentProps {
|
|
116
|
+
error: Error & { digest?: string };
|
|
117
|
+
/** Re-render the segment. */
|
|
118
|
+
reset: () => void;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface BoundaryProps {
|
|
122
|
+
resetKey: string;
|
|
123
|
+
children?: ReactNode;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface ErrorBoundaryProps extends BoundaryProps {
|
|
127
|
+
fallback: ComponentType<ErrorComponentProps>;
|
|
128
|
+
onError?: (error: unknown, info: ErrorInfo) => void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface ErrorBoundaryState {
|
|
132
|
+
error: unknown;
|
|
133
|
+
resetKey: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** `error.tsx` boundary. `notFound()` / `redirect()` pass through to their own boundaries. */
|
|
137
|
+
export class RouteErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
138
|
+
override state: ErrorBoundaryState = { error: null, resetKey: this.props.resetKey };
|
|
139
|
+
|
|
140
|
+
static getDerivedStateFromError(error: unknown): Partial<ErrorBoundaryState> {
|
|
141
|
+
return { error };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static getDerivedStateFromProps(props: ErrorBoundaryProps, state: ErrorBoundaryState): Partial<ErrorBoundaryState> | null {
|
|
145
|
+
if (props.resetKey !== state.resetKey) return { error: null, resetKey: props.resetKey };
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
override componentDidCatch(error: unknown, info: ErrorInfo): void {
|
|
150
|
+
if (!isNavigationError(error)) this.props.onError?.(error, info);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
reset = (): void => {
|
|
154
|
+
this.setState({ error: null });
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
override render(): ReactNode {
|
|
158
|
+
const { error } = this.state;
|
|
159
|
+
if (error !== null) {
|
|
160
|
+
if (isNavigationError(error)) throw error;
|
|
161
|
+
const Fallback = this.props.fallback;
|
|
162
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
163
|
+
return <Fallback error={err} reset={this.reset} />;
|
|
164
|
+
}
|
|
165
|
+
return this.props.children;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface NotFoundBoundaryProps extends BoundaryProps {
|
|
170
|
+
fallback: ReactNode;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface CaughtState {
|
|
174
|
+
error: unknown;
|
|
175
|
+
resetKey: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Renders the segment's `not-found.tsx` when `notFound()` is thrown below it. */
|
|
179
|
+
class NotFoundBoundary extends Component<NotFoundBoundaryProps, CaughtState> {
|
|
180
|
+
override state: CaughtState = { error: null, resetKey: this.props.resetKey };
|
|
181
|
+
|
|
182
|
+
static getDerivedStateFromError(error: unknown): Partial<CaughtState> {
|
|
183
|
+
return { error };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static getDerivedStateFromProps(props: NotFoundBoundaryProps, state: CaughtState): Partial<CaughtState> | null {
|
|
187
|
+
if (props.resetKey !== state.resetKey) return { error: null, resetKey: props.resetKey };
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
override render(): ReactNode {
|
|
192
|
+
const { error } = this.state;
|
|
193
|
+
if (error === null) return this.props.children;
|
|
194
|
+
if (isNotFoundError(error)) return this.props.fallback;
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface RedirectBoundaryProps extends BoundaryProps {
|
|
200
|
+
router: AppRouterInstance;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Performs `redirect()` navigations thrown while rendering. */
|
|
204
|
+
class RedirectBoundary extends Component<RedirectBoundaryProps, CaughtState> {
|
|
205
|
+
override state: CaughtState = { error: null, resetKey: this.props.resetKey };
|
|
206
|
+
|
|
207
|
+
static getDerivedStateFromError(error: unknown): Partial<CaughtState> {
|
|
208
|
+
return { error };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
static getDerivedStateFromProps(props: RedirectBoundaryProps, state: CaughtState): Partial<CaughtState> | null {
|
|
212
|
+
if (props.resetKey !== state.resetKey) return { error: null, resetKey: props.resetKey };
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
override componentDidMount(): void {
|
|
217
|
+
this.componentDidUpdate();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
override componentDidUpdate(): void {
|
|
221
|
+
const { error } = this.state;
|
|
222
|
+
if (!isRedirectError(error)) return;
|
|
223
|
+
const t = getRedirectTarget(error);
|
|
224
|
+
queueMicrotask(() => {
|
|
225
|
+
if (t.type === "push") this.props.router.push(t.href);
|
|
226
|
+
else this.props.router.replace(t.href);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
override render(): ReactNode {
|
|
231
|
+
const { error } = this.state;
|
|
232
|
+
if (error === null) return this.props.children;
|
|
233
|
+
if (isRedirectError(error)) return null;
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface RootErrorBoundaryProps extends BoundaryProps {
|
|
239
|
+
store: DevErrorStore | null;
|
|
240
|
+
fallback: ReactNode;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Catches errors no `error.tsx` handled; reports them to the dev Redbox. */
|
|
244
|
+
class RootErrorBoundary extends Component<RootErrorBoundaryProps, { error: unknown; resetKey: string }> {
|
|
245
|
+
override state = { error: null as unknown, resetKey: this.props.resetKey };
|
|
246
|
+
|
|
247
|
+
static getDerivedStateFromError(error: unknown): { error: unknown } {
|
|
248
|
+
return { error };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
static getDerivedStateFromProps(props: RootErrorBoundaryProps, state: { resetKey: string }): { error: null; resetKey: string } | null {
|
|
252
|
+
if (props.resetKey !== state.resetKey) return { error: null, resetKey: props.resetKey };
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
override componentDidCatch(error: unknown, info: ErrorInfo): void {
|
|
257
|
+
this.props.store?.reportRuntime(error, info.componentStack);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
override render(): ReactNode {
|
|
261
|
+
if (this.state.error !== null) return this.props.fallback;
|
|
262
|
+
return this.props.children;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Route tree
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
export interface PageProps<P extends RouteParams = RouteParams> {
|
|
271
|
+
params: P;
|
|
272
|
+
searchParams: SearchParamsRecord;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface LayoutProps<P extends RouteParams = RouteParams> {
|
|
276
|
+
params: P;
|
|
277
|
+
children?: ReactNode;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function segmentKey(node: RouteSegmentNode, params: RouteParams): string {
|
|
281
|
+
const seg = parseSegment(node.segment);
|
|
282
|
+
if (seg.kind === "static" || seg.kind === "group") return node.segment;
|
|
283
|
+
const v = params[seg.name];
|
|
284
|
+
return `${node.segment}=${Array.isArray(v) ? v.join("/") : (v ?? "")}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
interface RouteTreeProps {
|
|
288
|
+
match: RouteMatch;
|
|
289
|
+
searchParams: SearchParamsRecord;
|
|
290
|
+
resetKey: string;
|
|
291
|
+
onModuleLoad: () => void;
|
|
292
|
+
onRouteError?: (error: unknown, info: ErrorInfo) => void;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function RouteTree({ match, searchParams, resetKey, onModuleLoad, onRouteError }: RouteTreeProps): ReactNode {
|
|
296
|
+
const { chain, params } = match;
|
|
297
|
+
const leaf = chain[chain.length - 1] as RouteSegmentNode;
|
|
298
|
+
|
|
299
|
+
let element: ReactNode;
|
|
300
|
+
if (match.notFound) {
|
|
301
|
+
const NF = leaf.notFound ? moduleComponent(leaf.notFound, "not-found", leaf.files?.["not-found"], onModuleLoad) : DefaultNotFound;
|
|
302
|
+
element = <NF />;
|
|
303
|
+
} else {
|
|
304
|
+
const Page = moduleComponent(leaf.page as RouteModuleRef, "page", leaf.files?.page, onModuleLoad);
|
|
305
|
+
element = <Page params={params} searchParams={searchParams} />;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
309
|
+
const node = chain[i] as RouteSegmentNode;
|
|
310
|
+
const key = chain
|
|
311
|
+
.slice(0, i + 1)
|
|
312
|
+
.map((n) => segmentKey(n, params))
|
|
313
|
+
.join("/");
|
|
314
|
+
if (node.notFound || i === 0) {
|
|
315
|
+
const NF = node.notFound ? moduleComponent(node.notFound, "not-found", node.files?.["not-found"], onModuleLoad) : DefaultNotFound;
|
|
316
|
+
element = (
|
|
317
|
+
<NotFoundBoundary resetKey={resetKey} fallback={<NF />}>
|
|
318
|
+
{element}
|
|
319
|
+
</NotFoundBoundary>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (node.loading) {
|
|
323
|
+
const Loading = moduleComponent(node.loading, "loading", node.files?.loading, onModuleLoad);
|
|
324
|
+
element = <Suspense fallback={<Loading />}>{element}</Suspense>;
|
|
325
|
+
}
|
|
326
|
+
if (node.error) {
|
|
327
|
+
const ErrorComp = moduleComponent(node.error, "error", node.files?.error, onModuleLoad) as ComponentType<ErrorComponentProps>;
|
|
328
|
+
element = (
|
|
329
|
+
<RouteErrorBoundary resetKey={resetKey} fallback={ErrorComp} onError={onRouteError}>
|
|
330
|
+
{element}
|
|
331
|
+
</RouteErrorBoundary>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (node.layout) {
|
|
335
|
+
const Layout = moduleComponent(node.layout, "layout", node.files?.layout, onModuleLoad);
|
|
336
|
+
element = <Layout params={params}>{element}</Layout>;
|
|
337
|
+
}
|
|
338
|
+
element = <Fragment key={key}>{element}</Fragment>;
|
|
339
|
+
}
|
|
340
|
+
return element;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
// <AppRouter>
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
export interface AppRouterProps {
|
|
348
|
+
/** The `app/` route manifest (generated by `text dev` / `text build`). */
|
|
349
|
+
routes: RouteSegmentNode;
|
|
350
|
+
/** Browser history by default; pass `createMemoryHistory("/")` for tests and embedded previews. */
|
|
351
|
+
history?: RouterHistory;
|
|
352
|
+
/** Render `<TextDevOverlay />` (or a custom overlay node). */
|
|
353
|
+
devOverlay?: ReactNode | boolean;
|
|
354
|
+
/** Initial theme preset. */
|
|
355
|
+
theme?: TextTheme | ThemeName;
|
|
356
|
+
/** Write route `metadata.title` to `document.title` (default `true`). */
|
|
357
|
+
documentTitle?: boolean;
|
|
358
|
+
/** Fallback `document.title` when no route defines one. */
|
|
359
|
+
defaultTitle?: string;
|
|
360
|
+
/** Report errors to the dev Redbox store (default: the global store; `null` disables). */
|
|
361
|
+
errorStore?: DevErrorStore | null;
|
|
362
|
+
/** Called after every navigation. */
|
|
363
|
+
onNavigate?: (match: RouteMatch) => void;
|
|
364
|
+
/** Extra overlays rendered above the routes. */
|
|
365
|
+
children?: ReactNode;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function defaultHistory(): RouterHistory {
|
|
369
|
+
return typeof window !== "undefined" && typeof window.history !== "undefined" ? createBrowserHistory() : createMemoryHistory("/");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function themeKey(theme: TextTheme): string {
|
|
373
|
+
for (const [name, t] of Object.entries(themes)) if (t === theme) return name;
|
|
374
|
+
return theme.id;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function AppRouter({
|
|
378
|
+
routes,
|
|
379
|
+
history: historyProp,
|
|
380
|
+
devOverlay,
|
|
381
|
+
theme: themeProp,
|
|
382
|
+
documentTitle = true,
|
|
383
|
+
defaultTitle,
|
|
384
|
+
errorStore = devErrors,
|
|
385
|
+
onNavigate,
|
|
386
|
+
children,
|
|
387
|
+
}: AppRouterProps): ReactNode {
|
|
388
|
+
const [ownHistory] = useState(() => historyProp ?? defaultHistory());
|
|
389
|
+
const history = historyProp ?? ownHistory;
|
|
390
|
+
const location = useSyncExternalStore(history.listen, history.getLocation, history.getLocation);
|
|
391
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
392
|
+
const [, setLoadTick] = useState(0);
|
|
393
|
+
const onModuleLoad = useCallback(() => setLoadTick((n) => n + 1), []);
|
|
394
|
+
|
|
395
|
+
const [theme, setThemeState] = useState<TextTheme>(() => resolveTheme(themeProp));
|
|
396
|
+
const [wireframe, setWireframeState] = useState(false);
|
|
397
|
+
const app = useApp();
|
|
398
|
+
|
|
399
|
+
const store = errorStore;
|
|
400
|
+
const errors = useSyncExternalStore(store ? store.subscribe : noopSubscribe, store ? store.getSnapshot : nullSnapshot, store ? store.getSnapshot : nullSnapshot);
|
|
401
|
+
const generation = errors ? errors.generation : 0;
|
|
402
|
+
|
|
403
|
+
const match = useMemo(() => resolveRoute(routes, location.pathname), [routes, location.pathname]);
|
|
404
|
+
const searchParams = useMemo(() => searchParamsToRecord(location.search), [location.search]);
|
|
405
|
+
const metadata = resolveMetadata(match, searchParams);
|
|
406
|
+
|
|
407
|
+
const latest = useRef(location);
|
|
408
|
+
latest.current = location;
|
|
409
|
+
|
|
410
|
+
const router = useMemo<AppRouterInstance>(() => {
|
|
411
|
+
const navigate = (href: string, kind: "push" | "replace"): void => {
|
|
412
|
+
if (isExternalHref(href, typeof window !== "undefined" ? window.location.origin : undefined)) {
|
|
413
|
+
if (typeof window !== "undefined") window.location.assign(href);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const next = parseHref(href, latest.current.pathname);
|
|
417
|
+
if (kind === "push" && next.href === latest.current.href) return;
|
|
418
|
+
if (kind === "push") history.push(next.href);
|
|
419
|
+
else history.replace(next.href);
|
|
420
|
+
};
|
|
421
|
+
return {
|
|
422
|
+
push: (href) => navigate(href, "push"),
|
|
423
|
+
replace: (href) => navigate(href, "replace"),
|
|
424
|
+
back: () => history.back(),
|
|
425
|
+
forward: () => history.forward(),
|
|
426
|
+
refresh: () => setRefreshKey((k) => k + 1),
|
|
427
|
+
prefetch: (href) => {
|
|
428
|
+
const m = matchAppRoutes(routes, parseHref(href, latest.current.pathname).pathname);
|
|
429
|
+
if (!m) return;
|
|
430
|
+
for (const n of m.chain) for (const ref of [n.layout, n.page, n.loading]) if (ref && isLazyModule(ref)) void loadModule(ref).catch(() => undefined);
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}, [history, routes]);
|
|
434
|
+
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
onNavigate?.(match);
|
|
437
|
+
}, [match, onNavigate]);
|
|
438
|
+
|
|
439
|
+
const title = metadata.title ?? defaultTitle;
|
|
440
|
+
useEffect(() => {
|
|
441
|
+
if (!documentTitle || title === undefined || typeof document === "undefined") return;
|
|
442
|
+
document.title = title;
|
|
443
|
+
}, [documentTitle, title]);
|
|
444
|
+
|
|
445
|
+
const setTheme = useCallback((t: TextTheme | ThemeName) => setThemeState(resolveTheme(t)), []);
|
|
446
|
+
const cycleTheme = useCallback(() => {
|
|
447
|
+
setThemeState((cur) => {
|
|
448
|
+
const names = Object.keys(themes) as ThemeName[];
|
|
449
|
+
const idx = names.indexOf(themeKey(cur) as ThemeName);
|
|
450
|
+
return themes[names[(idx + 1) % names.length] as ThemeName];
|
|
451
|
+
});
|
|
452
|
+
}, []);
|
|
453
|
+
const setWireframe = useCallback(
|
|
454
|
+
(enabled: boolean) => {
|
|
455
|
+
app.renderer.setWireframe(enabled);
|
|
456
|
+
setWireframeState(enabled);
|
|
457
|
+
},
|
|
458
|
+
[app],
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const shell = useMemo<AppShellState>(
|
|
462
|
+
() => ({ theme, themeName: themeKey(theme), setTheme, cycleTheme, wireframe, setWireframe }),
|
|
463
|
+
[theme, setTheme, cycleTheme, wireframe, setWireframe],
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const state = useMemo<RouterState>(
|
|
467
|
+
() => ({ location, match, metadata, router, history, manifest: routes, refreshKey }),
|
|
468
|
+
// metadata is derived from match / search / module loads
|
|
469
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
470
|
+
[location, match, metadata.title, metadata.description, router, history, routes, refreshKey],
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const resetKey = `${location.key}:${refreshKey}:${generation}`;
|
|
474
|
+
|
|
475
|
+
return (
|
|
476
|
+
<AppShellContext value={shell}>
|
|
477
|
+
<ThemeProvider theme={theme}>
|
|
478
|
+
<RouterContext value={state}>
|
|
479
|
+
<Box width="100%" height="100%" flexDirection="column" bg={theme.bg} fg={theme.fg} wireframeLabel="app-router">
|
|
480
|
+
<RootErrorBoundary resetKey={resetKey} store={store} fallback={<DefaultAppError />}>
|
|
481
|
+
<RedirectBoundary resetKey={resetKey} router={router}>
|
|
482
|
+
<RouteTree match={match} searchParams={searchParams} resetKey={resetKey} onModuleLoad={onModuleLoad} />
|
|
483
|
+
</RedirectBoundary>
|
|
484
|
+
</RootErrorBoundary>
|
|
485
|
+
{children}
|
|
486
|
+
{devOverlay === true ? <TextDevOverlay /> : devOverlay}
|
|
487
|
+
</Box>
|
|
488
|
+
</RouterContext>
|
|
489
|
+
</ThemeProvider>
|
|
490
|
+
</AppShellContext>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const noopSubscribe = (): (() => void) => () => undefined;
|
|
495
|
+
const nullSnapshot = (): null => null;
|
|
496
|
+
|
|
497
|
+
export { useRouteInfo } from "./devtools";
|