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

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 (265) hide show
  1. package/README.md +1 -0
  2. package/abstractDoc.test.ts +6 -15
  3. package/abstractDoc.ts +6 -8
  4. package/agentsIndex.test.ts +4 -7
  5. package/agentsIndex.ts +6 -22
  6. package/akanApp/BackendImportGraph.test.ts +8 -22
  7. package/akanApp/BackendImportGraph.ts +1 -20
  8. package/akanApp/akanApp.host.test.ts +1 -3
  9. package/akanApp/akanApp.host.ts +82 -254
  10. package/akanApp/devHostPolicy.test.ts +10 -34
  11. package/akanApp/devHostPolicy.ts +12 -98
  12. package/akanConfig/akanConfig.test.ts +85 -69
  13. package/akanConfig/akanConfig.ts +69 -153
  14. package/akanConfig/types.ts +1 -0
  15. package/akanContext.ts +106 -159
  16. package/akanMcpContract.ts +28 -73
  17. package/appSelectionMemory.ts +1 -4
  18. package/applicationBuildReporter.ts +37 -5
  19. package/applicationBuildRunner.test.ts +5 -3
  20. package/applicationBuildRunner.ts +54 -61
  21. package/applicationReleasePackager.ts +24 -44
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +9 -30
  24. package/artifact/implicitRootLayout.ts +34 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +6 -1
  28. package/biomeBase.ts +2 -5
  29. package/capacitorApp.test.ts +32 -16
  30. package/capacitorApp.ts +110 -238
  31. package/cloud/cloudApi.test.ts +202 -0
  32. package/cloud/cloudApi.ts +77 -39
  33. package/cloud/configLock.test.ts +64 -0
  34. package/cloud/configLock.ts +96 -0
  35. package/cloud/constants.ts +26 -5
  36. package/cloud/globalConfig.ts +70 -32
  37. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  38. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  39. package/codeAgent/agent/CodeAgent.ts +551 -0
  40. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  41. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  42. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  43. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  44. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  45. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  46. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  47. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  48. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  49. package/codeAgent/agent/CodeMailbox.ts +148 -0
  50. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  51. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  52. package/codeAgent/agent/SubagentPool.ts +214 -0
  53. package/codeAgent/agent/akanCodeModel.ts +47 -0
  54. package/codeAgent/agent/akanCodePaths.ts +31 -0
  55. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  56. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  57. package/codeAgent/agent/codeAgent.test.ts +178 -0
  58. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  59. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  60. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  61. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  62. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  63. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  64. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  65. package/codeAgent/agent/consoleToStderr.ts +8 -0
  66. package/codeAgent/agent/jsonLines.ts +29 -0
  67. package/codeAgent/agent/stringArg.ts +5 -0
  68. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  69. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  70. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  71. package/codeAgent/feedback/PreviewView.ts +95 -0
  72. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  73. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  74. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  75. package/codeAgent/index.ts +34 -0
  76. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  77. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  78. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  79. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  80. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  81. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  82. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  83. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  84. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  85. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  86. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  87. package/codeAgent/tools/AkanCli.ts +50 -0
  88. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  89. package/codeAgent/tools/AkanEditScope.ts +48 -0
  90. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  91. package/codeAgent/tools/AkanToolPack.ts +92 -0
  92. package/codeAgent/tools/AkanVerifier.ts +48 -0
  93. package/codeAgent/tools/AskToolPack.ts +99 -0
  94. package/codeAgent/tools/MailToolPack.ts +76 -0
  95. package/codeAgent/tools/McpClient.ts +187 -0
  96. package/codeAgent/tools/McpOAuth.ts +233 -0
  97. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  98. package/codeAgent/tools/McpServerConfig.ts +129 -0
  99. package/codeAgent/tools/McpSignIn.ts +158 -0
  100. package/codeAgent/tools/McpTokenStore.ts +81 -0
  101. package/codeAgent/tools/McpToolPack.ts +199 -0
  102. package/codeAgent/tools/SessionToolPack.ts +91 -0
  103. package/codeAgent/tools/WebToolPack.ts +147 -0
  104. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  105. package/codeAgent/tools/askToolPack.test.ts +116 -0
  106. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  107. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  108. package/codegenLock.test.ts +4 -13
  109. package/codegenLock.ts +3 -17
  110. package/commandDecorators/argMeta.ts +1 -5
  111. package/commandDecorators/camelToKebabCase.ts +1 -0
  112. package/commandDecorators/command.ts +46 -102
  113. package/commandDecorators/commandBuilder.ts +30 -61
  114. package/commandDecorators/commandDecorators.test.ts +2 -10
  115. package/commandDecorators/dependencyBuilder.ts +10 -1
  116. package/commandDecorators/helpFormatter.ts +68 -145
  117. package/commandDecorators/index.ts +0 -2
  118. package/commandDecorators/targetMeta.ts +9 -7
  119. package/createTunnel.ts +7 -14
  120. package/dependencyScanner.test.ts +4 -16
  121. package/dependencyScanner.ts +31 -80
  122. package/devkitUtils.test.ts +48 -20
  123. package/executors.linkFallback.test.ts +103 -0
  124. package/executors.test.ts +132 -106
  125. package/executors.ts +194 -484
  126. package/fileEditor.ts +9 -37
  127. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  128. package/fileSys.ts +6 -8
  129. package/formSetterScanner.ts +3 -16
  130. package/frontendBuild/allRoutesBuilder.ts +7 -21
  131. package/frontendBuild/autoImportSync.test.ts +27 -38
  132. package/frontendBuild/autoImportSync.ts +31 -65
  133. package/frontendBuild/buildRouteClient.test.ts +62 -37
  134. package/frontendBuild/bundleDefine.ts +12 -0
  135. package/frontendBuild/clientBuildTypes.ts +5 -36
  136. package/frontendBuild/clientEntriesBundler.ts +9 -40
  137. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  138. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  139. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  140. package/frontendBuild/cssCandidateCache.ts +6 -41
  141. package/frontendBuild/cssCompiler.ts +29 -78
  142. package/frontendBuild/cssImportResolver.ts +6 -19
  143. package/frontendBuild/devChangePlanner.ts +7 -10
  144. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  145. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  146. package/frontendBuild/fontOptimizer.test.ts +6 -14
  147. package/frontendBuild/fontOptimizer.ts +15 -72
  148. package/frontendBuild/fontPruner.test.ts +5 -10
  149. package/frontendBuild/fontPruner.ts +5 -20
  150. package/frontendBuild/frontendBuild.test.ts +46 -69
  151. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  152. package/frontendBuild/hmrWatcher.test.ts +8 -32
  153. package/frontendBuild/hmrWatcher.ts +14 -71
  154. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  155. package/frontendBuild/precompressArtifacts.ts +5 -18
  156. package/frontendBuild/routeClientBuilder.ts +23 -46
  157. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  158. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  159. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  160. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  161. package/frontendBuild/styleContract.ts +1 -8
  162. package/frontendBuild/themeValidator.ts +4 -26
  163. package/frontendBuild/watchRootResolver.test.ts +23 -42
  164. package/frontendBuild/watchRootResolver.ts +5 -27
  165. package/getRelatedCnsts.ts +12 -92
  166. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  167. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  168. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  169. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  170. package/incrementalBuilder/builderChannel.test.ts +24 -49
  171. package/incrementalBuilder/builderChannel.ts +6 -37
  172. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  173. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  174. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  175. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  176. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  177. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  178. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  179. package/index.ts +5 -17
  180. package/integration/devResourceProbe.ts +50 -56
  181. package/integration/devStability.integration.test.ts +47 -194
  182. package/integration/devStabilityHarness.test.ts +13 -28
  183. package/integration/devStabilityHarness.ts +65 -330
  184. package/integration/ssrMemoryProbe.ts +41 -167
  185. package/libSource.test.ts +4 -15
  186. package/libSource.ts +19 -31
  187. package/lint/gritRules.test.ts +8 -24
  188. package/lint/no-deep-internal-import.grit +2 -2
  189. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  190. package/linter.test.ts +1 -2
  191. package/linter.ts +26 -116
  192. package/mobile/mobileTarget.ts +14 -13
  193. package/package.json +10 -5
  194. package/packageExports.test.ts +3 -18
  195. package/packageExportsMap.ts +3 -19
  196. package/piDependencyPins.test.ts +46 -0
  197. package/platformTest/BunTestReport.test.ts +60 -0
  198. package/platformTest/BunTestReport.ts +65 -0
  199. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  200. package/platformTest/IdleWatchedProcess.ts +73 -0
  201. package/platformTest/LinuxDockerTarget.ts +105 -0
  202. package/platformTest/PlatformTestRun.test.ts +64 -0
  203. package/platformTest/PlatformTestRun.ts +141 -0
  204. package/platformTest/PlatformTestTarget.ts +142 -0
  205. package/platformTest/TestSnapshot.test.ts +72 -0
  206. package/platformTest/TestSnapshot.ts +103 -0
  207. package/platformTest/WindowsSshTarget.ts +151 -0
  208. package/prompter.ts +3 -13
  209. package/qualityScanner.test.ts +9 -26
  210. package/qualityScanner.ts +197 -285
  211. package/recipeScanner.test.ts +7 -12
  212. package/recipeScanner.ts +9 -38
  213. package/repoIdentity.ts +2 -9
  214. package/routeSourceValidator.test.ts +0 -1
  215. package/routeSourceValidator.ts +7 -42
  216. package/scanInfo.ts +87 -216
  217. package/selectModel.ts +1 -2
  218. package/semver.ts +3 -5
  219. package/slicePlanner.test.ts +6 -24
  220. package/slicePlanner.ts +11 -32
  221. package/spinner.test.ts +3 -8
  222. package/spinner.ts +4 -16
  223. package/sshTunnel.ts +1 -6
  224. package/ssrScanner.test.ts +1 -6
  225. package/ssrScanner.ts +103 -113
  226. package/stripAnsi.ts +3 -0
  227. package/subspace.test.ts +30 -25
  228. package/subspace.ts +81 -115
  229. package/subspaceConfig.ts +11 -9
  230. package/testHelpers.ts +194 -0
  231. package/transforms/asyncDefaultExportDetector.ts +3 -11
  232. package/transforms/barrelAnalyzer.ts +43 -135
  233. package/transforms/barrelImportsPlugin.ts +38 -175
  234. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  235. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  236. package/transforms/moduleSyntax.ts +32 -0
  237. package/transforms/rscUseClientTransform.ts +35 -20
  238. package/transforms/transforms.test.ts +153 -44
  239. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  240. package/transforms/useClientBundlePlugin.ts +2 -22
  241. package/typeChecker.ts +7 -63
  242. package/types.ts +1 -0
  243. package/uploadRelease.ts +10 -25
  244. package/workflow/artifacts.ts +18 -0
  245. package/workflow/executor.test.ts +1 -12
  246. package/workflow/executor.ts +101 -110
  247. package/workflow/index.ts +0 -1
  248. package/workflow/moduleIndex.test.ts +4 -15
  249. package/workflow/moduleIndex.ts +9 -49
  250. package/workflow/plan.ts +18 -25
  251. package/workflow/render.ts +80 -96
  252. package/workflow/rolloutGate.test.ts +1 -1
  253. package/workflow/source.ts +52 -69
  254. package/workflow/sourceAst.ts +50 -0
  255. package/workspaceLayout.ts +4 -19
  256. package/abstractCompactor.ts +0 -112
  257. package/aiEditor.test.ts +0 -60
  258. package/aiEditor.ts +0 -387
  259. package/commandDecorators/commandMeta.ts +0 -7
  260. package/commandDecorators/types.ts +0 -10
  261. package/getDirname.ts +0 -4
  262. package/guideline.ts +0 -19
  263. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  264. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  265. package/workflow/primitive.ts +0 -59
