@akanjs/devkit 3.0.0-beta.9 → 3.0.0

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 +21 -7
  3. package/README.md +17 -8
  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 +439 -141
  19. package/akanConfig/akanConfig.ts +359 -275
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +22 -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 +270 -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 +182 -0
  241. package/mobile/desktopBin.ts +144 -0
  242. package/mobile/desktopServerStage.test.ts +252 -0
  243. package/mobile/desktopServerStage.ts +178 -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 +184 -0
  250. package/mobile/nativeApp.ts +454 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +438 -0
  254. package/mobile/nativeConfig.ts +294 -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 +9 -25
  298. package/subspace.ts +48 -115
  299. package/subspaceConfig.ts +3 -11
  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
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { optimize } from "@tailwindcss/node";
3
+ import type { AkanNativeAppId, AkanWebConfig } from "akanjs";
3
4
  import type { BaseBuildArtifact } from "akanjs/server";
4
5
  import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
5
6
  import { computeRouteSeedIndex, type RouteSeedIndex, saveRouteSeedIndex } from "../artifact/routeSeedIndex";
@@ -36,6 +37,12 @@ export class SsrBaseArtifactBuilder {
36
37
  this.#absArtifactDir = path.resolve(this.#artifactDir);
37
38
  }
38
39
 
