akanjs 3.0.0-beta.26 → 3.0.0-beta.27
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/csrTypes.ts +2 -0
- package/client/device.ts +9 -9
- package/common/Logger.ts +26 -2
- package/common/RouteLayering.ts +52 -0
- package/common/index.ts +1 -0
- package/package.json +1 -1
- package/server/routeTreeBuilder.ts +14 -39
- package/types/client/csrTypes.d.ts +2 -0
- package/types/common/RouteLayering.d.ts +28 -0
- package/types/common/index.d.ts +1 -0
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +3 -1
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeBridge.kt +19 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeShell.swift +12 -2
- package/vendor/@akanjs/native/packages/core/src/runtime.ts +26 -5
- package/vendor/@akanjs/native/packages/desktop/src/host.ts +5 -3
- package/webkit/CsrRouteTable.ts +15 -35
- package/webkit/bootCsr.tsx +11 -8
package/client/csrTypes.ts
CHANGED
|
@@ -174,6 +174,8 @@ export interface Route {
|
|
|
174
174
|
path: string;
|
|
175
175
|
renderPage?: RouteRender;
|
|
176
176
|
renderLayout?: RouteRender;
|
|
177
|
+
/** `renderLayout` is a generated `__root_layout`: a root boundary the page generator found. */
|
|
178
|
+
isRootLayout?: boolean;
|
|
177
179
|
/** Synthetic layout render from a `_overrides.tsx` at this node; wraps the subtree in a UI-override provider. */
|
|
178
180
|
renderOverrides?: RouteRender;
|
|
179
181
|
pageIncludesOwnLayout?: boolean;
|
package/client/device.ts
CHANGED
|
@@ -15,14 +15,13 @@ let debugSeq = 0;
|
|
|
15
15
|
|
|
16
16
|
const isFrameDebugEnabled = () => {
|
|
17
17
|
if (typeof window === "undefined") return false;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
);
|
|
18
|
+
if (new URLSearchParams(window.location.search).get("akanFrameDebug") === "1") return true;
|
|
19
|
+
try {
|
|
20
|
+
return window.localStorage.getItem("akan:debug:frame") === "1";
|
|
21
|
+
} catch {
|
|
22
|
+
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
export function debugFrame(event: string, payload: DebugPayload = {}) {
|
|
@@ -33,7 +32,8 @@ export function debugFrame(event: string, payload: DebugPayload = {}) {
|
|
|
33
32
|
now: Math.round(performance.now()),
|
|
34
33
|
...payload,
|
|
35
34
|
};
|
|
36
|
-
|
|
35
|
+
|
|
36
|
+
console.debug(`[akan:frame:${debugSessionId}:${debugSeq}] ${event}`, details);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
interface DeviceInitOption {
|
package/common/Logger.ts
CHANGED
|
@@ -77,6 +77,15 @@ const colorizeMap: { [key in LogLevel]: (text: string) => string } = {
|
|
|
77
77
|
error: clc.red,
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
+
const consoleMethodMap = {
|
|
81
|
+
trace: "debug",
|
|
82
|
+
verbose: "debug",
|
|
83
|
+
debug: "debug",
|
|
84
|
+
info: "info",
|
|
85
|
+
warn: "warn",
|
|
86
|
+
error: "error",
|
|
87
|
+
} as const satisfies { [key in LogLevel]: "debug" | "info" | "warn" | "error" };
|
|
88
|
+
|
|
80
89
|
const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
|
|
81
90
|
|
|
82
91
|
const redactedAttrKey = /password|passwd|token|jwt|authorization|cookie|secret|api[-_]?key|private[-_]?key/i;
|
|
@@ -332,7 +341,22 @@ export class Logger {
|
|
|
332
341
|
if (!Logger.consoleOutput) return;
|
|
333
342
|
if (typeof window === "undefined")
|
|
334
343
|
(process[entry.stream] as unknown as NodeJS.WriteStream | undefined)?.write(entry.message);
|
|
335
|
-
else
|
|
344
|
+
else Logger.#writeBrowserConsole(entry.record, entry.message);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
static #writeBrowserConsole(record: LogRecord, rendered: string) {
|
|
348
|
+
const level = record.level ?? "info";
|
|
349
|
+
const context = record.context ? `[${record.context}] ` : "";
|
|
350
|
+
const attrs = record.attrs ? ` ${Logger.formatAttrs(record.attrs)}` : "";
|
|
351
|
+
const text = Logger.#inNativeShell()
|
|
352
|
+
? `[${record.name}] ${context}${record.message}${attrs}`
|
|
353
|
+
: rendered.replace(/\n+$/, "");
|
|
354
|
+
|
|
355
|
+
console[consoleMethodMap[level]](text);
|
|
356
|
+
}
|
|
357
|
+
static #inNativeShell() {
|
|
358
|
+
const platform = (globalThis as { __AKAN_NATIVE__?: { platform?: string } }).__AKAN_NATIVE__?.platform;
|
|
359
|
+
return platform !== undefined && platform !== "web";
|
|
336
360
|
}
|
|
337
361
|
static #write(name: string | undefined, message: string, context: string, logLevel: LogLevel, attrs?: LogAttrs) {
|
|
338
362
|
const stream = logLevel === "error" ? "stderr" : "stdout";
|
|
@@ -367,7 +391,7 @@ export class Logger {
|
|
|
367
391
|
if (!Logger.consoleOutput) return;
|
|
368
392
|
if (typeof window === "undefined" && method !== "console" && (process as unknown as NodeJS.Process | undefined))
|
|
369
393
|
process[stream].write(msg);
|
|
370
|
-
else console[outputStream === "error" ? "error" : "log"](msg.trim());
|
|
394
|
+
else console[outputStream === "error" ? "error" : Logger.#inNativeShell() ? "debug" : "log"](msg.trim());
|
|
371
395
|
}
|
|
372
396
|
static {
|
|
373
397
|
if (envValue("AKAN_PUBLIC_LOG_LEVEL") === "log" && typeof window === "undefined")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
interface LayeredRoute<Render> {
|
|
2
|
+
/** The tree segment: `/`, `/:lang`, `/(group)`, `/foo`. */
|
|
3
|
+
path: string;
|
|
4
|
+
renderLayout?: Render;
|
|
5
|
+
/** A generated `__root_layout`: one of the root boundaries the page generator found. */
|
|
6
|
+
isRootLayout?: boolean;
|
|
7
|
+
renderOverrides?: Render;
|
|
8
|
+
/** `false` for a named page file, which sits beside its directory's layout rather than under it. */
|
|
9
|
+
pageIncludesOwnLayout?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RouteLayer<Render> {
|
|
13
|
+
/** The URL pattern down to this node; a route group adds nothing to it. */
|
|
14
|
+
path: string;
|
|
15
|
+
pathSegments: string[];
|
|
16
|
+
isRoot: boolean;
|
|
17
|
+
/** The root layout above or at this node, overrides left out: once there is one, nothing below is a root. */
|
|
18
|
+
rootLayouts: Render[];
|
|
19
|
+
renderRootLayouts: Render[];
|
|
20
|
+
renderLayouts: Render[];
|
|
21
|
+
/** What a page file at this node renders inside: without this node's layout for a named page file. */
|
|
22
|
+
pageRenderRootLayouts: Render[];
|
|
23
|
+
pageRenderLayouts: Render[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class RouteLayering {
|
|
27
|
+
static readonly #group = /^\/\(.*\)$/;
|
|
28
|
+
|
|
29
|
+
static of<Render>(route: LayeredRoute<Render>, parent: RouteLayer<Render> | null): RouteLayer<Render> {
|
|
30
|
+
const parentSegments = parent?.pathSegments ?? [];
|
|
31
|
+
const parentRootLayouts = parent?.rootLayouts ?? [];
|
|
32
|
+
const parentRootRenders = parent?.renderRootLayouts ?? [];
|
|
33
|
+
const parentLayouts = parent?.renderLayouts ?? [];
|
|
34
|
+
const segment = RouteLayering.#group.test(route.path) ? "" : route.path;
|
|
35
|
+
const path = parentSegments.filter((part) => part !== "/").join("") + segment;
|
|
36
|
+
const isRoot = parentRootLayouts.length === 0 && (route.isRootLayout === true || !route.renderLayout);
|
|
37
|
+
const overrideRenders = route.renderOverrides ? [route.renderOverrides] : [];
|
|
38
|
+
|
|
39
|
+
const nodeRenders = [...overrideRenders, ...(route.renderLayout ? [route.renderLayout] : [])];
|
|
40
|
+
const pageNodeRenders = route.pageIncludesOwnLayout === false ? overrideRenders : nodeRenders;
|
|
41
|
+
return {
|
|
42
|
+
path,
|
|
43
|
+
pathSegments: [...parentSegments, ...(segment ? [segment] : [])],
|
|
44
|
+
isRoot,
|
|
45
|
+
rootLayouts: isRoot && route.renderLayout ? [...parentRootLayouts, route.renderLayout] : parentRootLayouts,
|
|
46
|
+
renderRootLayouts: isRoot ? [...parentRootRenders, ...nodeRenders] : parentRootRenders,
|
|
47
|
+
renderLayouts: isRoot ? parentLayouts : [...parentLayouts, ...nodeRenders],
|
|
48
|
+
pageRenderRootLayouts: isRoot ? [...parentRootRenders, ...pageNodeRenders] : parentRootRenders,
|
|
49
|
+
pageRenderLayouts: isRoot ? parentLayouts : [...parentLayouts, ...pageNodeRenders],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
package/common/index.ts
CHANGED
|
@@ -128,6 +128,7 @@ export {
|
|
|
128
128
|
export { deepObjectify, objectify, plainFieldsOf } from "./objectify";
|
|
129
129
|
export { pathGet, pathGetLoose, pathSet, toPathSegments } from "./objectPath";
|
|
130
130
|
export { formatPhone, isPhoneNumber } from "./phone";
|
|
131
|
+
export { type RouteLayer, RouteLayering } from "./RouteLayering";
|
|
131
132
|
export { randomPick, randomPicks } from "./randomPick";
|
|
132
133
|
export { hostFromRequest, isJsonContentType, originFromRequest } from "./requestOrigin";
|
|
133
134
|
export { RestClient, type RestClientOptions, type RestRequestOptions } from "./restClient";
|
package/package.json
CHANGED
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
getRouteExports,
|
|
16
16
|
Logger,
|
|
17
17
|
matchRoutePattern,
|
|
18
|
-
parseBasePaths,
|
|
19
18
|
parseRouteModuleKey,
|
|
19
|
+
type RouteLayer,
|
|
20
|
+
RouteLayering,
|
|
20
21
|
routeSegmentToTreePath,
|
|
21
22
|
} from "akanjs/common";
|
|
22
23
|
import { createElement } from "react";
|
|
@@ -54,15 +55,12 @@ export class RouteTreeBuilder {
|
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
readonly #context: PagesContext;
|
|
57
|
-
readonly #baseLayoutPaths: string[];
|
|
58
58
|
readonly #routeMap = new Map<string, Route>();
|
|
59
59
|
readonly #pagePatterns: { key: string; pattern: string }[] = [];
|
|
60
60
|
readonly #fallbackRoutes: LayoutFallbackRoute[] = [];
|
|
61
61
|
|
|
62
62
|
constructor(context: PagesContext) {
|
|
63
63
|
this.#context = context;
|
|
64
|
-
const basePaths = process.env.AKAN_PUBLIC_BASE_PATHS ? parseBasePaths(process.env.AKAN_PUBLIC_BASE_PATHS) : null;
|
|
65
|
-
this.#baseLayoutPaths = ["/", "/:lang", ...(basePaths?.map((bp) => `/:lang/${bp}`) ?? [])];
|
|
66
64
|
this.#routeMap.set("/", { path: "/", children: new Map() });
|
|
67
65
|
}
|
|
68
66
|
|
|
@@ -169,7 +167,7 @@ export class RouteTreeBuilder {
|
|
|
169
167
|
targetRouteMap.set(targetPath, {
|
|
170
168
|
...(targetRouteMap.get(targetPath) ?? { path: targetPath, children: new Map<string, Route>() }),
|
|
171
169
|
...(parsed.kind === "layout"
|
|
172
|
-
? { renderLayout: routeRender }
|
|
170
|
+
? { renderLayout: routeRender, isRootLayout: parsed.isInternalRootLayout }
|
|
173
171
|
: {
|
|
174
172
|
renderPage: routeRender,
|
|
175
173
|
pageIncludesOwnLayout: parsed.leaf === "_index",
|
|
@@ -178,48 +176,27 @@ export class RouteTreeBuilder {
|
|
|
178
176
|
} as Route);
|
|
179
177
|
}
|
|
180
178
|
|
|
181
|
-
#getPathRoutes(
|
|
182
|
-
route
|
|
183
|
-
parentRootLayouts: RouteRender[] = [],
|
|
184
|
-
parentLayouts: RouteRender[] = [],
|
|
185
|
-
parentPaths: string[] = [],
|
|
186
|
-
parentHead?: ResolveHead,
|
|
187
|
-
parentRootRenders: RouteRender[] = [],
|
|
188
|
-
): PathRoute[] {
|
|
189
|
-
const parentPath = parentPaths.filter((p) => p !== "/").join("");
|
|
190
|
-
const currentPathSegment = /^\/\(.*\)$/.test(route.path) ? "" : route.path;
|
|
191
|
-
const isRoot = this.#baseLayoutPaths.includes(parentPath + currentPathSegment) && parentRootLayouts.length < 2;
|
|
192
|
-
const routePath = parentPath + currentPathSegment;
|
|
193
|
-
const pathSegments = [...parentPaths, ...(currentPathSegment ? [currentPathSegment] : [])];
|
|
194
|
-
const currentRootLayout = isRoot && route.renderLayout ? route.renderLayout : null;
|
|
195
|
-
|
|
196
|
-
const currentOverrideRenders = route.renderOverrides ? [route.renderOverrides] : [];
|
|
197
|
-
const nodeRenders = [...currentOverrideRenders, ...(route.renderLayout ? [route.renderLayout] : [])];
|
|
198
|
-
const rootLayoutStack = [...parentRootLayouts, ...(currentRootLayout ? [currentRootLayout] : [])];
|
|
199
|
-
const renderRootLayouts = isRoot ? [...parentRootRenders, ...nodeRenders] : parentRootRenders;
|
|
200
|
-
const renderLayouts = isRoot ? parentLayouts : [...parentLayouts, ...nodeRenders];
|
|
179
|
+
#getPathRoutes(route: Route, parent: RouteLayer<RouteRender> | null = null, parentHead?: ResolveHead): PathRoute[] {
|
|
180
|
+
const layer = RouteLayering.of(route, parent);
|
|
201
181
|
if (route.renderLayout) {
|
|
202
182
|
this.#fallbackRoutes.push({
|
|
203
|
-
path:
|
|
204
|
-
pathSegments,
|
|
205
|
-
renderRootLayouts,
|
|
206
|
-
renderLayouts,
|
|
183
|
+
path: layer.path,
|
|
184
|
+
pathSegments: layer.pathSegments,
|
|
185
|
+
renderRootLayouts: layer.renderRootLayouts,
|
|
186
|
+
renderLayouts: layer.renderLayouts,
|
|
207
187
|
});
|
|
208
188
|
}
|
|
209
189
|
const routeHead = RouteTreeBuilder.#composeHeadResolvers(route.renderLayout?.resolveHead, parentHead);
|
|
210
|
-
const pageNodeRenders = route.pageIncludesOwnLayout === false ? currentOverrideRenders : nodeRenders;
|
|
211
|
-
const pageRenderRootLayouts = isRoot ? [...parentRootRenders, ...pageNodeRenders] : parentRootRenders;
|
|
212
|
-
const pageRenderLayouts = isRoot ? parentLayouts : [...parentLayouts, ...pageNodeRenders];
|
|
213
190
|
const pageHead = route.pageIncludesOwnLayout === false ? parentHead : routeHead;
|
|
214
191
|
return [
|
|
215
192
|
...(route.renderPage
|
|
216
193
|
? [
|
|
217
194
|
{
|
|
218
|
-
path:
|
|
219
|
-
pathSegments,
|
|
195
|
+
path: layer.path,
|
|
196
|
+
pathSegments: layer.pathSegments,
|
|
220
197
|
renderPage: route.renderPage,
|
|
221
|
-
renderRootLayouts: pageRenderRootLayouts,
|
|
222
|
-
renderLayouts: pageRenderLayouts,
|
|
198
|
+
renderRootLayouts: layer.pageRenderRootLayouts,
|
|
199
|
+
renderLayouts: layer.pageRenderLayouts,
|
|
223
200
|
resolveHead: RouteTreeBuilder.#composeHeadResolvers(route.renderPage.resolveHead, pageHead),
|
|
224
201
|
isSpecialRoute: route.isSpecialRoute,
|
|
225
202
|
pageState: route.pageState ?? defaultPageState,
|
|
@@ -227,9 +204,7 @@ export class RouteTreeBuilder {
|
|
|
227
204
|
]
|
|
228
205
|
: []),
|
|
229
206
|
...(route.children.size
|
|
230
|
-
? [...route.children.values()].flatMap((child) =>
|
|
231
|
-
this.#getPathRoutes(child, rootLayoutStack, renderLayouts, pathSegments, routeHead, renderRootLayouts),
|
|
232
|
-
)
|
|
207
|
+
? [...route.children.values()].flatMap((child) => this.#getPathRoutes(child, layer, routeHead))
|
|
233
208
|
: []),
|
|
234
209
|
];
|
|
235
210
|
}
|
|
@@ -171,6 +171,8 @@ export interface Route {
|
|
|
171
171
|
path: string;
|
|
172
172
|
renderPage?: RouteRender;
|
|
173
173
|
renderLayout?: RouteRender;
|
|
174
|
+
/** `renderLayout` is a generated `__root_layout`: a root boundary the page generator found. */
|
|
175
|
+
isRootLayout?: boolean;
|
|
174
176
|
/** Synthetic layout render from a `_overrides.tsx` at this node; wraps the subtree in a UI-override provider. */
|
|
175
177
|
renderOverrides?: RouteRender;
|
|
176
178
|
pageIncludesOwnLayout?: boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
interface LayeredRoute<Render> {
|
|
2
|
+
/** The tree segment: `/`, `/:lang`, `/(group)`, `/foo`. */
|
|
3
|
+
path: string;
|
|
4
|
+
renderLayout?: Render;
|
|
5
|
+
/** A generated `__root_layout`: one of the root boundaries the page generator found. */
|
|
6
|
+
isRootLayout?: boolean;
|
|
7
|
+
renderOverrides?: Render;
|
|
8
|
+
/** `false` for a named page file, which sits beside its directory's layout rather than under it. */
|
|
9
|
+
pageIncludesOwnLayout?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface RouteLayer<Render> {
|
|
12
|
+
/** The URL pattern down to this node; a route group adds nothing to it. */
|
|
13
|
+
path: string;
|
|
14
|
+
pathSegments: string[];
|
|
15
|
+
isRoot: boolean;
|
|
16
|
+
/** The root layout above or at this node, overrides left out: once there is one, nothing below is a root. */
|
|
17
|
+
rootLayouts: Render[];
|
|
18
|
+
renderRootLayouts: Render[];
|
|
19
|
+
renderLayouts: Render[];
|
|
20
|
+
/** What a page file at this node renders inside: without this node's layout for a named page file. */
|
|
21
|
+
pageRenderRootLayouts: Render[];
|
|
22
|
+
pageRenderLayouts: Render[];
|
|
23
|
+
}
|
|
24
|
+
export declare class RouteLayering {
|
|
25
|
+
#private;
|
|
26
|
+
static of<Render>(route: LayeredRoute<Render>, parent: RouteLayer<Render> | null): RouteLayer<Render>;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
package/types/common/index.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export { isMcpDescribableArg, type McpExposureEndpoint, type McpExposureOption,
|
|
|
29
29
|
export { deepObjectify, objectify, plainFieldsOf } from "./objectify.d.ts";
|
|
30
30
|
export { pathGet, pathGetLoose, pathSet, toPathSegments } from "./objectPath.d.ts";
|
|
31
31
|
export { formatPhone, isPhoneNumber } from "./phone.d.ts";
|
|
32
|
+
export { type RouteLayer, RouteLayering } from "./RouteLayering.d.ts";
|
|
32
33
|
export { randomPick, randomPicks } from "./randomPick.d.ts";
|
|
33
34
|
export { hostFromRequest, isJsonContentType, originFromRequest } from "./requestOrigin.d.ts";
|
|
34
35
|
export { RestClient, type RestClientOptions, type RestRequestOptions } from "./restClient.d.ts";
|
|
@@ -396,13 +396,15 @@ class AkanNativeActivity : Activity(), AkanNativePluginContext.Host {
|
|
|
396
396
|
|
|
397
397
|
private inner class ChromeClient : WebChromeClient() {
|
|
398
398
|
override fun onConsoleMessage(message: ConsoleMessage): Boolean { // WV-3
|
|
399
|
+
// A dev page forwards its console through the bridge once its runtime is up; this copy covers the rest.
|
|
400
|
+
if (bridge.consoleForwarded) return true
|
|
399
401
|
val priority = when (message.messageLevel()) {
|
|
400
402
|
ConsoleMessage.MessageLevel.ERROR -> Log.ERROR
|
|
401
403
|
ConsoleMessage.MessageLevel.WARNING -> Log.WARN
|
|
402
404
|
ConsoleMessage.MessageLevel.DEBUG -> Log.DEBUG
|
|
403
405
|
else -> Log.INFO
|
|
404
406
|
}
|
|
405
|
-
Log.println(priority, "AkanNativeConsole", message.message())
|
|
407
|
+
Log.println(priority, "AkanNativeConsole", message.message().trimEnd('\n'))
|
|
406
408
|
return true
|
|
407
409
|
}
|
|
408
410
|
|
|
@@ -53,6 +53,8 @@ class AkanNativeBridge(private val webView: WebView) {
|
|
|
53
53
|
|
|
54
54
|
/** Dev builds: the self-test's \$host methods. */
|
|
55
55
|
var dev = false
|
|
56
|
+
/** The page's runtime forwards its console here (a dev build's `$console` attach), so the WebView's copy is spare. */
|
|
57
|
+
@Volatile var consoleForwarded = false
|
|
56
58
|
|
|
57
59
|
/** Dev builds: \$host.hang calls still open, and how many were cancelled. */
|
|
58
60
|
private val hanging = ArrayList<AkanNativeCall>()
|
|
@@ -207,6 +209,7 @@ class AkanNativeBridge(private val webView: WebView) {
|
|
|
207
209
|
if (pluginId == "\$bridge") return bridgeOp(call, doc)
|
|
208
210
|
doc.running[id] = java.lang.ref.WeakReference(call)
|
|
209
211
|
call.document = doc
|
|
212
|
+
if (pluginId == "\$console") return console(call)
|
|
210
213
|
if (pluginId == "\$host") return host(call)
|
|
211
214
|
val plugin = plugins[pluginId] ?: return call.reject(AkanNativeErrorCode.NOT_FOUND, "plugin $pluginId is not registered")
|
|
212
215
|
// L2: an undeclared method or event never reaches the plugin, whatever its code handles.
|
|
@@ -333,6 +336,22 @@ class AkanNativeBridge(private val webView: WebView) {
|
|
|
333
336
|
JSONObject().put("v", 1).put("id", id).put("ok", false).put("error", JSONObject().put("code", code).put("message", message))
|
|
334
337
|
|
|
335
338
|
/** Shell built-ins the page runtime calls. Dev builds only: the self-test's bridge checks. */
|
|
339
|
+
/** dev builds: the page console at its own priority (WV-3), through the bridge as on iOS and the desktop. */
|
|
340
|
+
private fun console(call: AkanNativeCall) {
|
|
341
|
+
if (!dev) return call.reject(AkanNativeErrorCode.NOT_FOUND, "plugin \$console is not registered")
|
|
342
|
+
consoleForwarded = true
|
|
343
|
+
if (call.method != "attach") {
|
|
344
|
+
val priority = when (call.method) {
|
|
345
|
+
"error" -> Log.ERROR
|
|
346
|
+
"warn" -> Log.WARN
|
|
347
|
+
"trace", "verbose", "debug" -> Log.DEBUG
|
|
348
|
+
else -> Log.INFO
|
|
349
|
+
}
|
|
350
|
+
Log.println(priority, "AkanNativeConsole", (call.string("message") ?: "").trimEnd('\n'))
|
|
351
|
+
}
|
|
352
|
+
call.resolve()
|
|
353
|
+
}
|
|
354
|
+
|
|
336
355
|
private fun host(call: AkanNativeCall) {
|
|
337
356
|
// AkanNativeFeatures.DEV is a constant: release builds (false) lose these methods and what only they
|
|
338
357
|
// reach (AkanNativeVectors) in R8 (architecture review stage 6).
|
|
@@ -13,10 +13,20 @@ enum AkanNativeLog {
|
|
|
13
13
|
logger.info("\(text, privacy: .public)")
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/// One `[page<+> <level>]` tag per line, the desktop host's grammar, which the akan CLI reads the level back from;
|
|
17
|
+
/// os_log takes the page's level too, so `log stream` and Console.app filter on it.
|
|
16
18
|
static func page(_ level: String, _ text: String) {
|
|
17
|
-
|
|
19
|
+
let lines = text.replacingOccurrences(of: "\\n+$", with: "", options: .regularExpression)
|
|
20
|
+
.components(separatedBy: "\n")
|
|
21
|
+
print(lines.enumerated().map { "[page\($0.offset == 0 ? "" : "+") \(level)] \($0.element)" }.joined(separator: "\n"))
|
|
18
22
|
fflush(stdout)
|
|
19
|
-
|
|
23
|
+
let message = lines.joined(separator: "\n")
|
|
24
|
+
switch level {
|
|
25
|
+
case "error": logger.error("\(message, privacy: .public)")
|
|
26
|
+
case "warn": logger.warning("\(message, privacy: .public)")
|
|
27
|
+
case "trace", "verbose", "debug": logger.debug("\(message, privacy: .public)")
|
|
28
|
+
default: logger.info("\(message, privacy: .public)")
|
|
29
|
+
}
|
|
20
30
|
}
|
|
21
31
|
}
|
|
22
32
|
|
|
@@ -93,7 +93,7 @@ export function runtime(): Runtime {
|
|
|
93
93
|
};
|
|
94
94
|
if (rt.transport) w.addEventListener?.("pageshow", (e) => e.persisted && w.location?.reload());
|
|
95
95
|
|
|
96
|
-
if (boot.dev && rt.transport
|
|
96
|
+
if (boot.dev && rt.transport) forwardConsole(boot.platform);
|
|
97
97
|
}
|
|
98
98
|
return g.__runtime as Runtime;
|
|
99
99
|
}
|
|
@@ -102,7 +102,7 @@ export function runtime(): Runtime {
|
|
|
102
102
|
* Dev builds on native hosts: mirror console output and uncaught errors to the host,
|
|
103
103
|
* which prints them next to its own logs (WV-3).
|
|
104
104
|
*/
|
|
105
|
-
function forwardConsole(
|
|
105
|
+
function forwardConsole(platform: AkanNativeBoot["platform"]): void {
|
|
106
106
|
|
|
107
107
|
const g = akanNativeGlobal() as { __consolePatched?: boolean };
|
|
108
108
|
if (g.__consolePatched) return;
|
|
@@ -110,13 +110,32 @@ function forwardConsole(_rt: Runtime): void {
|
|
|
110
110
|
const c = globalThis.console as unknown as Record<string, (...args: unknown[]) => void>;
|
|
111
111
|
const format = (value: unknown): string => {
|
|
112
112
|
if (typeof value === "string") return value;
|
|
113
|
-
|
|
113
|
+
|
|
114
|
+
if (value instanceof Error) {
|
|
115
|
+
const head = `${value.name}: ${value.message}`;
|
|
116
|
+
return value.stack?.startsWith(head) ? value.stack : `${head}${value.stack ? `\n${value.stack}` : ""}`;
|
|
117
|
+
}
|
|
114
118
|
try {
|
|
115
119
|
return JSON.stringify(value) ?? String(value);
|
|
116
120
|
} catch {
|
|
117
121
|
return String(value);
|
|
118
122
|
}
|
|
119
123
|
};
|
|
124
|
+
|
|
125
|
+
const render = (args: unknown[]): string => {
|
|
126
|
+
const [first, ...rest] = args;
|
|
127
|
+
if (typeof first !== "string" || !/%[sdifoOc]/.test(first)) return args.map(format).join(" ");
|
|
128
|
+
const text = first.replace(/%([sdifoOc%])/g, (match, spec: string) => {
|
|
129
|
+
if (spec === "%") return "%";
|
|
130
|
+
if (rest.length === 0) return match;
|
|
131
|
+
const value = rest.shift();
|
|
132
|
+
if (spec === "c") return "";
|
|
133
|
+
if (spec === "d" || spec === "i") return String(Number.parseInt(String(value), 10));
|
|
134
|
+
if (spec === "f") return String(Number(value));
|
|
135
|
+
return format(value);
|
|
136
|
+
});
|
|
137
|
+
return [text, ...rest.map(format)].join(" ");
|
|
138
|
+
};
|
|
120
139
|
const send = (level: string, args: unknown[]) => {
|
|
121
140
|
const rt = akanNativeGlobal().__runtime as Runtime | undefined;
|
|
122
141
|
if (!rt?.transport || !rt.boot.dev) return;
|
|
@@ -125,10 +144,12 @@ function forwardConsole(_rt: Runtime): void {
|
|
|
125
144
|
id: ++rt.nextId,
|
|
126
145
|
plugin: CONSOLE_PLUGIN,
|
|
127
146
|
method: level,
|
|
128
|
-
args: { message: args
|
|
147
|
+
args: { message: render(args).replace(/\n+$/, "") },
|
|
129
148
|
};
|
|
130
149
|
rt.transport.send(request).catch(() => {});
|
|
131
150
|
};
|
|
151
|
+
|
|
152
|
+
if (platform === "android") send("attach", []);
|
|
132
153
|
for (const level of ["log", "info", "warn", "error", "debug"]) {
|
|
133
154
|
const original = c[level]!.bind(globalThis.console);
|
|
134
155
|
c[level] = (...args: unknown[]) => {
|
|
@@ -137,7 +158,7 @@ function forwardConsole(_rt: Runtime): void {
|
|
|
137
158
|
};
|
|
138
159
|
}
|
|
139
160
|
const w = globalThis as { addEventListener?: (type: string, fn: (e: any) => void) => void };
|
|
140
|
-
w.addEventListener?.("error", (e) => send("error", [
|
|
161
|
+
w.addEventListener?.("error", (e) => send("error", ["uncaught:", e.error ?? e.message]));
|
|
141
162
|
w.addEventListener?.("unhandledrejection", (e) => send("error", ["unhandled rejection:", e.reason]));
|
|
142
163
|
}
|
|
143
164
|
|
|
@@ -132,10 +132,12 @@ export function startHost(plugins: DesktopPlugin[]): void {
|
|
|
132
132
|
emitJs(window, `window.__AKAN_NATIVE__&&window.__AKAN_NATIVE__.receive(${JSON.stringify(message)})`);
|
|
133
133
|
},
|
|
134
134
|
builtins: {
|
|
135
|
-
|
|
135
|
+
|
|
136
136
|
[CONSOLE_PLUGIN](req, window) {
|
|
137
|
-
const
|
|
138
|
-
|
|
137
|
+
const message = (req.args as { message?: string } | undefined)?.message ?? "";
|
|
138
|
+
const scope = window === 1 ? "" : `#${window}`;
|
|
139
|
+
const lines = message.replace(/\n+$/, "").split("\n");
|
|
140
|
+
console.info(lines.map((line, i) => `[page${i === 0 ? "" : "+"}${scope} ${req.method}] ${line}`).join("\n"));
|
|
139
141
|
},
|
|
140
142
|
|
|
141
143
|
async [HOST_PLUGIN](req, window, signal) {
|
package/webkit/CsrRouteTable.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
getRouteExports,
|
|
20
20
|
Logger,
|
|
21
21
|
parseRouteModuleKey,
|
|
22
|
+
type RouteLayer,
|
|
23
|
+
RouteLayering,
|
|
22
24
|
routeSegmentToTreePath,
|
|
23
25
|
} from "akanjs/common";
|
|
24
26
|
|
|
@@ -94,7 +96,6 @@ export class CsrRouteTable {
|
|
|
94
96
|
|
|
95
97
|
async #build(context: CsrRouteContext): Promise<CsrRoutes> {
|
|
96
98
|
const { device, basePaths, currentBasePath } = this.#options;
|
|
97
|
-
const baseLayoutPaths = ["/", "/:lang", ...(currentBasePath ? [`/:lang/${currentBasePath}`] : [])];
|
|
98
99
|
const otherBasePaths = basePaths?.filter((path) => path !== currentBasePath) ?? [];
|
|
99
100
|
const pages: { [key: string]: RouteModule } = {};
|
|
100
101
|
const asyncDefaultMap: { [key: string]: boolean | undefined } = {};
|
|
@@ -168,7 +169,11 @@ export class CsrRouteTable {
|
|
|
168
169
|
targetRouteMap.set(targetPath, {
|
|
169
170
|
...(targetRouteMap.get(targetPath) ?? { path: targetPath, children: new Map<string, Route>() }),
|
|
170
171
|
...(parsed.kind === "layout"
|
|
171
|
-
? {
|
|
172
|
+
? {
|
|
173
|
+
renderLayout: routeRender,
|
|
174
|
+
isRootLayout: parsed.isInternalRootLayout,
|
|
175
|
+
layoutPageConfig: (page as RouteModuleWithConfig).pageConfig,
|
|
176
|
+
}
|
|
172
177
|
: {
|
|
173
178
|
renderPage: routeRender,
|
|
174
179
|
pageIncludesOwnLayout: parsed.leaf === "_index",
|
|
@@ -181,38 +186,15 @@ export class CsrRouteTable {
|
|
|
181
186
|
assertUniqueRoutePatterns(pagePatterns);
|
|
182
187
|
const getPathRoutes = (
|
|
183
188
|
route: Route,
|
|
184
|
-
|
|
185
|
-
parentLayouts: RouteRender[] = [],
|
|
186
|
-
parentPaths: string[] = [],
|
|
189
|
+
parent: RouteLayer<RouteRender> | null = null,
|
|
187
190
|
parentPageConfigChain: PageConfig[] = [],
|
|
188
|
-
parentRootRenders: RouteRender[] = [],
|
|
189
191
|
): PathRoute[] => {
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const currentPathSegment = isRouteGroup ? "" : route.path;
|
|
193
|
-
const path = parentPath + currentPathSegment;
|
|
194
|
-
const isRoot = !isRouteGroup && baseLayoutPaths.includes(path);
|
|
195
|
-
const pathSegments = [...parentPaths, ...(currentPathSegment ? [currentPathSegment] : [])];
|
|
196
|
-
const currentRootLayout = isRoot && route.renderLayout ? route.renderLayout : null;
|
|
197
|
-
const currentLayout = !isRoot && route.renderLayout ? route.renderLayout : null;
|
|
192
|
+
const layer = RouteLayering.of(route, parent);
|
|
193
|
+
const { path, pathSegments } = layer;
|
|
198
194
|
const currentLayoutConfig = route.renderLayout && route.layoutPageConfig ? route.layoutPageConfig : null;
|
|
199
|
-
|
|
200
|
-
const currentOverrideRenders = route.renderOverrides ? [route.renderOverrides] : [];
|
|
201
|
-
const nodeRenders = [...currentOverrideRenders, ...(route.renderLayout ? [route.renderLayout] : [])];
|
|
202
|
-
const rootLayoutStack = [...parentRootLayouts, ...(currentRootLayout ? [currentRootLayout] : [])];
|
|
203
|
-
const renderRootLayouts = isRoot ? [...parentRootRenders, ...nodeRenders] : parentRootRenders;
|
|
204
|
-
const renderLayouts = isRoot ? parentLayouts : [...parentLayouts, ...nodeRenders];
|
|
205
|
-
const pageConfigChain = [
|
|
206
|
-
...parentPageConfigChain,
|
|
207
|
-
...(currentRootLayout || currentLayout ? (currentLayoutConfig ? [currentLayoutConfig] : []) : []),
|
|
208
|
-
];
|
|
209
|
-
const pageNodeRenders = route.pageIncludesOwnLayout === false ? currentOverrideRenders : nodeRenders;
|
|
210
|
-
const pageRenderRootLayouts = isRoot ? [...parentRootRenders, ...pageNodeRenders] : parentRootRenders;
|
|
211
|
-
const pageRenderLayouts = isRoot ? parentLayouts : [...parentLayouts, ...pageNodeRenders];
|
|
195
|
+
const pageConfigChain = [...parentPageConfigChain, ...(currentLayoutConfig ? [currentLayoutConfig] : [])];
|
|
212
196
|
const pageRenderConfigChain =
|
|
213
|
-
route.pageIncludesOwnLayout === false &&
|
|
214
|
-
? parentPageConfigChain
|
|
215
|
-
: pageConfigChain;
|
|
197
|
+
route.pageIncludesOwnLayout === false && route.renderLayout ? parentPageConfigChain : pageConfigChain;
|
|
216
198
|
const ownPageConfig = route.renderPage && route.pageConfig ? route.pageConfig : null;
|
|
217
199
|
const finalPageConfigChain = [...pageRenderConfigChain, ...(ownPageConfig ? [ownPageConfig] : [])];
|
|
218
200
|
const pageState = resolvePageState({
|
|
@@ -230,8 +212,8 @@ export class CsrRouteTable {
|
|
|
230
212
|
path,
|
|
231
213
|
pathSegments,
|
|
232
214
|
renderPage: route.renderPage,
|
|
233
|
-
renderRootLayouts: pageRenderRootLayouts,
|
|
234
|
-
renderLayouts: pageRenderLayouts,
|
|
215
|
+
renderRootLayouts: layer.pageRenderRootLayouts,
|
|
216
|
+
renderLayouts: layer.pageRenderLayouts,
|
|
235
217
|
isSpecialRoute: route.isSpecialRoute,
|
|
236
218
|
pageState: route.pageState ?? pageState,
|
|
237
219
|
pageConfigChain: finalPageConfigChain,
|
|
@@ -240,9 +222,7 @@ export class CsrRouteTable {
|
|
|
240
222
|
]
|
|
241
223
|
: []),
|
|
242
224
|
...(route.children.size
|
|
243
|
-
? [...route.children.values()].flatMap((child) =>
|
|
244
|
-
getPathRoutes(child, rootLayoutStack, renderLayouts, pathSegments, pageConfigChain, renderRootLayouts),
|
|
245
|
-
)
|
|
225
|
+
? [...route.children.values()].flatMap((child) => getPathRoutes(child, layer, pageConfigChain))
|
|
246
226
|
: []),
|
|
247
227
|
];
|
|
248
228
|
};
|
package/webkit/bootCsr.tsx
CHANGED
|
@@ -45,16 +45,19 @@ export const bootCsr = async (context: CsrRouteContext) => {
|
|
|
45
45
|
const { pathRoutes, routeGuide } = useSyncExternalStore(table.subscribe, table.snapshot);
|
|
46
46
|
const csrValues = useCsrValues(routeGuide, pathRoutes);
|
|
47
47
|
const { location } = csrValues;
|
|
48
|
+
|
|
49
|
+
if (location.pathRoute.renderRootLayouts.length === 0)
|
|
50
|
+
throw new Error(
|
|
51
|
+
`[csr] no root layout for ${location.pathRoute.path}: the route table put none of its layouts at the root`,
|
|
52
|
+
);
|
|
48
53
|
return (
|
|
49
54
|
<csrContext.Provider value={csrValues}>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
/>
|
|
57
|
-
) : null}
|
|
55
|
+
<RenderLayer
|
|
56
|
+
renders={location.pathRoute.renderRootLayouts}
|
|
57
|
+
index={0}
|
|
58
|
+
params={location.params}
|
|
59
|
+
searchParams={location.searchParams}
|
|
60
|
+
/>
|
|
58
61
|
</csrContext.Provider>
|
|
59
62
|
);
|
|
60
63
|
};
|