@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.
@@ -14,8 +14,12 @@ import {
14
14
  type AkanAssetsConfig,
15
15
  type AkanBinConfig,
16
16
  type AkanDatabaseConfig,
17
- type AkanMobileConfig,
18
- type AkanMobileTargetConfig,
17
+ type AkanNativeAppConfig,
18
+ type AkanNativeAppResult,
19
+ type AkanNativeDeepLinks,
20
+ type AkanNativeSettings,
21
+ type AkanNativeTarget,
22
+ type AkanNativeUpdatesConfig,
19
23
  type AkanRouteConfig,
20
24
  type AkanWebConfig,
21
25
  type AkanWebOption,
@@ -94,25 +98,18 @@ const normalizeDeepLinkDomain = (domain: string) => {
94
98
  }
95
99
  };
96
100
 
97
- const normalizeDeepLinks = (deepLinks: DeepPartial<AkanMobileTargetConfig["deepLinks"]> | undefined) => {
98
- if (!deepLinks) return undefined;
99
- const schemes = normalizeStringList(deepLinks.schemes as string[] | undefined);
100
- const domains = normalizeStringList((deepLinks.domains as string[] | undefined)?.map(normalizeDeepLinkDomain));
101
- const teamId = (deepLinks.ios?.teamId as string | undefined)?.trim();
102
- const sha256CertFingerprints = normalizeStringList(deepLinks.android?.sha256CertFingerprints as string[] | undefined);
103
- if (!schemes && !domains && !teamId && !sha256CertFingerprints) return undefined;
104
- return {
105
- ...(schemes ? { schemes } : {}),
106
- ...(domains ? { domains } : {}),
107
- ...(teamId ? { ios: { teamId } } : {}),
108
- ...(sha256CertFingerprints ? { android: { sha256CertFingerprints } } : {}),
109
- } satisfies AkanMobileTargetConfig["deepLinks"];
101
+ const normalizeDeepLinks = (deepLinks: AkanNativeDeepLinks | undefined): AkanNativeDeepLinks | undefined => {
102
+ const schemes = normalizeStringList(deepLinks?.schemes);
103
+ const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
104
+ if (!schemes && !domains) return undefined;
105
+ return { ...(schemes ? { schemes } : {}), ...(domains ? { domains } : {}) };
110
106
  };
111
107
 
112
- type AppConfigDeclaration = Omit<DeepPartial<AppConfigResult>, "docker" | "web" | "bin"> & {
108
+ type AppConfigDeclaration = Omit<DeepPartial<AppConfigResult>, "docker" | "web" | "bin" | "native"> & {
113
109
  docker?: DockerOption;
114
110
  web?: AkanWebOption;
115
111
  bin?: AkanBinConfig;
112
+ native?: AkanNativeAppConfig;
116
113
  };
117
114
 
118
115
  type LibConfigDeclaration = Omit<DeepPartial<LibConfigResult>, "bin"> & { bin?: AkanBinConfig };
@@ -172,9 +169,9 @@ export class AkanAppConfig implements AppConfigResult {
172
169
  i18n: AkanI18nConfig;
173
170
  api: AkanApiConfig;
174
171
  publicEnv: string[];
175
- mobile: AkanMobileConfig;
176
- /** True only when the app's akan.config.ts explicitly declares a `mobile` section (vs. the synthesized default). */
177
- hasMobileConfig: boolean;
172
+ native: AkanNativeAppResult;
173
+ /** True only when the app's akan.config.ts declares a `native` section (vs. the synthesized default target). */
174
+ hasNativeConfig: boolean;
178
175
  secrets: string[];
179
176
  assets: AkanAssetsConfig;
180
177
  /** Raw setting, resolved against the app's lib deps at sync time. */
@@ -240,8 +237,12 @@ export class AkanAppConfig implements AppConfigResult {
240
237
  ],
241
238
  };
242
239
  this.syncPageLibs = (config?.syncPageLibs as string[] | boolean | undefined) ?? false;
243
- this.hasMobileConfig = Boolean(config.mobile);
244
- this.mobile = this.#resolveMobileConfig(config.mobile);
240
+ if ((config as { mobile?: unknown }).mobile !== undefined)
241
+ throw new Error(
242
+ `apps/${this.app.name}/akan.config.ts declares \`mobile\`, which is now \`native\`: the platform settings sit in native.ios, native.android and native.desktop, and a target overrides them under native.targets.<name>.`,
243
+ );
244
+ this.hasNativeConfig = Boolean(config.native);
245
+ this.native = this.#resolveNativeConfig(config.native);
245
246
  this.web = this.#resolveWebConfig(config.web);
246
247
  this.docker = AkanAppConfig.#resolveDocker(config.docker, libContributions.docker);
247
248
  this.dockerfile = this.#makeDockerfile();
@@ -249,166 +250,185 @@ export class AkanAppConfig implements AppConfigResult {
249
250
  #resolveWebConfig(web: AkanWebOption | undefined): AkanWebConfig {
250
251
  const resolved = typeof web === "object" ? { ssr: true, csr: web.csr } : { ssr: web ?? true, csr: web ?? true };
251
252
  // `akan build-ios` / `build-android` copy `dist/apps/<app>/csr/<target>.html` into the native project.
252
- if (!resolved.csr && this.hasMobileConfig)
253
+ if (!resolved.csr && this.hasNativeConfig)
253
254
  throw new Error(
254
- `apps/${this.app.name}/akan.config.ts turns the CSR bundle off but declares mobile targets; the mobile apps ship that bundle. Drop the mobile section or leave CSR on.`,
255
+ `apps/${this.app.name}/akan.config.ts turns the CSR bundle off but declares a native section; the native apps ship that bundle. Drop the native section or leave CSR on.`,
255
256
  );
256
257
  return resolved;
257
258
  }
258
- #resolveMobileConfig(mobile: DeepPartial<AkanMobileConfig> | undefined): AkanMobileConfig {
259
- const {
260
- targets: rawTargets,
261
- indexPath: _indexPath,
262
- ...rawMobile
263
- } = (mobile ?? {}) as DeepPartial<AkanMobileConfig> & { indexPath?: unknown };
259
+ #resolveNativeConfig(native: AkanNativeAppConfig | undefined): AkanNativeAppResult {
264
260
  const configPath = `apps/${this.app.name}/akan.config.ts`;
265
- AkanAppConfig.#assertMobileKeys(rawMobile, AkanAppConfig.#mobileKeys, "mobile", configPath);
266
- const appName = rawMobile.appName ?? this.app.name;
267
- const appId = rawMobile.appId ?? deriveDefaultAppId(this.baseDevEnv.repoName, this.app.name);
268
- const version = rawMobile.version ?? "0.0.1";
269
- const buildNum = rawMobile.buildNum ?? 1;
270
- const files = AkanAppConfig.#resolveMobileFiles(rawMobile.files as AkanMobileConfig["files"], "mobile", configPath);
271
- const native = rawMobile.native as AkanMobileConfig["native"];
272
- const updates = rawMobile.updates as AkanMobileConfig["updates"];
273
- const defaultTargetName = this.#defaultMobileTargetName(rawTargets);
274
- const targetEntries = Object.entries(
275
- rawTargets ?? {
276
- [defaultTargetName]: {},
277
- },
278
- );
261
+ const { targets: rawTargets, ...root } = native ?? {};
262
+ AkanAppConfig.#assertNativeKeys(root, "native", configPath);
263
+ AkanAppConfig.#assertFiles(root, "native", configPath);
264
+ const appName = root.appName ?? this.app.name;
265
+ const appId = root.appId ?? deriveDefaultAppId(this.baseDevEnv.repoName, this.app.name);
266
+ const version = root.version ?? "0.0.1";
267
+ const buildNum = root.buildNum ?? 1;
268
+ const named = rawTargets !== undefined && Object.keys(rawTargets).length > 0;
269
+ const entries: [string, AkanNativeSettings][] = named
270
+ ? Object.entries(rawTargets)
271
+ : [[this.basePaths.has(this.app.name) ? this.app.name : "default", {}]];
279
272
  const targets = Object.fromEntries(
280
- targetEntries.map(([name, rawTarget]) => {
281
- const target = (rawTarget ?? {}) as DeepPartial<AkanMobileTargetConfig>;
282
- const where = `mobile.targets.${name}`;
283
- AkanAppConfig.#assertMobileKeys(target, AkanAppConfig.#targetKeys, where, configPath);
284
- const fallbackBasePath = !rawTargets && this.basePaths.has(name) ? name : undefined;
285
- const basePath = (target.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") || undefined;
286
- const indexPath = normalizeIndexPath(target.indexPath as string | undefined);
287
- const deepLinks = normalizeDeepLinks(target.deepLinks);
288
- if (basePath && !this.basePaths.has(basePath)) {
289
- throw new Error(`Mobile target '${name}' uses unknown basePath '${basePath}' in ${configPath}`);
290
- }
291
- const targetFiles = AkanAppConfig.#resolveMobileFiles(
292
- target.files as AkanMobileTargetConfig["files"],
293
- where,
294
- configPath,
295
- );
296
- const fileName = (target.fileName ?? rawMobile.fileName) as string | undefined;
297
- const targetUpdates = updates || target.updates ? { ...updates, ...target.updates } : undefined;
298
- if (targetUpdates && (!targetUpdates.url || !targetUpdates.publicKey))
273
+ entries.map(([name, own]) => {
274
+ const where = named ? `native.targets.${name}` : "native";
275
+ AkanAppConfig.#assertNativeKeys(own ?? {}, where, configPath);
276
+ const merged = AkanAppConfig.#overlay(root, own ?? {});
277
+ const fallbackBasePath = !named && this.basePaths.has(name) ? name : undefined;
278
+ const basePath = (merged.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") || undefined;
279
+ if (basePath && !this.basePaths.has(basePath))
280
+ throw new Error(`Native target '${name}' uses unknown basePath '${basePath}' in ${configPath}`);
281
+ const { updates } = merged;
282
+ if (updates && (!updates.url || !updates.publicKey))
299
283
  throw new Error(
300
- `${where}.updates in ${configPath} has no url or publicKey; give them in mobile.updates or the target's own.`,
284
+ `${where}.updates in ${configPath} has no url or publicKey${named ? "; give them in native.updates or the target's own" : ""}.`,
301
285
  );
302
- const resolved = {
286
+ AkanAppConfig.#assertFiles(own ?? {}, where, configPath);
287
+ const {
288
+ basePath: _basePath,
289
+ indexPath: rawIndexPath,
290
+ deepLinks: rawDeepLinks,
291
+ updates: _updates,
292
+ ...rest
293
+ } = merged;
294
+ const indexPath = normalizeIndexPath(rawIndexPath);
295
+ const deepLinks = normalizeDeepLinks(rawDeepLinks);
296
+ const { teamId: rawTeamId, ...iosRest } = merged.ios ?? {};
297
+ const teamId = rawTeamId?.trim();
298
+ const { sha256CertFingerprints: rawFingerprints, ...androidRest } = merged.android ?? {};
299
+ const fingerprints = normalizeStringList(rawFingerprints);
300
+ const target: AkanNativeTarget = {
301
+ ...rest,
303
302
  name,
304
- basePath,
305
- indexPath,
306
- deepLinks,
307
- appName: target.appName ?? appName,
308
- appId: target.appId ?? appId,
309
- ...(fileName ? { fileName } : {}),
310
- version: target.version ?? version,
311
- buildNum: target.buildNum ?? buildNum,
312
- ...(target.assets ? { assets: target.assets as AkanMobileTargetConfig["assets"] } : {}),
313
- ...(target.permissions ? { permissions: target.permissions as AkanMobileTargetConfig["permissions"] } : {}),
314
- ...(files || targetFiles ? { files: { ...files, ...targetFiles } } : {}),
315
- ...(native || target.native
316
- ? { native: AkanAppConfig.#mergeNative(native, target.native as AkanMobileTargetConfig["native"]) }
303
+ ...(basePath ? { basePath } : {}),
304
+ ...(indexPath ? { indexPath } : {}),
305
+ ...(deepLinks ? { deepLinks } : {}),
306
+ ...(merged.ios ? { ios: { ...iosRest, ...(teamId ? { teamId } : {}) } } : {}),
307
+ ...(merged.android
308
+ ? { android: { ...androidRest, ...(fingerprints ? { sha256CertFingerprints: fingerprints } : {}) } }
317
309
  : {}),
318
- ...(targetUpdates ? { updates: targetUpdates } : {}),
319
- } satisfies AkanMobileTargetConfig;
320
- return [name, resolved];
310
+ appName: merged.appName ?? this.app.name,
311
+ appId: merged.appId ?? appId,
312
+ version: merged.version ?? version,
313
+ buildNum: merged.buildNum ?? buildNum,
314
+ ...(updates ? { updates: updates as AkanNativeUpdatesConfig } : {}),
315
+ };
316
+ return [name, target];
321
317
  }),
322
318
  );
323
- return {
324
- appName,
325
- appId,
326
- ...(rawMobile.fileName ? { fileName: rawMobile.fileName as string } : {}),
327
- version,
328
- buildNum,
329
- ...(files ? { files } : {}),
330
- ...(native ? { native } : {}),
331
- ...(updates ? { updates } : {}),
332
- targets,
319
+ return { appName, appId, ...(root.fileName ? { fileName: root.fileName } : {}), version, buildNum, targets };
320
+ }
321
+ /** `own` over `base`: plain objects merge key by key, and any other value, a list included, replaces. */
322
+ static #overlay(base: AkanNativeSettings, own: AkanNativeSettings): AkanNativeSettings {
323
+ const merge = (kept: unknown, value: unknown, top: boolean, key: string): unknown => {
324
+ if (!AkanAppConfig.#isPlainObject(kept) || !AkanAppConfig.#isPlainObject(value)) return value;
325
+ //? An icon or splash given as an object is one value: its image and color go together.
326
+ if (top && (key === "icon" || key === "splash")) return value;
327
+ const out: Record<string, unknown> = { ...kept };
328
+ for (const [inner, next] of Object.entries(value))
329
+ if (next !== undefined) out[inner] = merge(out[inner], next, false, inner);
330
+ return out;
333
331
  };
332
+ const out: Record<string, unknown> = { ...base };
333
+ for (const [key, value] of Object.entries(own))
334
+ if (value !== undefined) out[key] = merge(out[key], value, true, key);
335
+ return out as AkanNativeSettings;
334
336
  }
335
- static readonly #mobileKeys = ["appName", "appId", "fileName", "version", "buildNum", "files", "native", "updates"];
336
- static readonly #targetKeys = [
337
- "basePath",
338
- "indexPath",
339
- "appName",
340
- "appId",
341
- "fileName",
342
- "version",
343
- "buildNum",
344
- "assets",
345
- "permissions",
346
- "deepLinks",
347
- "files",
348
- "native",
349
- "updates",
350
- ];
351
- //* The Capacitor keys used to pass straight through; the native runtime takes typed ones, so a leftover is refused.
352
- static #assertMobileKeys(section: object, known: string[], where: string, configPath: string) {
353
- for (const key of Object.keys(section)) {
354
- if (known.includes(key)) continue;
355
- if (key === "plugins" || key === "ios" || key === "android")
356
- throw new Error(
357
- `${where}.${key} in ${configPath} is a Capacitor setting. The native runtime reads native.plugins, native.ios.{infoPlist,entitlements} and native.android.{manifest,application,activity,googleServices} instead.`,
358
- );
359
- throw new Error(`${where}.${key} in ${configPath} is not a mobile setting. Known: ${known.join(", ")}.`);
360
- }
337
+ static #isPlainObject(value: unknown): value is Record<string, unknown> {
338
+ return typeof value === "object" && value !== null && !Array.isArray(value);
361
339
  }
362
- static readonly #fileTargetPattern = /^(?:ios\/.+|android\/res\/[^/]+\/[^/]+|android\/assets\/.+)$/;
363
- static #resolveMobileFiles(files: AkanMobileTargetConfig["files"] | undefined, where: string, configPath: string) {
364
- if (!files) return undefined;
365
- for (const [to, from] of Object.entries(files)) {
366
- if (typeof from !== "string")
367
- throw new Error(
368
- `${where}.files in ${configPath} maps where a file lands to its source now, e.g. { "android/res/raw/chime.mp3": "assets/chime.mp3" }. google-services.json goes to native.android.googleServices, and GoogleService-Info.plist is no longer needed.`,
369
- );
370
- if (!AkanAppConfig.#fileTargetPattern.test(to))
340
+ static readonly #nativeKeys = {
341
+ native: [
342
+ "basePath",
343
+ "indexPath",
344
+ "appName",
345
+ "appId",
346
+ "fileName",
347
+ "version",
348
+ "buildNum",
349
+ "icon",
350
+ "splash",
351
+ "permissions",
352
+ "plugins",
353
+ "deepLinks",
354
+ "updates",
355
+ "ios",
356
+ "android",
357
+ "desktop",
358
+ ],
359
+ ios: ["teamId", "infoPlist", "entitlements", "privacy", "files"],
360
+ android: [
361
+ "sha256CertFingerprints",
362
+ "googleServices",
363
+ "push",
364
+ "autoplay",
365
+ "manifest",
366
+ "application",
367
+ "activity",
368
+ "files",
369
+ ],
370
+ desktop: ["server", "recovery", "window", "screenCapture"],
371
+ deepLinks: ["schemes", "domains"],
372
+ } as const;
373
+ //* Settings that moved when `mobile` became `native` are named with where they went, instead of "unknown".
374
+ static readonly #movedKeys: Readonly<Record<string, string>> = {
375
+ native: "the platform sections and plugins sit directly in the native section: ios, android, desktop, plugins",
376
+ assets: "icon and splash sit directly in the native section",
377
+ files:
378
+ "ios.files (keyed by the path in the app bundle) or android.files (keyed res/<type>/<file> or assets/<path>)",
379
+ push: "android.push",
380
+ privacy: "ios.privacy",
381
+ "deepLinks.ios": "ios.teamId",
382
+ "deepLinks.android": "android.sha256CertFingerprints",
383
+ };
384
+ static #assertNativeKeys(section: object, where: string, configPath: string) {
385
+ const check = (value: unknown, known: readonly string[], at: string, kind: string) => {
386
+ if (!AkanAppConfig.#isPlainObject(value)) return;
387
+ for (const key of Object.keys(value)) {
388
+ if (known.includes(key)) continue;
389
+ const moved = AkanAppConfig.#movedKeys[kind === "native" ? key : `${kind}.${key}`];
371
390
  throw new Error(
372
- `${where}.files["${to}"] in ${configPath} must land under ios/<path>, android/res/<type>/<file> or android/assets/<path>.`,
391
+ moved
392
+ ? `${at}.${key} in ${configPath} has moved: ${moved}.`
393
+ : `${at}.${key} in ${configPath} is not a native setting. Known: ${known.join(", ")}.`,
373
394
  );
374
- }
375
- return files;
376
- }
377
- static #mergeNative(
378
- base: AkanMobileTargetConfig["native"],
379
- override: AkanMobileTargetConfig["native"],
380
- ): NonNullable<AkanMobileTargetConfig["native"]> {
381
- const plugins = [...new Set([...(base?.plugins ?? []), ...(override?.plugins ?? [])])];
382
- const ios = { ...base?.ios, ...override?.ios };
383
- const android: NonNullable<NonNullable<AkanMobileTargetConfig["native"]>["android"]> = {
384
- ...base?.android,
385
- ...override?.android,
386
- };
387
- for (const key of ["manifest", "application", "activity"] as const) {
388
- const merged = [...(base?.android?.[key] ?? []), ...(override?.android?.[key] ?? [])];
389
- if (merged.length) android[key] = merged;
390
- else delete android[key];
391
- }
392
- const pushAndroid = { ...base?.push?.android, ...override?.push?.android };
393
- const privacy = { ...base?.privacy, ...override?.privacy };
394
- const window = { ...base?.desktop?.window, ...override?.desktop?.window };
395
- const desktop = {
396
- ...base?.desktop,
397
- ...override?.desktop,
398
- ...(Object.keys(window).length ? { window } : {}),
399
- };
400
- return {
401
- ...(plugins.length ? { plugins } : {}),
402
- ...(Object.keys(ios).length ? { ios } : {}),
403
- ...(Object.keys(android).length ? { android } : {}),
404
- ...(Object.keys(pushAndroid).length ? { push: { android: pushAndroid } } : {}),
405
- ...(Object.keys(privacy).length ? { privacy } : {}),
406
- ...(Object.keys(desktop).length ? { desktop } : {}),
395
+ }
407
396
  };
397
+ check(section, AkanAppConfig.#nativeKeys.native, where, "native");
398
+ const settings = section as AkanNativeSettings;
399
+ check(settings.ios, AkanAppConfig.#nativeKeys.ios, `${where}.ios`, "ios");
400
+ check(settings.android, AkanAppConfig.#nativeKeys.android, `${where}.android`, "android");
401
+ check(settings.desktop, AkanAppConfig.#nativeKeys.desktop, `${where}.desktop`, "desktop");
402
+ check(settings.deepLinks, AkanAppConfig.#nativeKeys.deepLinks, `${where}.deepLinks`, "deepLinks");
408
403
  }
409
- #defaultMobileTargetName(rawTargets: DeepPartial<AkanMobileConfig>["targets"] | undefined) {
410
- if (rawTargets && Object.keys(rawTargets).length > 0) return Object.keys(rawTargets)[0] as string;
411
- return this.basePaths.has(this.app.name) ? this.app.name : "default";
404
+ static readonly #androidFilePattern = /^(?:res\/[^/]+\/[^/]+|assets\/.+)$/;
405
+ static #assertFiles(settings: AkanNativeSettings, where: string, configPath: string) {
406
+ const check = (
407
+ files: Record<string, string> | undefined,
408
+ at: string,
409
+ valid: (to: string) => boolean,
410
+ shape: string,
411
+ ) => {
412
+ for (const [to, from] of Object.entries(files ?? {})) {
413
+ if (typeof from !== "string")
414
+ throw new Error(
415
+ `${at}["${to}"] in ${configPath} must name its source file, relative to the app folder, e.g. { "${shape}": "assets/chime.mp3" }.`,
416
+ );
417
+ if (!valid(to)) throw new Error(`${at}["${to}"] in ${configPath} must land at ${shape}.`);
418
+ }
419
+ };
420
+ check(
421
+ settings.ios?.files,
422
+ `${where}.ios.files`,
423
+ (to) => to.length > 0 && !to.startsWith("/") && !to.split("/").includes(".."),
424
+ "<path in the app bundle>",
425
+ );
426
+ check(
427
+ settings.android?.files,
428
+ `${where}.android.files`,
429
+ (to) => AkanAppConfig.#androidFilePattern.test(to),
430
+ "res/<type>/<file> or assets/<path>",
431
+ );
412
432
  }
413
433
  #applyRoutes(routes: AkanRouteConfig[] = []) {
414
434
  for (const route of routes) {
@@ -695,7 +715,7 @@ export class AkanLibConfig implements LibConfigResult {
695
715
 
696
716
  //! need to refactor
697
717
  const shiftBuildNum = async (app: App, delta: number) => {
698
- const { buildNum } = (await AkanAppConfig.from(app)).mobile;
718
+ const { buildNum } = (await AkanAppConfig.from(app)).native;
699
719
  const akanConfigPath = path.join(app.cwdPath, "akan.config.ts");
700
720
  const akanConfig = fs.readFileSync(akanConfigPath, "utf8");
701
721
  fs.writeFileSync(akanConfigPath, akanConfig.replace(`buildNum: ${buildNum}`, `buildNum: ${buildNum + delta}`));
@@ -8,10 +8,16 @@ export type {
8
8
  AkanConfigFile,
9
9
  AkanDatabaseConfig,
10
10
  AkanExecutor,
11
- AkanMobileConfig,
12
- AkanMobileNativeConfig,
13
- AkanMobileTargetConfig,
14
- AkanMobileUpdatesConfig,
11
+ AkanNativeAndroidConfig,
12
+ AkanNativeAppConfig,
13
+ AkanNativeAppId,
14
+ AkanNativeAppResult,
15
+ AkanNativeDeepLinks,
16
+ AkanNativeDesktopConfig,
17
+ AkanNativeIosConfig,
18
+ AkanNativeSettings,
19
+ AkanNativeTarget,
20
+ AkanNativeUpdatesConfig,
15
21
  AkanNativeValue,
16
22
  AkanPlugin,
17
23
  AkanPluginNativeConfig,
@@ -39,8 +45,8 @@ export type {
39
45
  LibConfigResult,
40
46
  LibDockerConfig,
41
47
  LibScanResult,
42
- MobileEnv,
43
- MobilePermission,
48
+ NativeEnv,
49
+ NativePermission,
44
50
  PkgScanResult,
45
51
  ScanResult,
46
52
  WorkspaceScanResult,
package/executors.test.ts CHANGED
@@ -338,8 +338,8 @@ describe("Workspace and app executor environment contracts", () => {
338
338
  await writeFile(path.join(root, "apps/startclean/akan.config.ts"), "export default {};\n");
339
339
  const akan = path.join(root, "apps/startclean/.akan");
340
340
  const kept = [
341
- "mobile/desktop/updates/macos-arm64/main.json",
342
- "mobile/desktop/native/macos/App.app",
341
+ "native/desktop/updates/macos-arm64/main.json",
342
+ "native/desktop/build/macos/App.app",
343
343
  "cache/bin/ffmpeg",
344
344
  ];
345
345
  const removed = [
@@ -347,6 +347,7 @@ describe("Workspace and app executor environment contracts", () => {
347
347
  "generated/dict/index.ts",
348
348
  "cache/cssCandidates.json",
349
349
  "desktop/server/main.js",
350
+ "mobile/desktop/native/macos/App.app",
350
351
  ];
351
352
  for (const file of [...kept, ...removed]) await writeText(path.join(akan, file), "x");
352
353
 
@@ -354,7 +355,7 @@ describe("Workspace and app executor environment contracts", () => {
354
355
  await app.prepareCommand("start");
355
356
 
356
357
  for (const file of kept) expect(existsSync(path.join(akan, file))).toBe(true);
357
- for (const file of [...removed, "artifact", "generated", "desktop"])
358
+ for (const file of [...removed, "artifact", "generated", "desktop", "mobile"])
358
359
  expect(existsSync(path.join(akan, file))).toBe(false);
359
360
  });
360
361
 
package/executors.ts CHANGED
@@ -1120,7 +1120,7 @@ export class AppExecutor extends SysExecutor {
1120
1120
  return new AppExecutor({ workspace: executor instanceof WorkspaceExecutor ? executor : executor.workspace, name });
1121
1121
  }
1122
1122
  //* Not dev output: native builds and the update releases waiting to be uploaded, and the downloaded `bin` sources.
1123
- static readonly #keptOnStart = [path.join(".akan", "mobile"), path.join(".akan", "cache", "bin")];
1123
+ static readonly #keptOnStart = [path.join(".akan", "native"), path.join(".akan", "cache", "bin")];
1124
1124
  async #removeDevOutput(dir = ".akan") {
1125
1125
  const entries = await readDirEntries(this.getPath(dir)).catch(() => [] as string[]);
1126
1126
  await Promise.all(
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { optimize } from "@tailwindcss/node";
3
- import type { AkanMobileAppId, AkanWebConfig } from "akanjs";
3
+ import type { AkanNativeAppId, AkanWebConfig } from "akanjs";
4
4
  import type { BaseBuildArtifact } from "akanjs/server";
5
5
  import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
6
6
  import { computeRouteSeedIndex, type RouteSeedIndex, saveRouteSeedIndex } from "../artifact/routeSeedIndex";
@@ -85,15 +85,15 @@ export class SsrBaseArtifactBuilder {
85
85
  i18n: akanConfig.i18n,
86
86
  imageConfig: akanConfig.images,
87
87
  web: SsrBaseArtifactBuilder.servedWeb(this.#command, akanConfig.web),
88
- deepLinkAssociations: Object.values(akanConfig.mobile.targets)
88
+ deepLinkAssociations: Object.values(akanConfig.native.targets)
89
89
  .filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0)
90
90
  .map((target) => ({
91
91
  targetName: target.name,
92
92
  iosAppId: SsrBaseArtifactBuilder.#appIdOn(target.appId, "ios"),
93
93
  androidAppId: SsrBaseArtifactBuilder.#appIdOn(target.appId, "android"),
94
94
  domains: target.deepLinks?.domains ?? [],
95
- iosTeamId: target.deepLinks?.ios?.teamId,
96
- androidSha256CertFingerprints: target.deepLinks?.android?.sha256CertFingerprints,
95
+ iosTeamId: target.ios?.teamId,
96
+ androidSha256CertFingerprints: target.android?.sha256CertFingerprints,
97
97
  })),
98
98
  };
99
99
  await Bun.write(path.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}\n`);
@@ -224,7 +224,7 @@ export class SsrBaseArtifactBuilder {
224
224
  }
225
225
 
226
226
  // Checked against the written asset, not the compiled text: it is the only stylesheet an SSR render serves.
227
- static #appIdOn(appId: AkanMobileAppId, platform: "ios" | "android"): string | null {
227
+ static #appIdOn(appId: AkanNativeAppId, platform: "ios" | "android"): string | null {
228
228
  return typeof appId === "string" ? appId : (appId[platform] ?? appId.default ?? null);
229
229
  }
230
230
 
@@ -28,7 +28,7 @@ const stage = async (
28
28
  bin: AkanBinConfig,
29
29
  { libBins = [], libs = [] }: { libBins?: { lib: string; bin: AkanBinConfig }[]; libs?: string[] } = {},
30
30
  ) => {
31
- const binDir = path.join(root, "apps/portal/.akan/mobile/default/bin");
31
+ const binDir = path.join(root, "apps/portal/.akan/native/default/bin");
32
32
  const config = { app: { name: "portal" }, bin, libBins } as never;
33
33
  const carried = await new DesktopBin(fakeApp(path.join(root, "apps/portal"), libs), config).stage(binDir);
34
34
  return { carried, binDir };
@@ -1,6 +1,6 @@
1
1
  import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import type { AkanAppConfig, MobileEnv } from "../akanConfig";
3
+ import type { AkanAppConfig, NativeEnv } from "../akanConfig";
4
4
  import type { App } from "../commandDecorators";
5
5
  import type { PackageJson } from "../types";
6
6
  import { DesktopBin } from "./desktopBin";
@@ -40,11 +40,11 @@ export class DesktopServerStage {
40
40
  static assertCarriable(config: AkanAppConfig) {
41
41
  if (!config.database.modes.includes("single"))
42
42
  throw new Error(
43
- `native.desktop.server puts the server in the app, where only database mode single runs (no Redis or Postgres); apps/${config.app.name}/akan.config.ts declares ${config.database.modes.join(", ")}.`,
43
+ `desktop.server in native puts the server in the app, where only database mode single runs (no Redis or Postgres); apps/${config.app.name}/akan.config.ts declares ${config.database.modes.join(", ")}.`,
44
44
  );
45
45
  }
46
46
 
47
- static env(config: AkanAppConfig, environment: MobileEnv): Record<string, string> {
47
+ static env(config: AkanAppConfig, environment: NativeEnv): Record<string, string> {
48
48
  //? The launcher picks the port, and the log file stays on: a user's computer has no log collector.
49
49
  const { PORT: _port, AKAN_LOG_TO_FILE: _fileLog, ...image } = config.getProductionEnv(environment);
50
50
  return {
@@ -122,7 +122,7 @@ export class DesktopServerStage {
122
122
  }
123
123
 
124
124
  //? Copies the build beside what `install` put in the stage, so it runs after both.
125
- async prepare(environment: MobileEnv): Promise<DesktopServerBundle> {
125
+ async prepare(environment: NativeEnv): Promise<DesktopServerBundle> {
126
126
  const config = await this.app.getConfig();
127
127
  const dist = this.app.dist.cwdPath;
128
128
  await mkdir(this.dir, { recursive: true });
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import type { AkanMobileTargetConfig } from "../akanConfig";
2
+ import type { AkanNativeTarget } from "../akanConfig";
3
3
  import type { App } from "../commandDecorators";
4
4
  import {
5
5
  appIdsOf,
@@ -17,9 +17,9 @@ const target = {
17
17
  appId: "com.akanjs.app",
18
18
  version: "1.0.0",
19
19
  buildNum: 1,
20
- } as AkanMobileTargetConfig;
20
+ } as AkanNativeTarget;
21
21
 
22
- describe("mobile target helpers", () => {
22
+ describe("native target helpers", () => {
23
23
  test("maps deep links into target base paths without duplicating prefixes", () => {
24
24
  expect(resolveMobilePath(target, "/order/123")).toBe("/akanjs/order/123");
25
25
  expect(resolveMobilePath(target, "/akanjs/order/123")).toBe("/akanjs/order/123");
@@ -32,7 +32,7 @@ describe("mobile target helpers", () => {
32
32
  expect(resolveAppId(appId, "android")).toBe("com.yeollege");
33
33
  expect(resolveAppId("com.one.app", "macos")).toBe("com.one.app");
34
34
  expect(() => resolveAppId({ ios: "com.puffinplanet.yeollege" }, "android")).toThrow(
35
- "mobile.appId names no id for android",
35
+ "native.appId names no id for android",
36
36
  );
37
37
  expect(appIdsOf(appId)).toEqual(["com.yeollege", "com.puffinplanet.yeollege"]);
38
38
  });
@@ -42,11 +42,11 @@ describe("mobile target helpers", () => {
42
42
  expect(targetHtmlFilename({ ...target, basePath: undefined })).toBe("index.html");
43
43
  });
44
44
 
45
- test("uses the configured mobile target as the only choice when one target is configured", async () => {
45
+ test("uses the configured native target as the only choice when one target is configured", async () => {
46
46
  const app = {
47
47
  getConfig: async () => ({
48
48
  basePaths: new Set(["akanjs", "soft", "office"]),
49
- mobile: { targets: { akanjs: target } },
49
+ native: { targets: { akanjs: target } },
50
50
  }),
51
51
  } as unknown as App;
52
52
 
@@ -59,13 +59,13 @@ describe("mobile target helpers", () => {
59
59
  const appWith = (basePaths: string[]) =>
60
60
  ({
61
61
  name: "angelo",
62
- getConfig: async () => ({ basePaths: new Set(basePaths), mobile: { targets: { default: template } } }),
62
+ getConfig: async () => ({ basePaths: new Set(basePaths), native: { targets: { default: template } } }),
63
63
  }) as unknown as App;
64
64
 
65
65
  const two = appWith(["soft", "office"]);
66
66
  await expect(getMobileTargetChoices(two)).resolves.toEqual(["soft", "office"]);
67
67
  await expect(resolveMobileTargets(two, undefined)).rejects.toThrow(
68
- "Multiple mobile targets found for angelo. Pass --target <soft|office|all>.",
68
+ "Multiple native targets found for angelo. Pass --target <soft|office|all>.",
69
69
  );
70
70
  await expect(resolveMobileTargets(two, "office")).resolves.toEqual([
71
71
  { name: "office", config: { ...template, name: "office", basePath: "office" } },
@@ -85,11 +85,11 @@ describe("mobile target helpers", () => {
85
85
  ]);
86
86
  });
87
87
 
88
- test("resolves a route base path onto the default mobile target config", async () => {
88
+ test("resolves a route base path onto the default native target config", async () => {
89
89
  const app = {
90
90
  getConfig: async () => ({
91
91
  basePaths: new Set(["akanjs", "soft", "office"]),
92
- mobile: { targets: { akanjs: target } },
92
+ native: { targets: { akanjs: target } },
93
93
  }),
94
94
  } as unknown as App;
95
95