40
+ //? The web router reads this, so `akan start` writes the whole surface: a native shell asks the dev server for the
41
+ //? CSR page even when the app ships none, and a narrowed dev artifact hands it the SSR page, which cannot run there.
42
+ static servedWeb(command: "build" | "start", web: AkanWebConfig): AkanWebConfig {
43
+ return command === "start" ? { ssr: true, csr: true } : web;
44
+ }
45
+
39
46
  async build(): Promise<BuildSsrBaseArtifactResult> {
40
47
  const akanConfig = await this.#app.getConfig();
41
48
  const { rscClientUrl, rscRuntimeClientManifest, rscRuntimeSsrManifest, vendorMap } =
@@ -77,15 +84,16 @@ export class SsrBaseArtifactBuilder {
77
84
  branches: [...akanConfig.branches],
78
85
  i18n: akanConfig.i18n,
79
86
  imageConfig: akanConfig.images,
80
- web: akanConfig.web,
81
- deepLinkAssociations: Object.values(akanConfig.mobile.targets)
87
+ web: SsrBaseArtifactBuilder.servedWeb(this.#command, akanConfig.web),
88
+ deepLinkAssociations: Object.values(akanConfig.native.targets)
82
89
  .filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0)
83
90
  .map((target) => ({
84
91
  targetName: target.name,
85
- appId: target.appId,
92
+ iosAppId: SsrBaseArtifactBuilder.#appIdOn(target.appId, "ios"),
93
+ androidAppId: SsrBaseArtifactBuilder.#appIdOn(target.appId, "android"),
86
94
  domains: target.deepLinks?.domains ?? [],
87
- iosTeamId: target.deepLinks?.ios?.teamId,
88
- androidSha256CertFingerprints: target.deepLinks?.android?.sha256CertFingerprints,
95
+ iosTeamId: target.ios?.teamId,
96
+ androidSha256CertFingerprints: target.android?.sha256CertFingerprints,
89
97
  })),
90
98
  };
91
99
  await Bun.write(path.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}\n`);
@@ -185,8 +193,7 @@ export class SsrBaseArtifactBuilder {
185
193
  }> {
186
194
  const cssCompiler = new CssCompiler(this.#app);
187
195
  const cssByBasePath = await cssCompiler.getCssByBasePath();
188
- // 스타일 계약(어휘 폐쇄 + WCAG)은 빌드가 아니라 lint 가 강제한다: 어휘 폐쇄는 biome grit 플러그인
189
- // (devkit/lint/no-raw-palette-class.grit 외 3종), 콘트라스트는 `akan lint` 의 themeValidator.
196
+ // The style contract (vocabulary closure, WCAG contrast) is enforced by `akan lint`, not by the build.
190
197
  const optimizedFonts = await new FontOptimizer(this.#app, this.#command).optimize();
191
198
  const cssAssets = Object.fromEntries(
192
199
  await Promise.all(
@@ -216,12 +223,11 @@ export class SsrBaseArtifactBuilder {
216
223
  return [basePath, { cssUrl, cssRelPath }] as const;
217
224
  }
218
225
 
219
- /**
220
- * Checked against the file written here rather than against the compiled text, because this is the stylesheet
221
- * `base-artifact.json` points at and therefore the only one an SSR render serves. A declaration can survive
222
- * the compile and still be missing from the asset — a build that ships CSS to the CSR bundle and not to the
223
- * server is indistinguishable, in the browser, from a theme that was never written.
224
- */
226
+ // Checked against the written asset, not the compiled text: it is the only stylesheet an SSR render serves.
227
+ static #appIdOn(appId: AkanNativeAppId, platform: "ios" | "android"): string | null {
228
+ return typeof appId === "string" ? appId : (appId[platform] ?? appId.default ?? null);
229
+ }
230
+
225
231
  static #warnDroppedImports(app: App, cssRelPath: string, css: string, imported: ImportedStylesheet[]) {
226
232
  for (const { cssPath, declaredNames } of imported) {
227
233
  if (declaredNames.length === 0 || declaredNames.some((name) => css.includes(`${name}:`))) continue;
@@ -0,0 +1,85 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { SSR_DEV_DIRNAME, SSR_DEV_ROUTE_PREFIX } from "akanjs/server/hmr/csrDevManifest";
4
+ import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
5
+ import { computeRouteSeedIndex } from "../artifact/routeSeedIndex";
6
+ import type { App } from "../commandDecorators";
7
+ import { bundleDefine } from "./bundleDefine";
8
+ import type { ClientEntryDiscovery } from "./clientBuildTypes";
9
+ import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
10
+ import { CsrDevPaths } from "./csrDevPaths";
11
+ import type { CsrDevContext } from "./csrDevTypes";
12
+ import { DevRegistryBundler } from "./devRegistryBundler";
13
+ import { VENDOR_SPECIFIERS } from "./vendorSpecifiers";
14
+
15
+ //* Dev-only client code of SSR pages as a module registry, under `.akan/artifact/ssr-dev`. Its roots are a bootstrap
16
+ //* (the app's client runtime) and every `"use client"` entry; React and the akanjs vendor facets stay the import map's,
17
+ //* so the page keeps one React and one store registry.
18
+ export class SsrDevBundler extends DevRegistryBundler {
19
+ static readonly #formatVersion = 2;
20
+ static readonly bootstrapKey = "";
21
+ readonly reloadsOnEntryChange = false;
22
+ #discovery: ClientEntryDiscovery | null = null;
23
+ readonly #sharedDiscovery: (() => ClientEntryDiscovery) | null;
24
+
25
+ /** `discovery` is the resident builder's, which it keeps current with every save; a worker discovers afresh. */
26
+ constructor(app: App, { discovery }: { discovery?: () => ClientEntryDiscovery } = {}) {
27
+ super(app, { dirName: SSR_DEV_DIRNAME, routePrefix: SSR_DEV_ROUTE_PREFIX, library: true });
28
+ this.#sharedDiscovery = discovery ?? null;
29
+ }
30
+
31
+ async context(): Promise<CsrDevContext> {
32
+ const akanConfig = await this.app.getConfig();
33
+ const define = bundleDefine(this.app, "start", "ssr");
34
+ const optimizeImports = [...akanConfig.optimizeImports];
35
+ const configKey = Bun.hash(
36
+ JSON.stringify(["ssr", SsrDevBundler.#formatVersion, Bun.version, define, optimizeImports, VENDOR_SPECIFIERS]),
37
+ ).toString(36);
38
+ return {
39
+ pageEntries: [],
40
+ basePaths: [...akanConfig.basePaths],
41
+ htmlBasePaths: [],
42
+ define,
43
+ optimizeImports,
44
+ configKey,
45
+ refreshFile: null,
46
+ externals: VENDOR_SPECIFIERS,
47
+ };
48
+ }
49
+
50
+ //? The client runtime of an SSR page inlines no dictionary (`useClient.ts` reads one only when rendering csr), so
51
+ //? a dictionary save is none of this registry's business.
52
+ override isMetadataFile(file: string): boolean {
53
+ return super.isMetadataFile(file) && !file.endsWith(".dictionary.ts") && path.basename(file) !== "dict.ts";
54
+ }
55
+
56
+ // Run first in the tab, before any client module the payload names: the store registry must exist by then.
57
+ async writeEntries(): Promise<{ files: Record<string, string>; changed: string[] }> {
58
+ await mkdir(this.entryDir, { recursive: true });
59
+ const file = path.join(this.entryDir, "bootstrap.ts");
60
+ const hasStore = await Bun.file(path.join(this.app.cwdPath, "lib", "st.ts")).exists();
61
+ const source = hasStore ? `import ${JSON.stringify(`@apps/${this.app.name}/client`)};\n` : "export {};\n";
62
+ const unchanged = (await Bun.file(file).exists()) && (await Bun.file(file).text()) === source;
63
+ if (!unchanged) await Bun.write(file, source);
64
+ const entryFile = CsrDevPaths.realpath(file);
65
+ return { files: { [SsrDevBundler.bootstrapKey]: entryFile }, changed: unchanged ? [] : [entryFile] };
66
+ }
67
+
68
+ /** Every `"use client"` entry the app's routes reach, the ones a route build names in its manifest. */
69
+ async clientEntries(): Promise<string[]> {
70
+ const pageEntries = await resolveSsrPageEntriesForApp(this.app, await this.app.getPageKeys());
71
+ const seedIndex = computeRouteSeedIndex(pageEntries);
72
+ const seeds = [...new Set([...seedIndex.globalLayoutFiles, ...seedIndex.entries.flatMap((entry) => entry.seeds)])];
73
+ const discovery = this.#sharedDiscovery?.() ?? (await this.#ownDiscovery());
74
+ return (await discovery.discover(seeds)).map((file) => CsrDevPaths.realpath(file));
75
+ }
76
+
77
+ async #ownDiscovery(): Promise<ClientEntryDiscovery> {
78
+ this.#discovery ??= await GraphClientEntryDiscovery.create(this.app);
79
+ return this.#discovery;
80
+ }
81
+
82
+ protected async rootFiles(entryFiles: string[]): Promise<string[]> {
83
+ return [...entryFiles, ...(await this.clientEntries())];
84
+ }
85
+ }
@@ -1,11 +1,4 @@
1
- /**
2
- * Reports the style contract that `akan lint` enforces, in the shape build/dev/lint share.
3
- *
4
- * Only contrast lives here. The vocabulary closure — raw palette classes, arbitrary colors, dropped
5
- * daisyUI slots, inline color literals, interpolated arbitrary values — is enforced by the grit plugins
6
- * in `lint/*.grit` during the biome run, so it is not re-scanned. Contrast cannot be a lint rule at all:
7
- * it is arithmetic over resolved token *values*, which no syntactic pattern can reach.
8
- */
1
+ // Only contrast: `lint/*.grit` enforces the vocabulary closure, and contrast is arithmetic no syntactic rule can do.
9
2
  import type { ThemeContrastViolation } from "./themeValidator";
10
3
 
11
4
  export interface StyleContractViolations {
@@ -1,18 +1,4 @@
1
- /**
2
- * WCAG contrast checking over the semantic token pairs. No dependencies, pure functions, and no akanjs
3
- * runtime import — it has to run inside the lint path.
4
- *
5
- * The `--x` / `--x-foreground` pairing is what makes the check possible at all: it says which two values
6
- * are going to end up on top of each other. That is the guard against a generated palette shipping a site
7
- * nobody can read.
8
- *
9
- * Thresholds (WCAG 2.1):
10
- * - body and primary surfaces (background, primary, secondary, accent, neutral, card, popover): 4.5:1,
11
- * the AA floor for normal text
12
- * - status and secondary pairs (info, success, warning, destructive, open, muted): 3:1, the AA floor for
13
- * UI components and large text
14
- * The shipped light/dark palette in styles.css clears all of them.
15
- */
1
+ // No akanjs runtime import: this runs inside the lint path.
16
2
 
17
3
  export interface ThemeContrastViolation {
18
4
  scope: string;
@@ -30,6 +16,7 @@ interface PairDef {
30
16
  threshold: number;
31
17
  }
32
18
 
19
+ // WCAG 2.1 AA: 4.5:1 is the floor for normal text, 3:1 for UI components and large text.
33
20
  const PAIRS: PairDef[] = [
34
21
  { base: "background", fg: "foreground", threshold: 4.5 },
35
22
  { base: "primary", fg: "primary-foreground", threshold: 4.5 },
@@ -46,13 +33,11 @@ const PAIRS: PairDef[] = [
46
33
  { base: "open", fg: "open-foreground", threshold: 3 },
47
34
  ];
48
35
 
49
- // Only these scopes are paired up; tokens scoped to something else (`.campaign-x`, …) are left alone.
50
36
  const THEME_SCOPES = new Set([":root", '[data-theme="dark"]', '[data-theme="light"]']);
51
37
 
52
38
  export type ThemeTokensByScope = Record<string, Record<string, string>>;
53
39
 
54
40
  export class ThemeValidator {
55
- /** Extracts tokens from the css text and checks every scope it recognizes. */
56
41
  validate(css: string): ThemeContrastViolation[] {
57
42
  const tokensByScope = ThemeValidator.parseThemeTokens(css);
58
43
  const violations: ThemeContrastViolation[] = [];
@@ -71,7 +56,7 @@ export class ThemeValidator {
71
56
  if (!bg || !front) continue;
72
57
  const bgRgb = ThemeValidator.parseHex(bg);
73
58
  const fgRgb = ThemeValidator.parseHex(front);
74
- if (!bgRgb || !fgRgb) continue; // a var() or non-hex value has no ratio to compute
59
+ if (!bgRgb || !fgRgb) continue;
75
60
  const ratio = ThemeValidator.contrastRatio(bgRgb, fgRgb);
76
61
  if (ratio >= threshold) continue;
77
62
  violations.push({
@@ -87,15 +72,9 @@ export class ThemeValidator {
87
72
  return violations;
88
73
  }
89
74
 
90
- /**
91
- * Reads `--token: value` out of `:root` / `[data-theme="…"]` blocks. A grouped selector
92
- * (`:root, [data-theme="dark"] { … }`) distributes the same tokens to each selector, and a scope that
93
- * appears twice keeps the later value — so passing framework css first and the app's second reflects the
94
- * app's overrides.
95
- */
75
+ /** A scope declared twice keeps the later value, so pass the framework css before the app's. */
96
76
  static parseThemeTokens(css: string): ThemeTokensByScope {
97
77
  const result: ThemeTokensByScope = {};
98
- // Flat rule blocks only; an at-rule (@theme, @keyframes) carries `@` in the selector and is skipped.
99
78
  const blockRe = /(?:^|})\s*([^{}@]+?)\s*\{([^{}]*)\}/g;
100
79
  for (const block of css.matchAll(blockRe)) {
101
80
  const selectors = block[1].split(",").map((s) => s.trim());
@@ -114,7 +93,6 @@ export class ThemeValidator {
114
93
  }
115
94
 
116
95
  static #normalizeScope(selector: string): string {
117
- // Quote normalization: [data-theme=dark] / [data-theme='dark'] -> [data-theme="dark"]
118
96
  return selector.replace(/\[data-theme=['"]?([\w-]+)['"]?\]/g, '[data-theme="$1"]').trim();
119
97
  }
120
98
 
@@ -1,15 +1,14 @@
1
- import { afterEach, 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, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import type { App } from "../commandDecorators";
5
+ import { tempDirs } from "../testHelpers";
6
6
  import { WatchRootResolver } from "./watchRootResolver";
7
7
 
8
- const roots: string[] = [];
8
+ const tempRoot = tempDirs("akan-watch-roots-");
9
9
 
10
10
  const makeWorkspace = async (dirs: string[]) => {
11
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-watch-roots-"));
12
- roots.push(root);
11
+ const root = await tempRoot();
13
12
  await Promise.all(dirs.map((dir) => mkdir(path.join(root, dir), { recursive: true })));
14
13
  return root;
15
14
  };
@@ -25,24 +24,18 @@ interface AppStub {
25
24
  libDeps?: string[] | null;
26
25
  }
27
26
 
28
- const makeApp = (workspaceRoot: string, name: string, { paths = {}, libDeps = null }: AppStub = {}) =>
29
- ({
30
- cwdPath: path.join(workspaceRoot, "apps", name),
27
+ const resolveRoots = (workspaceRoot: string, { paths = {}, libDeps = null }: AppStub = {}) =>
28
+ new WatchRootResolver({
29
+ cwdPath: path.join(workspaceRoot, "apps", "app1"),
31
30
  workspace: { workspaceRoot },
32
31
  getTsConfig: async () => ({ compilerOptions: { paths } }),
33
32
  getScanInfo: () => (libDeps ? { type: "app", libDeps } : null),
34
- }) as unknown as App;
35
-
36
- afterEach(async () => {
37
- await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
38
- });
33
+ } as unknown as App).resolve();
39
34
 
40
35
  describe("WatchRootResolver", () => {
41
36
  test("narrows the apps container to the app being served", async () => {
42
37
  const workspaceRoot = await makeWorkspace(["apps/app1/page", "apps/app2/page", "libs/util"]);
43
- const resolved = await new WatchRootResolver(
44
- makeApp(workspaceRoot, "app1", { paths: { "@apps/*": ["./apps/*"], "@libs/*": ["./libs/*"] } }),
45
- ).resolve();
38
+ const resolved = await resolveRoots(workspaceRoot, { paths: { "@apps/*": ["./apps/*"], "@libs/*": ["./libs/*"] } });
46
39
 
47
40
  expect(resolved).toContain(path.join(workspaceRoot, "apps/app1"));
48
41
  expect(resolved).not.toContain(path.join(workspaceRoot, "apps"));
@@ -51,9 +44,7 @@ describe("WatchRootResolver", () => {
51
44
 
52
45
  test("narrows the libs container to the app's own dependencies", async () => {
53
46
  const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util", "libs/shared"]);
54
- const resolved = await new WatchRootResolver(
55
- makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] }, libDeps: ["util"] }),
56
- ).resolve();
47
+ const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] }, libDeps: ["util"] });
57
48
 
58
49
  expect(resolved).toContain(path.join(workspaceRoot, "libs/util"));
59
50
  expect(resolved).not.toContain(path.join(workspaceRoot, "libs"));
@@ -63,9 +54,7 @@ describe("WatchRootResolver", () => {
63
54
  test("drops the libs container entirely when the app depends on no lib", async () => {
64
55
  const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util"]);
65
56
  await writeManifest(workspaceRoot, "apps/app1", []);
66
- const resolved = await new WatchRootResolver(
67
- makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] } }),
68
- ).resolve();
57
+ const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] } });
69
58
 
70
59
  expect(resolved).toEqual([path.join(workspaceRoot, "apps/app1/page")]);
71
60
  });
@@ -76,9 +65,7 @@ describe("WatchRootResolver", () => {
76
65
  await writeManifest(workspaceRoot, "libs/shared", ["util"]);
77
66
  await writeManifest(workspaceRoot, "libs/util", []);
78
67
  await writeManifest(workspaceRoot, "libs/unused", []);
79
- const resolved = await new WatchRootResolver(
80
- makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] } }),
81
- ).resolve();
68
+ const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] } });
82
69
 
83
70
  expect(resolved).toContain(path.join(workspaceRoot, "libs/shared"));
84
71
  expect(resolved).toContain(path.join(workspaceRoot, "libs/util"));
@@ -87,9 +74,7 @@ describe("WatchRootResolver", () => {
87
74
 
88
75
  test("keeps the libs container whole when the app manifest is missing", async () => {
89
76
  const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util", "libs/shared"]);
90
- const resolved = await new WatchRootResolver(
91
- makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] } }),
92
- ).resolve();
77
+ const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] } });
93
78
 
