@akanjs/devkit 3.0.0-beta.29 → 3.0.0-beta.30

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.
@@ -1,5 +1,5 @@
1
- import type { AkanMobileAppId } from "akanjs";
2
- import type { AkanAppConfig, AkanMobileTargetConfig, MobileEnv } from "../akanConfig";
1
+ import type { AkanNativeAppId } from "akanjs";
2
+ import type { AkanAppConfig, AkanNativeTarget, NativeEnv } from "../akanConfig";
3
3
  import type { App } from "../commandDecorators";
4
4
 
5
5
  export type MobilePlatform = "ios" | "android";
@@ -9,26 +9,26 @@ export type MobileTargetSelection = string | "all" | undefined;
9
9
 
10
10
  export interface ResolvedMobileTarget {
11
11
  name: string;
12
- config: AkanMobileTargetConfig;
12
+ config: AkanNativeTarget;
13
13
  }
14
14
 
15
- export const MOBILE_ENVS = ["local", "debug", "develop", "main"] as const satisfies readonly MobileEnv[];
15
+ export const MOBILE_ENVS = ["local", "debug", "develop", "main"] as const satisfies readonly NativeEnv[];
16
16
 
17
17
  const trimSlashes = (value: string) => value.replace(/^\/+|\/+$/g, "");
18
18
 
19
19
  export const getMobileTargets = async (app: App): Promise<ResolvedMobileTarget[]> => {
20
20
  const config = await app.getConfig();
21
- return Object.entries(config.mobile.targets).map(([name, target]) => ({ name, config: target }));
21
+ return Object.entries(config.native.targets).map(([name, target]) => ({ name, config: target }));
22
22
  };
23
23
 
24
24
  //* An app with basePaths builds a CSR page per basePath and none at its root, so a target naming no basePath opens
25
25
  //* nothing on its own there; it is the template each basePath's target is made from.
26
- const isTemplateOnly = ({ basePaths, mobile }: Pick<AkanAppConfig, "basePaths" | "mobile">) =>
27
- basePaths.size > 0 && Object.values(mobile.targets).every((target) => !target.basePath);
26
+ const isTemplateOnly = ({ basePaths, native }: Pick<AkanAppConfig, "basePaths" | "native">) =>
27
+ basePaths.size > 0 && Object.values(native.targets).every((target) => !target.basePath);
28
28
 
29
29
  export const getMobileTargetChoices = async (app: App): Promise<string[]> => {
30
30
  const config = await app.getConfig();
31
- const targetNames = Object.keys(config.mobile.targets);
31
+ const targetNames = Object.keys(config.native.targets);
32
32
  if (targetNames.length > 0 && !isTemplateOnly(config)) return targetNames;
33
33
  return [...config.basePaths];
34
34
  };
