@akanjs/devkit 3.0.0-beta.20 → 3.0.0-beta.22

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 (44) hide show
  1. package/README.ko.md +2 -2
  2. package/README.md +2 -2
  3. package/akanConfig/akanConfig.test.ts +58 -38
  4. package/akanConfig/akanConfig.ts +88 -48
  5. package/akanConfig/types.ts +3 -3
  6. package/akanContext.ts +5 -2
  7. package/artifact/implicitRootLayout.test.ts +23 -0
  8. package/artifact/implicitRootLayout.ts +24 -0
  9. package/biome.base.json +26 -0
  10. package/commandDecorators/command.ts +1 -1
  11. package/executors.ts +1 -12
  12. package/incrementalBuilder/buildBatch.proc.ts +7 -1
  13. package/index.ts +0 -2
  14. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  15. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  16. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  17. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  18. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  19. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  20. package/lint/no-document-cookie.grit +29 -0
  21. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  22. package/lint/no-web-storage.grit +30 -0
  23. package/mobile/index.ts +6 -0
  24. package/mobile/mobileTarget.ts +2 -0
  25. package/mobile/nativeApi.test.ts +30 -0
  26. package/mobile/nativeApi.ts +40 -0
  27. package/mobile/nativeApp.test.ts +85 -0
  28. package/mobile/nativeApp.ts +210 -0
  29. package/mobile/nativeConfig.test.ts +200 -0
  30. package/mobile/nativeConfig.ts +190 -0
  31. package/mobile/nativeWebDir.test.ts +80 -0
  32. package/mobile/nativeWebDir.ts +62 -0
  33. package/mobile/placeholderAppId.test.ts +13 -0
  34. package/mobile/placeholderAppId.ts +16 -0
  35. package/mobile/usageDescriptions.test.ts +27 -0
  36. package/mobile/usageDescriptions.ts +24 -0
  37. package/package.json +2 -7
  38. package/qualityScanner.ts +4 -2
  39. package/scanInfo.ts +5 -10
  40. package/workspaceLayout.test.ts +13 -3
  41. package/workspaceLayout.ts +16 -4
  42. package/applicationReleasePackager.ts +0 -189
  43. package/capacitorApp.test.ts +0 -388
  44. package/capacitorApp.ts +0 -1520
