@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,17 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { DevResourceProbe, type DevResourceProc } from "./devResourceProbe";
4
5
 
5
6
  export type SsrProcRole = "gateway" | "replica" | "rsc" | "other";
6
7
 
7
- export interface SsrProc {
8
- pid: number;
9
- ppid: number;
10
- rssMb: number;
11
- cpuSec: number;
12
- role: SsrProcRole;
13
- command: string;
14
- }
8
+ export type SsrProc = DevResourceProc<SsrProcRole>;
15
9
 
16
10
  export interface SsrCgroupSample {
17
11
  currentMb: number;
@@ -35,12 +29,7 @@ export interface SsrCacheSample {
35
29
  heap: SsrHeapSample;
36
30
  }
37
31
 
38
- /**
39
- * RSS minus JS heap is what a module graph costs beyond its objects — compiled code, module
40
- * records, native allocator retention — so the split is what separates "the app retains objects"
41
- * from "the runtime retains code". `jscExtra` is JSC's off-heap attribution, which is where
42
- * typed-array backing stores (cached Flight chunks) land.
43
- */
32
+ // `jscExtra` is JSC's off-heap attribution, where typed-array backing stores (cached Flight chunks) land.
44
33
  export interface SsrHeapSample {
45
34
  replicaHeapUsedMb: number;
46
35
  replicaJscHeapMb: number;
@@ -52,63 +41,30 @@ export interface SsrHeapSample {
52
41
 
53
42
  export interface SsrMemoryProbeOptions {
54
43
  appName: string;
55
- /** Built app directory — `dist/apps/<app>` unless the build was relocated. */
56
44
  distDir: string;
57
45
  port: number;
58
46
  basePaths: string;
59
47
  repoName: string;
60
48
  serveDomain: string;
61
49
  scenarios: number[];
62
- /** Repeat count for scenario 1 (same route) and per-route passes in scenario 6. */
63
50
  repeats: number;
64
51
  concurrencies: number[];
65
52
  idleSeconds: number;
66
- /** Report cadence forced on the children; also bounds how long a sample waits for a fresh one. */
67
53
  metricsIntervalMs: number;
68
- /** Disables both result caches at boot — the second half of scenario 3. */
69
54
  cacheOff: boolean;
70
- /**
71
- * Forces `Bun.gc(true)` before every metrics sample. Required for the s7 ratchet test: without
72
- * it, growth across passes cannot be told apart from garbage that has not been collected yet.
73
- */
74
55
  gcOnReport: boolean;
75
- /** s7: how many times to walk the whole route list. */
76
56
  passes: number;
77
- /** s8: cumulative distinct-route counts to sample the per-route slope at. */
78
57
  sweep: number[];
79
58
  logPath: string;
80
59
  }
81
60
 
82
- /**
83
- * Production process-tree RSS and cache occupancy — probe §0.3 of
84
- * `local/reduce-ssr-ram/01-measurement-harness.md`, and the source of the phase 0 results table.
85
- *
86
- * bun pkgs/@akanjs/devkit/integration/ssrMemoryProbe.ts <app> [--scenarios=1,2,5] [--cache=off]
87
- *
88
- * The production tree is gateway → replica × N → rsc worker × N, with no builder; `DevResourceProbe`
89
- * measures the dev tree instead and the two are not interchangeable.
90
- *
91
- * Two things this exists to get right:
92
- *
93
- * - **Replica and worker RSS are reported separately.** They are separate processes and the worker
94
- * holds a private copy of the pages bundle, so one summed number hides which of the two grows.
95
- * - **On Linux it samples the cgroup's `anon` / `file` split, not just RSS.** Whether a cache is
96
- * anonymous heap (an OOM contributor) or page cache (evictable) is the entire question phase 2
97
- * asks; RSS cannot answer it, and neither can macOS, where the file half does not exist in the
98
- * same form. Read a macOS run as directional only.
99
- */
61
+ // Usage: bun pkgs/@akanjs/devkit/integration/ssrMemoryProbe.ts <app> [--scenarios=1,2,5] [--cache=off]
62
+ // The cgroup `anon`/`file` split exists only on Linux; read a macOS run as directional.
100
63
  export class SsrMemoryProbe {
101
64
  static readonly #columns: SsrProcRole[] = ["gateway", "replica", "rsc"];
102
65
 
103
66
  static parseArgs(argv: string[]): SsrMemoryProbeOptions {
104
- const args = new Map(
105
- argv.slice(1).map((arg) => {
106
- const [key, value] = arg.replace(/^--/, "").split("=");
107
- return [key ?? "", value ?? "1"];
108
- }),
109
- );
110
- const appName = argv[0] ?? "akan";
111
- const num = (key: string, fallback: number) => Number(args.get(key) ?? fallback);
67
+ const { appName, args, num } = DevResourceProbe.parseArgMap(argv);
112
68
  const list = (key: string, fallback: number[]) =>
113
69
  args.has(key)
114
70
  ? (args.get(key) ?? "")
@@ -155,10 +111,7 @@ export class SsrMemoryProbe {
155
111
  NODE_ENV: "production",
156
112
  USE_AKANJS_PKGS: "true",
157
113
  AKAN_PUBLIC_APP_NAME: appName,
158
- // `getEnv()` requires these one at a time, and a missing one fails the *replica* while the
159
- // gateway stays up: the process tree still looks like a healthy three, and only the child's
160
- // `ready` flag and the 503s give it away. Both are why `#waitForReady` checks child
161
- // readiness rather than the gateway's own 200.
114
+ // A missing one fails the replica while the gateway stays up and keeps answering.
162
115
  AKAN_PUBLIC_REPO_NAME: process.env.AKAN_PUBLIC_REPO_NAME ?? repoName,
163
116
  AKAN_PUBLIC_SERVE_DOMAIN: process.env.AKAN_PUBLIC_SERVE_DOMAIN ?? serveDomain,
164
117
  AKAN_PUBLIC_ENV: "local",
@@ -181,7 +134,7 @@ export class SsrMemoryProbe {
181
134
  try {
182
135
  await this.#measure();
183
136
  } finally {
184
- await this.#cleanup(gateway);
137
+ await DevResourceProbe.killTree(gateway, () => this.#sampleTree());
185
138
  }
186
139
  }
187
140
 
@@ -221,11 +174,7 @@ export class SsrMemoryProbe {
221
174
  for (const concurrency of concurrencies) await this.#run(`s6 concurrency=${concurrency}`, routes, concurrency);
222
175
  }
223
176
 
224
- // s7 — ratchet vs working set. Every pass renders the same routes, so pass 1 pays for module
225
- // evaluation and later passes pay for nothing new. Flat later passes mean the memory is a
226
- // working set and only a smaller bundle or fewer replicas reduces it; continued growth means
227
- // per-render retention, which is a bug worth more than any ceiling. Run with `--cache=off --gc`
228
- // or it measures cache hits and uncollected garbage instead.
177
+ // s7: flat passes after the first mean a working set; continued growth means per-render retention.
229
178
  if (scenarios.includes(7)) {
230
179
  if (!this.#options.cacheOff)
231
180
  console.info("[probe] WARNING s7 without --cache=off measures cache hits from pass 2 on");
@@ -235,8 +184,7 @@ export class SsrMemoryProbe {
235
184
  await this.#run(`s7 pass ${pass}/${this.#options.passes}`, routes, 1);
236
185
  }
237
186
 
238
- // s8 — per-route slope vs fixed intercept. Cumulative: each step adds routes the process has
239
- // not seen, so the RSS series against distinct-route count separates the two.
187
+ // s8: cumulative, so RSS against distinct-route count separates the per-route slope from the intercept.
240
188
  if (scenarios.includes(8)) {
241
189
  for (const count of this.#options.sweep) {
242
190
  const subset = routes.slice(0, Math.min(count, routes.length));
@@ -255,11 +203,7 @@ export class SsrMemoryProbe {
255
203
  }
256
204
  }
257
205
 
258
- /**
259
- * A scenario is only a measurement if its requests succeeded. A gateway whose replica died still
260
- * answers — with 503s, instantly — and the resulting RSS row looks like a legitimately cheap one.
261
- * Fail loudly rather than publish that number.
262
- */
206
+ // A dead replica still answers instant 503s, which would read as a legitimately cheap row.
263
207
  async #run(label: string, urls: string[], concurrency: number): Promise<void> {
264
208
  const started = Date.now();
265
209
  const { ok, failed, statuses } = await this.#browse(urls, concurrency);
@@ -307,11 +251,7 @@ export class SsrMemoryProbe {
307
251
  return `${(bytes / 1024 / 1024).toFixed(1)}MiB`;
308
252
  }
309
253
 
310
- /**
311
- * Reads the container's own accounting. `memory.current` counts page cache, so a disk-backed
312
- * cache still shows up here — but under `file`, which the kernel reclaims under pressure instead
313
- * of OOM-killing. That split is what phase 2 has to move. Returns null off cgroup v2.
314
- */
254
+ // cgroup v2 only. `file` is page cache the kernel reclaims under pressure; `anon` is what gets OOM-killed.
315
255
  static readCgroupSample(): SsrCgroupSample | null {
316
256
  try {
317
257
  const current = Number.parseInt(fs.readFileSync("/sys/fs/cgroup/memory.current", "utf8").trim(), 10);
@@ -328,22 +268,14 @@ export class SsrMemoryProbe {
328
268
  }
329
269
  }
330
270
 
331
- /**
332
- * Waits for a metrics report sampled *after* `since` before reading the cache columns.
333
- *
334
- * The counters ride a periodic IPC report, so reading immediately after a browse returns the
335
- * state from before it — and the two-hop path (worker → replica → gateway) means the `rsc*` and
336
- * `rscWorker*` fields can be a further interval behind. Without this wait every row silently
337
- * attributes one scenario's cache growth to the previous scenario.
338
- */
271
+ // Counters ride a periodic two-hop IPC report (worker → replica → gateway), so wait until both hops have
272
+ // re-reported after `since`, or one scenario's growth lands in the next row.
339
273
  async #sampleCaches(since: number): Promise<SsrCacheSample | null> {
340
274
  const deadline = Date.now() + this.#options.metricsIntervalMs * 3 + 5_000;
341
275
  let children: Array<Record<string, number>> = [];
342
276
  let fresh = false;
343
277
  while (Date.now() < deadline) {
344
278
  children = await this.#fetchChildMetrics();
345
- // Both hops must have re-reported: `reportedAt` is the replica's own sample time, and
346
- // `rscWorkerReportedAt` the worker's as of the replica's last read of it.
347
279
  fresh =
348
280
  children.length > 0 &&
349
281
  children.every(
@@ -354,32 +286,30 @@ export class SsrMemoryProbe {
354
286
  }
355
287
  if (children.length === 0) return null;
356
288
  if (!fresh) console.info(`${"".padEnd(24)} WARNING cache columns are stale — no fresh report within the wait`);
357
- {
358
- const sum = (key: string) => children.reduce((total, metrics) => total + Number(metrics[key] ?? 0), 0);
359
- const mb = (key: string) => sum(key) / 1024 / 1024;
360
- return {
361
- replicaRssMb: sum("rssBytes") / 1024 / 1024,
362
- rscWorkerRssMb: sum("rscWorkerRssBytes") / 1024 / 1024,
363
- htmlEntries: sum("httpHtmlCacheEntries"),
364
- htmlBytes: sum("httpHtmlCacheBytes"),
365
- rscEntries: sum("rscResultCacheEntries"),
366
- rscBytes: sum("rscResultCacheBytes"),
367
- patchEntries: sum("rscPatchResultCacheEntries"),
368
- patchBytes: sum("rscPatchResultCacheBytes"),
369
- ssrChunkKeys: sum("ssrChunkRegistrySize"),
370
- loadedRouteModules: sum("rscLoadedRouteModuleCount"),
371
- fullSsr: sum("httpFullSsrCount"),
372
- rscNavigation: sum("httpRscNavigationCount"),
373
- heap: {
374
- replicaHeapUsedMb: mb("heapUsedBytes"),
375
- replicaJscHeapMb: mb("jscHeapSizeBytes"),
376
- replicaJscExtraMb: mb("jscExtraMemorySizeBytes"),
377
- workerHeapUsedMb: mb("rscWorkerHeapUsedBytes"),
378
- workerJscHeapMb: mb("rscWorkerJscHeapSizeBytes"),
379
- workerJscExtraMb: mb("rscWorkerJscExtraMemorySizeBytes"),
380
- },
381
- };
382
- }
289
+ const sum = (key: string) => children.reduce((total, metrics) => total + Number(metrics[key] ?? 0), 0);
290
+ const mb = (key: string) => sum(key) / 1024 / 1024;
291
+ return {
292
+ replicaRssMb: mb("rssBytes"),
293
+ rscWorkerRssMb: mb("rscWorkerRssBytes"),
294
+ htmlEntries: sum("httpHtmlCacheEntries"),
295
+ htmlBytes: sum("httpHtmlCacheBytes"),
296
+ rscEntries: sum("rscResultCacheEntries"),
297
+ rscBytes: sum("rscResultCacheBytes"),
298
+ patchEntries: sum("rscPatchResultCacheEntries"),
299
+ patchBytes: sum("rscPatchResultCacheBytes"),
300
+ ssrChunkKeys: sum("ssrChunkRegistrySize"),
301
+ loadedRouteModules: sum("rscLoadedRouteModuleCount"),
302
+ fullSsr: sum("httpFullSsrCount"),
303
+ rscNavigation: sum("httpRscNavigationCount"),
304
+ heap: {
305
+ replicaHeapUsedMb: mb("heapUsedBytes"),
306
+ replicaJscHeapMb: mb("jscHeapSizeBytes"),
307
+ replicaJscExtraMb: mb("jscExtraMemorySizeBytes"),
308
+ workerHeapUsedMb: mb("rscWorkerHeapUsedBytes"),
309
+ workerJscHeapMb: mb("rscWorkerJscHeapSizeBytes"),
310
+ workerJscExtraMb: mb("rscWorkerJscExtraMemorySizeBytes"),
311
+ },
312
+ };
383
313
  }
384
314
 
385
315
  async #fetchChildMetrics(): Promise<Array<Record<string, number>>> {
@@ -402,49 +332,10 @@ export class SsrMemoryProbe {
402
332
  return "other";
403
333
  }
404
334
 
405
- /** One `ps`, then walk from the gateway pid to a fixpoint. */
406
- async #sampleTree(): Promise<SsrProc[]> {
407
- const proc = Bun.spawn(["ps", "-eo", "pid=,ppid=,rss=,time=,command="], { stdout: "pipe" });
408
- const text = await new Response(proc.stdout).text();
409
- await proc.exited;
410
- const all = text
411
- .split("\n")
412
- .map((line) => line.trim())
413
- .filter(Boolean)
414
- .map((line) => {
415
- const match = /^(\d+)\s+(\d+)\s+(\d+)\s+([\d:.]+)\s+(.*)$/.exec(line);
416
- if (!match) return null;
417
- const [, pid, ppid, rss, time, command] = match;
418
- const parts = (time ?? "0:0").split(":");
419
- const cpuSec =
420
- parts.length === 3
421
- ? Number(parts[0]) * 3600 + Number(parts[1]) * 60 + Number(parts[2])
422
- : Number(parts[0] ?? 0) * 60 + Number(parts[1] ?? 0);
423
- return {
424
- pid: Number(pid),
425
- ppid: Number(ppid),
426
- rssMb: Number(rss) / 1024,
427
- cpuSec,
428
- role: this.#roleOf(command ?? ""),
429
- command: command ?? "",
430
- } satisfies SsrProc;
431
- })
432
- .filter((proc): proc is SsrProc => proc !== null);
433
- const kept = new Map<number, SsrProc>();
434
- const root = all.find((proc) => proc.pid === this.#gatewayPid);
435
- if (root) kept.set(this.#gatewayPid, root);
436
- for (let pass = 0; pass < 12; pass++) {
437
- const before = kept.size;
438
- for (const proc of all) if (kept.has(proc.ppid) && !kept.has(proc.pid)) kept.set(proc.pid, proc);
439
- if (kept.size === before) break;
440
- }
441
- return [...kept.values()].filter((proc) => proc.role !== "other" || proc.pid === this.#gatewayPid);
335
+ #sampleTree(): Promise<SsrProc[]> {
336
+ return DevResourceProbe.sampleTree(this.#gatewayPid, (command) => this.#roleOf(command));
442
337
  }
443
338
 
444
- /**
445
- * Concrete urls from the built route seed index. `:lang` takes the default locale; any route with
446
- * another `:param` is skipped because it needs a real id to render.
447
- */
448
339
  async #staticRoutes(): Promise<string[]> {
449
340
  const artifactDir = path.join(this.#options.distDir, ".akan", "artifact");
450
341
  const seed = (await Bun.file(path.join(artifactDir, "route-seed-index.json")).json()) as {
@@ -495,11 +386,7 @@ export class SsrMemoryProbe {
495
386
  return { ok, failed, statuses };
496
387
  }
497
388
 
498
- /**
499
- * Waits for a **child** to report ready, not for the gateway to answer. The gateway binds and
500
- * serves `/health` happily while every replica is in a crash-restart loop, so a 200 here proves
501
- * nothing about whether anything can render.
502
- */
389
+ // The gateway answers `/health` while every replica crash-loops, so wait for a ready child instead.
503
390
  async #waitForReady(timeoutMs: number): Promise<boolean> {
504
391
  const deadline = Date.now() + timeoutMs;
505
392
  let lastError = "";
@@ -524,19 +411,6 @@ export class SsrMemoryProbe {
524
411
  if (lastError) console.info(`[probe] no child became ready; last child error: ${lastError}`);
525
412
  return false;
526
413
  }
527
-
528
- async #cleanup(gateway: { kill: (signal: NodeJS.Signals) => void }): Promise<void> {
529
- for (const proc of (await this.#sampleTree()).reverse()) {
530
- try {
531
- process.kill(proc.pid, "SIGKILL");
532
- } catch {}
533
- }
534
- try {
535
- gateway.kill("SIGKILL");
536
- } catch {}
537
- await Bun.sleep(1_000);
538
- console.info(`[probe] survivors after kill: ${(await this.#sampleTree()).length}`);
539
- }
540
414
  }
541
415
 
542
416
  if (import.meta.main) await new SsrMemoryProbe(SsrMemoryProbe.parseArgs(process.argv.slice(2))).run();
package/libSource.test.ts CHANGED
@@ -1,25 +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";
4
2
  import path from "node:path";
5
3
  import { Executor, LibExecutor, WorkspaceExecutor } from "./executors";
6
4
  import { formatLibStatuses, LibSource } from "./libSource";
5
+ import { tempDirs, writeText as write } from "./testHelpers";
7
6
 
8
- const tempRoots: string[] = [];
9
-
10
- afterEach(async () => {
11
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
12
- });
13
-
14
- const write = async (filePath: string, content: string) => {
15
- await mkdir(path.dirname(filePath), { recursive: true });
16
- await writeFile(filePath, content);
17
- };
7
+ const makeTempRoot = tempDirs("akan-libsource-");
18
8
 
19
9
  // `LibExecutor.from` memoises by name, so each fixture needs a name no other test has used.
20
10
  const makeLib = async (libName: string) => {
21
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-libsource-"));
22
- tempRoots.push(root);
11
+ const root = await makeTempRoot();
23
12
  await write(
24
13
  path.join(root, "package.json"),
25
14
  '{ "name": "workspace", "version": "0.0.1", "description": "workspace" }\n',
package/libSource.ts CHANGED
@@ -20,20 +20,10 @@ export interface LibStatus {
20
20
  hash: string;
21
21
  }
22
22
 
23
- /**
24
- * A library's origin, recorded in its own `package.json` under an `akan.source` key.
25
- *
26
- * The key rides `package.json` rather than a file of its own because every akan write to a library's
27
- * manifest is a spread of the existing object (`LibExecutor.syncPackageJson`), so an unknown top-level
28
- * key survives — while a new root file would have to be added to `libRootAllowedFiles` before
29
- * `akan sync` stopped rejecting it.
30
- */
23
+ // In package.json, not a file of its own: every akan manifest write spreads the existing object, so the key survives.
31
24
  export class LibSource {
32
25
  static readonly manifestKey = "akan";
33
- /**
34
- * Left out of the hash. `env/` holds per-deployment values that belong to the workspace the library
35
- * was installed into, not to the origin, so an env edit is not drift.
36
- */
26
+ // `env/` holds the installing workspace's per-deployment values, so an env edit is not drift.
37
27
  static readonly unhashedDirs = ["env"];
38
28
 
39
29
  #lib: LibExecutor;
@@ -50,33 +40,35 @@ export class LibSource {
50
40
  return !LibSource.unhashedDirs.includes(relative.split("/")[0] ?? "");
51
41
  }
52
42
 
53
- /** `package.json` cannot hash the stamp it carries, so the key comes off before hashing. */
54
- async #hashableContent(file: string) {
55
- const content = await this.#lib.workspace.readFile(file);
56
- if (file !== `${this.#prefix}package.json`) return content;
57
- const manifest = JSON.parse(content) as PackageJson;
43
+ static unstamped(manifestContent: string) {
44
+ const manifest = JSON.parse(manifestContent) as PackageJson;
58
45
  delete manifest[LibSource.manifestKey];
59
46
  return JSON.stringify(manifest);
60
47
  }
61
48
 
62
- /**
63
- * Hash over the library's own files. Untracked files count: a freshly copied library is not committed
64
- * yet, and its hash has to be the same one a later `status` recomputes.
65
- */
66
- async computeHash() {
67
- const files = (await this.#lib.workspace.listGitFiles([`libs/${this.#lib.name}`], { untracked: true })).filter(
68
- (file) => this.#isHashed(file),
69
- );
49
+ //* `package.json` cannot hash the stamp it carries, so the key comes off before hashing.
50
+ static async hashFiles(files: string[], manifestFile: string, read: (file: string) => Promise<string>) {
70
51
  const hasher = new Bun.CryptoHasher("sha256");
71
52
  for (const file of files) {
53
+ const content = await read(file);
72
54
  hasher.update(file);
73
55
  hasher.update("\0");
74
- hasher.update(await this.#hashableContent(file));
56
+ hasher.update(file === manifestFile ? LibSource.unstamped(content) : content);
75
57
  hasher.update("\0");
76
58
  }
77
59
  return hasher.digest("hex").slice(0, 32);
78
60
  }
79
61
 
62
+ // Untracked files count: a freshly copied library is not committed yet.
63
+ async computeHash() {
64
+ const files = (await this.#lib.workspace.listGitFiles([`libs/${this.#lib.name}`], { untracked: true })).filter(
65
+ (file) => this.#isHashed(file),
66
+ );
67
+ return await LibSource.hashFiles(files, `${this.#prefix}package.json`, (file) =>
68
+ this.#lib.workspace.readFile(file),
69
+ );
70
+ }
71
+
80
72
  async read(): Promise<LibSourceStamp | null> {
81
73
  const manifest = await this.#lib.getPackageJson();
82
74
  const akan = manifest[LibSource.manifestKey] as { source?: LibSourceStamp } | undefined;
@@ -91,11 +83,7 @@ export class LibSource {
91
83
  return stamp;
92
84
  }
93
85
 
94
- /**
95
- * Writes the stamp only when it would change. `syncedAt` moves on every write, so an unconditional
96
- * write leaves the manifest dirty and defeats an idempotent caller — the hash is computed with the
97
- * stamp removed, so comparing it first is sound.
98
- */
86
+ // `syncedAt` moves on every write, so an unchanged stamp is left alone (the hash excludes the stamp itself).
99
87
  async syncStamp({ origin, sha }: Pick<LibSourceStamp, "origin" | "sha">) {
100
88
  const [current, hash] = await Promise.all([this.read(), this.computeHash()]);
101
89
  if (current?.origin === origin && current.sha === sha && current.hash === hash)
@@ -0,0 +1,4 @@
1
+ export const readTheme = () => document.cookie; // @flag
2
+ export const writeTheme = (value: string) => { document.cookie = `theme=${value}`; }; // @flag
3
+ export const throughWindow = () => window.document.cookie; // @flag
4
+ export const throughGlobalThis = () => globalThis.document.cookie; // @flag
@@ -0,0 +1,7 @@
1
+ import { getCookie, setCookie } from "akanjs/client";
2
+ export const readTheme = () => getCookie("theme"); // @ok
3
+ export const writeTheme = (value: string) => setCookie("theme", value); // @ok
4
+ export const otherDocumentFields = () => document.title; // @ok
5
+ export const aFieldNamedCookie = (req: { cookie: string }) => req.cookie; // @ok
6
+ export const aLiteral = "document.cookie is empty on app://"; // @ok
7
+ export const Docs = () => <p>getCookie reads the request, document.cookie the browser.</p>; // @ok
@@ -0,0 +1,7 @@
1
+ export const share = (url: string) => navigator.share({ url }); // @flag
2
+ export const canShare = () => typeof navigator.canShare === "function"; // @flag
3
+ export const worker = () => navigator.serviceWorker.register("/sw.js"); // @flag
4
+ export const ask = () => Notification.requestPermission(); // @flag
5
+ export const permission = () => Notification.permission; // @flag
6
+ export const locate = () => navigator.geolocation.getCurrentPosition(() => undefined); // @flag
7
+ export const buzz = () => navigator.vibrate(50); // @flag
@@ -0,0 +1,11 @@
1
+ import { haptics } from "akanjs/client/native";
2
+ import { useGeoLocation } from "akanjs/webkit";
3
+ export const buzz = () => haptics.impact(); // @ok
4
+ export const locate = () => useGeoLocation(); // @ok
5
+ export const languages = () => navigator.languages; // @ok
6
+ export const clipboard = (text: string) => navigator.clipboard.writeText(text); // @ok
7
+ export const aField = (payload: { share: boolean }) => payload.share; // @ok
8
+ export const aLiteral = "navigator.share is missing on Android"; // @ok
9
+ export const Docs = () => <p>navigator.share is missing on Android.</p>; // @ok
10
+ export const ModelZone = () => <Notification.Zone.Initialize />; // @ok
11
+ export const model = (cnst: { Notification: unknown }) => cnst.Notification; // @ok
@@ -0,0 +1,6 @@
1
+ export const readDraft = () => localStorage.getItem("draft"); // @flag
2
+ export const writeDraft = (value: string) => localStorage.setItem("draft", value); // @flag
3
+ export const readTab = () => sessionStorage.getItem("tab"); // @flag
4
+ export const throughWindow = () => window.localStorage.getItem("draft"); // @flag
5
+ export const throughGlobalThis = () => globalThis.sessionStorage.clear(); // @flag
6
+ export const guarded = () => (typeof localStorage === "undefined" ? null : 1); // @flag
@@ -0,0 +1,7 @@
1
+ import { secretStorage, storage } from "akanjs/client";
2
+ export const readDraft = () => storage.getItem("draft"); // @ok
3
+ export const keepToken = (jwt: string) => secretStorage.setItem("jwt", jwt); // @ok
4
+ export const aFieldWithTheName = (cache: { localStorage: string }) => cache.localStorage; // @ok
5
+ export const anObjectKey = { sessionStorage: false }; // @ok
6
+ export const aLiteral = "localStorage works in the shells"; // @ok
7
+ export const Docs = () => <p>window.localStorage and sessionStorage work in the shells.</p>; // @ok
@@ -4,24 +4,14 @@ import { existsSync } from "node:fs";
4
4
  import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
+ import { Linter } from "../linter";
7
8
 
8
- /**
9
- * Fixture tests for the GritQL plugins in this folder.
10
- *
11
- * These rules break the apps/libs build, and a rule that stops matching reports **zero diagnostics** rather
12
- * than an error — so a biome upgrade can retire one silently. GritQL has known sharp edges here (escaped
13
- * quotes, argument-position matching), which is what makes the quiet failure plausible rather than
14
- * theoretical.
15
- *
16
- * Each rule runs alone, against a config this test writes, so a diagnostic can only have come from that
17
- * plugin. The path scoping in `biome.base.json` is deliberately not exercised — what is under test is
18
- * whether the pattern still matches. A rule that reads `$filename` needs a real path anyway, which is what
19
- * a case's `fixture.json` supplies.
20
- */
9
+ //? A rule that stops matching reports zero diagnostics, not an error, so a biome upgrade can retire one silently.
10
+ //? Each rule runs alone against a config written here; the path scoping in biome.base.json is deliberately not tested.
21
11
  const lintDir = import.meta.dir;
22
12
  const fixturesDir = path.join(lintDir, "__fixtures__");
23
13
  const workspaceRoot = path.resolve(lintDir, "../../../..");
24
- const biomeBin = path.join(workspaceRoot, "node_modules/.bin/biome");
14
+ const biomeBin = path.join(workspaceRoot, "node_modules", ".bin", Linter.biomeBinName);
25
15
 
26
16
  interface BiomeDiagnostic {
27
17
  severity: string;
@@ -30,13 +20,10 @@ interface BiomeDiagnostic {
30
20
  location?: { start?: { line?: number } };
31
21
  }
32
22
 
33
- /**
34
- * `path` is where the source is written inside the temp workspace, for a rule that gates on `$filename`;
35
- * its extension also decides whether the file may hold JSX. `expect: "file"` is for a rule whose span is
36
- * the whole module (`JsModule()`), where there is no per-line diagnostic to match.
37
- */
38
23
  interface FixtureMeta {
24
+ /** Where the source is written, for a rule that gates on `$filename`; its extension decides whether JSX parses. */
39
25
  path?: string;
26
+ /** `"file"`: the rule's span is the whole module (`JsModule()`), so there is no per-line diagnostic to match. */
40
27
  expect?: "lines" | "file";
41
28
  }
42
29
 
@@ -64,7 +51,6 @@ const runBiome = (args: string[], cwd: string) =>
64
51
  proc.on("close", () => resolve(stdout || stderr));
65
52
  });
66
53
 
67
- /** Every directory holding a `bad.*`, named `<rule>` or `<rule>/<case>`. */
68
54
  const collectCases = async (): Promise<FixtureCase[]> => {
69
55
  const cases: FixtureCase[] = [];
70
56
  const walk = async (dir: string, rule: string, name: string) => {
@@ -92,7 +78,6 @@ const sourceName = (fixture: FixtureCase, kind: "bad" | "good") => {
92
78
  const readFixture = async (fixture: FixtureCase, kind: "bad" | "good") =>
93
79
  await readFile(path.join(fixture.dir, sourceName(fixture, kind).read), "utf-8");
94
80
 
95
- /** Diagnostics from one plugin against one fixture, keyed by the 1-based line each was reported on. */
96
81
  const lintFixture = async (fixture: FixtureCase, kind: "bad" | "good") => {
97
82
  const dir = await mkdtemp(path.join(os.tmpdir(), `grit-${fixture.rule}-`));
98
83
  tempDirs.push(dir);
@@ -127,7 +112,7 @@ const lintFixture = async (fixture: FixtureCase, kind: "bad" | "good") => {
127
112
  };
128
113
  };
129
114
 
130
- /** The 1-based line of every line carrying `marker`. One case per line is the fixture contract. */
115
+ //? One case per line is the fixture contract.
131
116
  const markedLines = async (fixture: FixtureCase, kind: "bad" | "good", marker: string) =>
132
117
  (await readFixture(fixture, kind))
133
118
  .split("\n")
@@ -164,8 +149,7 @@ describe("grit lint rules", () => {
164
149
  }
165
150
  const expected = await markedLines(fixture, "bad", "// @flag");
166
151
  expect(expected.length).toBeGreaterThan(0);
167
- // Set equality, not containment: a marked line nobody reported is a pattern that stopped matching,
168
- // and a reported line nobody marked is the rule reaching further than the fixture claims.
152
+ // Set equality: an unreported mark is a pattern that stopped matching, an unmarked report is overreach.
169
153
  expect(lines).toEqual(expected);
170
154
  });
171
155
 
@@ -15,7 +15,7 @@ or {
15
15
  JsModuleSource() as $source where {
16
16
  $source <: within JsImport(),
17
17
  not $filename <: r".*\.(?:test|spec)\.tsx?",
18
- $filename <: r".*(?:apps|libs)/[^/]+/lib/[^/]+/[^/]+\.(?:ts|tsx)",
18
+ $filename <: r".*(?:apps|libs)[/\\][^/\\]+[/\\]lib[/\\][^/\\]+[/\\][^/\\]+\.(?:ts|tsx)",
19
19
  $source <: r"\"\.\./\.\./.*\"",
20
20
  register_diagnostic(
21
21
  span = $source,
@@ -25,7 +25,7 @@ or {
25
25
  JsModuleSource() as $source where {
26
26
  $source <: within JsImport(),
27
27
  not $filename <: r".*\.(?:test|spec)\.tsx?",
28
- $filename <: r".*/lib/.+\.tsx",
28
+ $filename <: r".*[/\\]lib[/\\].+\.tsx",
29
29
  $source <: r"\"\.\./.*\"",
30
30
  register_diagnostic(
31
31
  span = $source,
@@ -0,0 +1,29 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // A native shell serves the page from app://localhost on iOS, macOS and Linux, a custom scheme WebKit keeps no
5
+ // cookie jar for: `document.cookie` reads "" and a write is dropped without an error, so the value exists in the
6
+ // browser and silently vanishes in the app. akanjs/client's cookie helpers keep an app:// page's cookies in a
7
+ // storage-backed jar, which is the only spelling that works in every shell.
8
+ //
9
+ // Pinned to the member nodes: a bare snippet also matches JSX text that spells the same words (a docs page), and a
10
+ // write is its own node (JsStaticMemberAssignment) that the expression form does not match.
11
+ or {
12
+ JsStaticMemberExpression() as $access where {
13
+ $access <: or { `document.cookie`, `window.document.cookie`, `globalThis.document.cookie`, `self.document.cookie` },
14
+ register_diagnostic(
15
+ span = $access,
16
+ message = "`document.cookie` is always empty on a native app page (app://localhost keeps no cookies), so this value vanishes in the app. Use `getCookie` / `setCookie` / `removeCookie` from `akanjs/client`.",
17
+ severity = "error"
18
+ )
19
+ },
20
+ JsStaticMemberAssignment(object = $object, member = $member) as $access where {
21
+ $member <: `cookie`,
22
+ $object <: or { `document`, `window.document`, `globalThis.document`, `self.document` },
23
+ register_diagnostic(
24
+ span = $access,
25
+ message = "`document.cookie` is always empty on a native app page (app://localhost keeps no cookies), so this value vanishes in the app. Use `getCookie` / `setCookie` / `removeCookie` from `akanjs/client`.",
26
+ severity = "error"
27
+ )
28
+ }
29
+ }
@@ -14,7 +14,8 @@ language js(typescript, jsx)
14
14
  $base <: contains `endpoint`,
15
15
  $className <: r"([A-Z][a-zA-Z0-9]*)Endpoint"($Cap)
16
16
  },
17
- $filename <: r".*/([a-z][a-zA-Z0-9]*)\.signal\.ts"($model),
17
+ // `$filename` carries backslashes on Windows, so a separator here must accept both.
18
+ $filename <: r".*[/\\]([a-z][a-zA-Z0-9]*)\.signal\.ts"($model),
18
19
  or {
19
20
  $key <: $model,
20
21
  $key <: r"(?:light|create|update|remove|view|edit|merge)([A-Z][a-zA-Z0-9]*)"($keyCap) where {