94
79
  expect(resolved).toContain(path.join(workspaceRoot, "libs"));
95
80
  });
@@ -97,24 +82,20 @@ describe("WatchRootResolver", () => {
97
82
  test("keeps the libs container whole when a dependency lib is unsynced", async () => {
98
83
  const workspaceRoot = await makeWorkspace(["apps/app1/page", "libs/util", "libs/shared"]);
99
84
  await writeManifest(workspaceRoot, "apps/app1", ["shared"]);
100
- const resolved = await new WatchRootResolver(
101
- makeApp(workspaceRoot, "app1", { paths: { "@libs/*": ["./libs/*"] } }),
102
- ).resolve();
85
+ const resolved = await resolveRoots(workspaceRoot, { paths: { "@libs/*": ["./libs/*"] } });
103
86
 
104
87
  expect(resolved).toContain(path.join(workspaceRoot, "libs"));
105
88
  });
106
89
 
107
90
  test("resolves a package alias to its own directory, filename and glob stripped", async () => {
108
91
  const workspaceRoot = await makeWorkspace(["apps/app1/page", "pkgs/akanjs/base"]);
109
- const resolved = await new WatchRootResolver(
110
- makeApp(workspaceRoot, "app1", {
111
- paths: {
112
- akanjs: ["./pkgs/akanjs/index.ts"],
113
- "akanjs/*": ["./pkgs/akanjs/*"],
114
- missing: ["./pkgs/nothing/index.ts"],
115
- },
116
- }),
117
- ).resolve();
92
+ const resolved = await resolveRoots(workspaceRoot, {
93
+ paths: {
94
+ akanjs: ["./pkgs/akanjs/index.ts"],
95
+ "akanjs/*": ["./pkgs/akanjs/*"],
96
+ missing: ["./pkgs/nothing/index.ts"],
97
+ },
98
+ });
118
99
 
119
100
  expect(resolved).toContain(path.join(workspaceRoot, "pkgs/akanjs"));
120
101
  expect(resolved).not.toContain(path.join(workspaceRoot, "pkgs/nothing"));
@@ -122,7 +103,7 @@ describe("WatchRootResolver", () => {
122
103
 
123
104
  test("always watches the app's page tree", async () => {
124
105
  const workspaceRoot = await makeWorkspace(["apps/app1/page"]);
125
- const resolved = await new WatchRootResolver(makeApp(workspaceRoot, "app1")).resolve();
106
+ const resolved = await resolveRoots(workspaceRoot);
126
107
 
127
108
  expect(resolved).toEqual([path.join(workspaceRoot, "apps/app1/page")]);
128
109
  });
@@ -3,23 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import type { App } from "../commandDecorators";
5
5
 
6
- /**
7
- * The directories one app's dev watcher follows, resolved from its tsconfig `paths`.
8
- *
9
- * Two aliases strip down to a workspace container rather than a package, and taking either verbatim puts
10
- * code the app never imports under the watcher — where a save rebuilds, restarts and reloads it for
11
- * nothing, and both builders rewrite the same generated barrels:
12
- *
13
- * - `@apps/*` becomes the `apps/` container. Apps are leaves of the workspace graph — never one another's
14
- * dependencies — so it is replaced by this app's own directory.
15
- * - `@libs/*` becomes the `libs/` container. A lib is a dependency only if the app reaches it, so it is
16
- * replaced by the app's own lib dependencies, transitively. The set is resolved once, at watcher
17
- * install: a lib that becomes a dependency mid-session needs a fresh `akan start` anyway, because the
18
- * import that made it one also needs a `sync`.
19
- *
20
- * Failing to resolve that set keeps the container whole, because a wrong narrowing is a dev server that
21
- * silently ignores edits.
22
- */
6
+ /** Watch roots from tsconfig paths: `@apps/*` narrows to this app, `@libs/*` to its lib deps (whole when unknown). */
23
7
  export class WatchRootResolver {
24
8
  #app: App;
25
9
 
@@ -32,14 +16,15 @@ export class WatchRootResolver {
32
16
  const appDir = path.resolve(this.#app.cwdPath);
33
17
  const appsContainer = path.dirname(appDir);
34
18
  const libsContainer = path.resolve(this.#app.workspace.workspaceRoot, "libs");
35
- const libRoots = await this.#resolveLibRoots(libsContainer);
19
+ const libRoots = (await this.#resolveLibDeps(libsContainer))
20
+ ?.map((name) => path.join(libsContainer, name))
21
+ .filter((dir) => fs.existsSync(dir));
36
22
  const set = new Set<string>();
37
23
  set.add(path.resolve(`${this.#app.cwdPath}/page`));
38
24
  for (const targets of Object.values(tsconfig.compilerOptions.paths ?? {})) {
39
25
  for (const target of targets) {
40
26
  if (!target) continue;
41
27
  if (path.isAbsolute(target)) continue;
42
- // Strip the trailing filename and glob so we watch the package root dir.
43
28
  const cleaned = target.replace(/\/?\*+.*$/, "").replace(/\/[^/]+\.[^/]+$/, "");
44
29
  const resolved = path.resolve(this.#app.workspace.workspaceRoot, cleaned);
45
30
  if (resolved === libsContainer && libRoots) {
@@ -53,16 +38,9 @@ export class WatchRootResolver {
53
38
  return [...set];
54
39
  }
55
40
 
56
- async #resolveLibRoots(libsContainer: string): Promise<string[] | null> {
57
- const libDeps = await this.#resolveLibDeps(libsContainer);
58
- if (!libDeps) return null;
59
- return libDeps.map((name) => path.join(libsContainer, name)).filter((dir) => fs.existsSync(dir));
60
- }
61
-
62
41
  async #resolveLibDeps(libsContainer: string): Promise<string[] | null> {
63
42
  const scanInfo = this.#app.getScanInfo({ allowEmpty: true });
64
- // Already transitive, and present whenever this runs in a process that scanned. The builder and the
65
- // idle watcher run in processes that did not, so they take the manifests `scan` wrote instead.
43
+ // Transitive, but only in a process that scanned; the builder and idle watcher read the synced manifests.
66
44
  if (scanInfo?.type === "app") return scanInfo.libDeps;
67
45
  const direct = await WatchRootResolver.#readManifestLibDeps(path.join(this.#app.cwdPath, "akan.app.json"));
68
46
  if (!direct) return null;
@@ -40,9 +40,6 @@ const scanModuleSpecifiers = (source: string, filePath: string, includeExports:
40
40
  return importSpecifiers;
41
41
  };
42
42
 
43
- /**
44
- * TypeScript 설정 파일을 읽고 파싱하는 함수
45
- */
46
43
  export const parseTsConfig = (tsConfigPath: string = "./tsconfig.json") => {
47
44
  const configFile = ts.readConfigFile(tsConfigPath, (path) => {
48
45
  return ts.sys.readFile(path);
@@ -51,85 +48,36 @@ export const parseTsConfig = (tsConfigPath: string = "./tsconfig.json") => {
51
48
  return ts.parseJsonConfigFileContent(configFile.config, ts.sys, realpathSync(tsConfigPath).replace(/[^/\\]+$/, ""));
52
49
  };
53
50
 
54
- /**
55
- * 파일 내의 임포트를 기반으로 관련 파일을 수집하는 함수
56
- */
57
- export const collectImportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) => {
51
+ const collectFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine, includeExports: boolean) => {
58
52
  const allFilesToAnalyze = new Set<string>([constantFilePath]);
59
53
  const analyzedFiles = new Set<string>();
60
- const spinner = ora("Collecting related files...");
54
+ const spinner = ora(includeExports ? "Collecting files from exports..." : "Collecting related files...");
61
55
  spinner.start();
62
-
63
- function collectImported(filePath: string) {
56
+ const collect = (filePath: string) => {
64
57
  if (analyzedFiles.has(filePath)) return;
65
58
  analyzedFiles.add(filePath);
66
-
67
59
  const source = readFileSync(filePath, "utf-8");
68
- for (const importPath of scanModuleSpecifiers(source, filePath, false)) {
60
+ for (const importPath of scanModuleSpecifiers(source, filePath, includeExports)) {
69
61
  if (!importPath.startsWith(".")) continue;
70
-
71
62
  const resolved = ts.resolveModuleName(importPath, filePath, parsedConfig.options, ts.sys).resolvedModule
72
63
  ?.resolvedFileName;
73
-
74
64
  if (resolved && !allFilesToAnalyze.has(resolved)) {
75
65
  allFilesToAnalyze.add(resolved);
76
- // 재귀적으로 임포트된 파일의 임포트도 수집
77
- collectImported(resolved);
66
+ collect(resolved);
78
67
  }
79
68
  }
80
- }
81
-
82
- // 임포트 기반 관련 파일 수집 실행
83
- collectImported(constantFilePath);
84
- spinner.succeed(`Found ${allFilesToAnalyze.size} related files.`);
85
-
86
- return {
87
- allFilesToAnalyze,
88
- analyzedFiles,
89
69
  };
70
+ collect(constantFilePath);
71
+ spinner.succeed(`Found ${allFilesToAnalyze.size} related files${includeExports ? " from exports" : ""}.`);
72
+ return { allFilesToAnalyze, analyzedFiles };
90
73
  };
91
74
 
92
- /**
93
- * export 선언을 기반으로 관련 파일을 수집하는 함수
94
- */
95
- export const collectExportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) => {
96
- const allFilesToAnalyze = new Set<string>([constantFilePath]);
97
- const analyzedFiles = new Set<string>();
98
- const spinner = ora("Collecting files from exports...");
99
- spinner.start();
100
-
101
- function collectExported(filePath: string) {
102
- if (analyzedFiles.has(filePath)) return;
103
- analyzedFiles.add(filePath);
75
+ export const collectImportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) =>
76
+ collectFiles(constantFilePath, parsedConfig, false);
104
77
 
105
- const source = readFileSync(filePath, "utf-8");
106
- for (const importPath of scanModuleSpecifiers(source, filePath, true)) {
107
- if (!importPath.startsWith(".")) continue;
78
+ export const collectExportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) =>
79
+ collectFiles(constantFilePath, parsedConfig, true);
108
80
 
109
- const resolved = ts.resolveModuleName(importPath, filePath, parsedConfig.options, ts.sys).resolvedModule
110
- ?.resolvedFileName;
111
-
112
- if (resolved && !allFilesToAnalyze.has(resolved)) {
113
- allFilesToAnalyze.add(resolved);
114
- // 재귀적으로 export/import 연결 파일도 확인
115
- collectExported(resolved);
116
- }
117
- }
118
- }
119
-
120
- // export 기반 관련 파일 수집 실행
121
- collectExported(constantFilePath);
122
- spinner.succeed(`Found ${allFilesToAnalyze.size} related files from exports.`);
123
-
124
- return {
125
- allFilesToAnalyze,
126
- analyzedFiles,
127
- };
128
- };
129
-
130
- /**
131
- * TypeScript 프로그램 생성 함수
132
- */
133
81
  export const createTsProgram = (filePaths: Set<string>, options: ts.CompilerOptions) => {
134
82
  const spinner = ora("Creating TypeScript program for all files...");
135
83
  spinner.start();
@@ -145,9 +93,6 @@ export const createTsProgram = (filePaths: Set<string>, options: ts.CompilerOpti
145
93
  };
146
94
  };
147
95
 
148
- /**
149
- * 심볼 캐싱 및 조회 함수 생성기
150
- */
151
96
  export const createSymbolCache = (checker: ts.TypeChecker) => {
152
97
  const symbolCache = new Map<string, ts.Symbol | undefined>();
153
98
 
@@ -162,9 +107,6 @@ export const createSymbolCache = (checker: ts.TypeChecker) => {
162
107
  };
163
108
  };
164
109
 
165
- /**
166
- * 파일 속성 분석 함수
167
- */
168
110
  export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Program, checker: ts.TypeChecker) => {
169
111
  const propertyMap = new Map<
170
112
  string,
@@ -185,7 +127,6 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
185
127
  const spinner = ora("Analyzing property relationships...");
186
128
  spinner.start();
187
129
 
188
- // 파일 내 속성 분석 함수
189
130
  function analyzeFileProperties(filePath: string) {
190
131
  if (analyzedFiles.has(filePath)) return;
191
132
  analyzedFiles.add(filePath);
@@ -193,23 +134,19 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
193
134
  const source = program.getSourceFile(filePath);
194
135
  if (!source) return;
195
136
 
196
- // 소스 라인 캐싱 (한 번만 분할)
197
137
  if (!sourceLineCache.has(filePath)) {
198
138
  sourceLineCache.set(filePath, source.getFullText().split("\n"));
199
139
  }
200
140
  const sourceLines = sourceLineCache.get(filePath);
201
141
 
202
- // 속성 분석 함수
203
142
  function visit(node: ts.Node) {
204
143
  if (!source) return;
205
144
 
206
- // 프로퍼티 접근 표현식 처리
207
145
  if (ts.isPropertyAccessExpression(node)) {
208
146
  const left = node.expression;
209
147
  const right = node.name;
210
148
  const { line } = ts.getLineAndCharacterOfPosition(source, node.getStart());
211
149
 
212
- // 패턴 검색 로직 - 기존 문자열 기반 검색 유지
213
150
  if (
214
151
  ts.isIdentifier(left) &&
215
152
  sourceLines &&
@@ -219,14 +156,12 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
219
156
  (sourceLines[line]?.includes(`@Field.Prop(() => ${left.text}.${right.text}`) ||
220
157
  sourceLines[line].includes(`base.Filter(${left.text}.${right.text},`))
221
158
  ) {
222
- // 캐싱된 심볼 사용
223
159
  const symbol = getCachedSymbol(right);
224
160
 
225
161
  if (symbol?.declarations && symbol.declarations.length > 0) {
226
162
  const key = symbol.declarations[0]?.getSourceFile().fileName.split("/").pop()?.split(".")[0] ?? "";
227
163
  const property = propertyMap.get(key);
228
164
  const isScalar = symbol.declarations[0]?.getSourceFile().fileName.includes("_") ?? false;
229
- // 현재 작업 중인 디렉토리 이전은 제거
230
165
  const symbolFilePath = symbol.declarations[0]
231
166
  ?.getSourceFile()
232
167
  .fileName.replace(`${ts.sys.getCurrentDirectory()}/`, "");
@@ -254,7 +189,6 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
254
189
  }
255
190
  }
256
191
  } else if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
257
- // 모듈 선언 표현식 처리
258
192
  const importPath = node.moduleSpecifier.text;
259
193
 
260
194
  if (importPath.startsWith(".")) {
@@ -276,15 +210,12 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
276
210
  }
277
211
  }
278
212
 
279
- // 모든 자식 노드 처리
280
213
  ts.forEachChild(node, visit);
281
214
  }
282
215
 
283
- // 파일 분석 시작
284
216
  visit(source);
285
217
  }
286
218
 
287
- // 모든 파일의 속성 관계 분석
288
219
  for (const filePath of filesToAnalyze) {
289
220
  analyzeFileProperties(filePath);
290
221
  }
@@ -294,21 +225,10 @@ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Progr
294
225
  return propertyMap;
295
226
  };
296
227
 
297
- /**
298
- * 메인 함수: 상수 파일 관련 요소 분석
299
- */
300
228
  export const getRelatedCnsts = (constantFilePath: string) => {
301
- // 1. TypeScript 설정 파일 읽기 및 파싱
302
229
  const parsedConfig = parseTsConfig();
303
-
304
- // 2. 상수 파일 관련 파일 수집 (임포트 기반)
305
230
  const { allFilesToAnalyze } = collectImportedFiles(constantFilePath, parsedConfig);
306
- // 3. TypeScript 프로그램 생성 및 타입 체커 설정
307
231
  const { program, checker } = createTsProgram(allFilesToAnalyze, parsedConfig.options);
308
-
309
- // 4. 파일 속성 분석
310
232
  const propertyMap = analyzeProperties(allFilesToAnalyze, program, checker);
311
-
312
- // 5. 결과 반환
313
233
  return Array.from(propertyMap.entries()).map(([key, value]) => ({ key, ...value }));
314
234
  };