@@ -1,4 +1,3 @@
1
- import { mkdir } from "node:fs/promises";
2
1
  import path from "node:path";
3
2
  import type { App } from "../commandDecorators";
4
3
  import { AsyncDefaultExportDetector } from "../transforms/asyncDefaultExportDetector";
@@ -11,23 +10,17 @@ export interface PageEntry {
11
10
 
12
11
  const LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
13
12
 
14
- async function appHasStModule(appCwdPath: string): Promise<boolean> {
15
- return Bun.file(path.join(appCwdPath, "lib", "st.ts")).exists();
16
- }
17
-
18
13
  const IMPLICIT_LAYOUT_DIR = path.join(".akan", "generated", "root-layouts");
19
14
  const IMPLICIT_DICT_DIR = path.join(".akan", "generated", "dict");
20
15
  const IMPLICIT_OVERRIDES_DIR = path.join(".akan", "generated", "overrides");
21
16
  const OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
22
17
 
23
- /**
24
- * A `_overrides.tsx` manifest is a plain, server-safe module (`export default override({ Modal: BrandModal })`)
25
- * with no `"use client"` directive. The `UiOverrideProvider` that consumes it is a client component, so the
26
- * build emits a `"use client"` wrapper layout that reads the manifest's default map and mounts the provider
27
- * around the subtree. As a normal `"use client"` module the wrapper participates in client-entry discovery and
28
- * the RSC client manifest, so the server (as a client reference) and the client (as the real component) both
29
- * resolve it — and the author never writes `"use client"`.
30
- */
18
+ const importSpecifier = (fromAbsPath: string, toAbsPath: string) => {
19
+ const rel = path.relative(path.dirname(fromAbsPath), toAbsPath).split(path.sep).join("/");
20
+ return rel.startsWith(".") ? rel : `./${rel}`;
21
+ };
22
+
23
+ // UiOverrideProvider is a client component, so `_overrides.tsx` mounts through a generated "use client" layout.
31
24
  async function writeGeneratedOverridesLayoutFile(opts: {
32
25
  appCwdPath: string;
33
26
  key: string;
@@ -35,12 +28,10 @@ async function writeGeneratedOverridesLayoutFile(opts: {
35
28
  }): Promise<string> {
36
29
  const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
37
30
  const absPath = path.join(path.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
38
- const userRel = path.relative(path.dirname(absPath), opts.userAbsPath).split(path.sep).join("/");
39
- const userSpecifier = userRel.startsWith(".") ? userRel : `./${userRel}`;
40
31
  const source = `"use client";
41
32
  import { UiOverrideProvider } from "akanjs/ui";
42
33
  import { createElement, type ReactNode } from "react";
43
- import value from ${JSON.stringify(userSpecifier)};
34
+ import value from ${JSON.stringify(importSpecifier(absPath, opts.userAbsPath))};
44
35
 
45
36
  export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
46
37
  return createElement(UiOverrideProvider, { value }, children);
@@ -73,10 +64,6 @@ function implicitRootLayoutAbsPath(appCwdPath: string, segments: string[]): stri
73
64
  return path.join(path.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
74
65
  }
75
66
 
76
- function implicitDictionaryMacroAbsPath(appCwdPath: string): string {
77
- return path.join(path.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
78
- }
79
-
80
67
  export function isRootBoundarySegments(segments: string[], basePaths: Iterable<string>): boolean {
81
68
  const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
82
69
  if (firstVisibleIndex === -1) return segments.length <= 1;
@@ -99,10 +86,7 @@ function findRootBoundaries(pageKeys: string[], appCwdPath: string, basePaths: I
99
86
  segments,
100
87
  });
101
88
  }
102
- const hasExplicitRootBoundary = [...boundaries.values()].some((boundary) => boundary.segments.length === 0);
103
- if (!hasExplicitRootBoundary && boundaries.size === 0) {
104
- boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
105
- }
89
+ if (boundaries.size === 0) boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
106
90
  return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
107
91
  }
108
92
 
@@ -123,11 +107,6 @@ function findExplicitRootLayoutAbsPath(pageKeys: string[], appCwdPath: string):
123
107
  return rootLayoutKey ? path.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
124
108
  }
125
109
 
126
- function routePrefixForSegments(segments: string[]): string | null {
127
- const visible = segments.filter((segment) => !/^\(.+\)$/.test(segment));
128
- return visible[0] ?? null;
129
- }
130
-
131
110
  async function assertEnvClientConvention(appCwdPath: string, appName: string) {
132
111
  const envPath = path.join(appCwdPath, "env", "env.client.ts");
133
112
  if (!(await Bun.file(envPath).exists())) {
@@ -138,8 +117,7 @@ async function assertEnvClientConvention(appCwdPath: string, appName: string) {
138
117
  }
139
118
 
140
119
  async function writeGeneratedDictionaryMacroFile(appCwdPath: string, appName: string): Promise<string> {
141
- const absPath = implicitDictionaryMacroAbsPath(appCwdPath);
142
- await mkdir(path.dirname(absPath), { recursive: true });
120
+ const absPath = path.join(path.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
143
121
  await Bun.write(
144
122
  absPath,
145
123
  `import { getAllDictionary } from "@apps/${appName}/lib/dict" with { type: "macro" };
@@ -163,35 +141,20 @@ async function writeGeneratedRootLayoutFile(opts: {
163
141
  ? await writeGeneratedDictionaryMacroFile(opts.appCwdPath, opts.appName)
164
142
  : null;
165
143
  const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
166
- await mkdir(path.dirname(absPath), { recursive: true });
167
- const dictMacroRel = dictMacroAbsPath
168
- ? path.relative(path.dirname(absPath), dictMacroAbsPath).split(path.sep).join("/")
169
- : null;
170
- const dictMacroSpecifier = dictMacroRel ? (dictMacroRel.startsWith(".") ? dictMacroRel : `./${dictMacroRel}`) : null;
171
- const sourceRel = opts.boundary.sourceAbsPath
172
- ? path.relative(path.dirname(absPath), opts.boundary.sourceAbsPath).split(path.sep).join("/")
173
- : null;
174
- const sourceSpecifier = sourceRel ? (sourceRel.startsWith(".") ? sourceRel : `./${sourceRel}`) : null;
144
+ const dictMacroSpecifier = dictMacroAbsPath ? importSpecifier(absPath, dictMacroAbsPath) : null;
145
+ const sourceSpecifier = opts.boundary.sourceAbsPath ? importSpecifier(absPath, opts.boundary.sourceAbsPath) : null;
175
146
  const inheritedSourceAbsPath =
176
147
  opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null;
177
- const inheritedSourceRel = inheritedSourceAbsPath
178
- ? path.relative(path.dirname(absPath), inheritedSourceAbsPath).split(path.sep).join("/")
179
- : null;
180
- const inheritedSourceSpecifier = inheritedSourceRel
181
- ? inheritedSourceRel.startsWith(".")
182
- ? inheritedSourceRel
183
- : `./${inheritedSourceRel}`
184
- : null;
148
+ const inheritedSourceSpecifier = inheritedSourceAbsPath ? importSpecifier(absPath, inheritedSourceAbsPath) : null;
185
149
  const clientImport = opts.includeStInit
186
150
  ? `import { st } from "@apps/${opts.appName}/client";\nvoid st;\n`
187
151
  : `import "@apps/${opts.appName}/client";\n`;
188
- // Both user modules go through `resolveRouteModule`: a `rootLayout()` chain and the legacy named exports read
189
- // the same afterwards, and this file never has to know which shape the app wrote.
152
+ //? resolveRouteModule reads a rootLayout() chain and the legacy named exports alike.
190
153
  const inheritedLabel = inheritedSourceAbsPath ? path.relative(opts.appCwdPath, inheritedSourceAbsPath) : "";
191
154
  const inheritedImport = inheritedSourceSpecifier
192
155
  ? `import * as inheritedModule from ${JSON.stringify(inheritedSourceSpecifier)};\nconst inheritedLayout = resolveRouteModule(inheritedModule as never, ${JSON.stringify(inheritedLabel)}).module as LayoutModule;\n`
193
156
  : "const inheritedLayout: LayoutModule = {};\n";
194
- const prefix = routePrefixForSegments(opts.boundary.segments);
157
+ const prefix = opts.boundary.segments.find((segment) => !/^\(.+\)$/.test(segment)) ?? null;
195
158
  const userLabel = opts.boundary.sourceAbsPath ? path.relative(opts.appCwdPath, opts.boundary.sourceAbsPath) : "";
196
159
  const userImport = sourceSpecifier
197
160
  ? `import * as userModule from ${JSON.stringify(sourceSpecifier)};\nconst userLayout = resolveRouteModule(userModule as never, ${JSON.stringify(userLabel)}).module as LayoutModule;\nconst UserLayout = userLayout.default as (props: LayoutProps) => ReactNode | Promise<ReactNode>;\n`
@@ -200,9 +163,8 @@ async function writeGeneratedRootLayoutFile(opts: {
200
163
  ? await AsyncDefaultExportDetector.detect(opts.boundary.sourceAbsPath)
201
164
  : false;
202
165
  const userLayoutElement = "<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>";
203
- // React has no async client component, so the CSR bundle calls an async layout and awaits its node the way
204
- // `RenderLayer` does for pages. The RSC render keeps the element: awaiting there would hold the shell behind
205
- // the layout's own awaits instead of streaming it as its own Flight chunk.
166
+ // React has no async client component, so CSR awaits an async layout's node; RSC keeps the element, since
167
+ // awaiting there would hold the shell behind the layout's own awaits instead of streaming it.
206
168
  const layoutSignature = isAsyncUserLayout
207
169
  ? "export default async function GeneratedLayout"
208
170
  : "export default function GeneratedLayout";
@@ -215,6 +177,19 @@ async function writeGeneratedRootLayoutFile(opts: {
215
177
  : "";
216
178
  const layoutChild = isAsyncUserLayout ? "{layout}" : userLayoutElement;
217
179
  const layoutReturn = isAsyncUserLayout ? "layout" : userLayoutElement;
180
+ const layoutExports = `export async function generateHead(props: PageProps) {
181
+ if (userLayout.generateHead) return userLayout.generateHead(props);
182
+ if (userLayout.head !== undefined) return userLayout.head;
183
+ if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);
184
+ return inheritedLayout.head;
185
+ }
186
+
187
+ export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
188
+ export const Error = userLayout.Error ?? inheritedLayout.Error;
189
+ export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
190
+
191
+ ${layoutSignature}({ children, params, searchParams }: LayoutProps) {
192
+ ${layoutBinding} return `;
218
193
  const source = opts.includeSystemProvider
219
194
  ? `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
220
195
  import { loadFonts, resolveRouteModule } from "akanjs/client";
@@ -229,26 +204,7 @@ if (defaultFonts.length > 1) throw new Error("[route-convention] only one defaul
229
204
  const defaultFont = defaultFonts[0];
230
205
  const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
231
206
 
232
- export async function generateHead(props: PageProps) {
233
- if (userLayout.generateHead) return userLayout.generateHead(props);
234
- if (userLayout.head !== undefined) return userLayout.head;
235
- if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);
236
- return inheritedLayout.head;
237
- }
238
-
239
- export async function generateMetadata(props: PageProps) {
240
- if (userLayout.generateMetadata) return userLayout.generateMetadata(props);
241
- if (userLayout.metadata !== undefined) return userLayout.metadata;
242
- if (inheritedLayout.generateMetadata) return inheritedLayout.generateMetadata(props);
243
- return inheritedLayout.metadata;
244
- }
245
-
246
- export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
247
- export const Error = userLayout.Error ?? inheritedLayout.Error;
248
- export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
249
-
250
- ${layoutSignature}({ children, params, searchParams }: LayoutProps) {
251
- ${layoutBinding} return (
207
+ ${layoutExports}(
252
208
  <System.Provider
253
209
  of={GeneratedLayout as never}
254
210
  appName=${JSON.stringify(opts.appName)}
@@ -258,7 +214,6 @@ ${layoutBinding} return (
258
214
  theme={userLayout.theme ?? inheritedLayout.theme}
259
215
  fonts={loadFonts(userFonts)}
260
216
  className={defaultFontClassName}
261
- gaTrackingId={userLayout.gaTrackingId ?? inheritedLayout.gaTrackingId}
262
217
  layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
263
218
  reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
264
219
  wsConnect={userLayout.wsConnect ?? inheritedLayout.wsConnect ?? true}
@@ -273,36 +228,13 @@ ${layoutBinding} return (
273
228
  import { resolveRouteModule } from "akanjs/client";
274
229
  import type { ReactNode } from "react";
275
230
  ${inheritedImport}${userImport}
276
- export async function generateHead(props: PageProps) {
277
- if (userLayout.generateHead) return userLayout.generateHead(props);
278
- if (userLayout.head !== undefined) return userLayout.head;
279
- if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);
280
- return inheritedLayout.head;
281
- }
282
-
283
- export async function generateMetadata(props: PageProps) {
284
- if (userLayout.generateMetadata) return userLayout.generateMetadata(props);
285
- if (userLayout.metadata !== undefined) return userLayout.metadata;
286
- if (inheritedLayout.generateMetadata) return inheritedLayout.generateMetadata(props);
287
- return inheritedLayout.metadata;
288
- }
289
-
290
- export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
291
- export const Error = userLayout.Error ?? inheritedLayout.Error;
292
- export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
293
-
294
- ${layoutSignature}({ children, params, searchParams }: LayoutProps) {
295
- ${layoutBinding} return ${layoutReturn};
231
+ ${layoutExports}${layoutReturn};
296
232
  }
297
233
  `;
298
234
  await Bun.write(absPath, source);
299
235
  return absPath;
300
236
  }
301
237
 
302
- /**
303
- * When no root `page/_layout.*` exists on disk, merge a generated implicit root layout
304
- * (with generated client runtime registration and optional `void st` when `lib/st.ts` exists).
305
- */
306
238
  export async function resolveSsrPageEntries(opts: {
307
239
  appCwdPath: string;
308
240
  appName: string;
@@ -310,7 +242,7 @@ export async function resolveSsrPageEntries(opts: {
310
242
  basePaths?: Iterable<string>;
311
243
  }): Promise<PageEntry[]> {
312
244
  const absPageDir = path.resolve(opts.appCwdPath, "page");
313
- const hasSt = await appHasStModule(opts.appCwdPath);
245
+ const hasSt = await Bun.file(path.join(opts.appCwdPath, "lib", "st.ts")).exists();
314
246
  const basePaths = opts.basePaths ?? [];
315
247
  const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
316
248
  const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
@@ -325,9 +257,7 @@ export async function resolveSsrPageEntries(opts: {
325
257
  .filter((key) => !rootLayoutKeys.has(key))
326
258
  .map(async (key) => {
327
259
  const userAbsPath = path.resolve(absPageDir, key);
328
- // `_overrides.tsx` is served through a generated `"use client"` wrapper layout that mounts the provider,
329
- // so the author writes no directive; the raw manifest becomes a build seed so its slot components (and
330
- // the manifest itself) enter the client graph / RSC client manifest.
260
+ //? The raw manifest is a seed, so it and its slot components enter the client graph and RSC client manifest.
331
261
  if (OVERRIDES_KEY_RE.test(key)) {
332
262
  const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
333
263
  appCwdPath: opts.appCwdPath,
@@ -353,9 +283,7 @@ export async function resolveSsrPageEntries(opts: {
353
283
  seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))],
354
284
  })),
355
285
  );
356
- const entries = [...base, ...generated];
357
- entries.sort((a, b) => a.key.localeCompare(b.key));
358
- return entries;
286
+ return [...base, ...generated].sort((a, b) => a.key.localeCompare(b.key));
359
287
  }
360
288
 
361
289
  export async function resolveSsrPageEntriesForApp(app: App, pageKeys: string[]): Promise<PageEntry[]> {
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import path from "node:path";
2
3
  import { computeRouteSeedIndex, serializeRouteSeedIndexForArtifact } from "./routeSeedIndex";
3
4
 
4
5
  describe("computeRouteSeedIndex", () => {
@@ -19,10 +20,10 @@ describe("computeRouteSeedIndex", () => {
19
20
  ]);
20
21
 
21
22
  const leaf = index.entries.find((entry) => entry.routeId === "/:lang/foo");
22
- expect(leaf?.seeds).toContain("/app/page/foo.tsx");
23
- expect(leaf?.seeds).not.toContain("/app/page/foo/_layout.tsx");
24
- expect(leaf?.seeds).toContain("/app/.akan/generated/root-layouts/__root_layout.tsx");
25
- expect(leaf?.seeds).toContain("/app/page/_layout.tsx");
23
+ expect(leaf?.seeds).toContain(path.resolve("/app/page/foo.tsx"));
24
+ expect(leaf?.seeds).not.toContain(path.resolve("/app/page/foo/_layout.tsx"));
25
+ expect(leaf?.seeds).toContain(path.resolve("/app/.akan/generated/root-layouts/__root_layout.tsx"));
26
+ expect(leaf?.seeds).toContain(path.resolve("/app/page/_layout.tsx"));
26
27
 
27
28
  expect(() =>
28
29
  computeRouteSeedIndex([
@@ -42,8 +43,8 @@ describe("computeRouteSeedIndex", () => {
42
43
  { key: "./robots.txt.tsx", moduleAbsPath: "/app/page/robots.txt.tsx" },
43
44
  ]);
44
45
  const robots = index.entries.find((entry) => entry.routeId === "/robots.txt");
45
- expect(robots?.seeds).not.toContain("/app/.akan/generated/root-layouts/__root_layout.tsx");
46
- expect(robots?.seeds).not.toContain("/app/page/_layout.tsx");
46
+ expect(robots?.seeds).not.toContain(path.resolve("/app/.akan/generated/root-layouts/__root_layout.tsx"));
47
+ expect(robots?.seeds).not.toContain(path.resolve("/app/page/_layout.tsx"));
47
48
  });
48
49
 
49
50
  test("serializes seed paths relative to the artifact directory", () => {
@@ -1,29 +1,13 @@
1
1
  import path from "node:path";
2
2
  import { assertUniqueRoutePatterns, compareRouteSpecificity, parseRouteModuleKey } from "akanjs/common";
3
+ import {
4
+ ROUTE_SEED_INDEX_JSON,
5
+ type RouteSeedEntry,
6
+ type RouteSeedIndex,
7
+ } from "akanjs/server/artifact/routeSeedIndexStore";
3
8
  import type { PageEntry } from "./implicitRootLayout";
4
9
 
5
- /**
6
- * Build-time utilities for the route seed index. `computeRouteSeedIndex`
7
- * takes resolved `PageEntry` rows (`key` plus absolute module path — files
8
- * under `app/` or the generated implicit root layout) and produces — for every
9
- * route — the list of source files that seed the `"use client"` graph walk.
10
- *
11
- * The result is serialized to `route-seed-index.json` by
12
- * `saveRouteSeedIndex` so the runtime server can call
13
- * `loadRouteSeedIndex` to restore it without re-importing `pages.ts`
14
- * (which no longer exists) or reparsing loader sources.
15
- */
16
-
17
- export interface RouteSeedEntry {
18
- routeId: string;
19
- pattern: string;
20
- seeds: string[];
21
- }
22
-
23
- export interface RouteSeedIndex {
24
- entries: RouteSeedEntry[];
25
- globalLayoutFiles: string[];
26
- }
10
+ export { ROUTE_SEED_INDEX_JSON, type RouteSeedEntry, type RouteSeedIndex };
27
11
 
28
12
  export type SerializedRouteSeedEntry = Pick<RouteSeedEntry, "routeId"> &
29
13
  Partial<Pick<RouteSeedEntry, "pattern" | "seeds">>;
@@ -33,10 +17,6 @@ export interface SerializedRouteSeedIndex {
33
17
  globalLayoutFiles?: string[];
34
18
  }
35
19
 
36
- /**
37
- * Compute the route seed index from `PageEntry`s (disk paths or generated
38
- * implicit root layout absolute paths).
39
- */
40
20
  export function computeRouteSeedIndex(pageEntries: PageEntry[]): RouteSeedIndex {
41
21
  const layoutsByPrefix = new Map<string, string[]>();
42
22
  const pagesBySegments: Array<{
@@ -51,11 +31,8 @@ export function computeRouteSeedIndex(pageEntries: PageEntry[]): RouteSeedIndex
51
31
  const parsed = parseRouteModuleKey(key);
52
32
  const files = [path.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path.resolve(seed))];
53
33
  if (parsed.kind === "layout" || parsed.kind === "overrides") {
54
- // Overrides seed the client graph like layouts: every route under the prefix must pull the generated
55
- // `"use client"` override wrapper (and its slot components) into the client bundle / RSC client manifest.
56
- const prefix = parsed.routeSegments.join("/");
57
- const prev = layoutsByPrefix.get(prefix) ?? [];
58
- layoutsByPrefix.set(prefix, [...prev, ...files]);
34
+ //? Every route under an overrides prefix needs its "use client" wrapper in the client graph, as with a layout.
35
+ layoutsByPrefix.getOrInsert(parsed.routeSegments.join("/"), []).push(...files);
59
36
  } else if (parsed.kind === "page") {
60
37
  pagesBySegments.push({
61
38
  key,
@@ -68,9 +45,7 @@ export function computeRouteSeedIndex(pageEntries: PageEntry[]): RouteSeedIndex
68
45
  }
69
46
  assertUniqueRoutePatterns(pagesBySegments);
70
47
 
71
- const rootLayouts = layoutsByPrefix.get("") ?? [];
72
- const globalLayoutFiles = rootLayouts;
73
-
48
+ const globalLayoutFiles = layoutsByPrefix.get("") ?? [];
74
49
  const seedEntries: RouteSeedEntry[] = [];
75
50
  for (const { pattern, segments, files, includeOwnLayout } of pagesBySegments) {
76
51
  const layouts: string[] = [];
@@ -84,26 +59,18 @@ export function computeRouteSeedIndex(pageEntries: PageEntry[]): RouteSeedIndex
84
59
  const routeId = pattern || "/";
85
60
  seedEntries.push({ routeId, pattern: routeId, seeds });
86
61
  }
87
- // Static segments must beat `:param` ones at match time, otherwise
88
- // `/persona/new` resolves to `[personaId]=new`. Sort once at build time
89
- // so the runtime matcher can stay first-match-wins.
62
+ // Static segments must beat `:param` ones (`/persona/new` is not `[personaId]=new`); the runtime is first-match-wins.
90
63
  seedEntries.sort((a, b) => compareRouteSpecificity(a.pattern, b.pattern));
91
64
  return { entries: seedEntries, globalLayoutFiles };
92
65
  }
93
66
 
94
- export const ROUTE_SEED_INDEX_JSON = "route-seed-index.json";
95
-
96
67
  export function serializeRouteSeedIndexForArtifact(
97
68
  index: RouteSeedIndex,
98
69
  artifactDir: string,
99
70
  options: { production?: boolean } = {},
100
71
  ): SerializedRouteSeedIndex {
101
72
  const normalizedArtifactDir = path.resolve(artifactDir);
102
- if (options.production) {
103
- return {
104
- entries: index.entries.map((entry) => ({ routeId: entry.routeId })),
105
- };
106
- }
73
+ if (options.production) return { entries: index.entries.map((entry) => ({ routeId: entry.routeId })) };
107
74
  return {
108
75
  entries: index.entries.map((entry) => ({
109
76
  ...entry,
package/biome.base.json CHANGED
@@ -154,7 +154,12 @@
154
154
  ]
155
155
  },
156
156
  {
157
- "includes": ["{apps,libs}/**/page/**/*.ts", "{apps,libs}/**/page/**/*.tsx"],
157
+ "includes": [
158
+ "{apps,libs}/**/page/**/*.ts",
159
+ "{apps,libs}/**/page/**/*.tsx",
160
+ "{apps,libs}/**/*.Unit.tsx",
161
+ "{apps,libs}/**/*.View.tsx"
162
+ ],
158
163
  "plugins": ["./node_modules/@akanjs/devkit/lint/non-scalar-props-restricted.grit"]
159
164
  },
160
165
  {
package/biomeBase.ts CHANGED
@@ -1,9 +1,6 @@
1
1
  /** `extends` target for a workspace `biome.json`; Biome resolves it through node_modules. */
2
2
  export const biomeBaseConfig = "@akanjs/devkit/biome.base.json";
3
3
 
4
- // Biome moves rules between groups across minors — `noUnnecessaryConditions` sat in `nursery` at 2.4 and moved to
5
- // `suspicious` at 2.5 — and the stale position is a hard "unknown key" error, not a warning. A workspace whose
6
- // Biome disagrees with the shipped base config therefore fails to load it at all, which is why the version is
7
- // pinned here instead of resolved to latest at create time. Bump this and `biome.base.json` in one commit, and run
8
- // `biome migrate --write` in the workspace root and in `pkgs/@akanjs/devkit` so both configs move together.
4
+ // Pinned: Biome moves rules between groups across minors and a stale position is a hard config error. Bump it with
5
+ // `biome.base.json` in one commit, running `biome migrate --write` in the workspace root and in this package.
9
6
  export const biomeVersion = "2.5.12";
@@ -1,6 +1,5 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import type { AkanMobileTargetConfig } from "./akanConfig";
6
5
  import {
@@ -24,16 +23,12 @@ import {
24
23
  sanitizeIosNativeRunEnv,
25
24
  selectLocalDevHost,
26
25
  sortIosRunTargets,
26
+ toIosInfoPlistUsageDescriptions,
27
27
  writeRootCapacitorConfig,
28
28
  } from "./capacitorApp";
29
+ import { tempDirs } from "./testHelpers";
29
30
 
30
- const tempRoots: string[] = [];
31
-
32
- const makeTempRoot = async () => {
33
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-capacitor-app-"));
34
- tempRoots.push(root);
35
- return root;
36
- };
31
+ const makeTempRoot = tempDirs("akan-capacitor-app-");
37
32
 
38
33
  const baseTarget: AkanMobileTargetConfig = {
39
34
  name: "default",
@@ -49,10 +44,6 @@ const baseTarget: AkanMobileTargetConfig = {
49
44
  files: { android: { "app/google-services.json": "private/google-services.json" } },
50
45
  };
51
46
 
52
- afterEach(async () => {
53
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
54
- });
55
-
56
47
  describe("materializeCapacitorConfig", () => {
57
48
  test("writes only Capacitor fields for release config", () => {
58
49
  const config = materializeCapacitorConfig(
@@ -234,7 +225,7 @@ describe("iOS native run helpers", () => {
234
225
  expect(deviceCommand.xcodebuildArgs).toContain("id=device-1");
235
226
  expect(deviceCommand.xcodebuildArgs).toContain("App QA");
236
227
  expect(deviceCommand.appPath).toBe(
237
- "/repo/apps/minimal/ios/DerivedData/device-1/Build/Products/Debug-iphoneos/App.app",
228
+ path.join("/repo/apps/minimal/ios/DerivedData/device-1/Build/Products/Debug-iphoneos/App.app"),
238
229
  );
239
230
 
240
231
  const simulatorCommand = buildIosNativeRunCommand({
@@ -243,7 +234,7 @@ describe("iOS native run helpers", () => {
243
234
  });
244
235
  expect(simulatorCommand.xcodebuildArgs).toContain("platform=iOS Simulator,id=sim-1");
245
236
  expect(simulatorCommand.appPath).toBe(
246
- "/repo/apps/minimal/ios/DerivedData/sim-1/Build/Products/Debug-iphonesimulator/App.app",
237
+ path.join("/repo/apps/minimal/ios/DerivedData/sim-1/Build/Products/Debug-iphonesimulator/App.app"),
247
238
  );
248
239
  });
249
240
 
@@ -370,3 +361,28 @@ describe("raiseGradleMinSdkVersion", () => {
370
361
  expect(raiseGradleMinSdkVersion("minSdkVersion = 28", 26)).toBeNull();
371
362
  });
372
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
+ });