@@ -1,388 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { writeFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import type { AkanMobileTargetConfig } from "./akanConfig";
5
- import {
6
- ANDROID_MIN_SDK_VERSION,
7
- assertJsonSerializable,
8
- buildIosNativeRunCommand,
9
- classifyIosRunFailure,
10
- clearRootCapacitorConfigs,
11
- formatAndroidReleaseSigningError,
12
- getAdbDeviceStateIssues,
13
- getAndroidLocalServerHost,
14
- getMissingAndroidReleaseSigningKeys,
15
- isPlaceholderAppId,
16
- materializeCapacitorConfig,
17
- parseDevicectlDevices,
18
- parseIosRuntimeMajor,
19
- parseSimctlDevices,
20
- raiseGradleMinSdkVersion,
21
- rootCapacitorConfigFilenames,
22
- SWIFTUICORE_MIN_IOS_MAJOR,
23
- sanitizeIosNativeRunEnv,
24
- selectLocalDevHost,
25
- sortIosRunTargets,
26
- toIosInfoPlistUsageDescriptions,
27
- writeRootCapacitorConfig,
28
- } from "./capacitorApp";
29
- import { tempDirs } from "./testHelpers";
30
-
31
- const makeTempRoot = tempDirs("akan-capacitor-app-");
32
-
33
- const baseTarget: AkanMobileTargetConfig = {
34
- name: "default",
35
- appName: "Minimal",
36
- appId: "com.minimal.app",
37
- version: "1.2.3",
38
- buildNum: 7,
39
- basePath: "admin",
40
- indexPath: "/dashboard",
41
- assets: { icon: "mobile/icon.png" },
42
- permissions: ["camera"],
43
- deepLinks: { schemes: ["minimal"], domains: ["minimal.app"] },
44
- files: { android: { "app/google-services.json": "private/google-services.json" } },
45
- };
46
-
47
- describe("materializeCapacitorConfig", () => {
48
- test("writes only Capacitor fields for release config", () => {
49
- const config = materializeCapacitorConfig(
50
- {
51
- ...baseTarget,
52
- plugins: { CapacitorHttp: { enabled: true } },
53
- android: { flavor: "qa" },
54
- ios: { scheme: "App QA" },
55
- cordova: { preferences: { ScrollEnabled: "false" } },
56
- experimental: { ios: { spm: { swiftToolsVersion: "5.9" } } },
57
- },
58
- { operation: "release" },
59
- );
60
-
61
- expect(config).toMatchObject({
62
- appId: "com.minimal.app",
63
- appName: "Minimal",
64
- webDir: ".akan/mobile/default/www",
65
- plugins: {
66
- CapacitorCookies: { enabled: true },
67
- CapacitorHttp: { enabled: true },
68
- Keyboard: { resize: "none" },
69
- },
70
- android: { flavor: "qa", path: "android" },
71
- ios: { scheme: "App QA", path: "ios" },
72
- cordova: { preferences: { ScrollEnabled: "false" } },
73
- experimental: { ios: { spm: { swiftToolsVersion: "5.9" } } },
74
- });
75
- expect(config).not.toHaveProperty("name");
76
- expect(config).not.toHaveProperty("basePath");
77
- expect(config).not.toHaveProperty("indexPath");
78
- expect(config).not.toHaveProperty("version");
79
- expect(config).not.toHaveProperty("buildNum");
80
- expect(config).not.toHaveProperty("assets");
81
- expect(config).not.toHaveProperty("permissions");
82
- expect(config).not.toHaveProperty("deepLinks");
83
- expect(config).not.toHaveProperty("files");
84
- expect(config).not.toHaveProperty("server");
85
- });
86
-
87
- test("adds local server config without requiring env target switching", () => {
88
- const config = materializeCapacitorConfig(
89
- {
90
- ...baseTarget,
91
- server: {
92
- hostname: "localhost",
93
- allowNavigation: ["api.example.com"],
94
- },
95
- },
96
- {
97
- operation: "local",
98
- localIp: "192.168.0.5",
99
- localServerUrl: "http://192.168.0.5:8282/en/admin?csr=true&akanMobileTarget=default",
100
- },
101
- );
102
-
103
- expect(config.server).toEqual({
104
- hostname: "localhost",
105
- androidScheme: "http",
106
- url: "http://192.168.0.5:8282/en/admin?csr=true&akanMobileTarget=default",
107
- cleartext: true,
108
- allowNavigation: ["api.example.com", "192.168.0.5", "localhost"],
109
- });
110
- });
111
-
112
- test("allows mobile targets to override the default keyboard resize mode", () => {
113
- const config = materializeCapacitorConfig(
114
- {
115
- ...baseTarget,
116
- plugins: { Keyboard: { resize: "native" } },
117
- },
118
- { operation: "release" },
119
- );
120
-
121
- expect(config.plugins).toMatchObject({
122
- Keyboard: { resize: "native" },
123
- });
124
- });
125
-
126
- test("rejects non-json config values", () => {
127
- expect(() => assertJsonSerializable({ plugins: { Custom: () => null } })).toThrow("must be JSON serializable");
128
- });
129
- });
130
-
131
- describe("root capacitor config helpers", () => {
132
- test("clears root configs before writing the temporary json config", async () => {
133
- const root = await makeTempRoot();
134
- await Promise.all(rootCapacitorConfigFilenames.map((file) => writeFile(path.join(root, file), `old ${file}\n`)));
135
-
136
- await writeRootCapacitorConfig(root, '{\n "appId": "com.minimal.app"\n}\n');
137
-
138
- expect(await Bun.file(path.join(root, "capacitor.config.ts")).exists()).toBe(false);
139
- expect(await Bun.file(path.join(root, "capacitor.config.js")).exists()).toBe(false);
140
- expect(await Bun.file(path.join(root, "capacitor.config.json")).text()).toBe(
141
- '{\n "appId": "com.minimal.app"\n}\n',
142
- );
143
-
144
- await clearRootCapacitorConfigs(root);
145
-
146
- expect(await Bun.file(path.join(root, "capacitor.config.json")).exists()).toBe(false);
147
- });
148
- });
149
-
150
- describe("iOS native run helpers", () => {
151
- test("parses xcrun device and simulator outputs", () => {
152
- expect(
153
- parseDevicectlDevices(
154
- JSON.stringify({
155
- result: {
156
- devices: [
157
- {
158
- identifier: "1BCB6563-02FF-5D49-9655-1BFF02A638D3",
159
- connectionProperties: { pairingState: "paired", transportType: "wired", tunnelState: "available" },
160
- deviceProperties: { name: "Seok iPhone" },
161
- hardwareProperties: { udid: "00008130-000200113EC1001C" },
162
- },
163
- ],
164
- },
165
- }),
166
- ),
167
- ).toEqual([
168
- {
169
- id: "00008130-000200113EC1001C",
170
- name: "Seok iPhone",
171
- kind: "device",
172
- state: "available wired paired",
173
- devicectlId: "1BCB6563-02FF-5D49-9655-1BFF02A638D3",
174
- xcodebuildId: "00008130-000200113EC1001C",
175
- },
176
- ]);
177
-
178
- expect(
179
- parseSimctlDevices(
180
- JSON.stringify({
181
- devices: {
182
- "com.apple.CoreSimulator.SimRuntime.iOS-18-2": [
183
- { udid: "11111111-2222-3333-4444-555555555555", name: "iPhone 16", state: "Shutdown", isAvailable: true },
184
- ],
185
- },
186
- }),
187
- ),
188
- ).toEqual([
189
- {
190
- id: "11111111-2222-3333-4444-555555555555",
191
- name: "iPhone 16",
192
- kind: "simulator",
193
- state: "Shutdown",
194
- runtime: "iOS 18.2",
195
- },
196
- ]);
197
- });
198
-
199
- test("sorts run targets so booted / connected / newest-runtime targets come first", () => {
200
- const ios17 = { id: "a", name: "iPhone 15", kind: "simulator" as const, state: "Shutdown", runtime: "iOS 17.5" };
201
- const ios18 = { id: "b", name: "iPhone 16", kind: "simulator" as const, state: "Shutdown", runtime: "iOS 18.2" };
202
- const booted17 = { id: "c", name: "iPhone SE", kind: "simulator" as const, state: "Booted", runtime: "iOS 17.0" };
203
- const device = { id: "d", name: "Seok iPhone", kind: "device" as const, state: "available wired paired" };
204
- // Input is in the old ascending-runtime order that used to default to iOS 17.
205
- const sorted = sortIosRunTargets([ios17, ios18, booted17, device]);
206
- expect(sorted.map((t) => t.id)).toEqual(["d", "c", "b", "a"]);
207
- // Newest available simulator beats the older one, so the interactive default is no longer iOS 17.
208
- expect(sortIosRunTargets([ios17, ios18]).map((t) => t.id)).toEqual(["b", "a"]);
209
- });
210
-
211
- test("parses the major iOS runtime version", () => {
212
- expect(parseIosRuntimeMajor("iOS 18.2")).toBe(18);
213
- expect(parseIosRuntimeMajor("iOS 17.5")).toBe(17);
214
- expect(parseIosRuntimeMajor(undefined)).toBeUndefined();
215
- expect(SWIFTUICORE_MIN_IOS_MAJOR).toBe(18);
216
- });
217
-
218
- test("builds xcodebuild destinations and output paths by target kind", () => {
219
- const deviceCommand = buildIosNativeRunCommand({
220
- appRoot: "/repo/apps/minimal",
221
- device: { id: "device-1", name: "iPhone", kind: "device" },
222
- scheme: "App QA",
223
- configuration: "Debug",
224
- });
225
- expect(deviceCommand.xcodebuildArgs).toContain("id=device-1");
226
- expect(deviceCommand.xcodebuildArgs).toContain("App QA");
227
- expect(deviceCommand.appPath).toBe(
228
- path.join("/repo/apps/minimal/ios/DerivedData/device-1/Build/Products/Debug-iphoneos/App.app"),
229
- );
230
-
231
- const simulatorCommand = buildIosNativeRunCommand({
232
- appRoot: "/repo/apps/minimal",
233
- device: { id: "sim-1", name: "Simulator", kind: "simulator" },
234
- });
235
- expect(simulatorCommand.xcodebuildArgs).toContain("platform=iOS Simulator,id=sim-1");
236
- expect(simulatorCommand.appPath).toBe(
237
- path.join("/repo/apps/minimal/ios/DerivedData/sim-1/Build/Products/Debug-iphonesimulator/App.app"),
238
- );
239
- });
240
-
241
- test("classifies iOS signing and device-state failures", () => {
242
- expect(classifyIosRunFailure("There are no accounts registered with Xcode").kind).toBe("apple-account");
243
- expect(
244
- classifyIosRunFailure(
245
- 'Failed Registering Bundle Identifier: The app identifier "com.minimal.app" cannot be registered to your development team',
246
- ).kind,
247
- ).toBe("bundle-identifier");
248
- expect(classifyIosRunFailure("No profiles for 'com.minimal.app' were found").kind).toBe("provisioning-profile");
249
- expect(classifyIosRunFailure("Developer Mode is disabled on this device").kind).toBe("device-state");
250
- expect(classifyIosRunFailure("arm64-apple-darwin20.0: error: unknown argument: '-index-store-path'").kind).toBe(
251
- "compiler-toolchain",
252
- );
253
- expect(
254
- classifyIosRunFailure(
255
- "dyld[1234]: Library not loaded: /System/Library/Frameworks/SwiftUICore.framework/SwiftUICore",
256
- ).kind,
257
- ).toBe("simulator-runtime");
258
- });
259
-
260
- test("removes shell compiler variables from iOS native run env", () => {
261
- expect(
262
- sanitizeIosNativeRunEnv({
263
- AKAN_PUBLIC_APP_NAME: "minimal",
264
- CC: "arm64-apple-darwin20.0.0-clang",
265
- CXX: "arm64-apple-darwin20.0.0-clang++",
266
- SDKROOT: "/wrong/sdk",
267
- }),
268
- ).toEqual({ AKAN_PUBLIC_APP_NAME: "minimal" });
269
- });
270
-
271
- test("flags placeholder bundle identifiers that Apple's portal already claims", () => {
272
- for (const appId of ["com.myapp.app", "com.myorg.myapp", "com.example.foo", "com.changeme.app", ""]) {
273
- expect(isPlaceholderAppId(appId)).toBe(true);
274
- }
275
- for (const appId of ["com.minimal.app", "com.nearthlab.leadingflight", "io.akanjs.demo"]) {
276
- expect(isPlaceholderAppId(appId)).toBe(false);
277
- }
278
- });
279
- });
280
-
281
- describe("selectLocalDevHost", () => {
282
- const build = (map: Record<string, { family: string; address: string; internal?: boolean }[]>) =>
283
- map as unknown as Parameters<typeof selectLocalDevHost>[0];
284
-
285
- test("prefers a routable LAN NIC over an inactive bridge enumerated first", () => {
286
- const resolution = selectLocalDevHost(
287
- build({
288
- bridge0: [{ family: "IPv4", address: "192.168.2.1" }],
289
- en0: [{ family: "IPv4", address: "172.25.140.140" }],
290
- bridge100: [{ family: "IPv4", address: "192.168.3.1" }],
291
- en8: [{ family: "IPv4", address: "172.25.40.227" }],
292
- }),
293
- );
294
- expect(resolution.host).toBe("172.25.140.140");
295
- expect(resolution.source).toBe("detected");
296
- });
297
-
298
- test("an explicit override always wins and is trimmed", () => {
299
- const resolution = selectLocalDevHost(build({ en0: [{ family: "IPv4", address: "172.25.140.140" }] }), {
300
- override: " 10.0.0.9 ",
301
- });
302
- expect(resolution).toMatchObject({ host: "10.0.0.9", source: "override" });
303
- });
304
-
305
- test("never selects a link-local address when a real LAN address exists", () => {
306
- const resolution = selectLocalDevHost(
307
- build({
308
- en0: [{ family: "IPv4", address: "169.254.10.10" }],
309
- en1: [{ family: "IPv4", address: "192.168.1.5" }],
310
- }),
311
- );
312
- expect(resolution.host).toBe("192.168.1.5");
313
- });
314
-
315
- test("falls back to loopback when nothing routable is present", () => {
316
- const resolution = selectLocalDevHost(build({ lo0: [{ family: "IPv4", address: "127.0.0.1", internal: true }] }));
317
- expect(resolution).toMatchObject({ host: "127.0.0.1", source: "loopback" });
318
- });
319
- });
320
-
321
- describe("Android signing diagnostics", () => {
322
- test("reports missing release signing keys and adb authorization issues", () => {
323
- expect(
324
- getMissingAndroidReleaseSigningKeys({
325
- env: { MYAPP_RELEASE_STORE_FILE: "release.jks" } as NodeJS.ProcessEnv,
326
- gradleProperties: "MYAPP_RELEASE_STORE_PASSWORD=secret\n",
327
- }),
328
- ).toEqual(["MYAPP_RELEASE_KEY_ALIAS", "MYAPP_RELEASE_KEY_PASSWORD"]);
329
- expect(formatAndroidReleaseSigningError(["MYAPP_RELEASE_KEY_ALIAS"])).toContain("MYAPP_RELEASE_KEY_ALIAS");
330
- expect(getAdbDeviceStateIssues("List of devices attached\nabc123 unauthorized\nxyz offline\n")).toEqual([
331
- "Android device abc123 is unauthorized. Confirm USB debugging authorization on the device.",
332
- "Android device xyz is offline. Reconnect the device or restart adb.",
333
- ]);
334
- expect(getAndroidLocalServerHost("List of devices attached\nemulator-5554 device\n", "192.168.0.5")).toBe(
335
- "10.0.2.2",
336
- );
337
- expect(getAndroidLocalServerHost("List of devices attached\nabc123 device\n", "192.168.0.5")).toBe("192.168.0.5");
338
- expect(
339
- getAndroidLocalServerHost("List of devices attached\nemulator-5554 device\nabc123 device\n", "192.168.0.5"),
340
- ).toBe("192.168.0.5");
341
- });
342
- });
343
-
344
- describe("raiseGradleMinSdkVersion", () => {
345
- test("raises the scaffold minSdkVersion to the bundled-plugin floor, preserving formatting", () => {
346
- const scaffold = "ext {\n minSdkVersion = 24\n compileSdkVersion = 35\n}\n";
347
- expect(raiseGradleMinSdkVersion(scaffold)).toBe(
348
- `ext {\n minSdkVersion = ${ANDROID_MIN_SDK_VERSION}\n compileSdkVersion = 35\n}\n`,
349
- );
350
- });
351
-
352
- test("returns null when nothing needs changing", () => {
353
- // Already at the floor, already above it, and no assignment present.
354
- expect(raiseGradleMinSdkVersion(`ext {\n minSdkVersion = ${ANDROID_MIN_SDK_VERSION}\n}\n`)).toBeNull();
355
- expect(raiseGradleMinSdkVersion("ext {\n minSdkVersion = 34\n}\n")).toBeNull();
356
- expect(raiseGradleMinSdkVersion("ext {\n compileSdkVersion = 35\n}\n")).toBeNull();
357
- });
358
-
359
- test("honors an explicit floor and leaves higher user values untouched", () => {
360
- expect(raiseGradleMinSdkVersion("minSdkVersion = 21", 23)).toBe("minSdkVersion = 23");
361
- expect(raiseGradleMinSdkVersion("minSdkVersion = 28", 26)).toBeNull();
362
- });
363
- });
364
-
365
- describe("toIosInfoPlistUsageDescriptions", () => {
366
- test("writes Apple's real Info.plist keys, not NS + the description name", () => {
367
- expect(
368
- toIosInfoPlistUsageDescriptions({
369
- cameraUsageDescription: "camera",
370
- photoAddUsageDescription: "add",
371
- photoUsageDescription: "read",
372
- locationAlwaysUsageDescription: "always",
373
- }),
374
- ).toEqual({
375
- NSCameraUsageDescription: "camera",
376
- NSPhotoLibraryAddUsageDescription: "add",
377
- NSPhotoLibraryUsageDescription: "read",
378
- NSLocationAlwaysAndWhenInUseUsageDescription: "always",
379
- NSLocationAlwaysUsageDescription: "always",
380
- });
381
- });
382
-
383
- test("keeps the NS prefix convention for a key it does not know", () => {
384
- expect(toIosInfoPlistUsageDescriptions({ bluetoothAlwaysUsageDescription: "ble" })).toEqual({
385
- NSBluetoothAlwaysUsageDescription: "ble",
386
- });
387
- });
388
- });