akanjs 2.3.6-rc.2 → 2.3.6-rc.3
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/client/capacitor.ts +2 -0
- package/client/csrTypes.ts +132 -9
- package/client/device.ts +13 -4
- package/client/frameConfig.ts +239 -0
- package/client/frameDebug.ts +28 -0
- package/client/index.ts +2 -0
- package/client/router.ts +9 -0
- package/fetch/requestStorage.ts +11 -0
- package/package.json +1 -1
- package/server/hmr/clientScript.ts +28 -0
- package/server/hmr/wsHub.ts +29 -0
- package/server/routeElementComposer.tsx +56 -0
- package/server/routeTreeBuilder.ts +8 -0
- package/server/routing/apiRouter.ts +4 -1
- package/server/rscClient.tsx +8 -0
- package/server/rscWorker.tsx +30 -5
- package/store/baseSt.ts +2 -2
- package/types/client/capacitor.d.ts +4 -0
- package/types/client/csrTypes.d.ts +125 -7
- package/types/client/frameConfig.d.ts +19 -0
- package/types/client/frameDebug.d.ts +3 -0
- package/types/client/index.d.ts +2 -0
- package/types/client/router.d.ts +2 -0
- package/types/fetch/requestStorage.d.ts +3 -0
- package/types/server/hmr/clientScript.d.ts +1 -1
- package/types/server/hmr/wsHub.d.ts +6 -0
- package/types/server/routeElementComposer.d.ts +8 -0
- package/types/server/rscClient.d.ts +3 -0
- package/types/store/baseSt.d.ts +2 -2
- package/types/ui/Layout/BottomInset.d.ts +7 -1
- package/types/ui/Layout/Navbar.d.ts +1 -1
- package/types/ui/Layout/TopInset.d.ts +7 -0
- package/types/ui/Layout/index.d.ts +3 -2
- package/types/ui/Link/types.d.ts +9 -1
- package/types/ui/Portal.d.ts +2 -1
- package/types/ui/ServerPortal.d.ts +15 -0
- package/types/ui/System/Client.d.ts +7 -5
- package/types/ui/System/SSR.d.ts +3 -2
- package/types/ui/System/frameCssVars.d.ts +5 -0
- package/types/webkit/useCsrValues.d.ts +6 -1
- package/types/webkit/useFrameRuntime.d.ts +111 -0
- package/ui/Layout/BottomInset.tsx +89 -20
- package/ui/Layout/BottomTab.tsx +11 -6
- package/ui/Layout/Navbar.tsx +38 -12
- package/ui/Layout/TopInset.tsx +45 -0
- package/ui/Layout/TopLeftAction.tsx +2 -2
- package/ui/Layout/index.ts +2 -0
- package/ui/Link/types.ts +9 -1
- package/ui/Portal.tsx +16 -2
- package/ui/ServerPortal.tsx +49 -0
- package/ui/System/CSR.tsx +236 -90
- package/ui/System/Client.tsx +90 -10
- package/ui/System/SSR.tsx +119 -43
- package/ui/System/frameCssVars.ts +25 -0
- package/ui/styles.css +56 -0
- package/webkit/bootCsr.tsx +44 -35
- package/webkit/useCsrValues.ts +636 -105
- package/webkit/useFrameRuntime.ts +539 -0
- package/types/ui/Layout/BottomAction.d.ts +0 -6
- package/ui/Layout/BottomAction.tsx +0 -15
package/server/hmr/wsHub.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type HmrMessage =
|
|
|
22
22
|
routeIds?: string[];
|
|
23
23
|
}
|
|
24
24
|
| { type: "css-update"; cssAssets?: Record<string, { cssUrl: string; cssRelPath: string }> }
|
|
25
|
+
| { type: "sync-navigation"; clientId: string; href: string; kind?: "push" | "replace" | "back" | "pop" }
|
|
25
26
|
| { type: "ok"; generation?: number }
|
|
26
27
|
| {
|
|
27
28
|
type: "build-status";
|
|
@@ -63,4 +64,32 @@ export class HmrWsHub {
|
|
|
63
64
|
const payload = JSON.stringify(msg);
|
|
64
65
|
this.#publish?.(HMR_WS_TOPIC, payload);
|
|
65
66
|
}
|
|
67
|
+
|
|
68
|
+
handleMessage(message: string): void {
|
|
69
|
+
if (!isSyncNavigationEnabled()) return;
|
|
70
|
+
let parsed: unknown;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(message);
|
|
73
|
+
} catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!isSyncNavigationMessage(parsed)) return;
|
|
77
|
+
this.broadcast(parsed);
|
|
78
|
+
}
|
|
66
79
|
}
|
|
80
|
+
|
|
81
|
+
const isSyncNavigationEnabled = () =>
|
|
82
|
+
process.env.AKAN_PUBLIC_SYNC_NAVIGATION === "true" ||
|
|
83
|
+
process.env.AKAN_PUBLIC_SYNC_NAVIGATION === "1" ||
|
|
84
|
+
process.env.SYNC_DOMAIN === "true" ||
|
|
85
|
+
process.env.SYNC_DOMAIN === "1";
|
|
86
|
+
|
|
87
|
+
const isSyncNavigationMessage = (value: unknown): value is Extract<HmrMessage, { type: "sync-navigation" }> => {
|
|
88
|
+
if (!value || typeof value !== "object") return false;
|
|
89
|
+
const msg = value as Partial<Extract<HmrMessage, { type: "sync-navigation" }>>;
|
|
90
|
+
if (msg.type !== "sync-navigation") return false;
|
|
91
|
+
if (typeof msg.clientId !== "string" || msg.clientId.length === 0) return false;
|
|
92
|
+
if (typeof msg.href !== "string" || msg.href.length === 0) return false;
|
|
93
|
+
if (msg.kind === undefined) return true;
|
|
94
|
+
return msg.kind === "push" || msg.kind === "replace" || msg.kind === "back" || msg.kind === "pop";
|
|
95
|
+
};
|
|
@@ -2,11 +2,13 @@ import type {
|
|
|
2
2
|
Head,
|
|
3
3
|
LayoutErrorRender,
|
|
4
4
|
LayoutFallbackRoute,
|
|
5
|
+
PageConfig,
|
|
5
6
|
LayoutNotFoundRender,
|
|
6
7
|
PathRoute,
|
|
7
8
|
ResolvedHead,
|
|
8
9
|
RouteRender,
|
|
9
10
|
} from "akanjs/client";
|
|
11
|
+
import { getExplicitPageConfigKeys, resolvePageState } from "../client/frameConfig";
|
|
10
12
|
import { Children, cloneElement, isValidElement, type ReactElement, type ReactNode, Suspense } from "react";
|
|
11
13
|
import { resolveHeadResult } from "./metadata";
|
|
12
14
|
import { type AkanRouteSegmentState, createAkanRouteSegments, createAkanSegmentOutletKey } from "./routeState";
|
|
@@ -14,6 +16,47 @@ import { isAkanRscPartialCommitEnabled } from "./rscPartialCommit";
|
|
|
14
16
|
import { AkanSegmentOutletReference } from "./rscSegmentOutletReference";
|
|
15
17
|
|
|
16
18
|
export class RouteElementComposer {
|
|
19
|
+
static async resolveSsrFramePathRoute({
|
|
20
|
+
pathRoute,
|
|
21
|
+
basePath,
|
|
22
|
+
}: {
|
|
23
|
+
pathRoute: PathRoute;
|
|
24
|
+
basePath?: string | null;
|
|
25
|
+
}): Promise<PathRoute> {
|
|
26
|
+
const pageConfigChain = await RouteElementComposer.#resolvePageConfigChain(pathRoute);
|
|
27
|
+
return {
|
|
28
|
+
...pathRoute,
|
|
29
|
+
pageState: resolvePageState({
|
|
30
|
+
configChain: pageConfigChain,
|
|
31
|
+
path: pathRoute.path,
|
|
32
|
+
basePath: basePath ?? undefined,
|
|
33
|
+
platform: "web",
|
|
34
|
+
deviceSafeArea: { top: 0, bottom: 0 },
|
|
35
|
+
cssSafeArea: { top: 0, bottom: 0 },
|
|
36
|
+
}),
|
|
37
|
+
pageConfigChain,
|
|
38
|
+
explicitPageConfigKeys: getExplicitPageConfigKeys(pageConfigChain),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static async resolveSsrFallbackFrameState({
|
|
43
|
+
route,
|
|
44
|
+
basePath,
|
|
45
|
+
}: {
|
|
46
|
+
route: PathRoute | LayoutFallbackRoute;
|
|
47
|
+
basePath?: string | null;
|
|
48
|
+
}) {
|
|
49
|
+
const pageConfigChain = await RouteElementComposer.#resolveLayoutPageConfigChain(route);
|
|
50
|
+
return resolvePageState({
|
|
51
|
+
configChain: pageConfigChain,
|
|
52
|
+
path: route.path,
|
|
53
|
+
basePath: basePath ?? undefined,
|
|
54
|
+
platform: "web",
|
|
55
|
+
deviceSafeArea: { top: 0, bottom: 0 },
|
|
56
|
+
cssSafeArea: { top: 0, bottom: 0 },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
17
60
|
static compose({
|
|
18
61
|
pathRoute,
|
|
19
62
|
params,
|
|
@@ -231,6 +274,19 @@ export class RouteElementComposer {
|
|
|
231
274
|
return [...pathRoute.renderRootLayouts, ...pathRoute.renderLayouts, pathRoute.renderPage];
|
|
232
275
|
}
|
|
233
276
|
|
|
277
|
+
static async #resolveLayoutPageConfigChain(route: PathRoute | LayoutFallbackRoute): Promise<PageConfig[]> {
|
|
278
|
+
const configs = await Promise.all(
|
|
279
|
+
[...route.renderRootLayouts, ...route.renderLayouts].map((render) => render.getLayoutPageConfig?.()),
|
|
280
|
+
);
|
|
281
|
+
return configs.filter((config): config is PageConfig => Boolean(config));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
static async #resolvePageConfigChain(pathRoute: PathRoute): Promise<PageConfig[]> {
|
|
285
|
+
const layoutConfigs = await RouteElementComposer.#resolveLayoutPageConfigChain(pathRoute);
|
|
286
|
+
const pageConfig = await pathRoute.renderPage.getPageConfig?.();
|
|
287
|
+
return [...layoutConfigs, ...(pageConfig ? [pageConfig] : [])];
|
|
288
|
+
}
|
|
289
|
+
|
|
234
290
|
static #composeLoadingFallback(renders: RouteRender[], params: Record<string, string>): ReactNode {
|
|
235
291
|
let element: ReactNode = null;
|
|
236
292
|
for (let i = renders.length - 1; i >= 0; i--) {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
parseRouteModuleKey,
|
|
19
19
|
routeSegmentToTreePath,
|
|
20
20
|
} from "akanjs/common";
|
|
21
|
+
import { validatePageConfig } from "../client/frameConfig";
|
|
21
22
|
import { resolveHeadExport, resolveMetadataHead } from "./metadata";
|
|
22
23
|
|
|
23
24
|
export type PagesContext = Record<string, () => Promise<RouteModule>>;
|
|
@@ -55,6 +56,7 @@ export class RouteTreeBuilder {
|
|
|
55
56
|
]);
|
|
56
57
|
static readonly #rootLayoutExports = new Set([
|
|
57
58
|
"default",
|
|
59
|
+
"pageConfig",
|
|
58
60
|
"head",
|
|
59
61
|
"metadata",
|
|
60
62
|
"generateHead",
|
|
@@ -72,6 +74,7 @@ export class RouteTreeBuilder {
|
|
|
72
74
|
]);
|
|
73
75
|
static readonly #layoutRouteExports = new Set([
|
|
74
76
|
"default",
|
|
77
|
+
"pageConfig",
|
|
75
78
|
"head",
|
|
76
79
|
"metadata",
|
|
77
80
|
"generateHead",
|
|
@@ -271,6 +274,7 @@ export class RouteTreeBuilder {
|
|
|
271
274
|
RouteTreeBuilder.#moduleCacheStats.cacheMisses += 1;
|
|
272
275
|
const mod = await loader();
|
|
273
276
|
RouteTreeBuilder.#validateRouteModuleExports(key, kind, mod);
|
|
277
|
+
validatePageConfig(key, "pageConfig" in mod ? mod.pageConfig : undefined);
|
|
274
278
|
if (!loaded) {
|
|
275
279
|
RouteTreeBuilder.#moduleCacheStats.loadedModuleCount += 1;
|
|
276
280
|
RouteTreeBuilder.#moduleCacheStats.loadedModuleKeys.push(key);
|
|
@@ -354,6 +358,10 @@ export class RouteTreeBuilder {
|
|
|
354
358
|
return "pageConfig" in mod ? mod.pageConfig : undefined;
|
|
355
359
|
};
|
|
356
360
|
} else {
|
|
361
|
+
routeRender.getLayoutPageConfig = async () => {
|
|
362
|
+
const mod = await loadModule();
|
|
363
|
+
return "pageConfig" in mod ? mod.pageConfig : undefined;
|
|
364
|
+
};
|
|
357
365
|
routeRender.resolveNotFound = async () => {
|
|
358
366
|
const mod = (await loadModule()) as LayoutModule;
|
|
359
367
|
routeRender.NotFound = mod.NotFound;
|
|
@@ -130,7 +130,10 @@ export class ApiRouter {
|
|
|
130
130
|
},
|
|
131
131
|
message: async (ws, message) => {
|
|
132
132
|
const data = ws.data as WsTaggedData | undefined;
|
|
133
|
-
if (data?.kind === "akan-hmr")
|
|
133
|
+
if (data?.kind === "akan-hmr") {
|
|
134
|
+
if (typeof message === "string" && typeof hmrHub?.handleMessage === "function") hmrHub.handleMessage(message);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
134
137
|
try {
|
|
135
138
|
if (typeof message === "string") {
|
|
136
139
|
const msg = JSON.parse(message) as WebsocketReqData;
|
package/server/rscClient.tsx
CHANGED
|
@@ -47,6 +47,9 @@ declare global {
|
|
|
47
47
|
| undefined;
|
|
48
48
|
var __AKAN_RSC_REFRESH__: ((options?: { buildId?: number }) => Promise<void>) | undefined;
|
|
49
49
|
var __AKAN_RSC_CLEAR_CACHE__: (() => void) | undefined;
|
|
50
|
+
var __AKAN_DEV_SYNC_NAVIGATION__: ((href: string, kind: "push" | "replace" | "back" | "pop") => void) | undefined;
|
|
51
|
+
var __AKAN_DEV_SYNC_NAVIGATION_APPLYING__: boolean | undefined;
|
|
52
|
+
var __AKAN_GET_SYNC_ROUTE_HREF__: ((href: string) => string) | undefined;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
function decodeBase64(b64: string): Uint8Array {
|
|
@@ -514,6 +517,11 @@ function Root(): ReactNode {
|
|
|
514
517
|
|
|
515
518
|
window.addEventListener("popstate", () => {
|
|
516
519
|
void globalThis.__AKAN_RSC_NAVIGATE__?.(window.location.href, { replace: true, scrollToTop: false });
|
|
520
|
+
window.setTimeout(() => {
|
|
521
|
+
if (globalThis.__AKAN_DEV_SYNC_NAVIGATION_APPLYING__) return;
|
|
522
|
+
const href = globalThis.__AKAN_GET_SYNC_ROUTE_HREF__?.(window.location.href) ?? window.location.href;
|
|
523
|
+
globalThis.__AKAN_DEV_SYNC_NAVIGATION__?.(href, "pop");
|
|
524
|
+
}, 0);
|
|
517
525
|
});
|
|
518
526
|
|
|
519
527
|
const hydrate = () => hydrateRoot(document, createElement(Root));
|
package/server/rscWorker.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getRequestPolicy,
|
|
13
13
|
getRequestTheme,
|
|
14
14
|
requestStorage,
|
|
15
|
+
setRequestFrameState,
|
|
15
16
|
untrackedCookies,
|
|
16
17
|
untrackedRequest,
|
|
17
18
|
updateRequestPolicy,
|
|
@@ -1164,6 +1165,12 @@ export class RscRenderer {
|
|
|
1164
1165
|
error?: unknown;
|
|
1165
1166
|
digest?: string;
|
|
1166
1167
|
}): Promise<ReactNode | null> {
|
|
1168
|
+
setRequestFrameState(
|
|
1169
|
+
await RouteElementComposer.resolveSsrFallbackFrameState({
|
|
1170
|
+
route,
|
|
1171
|
+
basePath: this.#getBasePath(url),
|
|
1172
|
+
}),
|
|
1173
|
+
);
|
|
1167
1174
|
const body = await RouteElementComposer.composeFallback({
|
|
1168
1175
|
kind,
|
|
1169
1176
|
route,
|
|
@@ -1218,17 +1225,22 @@ export class RscRenderer {
|
|
|
1218
1225
|
this.#logger.verbose(
|
|
1219
1226
|
`composing route element pathname=${url.pathname} search=${url.search || "(none)"} params=${JSON.stringify(match.params)}`,
|
|
1220
1227
|
);
|
|
1221
|
-
const
|
|
1228
|
+
const pathRoute = await RouteElementComposer.resolveSsrFramePathRoute({
|
|
1222
1229
|
pathRoute: match.pathRoute,
|
|
1230
|
+
basePath: this.#getBasePath(url),
|
|
1231
|
+
});
|
|
1232
|
+
setRequestFrameState(pathRoute.pageState);
|
|
1233
|
+
const routeHead = await RouteElementComposer.resolveHeadWithMetadata({
|
|
1234
|
+
pathRoute,
|
|
1223
1235
|
params: match.params,
|
|
1224
1236
|
searchParams,
|
|
1225
1237
|
});
|
|
1226
1238
|
const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {
|
|
1227
|
-
isSpecialRoute:
|
|
1239
|
+
isSpecialRoute: pathRoute.isSpecialRoute,
|
|
1228
1240
|
hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
|
|
1229
1241
|
});
|
|
1230
1242
|
const body = RouteElementComposer.compose({
|
|
1231
|
-
pathRoute
|
|
1243
|
+
pathRoute,
|
|
1232
1244
|
params: match.params,
|
|
1233
1245
|
searchParams,
|
|
1234
1246
|
});
|
|
@@ -1245,7 +1257,7 @@ export class RscRenderer {
|
|
|
1245
1257
|
: (routeHead.node ?? this.#renderDefaultHead())}
|
|
1246
1258
|
{!routeHeadSnapshot &&
|
|
1247
1259
|
shouldRenderLocaleAlternates({
|
|
1248
|
-
isSpecialRoute:
|
|
1260
|
+
isSpecialRoute: pathRoute.isSpecialRoute,
|
|
1249
1261
|
hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
|
|
1250
1262
|
})
|
|
1251
1263
|
? this.#renderLocaleAlternates(url)
|
|
@@ -1266,8 +1278,13 @@ export class RscRenderer {
|
|
|
1266
1278
|
this.#logger.verbose(
|
|
1267
1279
|
`composing route suffix pathname=${url.pathname} start=${patchStartIndex} params=${JSON.stringify(match.params)}`,
|
|
1268
1280
|
);
|
|
1269
|
-
|
|
1281
|
+
const pathRoute = await RouteElementComposer.resolveSsrFramePathRoute({
|
|
1270
1282
|
pathRoute: match.pathRoute,
|
|
1283
|
+
basePath: this.#getBasePath(url),
|
|
1284
|
+
});
|
|
1285
|
+
setRequestFrameState(pathRoute.pageState);
|
|
1286
|
+
return RouteElementComposer.composeSuffix({
|
|
1287
|
+
pathRoute,
|
|
1271
1288
|
params: match.params,
|
|
1272
1289
|
searchParams,
|
|
1273
1290
|
patchStartIndex,
|
|
@@ -1315,6 +1332,14 @@ export class RscRenderer {
|
|
|
1315
1332
|
return <title key="title">{process.env.AKAN_PUBLIC_APP_NAME ?? "Akan App"}</title>;
|
|
1316
1333
|
}
|
|
1317
1334
|
|
|
1335
|
+
#getBasePath(url: URL): string | null {
|
|
1336
|
+
return getBasePathFromPathname(url.pathname, {
|
|
1337
|
+
basePaths: this.#basePaths,
|
|
1338
|
+
i18n: this.#i18n,
|
|
1339
|
+
headerBasePath: untrackedRequest()?.headers.get("x-base-path"),
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1318
1343
|
async #resolveRouteHeadSnapshot(
|
|
1319
1344
|
url: URL,
|
|
1320
1345
|
match: { pathRoute: PathRoute; params: Record<string, string> },
|
package/store/baseSt.ts
CHANGED
|
@@ -44,8 +44,8 @@ export class BaseStore extends store("base" as const, () => ({
|
|
|
44
44
|
bottomInset: 0,
|
|
45
45
|
gesture: true,
|
|
46
46
|
cache: false,
|
|
47
|
-
topSafeAreaColor: "
|
|
48
|
-
bottomSafeAreaColor: "
|
|
47
|
+
topSafeAreaColor: "var(--color-base-100, Canvas)",
|
|
48
|
+
bottomSafeAreaColor: "var(--color-base-100, Canvas)",
|
|
49
49
|
} as PageState,
|
|
50
50
|
devMode: false,
|
|
51
51
|
deviceToken: "" as string,
|
|
@@ -12,6 +12,7 @@ export type CapacitorAppModule = {
|
|
|
12
12
|
App: {
|
|
13
13
|
addListener: (eventName: string, listenerFunc: (...args: unknown[]) => void) => Promise<unknown> | unknown;
|
|
14
14
|
removeAllListeners: () => Promise<void> | void;
|
|
15
|
+
exitApp?: () => Promise<void> | void;
|
|
15
16
|
getInfo: () => Promise<{
|
|
16
17
|
id: string;
|
|
17
18
|
version: string;
|
|
@@ -128,6 +129,9 @@ export type CapacitorKeyboardModule = {
|
|
|
128
129
|
Keyboard: {
|
|
129
130
|
show: () => Promise<void> | void;
|
|
130
131
|
hide: () => Promise<void> | void;
|
|
132
|
+
setResizeMode?: (options: {
|
|
133
|
+
mode: "body" | "ionic" | "native" | "none";
|
|
134
|
+
}) => Promise<void> | void;
|
|
131
135
|
addListener: (eventName: string, listenerFunc: (info: CapacitorKeyboardInfo) => void) => Promise<unknown> | unknown;
|
|
132
136
|
removeAllListeners: () => Promise<void> | void;
|
|
133
137
|
};
|
|
@@ -5,15 +5,19 @@ import type { AnimatedComponent, AnimatedProps, Interpolation, SpringValue } fro
|
|
|
5
5
|
import type { RouterInstance } from "./router.d.ts";
|
|
6
6
|
import type { ReactFont } from "./types.d.ts";
|
|
7
7
|
export type TransitionType = "none" | "fade" | "bottomUp" | "stack" | "scaleOut";
|
|
8
|
+
export type PageSafeAreaConfig = boolean | "top" | "bottom" | {
|
|
9
|
+
top?: boolean;
|
|
10
|
+
bottom?: boolean;
|
|
11
|
+
android?: "auto" | "edge-to-edge" | "none";
|
|
12
|
+
};
|
|
8
13
|
/** Per-page CSR configuration for transition, safe-area, and gesture behavior. */
|
|
9
14
|
export interface PageConfig {
|
|
10
15
|
transition?: TransitionType;
|
|
11
|
-
safeArea?:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
bottomInset?: boolean | number;
|
|
16
|
+
safeArea?: PageSafeAreaConfig;
|
|
17
|
+
/** Top chrome reservation in px. Use 0 or omit for no reservation. */
|
|
18
|
+
topInset?: number;
|
|
19
|
+
/** Bottom chrome reservation in px. Use 0 or omit for no reservation. */
|
|
20
|
+
bottomInset?: number;
|
|
17
21
|
gesture?: boolean;
|
|
18
22
|
cache?: boolean;
|
|
19
23
|
/**
|
|
@@ -100,6 +104,7 @@ export interface RouteRender {
|
|
|
100
104
|
resolveError?: () => PromiseOrObject<LayoutErrorRender | undefined>;
|
|
101
105
|
resolveHead?: ResolveHead;
|
|
102
106
|
getPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
|
|
107
|
+
getLayoutPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
|
|
103
108
|
}
|
|
104
109
|
export interface WebAppManifestIcon {
|
|
105
110
|
src: string;
|
|
@@ -161,6 +166,7 @@ export interface PageModule {
|
|
|
161
166
|
}
|
|
162
167
|
export interface LayoutModule {
|
|
163
168
|
default?: LayoutRender;
|
|
169
|
+
pageConfig?: PageConfig;
|
|
164
170
|
head?: Head;
|
|
165
171
|
metadata?: AkanMetadata;
|
|
166
172
|
generateHead?: GenerateHead;
|
|
@@ -179,6 +185,8 @@ export interface LayoutModule {
|
|
|
179
185
|
export type RouteModule = PageModule | LayoutModule;
|
|
180
186
|
export interface Route {
|
|
181
187
|
PageConfig?: PageConfig;
|
|
188
|
+
pageConfig?: PageConfig;
|
|
189
|
+
layoutPageConfig?: PageConfig;
|
|
182
190
|
path: string;
|
|
183
191
|
renderPage?: RouteRender;
|
|
184
192
|
renderLayout?: RouteRender;
|
|
@@ -186,6 +194,8 @@ export interface Route {
|
|
|
186
194
|
isSpecialRoute?: boolean;
|
|
187
195
|
loader?: () => unknown;
|
|
188
196
|
pageState?: PageState;
|
|
197
|
+
pageConfigChain?: PageConfig[];
|
|
198
|
+
explicitPageConfigKeys?: Partial<Record<keyof PageConfig, boolean>>;
|
|
189
199
|
children: Map<string, Route>;
|
|
190
200
|
}
|
|
191
201
|
export type AnimatedDivProps = AnimatedComponent<"div"> extends ForwardRefExoticComponent<AnimatedProps<infer P>> ? P : never;
|
|
@@ -229,9 +239,23 @@ export interface Location {
|
|
|
229
239
|
pathRoute: PathRoute;
|
|
230
240
|
hash: string;
|
|
231
241
|
}
|
|
242
|
+
export type CsrNavigationPhase = "idle" | "preparing" | "transitioning";
|
|
243
|
+
export type CsrNavigationKind = "push" | "replace" | "back" | "popForward" | "popBack";
|
|
244
|
+
export interface NavigationIntent {
|
|
245
|
+
id: number;
|
|
246
|
+
kind: CsrNavigationKind;
|
|
247
|
+
from: Location;
|
|
248
|
+
to: Location;
|
|
249
|
+
scrollTop: number;
|
|
250
|
+
scrollToTop?: boolean;
|
|
251
|
+
createdAt: number;
|
|
252
|
+
}
|
|
232
253
|
export interface LocationState {
|
|
233
254
|
location: Location;
|
|
234
255
|
prevLocation: Location | null;
|
|
256
|
+
pendingLocation?: Location | null;
|
|
257
|
+
navigationIntent?: NavigationIntent | null;
|
|
258
|
+
phase: CsrNavigationPhase;
|
|
235
259
|
}
|
|
236
260
|
export interface History {
|
|
237
261
|
type: "initial" | "forward" | "back";
|
|
@@ -252,6 +276,9 @@ export interface RouteState {
|
|
|
252
276
|
clientHeight: number;
|
|
253
277
|
location: Location;
|
|
254
278
|
prevLocation: Location | null;
|
|
279
|
+
pendingLocation: Location | null;
|
|
280
|
+
navigationIntent: NavigationIntent | null;
|
|
281
|
+
phase: CsrNavigationPhase;
|
|
255
282
|
history: RefObject<History>;
|
|
256
283
|
topSafeAreaRef: RefObject<HTMLDivElement | null>;
|
|
257
284
|
bottomSafeAreaRef: RefObject<HTMLDivElement | null>;
|
|
@@ -263,6 +290,8 @@ export interface RouteState {
|
|
|
263
290
|
}>;
|
|
264
291
|
router: RouterInstance;
|
|
265
292
|
pathRoutes: PathRoute[];
|
|
293
|
+
registerFrameSlot: (path: string, slot: FrameSlotRegistration, bucket?: FrameSlotBucket) => () => void;
|
|
294
|
+
frameLayout: FrameLayoutState;
|
|
266
295
|
}
|
|
267
296
|
export type UseCsrTransition = CsrTransitionStyles & {
|
|
268
297
|
pageBind: (...args: unknown[]) => ReactDOMAttributes;
|
|
@@ -277,11 +306,12 @@ export type CsrContextType = RouteState & UseCsrTransition;
|
|
|
277
306
|
export declare const csrContext: import("react").Context<CsrContextType>;
|
|
278
307
|
export declare const useCsr: () => CsrContextType;
|
|
279
308
|
export interface PathContextType {
|
|
280
|
-
pageType: "current" | "prev" | "cached";
|
|
309
|
+
pageType: "current" | "prev" | "cached" | "pending";
|
|
281
310
|
location: Location;
|
|
282
311
|
prefix?: string;
|
|
283
312
|
gestureEnabled: boolean;
|
|
284
313
|
setGestureEnabled: (enabled: boolean) => void;
|
|
314
|
+
registerFrameSlot: (slot: FrameSlotRegistration) => () => void;
|
|
285
315
|
}
|
|
286
316
|
export declare const pathContext: import("react").Context<PathContextType>;
|
|
287
317
|
export declare const usePathCtx: () => PathContextType;
|
|
@@ -290,11 +320,99 @@ export interface PathRoute {
|
|
|
290
320
|
pathSegments: string[];
|
|
291
321
|
renderPage: RouteRender;
|
|
292
322
|
pageState: PageState;
|
|
323
|
+
pageConfigChain?: PageConfig[];
|
|
324
|
+
explicitPageConfigKeys?: Partial<Record<keyof PageConfig, boolean>>;
|
|
293
325
|
renderRootLayouts: RouteRender[];
|
|
294
326
|
renderLayouts: RouteRender[];
|
|
295
327
|
resolveHead?: ResolveHead;
|
|
296
328
|
isSpecialRoute?: boolean;
|
|
297
329
|
}
|
|
330
|
+
export type FrameSlotScope = "page" | "layout";
|
|
331
|
+
export type FrameSlotType = "topInset" | "bottomInset";
|
|
332
|
+
export type FrameSlotBucket = "active" | "pending";
|
|
333
|
+
export type FrameLayer = "page" | "topChrome" | "bottomChrome" | "keyboard" | "overlay";
|
|
334
|
+
export type FrameSlotRole = "topChrome" | "bottomChrome" | "keyboardAccessory";
|
|
335
|
+
export type FramePlatformProfile = "ios" | "android" | "web" | "mobileWeb";
|
|
336
|
+
export interface FrameViewportState {
|
|
337
|
+
width: number;
|
|
338
|
+
height: number;
|
|
339
|
+
visualWidth: number;
|
|
340
|
+
visualHeight: number;
|
|
341
|
+
visualOffsetTop: number;
|
|
342
|
+
}
|
|
343
|
+
export interface KeyboardFrameState {
|
|
344
|
+
height: number;
|
|
345
|
+
offset: number;
|
|
346
|
+
visible: boolean;
|
|
347
|
+
sticky: boolean;
|
|
348
|
+
frozen?: boolean;
|
|
349
|
+
source?: "native" | "visualViewport" | "fallback";
|
|
350
|
+
animationDuration?: number;
|
|
351
|
+
animationEasing?: string;
|
|
352
|
+
}
|
|
353
|
+
export interface FrameContentViewportState {
|
|
354
|
+
top: number;
|
|
355
|
+
bottom: number;
|
|
356
|
+
height: number;
|
|
357
|
+
}
|
|
358
|
+
export interface KeyboardAccessoryFrameState {
|
|
359
|
+
top: number;
|
|
360
|
+
bottom: number;
|
|
361
|
+
height: number;
|
|
362
|
+
visible: boolean;
|
|
363
|
+
slotHeight: number;
|
|
364
|
+
}
|
|
365
|
+
export interface FrameSnapshot {
|
|
366
|
+
location: Location;
|
|
367
|
+
pageState: PageState;
|
|
368
|
+
viewport: FrameViewportState;
|
|
369
|
+
frameSlots: FrameSlotRegistration[];
|
|
370
|
+
measuredAt: number;
|
|
371
|
+
}
|
|
372
|
+
export interface TransitionActionContext {
|
|
373
|
+
plan: TransitionPlan;
|
|
374
|
+
}
|
|
375
|
+
export interface TransitionAction {
|
|
376
|
+
type: "page" | "topChrome" | "bottomChrome" | "keyboard" | "safeArea" | (string & {});
|
|
377
|
+
run: (ctx: TransitionActionContext) => Promise<void> | void;
|
|
378
|
+
}
|
|
379
|
+
export interface TransitionPlan {
|
|
380
|
+
id: number;
|
|
381
|
+
intent: NavigationIntent;
|
|
382
|
+
type: TransitionType;
|
|
383
|
+
direction: "forward" | "back";
|
|
384
|
+
fromFrame: FrameSnapshot;
|
|
385
|
+
toFrame: FrameSnapshot;
|
|
386
|
+
actions: TransitionAction[];
|
|
387
|
+
duration: number;
|
|
388
|
+
}
|
|
389
|
+
export interface FrameLayerZIndex {
|
|
390
|
+
page: number;
|
|
391
|
+
previousPage: number;
|
|
392
|
+
cachedPage: number;
|
|
393
|
+
topChrome: number;
|
|
394
|
+
bottomChrome: number;
|
|
395
|
+
keyboard: number;
|
|
396
|
+
overlay: number;
|
|
397
|
+
}
|
|
398
|
+
export interface FrameLayoutState {
|
|
399
|
+
viewport: FrameViewportState;
|
|
400
|
+
keyboard: KeyboardFrameState;
|
|
401
|
+
contentViewport: FrameContentViewportState;
|
|
402
|
+
keyboardAccessory: KeyboardAccessoryFrameState;
|
|
403
|
+
platformProfile: FramePlatformProfile;
|
|
404
|
+
zIndex: FrameLayerZIndex;
|
|
405
|
+
pageStateByPath: Map<string, PageState>;
|
|
406
|
+
}
|
|
407
|
+
export interface FrameSlotRegistration {
|
|
408
|
+
scope?: FrameSlotScope;
|
|
409
|
+
type: FrameSlotType;
|
|
410
|
+
role?: FrameSlotRole;
|
|
411
|
+
height?: number;
|
|
412
|
+
estimatedHeight?: number;
|
|
413
|
+
source?: "navbar" | "topInset" | "bottomInset" | "bottomTab" | (string & {});
|
|
414
|
+
cache?: boolean;
|
|
415
|
+
}
|
|
298
416
|
export interface LayoutFallbackRoute {
|
|
299
417
|
path: string;
|
|
300
418
|
pathSegments: string[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PageConfig, PageState } from "./csrTypes.d.ts";
|
|
2
|
+
export type DevicePlatform = "ios" | "android" | "web" | (string & {});
|
|
3
|
+
export type SafeAreaInsets = {
|
|
4
|
+
top: number;
|
|
5
|
+
bottom: number;
|
|
6
|
+
};
|
|
7
|
+
export interface ResolvePageStateOptions {
|
|
8
|
+
configChain?: PageConfig[];
|
|
9
|
+
path: string;
|
|
10
|
+
basePath?: string;
|
|
11
|
+
platform: DevicePlatform;
|
|
12
|
+
deviceSafeArea: SafeAreaInsets;
|
|
13
|
+
cssSafeArea?: SafeAreaInsets;
|
|
14
|
+
}
|
|
15
|
+
export declare function validatePageConfig(routeKey: string, config?: PageConfig): void;
|
|
16
|
+
export declare function mergePageConfigs(configChain?: PageConfig[]): PageConfig;
|
|
17
|
+
export declare function getExplicitPageConfigKeys(configChain?: PageConfig[]): Partial<Record<keyof PageConfig, boolean>>;
|
|
18
|
+
export declare function resolvePageState({ configChain, path, basePath, platform, deviceSafeArea, cssSafeArea, }: ResolvePageStateOptions): PageState;
|
|
19
|
+
export declare function readCssSafeAreaInsets(): SafeAreaInsets;
|
package/types/client/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export * from "./createFont.d.ts";
|
|
|
4
4
|
export * from "./csrTypes.d.ts";
|
|
5
5
|
export * from "./decorators.d.ts";
|
|
6
6
|
export * from "./device.d.ts";
|
|
7
|
+
export * from "./frameConfig.d.ts";
|
|
8
|
+
export * from "./frameDebug.d.ts";
|
|
7
9
|
export * from "./locale.d.ts";
|
|
8
10
|
export * from "./makePageProto.d.ts";
|
|
9
11
|
export * from "./router.d.ts";
|
package/types/client/router.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export declare class AkanNotFoundError extends Error {
|
|
|
43
43
|
}
|
|
44
44
|
declare global {
|
|
45
45
|
var __AKAN_ROUTER__: Router | undefined;
|
|
46
|
+
var __AKAN_DEV_SYNC_NAVIGATION__: ((href: string, kind: "push" | "replace" | "back" | "pop") => void) | undefined;
|
|
47
|
+
var __AKAN_DEV_SYNC_NAVIGATION_APPLYING__: boolean | undefined;
|
|
46
48
|
}
|
|
47
49
|
export declare const getPathInfo: (requestUrl: string, lang: string, prefix: string) => {
|
|
48
50
|
path: string;
|
|
@@ -12,6 +12,7 @@ export interface AkanDynamicUsage {
|
|
|
12
12
|
export interface AkanRequestStore {
|
|
13
13
|
request: Request;
|
|
14
14
|
theme?: AkanTheme;
|
|
15
|
+
frameState?: unknown;
|
|
15
16
|
queryCache: Map<string, Promise<unknown>>;
|
|
16
17
|
policy: AkanRequestPolicy;
|
|
17
18
|
dynamicUsage: AkanDynamicUsage;
|
|
@@ -29,6 +30,8 @@ export declare function createRequestStore(request: Request, policy?: Partial<Om
|
|
|
29
30
|
/** Stores theme preference on the active request when server rendering. */
|
|
30
31
|
export declare function setRequestTheme(theme: AkanTheme | undefined): void;
|
|
31
32
|
export declare function getRequestTheme(): AkanTheme | undefined;
|
|
33
|
+
export declare function setRequestFrameState(frameState: unknown): void;
|
|
34
|
+
export declare function getRequestFrameState<FrameState = unknown>(): FrameState | undefined;
|
|
32
35
|
export declare function pushRequestFallback(storeOrRequest: Request | AkanRequestStore): () => void;
|
|
33
36
|
/** Returns the active server request store from AsyncLocalStorage or the fallback stack. */
|
|
34
37
|
export declare function getRequestStore(): AkanRequestStore | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const HMR_CLIENT_SCRIPT = "(function(){\n if (self.__AKAN_HMR_INSTALLED__) return;\n self.__AKAN_HMR_INSTALLED__ = true;\n var proto = location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n var url = proto + \"//\" + location.host + \"/_akan/hmr\";\n var attempts = 0;\n var socket = null;\n var lastBuildId = null;\n var refreshRuntimePromise = null;\n var refreshRuntime = null;\n var pendingRefreshRegistrations = [];\n var refreshQueue = Promise.resolve();\n var overlayEl = null;\n var overlayLabelEl = null;\n var overlayDetailEl = null;\n var overlayStyleEl = null;\n var overlayTimer = null;\n var overlayHideTimer = null;\n var overlayNextToken = 1;\n var overlayJobs = {};\n var buildErrorStates = {};\n self.__AKAN_HMR_PHASE__ = null;\n\n // Bun's React Fast Refresh transform can emit top-level calls to these globals\n // even when we fall back to full reload instead of applying React Refresh.\n self.$RefreshReg$ = self.$RefreshReg$ || function(type, id){\n if (refreshRuntime) refreshRuntime.register(type, id);\n else pendingRefreshRegistrations.push([type, id]);\n };\n self.$RefreshSig$ = self.$RefreshSig$ || function(){ return function(type){ return type; }; };\n // Start installing React Refresh before the application module graph loads.\n // Injecting the runtime only on the first update is too late for React's renderer hook.\n ensureRefreshRuntime().catch(function(err){\n console.warn(\"[akan-hmr] React Refresh runtime preload failed\", err);\n });\n\n function connect(){\n try { socket = new WebSocket(url); }\n catch(e){ console.error(\"[akan-hmr] ws init failed\", e); schedule(); return; }\n socket.addEventListener(\"open\", function(){ attempts = 0; });\n socket.addEventListener(\"message\", function(ev){\n var msg;\n try { msg = JSON.parse(ev.data); } catch (e){ return; }\n if (!msg || typeof msg.type !== \"string\") return;\n if (msg.type === \"hello\") {\n if (lastBuildId !== null && msg.buildId !== lastBuildId) {\n location.reload();\n return;\n }\n lastBuildId = msg.buildId;\n return;\n }\n if (msg.type === \"reload\") {\n beginHmrOverlay(\"Reloading...\", true);\n try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}\n setTimeout(function(){ location.reload(); }, 30);\n return;\n }\n if (msg.type === \"rsc-refresh\") {\n refreshRsc(msg);\n return;\n }\n if (msg.type === \"client-refresh\") {\n refreshClient(msg);\n return;\n }\n if (msg.type === \"css-update\") {\n var cssUrl = selectCssUrl(msg.cssAssets);\n if (cssUrl) swapCss(cssUrl);\n else {\n beginHmrOverlay(\"Reloading...\", true);\n location.reload();\n }\n return;\n }\n if (msg.type === \"build-status\") { handleBuildStatus(msg); return; }\n if (msg.type === \"ok\") {\n clearBuildErrorOverlay({\n phase: \"build\",\n generation: typeof msg.generation === \"number\" ? msg.generation : Number.MAX_SAFE_INTEGER,\n files: 0\n });\n return;\n }\n if (msg.type === \"error\") {\n console.error(\"[akan-hmr]\", msg.message);\n showBuildErrorOverlay({ phase: \"build\", generation: 0, message: msg.message, files: 0 });\n return;\n }\n });\n socket.addEventListener(\"close\", function(){ socket = null; schedule(); });\n socket.addEventListener(\"error\", function(){ try { socket && socket.close(); } catch(e){} });\n }\n\n function schedule(){\n attempts = Math.min(attempts + 1, 6);\n var delay = Math.min(30000, 250 * Math.pow(2, attempts - 1));\n setTimeout(connect, delay);\n }\n\n function ensureOverlay(){\n if (overlayEl && overlayLabelEl) return overlayEl;\n if (!overlayStyleEl) {\n overlayStyleEl = document.createElement(\"style\");\n overlayStyleEl.textContent =\n \"@keyframes akan-hmr-spin{to{transform:rotate(360deg)}}\" +\n \".__akan_hmr_overlay{position:fixed;left:16px;bottom:16px;z-index:2147483647;display:flex;align-items:flex-start;gap:9px;max-width:min(420px,calc(100vw - 32px));padding:10px 12px;border-radius:16px;background:rgba(17,24,39,.94);color:#fff;font:500 13px/1.25 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;box-shadow:0 10px 28px rgba(0,0,0,.28);pointer-events:none;opacity:0;transform:translateY(6px);transition:opacity .15s ease,transform .15s ease;backdrop-filter:blur(8px)}\" +\n \".__akan_hmr_overlay[data-show=true]{opacity:1;transform:translateY(0)}\" +\n \".__akan_hmr_overlay[data-status=error]{background:rgba(127,29,29,.96);border:1px solid rgba(252,165,165,.55)}\" +\n \".__akan_hmr_overlay[data-status=ok]{background:rgba(6,95,70,.94);border:1px solid rgba(110,231,183,.45)}\" +\n \".__akan_hmr_spinner{width:14px;height:14px;margin-top:1px;border:2px solid rgba(255,255,255,.32);border-top-color:#fff;border-radius:999px;animation:akan-hmr-spin .75s linear infinite;flex:none}\" +\n \".__akan_hmr_overlay[data-status=error] .__akan_hmr_spinner,.__akan_hmr_overlay[data-status=ok] .__akan_hmr_spinner{animation:none;border-color:rgba(255,255,255,.72);border-top-color:rgba(255,255,255,.72)}\" +\n \".__akan_hmr_body{display:flex;flex-direction:column;gap:3px;min-width:0}\" +\n \".__akan_hmr_detail{font:400 12px/1.35 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:rgba(255,255,255,.82);white-space:pre-wrap;overflow-wrap:anywhere}\" +\n \"@media (prefers-reduced-motion:reduce){.__akan_hmr_overlay{transition:none}.__akan_hmr_spinner{animation:none}}\";\n document.head.appendChild(overlayStyleEl);\n }\n overlayEl = document.createElement(\"div\");\n overlayEl.className = \"__akan_hmr_overlay\";\n overlayEl.setAttribute(\"data-status\", \"updating\");\n overlayEl.setAttribute(\"role\", \"status\");\n overlayEl.setAttribute(\"aria-live\", \"polite\");\n overlayEl.innerHTML = '<span class=\"__akan_hmr_spinner\" aria-hidden=\"true\"></span><span class=\"__akan_hmr_body\"><span data-akan-hmr-label>Updating...</span><span class=\"__akan_hmr_detail\" data-akan-hmr-detail></span></span>';\n overlayLabelEl = overlayEl.querySelector(\"[data-akan-hmr-label]\");\n overlayDetailEl = overlayEl.querySelector(\"[data-akan-hmr-detail]\");\n (document.body || document.documentElement).appendChild(overlayEl);\n return overlayEl;\n }\n\n function activeOverlayTokens(){\n return Object.keys(overlayJobs);\n }\n\n function latestOverlayLabel(){\n var keys = activeOverlayTokens();\n if (keys.length === 0) return \"Updating...\";\n return overlayJobs[keys[keys.length - 1]] || \"Updating...\";\n }\n\n function showOverlayNow(){\n overlayTimer = null;\n if (activeOverlayTokens().length === 0) return;\n if (hasBuildErrors()) {\n renderBuildErrorOverlay();\n return;\n }\n var el = ensureOverlay();\n el.setAttribute(\"data-status\", \"updating\");\n if (overlayHideTimer) {\n clearTimeout(overlayHideTimer);\n overlayHideTimer = null;\n }\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n if (overlayDetailEl) overlayDetailEl.textContent = \"\";\n requestAnimationFrame(function(){ el.setAttribute(\"data-show\", \"true\"); });\n }\n\n function beginHmrOverlay(label, immediate){\n var token = overlayNextToken++;\n overlayJobs[token] = label || \"Updating...\";\n if (!hasBuildErrors() && overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n if (overlayHideTimer) {\n clearTimeout(overlayHideTimer);\n overlayHideTimer = null;\n }\n if (immediate) {\n if (overlayTimer) clearTimeout(overlayTimer);\n showOverlayNow();\n } else if (!overlayTimer && (!overlayEl || overlayEl.getAttribute(\"data-show\") !== \"true\")) {\n overlayTimer = setTimeout(showOverlayNow, 120);\n }\n return token;\n }\n\n function setHmrOverlayLabel(token, label){\n if (!overlayJobs[token]) return;\n overlayJobs[token] = label || \"Updating...\";\n if (!hasBuildErrors() && overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n }\n\n function endHmrOverlay(token){\n delete overlayJobs[token];\n if (hasBuildErrors()) {\n renderBuildErrorOverlay();\n return;\n }\n if (activeOverlayTokens().length > 0) {\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n return;\n }\n if (overlayTimer) {\n clearTimeout(overlayTimer);\n overlayTimer = null;\n }\n if (!overlayEl) return;\n overlayEl.setAttribute(\"data-show\", \"false\");\n if (overlayHideTimer) clearTimeout(overlayHideTimer);\n overlayHideTimer = setTimeout(function(){\n if (overlayEl && activeOverlayTokens().length === 0 && overlayEl.parentNode) {\n overlayEl.parentNode.removeChild(overlayEl);\n overlayEl = null;\n overlayLabelEl = null;\n overlayDetailEl = null;\n }\n }, 180);\n }\n\n function handleBuildStatus(msg){\n if (msg.status === \"error\") {\n showBuildErrorOverlay(msg);\n return;\n }\n if (msg.status === \"ok\") clearBuildErrorOverlay(msg);\n }\n\n function showBuildErrorOverlay(msg){\n var phase = msg.phase || \"build\";\n var generation = typeof msg.generation === \"number\" ? msg.generation : 0;\n var previous = buildErrorStates[phase];\n if (previous && generation < previous.generation) return;\n buildErrorStates[phase] = {\n phase: msg.phase || \"build\",\n generation: generation,\n message: msg.message || \"Build failed\",\n files: typeof msg.files === \"number\" ? msg.files : 0\n };\n console.error(\"[akan-hmr] build failed\", buildErrorStates[phase]);\n renderBuildErrorOverlay();\n }\n\n function renderBuildErrorOverlay(){\n if (!hasBuildErrors()) return;\n if (overlayTimer) {\n clearTimeout(overlayTimer);\n overlayTimer = null;\n }\n if (overlayHideTimer) {\n clearTimeout(overlayHideTimer);\n overlayHideTimer = null;\n }\n var el = ensureOverlay();\n var phases = buildErrorPhases();\n var latest = latestBuildErrorState();\n el.setAttribute(\"data-status\", \"error\");\n if (overlayLabelEl) overlayLabelEl.textContent = \"Build failed: \" + phases.join(\", \");\n if (overlayDetailEl) overlayDetailEl.textContent = formatBuildStatusDetail(latest, phases.length);\n requestAnimationFrame(function(){ el.setAttribute(\"data-show\", \"true\"); });\n }\n\n function clearBuildErrorOverlay(msg){\n var phase = msg.phase || \"build\";\n var current = buildErrorStates[phase];\n if (!current) return;\n var generation = typeof msg.generation === \"number\" ? msg.generation : 0;\n var recovered = phase === \"backend\" ? generation >= current.generation : generation > current.generation;\n if (!recovered) return;\n delete buildErrorStates[phase];\n if (hasBuildErrors()) {\n renderBuildErrorOverlay();\n return;\n }\n if (overlayHideTimer) clearTimeout(overlayHideTimer);\n var el = ensureOverlay();\n el.setAttribute(\"data-status\", \"ok\");\n if (overlayLabelEl) overlayLabelEl.textContent = \"Build recovered\";\n if (overlayDetailEl) overlayDetailEl.textContent = formatBuildStatusDetail(msg);\n requestAnimationFrame(function(){ el.setAttribute(\"data-show\", \"true\"); });\n overlayHideTimer = setTimeout(function(){\n if (activeOverlayTokens().length > 0) {\n showOverlayNow();\n return;\n }\n if (!overlayEl) return;\n overlayEl.setAttribute(\"data-show\", \"false\");\n overlayHideTimer = setTimeout(function(){\n if (overlayEl && activeOverlayTokens().length === 0 && !hasBuildErrors() && overlayEl.parentNode) {\n overlayEl.parentNode.removeChild(overlayEl);\n overlayEl = null;\n overlayLabelEl = null;\n overlayDetailEl = null;\n }\n }, 180);\n }, 900);\n }\n\n function hasBuildErrors(){\n return Object.keys(buildErrorStates).length > 0;\n }\n\n function buildErrorPhases(){\n return Object.keys(buildErrorStates).sort();\n }\n\n function latestBuildErrorState(){\n var phases = buildErrorPhases();\n var latest = buildErrorStates[phases[0]];\n for (var i = 1; i < phases.length; i++) {\n var next = buildErrorStates[phases[i]];\n if (!latest || next.generation >= latest.generation) latest = next;\n }\n return latest;\n }\n\n function formatBuildStatusDetail(msg, failedPhaseCount){\n var parts = [];\n if (typeof msg.generation === \"number\" && msg.generation > 0) parts.push(\"generation \" + msg.generation);\n if (typeof msg.files === \"number\") parts.push(msg.files + \" file\" + (msg.files === 1 ? \"\" : \"s\"));\n if (failedPhaseCount > 1) parts.push(failedPhaseCount + \" failed phases\");\n var prefix = parts.length > 0 ? parts.join(\" \u00B7 \") : \"\";\n if (!msg.message) return prefix;\n return prefix ? prefix + \"\\n\" + msg.message : msg.message;\n }\n\n function refreshRsc(msg){\n var started = performance.now();\n var overlayToken = beginHmrOverlay(\"Refreshing page...\");\n try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}\n if (!self.__AKAN_RSC_REFRESH__) {\n console.warn(\"[akan-hmr] RSC refresh API unavailable, falling back to full reload\");\n setHmrOverlayLabel(overlayToken, \"Reloading...\");\n setTimeout(function(){ location.reload(); }, 30);\n return;\n }\n Promise.resolve(self.__AKAN_RSC_REFRESH__({ buildId: msg.buildId })).then(function(){\n lastBuildId = msg.buildId;\n endHmrOverlay(overlayToken);\n console.debug && console.debug(\"[akan-hmr] RSC refreshed\", {\n buildId: msg.buildId,\n generation: msg.generation,\n routeIds: msg.routeIds,\n changedFiles: msg.changedFiles && msg.changedFiles.length,\n durationMs: Math.round(performance.now() - started)\n });\n }, function(err){\n console.error(\"[akan-hmr] RSC refresh failed, falling back to full reload\", err);\n setHmrOverlayLabel(overlayToken, \"Update failed, reloading...\");\n setTimeout(function(){ location.reload(); }, 250);\n });\n }\n\n function ensureRefreshRuntime(){\n if (refreshRuntimePromise) return refreshRuntimePromise;\n refreshRuntimePromise = import(\"react-refresh/runtime\").then(function(mod){\n var runtime = mod.default || mod;\n if (!self.__AKAN_REACT_REFRESH_READY__) {\n refreshRuntime = runtime;\n runtime.injectIntoGlobalHook(self);\n self.$RefreshReg$ = function(type, id){ runtime.register(type, id); };\n self.$RefreshSig$ = runtime.createSignatureFunctionForTransform;\n for (var i = 0; i < pendingRefreshRegistrations.length; i++) {\n self.$RefreshReg$(pendingRefreshRegistrations[i][0], pendingRefreshRegistrations[i][1]);\n }\n pendingRefreshRegistrations = [];\n self.__AKAN_REACT_REFRESH_READY__ = true;\n self.__AKAN_REACT_REFRESH_RUNTIME__ = runtime;\n }\n return runtime;\n });\n return refreshRuntimePromise;\n }\n\n function refreshClient(msg){\n refreshQueue = refreshQueue.then(function(){ return doRefreshClient(msg); }, function(){ return doRefreshClient(msg); });\n }\n\n function setHmrPhase(phase){\n self.__AKAN_HMR_PHASE__ = phase;\n }\n\n function doRefreshClient(msg){\n var started = performance.now();\n var metadataAt = started;\n var importAt = started;\n var refreshAt = started;\n var overlayToken = beginHmrOverlay(\"Updating...\");\n var fallbackToRsc = false;\n return ensureRefreshRuntime().then(function(runtime){\n setHmrOverlayLabel(overlayToken, \"Fetching update...\");\n var endpoint = new URL(\"/_akan/hmr/client-refresh\", location.origin);\n endpoint.searchParams.set(\"url\", location.href);\n if (msg.buildId != null) endpoint.searchParams.set(\"buildId\", String(msg.buildId));\n return fetch(endpoint, { credentials: \"same-origin\", cache: \"no-store\" })\n .then(function(res){\n if (!res.ok) throw new Error(\"client-refresh metadata failed \" + res.status + \" \" + res.statusText);\n return res.json();\n })\n .then(function(info){\n metadataAt = performance.now();\n var chunks = Array.isArray(info.chunks) ? info.chunks : [];\n if (chunks.length === 0) throw new Error(\"no client chunks returned\");\n setHmrPhase(\"refresh-import\");\n setHmrOverlayLabel(overlayToken, \"Importing update...\");\n return Promise.all(chunks.map(function(chunk){ return import(chunk); })).then(function(){\n importAt = performance.now();\n setHmrPhase(\"react-refresh\");\n setHmrOverlayLabel(overlayToken, \"Applying update...\");\n try {\n runtime.performReactRefresh();\n } finally {\n setHmrPhase(null);\n }\n refreshAt = performance.now();\n lastBuildId = msg.buildId;\n console.debug && console.debug(\"[akan-hmr] React Fast Refresh applied\", {\n buildId: msg.buildId,\n generation: msg.generation,\n chunks: chunks.length,\n routeIds: info.routeIds || msg.routeIds,\n changedFiles: msg.changedFiles && msg.changedFiles.length,\n metadataMs: Math.round(metadataAt - started),\n importMs: Math.round(importAt - metadataAt),\n refreshMs: Math.round(refreshAt - importAt),\n durationMs: Math.round(refreshAt - started)\n });\n endHmrOverlay(overlayToken);\n }, function(err){\n setHmrPhase(null);\n throw err;\n });\n });\n }).catch(function(err){\n console.warn(\"[akan-hmr] React Fast Refresh failed, falling back to RSC refresh\", err);\n fallbackToRsc = true;\n endHmrOverlay(overlayToken);\n refreshRsc(msg);\n }).finally(function(){\n if (!fallbackToRsc) endHmrOverlay(overlayToken);\n });\n }\n\n function swapCss(href){\n var overlayToken = beginHmrOverlay(\"Updating styles...\");\n var link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = href;\n link.setAttribute(\"data-akan-css\", \"pending\");\n link.addEventListener(\"load\", function(){\n var prev = document.querySelectorAll(\"link[data-akan-css=active]\");\n for (var i = 0; i < prev.length; i++) prev[i].parentNode && prev[i].parentNode.removeChild(prev[i]);\n link.setAttribute(\"data-akan-css\", \"active\");\n endHmrOverlay(overlayToken);\n });\n link.addEventListener(\"error\", function(){\n if (link.parentNode) link.parentNode.removeChild(link);\n endHmrOverlay(overlayToken);\n });\n document.head.appendChild(link);\n }\n\n function selectCssUrl(cssAssets){\n if (!cssAssets || typeof cssAssets !== \"object\") return null;\n var parts = location.pathname.split(\"/\").filter(Boolean);\n for (var i = 0; i < parts.length; i++) {\n var asset = cssAssets[parts[i]];\n if (asset && asset.cssUrl) return asset.cssUrl;\n }\n return cssAssets[\"\"] && cssAssets[\"\"].cssUrl ? cssAssets[\"\"].cssUrl : null;\n }\n\n connect();\n})();";
|
|
1
|
+
export declare const HMR_CLIENT_SCRIPT: string;
|
|
@@ -31,6 +31,11 @@ export type HmrMessage = {
|
|
|
31
31
|
cssUrl: string;
|
|
32
32
|
cssRelPath: string;
|
|
33
33
|
}>;
|
|
34
|
+
} | {
|
|
35
|
+
type: "sync-navigation";
|
|
36
|
+
clientId: string;
|
|
37
|
+
href: string;
|
|
38
|
+
kind?: "push" | "replace" | "back" | "pop";
|
|
34
39
|
} | {
|
|
35
40
|
type: "ok";
|
|
36
41
|
generation?: number;
|
|
@@ -53,4 +58,5 @@ export declare class HmrWsHub {
|
|
|
53
58
|
attach(ws: Bun.ServerWebSocket<HmrWsData>): void;
|
|
54
59
|
detach(ws: Bun.ServerWebSocket<HmrWsData>): void;
|
|
55
60
|
broadcast(msg: HmrMessage): void;
|
|
61
|
+
handleMessage(message: string): void;
|
|
56
62
|
}
|