akanjs 2.1.2-rc.1 → 2.2.0-rc.1
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/CHANGELOG.md +8 -0
- package/capacitor.base.config.ts +44 -4
- package/client/capacitor.ts +17 -19
- package/client/csrTypes.ts +1 -0
- package/client/router.ts +18 -2
- package/package.json +1 -1
- package/server/routeTreeBuilder.ts +1 -0
- package/types/client/csrTypes.d.ts +1 -0
- package/types/webkit/bootCsr.d.ts +7 -1
- package/ui/System/CSR.tsx +5 -1
- package/webkit/bootCsr.tsx +25 -3
package/CHANGELOG.md
ADDED
package/capacitor.base.config.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
1
2
|
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import type { CapacitorConfig } from "@capacitor/cli";
|
|
3
5
|
import type { AkanMobileTargetConfig, AppScanResult } from "akanjs";
|
|
6
|
+
import { parseAkanI18nEnv } from "akanjs/common";
|
|
4
7
|
|
|
5
8
|
const getLocalIP = () => {
|
|
6
9
|
const interfaces = os.networkInterfaces();
|
|
@@ -16,6 +19,39 @@ const getLocalIP = () => {
|
|
|
16
19
|
|
|
17
20
|
const normalizeBasePath = (basePath: string | undefined) => basePath?.replace(/^\/+|\/+$/g, "");
|
|
18
21
|
|
|
22
|
+
const findAppsDir = (appName: string) => {
|
|
23
|
+
let dir = process.cwd();
|
|
24
|
+
while (dir !== path.dirname(dir)) {
|
|
25
|
+
const appsDir = path.join(dir, "apps");
|
|
26
|
+
if (existsSync(path.join(appsDir, appName, "akan.config.ts"))) return appsDir;
|
|
27
|
+
if (path.basename(dir) === appName && existsSync(path.join(dir, "akan.config.ts"))) return path.dirname(dir);
|
|
28
|
+
dir = path.dirname(dir);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const getAppNames = (appsDir: string, maxDepth = 3, prefix = ""): string[] => {
|
|
33
|
+
const appNames: string[] = [];
|
|
34
|
+
for (const entry of readdirSync(path.join(appsDir, prefix))) {
|
|
35
|
+
if (["node_modules", "dist", "public", "webkit"].includes(entry)) continue;
|
|
36
|
+
const entryPath = path.join(appsDir, prefix, entry);
|
|
37
|
+
if (!statSync(entryPath).isDirectory()) continue;
|
|
38
|
+
const appName = path.join(prefix, entry).split(path.sep).join("/");
|
|
39
|
+
if (existsSync(path.join(entryPath, "akan.config.ts"))) appNames.push(appName);
|
|
40
|
+
if (maxDepth > 0) appNames.push(...getAppNames(appsDir, maxDepth - 1, appName));
|
|
41
|
+
}
|
|
42
|
+
return appNames;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const resolveLocalCsrPort = (appInfo: AppScanResult) => {
|
|
46
|
+
const explicitPort = process.env.AKAN_PUBLIC_CLIENT_PORT ?? process.env.PORT;
|
|
47
|
+
if (explicitPort) return explicitPort;
|
|
48
|
+
const appsDir = findAppsDir(appInfo.name);
|
|
49
|
+
const appNames = appsDir ? getAppNames(appsDir).sort((a, b) => a.localeCompare(b)) : [];
|
|
50
|
+
const appIndex = Math.max(appNames.indexOf(appInfo.name), 0);
|
|
51
|
+
const portOffset = Number.parseInt(process.env.PORT_OFFSET ?? "0");
|
|
52
|
+
return (8282 + appIndex + portOffset).toString();
|
|
53
|
+
};
|
|
54
|
+
|
|
19
55
|
const routeBasePaths = (appInfo: AppScanResult) =>
|
|
20
56
|
new Set(
|
|
21
57
|
appInfo.routes
|
|
@@ -42,10 +78,14 @@ const resolveTarget = (appInfo: AppScanResult, targetName = process.env.AKAN_MOB
|
|
|
42
78
|
return entries[0]?.[1] as AkanMobileTargetConfig;
|
|
43
79
|
};
|
|
44
80
|
|
|
45
|
-
const localCsrUrl = (ip: string, target: AkanMobileTargetConfig) => {
|
|
81
|
+
const localCsrUrl = (ip: string, target: AkanMobileTargetConfig, appInfo: AppScanResult) => {
|
|
46
82
|
const basePath = normalizeBasePath(target.basePath);
|
|
47
|
-
const
|
|
48
|
-
|
|
83
|
+
const locale = parseAkanI18nEnv().defaultLocale;
|
|
84
|
+
const pathname = basePath ? `${locale}/${basePath}` : `${locale}/`;
|
|
85
|
+
const port = resolveLocalCsrPort(appInfo);
|
|
86
|
+
const params = new URLSearchParams({ csr: "true", akanMobileTarget: target.name });
|
|
87
|
+
if (basePath) params.set("akanMobileBasePath", basePath);
|
|
88
|
+
return `http://${ip}:${port}/${pathname}?${params}`;
|
|
49
89
|
};
|
|
50
90
|
|
|
51
91
|
export const withBase = (
|
|
@@ -66,7 +106,7 @@ export const withBase = (
|
|
|
66
106
|
process.env.APP_OPERATION_MODE !== "release"
|
|
67
107
|
? {
|
|
68
108
|
androidScheme: "http",
|
|
69
|
-
url: localCsrUrl(ip, target),
|
|
109
|
+
url: localCsrUrl(ip, target, appInfo),
|
|
70
110
|
cleartext: true,
|
|
71
111
|
allowNavigation: [ip, "localhost"],
|
|
72
112
|
}
|
package/client/capacitor.ts
CHANGED
|
@@ -169,58 +169,56 @@ const loadCapacitorModule = <K extends keyof CapacitorModuleMap>(
|
|
|
169
169
|
return loaded;
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
const capacitorPackage = (name: string) => `@capacitor/${name}`;
|
|
175
|
-
|
|
176
|
-
const capacitorCommunityPackage = (name: string) => `@capacitor-community/${name}`;
|
|
172
|
+
const asCapacitorModule = <T>(modulePromise: Promise<unknown>) => modulePromise as Promise<T>;
|
|
177
173
|
|
|
178
174
|
export const loadCapacitorApp = () =>
|
|
179
|
-
loadCapacitorModule("app", () =>
|
|
175
|
+
loadCapacitorModule("app", () => asCapacitorModule<CapacitorAppModule>(import("@capacitor/app")));
|
|
180
176
|
|
|
181
177
|
export const loadCapacitorBrowser = () =>
|
|
182
|
-
loadCapacitorModule("browser", () =>
|
|
178
|
+
loadCapacitorModule("browser", () => asCapacitorModule<CapacitorBrowserModule>(import("@capacitor/browser")));
|
|
183
179
|
|
|
184
180
|
export const loadCapacitorCamera = () =>
|
|
185
|
-
loadCapacitorModule("camera", () =>
|
|
181
|
+
loadCapacitorModule("camera", () => asCapacitorModule<CapacitorCameraModule>(import("@capacitor/camera")));
|
|
186
182
|
|
|
187
183
|
export const loadCapacitorContacts = () =>
|
|
188
184
|
loadCapacitorModule("contacts", () =>
|
|
189
|
-
|
|
185
|
+
asCapacitorModule<CapacitorContactsModule>(import("@capacitor-community/contacts")),
|
|
190
186
|
);
|
|
191
187
|
|
|
192
188
|
export const loadCapacitorCore = () =>
|
|
193
|
-
loadCapacitorModule("core", () =>
|
|
189
|
+
loadCapacitorModule("core", () => asCapacitorModule<CapacitorCoreModule>(import("@capacitor/core")));
|
|
194
190
|
|
|
195
191
|
export const loadCapacitorDevice = () =>
|
|
196
|
-
loadCapacitorModule("device", () =>
|
|
192
|
+
loadCapacitorModule("device", () => asCapacitorModule<CapacitorDeviceModule>(import("@capacitor/device")));
|
|
197
193
|
|
|
198
194
|
export const loadCapacitorFcm = () =>
|
|
199
|
-
loadCapacitorModule("fcm", () =>
|
|
195
|
+
loadCapacitorModule("fcm", () => asCapacitorModule<CapacitorFcmModule>(import("@capacitor-community/fcm")));
|
|
200
196
|
|
|
201
197
|
export const loadCapacitorGeolocation = () =>
|
|
202
198
|
loadCapacitorModule("geolocation", () =>
|
|
203
|
-
|
|
199
|
+
asCapacitorModule<CapacitorGeolocationModule>(import("@capacitor/geolocation")),
|
|
204
200
|
);
|
|
205
201
|
|
|
206
202
|
export const loadCapacitorHaptics = () =>
|
|
207
|
-
loadCapacitorModule("haptics", () =>
|
|
203
|
+
loadCapacitorModule("haptics", () => asCapacitorModule<CapacitorHapticsModule>(import("@capacitor/haptics")));
|
|
208
204
|
|
|
209
205
|
export const loadCapacitorKeyboard = () =>
|
|
210
|
-
loadCapacitorModule("keyboard", () =>
|
|
206
|
+
loadCapacitorModule("keyboard", () => asCapacitorModule<CapacitorKeyboardModule>(import("@capacitor/keyboard")));
|
|
211
207
|
|
|
212
208
|
export const loadCapacitorPreferences = () =>
|
|
213
209
|
loadCapacitorModule("preferences", () =>
|
|
214
|
-
|
|
210
|
+
asCapacitorModule<CapacitorPreferencesModule>(import("@capacitor/preferences")),
|
|
215
211
|
);
|
|
216
212
|
|
|
217
213
|
export const loadCapacitorPushNotifications = () =>
|
|
218
214
|
loadCapacitorModule("pushNotifications", () =>
|
|
219
|
-
|
|
215
|
+
asCapacitorModule<CapacitorPushNotificationsModule>(import("@capacitor/push-notifications")),
|
|
220
216
|
);
|
|
221
217
|
|
|
222
218
|
export const loadCapacitorSafeArea = () =>
|
|
223
|
-
loadCapacitorModule("safeArea", () =>
|
|
219
|
+
loadCapacitorModule("safeArea", () =>
|
|
220
|
+
asCapacitorModule<CapacitorSafeAreaModule>(import("capacitor-plugin-safe-area")),
|
|
221
|
+
);
|
|
224
222
|
|
|
225
223
|
export const loadCapacitorUpdater = () =>
|
|
226
|
-
loadCapacitorModule("updater", () =>
|
|
224
|
+
loadCapacitorModule("updater", () => asCapacitorModule<CapacitorUpdaterModule>(import("@capgo/capacitor-updater")));
|
package/client/csrTypes.ts
CHANGED
|
@@ -70,6 +70,7 @@ export type LayoutNotFoundRender = (props: LayoutNotFoundProps) => PromiseOrObje
|
|
|
70
70
|
export type LayoutErrorRender = (props: LayoutErrorProps) => PromiseOrObject<ReactNode>;
|
|
71
71
|
export interface RouteRender {
|
|
72
72
|
render: LayoutRender | PageRender;
|
|
73
|
+
isAsync?: boolean;
|
|
73
74
|
Loading?: LayoutLoadingRender | PageLoadingRender;
|
|
74
75
|
NotFound?: LayoutNotFoundRender;
|
|
75
76
|
Error?: LayoutErrorRender;
|
package/client/router.ts
CHANGED
|
@@ -63,6 +63,7 @@ function getServerRequestContext() {
|
|
|
63
63
|
const getConfiguredBasePaths = () => new Set(parseBasePaths(process.env.AKAN_PUBLIC_BASE_PATHS));
|
|
64
64
|
|
|
65
65
|
const shouldExposeBasePath = () => getEnv().operationMode === "local";
|
|
66
|
+
const CSR_RUNTIME_SEARCH_PARAMS = ["csr", "akanMobileTarget", "akanMobileBasePath"] as const;
|
|
66
67
|
|
|
67
68
|
const getLocaleFromPathname = (pathname: string) => {
|
|
68
69
|
const [firstSegment] = pathname.split("/").filter(Boolean);
|
|
@@ -168,14 +169,14 @@ class Router {
|
|
|
168
169
|
push: (href: string, routeOptions) => {
|
|
169
170
|
const { path, pathname, hash, href: fullHref } = this.#getPathInfo(href);
|
|
170
171
|
this.#postPathChange({ path, pathname, hash });
|
|
171
|
-
options.router.push(fullHref, routeOptions);
|
|
172
|
+
options.router.push(this.#withCsrRuntimeSearchParams(fullHref), routeOptions);
|
|
172
173
|
},
|
|
173
174
|
replace: (href: string, routeOptions) => {
|
|
174
175
|
const { path, pathname, hash, href: fullHref } = this.#getPathInfo(href);
|
|
175
176
|
this.#postPathChange({ path, pathname, hash });
|
|
176
177
|
|
|
177
178
|
setTimeout(() => {
|
|
178
|
-
options.router.replace(fullHref, routeOptions);
|
|
179
|
+
options.router.replace(this.#withCsrRuntimeSearchParams(fullHref), routeOptions);
|
|
179
180
|
}, 0);
|
|
180
181
|
},
|
|
181
182
|
back: (routeOptions) => {
|
|
@@ -200,6 +201,21 @@ class Router {
|
|
|
200
201
|
#getNavigationPathInfo(href: string) {
|
|
201
202
|
return this.#getPathInfo(href, shouldExposeBasePath() ? this.#prefix : "");
|
|
202
203
|
}
|
|
204
|
+
#withCsrRuntimeSearchParams(href: string) {
|
|
205
|
+
const currentSearch = new URLSearchParams(window.location.search);
|
|
206
|
+
if (currentSearch.get("csr") !== "true") return href;
|
|
207
|
+
|
|
208
|
+
const [hrefWithoutHash, hash = ""] = href.split("#");
|
|
209
|
+
const [pathname, search = ""] = hrefWithoutHash.split("?");
|
|
210
|
+
const nextSearch = new URLSearchParams(search);
|
|
211
|
+
for (const param of CSR_RUNTIME_SEARCH_PARAMS) {
|
|
212
|
+
if (nextSearch.has(param)) continue;
|
|
213
|
+
const value = currentSearch.get(param);
|
|
214
|
+
if (value !== null) nextSearch.set(param, value);
|
|
215
|
+
}
|
|
216
|
+
const query = nextSearch.toString();
|
|
217
|
+
return `${pathname}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`;
|
|
218
|
+
}
|
|
203
219
|
#getVisiblePathInfo(href: string, lang = this.#lang) {
|
|
204
220
|
return getPathInfo(href, lang, shouldExposeBasePath() ? this.#prefix : "");
|
|
205
221
|
}
|
package/package.json
CHANGED
|
@@ -283,6 +283,7 @@ export class RouteTreeBuilder {
|
|
|
283
283
|
static #makeRouteRender(key: string, kind: "page" | "layout", loader: () => Promise<RouteModule>): RouteRender {
|
|
284
284
|
const loadModule = RouteTreeBuilder.#makeLazyModule(key, kind, loader);
|
|
285
285
|
const routeRender: RouteRender = {
|
|
286
|
+
isAsync: true,
|
|
286
287
|
render: async (props: LayoutProps | PageProps) => {
|
|
287
288
|
const mod = await loadModule();
|
|
288
289
|
routeRender.Loading = mod.Loading as never;
|
|
@@ -73,6 +73,7 @@ export type LayoutNotFoundRender = (props: LayoutNotFoundProps) => PromiseOrObje
|
|
|
73
73
|
export type LayoutErrorRender = (props: LayoutErrorProps) => PromiseOrObject<ReactNode>;
|
|
74
74
|
export interface RouteRender {
|
|
75
75
|
render: LayoutRender | PageRender;
|
|
76
|
+
isAsync?: boolean;
|
|
76
77
|
Loading?: LayoutLoadingRender | PageLoadingRender;
|
|
77
78
|
NotFound?: LayoutNotFoundRender;
|
|
78
79
|
Error?: LayoutErrorRender;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { type RouteModule } from "akanjs/client";
|
|
2
|
+
type CsrRouteModuleLoader = () => Promise<RouteModule>;
|
|
3
|
+
type CsrRouteModuleEntry = CsrRouteModuleLoader | {
|
|
4
|
+
loader: CsrRouteModuleLoader;
|
|
5
|
+
isAsyncDefault?: boolean;
|
|
6
|
+
};
|
|
2
7
|
declare global {
|
|
3
8
|
interface Window {
|
|
4
9
|
__AKAN_MOBILE_TARGET__?: {
|
|
@@ -7,4 +12,5 @@ declare global {
|
|
|
7
12
|
};
|
|
8
13
|
}
|
|
9
14
|
}
|
|
10
|
-
export declare const bootCsr: (context: Record<string,
|
|
15
|
+
export declare const bootCsr: (context: Record<string, CsrRouteModuleEntry>) => Promise<void>;
|
|
16
|
+
export {};
|
package/ui/System/CSR.tsx
CHANGED
|
@@ -396,7 +396,7 @@ const RenderLayer = memo(({ renders, index, params, searchParams }: RenderLayerP
|
|
|
396
396
|
<RenderLayer renders={renders} index={index + 1} params={params} searchParams={searchParams} />
|
|
397
397
|
);
|
|
398
398
|
const routeRender = renders[index];
|
|
399
|
-
const isAsyncRender = routeRender
|
|
399
|
+
const isAsyncRender = isAsyncRouteRender(routeRender);
|
|
400
400
|
const resultRef = useRef<ReactNode | Promise<ReactNode> | null>(null);
|
|
401
401
|
if (isAsyncRender && resultRef.current === null) {
|
|
402
402
|
resultRef.current = routeRender?.render({ children, params, searchParams } as never) ?? null;
|
|
@@ -408,6 +408,10 @@ const RenderLayer = memo(({ renders, index, params, searchParams }: RenderLayerP
|
|
|
408
408
|
return <>{Component}</>;
|
|
409
409
|
});
|
|
410
410
|
|
|
411
|
+
function isAsyncRouteRender(routeRender?: RouteRender): boolean {
|
|
412
|
+
return Boolean(routeRender?.isAsync || routeRender?.render.constructor.name === "AsyncFunction");
|
|
413
|
+
}
|
|
414
|
+
|
|
411
415
|
function composeLoadingFallback(renders: RouteRender[], params: Record<string, string>): ReactNode {
|
|
412
416
|
let element: ReactNode = null;
|
|
413
417
|
for (let i = renders.length - 1; i >= 0; i--) {
|
package/webkit/bootCsr.tsx
CHANGED
|
@@ -30,6 +30,8 @@ import { useCsrValues } from "./useCsrValues";
|
|
|
30
30
|
import { useFetch } from "./useFetch";
|
|
31
31
|
|
|
32
32
|
type RouteModuleWithConfig = RouteModule & { pageConfig?: PageConfig };
|
|
33
|
+
type CsrRouteModuleLoader = () => Promise<RouteModule>;
|
|
34
|
+
type CsrRouteModuleEntry = CsrRouteModuleLoader | { loader: CsrRouteModuleLoader; isAsyncDefault?: boolean };
|
|
33
35
|
|
|
34
36
|
declare global {
|
|
35
37
|
interface Window {
|
|
@@ -49,7 +51,7 @@ const RootRenderLayer = memo(({ renders, index, params, searchParams }: RootRend
|
|
|
49
51
|
<RootRenderLayer renders={renders} index={index + 1} params={params} searchParams={searchParams} />
|
|
50
52
|
);
|
|
51
53
|
const routeRender = renders[index];
|
|
52
|
-
const isAsyncRender = routeRender
|
|
54
|
+
const isAsyncRender = isAsyncRouteRender(routeRender);
|
|
53
55
|
const resultRef = useRef<ReactNode | Promise<ReactNode> | null>(null);
|
|
54
56
|
if (isAsyncRender && resultRef.current === null) {
|
|
55
57
|
resultRef.current = routeRender?.render({ children, params, searchParams } as never) ?? null;
|
|
@@ -61,6 +63,10 @@ const RootRenderLayer = memo(({ renders, index, params, searchParams }: RootRend
|
|
|
61
63
|
return Layout;
|
|
62
64
|
});
|
|
63
65
|
|
|
66
|
+
function isAsyncRouteRender(routeRender?: RouteRender): boolean {
|
|
67
|
+
return Boolean(routeRender?.isAsync || routeRender?.render.constructor.name === "AsyncFunction");
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
function composeLoadingFallback(renders: RouteRender[], params: Record<string, string>): ReactNode {
|
|
65
71
|
let element: ReactNode = null;
|
|
66
72
|
for (let i = renders.length - 1; i >= 0; i--) {
|
|
@@ -71,9 +77,10 @@ function composeLoadingFallback(renders: RouteRender[], params: Record<string, s
|
|
|
71
77
|
return element;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
export const bootCsr = async (context: Record<string,
|
|
80
|
+
export const bootCsr = async (context: Record<string, CsrRouteModuleEntry>) => {
|
|
75
81
|
const i18n = parseAkanI18nEnv();
|
|
76
82
|
window.document.body.style.overflow = "hidden";
|
|
83
|
+
initializeMobileTargetFromSearch();
|
|
77
84
|
const mobileBasePath = window.__AKAN_MOBILE_TARGET__?.basePath?.replace(/^\/+|\/+$/g, "");
|
|
78
85
|
const pathname = mobileBasePath && window.location.pathname === "/" ? `/${mobileBasePath}` : window.location.pathname;
|
|
79
86
|
if (pathname === "/404") return;
|
|
@@ -93,6 +100,7 @@ export const bootCsr = async (context: Record<string, () => Promise<RouteModule>
|
|
|
93
100
|
const otherBasePaths = basePaths?.filter((path) => path !== currentBasePath) ?? [];
|
|
94
101
|
|
|
95
102
|
const pages: { [key: string]: RouteModule } = {};
|
|
103
|
+
const asyncDefaultMap: { [key: string]: boolean | undefined } = {};
|
|
96
104
|
await Promise.all(
|
|
97
105
|
Object.entries(context).map(async ([key, value]) => {
|
|
98
106
|
const parsed = parseRouteModuleKey(key);
|
|
@@ -100,8 +108,10 @@ export const bootCsr = async (context: Record<string, () => Promise<RouteModule>
|
|
|
100
108
|
const pageBasePath = parsed.sourceRouteSegments.find((segment) => !/^\(.+\)$/.test(segment));
|
|
101
109
|
if (pageBasePath && otherBasePaths.includes(pageBasePath)) return;
|
|
102
110
|
}
|
|
103
|
-
const
|
|
111
|
+
const entry = typeof value === "function" ? { loader: value } : value;
|
|
112
|
+
const pageContent = await entry.loader();
|
|
104
113
|
validateRouteModuleExports(key, pageContent);
|
|
114
|
+
asyncDefaultMap[key] = entry.isAsyncDefault;
|
|
105
115
|
if (pageContent.default) pages[key] = pageContent;
|
|
106
116
|
}),
|
|
107
117
|
);
|
|
@@ -155,6 +165,7 @@ export const bootCsr = async (context: Record<string, () => Promise<RouteModule>
|
|
|
155
165
|
const layoutPage = parsed.kind === "layout" ? (page as LayoutModule) : null;
|
|
156
166
|
const routeRender: RouteRender = {
|
|
157
167
|
render: page.default as never,
|
|
168
|
+
isAsync: asyncDefaultMap[filePath] || page.default?.constructor.name === "AsyncFunction",
|
|
158
169
|
Loading: page.Loading as never,
|
|
159
170
|
NotFound: layoutPage?.NotFound,
|
|
160
171
|
Error: layoutPage?.Error,
|
|
@@ -257,6 +268,17 @@ export const bootCsr = async (context: Record<string, () => Promise<RouteModule>
|
|
|
257
268
|
root.render(<RouterProvider />);
|
|
258
269
|
};
|
|
259
270
|
|
|
271
|
+
function initializeMobileTargetFromSearch() {
|
|
272
|
+
if (window.__AKAN_MOBILE_TARGET__) return;
|
|
273
|
+
|
|
274
|
+
const params = new URLSearchParams(window.location.search);
|
|
275
|
+
const name = params.get("akanMobileTarget");
|
|
276
|
+
if (!name) return;
|
|
277
|
+
|
|
278
|
+
const basePath = params.get("akanMobileBasePath")?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
279
|
+
window.__AKAN_MOBILE_TARGET__ = { name, basePath };
|
|
280
|
+
}
|
|
281
|
+
|
|
260
282
|
function validateRouteModuleExports(key: string, mod: RouteModule) {
|
|
261
283
|
const parsed = parseRouteModuleKey(key);
|
|
262
284
|
const allowed =
|