@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
@@ -0,0 +1,307 @@
1
+ import fs from "node:fs";
2
+ import { mkdir, readdir, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import {
5
+ appGenerationOf,
6
+ CSR_DEV_APP_FILE,
7
+ CSR_DEV_KEPT_PATCHES,
8
+ CSR_DEV_MANIFEST_FILE,
9
+ CSR_DEV_PATCHING_MARKER,
10
+ type CsrDevLayout,
11
+ type CsrDevManifest,
12
+ csrDevModuleFile,
13
+ } from "akanjs/server/hmr/csrDevManifest";
14
+ import { FileSys } from "../fileSys";
15
+ import { CsrDevPaths } from "./csrDevPaths";
16
+ import type { CsrDevCode, CsrDevCompiledModule, CsrDevGraph } from "./csrDevTypes";
17
+
18
+ export interface CsrDevArtifactWriterOptions {
19
+ /** Where the dev server serves `outDir`; a patch is announced by its URL. */
20
+ routePrefix: string;
21
+ /** `app.js` starts no entry: an SSR page requires the modules its RSC payload names, one at a time. */
22
+ library?: boolean;
23
+ }
24
+
25
+ export class CsrDevArtifactWriter {
26
+ static readonly #keptVendors = 3;
27
+ static readonly #buildingMarker = ".building";
28
+ readonly #outDir: string;
29
+ readonly #routePrefix: string;
30
+ readonly #library: boolean;
31
+
32
+ constructor(outDir: string, { routePrefix, library = false }: CsrDevArtifactWriterOptions) {
33
+ this.#outDir = outDir;
34
+ this.#routePrefix = routePrefix;
35
+ this.#library = library;
36
+ }
37
+
38
+ //? Set between a whole build's first write and its last: module files are named by id and overwritten in place, so a
39
+ //? build cut short there leaves new modules under the previous graph and app.js, which no patch may read.
40
+ async markBuilding(): Promise<void> {
41
+ await Bun.write(path.join(this.#outDir, CsrDevArtifactWriter.#buildingMarker), String(process.pid));
42
+ }
43
+
44
+ async clearBuilding(): Promise<void> {
45
+ await CsrDevArtifactWriter.#remove(path.join(this.#outDir, CsrDevArtifactWriter.#buildingMarker));
46
+ }
47
+
48
+ async isBuilding(): Promise<boolean> {
49
+ return await Bun.file(path.join(this.#outDir, CsrDevArtifactWriter.#buildingMarker)).exists();
50
+ }
51
+
52
+ //? A reload's generation has no patch: one an attempt cut short left under that number would be applied by a tab
53
+ //? catching up (catchUp reloads only when the file is missing).
54
+ async forgetPatch(generation: number): Promise<void> {
55
+ await Promise.all(
56
+ [`patch-${generation}.js`, `patch-${generation}.js.layout.json`].map((name) =>
57
+ CsrDevArtifactWriter.#remove(path.join(this.#outDir, name)),
58
+ ),
59
+ );
60
+ }
61
+
62
+ async readJson<T>(name: string): Promise<T | null> {
63
+ const file = Bun.file(path.join(this.#outDir, name));
64
+ if (!(await file.exists())) return null;
65
+ return (await file.json().catch(() => null)) as T | null;
66
+ }
67
+
68
+ async writeJson(name: string, value: unknown): Promise<void> {
69
+ await this.#writeAtomic(name, JSON.stringify(value));
70
+ }
71
+
72
+ async writeModules(compiled: CsrDevCompiledModule[]): Promise<void> {
73
+ await mkdir(path.join(this.#outDir, "modules"), { recursive: true });
74
+ const helpers = new Map(
75
+ compiled.flatMap((module) => (module.helpers ? [[module.helpers.hash, module.helpers]] : [])),
76
+ );
77
+ await Promise.all([
78
+ ...compiled.flatMap((module) => [
79
+ Bun.write(this.#modulePath(module.id, ".js"), module.factory),
80
+ ...(module.sourceMap ? [Bun.write(this.#modulePath(module.id, ".js.map"), module.sourceMap)] : []),
81
+ ]),
82
+ ...[...helpers.values()].map((shared) => Bun.write(this.#helpersPath(shared.hash), shared.definition)),
83
+ ]);
84
+ }
85
+
86
+ //? A module whose file is missing is left out rather than failing the read: the patcher compiles it again.
87
+ async readCode(graph: CsrDevGraph): Promise<CsrDevCode> {
88
+ const ids = Object.keys(graph.modules).filter((id) => !graph.modules[id]?.vendor && this.hasModule(id));
89
+ const hashes = [...new Set(ids.flatMap((id) => graph.modules[id]?.helpers ?? []))];
90
+ const [modules, helpers] = await Promise.all([
91
+ Promise.all(ids.map(async (id) => [id, await Bun.file(this.#modulePath(id, ".js")).text()] as const)),
92
+ Promise.all(hashes.map(async (hash) => [hash, await Bun.file(this.#helpersPath(hash)).text()] as const)),
93
+ ]);
94
+ return { modules: new Map(modules), helpers: new Map(helpers) };
95
+ }
96
+
97
+ hasModule(id: string): boolean {
98
+ return fs.existsSync(this.#modulePath(id, ".js"));
99
+ }
100
+
101
+ async forgetModules(ids: string[]): Promise<void> {
102
+ await Promise.all(
103
+ ids.flatMap((id) => [
104
+ CsrDevArtifactWriter.#remove(this.#modulePath(id, ".js")),
105
+ CsrDevArtifactWriter.#remove(this.#modulePath(id, ".js.map")),
106
+ ]),
107
+ );
108
+ }
109
+
110
+ async writeVendor(graph: CsrDevGraph): Promise<string> {
111
+ const ids = Object.keys(graph.modules)
112
+ .filter((id) => graph.modules[id]?.vendor)
113
+ .sort();
114
+ const stubs = new Set(Object.values(graph.modules).flatMap((module) => module.deps.filter(CsrDevPaths.isStub)));
115
+ const lines = [await this.#helperDefinitions(graph, ids), ...(await this.#defineLines(ids))];
116
+ for (const stub of [...stubs].sort()) {
117
+ if (stub.startsWith(CsrDevPaths.emptyStubPrefix)) {
118
+ lines.push(`__akan.define(${JSON.stringify(stub)}, function () {});\n`);
119
+ continue;
120
+ }
121
+ const message = `[akan-csr] ${stub.slice(CsrDevPaths.stubPrefix.length)} is a Node built-in the browser does not have`;
122
+ lines.push(
123
+ `__akan.define(${JSON.stringify(stub)}, function () {\n throw new Error(${JSON.stringify(message)});\n});\n`,
124
+ );
125
+ }
126
+ const code = lines.join("");
127
+ const vendorFile = `vendor-${Bun.hash(code).toString(36)}.js`;
128
+ await this.#writeAtomic(vendorFile, code);
129
+ return vendorFile;
130
+ }
131
+
132
+ //? A library app.js names the vendor file and epoch it was built with: a tab that booted it beside another build's
133
+ //? vendor file (a whole build landing between its render and its load) tells, and keeps the right epoch for hello.
134
+ async writeApp(
135
+ graph: CsrDevGraph,
136
+ { generation, vendorFile, epoch }: Pick<CsrDevManifest, "generation" | "vendorFile" | "epoch">,
137
+ code?: CsrDevCode,
138
+ ): Promise<void> {
139
+ const ids = Object.keys(graph.modules)
140
+ .filter((id) => !graph.modules[id]?.vendor)
141
+ .sort();
142
+ const lines = await this.#defineLines(ids, code);
143
+ const blocks = ids.map((id, index) => [id, lines[index] ?? ""] as [string, string]);
144
+ const start = this.#library
145
+ ? `__akan.startLibrary(${JSON.stringify({ generation, refresh: graph.refresh, bootstrap: graph.entries[""], vendorFile, epoch })});\n`
146
+ : `__akan.start(${JSON.stringify({ generation, refresh: graph.refresh })});\n`;
147
+ await this.#writeWithSourceMap(CSR_DEV_APP_FILE, await this.#helperDefinitions(graph, ids, code), blocks, start);
148
+ }
149
+
150
+ async writePatch(generation: number, modules: CsrDevCompiledModule[]): Promise<string> {
151
+ const patchFile = `patch-${generation}.js`;
152
+ const blocks = modules.map(
153
+ (module, index) =>
154
+ [module.id, `${JSON.stringify(module.id)}: ${module.factory}${index < modules.length - 1 ? ",\n" : "\n"}`] as [
155
+ string,
156
+ string,
157
+ ],
158
+ );
159
+ const helpers = new Map(
160
+ modules.flatMap((module) => (module.helpers ? [[module.helpers.hash, module.helpers]] : [])),
161
+ );
162
+ const header = `${[...helpers.values()].map((shared) => shared.definition).join("")}__akan.update(${generation}, {\n`;
163
+ await this.#writeWithSourceMap(patchFile, header, blocks, "});\n");
164
+ return `${this.#routePrefix}${patchFile}`;
165
+ }
166
+
167
+ // The manifest goes last: the server renders shells from it, so it must never name a file not yet written.
168
+ async writeState(graph: CsrDevGraph, manifest: CsrDevManifest): Promise<void> {
169
+ await this.writeJson("graph.json", graph);
170
+ await this.writeJson(CSR_DEV_MANIFEST_FILE, manifest);
171
+ }
172
+
173
+ //? The open tabs need only the patch file and the manifest naming it, so they hear of a patch before the rest lands:
174
+ //? the module files and graph.json (read only by the next build), then app.js (a full rewrite). The manifest says
175
+ //? which generation app.js holds, and the shell holds a tab booting in that gap until it does. A crash in the gap is
176
+ //? recovered by the next build, which sees the edited files' mtimes differ from graph.json and compiles them again.
177
+ async commitPatch(
178
+ graph: CsrDevGraph,
179
+ manifest: CsrDevManifest,
180
+ announce: () => void,
181
+ { modules, code }: { modules: CsrDevCompiledModule[]; code?: CsrDevCode },
182
+ ): Promise<void> {
183
+ await this.writeJson(CSR_DEV_MANIFEST_FILE, manifest);
184
+ announce();
185
+ await CsrDevArtifactWriter.#appWriteDelay();
186
+ await this.writeModules(modules);
187
+ await this.writeJson("graph.json", graph);
188
+ await this.writeApp(graph, manifest, code);
189
+ await this.writeJson(CSR_DEV_MANIFEST_FILE, { ...manifest, appGeneration: manifest.generation });
190
+ }
191
+
192
+ // A process that died between announcing a patch and rewriting app.js left every booting tab one generation behind.
193
+ async healApp(graph: CsrDevGraph, manifest: CsrDevManifest, code?: CsrDevCode): Promise<CsrDevManifest> {
194
+ if (appGenerationOf(manifest) >= manifest.generation) return manifest;
195
+ await this.writeApp(graph, manifest, code);
196
+ const healed = { ...manifest, appGeneration: manifest.generation };
197
+ await this.writeJson(CSR_DEV_MANIFEST_FILE, healed);
198
+ return healed;
199
+ }
200
+
201
+ async prune(vendorFile: string, generation: number): Promise<void> {
202
+ const names = await readdir(this.#outDir);
203
+ const stale = names.filter((name) => {
204
+ const patch = /^patch-(\d+)\.js(?:\.layout\.json)?$/.exec(name);
205
+ return patch ? Number(patch[1]) <= generation - CSR_DEV_KEPT_PATCHES : false;
206
+ });
207
+ const vendors = names
208
+ .filter((name) => /^vendor-[\w-]+\.js$/.test(name) && name !== vendorFile)
209
+ .map((name) => ({ name, mtimeMs: CsrDevPaths.mtimeOf(path.join(this.#outDir, name)) }))
210
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
211
+ .slice(CsrDevArtifactWriter.#keptVendors - 1)
212
+ .map(({ name }) => name);
213
+ await Promise.all(
214
+ [...stale, ...vendors].map((name) => CsrDevArtifactWriter.#remove(path.join(this.#outDir, name))),
215
+ );
216
+ }
217
+
218
+ //? A whole build writes over the registry before it instead of clearing it first, so it clears what that one left:
219
+ //? the modules and helpers it no longer holds, every patch (each was made against the previous registry), the assets
220
+ //? this build did not write (by path: a file's mtime and this process's clock need not agree on a mounted volume),
221
+ //? temp files of a write cut short, and the marker of a builder that died mid-patch.
222
+ async pruneAfterFullBuild(
223
+ graph: CsrDevGraph,
224
+ vendorFile: string,
225
+ { generation, assets }: { generation: number; assets: string[] },
226
+ ): Promise<void> {
227
+ const kept = new Set(
228
+ Object.keys(graph.modules).flatMap((id) => [this.#modulePath(id, ".js"), this.#modulePath(id, ".js.map")]),
229
+ );
230
+ for (const module of Object.values(graph.modules)) if (module.helpers) kept.add(this.#helpersPath(module.helpers));
231
+ for (const asset of assets) kept.add(path.resolve(asset));
232
+ const listed = async (dir: string) =>
233
+ (await readdir(path.join(this.#outDir, dir)).catch(() => [] as string[])).map((name) =>
234
+ path.join(this.#outDir, dir, name),
235
+ );
236
+ const stale = [
237
+ ...(await listed("modules")).filter((file) => !kept.has(file)),
238
+ ...(await listed("assets")).filter((file) => !kept.has(file)),
239
+ ...(await listed(".")).filter((file) => {
240
+ const name = path.basename(file);
241
+ return /^patch-\d+\.js(?:\.layout\.json)?$/.test(name) || name.endsWith(".tmp");
242
+ }),
243
+ path.join(this.#outDir, CSR_DEV_PATCHING_MARKER),
244
+ ];
245
+ await Promise.all(stale.map((file) => CsrDevArtifactWriter.#remove(file)));
246
+ await this.prune(vendorFile, generation);
247
+ }
248
+
249
+ async #helperDefinitions(graph: CsrDevGraph, ids: string[], code?: CsrDevCode): Promise<string> {
250
+ const hashes = [...new Set(ids.flatMap((id) => graph.modules[id]?.helpers ?? []))].sort();
251
+ return (
252
+ await Promise.all(hashes.map(async (hash) => code?.helpers.get(hash) ?? Bun.file(this.#helpersPath(hash)).text()))
253
+ ).join("");
254
+ }
255
+
256
+ async #defineLines(ids: string[], code?: CsrDevCode): Promise<string[]> {
257
+ return await Promise.all(
258
+ ids.map(async (id) => {
259
+ const factory = code?.modules.get(id) ?? (await Bun.file(this.#modulePath(id, ".js")).text());
260
+ return `__akan.define(${JSON.stringify(id)}, ${factory});\n`;
261
+ }),
262
+ );
263
+ }
264
+
265
+ //? A factory's code starts on the line after its header, so each module's map is placed one line below it.
266
+ async #writeWithSourceMap(name: string, header: string, blocks: [id: string, text: string][], footer: string) {
267
+ const layout: CsrDevLayout = { lineCount: 0, modules: [] };
268
+ let line = CsrDevPaths.lineCount(header);
269
+ for (const [id, text] of blocks) {
270
+ layout.modules.push([id, line + 1]);
271
+ line += CsrDevPaths.lineCount(text);
272
+ }
273
+ const code = `${header}${blocks.map(([, text]) => text).join("")}${footer}//# sourceMappingURL=${name}.map\n`;
274
+ layout.lineCount = CsrDevPaths.lineCount(code);
275
+ await this.writeJson(`${name}.layout.json`, layout);
276
+ await this.#writeAtomic(name, code);
277
+ }
278
+
279
+ // A rename is atomic: the dev server may be streaming the previous app.js to a reloading page right now.
280
+ async #writeAtomic(name: string, content: string): Promise<void> {
281
+ const target = path.join(this.#outDir, name);
282
+ const temp = `${target}.${process.pid}.tmp`;
283
+ await Bun.write(temp, content);
284
+ await FileSys.replace(temp, target);
285
+ }
286
+
287
+ //? Best effort, after the tabs already have the update: Windows refuses to delete a file a reader holds open (the
288
+ //? server streaming it to a tab), and throwing here would send a patch that already went out to a build worker.
289
+ //? A file that stays is removed by a later prune.
290
+ static async #remove(file: string): Promise<void> {
291
+ await rm(file, { force: true }).catch(() => undefined);
292
+ }
293
+
294
+ //? A test-only hook that widens the gap between the patch and app.js, so an E2E can boot a tab inside it.
295
+ static async #appWriteDelay(): Promise<void> {
296
+ const ms = Number(process.env.AKAN_CSR_DEV_APP_WRITE_DELAY_MS);
297
+ if (Number.isInteger(ms) && ms > 0) await Bun.sleep(ms);
298
+ }
299
+
300
+ #modulePath(id: string, extension: ".js" | ".js.map"): string {
301
+ return path.join(this.#outDir, csrDevModuleFile(id, extension));
302
+ }
303
+
304
+ #helpersPath(hash: string): string {
305
+ return path.join(this.#outDir, "modules", `helpers-${hash}.js`);
306
+ }
307
+ }
@@ -0,0 +1,66 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ //* Pins the Bun behaviour CsrDevBundler is built on, so a Bun upgrade that changes it fails here, not in a browser.
7
+ describe("Bun output the CSR registry bundle relies on", () => {
8
+ let dir: string;
9
+ const externalizeAll = {
10
+ name: "externalize-all",
11
+ setup(build: Bun.PluginBuilder) {
12
+ build.onResolve({ filter: /.*/ }, (args) =>
13
+ args.kind === "entry-point-build" ? undefined : { path: `akan-module:${args.path}`, external: true },
14
+ );
15
+ },
16
+ };
17
+
18
+ beforeAll(async () => {
19
+ dir = await mkdtemp(path.join(os.tmpdir(), "akan-csr-dev-bundler-"));
20
+ await Bun.write(path.join(dir, "a.ts"), "export const a = 1;\n");
21
+ await Bun.write(
22
+ path.join(dir, "entry.ts"),
23
+ 'import value from "./a";\nexport * from "./a";\nexport const lazy = () => import("./a");\nconsole.log(value);\n',
24
+ );
25
+ });
26
+
27
+ afterAll(async () => {
28
+ await rm(dir, { recursive: true, force: true });
29
+ });
30
+
31
+ const build = async (entrypoints: string[]) =>
32
+ await Bun.build({
33
+ entrypoints: entrypoints.map((file) => path.join(dir, file)),
34
+ root: dir,
35
+ target: "browser",
36
+ format: "cjs",
37
+ naming: { entry: "[dir]/[name].[ext]" },
38
+ plugins: [externalizeAll],
39
+ });
40
+
41
+ test("the CJS helpers keep the shapes CsrDevBundler rewrites", async () => {
42
+ const [output] = (await build(["entry.ts"])).outputs;
43
+ const code = (await output?.text()) ?? "";
44
+ expect(code).toContain("var __toESM = (mod, isNodeMode, target) => {");
45
+ expect(code).toContain("var __reExport = (target, mod, secondTarget) => {");
46
+ expect(code).toContain('require("akan-module:./a")');
47
+ expect(code).toContain('import("akan-module:./a")');
48
+ });
49
+
50
+ test("the helper preamble ends at the first path comment and names no factory argument", async () => {
51
+ const [output] = (await build(["entry.ts"])).outputs;
52
+ const code = (await output?.text()) ?? "";
53
+ const marker = code.search(/^\/\/ /m);
54
+ expect(marker).toBeGreaterThan(0);
55
+ const preamble = code.slice(0, marker);
56
+ expect(preamble).toMatch(/^var __toESM = /m);
57
+ expect(preamble).not.toMatch(/(?<![\w$.])(?:require|module|exports)(?![\w$]|\s*:)/);
58
+ });
59
+
60
+ test("a relative import of another entrypoint in the same build stays external", async () => {
61
+ const outputs = (await build(["entry.ts", "a.ts"])).outputs;
62
+ const entry = await outputs.find((output) => output.path.includes("entry"))?.text();
63
+ expect(entry).toContain('require("akan-module:./a")');
64
+ expect(entry).not.toContain("var a = 1");
65
+ });
66
+ });
@@ -0,0 +1,95 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { CSR_DEV_DIRNAME, CSR_DEV_ROUTE_PREFIX } from "akanjs/server/hmr/csrDevManifest";
4
+ import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
5
+ import type { App } from "../commandDecorators";
6
+ import { AsyncDefaultExportDetector } from "../transforms/asyncDefaultExportDetector";
7
+ import { bundleDefine } from "./bundleDefine";
8
+ import { CsrDevPaths } from "./csrDevPaths";
9
+ import type { CsrDevContext } from "./csrDevTypes";
10
+ import { CsrEntryFiles } from "./csrEntryFiles";
11
+ import { DevRegistryBundler } from "./devRegistryBundler";
12
+ import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
13
+
14
+ //* Dev-only CSR as a module registry, under `.akan/artifact/csr-dev`: one entry per basePath boots every page.
15
+ export class CsrDevBundler extends DevRegistryBundler {
16
+ static readonly #formatVersion = 4;
17
+ readonly reloadsOnEntryChange = true;
18
+ //? Detecting a page's async default parses it with TypeScript: on apps/akan every page each save cost ~100ms.
19
+ readonly #asyncDefaults = new Map<string, { mtimeMs: number; detected: Promise<boolean> }>();
20
+
21
+ constructor(app: App) {
22
+ super(app, { dirName: CSR_DEV_DIRNAME, routePrefix: CSR_DEV_ROUTE_PREFIX, library: false });
23
+ }
24
+
25
+ async context(): Promise<CsrDevContext | null> {
26
+ const akanConfig = await this.app.getConfig();
27
+ const pageEntries = await resolveSsrPageEntriesForApp(this.app, await this.app.getPageKeys());
28
+ if (pageEntries.length === 0) return null;
29
+ const basePaths = [...akanConfig.basePaths];
30
+ const htmlBasePaths = basePaths.length > 0 ? basePaths : [""];
31
+ const define = bundleDefine(this.app, "start", "csr");
32
+ const optimizeImports = [...akanConfig.optimizeImports];
33
+ const configKey = Bun.hash(
34
+ JSON.stringify([CsrDevBundler.#formatVersion, Bun.version, define, optimizeImports, htmlBasePaths]),
35
+ ).toString(36);
36
+ return {
37
+ pageEntries,
38
+ basePaths,
39
+ htmlBasePaths,
40
+ define,
41
+ optimizeImports,
42
+ configKey,
43
+ refreshFile: this.#resolveRefreshRuntime(),
44
+ };
45
+ }
46
+
47
+ async writeEntries(context: CsrDevContext): Promise<{ files: Record<string, string>; changed: string[] }> {
48
+ await mkdir(this.entryDir, { recursive: true });
49
+ const files: Record<string, string> = {};
50
+ const changed: string[] = [];
51
+ for (const basePath of context.htmlBasePaths) {
52
+ const file = path.join(this.entryDir, CsrEntryFiles.entryFilename(basePath));
53
+ const entryPages = CsrEntryFiles.pageEntriesForBasePath(context.pageEntries, basePath, context.basePaths);
54
+ const generator = new PagesEntrySourceGenerator(entryPages, {
55
+ isAsyncDefault: (moduleAbsPath) => this.#isAsyncDefault(moduleAbsPath),
56
+ });
57
+ const pages = await generator.generateStatic({ fromDir: this.entryDir });
58
+ const hot = await generator.generateHotReplace({
59
+ fromDir: this.entryDir,
60
+ ownerId: this.paths.idOf(file),
61
+ moduleIds: entryPages.map(({ moduleAbsPath }) => this.paths.idOf(moduleAbsPath)),
62
+ });
63
+ const source = `import * as akanWebkit from "akanjs/webkit";\n${pages}\nvoid akanWebkit.bootCsr(pages);\n${hot}`;
64
+ const unchanged = (await Bun.file(file).exists()) && (await Bun.file(file).text()) === source;
65
+ if (!unchanged) await Bun.write(file, source);
66
+ const entryFile = CsrDevPaths.realpath(file);
67
+ files[basePath] = entryFile;
68
+ if (!unchanged) changed.push(entryFile);
69
+ }
70
+ return { files, changed };
71
+ }
72
+
73
+ protected async rootFiles(entryFiles: string[]): Promise<string[]> {
74
+ return entryFiles;
75
+ }
76
+
77
+ #isAsyncDefault(moduleAbsPath: string): Promise<boolean> {
78
+ const mtimeMs = CsrDevPaths.mtimeOf(moduleAbsPath);
79
+ const cached = this.#asyncDefaults.get(moduleAbsPath);
80
+ if (cached?.mtimeMs === mtimeMs) return cached.detected;
81
+ const detected = AsyncDefaultExportDetector.detect(moduleAbsPath);
82
+ this.#asyncDefaults.set(moduleAbsPath, { mtimeMs, detected });
83
+ return detected;
84
+ }
85
+
86
+ #resolveRefreshRuntime(): string {
87
+ const akanWebkit = CsrDevPaths.tryResolve("akanjs/webkit", this.app.cwdPath);
88
+ const bases = [this.app.cwdPath, this.paths.root, ...(akanWebkit ? [path.dirname(path.dirname(akanWebkit))] : [])];
89
+ for (const base of bases) {
90
+ const resolved = CsrDevPaths.tryResolve("react-refresh/runtime", base);
91
+ if (resolved) return CsrDevPaths.realpath(resolved);
92
+ }
93
+ throw new Error(`[csr-dev] react-refresh/runtime is not resolvable from ${bases.join(", ")}`);
94
+ }
95
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { CsrDevFactoryCheck } from "./csrDevFactoryCheck";
3
+
4
+ const factoryOf = (body: string) =>
5
+ `function (require, module, exports, $RefreshReg$, $RefreshSig$, __akanImport) {\n${body}\n}`;
6
+
7
+ describe("CsrDevFactoryCheck", () => {
8
+ test("a factory that parses as a classic script has no problem, twice over", () => {
9
+ const factory = factoryOf('var a = require("akan-module:x");\nmodule.exports = { a, label: "import.meta" };');
10
+ expect(CsrDevFactoryCheck.problemOf(factory)).toBeNull();
11
+ expect(CsrDevFactoryCheck.problemOf(factory)).toBeNull();
12
+ });
13
+
14
+ test("an import.meta left in a factory is named with its compiled line", () => {
15
+ const problem = CsrDevFactoryCheck.problemOf(factoryOf("var a = 1;\nvar url = import.meta.url;"));
16
+ expect(problem).toStartWith("import.meta is left in it (compiled line 2), which a classic script cannot hold");
17
+ });
18
+
19
+ test("a top-level await left in a factory is named as one", () => {
20
+ const problem = CsrDevFactoryCheck.problemOf(factoryOf("var a = await Promise.resolve(1);"));
21
+ expect(problem).toStartWith("a top-level await is left in it, which a CommonJS factory cannot wait on");
22
+ });
23
+
24
+ test("any other syntax error answers the parser's message, and a broken helper preamble counts too", () => {
25
+ expect(CsrDevFactoryCheck.problemOf(factoryOf("var = ;"))).toBeString();
26
+ expect(CsrDevFactoryCheck.problemOf(factoryOf("module.exports = 1;"), "var { a } = ;\n")).toBeString();
27
+ });
28
+
29
+ test("the stand-in for a module left out throws its id and the reason", () => {
30
+ const thrower = new Function(`return (${CsrDevFactoryCheck.thrower("node_modules/x/index.js", "why")});`)();
31
+ expect(() => thrower()).toThrow("[akan-csr] node_modules/x/index.js was left out of the dev registry: why");
32
+ });
33
+ });
@@ -0,0 +1,39 @@
1
+ //* A module factory runs inside one classic script with every module written beside it, so a factory that does not
2
+ //* parse as a script drops them all, far from the cause. Each is parsed once, as the page will, before it is written.
3
+ export class CsrDevFactoryCheck {
4
+ static readonly #parsed = new Set<number | bigint>();
5
+
6
+ /** Why the page could not parse `factory` after `helperDefinition`, or null when it can. */
7
+ static problemOf(factory: string, helperDefinition = ""): string | null {
8
+ const key = Bun.hash(`${helperDefinition}\0${factory}`);
9
+ if (CsrDevFactoryCheck.#parsed.has(key)) return null;
10
+ const problem = CsrDevFactoryCheck.#syntaxError(`${helperDefinition}return (${factory});`);
11
+ if (problem === null) {
12
+ CsrDevFactoryCheck.#parsed.add(key);
13
+ return null;
14
+ }
15
+ if (problem.includes("import.meta")) {
16
+ //? JSC's own line is the caller's, not the factory's; the factory's line N is line N of Bun's output.
17
+ const line = factory.split("\n").findIndex((text) => /(?<![\w$.])import\.meta\b/.test(text));
18
+ return `import.meta is left in it${line > 0 ? ` (compiled line ${line})` : ""}, which a classic script cannot hold: ${problem}`;
19
+ }
20
+ if (CsrDevFactoryCheck.#syntaxError(`${helperDefinition}return (async ${factory});`) === null)
21
+ return `a top-level await is left in it, which a CommonJS factory cannot wait on: ${problem}`;
22
+ return problem;
23
+ }
24
+
25
+ /** Stands in for a module left out of the registry: the page runs, and requiring the module says why. */
26
+ static thrower(id: string, problem: string): string {
27
+ const message = `[akan-csr] ${id} was left out of the dev registry: ${problem}`;
28
+ return `function () {\n throw new Error(${JSON.stringify(message)});\n}`;
29
+ }
30
+
31
+ static #syntaxError(source: string): string | null {
32
+ try {
33
+ new Function(source);
34
+ return null;
35
+ } catch (error) {
36
+ return error instanceof Error ? error.message : String(error);
37
+ }
38
+ }
39
+ }