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

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.
@@ -491,6 +491,29 @@ describe("AkanAppConfig", () => {
491
491
  expect(config.native.targets.default).toMatchObject({ indexPath: "/explore", desktop: { server: true } });
492
492
  });
493
493
 
494
+ test("a platform section may name its own indexPath, normalized like the section's", () => {
495
+ const config = new AkanAppConfig(
496
+ app,
497
+ [],
498
+ packageJson,
499
+ {
500
+ native: {
501
+ indexPath: "/mobile",
502
+ ios: { indexPath: "cockpit/" },
503
+ desktop: { indexPath: " / ", server: true },
504
+ },
505
+ },
506
+ baseDevEnv,
507
+ );
508
+
509
+ expect(config.native.targets.default).toMatchObject({
510
+ indexPath: "/mobile",
511
+ ios: { indexPath: "/cockpit" },
512
+ desktop: { indexPath: "/", server: true },
513
+ });
514
+ expect(config.native.targets.default.android).toBeUndefined();
515
+ });
516
+
494
517
  test("a target takes the native section with its own fields over it: objects merge, lists and values replace", () => {
495
518
  const config = new AkanAppConfig(
496
519
  app,
@@ -293,9 +293,11 @@ export class AkanAppConfig implements AppConfigResult {
293
293
  } = merged;
294
294
  const indexPath = normalizeIndexPath(rawIndexPath);
295
295
  const deepLinks = normalizeDeepLinks(rawDeepLinks);
296
- const { teamId: rawTeamId, ...iosRest } = merged.ios ?? {};
296
+ const { teamId: rawTeamId, ...iosRest } = AkanAppConfig.#platformIndexPath(merged.ios ?? {});
297
297
  const teamId = rawTeamId?.trim();
298
- const { sha256CertFingerprints: rawFingerprints, ...androidRest } = merged.android ?? {};
298
+ const { sha256CertFingerprints: rawFingerprints, ...androidRest } = AkanAppConfig.#platformIndexPath(
299
+ merged.android ?? {},
300
+ );
299
301
  const fingerprints = normalizeStringList(rawFingerprints);
300
302
  const target: AkanNativeTarget = {
301
303
  ...rest,
@@ -307,6 +309,7 @@ export class AkanAppConfig implements AppConfigResult {
307
309
  ...(merged.android
308
310
  ? { android: { ...androidRest, ...(fingerprints ? { sha256CertFingerprints: fingerprints } : {}) } }
309
311
  : {}),
312
+ ...(merged.desktop ? { desktop: AkanAppConfig.#platformIndexPath(merged.desktop) } : {}),
310
313
  appName: merged.appName ?? this.app.name,
311
314
  appId: merged.appId ?? appId,
312
315
  version: merged.version ?? version,
@@ -318,6 +321,11 @@ export class AkanAppConfig implements AppConfigResult {
318
321
  );
319
322
  return { appName, appId, ...(root.fileName ? { fileName: root.fileName } : {}), version, buildNum, targets };
320
323
  }
324
+ static #platformIndexPath<T extends { indexPath?: string }>(section: T): T {
325
+ const { indexPath: rawIndexPath, ...rest } = section;
326
+ const indexPath = normalizeIndexPath(rawIndexPath);
327
+ return { ...rest, ...(indexPath ? { indexPath } : {}) } as T;
328
+ }
321
329
  /** `own` over `base`: plain objects merge key by key, and any other value, a list included, replaces. */
322
330
  static #overlay(base: AkanNativeSettings, own: AkanNativeSettings): AkanNativeSettings {
323
331
  const merge = (kept: unknown, value: unknown, top: boolean, key: string): unknown => {
@@ -356,8 +364,9 @@ export class AkanAppConfig implements AppConfigResult {
356
364
  "android",
357
365
  "desktop",
358
366
  ],
359
- ios: ["teamId", "infoPlist", "entitlements", "privacy", "files"],
367
+ ios: ["indexPath", "teamId", "infoPlist", "entitlements", "privacy", "files"],
360
368
  android: [
369
+ "indexPath",
361
370
  "sha256CertFingerprints",
362
371
  "googleServices",
363
372
  "push",
@@ -367,7 +376,7 @@ export class AkanAppConfig implements AppConfigResult {
367
376
  "activity",
368
377
  "files",
369
378
  ],
370
- desktop: ["server", "recovery", "window", "screenCapture"],
379
+ desktop: ["indexPath", "server", "recovery", "window", "screenCapture"],
371
380
  deepLinks: ["schemes", "domains"],
372
381
  } as const;
373
382
  //* Settings that moved when `mobile` became `native` are named with where they went, instead of "unknown".
@@ -87,14 +87,40 @@ describe("NativeApp", () => {
87
87
  });
88
88
 
89
89
  test("opens a dev build on its target's home, the CSR shell under the locale", () => {
90
- expect(new NativeApp(fakeApp(), target({ indexPath: "/explore" })).startPath("en")).toBe(
90
+ expect(new NativeApp(fakeApp(), target({ indexPath: "/explore" })).startPath("en", "ios")).toBe(
91
91
  "/en/explore?csr=true&akanMobileTarget=default&akanMobileIndexPath=%2Fexplore",
92
92
  );
93
- expect(new NativeApp(fakeApp(), target({ name: "admin", basePath: "/admin/" })).startPath("ko")).toBe(
93
+ expect(new NativeApp(fakeApp(), target({ name: "admin", basePath: "/admin/" })).startPath("ko", "android")).toBe(
94
94
  "/ko/admin?csr=true&akanMobileTarget=admin&akanMobileBasePath=admin",
95
95
  );
96
96
  });
97
97
 
98
+ test("a platform section's indexPath wins on that platform, and the others keep the target's", () => {
99
+ const app = new NativeApp(fakeApp(), target({ indexPath: "/mobile", desktop: { indexPath: "/", server: true } }));
100
+
101
+ expect(app.indexPath("ios")).toBe("/mobile");
102
+ expect(app.indexPath("android")).toBe("/mobile");
103
+ expect(app.indexPath("macos")).toBe("/");
104
+ expect(app.indexPath("windows")).toBe("/");
105
+ expect(app.startPath("en", "macos")).toBe("/en?csr=true&akanMobileTarget=default&akanMobileIndexPath=%2F");
106
+ expect(app.startPath("en", "ios")).toBe(
107
+ "/en/mobile?csr=true&akanMobileTarget=default&akanMobileIndexPath=%2Fmobile",
108
+ );
109
+ });
110
+
111
+ test("assembles each platform's web root with that platform's indexPath", async () => {
112
+ const root = await makeTempRoot();
113
+ await writeText(path.join(root, "dist/csr/index.html"), "<html><head></head><body></body></html>");
114
+ const app = { ...fakeApp(), cwdPath: root, dist: { cwdPath: path.join(root, "dist") } } as unknown as App;
115
+ const nativeApp = new NativeApp(app, target({ indexPath: "/mobile", desktop: { indexPath: "/console" } }));
116
+ const injected = async () => await Bun.file(path.join(nativeApp.web.dir, "index.html")).text();
117
+
118
+ await nativeApp.assembleWeb("linux");
119
+ expect(await injected()).toContain('"indexPath":"/console"');
120
+ await nativeApp.assembleWeb("android");
121
+ expect(await injected()).toContain('"indexPath":"/mobile"');
122
+ });
123
+
98
124
  test("builds its config from the app's locales and the plugins' native contributions", async () => {
99
125
  const push: AkanPlugin = { name: "push-notification", native: { permission: "push", plugins: ["push"] } };
100
126
  const app = new NativeApp(
@@ -74,14 +74,24 @@ export class NativeApp {
74
74
  return path.join(this.targetRoot, "dev", platform);
75
75
  }
76
76
 
77
+ /** Where the app starts on `platform`: its platform section's `indexPath`, else the target's. */
78
+ indexPath(platform: NativePlatform) {
79
+ const { config } = this.target;
80
+ const section = platform === "ios" ? config.ios : platform === "android" ? config.android : config.desktop;
81
+ return section?.indexPath ?? config.indexPath;
82
+ }
83
+
77
84
  /** The web root, from the production build the native commands run first. */
78
- async assembleWeb() {
85
+ async assembleWeb(platform: NativePlatform) {
79
86
  const dist = this.app.dist.cwdPath;
80
- return await this.web.assemble(this.target.config, {
81
- html: path.join(dist, "csr", targetHtmlFilename(this.target.config)),
82
- publicDir: path.join(dist, "public"),
83
- fontsDir: path.join(dist, ".akan", "artifact", "fonts"),
84
- });
87
+ return await this.web.assemble(
88
+ { ...this.target.config, indexPath: this.indexPath(platform) },
89
+ {
90
+ html: path.join(dist, "csr", targetHtmlFilename(this.target.config)),
91
+ publicDir: path.join(dist, "public"),
92
+ fontsDir: path.join(dist, ".akan", "artifact", "fonts"),
93
+ },
94
+ );
85
95
  }
86
96
 
87
97
  /** Where a desktop build stages the executables it carries (akan.config.ts `bin`). */
@@ -168,7 +178,7 @@ export class NativeApp {
168
178
  async build(platform: NativePlatform, { profile = "release", server, installer = false }: NativeBuildOptions = {}) {
169
179
  NativeApp.#assertServerPlatform(platform, server);
170
180
  if (installer && platform !== "windows") throw new Error(`An installer is built for Windows, not for ${platform}.`);
171
- await this.assembleWeb();
181
+ await this.assembleWeb(platform);
172
182
  const { api, config } = await this.prepare(platform, server);
173
183
  return await api.build({
174
184
  ...this.#task(platform, config),
@@ -182,7 +192,7 @@ export class NativeApp {
182
192
  { device, teamId, profile = "debug", server }: NativeRunOptions & NativeBuildOptions = {},
183
193
  ) {
184
194
  NativeApp.#assertServerPlatform(platform, server);
185
- await this.assembleWeb();
195
+ await this.assembleWeb(platform);
186
196
  const { api, config } = await this.prepare(platform, server);
187
197
  return await api.run({
188
198
  ...this.#task(platform, config),
@@ -211,7 +221,7 @@ export class NativeApp {
211
221
  wsPaths: [`${routes.prefix}${routes.websocketPrefix}`],
212
222
  reversePorts: [Number(new URL(upstream).port)],
213
223
  onLine: this.#appLine,
214
- startPath: this.startPath(lang),
224
+ startPath: this.startPath(lang, platform),
215
225
  ...(device ? { device } : {}),
216
226
  ...(teamId ? { ios: { signing: { teamId } } } : {}),
217
227
  });
@@ -242,7 +252,7 @@ export class NativeApp {
242
252
  }
243
253
 
244
254
  async releaseIos({ teamId, adHoc = false }: { teamId?: string; adHoc?: boolean } = {}) {
245
- await this.assembleWeb();
255
+ await this.assembleWeb("ios");
246
256
  const { api, config } = await this.prepare("ios");
247
257
  return await api.release({
248
258
  ...this.#task("ios", config),
@@ -263,7 +273,7 @@ export class NativeApp {
263
273
  ) {
264
274
  NativeApp.#assertServerPlatform(platform, server);
265
275
  this.#assertUpdates();
266
- await this.assembleWeb();
276
+ await this.assembleWeb(platform);
267
277
  const { api, config } = await this.prepare(platform, server);
268
278
  return await api.publishUpdate({
269
279
  ...this.#task(platform, config),
@@ -324,14 +334,14 @@ export class NativeApp {
324
334
 
325
335
  async releaseAndroid({ formats = ["aab"] }: { formats?: ("aab" | "apk")[] } = {}) {
326
336
  const signing = NativeApp.androidSigning();
327
- await this.assembleWeb();
337
+ await this.assembleWeb("android");
328
338
  const { api, config } = await this.prepare("android");
329
339
  return await api.release({ ...this.#task("android", config), platform: "android", signing, formats });
330
340
  }
331
341
 
332
342
  /** An unsigned OTA update of this target's web bundle for `platform`, and the channel its binary follows. */
333
343
  async packUpdate(platform: MobilePlatform, { out }: { out?: string } = {}) {
334
- await this.assembleWeb();
344
+ await this.assembleWeb(platform);
335
345
  const { api, config } = await this.prepare(platform);
336
346
  if (!config.updates)
337
347
  throw new Error(
@@ -370,12 +380,13 @@ export class NativeApp {
370
380
 
371
381
  /** The first page of a dev build, the target's home as a release bundle opens it; the dev server answers the CSR
372
382
  * shell only for `?csr=true`. */
373
- startPath(lang: string) {
383
+ startPath(lang: string, platform: NativePlatform) {
374
384
  const basePath = this.target.config.basePath?.replace(/^\/+|\/+$/g, "");
385
+ const indexPath = this.indexPath(platform);
375
386
  const params = new URLSearchParams({ csr: "true", akanMobileTarget: this.target.name });
376
387
  if (basePath) params.set("akanMobileBasePath", basePath);
377
- if (this.target.config.indexPath) params.set("akanMobileIndexPath", this.target.config.indexPath);
378
- const home = [lang, basePath, this.target.config.indexPath]
388
+ if (indexPath) params.set("akanMobileIndexPath", indexPath);
389
+ const home = [lang, basePath, indexPath]
379
390
  .flatMap((part) => (part ?? "").split("/"))
380
391
  .filter((segment) => segment.length > 0);
381
392
  return `/${home.join("/")}?${params}`;
@@ -234,10 +234,7 @@ export class NativeConfig {
234
234
  }
235
235
 
236
236
  //? akanConfig refuses a target whose merged updates lack url or publicKey; the type still has them optional.
237
- static #updates(
238
- target: AkanNativeTarget,
239
- env?: NativeEnv,
240
- ): NonNullable<AkanNativeConfig["updates"]> | undefined {
237
+ static #updates(target: AkanNativeTarget, env?: NativeEnv): NonNullable<AkanNativeConfig["updates"]> | undefined {
241
238
  const { url, publicKey, channel = env, ...rest } = target.updates ?? {};
242
239
  return url && publicKey ? { ...rest, url, publicKey, ...(channel ? { channel } : {}) } : undefined;
243
240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "3.0.0-beta.30",
3
+ "version": "3.0.0-beta.31",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -47,7 +47,7 @@
47
47
  "@earendil-works/pi-tui": "0.86.1",
48
48
  "@inquirer/prompts": "^8.4.3",
49
49
  "@tailwindcss/node": "^4.3.0",
50
- "akanjs": "3.0.0-beta.30",
50
+ "akanjs": "3.0.0-beta.31",
51
51
  "chalk": "^5.6.2",
52
52
  "commander": "^14.0.3",
53
53
  "dayjs": "^1.11.20",