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.
Files changed (81) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/cli/application/application.command.ts +4 -1
  4. package/cli/application/application.runner.ts +6 -8
  5. package/cli/build.ts +3 -1
  6. package/cli/cloud/cloud.runner.ts +7 -8
  7. package/cli/index.js +288 -115
  8. package/cli/library/library.runner.ts +2 -2
  9. package/cli/module/module.runner.ts +2 -2
  10. package/cli/npmRegistry.ts +13 -0
  11. package/cli/openBrowser.ts +15 -0
  12. package/cli/pluralizeName.ts +5 -0
  13. package/cli/scalar/scalar.prompt.ts +2 -2
  14. package/cli/scalar/scalar.runner.ts +2 -2
  15. package/cli/semver.ts +18 -0
  16. package/cli/templates/lib/sig.ts +2 -2
  17. package/cli/workspace/workspace.runner.ts +3 -3
  18. package/client/cookie.ts +10 -15
  19. package/common/index.ts +1 -0
  20. package/common/jwtDecode.ts +17 -0
  21. package/constant/serialize.ts +1 -1
  22. package/devkit/akanApp/akanApp.host.ts +46 -9
  23. package/devkit/akanConfig/akanConfig.ts +2 -1
  24. package/devkit/capacitor.base.config.ts +18 -4
  25. package/devkit/capacitorApp.ts +118 -64
  26. package/devkit/incrementalBuilder/incrementalBuilder.host.ts +83 -9
  27. package/devkit/mobile/mobileTarget.ts +2 -1
  28. package/devkit/scanInfo.ts +1 -0
  29. package/document/dataLoader.ts +140 -6
  30. package/document/database.ts +1 -1
  31. package/package.json +7 -13
  32. package/server/akanApp.ts +250 -44
  33. package/server/di/diLifecycle.ts +1 -1
  34. package/server/processMetricsCollector.ts +79 -1
  35. package/server/proxy/localeWebProxy.ts +29 -12
  36. package/server/resolver/database.resolver.ts +82 -31
  37. package/server/resolver/signal.resolver.ts +67 -28
  38. package/service/ipcTypes.ts +5 -0
  39. package/service/predefinedAdaptor/database.adaptor.ts +95 -27
  40. package/service/predefinedAdaptor/solidSqlite.ts +7 -7
  41. package/service/predefinedAdaptor/storage.adaptor.ts +35 -9
  42. package/service/serviceModule.ts +1 -6
  43. package/signal/base.signal.ts +1 -1
  44. package/signal/index.ts +1 -0
  45. package/signal/middleware.ts +5 -1
  46. package/signal/signalContext.ts +85 -31
  47. package/signal/signalRegistry.ts +35 -10
  48. package/signal/trace.ts +279 -0
  49. package/types/cli/npmRegistry.d.ts +1 -0
  50. package/types/cli/openBrowser.d.ts +1 -0
  51. package/types/cli/pluralizeName.d.ts +1 -0
  52. package/types/cli/semver.d.ts +1 -0
  53. package/types/client/cookie.d.ts +6 -1
  54. package/types/common/index.d.ts +1 -0
  55. package/types/common/jwtDecode.d.ts +2 -0
  56. package/types/devkit/capacitorApp.d.ts +14 -5
  57. package/types/devkit/incrementalBuilder/incrementalBuilder.host.d.ts +9 -5
  58. package/types/document/dataLoader.d.ts +21 -2
  59. package/types/document/database.d.ts +1 -1
  60. package/types/server/processMetricsCollector.d.ts +2 -0
  61. package/types/service/ipcTypes.d.ts +5 -0
  62. package/types/service/predefinedAdaptor/database.adaptor.d.ts +26 -32
  63. package/types/service/predefinedAdaptor/solidSqlite.d.ts +3 -3
  64. package/types/service/predefinedAdaptor/storage.adaptor.d.ts +8 -2
  65. package/types/service/serviceModule.d.ts +1 -1
  66. package/types/signal/index.d.ts +1 -0
  67. package/types/signal/signalContext.d.ts +4 -1
  68. package/types/signal/signalRegistry.d.ts +25 -4
  69. package/types/signal/trace.d.ts +97 -0
  70. package/types/ui/Signal/style.d.ts +15 -0
  71. package/ui/Signal/Arg.tsx +22 -15
  72. package/ui/Signal/Doc.tsx +30 -24
  73. package/ui/Signal/Listener.tsx +15 -39
  74. package/ui/Signal/Message.tsx +32 -50
  75. package/ui/Signal/Object.tsx +16 -13
  76. package/ui/Signal/PubSub.tsx +29 -47
  77. package/ui/Signal/Response.tsx +7 -17
  78. package/ui/Signal/RestApi.tsx +41 -57
  79. package/ui/Signal/WebSocket.tsx +1 -1
  80. package/ui/Signal/style.ts +36 -0
  81. package/webkit/useCsrValues.ts +147 -37
@@ -1,5 +1,5 @@
1
1
  import { type Lib, LibExecutor, runner, type Workspace } from "akanjs/devkit";
2
- import { compareVersions } from "compare-versions";
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 = compareVersions(rootVersion, libVersion) > 0 ? rootVersion : libVersion;
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 pluralize from "pluralize";
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 = pluralize(module.name);
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
+ }
@@ -0,0 +1,5 @@
1
+ export function pluralizeName(name: string): string {
2
+ if (/[^aeiou]y$/i.test(name)) return `${name.slice(0, -1)}ies`;
3
+ if (/(s|x|z|ch|sh)$/i.test(name)) return `${name}es`;
4
+ return `${name}s`;
5
+ }
@@ -1,6 +1,6 @@
1
1
  import { input } from "@inquirer/prompts";
2
2
  import { type FileContent, Prompter, type Sys } from "akanjs/devkit";
3
- import pluralize from "pluralize";
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: pluralize(this.name), sysName: this.sys.name },
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 pluralize from "pluralize";
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: pluralize(scalarName), sysName: sys.name },
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
+ }
@@ -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 { FileSys, type Exec, type PackageJson, runner, type Workspace, WorkspaceExecutor } from "akanjs/devkit";
3
- import latestVersion from "latest-version";
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 latestVersion("@types/bun");
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
- const cookie = Cookies.get();
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: Cookies.CookieAttributes = { path: "/", sameSite: "none", secure: true },
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: Account<AddData> = jwtDecode<Account<AddData>>(jwt);
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
+ }
@@ -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.#stopBuilder();
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) this.#builder.send(message);
349
- else this.logger.warn("akanAppHost is not running");
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)) return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[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) throw new Error(`Akan mobile target '${targetName}' was not found.`);
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
- return `http://${ip}:8282/${basePath ? `${basePath}` : ""}?csr=true`;
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: "www",
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
  },