@akanjs/devkit 3.0.0-beta.3 → 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.
Files changed (341) hide show
  1. package/DEV_RUNTIME_KNOBS.md +44 -26
  2. package/README.ko.md +2 -2
  3. package/README.md +3 -2
  4. package/abstractDoc.test.ts +6 -15
  5. package/abstractDoc.ts +6 -8
  6. package/agentsIndex.test.ts +4 -7
  7. package/agentsIndex.ts +6 -22
  8. package/akanApp/BackendImportGraph.test.ts +8 -22
  9. package/akanApp/BackendImportGraph.ts +1 -20
  10. package/akanApp/akanApp.host.test.ts +56 -3
  11. package/akanApp/akanApp.host.ts +218 -278
  12. package/akanApp/devBootLatch.test.ts +39 -0
  13. package/akanApp/devBootLatch.ts +39 -0
  14. package/akanApp/devHostPolicy.test.ts +81 -35
  15. package/akanApp/devHostPolicy.ts +41 -102
  16. package/akanConfig/akanBin.test.ts +62 -0
  17. package/akanConfig/akanBin.ts +78 -0
  18. package/akanConfig/akanConfig.test.ts +404 -141
  19. package/akanConfig/akanConfig.ts +317 -274
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +21 -8
  22. package/akanContext.ts +110 -160
  23. package/akanMcpContract.ts +28 -73
  24. package/appSelectionMemory.ts +1 -4
  25. package/applicationBuildReporter.ts +37 -5
  26. package/applicationBuildRunner.test.ts +41 -5
  27. package/applicationBuildRunner.ts +71 -63
  28. package/applicationTestPreload.ts +3 -12
  29. package/artifact/implicitRootLayout.test.ts +32 -30
  30. package/artifact/implicitRootLayout.ts +58 -106
  31. package/artifact/routeSeedIndex.test.ts +7 -6
  32. package/artifact/routeSeedIndex.ts +11 -44
  33. package/biome.base.json +35 -2
  34. package/biomeBase.ts +2 -5
  35. package/cloud/cloudApi.test.ts +202 -0
  36. package/cloud/cloudApi.ts +77 -39
  37. package/cloud/configLock.test.ts +64 -0
  38. package/cloud/configLock.ts +96 -0
  39. package/cloud/constants.ts +26 -5
  40. package/cloud/globalConfig.ts +71 -32
  41. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  42. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  43. package/codeAgent/agent/CodeAgent.ts +551 -0
  44. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  45. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  46. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  47. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  48. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  49. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  50. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  51. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  52. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  53. package/codeAgent/agent/CodeMailbox.ts +148 -0
  54. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  55. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  56. package/codeAgent/agent/SubagentPool.ts +214 -0
  57. package/codeAgent/agent/akanCodeModel.ts +47 -0
  58. package/codeAgent/agent/akanCodePaths.ts +31 -0
  59. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  60. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  61. package/codeAgent/agent/codeAgent.test.ts +178 -0
  62. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  63. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  64. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  65. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  66. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  67. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  68. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  69. package/codeAgent/agent/consoleToStderr.ts +8 -0
  70. package/codeAgent/agent/jsonLines.ts +29 -0
  71. package/codeAgent/agent/stringArg.ts +5 -0
  72. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  73. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  74. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  75. package/codeAgent/feedback/PreviewView.ts +95 -0
  76. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  77. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  78. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  79. package/codeAgent/index.ts +34 -0
  80. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  81. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  82. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  83. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  84. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  85. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  86. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  87. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  88. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  89. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  90. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  91. package/codeAgent/tools/AkanCli.ts +50 -0
  92. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  93. package/codeAgent/tools/AkanEditScope.ts +48 -0
  94. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  95. package/codeAgent/tools/AkanToolPack.ts +92 -0
  96. package/codeAgent/tools/AkanVerifier.ts +48 -0
  97. package/codeAgent/tools/AskToolPack.ts +99 -0
  98. package/codeAgent/tools/MailToolPack.ts +76 -0
  99. package/codeAgent/tools/McpClient.ts +187 -0
  100. package/codeAgent/tools/McpOAuth.ts +233 -0
  101. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  102. package/codeAgent/tools/McpServerConfig.ts +129 -0
  103. package/codeAgent/tools/McpSignIn.ts +158 -0
  104. package/codeAgent/tools/McpTokenStore.ts +81 -0
  105. package/codeAgent/tools/McpToolPack.ts +199 -0
  106. package/codeAgent/tools/SessionToolPack.ts +91 -0
  107. package/codeAgent/tools/WebToolPack.ts +147 -0
  108. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  109. package/codeAgent/tools/askToolPack.test.ts +116 -0
  110. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  111. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  112. package/codegenLock.test.ts +4 -13
  113. package/codegenLock.ts +3 -17
  114. package/commandDecorators/argMeta.ts +1 -5
  115. package/commandDecorators/camelToKebabCase.ts +1 -0
  116. package/commandDecorators/command.ts +46 -102
  117. package/commandDecorators/commandBuilder.ts +30 -61
  118. package/commandDecorators/commandDecorators.test.ts +2 -10
  119. package/commandDecorators/dependencyBuilder.ts +10 -1
  120. package/commandDecorators/helpFormatter.ts +68 -145
  121. package/commandDecorators/index.ts +0 -2
  122. package/commandDecorators/targetMeta.ts +9 -7
  123. package/createTunnel.ts +7 -14
  124. package/csrE2e/csrE2eHarness.fixture.ts +505 -0
  125. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  126. package/csrE2e/csrPageStack.e2e.test.ts +252 -0
  127. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  128. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  129. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  130. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  131. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  132. package/dependencyScanner.test.ts +4 -16
  133. package/dependencyScanner.ts +31 -80
  134. package/devkitUtils.test.ts +48 -20
  135. package/executors.linkFallback.test.ts +103 -0
  136. package/executors.test.ts +176 -106
  137. package/executors.ts +212 -498
  138. package/fileEditor.ts +9 -37
  139. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  140. package/fileSys.ts +22 -9
  141. package/formSetterScanner.ts +3 -16
  142. package/frontendBuild/allRoutesBuilder.ts +7 -21
  143. package/frontendBuild/autoImportSync.test.ts +27 -38
  144. package/frontendBuild/autoImportSync.ts +31 -65
  145. package/frontendBuild/buildRouteClient.test.ts +123 -37
  146. package/frontendBuild/bundleDefine.test.ts +12 -0
  147. package/frontendBuild/bundleDefine.ts +16 -0
  148. package/frontendBuild/clientBuildTypes.ts +5 -36
  149. package/frontendBuild/clientEntriesBundler.ts +9 -40
  150. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  151. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  152. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  153. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  154. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  155. package/frontendBuild/csrDevBundler.test.ts +66 -0
  156. package/frontendBuild/csrDevBundler.ts +95 -0
  157. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  158. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  159. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  160. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  161. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  162. package/frontendBuild/csrDevPatcher.ts +548 -0
  163. package/frontendBuild/csrDevPaths.ts +145 -0
  164. package/frontendBuild/csrDevResolver.test.ts +239 -0
  165. package/frontendBuild/csrDevResolver.ts +363 -0
  166. package/frontendBuild/csrDevTypes.ts +61 -0
  167. package/frontendBuild/csrEntryFiles.ts +24 -0
  168. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  169. package/frontendBuild/cssCandidateCache.ts +6 -41
  170. package/frontendBuild/cssCompiler.ts +31 -82
  171. package/frontendBuild/cssImportResolver.ts +6 -19
  172. package/frontendBuild/devChangePlanner.ts +7 -10
  173. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  174. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  175. package/frontendBuild/devRegistryBundler.ts +192 -0
  176. package/frontendBuild/fontOptimizer.test.ts +6 -14
  177. package/frontendBuild/fontOptimizer.ts +15 -72
  178. package/frontendBuild/fontPruner.test.ts +5 -10
  179. package/frontendBuild/fontPruner.ts +5 -20
  180. package/frontendBuild/frontendBuild.test.ts +57 -70
  181. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  182. package/frontendBuild/hmrWatcher.test.ts +59 -32
  183. package/frontendBuild/hmrWatcher.ts +31 -74
  184. package/frontendBuild/index.ts +4 -0
  185. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  186. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  187. package/frontendBuild/precompressArtifacts.ts +5 -18
  188. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  189. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  190. package/frontendBuild/routeClientBuilder.ts +100 -62
  191. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  192. package/frontendBuild/serverGraphFile.test.ts +77 -0
  193. package/frontendBuild/serverGraphFile.ts +93 -0
  194. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  195. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  196. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  197. package/frontendBuild/ssrDevBundler.ts +85 -0
  198. package/frontendBuild/styleContract.ts +1 -8
  199. package/frontendBuild/themeValidator.ts +4 -26
  200. package/frontendBuild/watchRootResolver.test.ts +23 -42
  201. package/frontendBuild/watchRootResolver.ts +5 -27
  202. package/getRelatedCnsts.ts +12 -92
  203. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  204. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  205. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  206. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  207. package/incrementalBuilder/builderChannel.test.ts +24 -49
  208. package/incrementalBuilder/builderChannel.ts +6 -37
  209. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  210. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  211. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  212. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  213. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  214. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  215. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  216. package/incrementalBuilder/incrementalBuilder.proc.test.ts +269 -0
  217. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  218. package/index.ts +5 -19
  219. package/integration/devResourceProbe.ts +50 -56
  220. package/integration/devStability.integration.test.ts +105 -199
  221. package/integration/devStabilityHarness.test.ts +13 -28
  222. package/integration/devStabilityHarness.ts +65 -330
  223. package/integration/ssrMemoryProbe.ts +41 -167
  224. package/libSource.test.ts +4 -15
  225. package/libSource.ts +19 -31
  226. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  227. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  228. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  229. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  230. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  231. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  232. package/lint/gritRules.test.ts +8 -24
  233. package/lint/no-deep-internal-import.grit +2 -2
  234. package/lint/no-document-cookie.grit +29 -0
  235. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  236. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  237. package/lint/no-web-storage.grit +30 -0
  238. package/linter.test.ts +1 -2
  239. package/linter.ts +26 -116
  240. package/mobile/desktopBin.test.ts +177 -0
  241. package/mobile/desktopBin.ts +143 -0
  242. package/mobile/desktopServerStage.test.ts +204 -0
  243. package/mobile/desktopServerStage.ts +138 -0
  244. package/mobile/index.ts +9 -0
  245. package/mobile/mobileTarget.test.ts +57 -8
  246. package/mobile/mobileTarget.ts +45 -26
  247. package/mobile/nativeApi.test.ts +30 -0
  248. package/mobile/nativeApi.ts +40 -0
  249. package/mobile/nativeApp.test.ts +151 -0
  250. package/mobile/nativeApp.ts +405 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +414 -0
  254. package/mobile/nativeConfig.ts +296 -0
  255. package/mobile/nativePluginFolders.test.ts +81 -0
  256. package/mobile/nativePluginFolders.ts +60 -0
  257. package/mobile/nativeWebDir.test.ts +80 -0
  258. package/mobile/nativeWebDir.ts +66 -0
  259. package/mobile/placeholderAppId.test.ts +13 -0
  260. package/mobile/placeholderAppId.ts +16 -0
  261. package/mobile/usageDescriptions.test.ts +27 -0
  262. package/mobile/usageDescriptions.ts +24 -0
  263. package/package.json +11 -11
  264. package/packageExports.test.ts +3 -18
  265. package/packageExportsMap.ts +3 -19
  266. package/piDependencyPins.test.ts +46 -0
  267. package/platformTest/BunTestReport.test.ts +60 -0
  268. package/platformTest/BunTestReport.ts +65 -0
  269. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  270. package/platformTest/IdleWatchedProcess.ts +73 -0
  271. package/platformTest/LinuxDockerTarget.ts +105 -0
  272. package/platformTest/PlatformTestRun.test.ts +64 -0
  273. package/platformTest/PlatformTestRun.ts +141 -0
  274. package/platformTest/PlatformTestTarget.ts +142 -0
  275. package/platformTest/TestSnapshot.test.ts +72 -0
  276. package/platformTest/TestSnapshot.ts +103 -0
  277. package/platformTest/WindowsSshTarget.ts +151 -0
  278. package/prompter.ts +3 -13
  279. package/qualityScanner.test.ts +9 -26
  280. package/qualityScanner.ts +200 -286
  281. package/recipeScanner.test.ts +7 -12
  282. package/recipeScanner.ts +9 -38
  283. package/repoIdentity.ts +2 -9
  284. package/routeSourceValidator.test.ts +0 -1
  285. package/routeSourceValidator.ts +7 -42
  286. package/scanInfo.ts +91 -225
  287. package/selectModel.ts +1 -2
  288. package/semver.ts +3 -5
  289. package/slicePlanner.test.ts +6 -24
  290. package/slicePlanner.ts +11 -32
  291. package/spinner.test.ts +3 -8
  292. package/spinner.ts +4 -16
  293. package/sshTunnel.ts +1 -6
  294. package/ssrScanner.test.ts +1 -6
  295. package/ssrScanner.ts +103 -113
  296. package/stripAnsi.ts +3 -0
  297. package/subspace.test.ts +30 -25
  298. package/subspace.ts +81 -115
  299. package/subspaceConfig.ts +11 -9
  300. package/testHelpers.ts +194 -0
  301. package/transforms/asyncDefaultExportDetector.ts +3 -11
  302. package/transforms/barrelAnalyzer.ts +43 -135
  303. package/transforms/barrelImportsPlugin.ts +38 -175
  304. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  305. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  306. package/transforms/moduleSyntax.ts +32 -0
  307. package/transforms/rscUseClientTransform.ts +42 -20
  308. package/transforms/serverEnvPlugin.test.ts +103 -0
  309. package/transforms/serverEnvPlugin.ts +41 -0
  310. package/transforms/transforms.test.ts +166 -44
  311. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  312. package/transforms/useClientBundlePlugin.ts +6 -23
  313. package/typeChecker.ts +7 -63
  314. package/types.ts +2 -0
  315. package/uploadRelease.ts +10 -25
  316. package/workflow/artifacts.ts +18 -0
  317. package/workflow/executor.test.ts +1 -12
  318. package/workflow/executor.ts +101 -110
  319. package/workflow/index.ts +0 -1
  320. package/workflow/moduleIndex.test.ts +4 -15
  321. package/workflow/moduleIndex.ts +9 -49
  322. package/workflow/plan.ts +18 -25
  323. package/workflow/render.ts +80 -96
  324. package/workflow/rolloutGate.test.ts +1 -1
  325. package/workflow/source.ts +52 -69
  326. package/workflow/sourceAst.ts +50 -0
  327. package/workspaceLayout.test.ts +18 -3
  328. package/workspaceLayout.ts +22 -23
  329. package/abstractCompactor.ts +0 -112
  330. package/aiEditor.test.ts +0 -60
  331. package/aiEditor.ts +0 -387
  332. package/applicationReleasePackager.ts +0 -209
  333. package/capacitorApp.test.ts +0 -372
  334. package/capacitorApp.ts +0 -1648
  335. package/commandDecorators/commandMeta.ts +0 -7
  336. package/commandDecorators/types.ts +0 -10
  337. package/getDirname.ts +0 -4
  338. package/guideline.ts +0 -19
  339. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  340. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  341. package/workflow/primitive.ts +0 -59
