akanjs 2.0.5 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/cli/application/application.command.ts +4 -1
- package/cli/application/application.runner.ts +6 -8
- package/cli/build.ts +3 -1
- package/cli/cloud/cloud.runner.ts +7 -8
- package/cli/index.js +288 -115
- package/cli/library/library.runner.ts +2 -2
- package/cli/module/module.runner.ts +2 -2
- package/cli/npmRegistry.ts +13 -0
- package/cli/openBrowser.ts +15 -0
- package/cli/pluralizeName.ts +5 -0
- package/cli/scalar/scalar.prompt.ts +2 -2
- package/cli/scalar/scalar.runner.ts +2 -2
- package/cli/semver.ts +18 -0
- package/cli/templates/lib/sig.ts +2 -2
- package/cli/workspace/workspace.runner.ts +3 -3
- package/client/cookie.ts +10 -15
- package/common/index.ts +1 -0
- package/common/jwtDecode.ts +17 -0
- package/constant/serialize.ts +1 -1
- package/devkit/akanApp/akanApp.host.ts +46 -9
- package/devkit/akanConfig/akanConfig.ts +2 -1
- package/devkit/capacitor.base.config.ts +18 -4
- package/devkit/capacitorApp.ts +118 -64
- package/devkit/incrementalBuilder/incrementalBuilder.host.ts +83 -9
- package/devkit/mobile/mobileTarget.ts +2 -1
- package/devkit/scanInfo.ts +1 -0
- package/document/dataLoader.ts +140 -6
- package/document/database.ts +1 -1
- package/package.json +7 -13
- package/server/akanApp.ts +250 -44
- package/server/di/diLifecycle.ts +1 -1
- package/server/processMetricsCollector.ts +79 -1
- package/server/proxy/localeWebProxy.ts +29 -12
- package/server/resolver/database.resolver.ts +82 -31
- package/server/resolver/signal.resolver.ts +67 -28
- package/service/ipcTypes.ts +5 -0
- package/service/predefinedAdaptor/database.adaptor.ts +95 -27
- package/service/predefinedAdaptor/solidSqlite.ts +7 -7
- package/service/predefinedAdaptor/storage.adaptor.ts +35 -9
- package/service/serviceModule.ts +1 -6
- package/signal/base.signal.ts +1 -1
- package/signal/index.ts +1 -0
- package/signal/middleware.ts +5 -1
- package/signal/signalContext.ts +85 -31
- package/signal/signalRegistry.ts +35 -10
- package/signal/trace.ts +279 -0
- package/types/cli/npmRegistry.d.ts +1 -0
- package/types/cli/openBrowser.d.ts +1 -0
- package/types/cli/pluralizeName.d.ts +1 -0
- package/types/cli/semver.d.ts +1 -0
- package/types/client/cookie.d.ts +6 -1
- package/types/common/index.d.ts +1 -0
- package/types/common/jwtDecode.d.ts +2 -0
- package/types/devkit/capacitorApp.d.ts +14 -5
- package/types/devkit/incrementalBuilder/incrementalBuilder.host.d.ts +9 -5
- package/types/document/dataLoader.d.ts +21 -2
- package/types/document/database.d.ts +1 -1
- package/types/server/processMetricsCollector.d.ts +2 -0
- package/types/service/ipcTypes.d.ts +5 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +26 -32
- package/types/service/predefinedAdaptor/solidSqlite.d.ts +3 -3
- package/types/service/predefinedAdaptor/storage.adaptor.d.ts +8 -2
- package/types/service/serviceModule.d.ts +1 -1
- package/types/signal/index.d.ts +1 -0
- package/types/signal/signalContext.d.ts +4 -1
- package/types/signal/signalRegistry.d.ts +25 -4
- package/types/signal/trace.d.ts +97 -0
- package/types/ui/Signal/style.d.ts +15 -0
- package/ui/Signal/Arg.tsx +22 -15
- package/ui/Signal/Doc.tsx +30 -24
- package/ui/Signal/Listener.tsx +15 -39
- package/ui/Signal/Message.tsx +32 -50
- package/ui/Signal/Object.tsx +16 -13
- package/ui/Signal/PubSub.tsx +29 -47
- package/ui/Signal/Response.tsx +7 -17
- package/ui/Signal/RestApi.tsx +41 -57
- package/ui/Signal/WebSocket.tsx +1 -1
- package/ui/Signal/style.ts +36 -0
- package/webkit/useCsrValues.ts +147 -37
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Lib, LibExecutor, runner, type Workspace } from "akanjs/devkit";
|
|
2
|
-
import {
|
|
2
|
+
import { compareSemver } from "../semver";
|
|
3
3
|
|
|
4
4
|
export class LibraryRunner extends runner("library") {
|
|
5
5
|
async createLibrary(libName: string, workspace: Workspace) {
|
|
@@ -44,7 +44,7 @@ export class LibraryRunner extends runner("library") {
|
|
|
44
44
|
Object.keys({ ...libDependencies, ...rootDependencies }).map((dep) => {
|
|
45
45
|
const libVersion = libDependencies[dep] ?? "0.0.0";
|
|
46
46
|
const rootVersion = rootDependencies[dep] ?? "0.0.0";
|
|
47
|
-
const newerVersion =
|
|
47
|
+
const newerVersion = compareSemver(rootVersion, libVersion) > 0 ? rootVersion : libVersion;
|
|
48
48
|
return [dep, newerVersion];
|
|
49
49
|
}),
|
|
50
50
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { capitalize } from "akanjs/common";
|
|
2
2
|
import { type Module, runner, type Workspace } from "akanjs/devkit";
|
|
3
|
-
import
|
|
3
|
+
import { pluralizeName } from "../pluralizeName";
|
|
4
4
|
|
|
5
5
|
export class ModuleRunner extends runner("module") {
|
|
6
6
|
async createModule(
|
|
@@ -30,7 +30,7 @@ export class ModuleRunner extends runner("module") {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
async createModuleTemplate(module: Module) {
|
|
33
|
-
const names =
|
|
33
|
+
const names = pluralizeName(module.name);
|
|
34
34
|
await module.applyTemplate({
|
|
35
35
|
basePath: `.`,
|
|
36
36
|
template: "module",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface NpmPackageMetadata {
|
|
2
|
+
"dist-tags"?: Record<string, string>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export async function getLatestPackageVersion(packageName: string, tag = "latest"): Promise<string> {
|
|
6
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName).replace(/^%40/, "@")}`;
|
|
7
|
+
const res = await fetch(url);
|
|
8
|
+
if (!res.ok) throw new Error(`Failed to fetch ${packageName} metadata from npm registry`);
|
|
9
|
+
const metadata = (await res.json()) as NpmPackageMetadata;
|
|
10
|
+
const version = metadata["dist-tags"]?.[tag];
|
|
11
|
+
if (!version) throw new Error(`No npm dist-tag "${tag}" found for ${packageName}`);
|
|
12
|
+
return version;
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function openBrowser(url: string): Promise<void> {
|
|
4
|
+
const command =
|
|
5
|
+
process.platform === "darwin"
|
|
6
|
+
? ["open", url]
|
|
7
|
+
: process.platform === "win32"
|
|
8
|
+
? ["cmd", "/c", "start", "", url]
|
|
9
|
+
: ["xdg-open", url];
|
|
10
|
+
const child = spawn(command[0], command.slice(1), { detached: true, stdio: "ignore" });
|
|
11
|
+
child.on("error", () => {
|
|
12
|
+
});
|
|
13
|
+
child.unref();
|
|
14
|
+
return Promise.resolve();
|
|
15
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { input } from "@inquirer/prompts";
|
|
2
2
|
import { type FileContent, Prompter, type Sys } from "akanjs/devkit";
|
|
3
|
-
import
|
|
3
|
+
import { pluralizeName } from "../pluralizeName";
|
|
4
4
|
|
|
5
5
|
export class ScalarPrompt extends Prompter {
|
|
6
6
|
constructor(
|
|
@@ -20,7 +20,7 @@ export class ScalarPrompt extends Prompter {
|
|
|
20
20
|
await this.sys.applyTemplate({
|
|
21
21
|
basePath: "./lib/__scalar",
|
|
22
22
|
template: "__scalar",
|
|
23
|
-
dict: { model: this.name, models:
|
|
23
|
+
dict: { model: this.name, models: pluralizeName(this.name), sysName: this.sys.name },
|
|
24
24
|
});
|
|
25
25
|
const boilerplate = await this.sys.readFile(`lib/__scalar/${this.name}/${this.name}.constant.ts`);
|
|
26
26
|
return await this.#requestConstant({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AiSession, runner, type Sys } from "akanjs/devkit";
|
|
2
|
-
import
|
|
2
|
+
import { pluralizeName } from "../pluralizeName";
|
|
3
3
|
|
|
4
4
|
import { ScalarPrompt } from "./scalar.prompt";
|
|
5
5
|
|
|
@@ -8,7 +8,7 @@ export class ScalarRunner extends runner("scalar") {
|
|
|
8
8
|
await sys.applyTemplate({
|
|
9
9
|
basePath: "./lib/__scalar",
|
|
10
10
|
template: "__scalar",
|
|
11
|
-
dict: { model: scalarName, models:
|
|
11
|
+
dict: { model: scalarName, models: pluralizeName(scalarName), sysName: sys.name },
|
|
12
12
|
overwrite: false,
|
|
13
13
|
});
|
|
14
14
|
}
|
package/cli/semver.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function parseVersion(version: string): number[] {
|
|
2
|
+
return version
|
|
3
|
+
.replace(/^[^\d]*/, "")
|
|
4
|
+
.split(/[.-]/)
|
|
5
|
+
.map((part) => Number.parseInt(part, 10))
|
|
6
|
+
.map((part) => (Number.isFinite(part) ? part : 0));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function compareSemver(a: string, b: string): number {
|
|
10
|
+
const left = parseVersion(a);
|
|
11
|
+
const right = parseVersion(b);
|
|
12
|
+
const length = Math.max(left.length, right.length);
|
|
13
|
+
for (let i = 0; i < length; i++) {
|
|
14
|
+
const diff = (left[i] ?? 0) - (right[i] ?? 0);
|
|
15
|
+
if (diff !== 0) return diff > 0 ? 1 : -1;
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
package/cli/templates/lib/sig.ts
CHANGED
|
@@ -50,8 +50,8 @@ ${[...scanInfo.service.entries()]
|
|
|
50
50
|
)
|
|
51
51
|
.join("\n")}
|
|
52
52
|
|
|
53
|
-
${databaseModules.map((module) => `export const ${module} = SignalRegistry.registerDatabase(${module}Sig.${capitalize(module)}Internal, ${module}Sig.${capitalize(module)}Endpoint, ${module}Sig.${capitalize(module)}Slice, ${capitalize(module)});`).join("\n")}
|
|
54
|
-
${serviceModules.map((module) => `export const ${module} = SignalRegistry.registerService(${module}Sig.${capitalize(module)}Internal, ${module}Sig.${capitalize(module)}Endpoint, ${capitalize(module)});`).join("\n")}
|
|
53
|
+
${databaseModules.map((module) => `export const ${module} = SignalRegistry.registerDatabase("${module}" as const, ${module}Sig.${capitalize(module)}Internal, ${module}Sig.${capitalize(module)}Endpoint, ${module}Sig.${capitalize(module)}Slice, ${capitalize(module)});`).join("\n")}
|
|
54
|
+
${serviceModules.map((module) => `export const ${module} = SignalRegistry.registerService("${module}" as const, ${module}Sig.${capitalize(module)}Internal, ${module}Sig.${capitalize(module)}Endpoint, ${capitalize(module)});`).join("\n")}
|
|
55
55
|
|
|
56
56
|
export const fetch = FetchClient.from(${[...(libs.length === 0 ? ["base"] : libs), ...databaseModules, ...serviceModules].join(", ")});
|
|
57
57
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { type Exec, FileSys, type PackageJson, runner, type Workspace, WorkspaceExecutor } from "akanjs/devkit";
|
|
3
|
+
import { getLatestPackageVersion } from "../npmRegistry";
|
|
4
4
|
|
|
5
5
|
export class WorkspaceRunner extends runner("workspace") {
|
|
6
6
|
async createWorkspace(
|
|
@@ -13,7 +13,7 @@ export class WorkspaceRunner extends runner("workspace") {
|
|
|
13
13
|
|
|
14
14
|
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot, repoName });
|
|
15
15
|
const templateSpinner = workspace.spinning(`Creating workspace template files in ${dirname}/${repoName}...`);
|
|
16
|
-
const latestTypesBunVersion = await
|
|
16
|
+
const latestTypesBunVersion = await getLatestPackageVersion("@types/bun");
|
|
17
17
|
await workspace.applyTemplate({
|
|
18
18
|
basePath: ".",
|
|
19
19
|
template: "workspaceRoot",
|
package/client/cookie.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { getEnv } from "akanjs/base";
|
|
2
|
-
import { Logger } from "akanjs/common";
|
|
2
|
+
import { decodeJwtPayload, Logger } from "akanjs/common";
|
|
3
3
|
import type { Account } from "akanjs/fetch";
|
|
4
4
|
import { requestStorage } from "akanjs/fetch";
|
|
5
|
-
import Cookies from "js-cookie";
|
|
6
|
-
import { jwtDecode } from "jwt-decode";
|
|
7
5
|
import { storage } from "./storage";
|
|
8
6
|
import { fetch } from "./useClient";
|
|
9
7
|
|
|
8
|
+
interface CookieOptions {
|
|
9
|
+
path?: string;
|
|
10
|
+
sameSite?: "strict" | "lax" | "none";
|
|
11
|
+
secure?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
function parseCookieHeader(cookieHeader: string): Map<string, { name: string; value: string }> {
|
|
11
15
|
const entries = cookieHeader
|
|
12
16
|
.split(";")
|
|
@@ -30,22 +34,13 @@ export const cookies = (): Map<string, { name: string; value: string }> => {
|
|
|
30
34
|
if (!req) return new Map();
|
|
31
35
|
return parseCookieHeader(req.headers.get("cookie") ?? "");
|
|
32
36
|
}
|
|
33
|
-
|
|
34
|
-
return new Map(
|
|
35
|
-
Object.entries(cookie).map(([key, value]) => [
|
|
36
|
-
key,
|
|
37
|
-
{
|
|
38
|
-
name: key,
|
|
39
|
-
value: typeof value === "string" && value.startsWith("j:") ? (JSON.parse(value.slice(2)) as string) : value,
|
|
40
|
-
},
|
|
41
|
-
]),
|
|
42
|
-
);
|
|
37
|
+
return parseCookieHeader(document.cookie);
|
|
43
38
|
};
|
|
44
39
|
|
|
45
40
|
export const setCookie = (
|
|
46
41
|
key: string,
|
|
47
42
|
value: string,
|
|
48
|
-
options:
|
|
43
|
+
options: CookieOptions = { path: "/", sameSite: "none", secure: true },
|
|
49
44
|
) => {
|
|
50
45
|
if (getEnv().side === "server") return;
|
|
51
46
|
else
|
|
@@ -88,7 +83,7 @@ export const getAccount = <AddData = unknown>(): Account<AddData> => {
|
|
|
88
83
|
const jwt = getCookie("jwt") ?? getHeader("jwt");
|
|
89
84
|
const defaultAccount = { appName: getEnv().appName, environment: getEnv().environment } as Account<AddData>;
|
|
90
85
|
if (!jwt) return defaultAccount;
|
|
91
|
-
const account
|
|
86
|
+
const account = decodeJwtPayload<Account<AddData>>(jwt);
|
|
92
87
|
if (account.appName !== getEnv().appName || account.environment !== getEnv().environment) return defaultAccount;
|
|
93
88
|
return account;
|
|
94
89
|
};
|
package/common/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { isEmail } from "./isEmail";
|
|
|
11
11
|
export { isPhoneNumber } from "./isPhoneNumber";
|
|
12
12
|
export { isQueryEqual } from "./isQueryEqual";
|
|
13
13
|
export { isValidDate } from "./isValidDate";
|
|
14
|
+
export { decodeJwtPayload } from "./jwtDecode";
|
|
14
15
|
export { Logger, type LoggerSink, type LoggerSinkEntry, type LogLevel } from "./Logger";
|
|
15
16
|
export {
|
|
16
17
|
type AkanI18nConfig,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function decodeBase64Url(input: string): string {
|
|
2
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
3
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
4
|
+
if (typeof atob === "function") return atob(padded);
|
|
5
|
+
return Buffer.from(padded, "base64").toString("binary");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Decodes a JWT payload without validating its signature. */
|
|
9
|
+
export function decodeJwtPayload<T = unknown>(jwt: string): T {
|
|
10
|
+
const [, payload] = jwt.split(".");
|
|
11
|
+
if (!payload) throw new Error("Invalid JWT payload");
|
|
12
|
+
const binary = decodeBase64Url(payload);
|
|
13
|
+
const json = decodeURIComponent(
|
|
14
|
+
[...binary].map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`).join(""),
|
|
15
|
+
);
|
|
16
|
+
return JSON.parse(json) as T;
|
|
17
|
+
}
|
package/constant/serialize.ts
CHANGED
|
@@ -53,7 +53,7 @@ const serializeInput = <Input = unknown>(
|
|
|
53
53
|
return Object.fromEntries(
|
|
54
54
|
Object.entries(modelRef[FIELD_META]).map(([key, field]) => [
|
|
55
55
|
key,
|
|
56
|
-
field.isClass && !field.isScalar
|
|
56
|
+
serializeType === "input" && field.isClass && !field.isScalar
|
|
57
57
|
? serialize(ID, field.arrDepth, getRelationId((value as Record<string, unknown>)?.[key]), serializeType, {
|
|
58
58
|
nullable: field.nullable,
|
|
59
59
|
key,
|
|
@@ -9,6 +9,8 @@ import { IncrementalBuilderHost } from "../incrementalBuilder";
|
|
|
9
9
|
const backendMsgTypeSet = new Set<BuilderMessage["type"]>(["build-route"]);
|
|
10
10
|
const BACKEND_RESTART_DEBOUNCE_MS = 120;
|
|
11
11
|
const BACKEND_GRACEFUL_TIMEOUT_MS = 3000;
|
|
12
|
+
const BACKEND_RECOVERY_BASE_DELAY_MS = 1_000;
|
|
13
|
+
const BACKEND_RECOVERY_MAX_DELAY_MS = 30_000;
|
|
12
14
|
const BUILDER_READY_TIMEOUT_MS = 15000;
|
|
13
15
|
const BUILDER_START_MAX_ATTEMPTS = 3;
|
|
14
16
|
const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
@@ -132,6 +134,8 @@ export class AkanAppHost {
|
|
|
132
134
|
#backendReady = false;
|
|
133
135
|
#plannedBackendStops = new WeakSet<Bun.Subprocess<"ignore", "inherit", "inherit">>();
|
|
134
136
|
#restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
137
|
+
#backendRecoveryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
138
|
+
#backendRecoveryAttempts = 0;
|
|
135
139
|
#restartFiles = new Set<string>();
|
|
136
140
|
#latestPagesUpdated: Extract<BuilderMessage, { type: "pages-updated" }> | null = null;
|
|
137
141
|
#latestCssUpdated: Extract<BuilderMessage, { type: "css-updated" }> | null = null;
|
|
@@ -162,6 +166,10 @@ export class AkanAppHost {
|
|
|
162
166
|
clearTimeout(this.#restartTimer);
|
|
163
167
|
this.#restartTimer = null;
|
|
164
168
|
}
|
|
169
|
+
if (this.#backendRecoveryTimer) {
|
|
170
|
+
clearTimeout(this.#backendRecoveryTimer);
|
|
171
|
+
this.#backendRecoveryTimer = null;
|
|
172
|
+
}
|
|
165
173
|
await this.#stopBackend();
|
|
166
174
|
this.#stopBuilder();
|
|
167
175
|
return this;
|
|
@@ -185,6 +193,7 @@ export class AkanAppHost {
|
|
|
185
193
|
if (!msg || typeof msg !== "object") return;
|
|
186
194
|
if (msg.type === "backend-ready") {
|
|
187
195
|
this.#backendReady = true;
|
|
196
|
+
this.#backendRecoveryAttempts = 0;
|
|
188
197
|
this.logger.verbose(`backend ready pid=${msg.pid}`);
|
|
189
198
|
this.#replayBuilderState();
|
|
190
199
|
return;
|
|
@@ -199,7 +208,7 @@ export class AkanAppHost {
|
|
|
199
208
|
this.#plannedBackendStops.delete(backend);
|
|
200
209
|
return;
|
|
201
210
|
}
|
|
202
|
-
this.#
|
|
211
|
+
this.#scheduleBackendRecovery("backend-exit");
|
|
203
212
|
},
|
|
204
213
|
});
|
|
205
214
|
this.#backend = backend;
|
|
@@ -245,6 +254,10 @@ export class AkanAppHost {
|
|
|
245
254
|
}
|
|
246
255
|
#scheduleBackendRestart(files: string[]) {
|
|
247
256
|
for (const file of files) this.#restartFiles.add(file);
|
|
257
|
+
if (this.#backendRecoveryTimer) {
|
|
258
|
+
clearTimeout(this.#backendRecoveryTimer);
|
|
259
|
+
this.#backendRecoveryTimer = null;
|
|
260
|
+
}
|
|
248
261
|
if (this.#restartTimer) clearTimeout(this.#restartTimer);
|
|
249
262
|
this.#restartTimer = setTimeout(() => {
|
|
250
263
|
this.#restartTimer = null;
|
|
@@ -255,9 +268,26 @@ export class AkanAppHost {
|
|
|
255
268
|
}
|
|
256
269
|
async #restartBackend(files: string[]) {
|
|
257
270
|
this.logger.verbose(`[backend-reload] restarting backend for ${files.length} file(s)`);
|
|
271
|
+
this.#backendRecoveryAttempts = 0;
|
|
258
272
|
await Promise.all([this.#stopBackend(), this.#backendGraph.refresh()]);
|
|
259
273
|
this.#startBackend();
|
|
260
274
|
}
|
|
275
|
+
#scheduleBackendRecovery(reason: string) {
|
|
276
|
+
if (this.#backendRecoveryTimer || this.#backend) return;
|
|
277
|
+
const attempt = this.#backendRecoveryAttempts;
|
|
278
|
+
const delay = Math.min(BACKEND_RECOVERY_BASE_DELAY_MS * 2 ** attempt, BACKEND_RECOVERY_MAX_DELAY_MS);
|
|
279
|
+
this.#backendRecoveryAttempts = attempt + 1;
|
|
280
|
+
this.logger.warn(
|
|
281
|
+
`[backend-recovery] backend exited unexpectedly (${reason}); restarting in ${delay}ms (attempt ${this.#backendRecoveryAttempts})`,
|
|
282
|
+
);
|
|
283
|
+
this.#backendRecoveryTimer = setTimeout(() => {
|
|
284
|
+
this.#backendRecoveryTimer = null;
|
|
285
|
+
if (this.#backend) return;
|
|
286
|
+
void this.#backendGraph.refresh().finally(() => {
|
|
287
|
+
if (!this.#backend) this.#startBackend();
|
|
288
|
+
});
|
|
289
|
+
}, delay);
|
|
290
|
+
}
|
|
261
291
|
#enqueueBuilderMessage(message: BuilderMessage) {
|
|
262
292
|
this.#builderMessageQueue = this.#builderMessageQueue
|
|
263
293
|
.then(() => this.#handleBuilderMessage(message))
|
|
@@ -319,7 +349,6 @@ export class AkanAppHost {
|
|
|
319
349
|
return new Promise<void>((resolve, reject) => {
|
|
320
350
|
if (!this.#builder) throw new Error("Builder Not Found");
|
|
321
351
|
let settled = false;
|
|
322
|
-
let ready = false;
|
|
323
352
|
const settle = (fn: () => void) => {
|
|
324
353
|
if (settled) return;
|
|
325
354
|
settled = true;
|
|
@@ -331,22 +360,30 @@ export class AkanAppHost {
|
|
|
331
360
|
}, BUILDER_READY_TIMEOUT_MS);
|
|
332
361
|
this.#builder.start({
|
|
333
362
|
onExit: () => {
|
|
334
|
-
if (settled && ready) {
|
|
335
|
-
void this.#stopBackend();
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
363
|
settle(() => reject(new Error(`[cli] builder exited before emitting builder-ready (attempt ${attempt})`)));
|
|
339
364
|
},
|
|
340
365
|
onReady: () => {
|
|
341
|
-
ready = true;
|
|
342
366
|
settle(resolve);
|
|
343
367
|
},
|
|
368
|
+
onRestartReady: () => {
|
|
369
|
+
this.logger.verbose("[builder-recovery] builder ready after restart; replaying latest state");
|
|
370
|
+
this.#replayBuilderState();
|
|
371
|
+
},
|
|
344
372
|
});
|
|
345
373
|
});
|
|
346
374
|
}
|
|
347
375
|
#sendToBuilder(message: BuilderMessage) {
|
|
348
|
-
if (this.#builder
|
|
349
|
-
|
|
376
|
+
if (this.#builder?.send(message)) return;
|
|
377
|
+
if (message.type === "build-route") {
|
|
378
|
+
this.#sendToBackend({
|
|
379
|
+
type: "build-route-res",
|
|
380
|
+
id: message.id,
|
|
381
|
+
ok: false,
|
|
382
|
+
error: `builder is ${this.#builder?.status ?? "stopped"}; reload after the builder is ready`,
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
this.logger.warn("akanAppHost builder is not running");
|
|
350
387
|
}
|
|
351
388
|
#stopBuilder() {
|
|
352
389
|
if (!this.#builder) return;
|
|
@@ -269,7 +269,8 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
269
269
|
const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
|
|
270
270
|
if (rootVersion) return rootVersion;
|
|
271
271
|
const akanPackageJson = getAkanPackageJson();
|
|
272
|
-
if (AKAN_RUNTIME_PACKAGES.has(lib))
|
|
272
|
+
if (AKAN_RUNTIME_PACKAGES.has(lib))
|
|
273
|
+
return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
|
|
273
274
|
}
|
|
274
275
|
getProductionPackageJson(data: Partial<PackageJson> = {}): PackageJson {
|
|
275
276
|
return {
|
|
@@ -16,12 +16,25 @@ const getLocalIP = () => {
|
|
|
16
16
|
|
|
17
17
|
const normalizeBasePath = (basePath: string | undefined) => basePath?.replace(/^\/+|\/+$/g, "");
|
|
18
18
|
|
|
19
|
+
const routeBasePaths = (appInfo: AppScanResult) =>
|
|
20
|
+
new Set(
|
|
21
|
+
appInfo.routes
|
|
22
|
+
.map((route) => route.replace(/^\.\//, "").split("/")[0])
|
|
23
|
+
.filter((segment): segment is string => !!segment && !segment.startsWith("_") && !segment.startsWith("(")),
|
|
24
|
+
);
|
|
25
|
+
|
|
19
26
|
const resolveTarget = (appInfo: AppScanResult, targetName = process.env.AKAN_MOBILE_TARGET) => {
|
|
20
27
|
const targets = appInfo.akanConfig.mobile.targets;
|
|
21
28
|
if (!targets || Object.keys(targets).length === 0) throw new Error("Akan mobile target metadata is missing.");
|
|
22
29
|
if (targetName) {
|
|
23
30
|
const target = targets[targetName];
|
|
24
|
-
if (!target)
|
|
31
|
+
if (!target) {
|
|
32
|
+
const basePath = normalizeBasePath(targetName);
|
|
33
|
+
const [template] = Object.values(targets);
|
|
34
|
+
if (basePath && template && routeBasePaths(appInfo).has(basePath))
|
|
35
|
+
return { ...template, name: basePath, basePath };
|
|
36
|
+
throw new Error(`Akan mobile target '${targetName}' was not found.`);
|
|
37
|
+
}
|
|
25
38
|
return target;
|
|
26
39
|
}
|
|
27
40
|
const entries = Object.entries(targets);
|
|
@@ -31,7 +44,8 @@ const resolveTarget = (appInfo: AppScanResult, targetName = process.env.AKAN_MOB
|
|
|
31
44
|
|
|
32
45
|
const localCsrUrl = (ip: string, target: AkanMobileTargetConfig) => {
|
|
33
46
|
const basePath = normalizeBasePath(target.basePath);
|
|
34
|
-
|
|
47
|
+
const port = process.env.AKAN_PUBLIC_CLIENT_PORT ?? process.env.PORT ?? "8282";
|
|
48
|
+
return `http://${ip}:${port}/${basePath ? `${basePath}` : ""}?csr=true`;
|
|
35
49
|
};
|
|
36
50
|
|
|
37
51
|
export const withBase = (
|
|
@@ -44,9 +58,10 @@ export const withBase = (
|
|
|
44
58
|
if (!appInfo) throw new Error("withBase requires apps/<app>/akan.app.json metadata.");
|
|
45
59
|
const target = resolveTarget(appInfo, targetName);
|
|
46
60
|
const baseConfig: CapacitorConfig = {
|
|
61
|
+
...target,
|
|
47
62
|
appId: target.appId,
|
|
48
63
|
appName: target.appName,
|
|
49
|
-
webDir: "
|
|
64
|
+
webDir: "dist",
|
|
50
65
|
server:
|
|
51
66
|
process.env.APP_OPERATION_MODE !== "release"
|
|
52
67
|
? {
|
|
@@ -62,7 +77,6 @@ export const withBase = (
|
|
|
62
77
|
CapacitorCookies: { enabled: true },
|
|
63
78
|
...target.plugins,
|
|
64
79
|
},
|
|
65
|
-
...target,
|
|
66
80
|
android: {
|
|
67
81
|
...target.android,
|
|
68
82
|
},
|