@@ -56,15 +56,15 @@ export const resolveMobileTargets = async (
56
56
  ): Promise<ResolvedMobileTarget[]> => {
57
57
  const config = await app.getConfig();
58
58
  const targets = await getMobileTargets(app);
59
- if (targets.length === 0) throw new Error(`No mobile targets configured for ${app.name}`);
59
+ if (targets.length === 0) throw new Error(`No native targets configured for ${app.name}`);
60
60
  if (!selection && targets.length === 1 && !isTemplateOnly(config)) return targets;
61
61
  if (!selection) {
62
62
  const choices = await getMobileTargetChoices(app);
63
63
  if (choices.length === 1) return resolveMobileTargets(app, choices[0]);
64
- throw new Error(`Multiple mobile targets found for ${app.name}. Pass --target <${choices.join("|")}|all>.`);
64
+ throw new Error(`Multiple native targets found for ${app.name}. Pass --target <${choices.join("|")}|all>.`);
65
65
  }
66
66
  if (selection === "all") {
67
- if (Object.keys(config.mobile.targets).length > 1) return targets;
67
+ if (Object.keys(config.native.targets).length > 1) return targets;
68
68
  const basePaths = [...config.basePaths];
69
69
  if (basePaths.length > 1 || isTemplateOnly(config)) {
70
70
  return basePaths.flatMap((basePath) => {
@@ -79,20 +79,20 @@ export const resolveMobileTargets = async (
79
79
  const basePathTarget = resolveMobileTargetByBasePath(targets, selection);
80
80
  if (basePathTarget && config.basePaths.has(trimSlashes(selection))) return [basePathTarget];
81
81
  const choices = await getMobileTargetChoices(app);
82
- throw new Error(`Mobile target '${selection}' was not found. Available: ${choices.join(", ")}`);
82
+ throw new Error(`Native target '${selection}' was not found. Available: ${choices.join(", ")}`);
83
83
  };
84
84
 
85
- export const resolveAppId = (appId: AkanMobileAppId, platform: NativePlatform): string => {
85
+ export const resolveAppId = (appId: AkanNativeAppId, platform: NativePlatform): string => {
86
86
  if (typeof appId === "string") return appId;
87
87
  const id = appId[platform] ?? appId.default;
88
- if (!id) throw new Error(`mobile.appId names no id for ${platform}; give it ${platform} or default.`);
88
+ if (!id) throw new Error(`native.appId names no id for ${platform}; give it ${platform} or default.`);
89
89
  return id;
90
90
  };
91
91
 
92
- export const appIdsOf = (appId: AkanMobileAppId): string[] =>
92
+ export const appIdsOf = (appId: AkanNativeAppId): string[] =>
93
93
  typeof appId === "string" ? [appId] : Object.values(appId).filter((id): id is string => !!id);
94
94
 
95
- export const resolveMobilePath = (target: AkanMobileTargetConfig, pathname: string) => {
95
+ export const resolveMobilePath = (target: AkanNativeTarget, pathname: string) => {
96
96
  const basePath = trimSlashes(target.basePath ?? "");
97
97
  const normalizedPath = `/${pathname.replace(/^\/+/, "")}`;
98
98
  if (!basePath) return normalizedPath;
@@ -100,7 +100,7 @@ export const resolveMobilePath = (target: AkanMobileTargetConfig, pathname: stri
100
100
  return `/${basePath}${normalizedPath === "/" ? "" : normalizedPath}`;
101
101
  };
102
102
 
103
- export const targetHtmlFilename = (target: AkanMobileTargetConfig) => {
103
+ export const targetHtmlFilename = (target: AkanNativeTarget) => {
104
104
  const basePath = trimSlashes(target.basePath ?? "");
105
105
  return basePath ? `${basePath}.html` : "index.html";
106
106
  };
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import path from "node:path";
3
3
  import type { AkanPlugin } from "akanjs";
4
- import type { AkanMobileTargetConfig } from "../akanConfig";
4
+ import type { AkanNativeTarget } from "../akanConfig";
5
5
  import type { App } from "../commandDecorators";
6
6
  import { tempDirs, writeText } from "../testHelpers";
7
7
  import { NativeApp } from "./nativeApp";
@@ -9,7 +9,7 @@ import { NativeConfig } from "./nativeConfig";
9
9
 
10
10
  const makeTempRoot = tempDirs("akan-native-app-");
11
11
 
12
- const target = (config: Partial<AkanMobileTargetConfig> = {}) => ({
12
+ const target = (config: Partial<AkanNativeTarget> = {}) => ({
13
13
  name: config.name ?? "default",
14
14
  config: {
15
15
  name: config.name ?? "default",
@@ -39,10 +39,10 @@ describe("NativeApp", () => {
39
39
  test("keeps each target's web root and each platform's output apart", () => {
40
40
  const admin = new NativeApp(fakeApp(), target({ name: "admin", basePath: "admin" }));
41
41
 
42
- expect(admin.web.dir).toBe(path.join(appDir, ".akan/mobile/admin/web"));
43
- expect(admin.outDir("ios")).toBe(path.join(appDir, ".akan/mobile/admin/native/ios"));
44
- expect(admin.outDir("android")).toBe(path.join(appDir, ".akan/mobile/admin/native/android"));
45
- expect(admin.devOutDir("macos")).toBe(path.join(appDir, ".akan/mobile/admin/dev/macos"));
42
+ expect(admin.web.dir).toBe(path.join(appDir, ".akan/native/admin/web"));
43
+ expect(admin.outDir("ios")).toBe(path.join(appDir, ".akan/native/admin/build/ios"));
44
+ expect(admin.outDir("android")).toBe(path.join(appDir, ".akan/native/admin/build/android"));
45
+ expect(admin.devOutDir("macos")).toBe(path.join(appDir, ".akan/native/admin/dev/macos"));
46
46
  });
47
47
 
48
48
  test("a dev build bundles no page a release build left, and builds apart from the release it would empty", async () => {
@@ -106,7 +106,7 @@ describe("NativeApp", () => {
106
106
 
107
107
  expect(warnings).toEqual([]);
108
108
  expect(config.plugins).toEqual([...NativeConfig.basePlugins, "push"]);
109
- expect(config.web.dir).toBe(path.join(appDir, ".akan/mobile/default/web"));
109
+ expect(config.web.dir).toBe(path.join(appDir, ".akan/native/default/web"));
110
110
  });
111
111
 
112
112
  test("a config nothing is built from (the update key's) stages no desktop bin", async () => {
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import type { AkanNativeConfig } from "@akanjs/native/config";
3
- import type { MobileEnv } from "akanjs";
3
+ import type { NativeEnv } from "akanjs";
4
4
  import type { App } from "../commandDecorators";
5
5
  import { Executor } from "../executors";
6
6
  import { DesktopBin } from "./desktopBin";
@@ -36,7 +36,7 @@ interface NativeDevBoot {
36
36
 
37
37
  export interface NativeBuildOptions {
38
38
  profile?: "debug" | "release";
39
- /** The server a desktop app carries (the target's `native.desktop.server`), staged by DesktopServerStage. */
39
+ /** The server a desktop app carries (the target's `desktop.server`), staged by DesktopServerStage. */
40
40
  server?: DesktopServerBundle;
41
41
  /** Windows: an NSIS setup program beside the app folder. */
42
42
  installer?: boolean;
@@ -48,7 +48,7 @@ export interface NativeDevOptions extends NativeRunOptions {
48
48
  lang: string;
49
49
  }
50
50
 
51
- /** One mobile target of an app on the native runtime (a phone, or this computer as a desktop app): where it builds,
51
+ /** One native target of an app on the native runtime (a phone, or this computer as a desktop app): where it builds,
52
52
  * what it ships, and the calls into the API. */
53
53
  export class NativeApp {
54
54
  readonly targetRoot: string;
@@ -59,14 +59,14 @@ export class NativeApp {
59
59
  constructor(
60
60
  readonly app: App,
61
61
  readonly target: ResolvedMobileTarget,
62
- readonly env?: MobileEnv,
62
+ readonly env?: NativeEnv,
63
63
  ) {
64
- this.targetRoot = path.join(app.cwdPath, ".akan", "mobile", target.name);
64
+ this.targetRoot = path.join(app.cwdPath, ".akan", "native", target.name);
65
65
  this.web = new NativeWebDir(path.join(this.targetRoot, "web"));
66
66
  }
67
67
 
68
68
  outDir(platform: NativePlatform) {
69
- return path.join(this.targetRoot, "native", platform);
69
+ return path.join(this.targetRoot, "build", platform);
70
70
  }
71
71
 
72
72
  //? Apart from the builds: a desktop build empties its folder first, which would take a release and its installer.
@@ -74,7 +74,7 @@ export class NativeApp {
74
74
  return path.join(this.targetRoot, "dev", platform);
75
75
  }
76
76
 
77
- /** The web root, from the production build the mobile commands run first. */
77
+ /** The web root, from the production build the native commands run first. */
78
78
  async assembleWeb() {
79
79
  const dist = this.app.dist.cwdPath;
80
80
  return await this.web.assemble(this.target.config, {
@@ -135,7 +135,7 @@ export class NativeApp {
135
135
  const problems = api.validateConfig(config, { appDir: this.app.cwdPath });
136
136
  if (problems.length)
137
137
  throw new Error(
138
- `Mobile target '${this.target.name}' makes an invalid native config:\n- ${problems.join("\n- ")}`,
138
+ `Native target '${this.target.name}' makes an invalid native config:\n- ${problems.join("\n- ")}`,
139
139
  );
140
140
  }
141
141
 
@@ -298,7 +298,7 @@ export class NativeApp {
298
298
  });
299
299
  } catch (error) {
300
300
  throw new Error(
301
- `Mobile target '${this.target.name}': ${error instanceof Error ? error.message : String(error)}`,
301
+ `Native target '${this.target.name}': ${error instanceof Error ? error.message : String(error)}`,
302
302
  {
303
303
  cause: error,
304
304
  },
@@ -309,7 +309,7 @@ export class NativeApp {
309
309
  #assertUpdates() {
310
310
  if (!this.target.config.updates)
311
311
  throw new Error(
312
- `Mobile target '${this.target.name}' has no updates: { url, publicKey } in akan.config.ts; \`akan update-keygen ${this.app.name}\` prints the key.`,
312
+ `Native target '${this.target.name}' has no updates: add native.updates: { url, publicKey } to akan.config.ts; \`akan update-keygen ${this.app.name}\` prints the key.`,
313
313
  );
314
314
  }
315
315
 
@@ -335,7 +335,7 @@ export class NativeApp {
335
335
  const { api, config } = await this.prepare(platform);
336
336
  if (!config.updates)
337
337
  throw new Error(
338
- `Mobile target '${this.target.name}' takes no updates: add mobile.updates: { url, publicKey } to akan.config.ts first.`,
338
+ `Native target '${this.target.name}' takes no updates: add native.updates: { url, publicKey } to akan.config.ts first.`,
339
339
  );
340
340
  const packed = await api.packUpdate({
341
341
  ...this.#task(platform, config),
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import type { AkanMobileTargetConfig } from "../akanConfig";
4
+ import type { AkanNativeTarget } from "../akanConfig";
5
5
  import { tempDirs } from "../testHelpers";
6
6
  import { NativeApi } from "./nativeApi";
7
7
  import { NativeConfig } from "./nativeConfig";
@@ -9,7 +9,7 @@ import { NativeConfig } from "./nativeConfig";
9
9
  const makeTempRoot = tempDirs("akan-native-config-");
10
10
  const repoApp = path.resolve(import.meta.dir, "../../../../apps/minimal");
11
11
 
12
- const minimalTarget: AkanMobileTargetConfig = {
12
+ const minimalTarget: AkanNativeTarget = {
13
13
  name: "default",
14
14
  indexPath: "/explore",
15
15
  appName: "minimal",
@@ -17,16 +17,12 @@ const minimalTarget: AkanMobileTargetConfig = {
17
17
  version: "0.0.1",
18
18
  buildNum: 1,
19
19
  permissions: ["push"],
20
- native: { android: { googleServices: "secrets/google-services.json" } },
21
- deepLinks: {
22
- schemes: ["minimal"],
23
- domains: ["example.com"],
24
- ios: { teamId: "TEAMID" },
25
- android: { sha256CertFingerprints: ["00:11"] },
26
- },
20
+ deepLinks: { schemes: ["minimal"], domains: ["example.com"] },
21
+ ios: { teamId: "TEAMID" },
22
+ android: { googleServices: "secrets/google-services.json", sha256CertFingerprints: ["00:11"] },
27
23
  };
28
24
 
29
- const adminTarget: AkanMobileTargetConfig = {
25
+ const adminTarget: AkanNativeTarget = {
30
26
  name: "admin",
31
27
  basePath: "admin",
32
28
  appName: "Portal Admin",
@@ -34,14 +30,15 @@ const adminTarget: AkanMobileTargetConfig = {
34
30
  fileName: "portal-admin",
35
31
  version: "2.1.0",
36
32
  buildNum: 42,
37
- assets: { icon: "assets/icon.png", splash: "assets/splash.png" },
33
+ icon: "assets/icon.png",
34
+ splash: "assets/splash.png",
38
35
  permissions: ["camera", "location", "contacts", "speech"],
39
36
  deepLinks: { domains: ["portal.example"] },
40
- files: { "android/res/raw/chime.mp3": "assets/chime.mp3" },
41
- native: {
42
- plugins: ["iap"],
43
- ios: { infoPlist: { ITSAppUsesNonExemptEncryption: false } },
44
- android: { manifest: ['<queries><package android:name="com.kakao.talk" /></queries>'] },
37
+ plugins: ["iap"],
38
+ ios: { infoPlist: { ITSAppUsesNonExemptEncryption: false } },
39
+ android: {
40
+ manifest: ['<queries><package android:name="com.kakao.talk" /></queries>'],
41
+ files: { "res/raw/chime.mp3": "assets/chime.mp3" },
45
42
  },
46
43
  };
47
44
 
@@ -52,7 +49,7 @@ describe("NativeConfig.build", () => {
52
49
  const { config, warnings } = NativeConfig.build({
53
50
  appPath: "/repo/apps/minimal",
54
51
  target: minimalTarget,
55
- webDir: "/repo/apps/minimal/.akan/mobile/default/web",
52
+ webDir: "/repo/apps/minimal/.akan/native/default/web",
56
53
  contributions: [{ permission: "push", plugins: ["push"] }],
57
54
  locales: ["en", "ko"],
58
55
  platform: "android",
@@ -61,7 +58,7 @@ describe("NativeConfig.build", () => {
61
58
  expect(warnings).toEqual([]);
62
59
  expect(config).toEqual({
63
60
  app: { id: "com.minimal.dev.app", name: "minimal", fileName: "minimal", version: "0.0.1", build: 1 },
64
- web: { dir: "/repo/apps/minimal/.akan/mobile/default/web" },
61
+ web: { dir: "/repo/apps/minimal/.akan/native/default/web" },
65
62
  plugins: [...NativeConfig.basePlugins, "single-instance", "push"],
66
63
  capabilities: [
67
64
  {
@@ -83,7 +80,7 @@ describe("NativeConfig.build", () => {
83
80
  const { config, warnings } = NativeConfig.build({
84
81
  appPath: "/repo/apps/portal",
85
82
  target: adminTarget,
86
- webDir: "/repo/apps/portal/.akan/mobile/admin/web",
83
+ webDir: "/repo/apps/portal/.akan/native/admin/web",
87
84
  contributions: [],
88
85
  locales: ["en", "ko"],
89
86
  platform: "android",
@@ -92,7 +89,7 @@ describe("NativeConfig.build", () => {
92
89
  expect(warnings).toEqual(["Permission 'speech' has no native plugin yet; the app ships without it."]);
93
90
  expect(config).toEqual({
94
91
  app: { id: "com.portal.admin", name: "Portal Admin", fileName: "portal-admin", version: "2.1.0", build: 42 },
95
- web: { dir: "/repo/apps/portal/.akan/mobile/admin/web" },
92
+ web: { dir: "/repo/apps/portal/.akan/native/admin/web" },
96
93
  plugins: [...NativeConfig.basePlugins, "camera", "geolocation", "contacts", "iap"],
97
94
  capabilities: [
98
95
  {
@@ -129,7 +126,15 @@ describe("NativeConfig.build", () => {
129
126
  test("a plugin that claims a permission replaces the builtin one and adds its Android entries", () => {
130
127
  const { config } = NativeConfig.build({
131
128
  appPath: "/repo/apps/portal",
132
- target: { ...adminTarget, permissions: ["camera"], native: undefined, files: undefined, assets: undefined },
129
+ target: {
130
+ ...adminTarget,
131
+ permissions: ["camera"],
132
+ plugins: undefined,
133
+ ios: undefined,
134
+ android: undefined,
135
+ icon: undefined,
136
+ splash: undefined,
137
+ },
133
138
  webDir: "/web",
134
139
  contributions: [
135
140
  {
@@ -158,7 +163,7 @@ describe("NativeConfig.build", () => {
158
163
  });
159
164
 
160
165
  test("updates bring their plugin and follow the binary's backend unless a channel is named", () => {
161
- const build = (updates: AkanMobileTargetConfig["updates"], env?: "main" | "develop") =>
166
+ const build = (updates: AkanNativeTarget["updates"], env?: "main" | "develop") =>
162
167
  NativeConfig.build({
163
168
  appPath: "/repo/apps/portal",
164
169
  target: { ...adminTarget, updates },
@@ -190,14 +195,10 @@ describe("NativeConfig.build", () => {
190
195
  appPath: "/repo/apps/portal",
191
196
  target: {
192
197
  ...adminTarget,
193
- assets: {
194
- icon: { image: "assets/icon.png", backgroundColor: "#ffffff" },
195
- splash: { backgroundColor: { light: "#ffffff", dark: "#000000" }, autoHide: false },
196
- },
197
- native: {
198
- push: { android: { smallIcon: "assets/noti.png", color: "#ff5a5f", channel: { id: "chat", name: "Chat" } } },
199
- privacy: { tracking: false, accessedApis: { DiskSpace: ["E174.1"] } },
200
- },
198
+ icon: { image: "assets/icon.png", backgroundColor: "#ffffff" },
199
+ splash: { backgroundColor: { light: "#ffffff", dark: "#000000" }, autoHide: false },
200
+ android: { push: { smallIcon: "assets/noti.png", color: "#ff5a5f", channel: { id: "chat", name: "Chat" } } },
201
+ ios: { privacy: { tracking: false, accessedApis: { DiskSpace: ["E174.1"] } } },
201
202
  },
202
203
  webDir: "/web",
203
204
  contributions: [],
@@ -238,14 +239,11 @@ describe("NativeConfig.build", () => {
238
239
  await writeFile(path.join(root, "web/index.html"), "<html><head></head><body></body></html>");
239
240
  const api = await NativeApi.load(repoApp);
240
241
 
241
- const adminWithEverything: AkanMobileTargetConfig = {
242
+ const adminWithEverything: AkanNativeTarget = {
242
243
  ...adminTarget,
243
244
  updates: { url: "https://updates.example.com/portal", publicKey: updatesKey },
244
- native: {
245
- ...adminTarget.native,
246
- push: { android: { smallIcon: "assets/icon.png", color: "#ff5a5f" } },
247
- privacy: { tracking: false },
248
- },
245
+ ios: { ...adminTarget.ios, privacy: { tracking: false } },
246
+ android: { ...adminTarget.android, push: { smallIcon: "assets/icon.png", color: "#ff5a5f" } },
249
247
  };
250
248
  for (const target of [minimalTarget, adminWithEverything]) {
251
249
  const { config } = NativeConfig.build({
@@ -267,7 +265,13 @@ describe("NativeConfig.build", () => {
267
265
  await writeManifest(path.join(root, "vendor", "led-panel"), "led-panel");
268
266
  const { config } = NativeConfig.build({
269
267
  appPath: root,
270
- target: { ...minimalTarget, permissions: [], native: { plugins: ["./vendor/led-panel"] }, deepLinks: undefined },
268
+ target: {
269
+ ...minimalTarget,
270
+ permissions: [],
271
+ plugins: ["./vendor/led-panel"],
272
+ deepLinks: undefined,
273
+ android: undefined,
274
+ },
271
275
  webDir: path.join(root, "web"),
272
276
  contributions: [],
273
277
  locales: ["en"],
@@ -293,8 +297,9 @@ describe("NativeConfig.build", () => {
293
297
  target: {
294
298
  ...minimalTarget,
295
299
  permissions: [],
296
- native: { plugins: ["./native/kiosk/", "kiosk", "haptics"] },
300
+ plugins: ["./native/kiosk/", "kiosk", "haptics"],
297
301
  deepLinks: undefined,
302
+ android: undefined,
298
303
  },
299
304
  webDir: path.join(root, "web"),
300
305
  contributions: [],
@@ -314,10 +319,8 @@ describe("NativeConfig.build", () => {
314
319
  ...minimalTarget,
315
320
  permissions: [],
316
321
  deepLinks: undefined,
317
- native: {
318
- android: { autoplay: true },
319
- desktop: { recovery: "reload", window: { fullscreen: true, skipTaskbar: true }, screenCapture: "auto" },
320
- },
322
+ android: { autoplay: true },
323
+ desktop: { recovery: "reload", window: { fullscreen: true, skipTaskbar: true }, screenCapture: "auto" },
321
324
  },
322
325
  webDir: path.join(root, "web"),
323
326
  contributions: [],
@@ -342,7 +345,7 @@ describe("NativeConfig.build", () => {
342
345
  const updates = { url: "https://releases.example.com/board", publicKey: `${"a".repeat(43)}=`, channel: "pilot" };
343
346
  const { config } = NativeConfig.build({
344
347
  appPath: "/repo/apps/board",
345
- target: { ...minimalTarget, permissions: [], deepLinks: undefined, native: undefined, updates },
348
+ target: { ...minimalTarget, permissions: [], deepLinks: undefined, android: undefined, updates },
346
349
  webDir: "/web",
347
350
  contributions: [],
348
351
  locales: ["en"],
@@ -362,7 +365,7 @@ describe("NativeConfig.build", () => {
362
365
  ...minimalTarget,
363
366
  permissions: [],
364
367
  deepLinks: undefined,
365
- native: undefined,
368
+ android: undefined,
366
369
  updates: { ...updates, ...(channel ? { channel } : {}) },
367
370
  },
368
371
  webDir: "/web",
@@ -1,13 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import type { AkanNativeConfig } from "@akanjs/native/config";
4
- import type {
5
- AkanMobileTargetConfig,
6
- AkanNativeValue,
7
- AkanPluginNativeConfig,
8
- MobileEnv,
9
- MobilePermission,
10
- } from "akanjs";
4
+ import type { AkanNativeTarget, AkanNativeValue, AkanPluginNativeConfig, NativeEnv, NativePermission } from "akanjs";
11
5
  import type { DesktopServerBundle } from "./desktopServerStage";
12
6
  import { type NativePlatform, resolveAppId } from "./mobileTarget";
13
7
  import type { NativePluginFolder } from "./nativePluginFolders";
@@ -16,16 +10,16 @@ import { toIosInfoPlistUsageDescriptions } from "./usageDescriptions";
16
10
  export interface NativeConfigInput {
17
11
  /** The app folder; every relative path in the target resolves against it. */
18
12
  appPath: string;
19
- target: AkanMobileTargetConfig;
13
+ target: AkanNativeTarget;
20
14
  /** The assembled web root the app loads. */
21
15
  webDir: string;
22
16
  /** What the app's and its libs' plugins declare. */
23
17
  contributions: AkanPluginNativeConfig[];
24
18
  locales: readonly string[];
25
19
  /** The backend the binary talks to; an updates channel left unnamed follows it. */
26
- env?: MobileEnv;
20
+ env?: NativeEnv;
27
21
  platform: NativePlatform;
28
- /** A desktop build that carries its server (the target's `native.desktop.server`). */
22
+ /** A desktop build that carries its server (the target's `desktop.server`). */
29
23
  desktopServer?: DesktopServerBundle;
30
24
  /** The plugins in the app's and its libs' `native/` folders. */
31
25
  nativePlugins?: NativePluginFolder[];
@@ -57,7 +51,7 @@ export class NativeConfig {
57
51
  "haptics",
58
52
  ] as const;
59
53
  //* Used when no plugin of the app or its libs claims the permission; speech has no native plugin yet.
60
- static readonly builtinContributions: { [permission in MobilePermission]: AkanPluginNativeConfig } = {
54
+ static readonly builtinContributions: { [permission in NativePermission]: AkanPluginNativeConfig } = {
61
55
  camera: {
62
56
  permission: "camera",
63
57
  plugins: ["camera"],
@@ -112,7 +106,7 @@ export class NativeConfig {
112
106
  const folders = new Set(nativePlugins.map((plugin) => path.resolve(plugin.dir)));
113
107
  const folderIds = new Set(nativePlugins.map((plugin) => plugin.id));
114
108
  //? A `native/<id>` folder ships by itself; listed again, by path or by id, it would reach the runtime twice.
115
- const listed = (target.native?.plugins ?? []).filter((spec) =>
109
+ const listed = (target.plugins ?? []).filter((spec) =>
116
110
  NativeConfig.#isId(spec) ? !folderIds.has(spec) : !folders.has(path.resolve(appPath, spec)),
117
111
  );
118
112
  const plugins = [
@@ -142,11 +136,11 @@ export class NativeConfig {
142
136
  const ios = NativeConfig.#compact<NativeIos>({
143
137
  infoPlist: NativeConfig.#merged(
144
138
  applied.map((c) => c.infoPlist),
145
- target.native?.ios?.infoPlist,
139
+ target.ios?.infoPlist,
146
140
  ),
147
141
  entitlements: NativeConfig.#merged(
148
142
  applied.map((c) => c.entitlements),
149
- target.native?.ios?.entitlements,
143
+ target.ios?.entitlements,
150
144
  ),
151
145
  });
152
146
  const manifest = [
@@ -156,21 +150,24 @@ export class NativeConfig {
156
150
  ...[...new Set(applied.flatMap((c) => c.androidFeatures ?? []))].map(
157
151
  (name) => `<uses-feature android:name="${name}" android:required="false" />`,
158
152
  ),
159
- ...(target.native?.android?.manifest ?? []),
153
+ ...(target.android?.manifest ?? []),
160
154
  ];
161
155
  const android = NativeConfig.#compact({
162
156
  manifest,
163
- application: target.native?.android?.application ?? [],
164
- activity: target.native?.android?.activity ?? [],
157
+ application: target.android?.application ?? [],
158
+ activity: target.android?.activity ?? [],
165
159
  });
166
- const resources = Object.entries(target.files ?? {}).map(([to, from]) => ({ from: abs(from), to }));
160
+ const resources = [
161
+ ...Object.entries(target.ios?.files ?? {}).map(([to, from]) => ({ from: abs(from), to: `ios/${to}` })),
162
+ ...Object.entries(target.android?.files ?? {}).map(([to, from]) => ({ from: abs(from), to: `android/${to}` })),
163
+ ];
167
164
  const native = NativeConfig.#compact({ ios, android, resources });
168
- const googleServices = target.native?.android?.googleServices;
169
- const pushAndroid = target.native?.push?.android;
165
+ const googleServices = target.android?.googleServices;
166
+ const pushAndroid = target.android?.push;
170
167
  const desktop = NativeConfig.#compact<NonNullable<AkanNativeConfig["desktop"]>>({
171
- recovery: target.native?.desktop?.recovery,
172
- screenCapture: target.native?.desktop?.screenCapture,
173
- window: NativeConfig.#compact({ ...target.native?.desktop?.window }),
168
+ recovery: target.desktop?.recovery,
169
+ screenCapture: target.desktop?.screenCapture,
170
+ window: NativeConfig.#compact({ ...target.desktop?.window }),
174
171
  server: desktopServer,
175
172
  bin: desktopBin,
176
173
  });
@@ -206,30 +203,30 @@ export class NativeConfig {
206
203
  },
207
204
  }
208
205
  : {}),
209
- ...(target.native?.privacy ? { privacy: target.native.privacy } : {}),
206
+ ...(target.ios?.privacy ? { privacy: target.ios.privacy } : {}),
210
207
  //? assetlinks.json vouches for `<appId>.debug` outside main, the suffix a debug build installs under.
211
208
  android: {
212
209
  debugAppIdSuffix: ".debug",
213
210
  ...(googleServices ? { googleServices: abs(googleServices) } : {}),
214
- ...(target.native?.android?.autoplay ? { autoplay: true } : {}),
211
+ ...(target.android?.autoplay ? { autoplay: true } : {}),
215
212
  },
216
213
  keyboard: { resize: "none" },
217
214
  ...(desktop ? { desktop } : {}),
218
- ...(target.assets?.icon ? { icon: NativeConfig.#icon(target.assets.icon, abs) } : {}),
219
- ...(target.assets?.splash ? { splash: NativeConfig.#splash(target.assets.splash, abs) } : {}),
215
+ ...(target.icon ? { icon: NativeConfig.#icon(target.icon, abs) } : {}),
216
+ ...(target.splash ? { splash: NativeConfig.#splash(target.splash, abs) } : {}),
220
217
  };
221
218
  return { config, warnings };
222
219
  }
223
220
 
224
221
  static #icon(
225
- icon: NonNullable<NonNullable<AkanMobileTargetConfig["assets"]>["icon"]>,
222
+ icon: NonNullable<AkanNativeTarget["icon"]>,
226
223
  abs: (relative: string) => string,
227
224
  ): NonNullable<AkanNativeConfig["icon"]> {
228
225
  return typeof icon === "string" ? abs(icon) : { ...icon, image: abs(icon.image) };
229
226
  }
230
227
 
231
228
  static #splash(
232
- splash: NonNullable<NonNullable<AkanMobileTargetConfig["assets"]>["splash"]>,
229
+ splash: NonNullable<AkanNativeTarget["splash"]>,
233
230
  abs: (relative: string) => string,
234
231
  ): NonNullable<AkanNativeConfig["splash"]> {
235
232
  if (typeof splash === "string") return { image: abs(splash) };
@@ -238,8 +235,8 @@ export class NativeConfig {
238
235
 
239
236
  //? akanConfig refuses a target whose merged updates lack url or publicKey; the type still has them optional.
240
237
  static #updates(
241
- target: AkanMobileTargetConfig,
242
- env?: MobileEnv,
238
+ target: AkanNativeTarget,
239
+ env?: NativeEnv,
243
240
  ): NonNullable<AkanNativeConfig["updates"]> | undefined {
244
241
  const { url, publicKey, channel = env, ...rest } = target.updates ?? {};
245
242
  return url && publicKey ? { ...rest, url, publicKey, ...(channel ? { channel } : {}) } : undefined;
@@ -286,7 +283,7 @@ export class NativeConfig {
286
283
  }
287
284
 
288
285
  //* Every route sits under /:lang, so an app link limited to a basePath lists it once per locale.
289
- static #deepLinks(target: AkanMobileTargetConfig, locales: readonly string[]) {
286
+ static #deepLinks(target: AkanNativeTarget, locales: readonly string[]) {
290
287
  const basePath = target.basePath?.replace(/^\/+|\/+$/g, "");
291
288
  const pathPrefixes = basePath ? locales.map((locale) => `/${locale}/${basePath}`) : undefined;
292
289
  return {
@@ -1,13 +1,13 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import type { AkanMobileTargetConfig } from "../akanConfig";
4
+ import type { AkanNativeTarget } from "../akanConfig";
5
5
  import { tempDirs } from "../testHelpers";
6
6
  import { NativeWebDir } from "./nativeWebDir";
7
7
 
8
8
  const makeTempRoot = tempDirs("akan-native-web-");
9
9
 
10
- const target: AkanMobileTargetConfig = {
10
+ const target: AkanNativeTarget = {
11
11
  name: "admin",
12
12
  basePath: "/admin/",
13
13
  indexPath: "/home",
@@ -68,7 +68,7 @@ describe("NativeWebDir", () => {
68
68
  const root = await makeTempRoot();
69
69
  const web = new NativeWebDir(path.join(root, "web"));
70
70
  await expect(web.assemble(target, { html: path.join(root, "none.html") })).rejects.toThrow(
71
- "CSR html for mobile target 'admin' not found",
71
+ "CSR html for native target 'admin' not found",
72
72
  );
73
73
 
74
74
  await write(path.join(root, "index.html"), "<html><head></head></html>");
@@ -1,6 +1,6 @@
1
1
  import { cp, mkdir, rm, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import type { AkanMobileTargetConfig } from "akanjs";
3
+ import type { AkanNativeTarget } from "akanjs";
4
4
 
5
5
  export interface NativeWebDirSources {
6
6
  /** The target's CSR bundle: one HTML file with its scripts and styles inlined. */
@@ -19,7 +19,7 @@ export class NativeWebDir {
19
19
 
20
20
  //* The bundle is inlined into this page and its own code names the global, so only the tag marks an injection;
21
21
  //* it goes first in <head>, ahead of every script, because the page reads it as it boots.
22
- static injectTarget(html: string, target: Pick<AkanMobileTargetConfig, "name" | "basePath" | "indexPath">) {
22
+ static injectTarget(html: string, target: Pick<AkanNativeTarget, "name" | "basePath" | "indexPath">) {
23
23
  if (html.includes(NativeWebDir.#targetTag)) return html;
24
24
  const basePath = target.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
25
25
  const script = `${NativeWebDir.#targetTag}${JSON.stringify({
@@ -40,9 +40,9 @@ export class NativeWebDir {
40
40
  }
41
41
 
42
42
  /** Rebuilds the folder from scratch and answers its files as web paths: relative, `/`-separated and sorted. */
43
- async assemble(target: AkanMobileTargetConfig, { html, publicDir, fontsDir }: NativeWebDirSources) {
43
+ async assemble(target: AkanNativeTarget, { html, publicDir, fontsDir }: NativeWebDirSources) {
44
44
  if (!(await Bun.file(html).exists()))
45
- throw new Error(`CSR html for mobile target '${target.name}' not found: ${html}`);
45
+ throw new Error(`CSR html for native target '${target.name}' not found: ${html}`);
46
46
  await this.clear();
47
47
  if (publicDir && (await NativeWebDir.#isDir(publicDir))) await cp(publicDir, this.dir, { recursive: true });
48
48
  if (fontsDir && (await NativeWebDir.#isDir(fontsDir)))