package/scanInfo.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { rm } from "node:fs/promises";
2
1
  import path from "node:path";
3
2
  import type {
4
3
  AppConfigResult,
@@ -11,7 +10,7 @@ import type {
11
10
  } from "./akanConfig";
12
11
  import { AkanAppConfig } from "./akanConfig";
13
12
  import { AppExecutor, LibExecutor, PkgExecutor, WorkspaceExecutor } from "./executors";
14
- import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
13
+ import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles, rootEntryHintOf } from "./workspaceLayout";
15
14
 
16
15
  const scalarFileTypes = ["constant", "dictionary", "document", "template", "unit", "util", "view", "zone"] as const;
17
16
  type ScalarFileType = (typeof scalarFileTypes)[number];
@@ -27,24 +26,16 @@ const serviceFileTypes = [
27
26
  "zone",
28
27
  ] as const;
29
28
  type ServiceFileType = (typeof serviceFileTypes)[number];
30
- const databaseFileTypes = [
31
- "constant",
32
- "dictionary",
33
- "document",
34
- "service",
35
- "signal",
36
- "store",
37
- "template",
38
- "unit",
39
- "util",
40
- "view",
41
- "zone",
42
- ] as const;
29
+ const databaseFileTypes = ["constant", "dictionary", "document", ...serviceFileTypes.slice(1)] as const;
43
30
  type DatabaseFileType = (typeof databaseFileTypes)[number];
31
+ const uiFileTypes = new Set<DatabaseFileType>(["template", "unit", "util", "view", "zone"]);
32
+ const fileTypeOf = (filename: string) =>
33
+ databaseFileTypes.find((type) =>
34
+ filename.endsWith(uiFileTypes.has(type) ? `.${type[0].toUpperCase()}${type.slice(1)}.tsx` : `.${type}.ts`),
35
+ );
44
36
 
45
37
  type ModuleKind = "database" | "service" | "scalar";
46
38
 
47
- const generatedRootCapacitorConfigFiles = ["capacitor.config.js", "capacitor.config.json"] as const;
48
39
  const internalLibDirs = new Set(["__lib", "__scalar"]);
49
40
  const moduleNonUiFileTypes = {
50
41
  database: new Set(["constant", "dictionary", "document", "service", "signal", "store"]),
@@ -56,27 +47,19 @@ const moduleUiFileTypes = {
56
47
  service: new Set(["Util", "Zone"]),
57
48
  scalar: new Set(["Template", "Unit"]),
58
49
  } satisfies Record<ModuleKind, Set<string>>;
59
- const testFilePattern = /\.(test|spec)\.(ts|tsx)$/;
60
50
 
61
- // The dependency scanner needs `typescript` (~65MB resident). Only the scan/sync commands run it, so
62
- // load it on demand rather than through the module graph of every process that imports scan info.
51
+ // Lazy: the dependency scanner pulls in `typescript` (~65MB resident), which only scan and sync need.
63
52
  const createDependencyScanner = async (exec: AppExecutor | LibExecutor | PkgExecutor) =>
64
53
  (await import("./dependencyScanner")).TypeScriptDependencyScanner.from(exec);
65
54
 
66
- const isAllowedTestFile = (filename: string) => testFilePattern.test(filename);
67
55
  const getScanPath = (exec: AppExecutor | LibExecutor, relativePath: string) =>
68
56
  path.posix.join(`${exec.type}s`, exec.name, relativePath.split(path.sep).join("/"));
69
- async function clearGeneratedRootCapacitorConfigs(exec: AppExecutor | LibExecutor) {
70
- if (exec.type !== "app") return;
71
- await Promise.all(generatedRootCapacitorConfigFiles.map((filename) => rm(exec.getPath(filename), { force: true })));
72
- }
73
57
  const getModuleNameFromPath = (kind: ModuleKind, modulePath: string) => {
74
58
  const dirname = path.basename(modulePath);
75
59
  return kind === "service" ? dirname.replace(/^_+/, "") : dirname;
76
60
  };
77
61
 
78
62
  async function assertScanConvention(exec: AppExecutor | LibExecutor, libRoot: { files: string[]; dirs: string[] }) {
79
- await clearGeneratedRootCapacitorConfigs(exec);
80
63
  const violations: string[] = [];
81
64
  const addViolation = (relativePath: string, reason: string) => {
82
65
  violations.push(`${getScanPath(exec, relativePath)}: ${reason}`);
@@ -85,33 +68,23 @@ async function assertScanConvention(exec: AppExecutor | LibExecutor, libRoot: {
85
68
  const allowedRootFiles: ReadonlySet<string> = rootAllowedFiles[exec.type];
86
69
  const allowedRootDirs: ReadonlySet<string> = rootAllowedDirs[exec.type];
87
70
  const { files, dirs } = await exec.getFilesAndDirs(".");
88
- files
89
- .filter((filename) => !allowedRootFiles.has(filename))
90
- .forEach((filename) => {
91
- addViolation(filename, `unsupported ${exec.type} root file`);
92
- });
93
- dirs
94
- .filter((dirname) => !allowedRootDirs.has(dirname))
95
- .forEach((dirname) => {
96
- addViolation(dirname, `unsupported ${exec.type} root folder`);
97
- });
98
-
99
- //* An app validates its page tree in `getPageKeys`, which a lib has no equivalent of — so a lib's own
100
- //* routes went unchecked until whichever app opted into them with `syncPageLibs` blew up on the sync.
71
+ for (const filename of files)
72
+ if (!allowedRootFiles.has(filename))
73
+ addViolation(filename, rootEntryHintOf(exec.type, filename) ?? `unsupported ${exec.type} root file`);
74
+ for (const dirname of dirs)
75
+ if (!allowedRootDirs.has(dirname))
76
+ addViolation(dirname, rootEntryHintOf(exec.type, dirname) ?? `unsupported ${exec.type} root folder`);
77
+
78
+ //* A lib has no `getPageKeys`, so its own route files are validated here.
101
79
  if (exec.type === "lib")
102
80
  for (const { relativePath, reason } of await exec.getPageConventionViolations()) addViolation(relativePath, reason);
103
81
 
104
- libRoot.files
105
- .filter((filename) => !isAllowedLibFacetRootFile(filename))
106
- .forEach((filename) => {
82
+ for (const filename of libRoot.files)
83
+ if (!isAllowedLibFacetRootFile(filename))
107
84
  addViolation(path.join("lib", filename), "unsupported lib facet root file");
108
- });
109
-
110
- libRoot.dirs
111
- .filter((dirname) => dirname.startsWith("__") && !internalLibDirs.has(dirname))
112
- .forEach((dirname) => {
85
+ for (const dirname of libRoot.dirs)
86
+ if (dirname.startsWith("__") && !internalLibDirs.has(dirname))
113
87
  addViolation(path.join("lib", dirname), "unsupported internal lib folder");
114
- });
115
88
 
116
89
  const databaseDirs = libRoot.dirs.filter((dirname) => !dirname.startsWith("_"));
117
90
  const serviceDirs = libRoot.dirs.filter((dirname) => dirname.startsWith("_") && !dirname.startsWith("__"));
@@ -146,43 +119,23 @@ async function validateModuleFiles(
146
119
 
147
120
  const uiModuleName = moduleName[0].toUpperCase() + moduleName.slice(1);
148
121
 
149
- files.forEach((filename) => {
150
- const filePath = path.join(modulePath, filename);
151
- if (filename === "index.ts" || filename === "index.tsx" || isAllowedTestFile(filename)) return;
152
- if (filename === `${moduleName}.abstract.md`) return;
153
-
122
+ for (const filename of files) {
123
+ const scanPath = getScanPath(exec, path.join(modulePath, filename));
124
+ if (filename === "index.ts" || filename === "index.tsx" || /\.(test|spec)\.(ts|tsx)$/.test(filename)) continue;
125
+ if (filename === `${moduleName}.abstract.md`) continue;
154
126
  const uiMatch = filename.match(/^([A-Z][A-Za-z0-9]+)\.([A-Z][A-Za-z0-9]*)\.tsx$/);
155
- if (uiMatch) {
156
- const fileModuleName = uiMatch[1];
157
- const fileType = uiMatch[2];
158
- if (fileModuleName !== uiModuleName) {
159
- violations.push(
160
- `${getScanPath(exec, filePath)}: module name mismatch: expected '${uiModuleName}', got '${fileModuleName}'`,
161
- );
162
- }
163
- if (!moduleUiFileTypes[kind].has(fileType)) {
164
- violations.push(`${getScanPath(exec, filePath)}: unsupported ${kind} UI file`);
165
- }
166
- return;
127
+ const match = uiMatch ?? filename.match(/^([a-z][a-zA-Z0-9]*)\.([a-z][a-z0-9]*)\.ts$/);
128
+ if (!match) {
129
+ violations.push(`${scanPath}: unsupported module file`);
130
+ continue;
167
131
  }
168
-
169
- const nonUiMatch = filename.match(/^([a-z][a-zA-Z0-9]*)\.([a-z][a-z0-9]*)\.ts$/);
170
- if (nonUiMatch) {
171
- const fileModuleName = nonUiMatch[1];
172
- const fileType = nonUiMatch[2];
173
- if (fileModuleName !== moduleName) {
174
- violations.push(
175
- `${getScanPath(exec, filePath)}: module name mismatch: expected '${moduleName}', got '${fileModuleName}'`,
176
- );
177
- }
178
- if (!moduleNonUiFileTypes[kind].has(fileType)) {
179
- violations.push(`${getScanPath(exec, filePath)}: unsupported ${kind} file`);
180
- }
181
- return;
182
- }
183
-
184
- violations.push(`${getScanPath(exec, filePath)}: unsupported module file`);
185
- });
132
+ const [, fileModuleName, fileType] = match;
133
+ const expectedName = uiMatch ? uiModuleName : moduleName;
134
+ if (fileModuleName !== expectedName)
135
+ violations.push(`${scanPath}: module name mismatch: expected '${expectedName}', got '${fileModuleName}'`);
136
+ if (!(uiMatch ? moduleUiFileTypes : moduleNonUiFileTypes)[kind].has(fileType))
137
+ violations.push(`${scanPath}: unsupported ${kind}${uiMatch ? " UI" : ""} file`);
138
+ }
186
139
  }
187
140
 
188
141
  class ScanInfo {
@@ -239,51 +192,16 @@ class ScanInfo {
239
192
  } else databaseDirs.push(name);
240
193
  });
241
194
 
195
+ const collect = async (dir: string, name: string, kind: "databases" | "services" | "scalars") => {
196
+ for (const filename of await exec.readdir(dir)) {
197
+ const type = fileTypeOf(filename);
198
+ if (type) (files[type] as { [key in typeof kind]?: string[] })[kind]?.push(name);
199
+ }
200
+ };
242
201
  await Promise.all([
243
- ...databaseDirs.map(async (name) => {
244
- const filenames = await exec.readdir(path.join("lib", name));
245
- filenames.forEach((filename) => {
246
- if (filename.endsWith(".constant.ts")) files.constant.databases.push(name);
247
- else if (filename.endsWith(".dictionary.ts")) files.dictionary.databases.push(name);
248
- else if (filename.endsWith(".document.ts")) files.document.databases.push(name);
249
- else if (filename.endsWith(".service.ts")) files.service.databases.push(name);
250
- else if (filename.endsWith(".signal.ts")) files.signal.databases.push(name);
251
- else if (filename.endsWith(".store.ts")) files.store.databases.push(name);
252
- else if (filename.endsWith(".Template.tsx")) files.template.databases.push(name);
253
- else if (filename.endsWith(".Unit.tsx")) files.unit.databases.push(name);
254
- else if (filename.endsWith(".Util.tsx")) files.util.databases.push(name);
255
- else if (filename.endsWith(".View.tsx")) files.view.databases.push(name);
256
- else if (filename.endsWith(".Zone.tsx")) files.zone.databases.push(name);
257
- });
258
- }),
259
- ...serviceDirs.map(async (dirname) => {
260
- const name = dirname.slice(1);
261
- const filenames = await exec.readdir(path.join("lib", dirname));
262
- filenames.forEach((filename) => {
263
- if (filename.endsWith(".dictionary.ts")) files.dictionary.services.push(name);
264
- else if (filename.endsWith(".service.ts")) files.service.services.push(name);
265
- else if (filename.endsWith(".signal.ts")) files.signal.services.push(name);
266
- else if (filename.endsWith(".store.ts")) files.store.services.push(name);
267
- else if (filename.endsWith(".Template.tsx")) files.template.services.push(name);
268
- else if (filename.endsWith(".Unit.tsx")) files.unit.services.push(name);
269
- else if (filename.endsWith(".Util.tsx")) files.util.services.push(name);
270
- else if (filename.endsWith(".View.tsx")) files.view.services.push(name);
271
- else if (filename.endsWith(".Zone.tsx")) files.zone.services.push(name);
272
- });
273
- }),
274
- ...scalarDirs.map(async (name) => {
275
- const filenames = await exec.readdir(path.join("lib/__scalar", name));
276
- filenames.forEach((filename) => {
277
- if (filename.endsWith(".constant.ts")) files.constant.scalars.push(name);
278
- else if (filename.endsWith(".dictionary.ts")) files.dictionary.scalars.push(name);
279
- else if (filename.endsWith(".document.ts")) files.document.scalars.push(name);
280
- else if (filename.endsWith(".Template.tsx")) files.template.scalars.push(name);
281
- else if (filename.endsWith(".Unit.tsx")) files.unit.scalars.push(name);
282
- else if (filename.endsWith(".Util.tsx")) files.util.scalars.push(name);
283
- else if (filename.endsWith(".View.tsx")) files.view.scalars.push(name);
284
- else if (filename.endsWith(".Zone.tsx")) files.zone.scalars.push(name);
285
- });
286
- }),
202
+ ...databaseDirs.map((name) => collect(path.join("lib", name), name, "databases")),
203
+ ...serviceDirs.map((dirname) => collect(path.join("lib", dirname), dirname.slice(1), "services")),
204
+ ...scalarDirs.map((name) => collect(path.join("lib/__scalar", name), name, "scalars")),
287
205
  ]);
288
206
  const routes = exec.type === "lib" ? [] : await (exec as AppExecutor).getPageKeys();
289
207
  const common = {
@@ -309,35 +227,16 @@ class ScanInfo {
309
227
  constructor(scanResult: ScanResult) {
310
228
  this.name = scanResult.name;
311
229
  this.scanResult = scanResult;
312
- Object.entries(scanResult.files).forEach(([_key, value]) => {
313
- const key = _key as DatabaseFileType;
314
- const { databases, services, scalars } = value as {
315
- databases: string[];
316
- services?: string[];
317
- scalars?: string[];
318
- };
319
- databases.forEach((modelName) => {
320
- const model = this.database.get(modelName) ?? new Set<DatabaseFileType>();
321
- model.add(key);
322
- this.database.set(modelName, model);
323
- this.file[key].all.add(modelName);
324
- this.file[key].databases.add(modelName);
325
- });
326
- services?.forEach((serviceName) => {
327
- const service = this.service.get(serviceName) ?? new Set<ServiceFileType>();
328
- service.add(key as ServiceFileType);
329
- this.service.set(serviceName, service);
330
- this.file[key].all.add(serviceName);
331
- this.file[key].services.add(serviceName);
332
- });
333
- scalars?.forEach((scalarName) => {
334
- const scalar = this.scalar.get(scalarName) ?? new Set<ScalarFileType>();
335
- scalar.add(key as ScalarFileType);
336
- this.scalar.set(scalarName, scalar);
337
- this.file[key].all.add(scalarName);
338
- this.file[key].scalars.add(scalarName);
339
- });
340
- });
230
+ const modulesOf = { databases: this.database, services: this.service, scalars: this.scalar } as {
231
+ [key in "databases" | "services" | "scalars"]: Map<string, Set<DatabaseFileType>>;
232
+ };
233
+ for (const [key, groups] of Object.entries(scanResult.files) as [DatabaseFileType, { [kind: string]: string[] }][])
234
+ for (const kind of ["databases", "services", "scalars"] as const)
235
+ for (const name of groups[kind] ?? []) {
236
+ modulesOf[kind].set(name, (modulesOf[kind].get(name) ?? new Set()).add(key));
237
+ this.file[key].all.add(name);
238
+ this.file[key][kind].add(name);
239
+ }
341
240
  }
342
241
  getScanResult() {
343
242
  return this.scanResult;
@@ -351,6 +250,29 @@ class ScanInfo {
351
250
  getScalarModules() {
352
251
  return [...this.scalar.keys()];
353
252
  }
253
+
254
+ #sortedLibs: string[] | null = null;
255
+ protected sortedLibs(libDeps: string[]) {
256
+ if (this.#sortedLibs) return this.#sortedLibs;
257
+ const libIndices = LibInfo.getSortedLibIndices();
258
+ this.#sortedLibs = libDeps.sort((libNameA, libNameB) => {
259
+ const indexA = libIndices.get(libNameA);
260
+ const indexB = libIndices.get(libNameB);
261
+ if (indexA === undefined || indexB === undefined)
262
+ throw new Error(`LibInfo not found: ${libNameA} or ${libNameB}`);
263
+ return indexA - indexB;
264
+ });
265
+ return this.#sortedLibs;
266
+ }
267
+ protected sortedLibInfos(libDeps: string[]) {
268
+ return new Map(
269
+ this.sortedLibs(libDeps).map((libName) => {
270
+ const libInfo = LibInfo.libInfos.get(libName);
271
+ if (!libInfo) throw new Error(`LibInfo not found: ${libName}`);
272
+ return [libName, libInfo];
273
+ }),
274
+ );
275
+ }
354
276
  }
355
277
 
356
278
  const isAkanFrameworkDependency = (dep: string) => dep === "akanjs" || dep.startsWith("akanjs/");
@@ -362,7 +284,6 @@ export class AppInfo extends ScanInfo {
362
284
 
363
285
  static appInfos = new Map<string, AppInfo>();
364
286
  static async fromExecutor(exec: AppExecutor, options: { refresh?: boolean } = {}) {
365
- // cache check
366
287
  const existingAppInfo = AppInfo.appInfos.get(exec.name);
367
288
  if (existingAppInfo && !options.refresh) return existingAppInfo;
368
289
  const scanResult = await ScanInfo.getScanResult(exec);
@@ -408,30 +329,11 @@ export class AppInfo extends ScanInfo {
408
329
  return [...libSet];
409
330
  }
410
331
 
411
- #sortedLibs: string[] | null = null;
412
- #getSortedLibs() {
413
- if (this.#sortedLibs) return this.#sortedLibs;
414
- const libIndices = LibInfo.getSortedLibIndices();
415
- this.#sortedLibs = this.libDeps.sort((libNameA, libNameB) => {
416
- const indexA = libIndices.get(libNameA);
417
- const indexB = libIndices.get(libNameB);
418
- if (indexA === undefined || indexB === undefined)
419
- throw new Error(`LibInfo not found: ${libNameA} or ${libNameB}`);
420
- return indexA - indexB;
421
- });
422
- return this.#sortedLibs;
423
- }
424
332
  getLibs() {
425
- return this.#getSortedLibs();
333
+ return this.sortedLibs(this.libDeps);
426
334
  }
427
335
  getLibInfos() {
428
- return new Map(
429
- this.#getSortedLibs().map((libName) => {
430
- const libInfo = LibInfo.libInfos.get(libName);
431
- if (!libInfo) throw new Error(`LibInfo not found: ${libName}`);
432
- return [libName, libInfo];
433
- }),
434
- );
336
+ return this.sortedLibInfos(this.libDeps);
435
337
  }
436
338
  }
437
339
  export class LibInfo extends ScanInfo {
@@ -482,36 +384,16 @@ export class LibInfo extends ScanInfo {
482
384
  return this.scanResult as LibScanResult;
483
385
  }
484
386
 
485
- #sortedLibs: string[] | null = null;
486
- #getSortedLibs() {
487
- if (this.#sortedLibs) return this.#sortedLibs;
488
- const libs = LibInfo.getSortedLibIndices();
489
- this.#sortedLibs = this.scanResult.libDeps.sort((libNameA, libNameB) => {
490
- const indexA = libs.get(libNameA);
491
- const indexB = libs.get(libNameB);
492
- if (indexA === undefined || indexB === undefined)
493
- throw new Error(`LibInfo not found: ${libNameA} or ${libNameB}`);
494
- return indexA - indexB;
495
- });
496
- return this.#sortedLibs;
497
- }
498
387
  getLibs() {
499
- return this.#getSortedLibs();
388
+ return this.sortedLibs(this.scanResult.libDeps);
500
389
  }
501
390
  getLibInfo(libName: string) {
502
391
  if (!this.getScanResult().libDeps.includes(libName)) return undefined;
503
- const libSet = new Set(this.#getSortedLibs());
504
- if (!libSet.has(libName)) throw new Error(`LibInfo is invalid: ${libName}`);
392
+ if (!this.getLibs().includes(libName)) throw new Error(`LibInfo is invalid: ${libName}`);
505
393
  return LibInfo.libInfos.get(libName);
506
394
  }
507
395
  getLibInfos() {
508
- return new Map(
509
- this.#getSortedLibs().map((libName) => {
510
- const libInfo = LibInfo.libInfos.get(libName);
511
- if (!libInfo) throw new Error(`LibInfo not found: ${libName}`);
512
- return [libName, libInfo];
513
- }),
514
- );
396
+ return this.sortedLibInfos(this.scanResult.libDeps);
515
397
  }
516
398
  }
517
399
 
@@ -524,27 +406,25 @@ export class PkgInfo {
524
406
  const [tsconfig, rootPackageJson] = await Promise.all([exec.getTsConfig(), exec.workspace.getPackageJson()]);
525
407
  const scanner = await createDependencyScanner(exec);
526
408
  const npmSet = new Set(Object.keys({ ...rootPackageJson.dependencies, ...rootPackageJson.devDependencies }));
409
+ const workspacePathOf = (resolve: string) => resolve.replace(/^\.\//, "");
527
410
  const pkgPathSet = new Set(
528
411
  Object.keys(tsconfig.compilerOptions.paths ?? {})
529
- .filter((path) => tsconfig.compilerOptions.paths?.[path]?.some((resolve) => resolve.startsWith("pkgs/")))
412
+ .filter((path) =>
413
+ tsconfig.compilerOptions.paths?.[path]?.some((resolve) => workspacePathOf(resolve).startsWith("pkgs/")),
414
+ )
530
415
  .map((path) => path.replace("/*", "")),
531
416
  );
532
417
  const [npmDepSet, pkgPathDepSet] = await scanner.getImportSets([npmSet, pkgPathSet]);
533
418
  const pkgDeps = [...pkgPathDepSet]
534
419
  .map((path) => {
535
420
  const pathSplitLength = path.split("/").length;
536
- return (tsconfig.compilerOptions.paths?.[path]?.[0] ?? "*")
421
+ return workspacePathOf(tsconfig.compilerOptions.paths?.[path]?.[0] ?? "*")
537
422
  .split("/")
538
423
  .slice(1, 1 + pathSplitLength)
539
424
  .join("/");
540
425
  })
541
426
  .filter((pkg) => pkg !== exec.name);
542
- const pkgScanResult = {
543
- name: exec.name,
544
- pkgDeps,
545
- dependencies: [...npmDepSet],
546
- };
547
- return pkgScanResult;
427
+ return { name: exec.name, pkgDeps, dependencies: [...npmDepSet] };
548
428
  }
549
429
 
550
430
  static #pkgInfos = new Map<string, PkgInfo>();
@@ -582,23 +462,9 @@ export class WorkspaceInfo {
582
462
  const [appNames, libNames, pkgNames] = await Promise.all([exec.getApps(), exec.getLibs(), exec.getPkgs()]);
583
463
  // TODO: prevent duplicate scan by resolving the dependency graph
584
464
  const [appInfos, libInfos, pkgInfos] = await Promise.all([
585
- Promise.all(
586
- appNames.map(async (appName) => {
587
- const app = AppExecutor.from(exec, appName);
588
- return await app.scan();
589
- }),
590
- ),
591
- Promise.all(
592
- libNames.map(async (libName) => {
593
- const lib = LibExecutor.from(exec, libName);
594
- return await lib.scan();
595
- }),
596
- ),
597
- Promise.all(
598
- pkgNames.map(async (pkgName) => {
599
- return await PkgExecutor.from(exec, pkgName).scan();
600
- }),
601
- ),
465
+ Promise.all(appNames.map(async (appName) => await AppExecutor.from(exec, appName).scan())),
466
+ Promise.all(libNames.map(async (libName) => await LibExecutor.from(exec, libName).scan())),
467
+ Promise.all(pkgNames.map(async (pkgName) => await PkgExecutor.from(exec, pkgName).scan())),
602
468
  ]);
603
469
  const workspaceInfo = new WorkspaceInfo(
604
470
  new Map(appInfos.map((app) => [app.exec.name, app as AppInfo])),
package/selectModel.ts CHANGED
@@ -3,9 +3,8 @@ import { select } from "@inquirer/prompts";
3
3
 
4
4
  export const selectModel = async (modulePath: string) => {
5
5
  const modelNames = readdirSync(`${modulePath}/lib`).filter((dir) => !dir.includes(".") && !dir.startsWith("_"));
6
- const modelName = await select({
6
+ return await select({
7
7
  message: "Select the model to create the unit for",
8
8
  choices: modelNames.map((name) => ({ name, value: name })),
9
9
  });
10
- return modelName;
11
10
  };
package/semver.ts CHANGED
@@ -1,14 +1,12 @@
1
- // `Bun.semver.order` compares range strings loosely — `^3.1049.0` vs `^3.721.0` answers 0 — so the
2
- // operator prefix has to come off before it sees the version.
1
+ // `Bun.semver.order` compares ranges loosely (`^3.1049.0` vs `^3.721.0` answers 0), so the operator comes off first.
3
2
  const stripRangeOperator = (version: string) => version.replace(/^[\s^~>=<v]+/, "").trim();
4
3
 
5
- const parseVersion = (version: string): number[] => {
6
- return version
4
+ const parseVersion = (version: string): number[] =>
5
+ version
7
6
  .replace(/^[^\d]*/, "")
8
7
  .split(/[.-]/)
9
8
  .map((part) => Number.parseInt(part, 10))
10
9
  .map((part) => (Number.isFinite(part) ? part : 0));
11
- };
12
10
 
13
11
  const compareNumeric = (a: string, b: string): number => {
14
12
  const left = parseVersion(a);
@@ -1,30 +1,13 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdir, rm } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { AppExecutor, Executor, WorkspaceExecutor } from "./executors";
6
5
  import { formatSlicePlan, SlicePlanner } from "./slicePlanner";
6
+ import { isolateEnv, tempDirs, writeText as write } from "./testHelpers";
7
7
  import type { PackageJson } from "./types";
8
8
 
9
- const tempRoots: string[] = [];
10
- const originalEnv = { ...process.env };
11
-
12
- beforeEach(() => {
13
- process.env = { ...originalEnv };
14
- process.env.AKAN_PUBLIC_REPO_NAME = "workspace";
15
- process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
16
- process.env.AKAN_PUBLIC_ENV = "local";
17
- });
18
-
19
- afterEach(async () => {
20
- process.env = { ...originalEnv };
21
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
22
- });
23
-
24
- const write = async (filePath: string, content: string) => {
25
- await mkdir(path.dirname(filePath), { recursive: true });
26
- await writeFile(filePath, content);
27
- };
9
+ isolateEnv({ AKAN_PUBLIC_REPO_NAME: "workspace", AKAN_PUBLIC_SERVE_DOMAIN: "example.com", AKAN_PUBLIC_ENV: "local" });
10
+ const makeTempRoot = tempDirs("akan-slice-");
28
11
 
29
12
  const gitignore = [
30
13
  "node_modules",
@@ -36,8 +19,7 @@ const gitignore = [
36
19
 
37
20
  // `AppExecutor.from` and `AppInfo.fromExecutor` both memoise by name, so each fixture needs a fresh one.
38
21
  const makeWorkspace = async (appName: string, libName: string) => {
39
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-slice-"));
40
- tempRoots.push(root);
22
+ const root = await makeTempRoot();
41
23
  const rootPackageJson: PackageJson = {
42
24
  name: "workspace",
43
25
  version: "0.0.1",
package/slicePlanner.ts CHANGED
@@ -18,12 +18,6 @@ export interface SlicePlan {
18
18
  warnings: string[];
19
19
  }
20
20
 
21
- /**
22
- * The exact file set a single app needs to live in a workspace of its own: the app, its transitive lib
23
- * closure, the workspace shell around them, and a root manifest for the result.
24
- *
25
- * Consumed by `akan plan-slice`, and by anything that moves an app or a lib between workspaces.
26
- */
27
21
  export class SlicePlanner {
28
22
  /** Root entries owned by an app, a lib or an in-tree package rather than by the workspace shell. */
29
23
  static readonly memberDirs = ["apps", "libs", "pkgs"];
@@ -39,13 +33,8 @@ export class SlicePlanner {
39
33
  ];
40
34
  static readonly akanPackages = ["akanjs", "@akanjs/devkit", "@akanjs/cli"];
41
35
 
42
- /**
43
- * What the framework imports but does not install, so a workspace root has to declare it: `react`,
44
- * `scheduler` and the RSC runtime, but also `ioredis`, `postgres` and `bullmq` the moment an app uses
45
- * a database or a queue. No source file in the workspace names any of them, so no scan can find them
46
- * — they are the akan packages' own `peerDependencies`, read wherever those are installed. Anything
47
- * they declare as a plain dependency is installed with them and needs no root entry.
48
- */
36
+ // The akan packages' peerDependencies (react, the RSC runtime, ioredis, postgres, bullmq): the framework imports
37
+ // them but no workspace source names them, so no scan can find them.
49
38
  static async #frameworkPeers(workspace: WorkspaceExecutor) {
50
39
  const peers = new Set<string>();
51
40
  let found = false;
@@ -67,14 +56,8 @@ export class SlicePlanner {
67
56
  return dependency.startsWith("@") ? `@types/${dependency.slice(1).replace("/", "__")}` : `@types/${dependency}`;
68
57
  }
69
58
 
70
- /**
71
- * The root manifest a workspace holding only these dependencies would carry, with the workspace's own
72
- * version specs kept — that is what makes one branch resolve to one dependency tree everywhere.
73
- *
74
- * A `@types/x` package is kept alongside `x`: nothing imports it by name, so no scan can see it, and
75
- * dropping it breaks the typecheck of every file that imports `x`. `workspaces` is dropped because it
76
- * names in-tree packages, which a slice never carries — it consumes akanjs from the registry.
77
- */
59
+ // `@types/x` is kept alongside `x` since nothing imports it by name; `workspaces` is dropped because a slice
60
+ // consumes akanjs from the registry.
78
61
  static async pruneDependencies(
79
62
  workspace: WorkspaceExecutor,
80
63
  rootPackageJson: PackageJson,
@@ -82,9 +65,8 @@ export class SlicePlanner {
82
65
  ) {
83
66
  const peers = await SlicePlanner.#frameworkPeers(workspace);
84
67
  const { workspaces: _workspaces, ...rest } = rootPackageJson;
85
- //* Without akanjs's peer list every peer looks unused, and pruning it produces an install that
86
- //* succeeds and a server that fails at its first redis or postgres call. Carry the dependencies
87
- //* whole instead and say why.
68
+ //* Without the peer list every peer looks unused, and pruning it breaks the server at its first redis or
69
+ //* postgres call.
88
70
  if (!peers)
89
71
  return {
90
72
  packageJson: rest as PackageJson,
@@ -121,10 +103,6 @@ export class SlicePlanner {
121
103
  this.#app = app;
122
104
  }
123
105
 
124
- async #trackedFiles(paths: string[]) {
125
- return await this.#app.workspace.listGitFiles(paths);
126
- }
127
-
128
106
  async #untrackedFiles(paths: string[]) {
129
107
  const [tracked, all] = await Promise.all([
130
108
  this.#app.workspace.listGitFiles(paths),
@@ -170,14 +148,16 @@ export class SlicePlanner {
170
148
  const slicePaths = [`apps/${this.#app.name}`, ...libs.map((lib) => `libs/${lib}`)];
171
149
 
172
150
  const [appFiles, allTracked, untracked, rootPackageJson] = await Promise.all([
173
- this.#trackedFiles([`apps/${this.#app.name}`]),
174
- this.#trackedFiles(["."]),
151
+ this.#app.workspace.listGitFiles([`apps/${this.#app.name}`]),
152
+ this.#app.workspace.listGitFiles(["."]),
175
153
  this.#untrackedFiles(slicePaths),
176
154
  this.#app.workspace.getPackageJson(),
177
155
  ]);
178
156
 
179
157
  const libFiles = Object.fromEntries(
180
- await Promise.all(libs.map(async (lib) => [lib, await this.#trackedFiles([`libs/${lib}`])] as const)),
158
+ await Promise.all(
159
+ libs.map(async (lib) => [lib, await this.#app.workspace.listGitFiles([`libs/${lib}`])] as const),
160
+ ),
181
161
  );
182
162
  const rootFiles = allTracked.filter((file) => !this.#isMemberFile(file));
183
163
 
@@ -211,7 +191,6 @@ export class SlicePlanner {
211
191
  }
212
192
  }
213
193
 
214
- /** Top-level entry each path sits under, so a long tree (`infra/**`) reads as one line. */
215
194
  const rootEntriesOf = (rootFiles: string[]) => [...new Set(rootFiles.map((file) => file.split("/")[0] ?? file))].sort();
216
195
 
217
196
  export function formatSlicePlan(plan: SlicePlan) {
package/spinner.test.ts CHANGED
@@ -6,10 +6,7 @@ interface TtyStub {
6
6
  rawModeCalls: boolean[];
7
7
  }
8
8
 
9
- /**
10
- * ora only reaches for stdin when both streams look interactive, so the stub has to fake a tty on
11
- * stderr (which decides `isEnabled`) as well as on stdin.
12
- */
9
+ // ora only reaches for stdin when both streams look interactive, so stderr (which decides `isEnabled`) is faked too.
13
10
  const stubTty = (): TtyStub => {
14
11
  const rawModeCalls: boolean[] = [];
15
12
  const stdin = process.stdin as NodeJS.ReadStream & { setRawMode?: (mode: boolean) => unknown };
@@ -41,8 +38,7 @@ const stubTty = (): TtyStub => {
41
38
  };
42
39
 
43
40
  describe("Spinner", () => {
44
- // A raw terminal at spawn time is what a Bun child snapshots and writes back when it exits, which is
45
- // how `akan start` used to leave a terminal that no longer turns Ctrl+C into SIGINT.
41
+ // A Bun child snapshots a raw terminal at spawn and writes it back on exit, leaving Ctrl+C no longer a SIGINT.
46
42
  test("never puts the terminal into raw mode while it spins", () => {
47
43
  const tty = stubTty();
48
44
  try {
@@ -59,8 +55,7 @@ describe("Spinner", () => {
59
55
  expect(Spinner.oraOptions.discardStdin).toBe(false);
60
56
  });
61
57
 
62
- // An unsized pty reports `isTTY: true` with `columns: 0`, which turns ora's clear loop into an
63
- // infinite one — the process then writes cursor moves until it is SIGKILLed.
58
+ // An unsized pty reports `isTTY: true` with `columns: 0`, which turns ora's clear loop into an infinite one.
64
59
  test("refuses to animate against a tty that reports no width", () => {
65
60
  expect(Spinner.canAnimate({ isTTY: true, columns: 0 } as NodeJS.WriteStream)).toBe(false);
66
61
  expect(Spinner.canAnimate({ isTTY: true, columns: 120 } as NodeJS.WriteStream)).toBe(true);