@akanjs/cli 3.0.0-beta.8 → 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 (224) hide show
  1. package/.build-stamp +1 -1
  2. package/CodeAgent-2vmp9qe0.js +9 -0
  3. package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
  4. package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
  5. package/CodeTui-5ed6bg2a.js +1457 -0
  6. package/README.ko.md +83 -28
  7. package/README.md +70 -27
  8. package/TunnelShare-cwh9ezsm.js +9 -0
  9. package/{agent.command-0q17pnrs.js → agent.command-gn73rhe7.js} +8 -9
  10. package/akanCode.js +102 -0
  11. package/application.command-e968t0hw.js +246 -0
  12. package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-64taxseg.js} +121 -77
  13. package/buildBatch.proc.js +120 -32
  14. package/cloud.command-0jm301rr.js +95 -0
  15. package/cloud.script-nemnzjpv.js +20 -0
  16. package/code.command-18kcm7hq.js +154 -0
  17. package/commandManifest.json +1 -1
  18. package/consoleToStderr-h5bryj94.js +7 -0
  19. package/context.command-trd9bn18.js +48 -0
  20. package/context.runner-y5hwh673.js +21 -0
  21. package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
  22. package/{devTui-m1atgkc9.js → devTui-q7k7bakd.js} +122 -40
  23. package/fileSys-wzzrntkw.js +9 -0
  24. package/guideline.command-bdmdn2de.js +42 -0
  25. package/guidelines/agentRule/agentRule.instruction.md +154 -26
  26. package/guidelines/conventions/conventions.instruction.md +134 -48
  27. package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
  28. package/guidelines/queryRule/queryRule.instruction.md +21 -7
  29. package/guidelines/runtimeRule/runtimeRule.instruction.md +291 -6
  30. package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
  31. package/guidelines/transportRule/transportRule.instruction.md +10 -8
  32. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +20 -0
  33. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
  34. package/incrementalBuilder.proc.js +564 -109
  35. package/index-0wpybxtr.js +2729 -0
  36. package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
  37. package/index-2wc41k5w.js +49 -0
  38. package/index-3hx1z96c.js +87 -0
  39. package/{index-2tv3fdx4.js → index-3w94e7n3.js} +1228 -1458
  40. package/{index-hwkygn9s.js → index-48k96p55.js} +325 -36
  41. package/index-4npz05nm.js +187 -0
  42. package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
  43. package/index-6he6gvfg.js +4300 -0
  44. package/index-7md2g9vw.js +6 -0
  45. package/{index-04bk599g.js → index-93j60em0.js} +10 -17
  46. package/index-94s080qm.js +266 -0
  47. package/index-9dp031by.js +12 -0
  48. package/{index-ktb5njgy.js → index-9z771ptz.js} +49 -99
  49. package/{index-y6n8nmny.js → index-abxpehf8.js} +75 -203
  50. package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
  51. package/{index-9077wzd2.js → index-by9pxkr0.js} +43 -26
  52. package/index-c6eggbee.js +123 -0
  53. package/index-d2242ksy.js +99 -0
  54. package/index-drehwpdt.js +1119 -0
  55. package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
  56. package/{index-ffvk10zz.js → index-f3c78nd6.js} +107 -241
  57. package/index-gbggbqcp.js +1058 -0
  58. package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
  59. package/index-hsmtysz4.js +53 -0
  60. package/{index-j9yhg404.js → index-jmrn54tt.js} +4 -7
  61. package/{index-a64esmtb.js → index-kcdj1z5v.js} +523 -1022
  62. package/{index-d43r08tf.js → index-kqabytj7.js} +77 -150
  63. package/{index-vyvre9en.js → index-mbxbhay5.js} +6 -2
  64. package/{index-vxtad5bd.js → index-mgxpx3f2.js} +5 -8
  65. package/index-p6pbz9n5.js +25 -0
  66. package/{index-sj5vxr8f.js → index-p993efkq.js} +981 -897
  67. package/{index-2xp0kfkt.js → index-pmhqttv3.js} +37 -25
  68. package/index-tqm7jjcn.js +700 -0
  69. package/{index-0g7r8xz3.js → index-vpj42xwt.js} +114 -232
  70. package/index-wa5jbxj1.js +760 -0
  71. package/{index-k2j1hc4r.js → index-wprnsjms.js} +12 -12
  72. package/{index-gyd220p2.js → index-y3mphr1m.js} +554 -577
  73. package/index-yg7jhz8x.js +69 -0
  74. package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
  75. package/index.js +23 -22
  76. package/{library.command-qzzbj6d8.js → library.command-9h749a1x.js} +4 -5
  77. package/{localRegistry.command-vyrgg4xw.js → localRegistry.command-2bqqhxyz.js} +23 -43
  78. package/{module.command-rq5hq3ax.js → module.command-nv8nzgft.js} +8 -12
  79. package/{package.command-d9hd6v9n.js → package.command-mq4bggp6.js} +6 -7
  80. package/package.json +15 -7
  81. package/{page.command-126d56te.js → page.command-j639n707.js} +4 -5
  82. package/{primitive.command-taxd1538.js → primitive.command-kz6849th.js} +9 -23
  83. package/prompter-76k56b37.js +8 -0
  84. package/{quality.command-6gsna0a3.js → quality.command-4hyabwkb.js} +205 -377
  85. package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
  86. package/{repair.command-tq2dgy72.js → repair.command-a9awpbqr.js} +11 -35
  87. package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
  88. package/{scalar.command-3bcmrrkv.js → scalar.command-ma4fpm9m.js} +7 -11
  89. package/skills/akan-code/SKILL.md +130 -0
  90. package/skills/akan-debug/SKILL.md +42 -0
  91. package/skills/akan-deps/SKILL.md +50 -0
  92. package/skills/akan-err/SKILL.md +39 -0
  93. package/skills/akan-module/SKILL.md +45 -0
  94. package/skills/akan-scaffold/SKILL.md +76 -0
  95. package/skills/akan-scalar/SKILL.md +52 -0
  96. package/skills/akan-service/SKILL.md +34 -0
  97. package/skills/akan-store/SKILL.md +67 -0
  98. package/skills/akan-ui/SKILL.md +63 -0
  99. package/skills/akan-validate/SKILL.md +44 -0
  100. package/{subspace.command-eddzqpx3.js → subspace.command-3k2pmrs4.js} +53 -62
  101. package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
  102. package/templates/app/page/_index.tsx +257 -258
  103. package/templates/app/page/_layout.tsx +5 -1
  104. package/templates/app/page/_overrides.tsx +12 -0
  105. package/templates/app/page/styles.css.template +316 -14
  106. package/templates/app/public/apple-touch-icon.png +0 -0
  107. package/templates/app/public/favicon.ico +0 -0
  108. package/templates/app/public/jelly/cloud.webp +0 -0
  109. package/templates/app/public/jelly/comet.webp +0 -0
  110. package/templates/app/public/jelly/moon.webp +0 -0
  111. package/templates/app/public/jelly/planet.webp +0 -0
  112. package/templates/app/public/jelly/rocket.webp +0 -0
  113. package/templates/app/public/jelly/star.webp +0 -0
  114. package/templates/app/public/jelly/sugar.webp +0 -0
  115. package/templates/app/public/logo.png +0 -0
  116. package/templates/app/ui/Recipe/index.ts +9 -0
  117. package/templates/app/ui/Recipe/jellyButton.ts +63 -0
  118. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
  119. package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
  120. package/templates/appSample/lib/task/Task.Unit.tsx +1 -1
  121. package/templates/appSample/lib/task/Task.Util.tsx +10 -13
  122. package/templates/appSample/lib/task/Task.View.tsx +2 -2
  123. package/templates/appSample/lib/task/Task.Zone.tsx +5 -2
  124. package/templates/appSample/lib/task/task.abstract.ts +12 -32
  125. package/templates/appSample/lib/task/task.signal.spec.ts +1 -15
  126. package/templates/appSample/lib/task/task.signal.test.ts +15 -1
  127. package/templates/appSample/lib/task/task.signal.ts +10 -8
  128. package/templates/appSample/page/task/[taskId]/_index.tsx +6 -6
  129. package/templates/appSample/page/task/[taskId]/edit.tsx +5 -4
  130. package/templates/appSample/page/task/_index.tsx +4 -3
  131. package/templates/appSample/page/task/_layout.tsx +1 -1
  132. package/templates/appSample/page/task/new.tsx +5 -4
  133. package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
  134. package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
  135. package/templates/appSample/ui/GlobalLoading.tsx +2 -1
  136. package/templates/appSample/ui/QuantityControl.tsx +3 -3
  137. package/templates/client.ts +7 -30
  138. package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
  139. package/templates/crudPages/[__model__Id]/page.tsx +2 -1
  140. package/templates/crudPages/new/page.tsx +2 -1
  141. package/templates/crudPages/page.tsx +2 -1
  142. package/templates/crudSinglePage/page.tsx +2 -1
  143. package/templates/lib/__lib/lib.dictionary.ts +0 -2
  144. package/templates/lib/__lib/lib.document.ts +0 -2
  145. package/templates/lib/sig.ts +8 -30
  146. package/templates/localDev/docker-compose.yaml.template +3 -14
  147. package/templates/module/__model__.signal.ts +12 -4
  148. package/templates/pkgRoot/tsconfig.json.template +2 -7
  149. package/templates/server.ts +1 -3
  150. package/templates/service/__model__.abstract.md.template +8 -0
  151. package/templates/workspaceRoot/.gitignore.template +0 -1
  152. package/templates/workspaceRoot/AGENTS.md.template +6 -0
  153. package/templates/workspaceRoot/package.json.template +1 -3
  154. package/tunnel.command-xwr46b66.js +129 -0
  155. package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
  156. package/typecheck.proc.js +1 -1
  157. package/workflow.command-t7wqe4h7.js +79 -0
  158. package/{workspace.command-06wvstgr.js → workspace.command-4derhzrh.js} +79 -109
  159. package/abstractCompactor-gmkc0z8j.js +0 -102
  160. package/application.command-18y0s485.js +0 -204
  161. package/applicationReleasePackager-4yasxwv9.js +0 -244
  162. package/capacitorApp-36rbr858.js +0 -12
  163. package/cloud.command-av97ndh6.js +0 -94
  164. package/cloud.script-cm11xjdz.js +0 -20
  165. package/context.command-5v37ef3n.js +0 -81
  166. package/devTuiView-0hc2emhe.js +0 -9
  167. package/getDirname-dr3jz1xg.js +0 -7
  168. package/guideline.command-4pz9g1rt.js +0 -360
  169. package/guidelines/agentRule/agentRule.generate.json +0 -31
  170. package/guidelines/componentRule/componentRule.generate.json +0 -31
  171. package/guidelines/conventions/conventions.generate.json +0 -25
  172. package/guidelines/cssRule/cssRule.generate.json +0 -31
  173. package/guidelines/docPageRule/docPageRule.generate.json +0 -31
  174. package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
  175. package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
  176. package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
  177. package/guidelines/enumConstant/enumConstant.generate.json +0 -31
  178. package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
  179. package/guidelines/fieldRule/fieldRule.generate.json +0 -31
  180. package/guidelines/framework/framework.generate.json +0 -32
  181. package/guidelines/mcpRule/mcpRule.generate.json +0 -35
  182. package/guidelines/modelConstant/modelConstant.generate.json +0 -32
  183. package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
  184. package/guidelines/modelDocument/modelDocument.generate.json +0 -31
  185. package/guidelines/modelService/modelService.generate.json +0 -31
  186. package/guidelines/modelSignal/modelSignal.generate.json +0 -31
  187. package/guidelines/modelStore/modelStore.generate.json +0 -31
  188. package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
  189. package/guidelines/modelUnit/modelUnit.generate.json +0 -31
  190. package/guidelines/modelUtil/modelUtil.generate.json +0 -31
  191. package/guidelines/modelView/modelView.generate.json +0 -31
  192. package/guidelines/modelZone/modelZone.generate.json +0 -31
  193. package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
  194. package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
  195. package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
  196. package/guidelines/queryRule/queryRule.generate.json +0 -31
  197. package/guidelines/recipeRule/recipeRule.generate.json +0 -31
  198. package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
  199. package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
  200. package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
  201. package/guidelines/scalarModule/scalarModule.generate.json +0 -31
  202. package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
  203. package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
  204. package/guidelines/ssrRule/ssrRule.generate.json +0 -31
  205. package/guidelines/transportRule/transportRule.generate.json +0 -31
  206. package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
  207. package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
  208. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
  209. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
  210. package/index-47pj3aee.js +0 -76
  211. package/index-5ystnsd5.js +0 -841
  212. package/index-6wwzs1bc.js +0 -7
  213. package/index-a17x49vg.js +0 -229
  214. package/index-m0xccmmm.js +0 -228
  215. package/index-meh558jy.js +0 -65
  216. package/index-rtxxan88.js +0 -471
  217. package/index-te54x9zm.js +0 -338
  218. package/index-w8kmx1b6.js +0 -2176
  219. package/index-wt7qrtd4.js +0 -1516
  220. package/templates/__scalar/__model__/__model__.abstract.md +0 -28
  221. package/templates/module/__model__.abstract.md +0 -28
  222. package/templates/service/__model__.abstract.md +0 -28
  223. package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +0 -62
  224. package/workflow.command-gtdnzvbz.js +0 -108
@@ -1,41 +1,41 @@
1
1
  // @bun
2
+ import {
3
+ ApplicationBuildReporter,
4
+ HmrWatcher,
5
+ WatchRootResolver
6
+ } from "./index-48k96p55.js";
2
7
  import {
3
8
  CodegenLock,
4
9
  WorkspaceExecutor,
5
10
  AppExecutor
6
- } from "./index-2tv3fdx4.js";
7
- import"./index-q0y2jq9j.js";
8
- import"./index-6wwzs1bc.js";
11
+ } from "./index-3w94e7n3.js";
12
+ import"./index-52w0fvh8.js";
9
13
  import {
10
14
  GraphClientEntryDiscovery,
11
15
  RouteClientBuilder,
12
16
  AutoImportSync,
17
+ ServerGraphFile,
18
+ CsrDevPatcher,
19
+ CsrDevBundler,
13
20
  DevChangePlanner,
14
- DevGeneratedIndexSync
15
- } from "./index-w8kmx1b6.js";
16
- import {
17
- HmrWatcher,
18
- WatchRootResolver
19
- } from "./index-hwkygn9s.js";
21
+ DevGeneratedIndexSync,
22
+ SsrDevBundler
23
+ } from "./index-6he6gvfg.js";
20
24
  import {
21
25
  prepareDevWatchBatch
22
26
  } from "./index-a5vmm6x4.js";
23
27
 
24
28
  // pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts
29
+ import { rm } from "fs/promises";
25
30
  import path2 from "path";
26
31
  import { Logger as Logger2 } from "akanjs/common";
32
+ import { CSR_DEV_PATCHING_MARKER } from "akanjs/server/hmr/csrDevManifest";
27
33
 
28
34
  // pkgs/@akanjs/devkit/incrementalBuilder/buildBatchRunner.ts
29
35
  import path from "path";
30
36
  import { Logger } from "akanjs/common";
31
37
 
32
38
  class BuildBatchRunner {
33
- static #entryCandidates = (workspaceRoot) => [
34
- path.join(workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
35
- path.join(workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
36
- path.join(import.meta.dir, "buildBatch.proc.js"),
37
- path.join(import.meta.dir, "buildBatch.proc.ts")
38
- ];
39
39
  #logger = new Logger("BuildBatchRunner");
40
40
  #entry = null;
41
41
  #workspaceRoot;
@@ -47,7 +47,12 @@ class BuildBatchRunner {
47
47
  async#resolveEntry() {
48
48
  if (this.#entry)
49
49
  return this.#entry;
50
- const candidates = BuildBatchRunner.#entryCandidates(this.#workspaceRoot);
50
+ const candidates = [
51
+ path.join(this.#workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
52
+ path.join(this.#workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
53
+ path.join(import.meta.dir, "buildBatch.proc.js"),
54
+ path.join(import.meta.dir, "buildBatch.proc.ts")
55
+ ];
51
56
  for (const candidate of candidates) {
52
57
  if (!await Bun.file(candidate).exists())
53
58
  continue;
@@ -62,7 +67,9 @@ class BuildBatchRunner {
62
67
  const started = Date.now();
63
68
  const entry = await this.#resolveEntry();
64
69
  let result = null;
65
- const proc = Bun.spawn(["bun", entry, JSON.stringify(request)], {
70
+ const reported = new Map;
71
+ const payload = request.trace ? { ...request, trace: { ...request.trace, spawnAt: Date.now() } } : request;
72
+ const proc = Bun.spawn(["bun", entry, JSON.stringify(payload)], {
66
73
  cwd: this.#cwd,
67
74
  env: process.env,
68
75
  stdio: ["ignore", "inherit", "inherit"],
@@ -72,8 +79,11 @@ class BuildBatchRunner {
72
79
  return;
73
80
  if (message.type === "build-batch-result")
74
81
  result = message.data;
75
- else
82
+ else {
83
+ if (message.type === "build-status")
84
+ reported.set(message.data.phase, message.data.message);
76
85
  onMessage(message);
86
+ }
77
87
  }
78
88
  });
79
89
  const exitCode = await proc.exited;
@@ -83,11 +93,14 @@ class BuildBatchRunner {
83
93
  }
84
94
  const message = proc.signalCode ? `build worker was killed by ${proc.signalCode} before reporting a result${proc.signalCode === "SIGKILL" ? " \u2014 most often the kernel OOM killer; check the sandbox's memory limit" : ""}` : `build worker exited with code ${exitCode} before reporting a result`;
85
95
  this.#logger.error(`[build-batch] generation=${request.generation} ${message}`);
86
- return {
87
- generation: request.generation,
88
- errors: Object.fromEntries(request.needs.map((need) => [need, message])),
89
- crashed: true
90
- };
96
+ const crashedNeeds = request.needs.filter((need) => !reported.has(need));
97
+ const failed = request.needs.flatMap((need) => {
98
+ if (!reported.has(need))
99
+ return [[need, message]];
100
+ const error = reported.get(need);
101
+ return error ? [[need, error]] : [];
102
+ });
103
+ return { generation: request.generation, errors: Object.fromEntries(failed), crashed: true, crashedNeeds };
91
104
  }
92
105
  }
93
106
 
@@ -124,6 +137,90 @@ class BuilderChannel {
124
137
  }
125
138
  }
126
139
 
140
+ // pkgs/@akanjs/devkit/incrementalBuilder/builderWorkQueue.ts
141
+ class BuilderWorkQueue {
142
+ #runBatch;
143
+ #onSettled;
144
+ #pending = [];
145
+ #running = null;
146
+ #idle = Promise.resolve();
147
+ #wakeIdle = null;
148
+ constructor({ runBatch, onSettled }) {
149
+ this.#runBatch = runBatch;
150
+ this.#onSettled = onSettled;
151
+ }
152
+ get size() {
153
+ return this.#pending.length + (this.#running ? 1 : 0);
154
+ }
155
+ enqueueBatch(batch) {
156
+ const tail = this.#pending.at(-1);
157
+ if (tail?.batch) {
158
+ tail.batch = BuilderWorkQueue.merge(tail.batch, batch);
159
+ return tail.settle.promise;
160
+ }
161
+ const job = this.#job("batch", batch, async () => {
162
+ return;
163
+ });
164
+ job.run = async () => await this.#runBatch(job.batch ?? batch);
165
+ return this.#push(job);
166
+ }
167
+ enqueue(label, run) {
168
+ let result;
169
+ const job = this.#job(label, null, async () => {
170
+ result = await run();
171
+ });
172
+ return this.#push(job).then(() => result);
173
+ }
174
+ async drain() {
175
+ while (this.size > 0)
176
+ await this.#idle;
177
+ }
178
+ static merge(into, next) {
179
+ const discovery = into.discovery || next.discovery ? {
180
+ files: [...new Set([...into.discovery?.files ?? [], ...next.discovery?.files ?? []])],
181
+ refresh: !!into.discovery?.refresh || !!next.discovery?.refresh,
182
+ generation: Math.max(into.discovery?.generation ?? 0, next.discovery?.generation ?? 0)
183
+ } : undefined;
184
+ return {
185
+ generation: Math.max(into.generation, next.generation),
186
+ needs: [...new Set([...into.needs, ...next.needs])],
187
+ changedFiles: [...new Set([...into.changedFiles, ...next.changedFiles])],
188
+ trace: next.trace ?? into.trace,
189
+ ...discovery ? { discovery } : {}
190
+ };
191
+ }
192
+ #job(label, batch, run) {
193
+ return { label, batch, run, settle: Promise.withResolvers() };
194
+ }
195
+ #push(job) {
196
+ if (this.size === 0)
197
+ this.#idle = new Promise((resolve) => {
198
+ this.#wakeIdle = resolve;
199
+ });
200
+ this.#pending.push(job);
201
+ if (!this.#running)
202
+ this.#pump();
203
+ return job.settle.promise;
204
+ }
205
+ async#pump() {
206
+ for (let job = this.#pending.shift();job; job = this.#pending.shift()) {
207
+ this.#running = job;
208
+ const started = Date.now();
209
+ try {
210
+ await job.run();
211
+ job.settle.resolve();
212
+ } catch (error) {
213
+ job.settle.reject(error);
214
+ } finally {
215
+ this.#running = null;
216
+ this.#onSettled?.(job.label, Date.now() - started);
217
+ }
218
+ }
219
+ this.#wakeIdle?.();
220
+ this.#wakeIdle = null;
221
+ }
222
+ }
223
+
127
224
  // pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts
128
225
  class IncrementalBuilder {
129
226
  #logger = new Logger2("IncrementalBuilder");
@@ -138,8 +235,29 @@ class IncrementalBuilder {
138
235
  #autoImportSync;
139
236
  #watcher = null;
140
237
  #generation = 0;
238
+ #discoveredThrough = 0;
141
239
  #csrActive = IncrementalBuilder.#csrArmedByEnv();
142
- #workQueue = Promise.resolve();
240
+ #csrBundler;
241
+ #patcher;
242
+ #csrGate = Promise.resolve();
243
+ #csrArming = false;
244
+ #csrArms = 0;
245
+ #crashRetryTimer = null;
246
+ #crashRetryEntries = new Set;
247
+ #csrWasActive = false;
248
+ #csrArmSucceeded = false;
249
+ #ssrBundler;
250
+ #ssrPatcher;
251
+ #ssrLock = Promise.resolve();
252
+ #delegations = new Map;
253
+ #ssrArming = null;
254
+ #ssrBroken = false;
255
+ #ssrCrashedAt = null;
256
+ static #crashRetryMs = 1e4;
257
+ #beside = new Set;
258
+ #barrelFailed = false;
259
+ #fastQueue = Promise.resolve();
260
+ #workQueue;
143
261
  #inFlight = 0;
144
262
  #workCount = 0;
145
263
  #shuttingDown = false;
@@ -153,6 +271,7 @@ class IncrementalBuilder {
153
271
  this.#optimizedFonts = options.optimizedFonts;
154
272
  this.#discovery = options.discovery;
155
273
  this.#generation = options.initialGeneration ?? 0;
274
+ this.#discoveredThrough = this.#generation;
156
275
  this.#batchRunner = new BuildBatchRunner({
157
276
  workspaceRoot: options.app.workspace.workspaceRoot,
158
277
  cwd: options.app.cwdPath
@@ -160,25 +279,41 @@ class IncrementalBuilder {
160
279
  this.#changePlanner = new DevChangePlanner({ workspaceRoot: options.app.workspace.workspaceRoot });
161
280
  this.#generatedIndexSync = new DevGeneratedIndexSync({ workspaceRoot: options.app.workspace.workspaceRoot });
162
281
  this.#autoImportSync = new AutoImportSync({ workspaceRoot: options.app.workspace.workspaceRoot });
282
+ this.#csrBundler = new CsrDevBundler(options.app);
283
+ this.#patcher = process.env.AKAN_DEV_CSR_PATCHER === "off" ? null : new CsrDevPatcher(this.#csrBundler, { resident: true });
284
+ this.#ssrBundler = new SsrDevBundler(options.app, { discovery: () => this.#discovery });
285
+ this.#ssrPatcher = process.env.AKAN_DEV_CSR_PATCHER === "off" ? null : new CsrDevPatcher(this.#ssrBundler, { resident: true });
286
+ this.#workQueue = new BuilderWorkQueue({
287
+ runBatch: async (batch) => await this.#runQueuedBatch(batch),
288
+ onSettled: (label, ms) => {
289
+ this.#workCount += 1;
290
+ this.#logger.verbose(`[work-queue] ${label} finished in ${ms}ms`);
291
+ this.#reportMetrics();
292
+ }
293
+ });
163
294
  }
164
295
  get #artifactDir() {
165
296
  return `${this.#app.cwdPath}/.akan/artifact`;
166
297
  }
167
298
  async handleBuildRoute(msg) {
168
- await this.#enqueueWork(`build-route:${msg.routeId}`, async () => BuilderChannel.send(await this.#handleBuildRoute(msg)));
299
+ await this.#workQueue.enqueue(`build-route:${msg.routeId}`, async () => BuilderChannel.send(await this.#handleBuildRoute(msg)));
169
300
  }
170
301
  async#handleBuildRoute(msg) {
302
+ const seenGeneration = this.#discoveredThrough;
171
303
  try {
172
304
  const delta = await new RouteClientBuilder({
173
305
  app: this.#app,
174
306
  routeId: msg.routeId,
175
307
  seeds: msg.seeds,
308
+ graphSeeds: msg.graphSeeds,
176
309
  artifact: this.#artifact,
177
310
  knownEntries: new Set(msg.knownEntries),
178
- discovery: this.#discovery
311
+ discovery: this.#discovery,
312
+ browser: "registry"
179
313
  }).build();
314
+ await this.#ensureSsrEntries(delta.registryEntries ?? []);
180
315
  this.#logger.verbose(`build-route ok routeId=${msg.routeId} newEntries=${delta.newEntries.length}`);
181
- this.#sendBuildStatus("route", { generation: msg.generation, ok: true, files: msg.seeds });
316
+ this.#sendBuildStatus("route", { generation: this.#generation, ok: true, files: msg.seeds, scope: msg.routeId });
182
317
  return {
183
318
  type: "build-route-res",
184
319
  id: msg.id,
@@ -191,35 +326,42 @@ class IncrementalBuilder {
191
326
  clientDeps: delta.clientDeps,
192
327
  clientDepsByEntry: delta.clientDepsByEntry,
193
328
  routeId: msg.routeId,
194
- generation: msg.generation
329
+ generation: msg.generation,
330
+ seenGeneration
195
331
  }
196
332
  };
197
333
  } catch (err) {
198
- const errMsg = err instanceof Error ? err.message : String(err);
334
+ const errMsg = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
199
335
  this.#logger.error(`build-route failed routeId=${msg.routeId}: ${errMsg}`);
200
- this.#sendBuildStatus("route", { generation: msg.generation, ok: false, files: msg.seeds, message: errMsg });
336
+ this.#sendBuildStatus("route", {
337
+ generation: this.#generation,
338
+ ok: false,
339
+ files: msg.seeds,
340
+ message: errMsg,
341
+ scope: msg.routeId
342
+ });
201
343
  return { type: "build-route-res", id: msg.id, ok: false, error: errMsg };
202
344
  }
203
345
  }
204
- #sendBuildStatus(phase, { generation, ok, files, message }) {
346
+ #sendBuildStatus(phase, {
347
+ generation,
348
+ ok,
349
+ files,
350
+ message,
351
+ scope
352
+ }) {
205
353
  if (typeof generation !== "number")
206
354
  return;
207
355
  BuilderChannel.emit({
208
356
  type: "build-status",
209
- data: {
210
- generation,
211
- phase,
212
- ok,
213
- files: files ?? [],
214
- message
215
- }
357
+ data: { generation, phase, ok, files: files ?? [], message, ...scope !== undefined ? { scope } : {} }
216
358
  });
217
359
  }
218
- async#enqueueWork(label, fn) {
360
+ async#enqueueFast(label, fn) {
219
361
  const started = Date.now();
220
362
  this.#inFlight += 1;
221
- const run = this.#workQueue.then(fn, fn);
222
- this.#workQueue = run.then(() => {
363
+ const run = this.#fastQueue.then(fn, fn);
364
+ this.#fastQueue = run.then(() => {
223
365
  return;
224
366
  }).catch(() => {
225
367
  return;
@@ -229,12 +371,22 @@ class IncrementalBuilder {
229
371
  } finally {
230
372
  this.#inFlight -= 1;
231
373
  this.#workCount += 1;
232
- this.#logger.verbose(`[work-queue] ${label} finished in ${Date.now() - started}ms`);
374
+ this.#logger.verbose(`[fast-queue] ${label} finished in ${Date.now() - started}ms`);
233
375
  this.#reportMetrics();
234
376
  }
235
377
  }
236
378
  get #idle() {
237
- return this.#inFlight === 0 && this.#cssRebuildTimer === null;
379
+ return this.#inFlight === 0 && this.#workQueue.size === 0 && this.#cssRebuildTimer === null;
380
+ }
381
+ #runBeside(work) {
382
+ this.#inFlight += 1;
383
+ const tracked = work.finally(() => {
384
+ this.#inFlight -= 1;
385
+ this.#beside.delete(tracked);
386
+ this.#reportMetrics();
387
+ });
388
+ this.#beside.add(tracked);
389
+ return tracked;
238
390
  }
239
391
  #reportMetrics() {
240
392
  if (!this.#idle || this.#shuttingDown)
@@ -256,12 +408,17 @@ class IncrementalBuilder {
256
408
  this.#cssRebuildTimer = null;
257
409
  this.#pendingCssRebuild = null;
258
410
  }
259
- await this.#workQueue.catch(() => {
411
+ await this.#fastQueue.catch(() => {
260
412
  return;
261
413
  });
414
+ await this.#workQueue.drain();
262
415
  await this.#cssRebuildQueue.catch(() => {
263
416
  return;
264
417
  });
418
+ while (this.#beside.size > 0)
419
+ await Promise.all([...this.#beside]);
420
+ if (this.#delegations.size > 0)
421
+ this.#logger.verbose(`registry saves handed to build workers this session: ${[...this.#delegations].map(([reason, count]) => `${reason}=${count}`).join(", ")}`);
265
422
  const flushed = await BuilderChannel.drain();
266
423
  this.#logger.debug(`drained in ${Date.now() - started}ms${flushed ? ` after flushing ${flushed} ipc write(s)` : ""}; exiting for recycle`);
267
424
  process.exit(0);
@@ -335,24 +492,24 @@ class IncrementalBuilder {
335
492
  }, 150);
336
493
  }
337
494
  async installWatcher() {
338
- const artifactDir = this.#artifactDir;
339
495
  const roots = await new WatchRootResolver(this.#app).resolve();
340
496
  const watcher = new HmrWatcher({
341
497
  roots,
342
498
  logger: this.#logger,
343
499
  onBatch: async (batch) => {
344
- await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(artifactDir, batch));
500
+ await this.#enqueueFast("hmr-batch", async () => this.#handleWatchBatch(batch));
345
501
  }
346
502
  });
347
503
  await watcher.start();
348
504
  this.#watcher = watcher;
349
505
  this.#logger.verbose(`watching ${roots.length} roots`);
350
506
  }
351
- async#handleWatchBatch(artifactDir, batch) {
507
+ async#handleWatchBatch(batch) {
352
508
  const rawKinds = new Set(batch.kinds);
353
509
  if (rawKinds.size === 0)
354
510
  return;
355
511
  const generation = ++this.#generation;
512
+ const trace = { ...batch.trace, batchAt: Date.now() };
356
513
  const [autoImport, indexSync] = await CodegenLock.run(this.#app.workspace.workspaceRoot, `hmr-batch:${this.#app.name}`, async () => {
357
514
  const auto = await this.#autoImportSync.syncForBatch(batch.files);
358
515
  return [auto, await this.#generatedIndexSync.syncForBatch(batch.files)];
@@ -371,22 +528,19 @@ class IncrementalBuilder {
371
528
  this.#logger.verbose(`[hmr] batch generation=${generation} kinds=${kinds.join(",")} files=${files.length} generated=${indexSync.changedFiles.length} roles=${devPlan.roles.join(",") || "(none)"} actions=${devPlan.actions.join(",") || "(none)"}`);
372
529
  for (const error of indexSync.errors)
373
530
  this.#logger.error(error);
374
- if (kinds.includes("code")) {
375
- const started = Date.now();
376
- if (kinds.includes("config"))
377
- this.#discovery = await GraphClientEntryDiscovery.create(this.#app);
378
- else
379
- this.#discovery.invalidate?.(files);
380
- this.#logger.verbose(`client-entry-discovery ${kinds.includes("config") ? "refreshed" : "invalidated"} (${Date.now() - started}ms)`);
381
- }
531
+ const discovery = kinds.includes("code") ? { files, refresh: kinds.includes("config"), generation } : undefined;
382
532
  if (hasSyncErrors) {
533
+ this.#barrelFailed = true;
383
534
  this.#sendBuildStatus("barrel", { generation, ok: false, files, message: indexSync.errors.join(`
384
535
  `) });
385
536
  BuilderChannel.emit(event);
537
+ if (discovery)
538
+ this.#workQueue.enqueue("discovery", async () => await this.#refreshDiscovery(discovery)).catch(this.#slowLaneFailed("discovery"));
386
539
  return;
387
540
  }
388
- if (indexSync.changedFiles.length > 0)
541
+ if (indexSync.changedFiles.length > 0 || this.#barrelFailed)
389
542
  this.#sendBuildStatus("barrel", { generation, ok: true, files });
543
+ this.#barrelFailed = false;
390
544
  const rebuildClient = devPlan.actions.includes("rebuild-client");
391
545
  if (kinds.includes("code") && !rebuildClient) {
392
546
  this.#logger.verbose(`client rebuild skipped; devPlan actions=${devPlan.actions.join(",") || "(none)"}`);
@@ -399,48 +553,318 @@ class IncrementalBuilder {
399
553
  } else if (kinds.includes("code") && rebuildClient && this.batchTouchesPagesTree(pageRoots, expandedBatch)) {
400
554
  this.#logger.verbose("pageKeys refresh skipped; changed page source cannot add/remove a route key");
401
555
  }
402
- const needs = [];
556
+ BuilderChannel.emit(event);
403
557
  if (kinds.includes("code") && rebuildClient) {
404
- if (this.#shouldRebuildCsr())
405
- needs.push("csr");
406
- else
558
+ const needs = ["pages", "css"];
559
+ if (!this.#csrActive)
407
560
  this.#logger.verbose(`csr-rebundle skipped; request /__csr or ?csr=true (or set AKAN_DEV_CSR_REBUILD=1) to enable per-save CSR rebuilds`);
408
- needs.push("pages");
409
- needs.push("css");
561
+ const patchCsr = async () => this.#csrActive ? await this.#patchCsr(generation, files, trace) : false;
562
+ const patchSsr = async () => await this.#patchSsr(generation, files, trace);
563
+ let csrToWorker;
564
+ let ssrToWorker;
565
+ if (this.#csrArming) {
566
+ ssrToWorker = await patchSsr();
567
+ csrToWorker = await patchCsr();
568
+ } else {
569
+ csrToWorker = await patchCsr();
570
+ ssrToWorker = await patchSsr();
571
+ }
572
+ if (csrToWorker)
573
+ needs.unshift("csr");
574
+ if (ssrToWorker)
575
+ needs.unshift("ssr");
576
+ const batch = { generation, needs, changedFiles: files, trace, ...discovery ? { discovery } : {} };
577
+ if (needs.includes("csr") || needs.includes("ssr"))
578
+ await this.#workQueue.enqueueBatch(batch);
579
+ else
580
+ this.#workQueue.enqueueBatch(batch).catch(this.#slowLaneFailed("batch"));
581
+ return;
410
582
  }
411
- BuilderChannel.emit(event);
412
- if (needs.length > 0)
413
- await this.#runBatch({ generation, needs, changedFiles: files });
414
- else if (kinds.includes("css")) {
583
+ if (discovery)
584
+ this.#workQueue.enqueue("discovery", async () => await this.#refreshDiscovery(discovery)).catch(this.#slowLaneFailed("discovery"));
585
+ if (kinds.includes("css")) {
415
586
  this.scheduleCssRebuild({ generation, changedFiles: files });
416
587
  this.#logger.verbose(`css-rebuild scheduled generation=${generation}`);
417
588
  }
418
589
  }
419
- async#runBatch({
420
- generation,
421
- needs,
422
- changedFiles
423
- }) {
590
+ async#patchCsr(generation, files, trace) {
591
+ const patcher = this.#patcher;
592
+ if (!patcher)
593
+ return true;
594
+ await this.#csrGate;
595
+ if (!this.#csrActive)
596
+ return false;
424
597
  const started = Date.now();
425
- const result = await this.#batchRunner.run(await this.#batchRequest({ generation, needs, changedFiles }), (msg) => BuilderChannel.emit(msg));
598
+ const marker = path2.join(this.#csrBundler.outDir, CSR_DEV_PATCHING_MARKER);
599
+ await Bun.write(marker, String(process.pid));
600
+ try {
601
+ const result = await patcher.update(files, {
602
+ announce: (update) => {
603
+ const now = Date.now();
604
+ BuilderChannel.emit({
605
+ type: "csr-updated",
606
+ data: {
607
+ generation: update.generation,
608
+ mode: "registry",
609
+ reload: update.reload,
610
+ reason: update.reason,
611
+ patchUrl: update.patchUrl,
612
+ changedIds: update.changedIds,
613
+ trace: { ...trace, patchAt: now, sentAt: now }
614
+ }
615
+ });
616
+ }
617
+ });
618
+ if (result.kind === "delegate") {
619
+ this.#logger.verbose(`csr-patch handed to a build worker (${this.#countDelegation("csr", result.reason)}): ${result.reason}`);
620
+ return true;
621
+ }
622
+ this.#sendBuildStatus("csr", { generation, ok: true, files });
623
+ this.#logger.verbose(result.kind === "unchanged" ? `csr-patch unchanged (${Date.now() - started}ms)` : `csr-patch generation=${result.update.generation} ${result.update.reload ? `reload (${result.update.reason})` : `patch modules=${result.update.changedIds.length}`} (${Date.now() - started}ms)`);
624
+ return false;
625
+ } catch (err) {
626
+ const message = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
627
+ if (IncrementalBuilder.#isCompileError(err)) {
628
+ this.#sendBuildStatus("csr", { generation, ok: false, files, message });
629
+ return false;
630
+ }
631
+ this.#logger.verbose(`csr-patch threw; a build worker takes this save: ${message}`);
632
+ patcher.forget();
633
+ return true;
634
+ } finally {
635
+ await rm(marker, { force: true });
636
+ }
637
+ }
638
+ async#patchSsr(generation, files, trace) {
639
+ const patcher = this.#ssrPatcher;
640
+ if (!patcher)
641
+ return true;
642
+ const hold = await ServerGraphFile.touches(await ServerGraphFile.read(this.#artifactDir), files, (file) => ServerGraphFile.clientExportsOf(file));
643
+ this.#discovery.invalidate?.(files);
644
+ return await this.#withSsrLock(async () => await this.#runSsrPatcher(patcher, files, { trace, hold, batchGeneration: generation }));
645
+ }
646
+ async#ensureSsrEntries(entries, { crashRetry = false } = {}) {
647
+ if (entries.length === 0)
648
+ return;
649
+ const arming = this.#ssrArming;
650
+ if (arming) {
651
+ this.#runBeside(arming.then(async () => await this.#ensureSsrEntries(entries, { crashRetry })).catch(this.#slowLaneFailed("ssr-ensure")));
652
+ return;
653
+ }
654
+ await this.#withSsrLock(async () => {
655
+ const patcher = this.#ssrPatcher;
656
+ const generation = this.#generation;
657
+ if (patcher && !await this.#runSsrPatcher(patcher, [], { roots: entries, onlyRoots: true, batchGeneration: generation }))
658
+ return;
659
+ if (!patcher && await this.#ssrBundler.holds(entries))
660
+ return;
661
+ if (this.#ssrBroken)
662
+ return;
663
+ if (this.#ssrCrashedAt !== null && Date.now() - this.#ssrCrashedAt < IncrementalBuilder.#crashRetryMs) {
664
+ patcher?.releaseHandedOver();
665
+ this.#retryAfterCrash(this.#ssrCrashedAt, entries);
666
+ return;
667
+ }
668
+ await this.#runSsrWorker(generation).catch((err) => {
669
+ this.#logger.error(`ssr-registry could not take a route's entries; the save that fixes it rebuilds it: ${ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot)}`);
670
+ });
671
+ if (this.#ssrCrashedAt !== null && !crashRetry)
672
+ this.#retryAfterCrash(this.#ssrCrashedAt, entries);
673
+ });
674
+ }
675
+ #retryAfterCrash(crashedAt, entries) {
676
+ for (const entry of entries)
677
+ this.#crashRetryEntries.add(entry);
678
+ if (this.#crashRetryTimer)
679
+ return;
680
+ const timer = setTimeout(() => {
681
+ this.#crashRetryTimer = null;
682
+ const retried = [...this.#crashRetryEntries];
683
+ this.#crashRetryEntries.clear();
684
+ if (this.shuttingDown || retried.length === 0)
685
+ return;
686
+ this.#runBeside(this.#ensureSsrEntries(retried, { crashRetry: true })).catch(this.#slowLaneFailed("ssr-crash-retry"));
687
+ }, Math.max(0, crashedAt + IncrementalBuilder.#crashRetryMs - Date.now()));
688
+ timer.unref?.();
689
+ this.#crashRetryTimer = timer;
690
+ }
691
+ async armSsrRegistry() {
692
+ const delayMs = Number(process.env.AKAN_DEV_SSR_ARM_DELAY_MS);
693
+ if (Number.isInteger(delayMs) && delayMs > 0)
694
+ await Bun.sleep(delayMs);
695
+ const arm = async () => await this.#withSsrLock(async () => {
696
+ const generation = this.#generation;
697
+ if (this.#ssrPatcher && !await this.#runSsrPatcher(this.#ssrPatcher, [], { batchGeneration: generation }))
698
+ return;
699
+ await this.#runSsrWorker(generation);
700
+ }).catch((err) => {
701
+ this.#logger.error(`ssr-registry boot build failed; the next save retries it: ${ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot)}`);
702
+ });
703
+ const arming = this.#runBeside((async () => {
704
+ await arm();
705
+ const crashedAt = this.#ssrCrashedAt;
706
+ if (crashedAt === null)
707
+ return;
708
+ while (!this.shuttingDown && Date.now() < crashedAt + IncrementalBuilder.#crashRetryMs)
709
+ await Bun.sleep(250);
710
+ if (!this.shuttingDown)
711
+ await arm();
712
+ })());
713
+ this.#ssrArming = arming;
714
+ await arming;
715
+ this.#ssrArming = null;
716
+ }
717
+ static #isCompileError(error) {
718
+ return error instanceof AggregateError;
719
+ }
720
+ #slowLaneFailed(label) {
721
+ return (error) => this.#logger.error(`${label} failed in the slow lane: ${ApplicationBuildReporter.formatError(error, this.#app.workspace.workspaceRoot)}`);
722
+ }
723
+ #countDelegation(registry, reason) {
724
+ const key = `${registry}:${reason.split(":")[0]}`;
725
+ const count = (this.#delegations.get(key) ?? 0) + 1;
726
+ this.#delegations.set(key, count);
727
+ return `#${count} for this reason`;
728
+ }
729
+ async#withSsrLock(fn) {
730
+ const run = this.#ssrLock.then(fn, fn);
731
+ this.#ssrLock = run.then(() => {
732
+ return;
733
+ }, () => {
734
+ return;
735
+ });
736
+ return await run;
737
+ }
738
+ async#runSsrWorker(generation) {
739
+ let result;
740
+ try {
741
+ result = await this.#runBatch({ generation, needs: ["ssr"], changedFiles: [] });
742
+ } finally {
743
+ this.#ssrPatcher?.forget();
744
+ }
745
+ this.#noteSsrWorker(result);
746
+ if (result.errors.ssr)
747
+ throw new Error(result.errors.ssr);
748
+ }
749
+ #noteSsrWorker(result) {
750
+ const crashed = !!result.crashed && (result.crashedNeeds ?? ["ssr"]).includes("ssr");
751
+ this.#ssrBroken = !!result.errors.ssr && !crashed;
752
+ this.#ssrCrashedAt = crashed ? Date.now() : null;
753
+ }
754
+ async#runSsrPatcher(patcher, files, {
755
+ roots,
756
+ onlyRoots,
757
+ trace,
758
+ hold,
759
+ batchGeneration
760
+ } = {}) {
761
+ const started = Date.now();
762
+ const marker = path2.join(this.#ssrBundler.outDir, CSR_DEV_PATCHING_MARKER);
763
+ await Bun.write(marker, String(process.pid));
764
+ try {
765
+ const result = await patcher.update(files, {
766
+ roots,
767
+ onlyRoots,
768
+ announce: (update) => {
769
+ const now = Date.now();
770
+ BuilderChannel.emit({
771
+ type: "ssr-updated",
772
+ data: {
773
+ generation: update.generation,
774
+ reload: update.reload,
775
+ reason: update.reason,
776
+ patchUrl: update.patchUrl,
777
+ changedIds: update.changedIds,
778
+ ...trace ? { trace: { ...trace, patchAt: now, sentAt: now } } : {},
779
+ ...hold ? { hold } : {},
780
+ ...batchGeneration !== undefined ? { batchGeneration } : {},
781
+ ...update.epoch !== undefined ? { epoch: update.epoch } : {},
782
+ ...update.first ? { first: true } : {}
783
+ }
784
+ });
785
+ }
786
+ });
787
+ if (result.kind === "delegate") {
788
+ this.#logger.verbose(`ssr-patch handed to a build worker (${this.#countDelegation("ssr", result.reason)}): ${result.reason}`);
789
+ return true;
790
+ }
791
+ if (!onlyRoots) {
792
+ this.#ssrBroken = false;
793
+ this.#ssrCrashedAt = null;
794
+ this.#sendBuildStatus("ssr", { generation: batchGeneration, ok: true, files });
795
+ }
796
+ this.#logger.verbose(result.kind === "unchanged" ? `ssr-patch unchanged (${Date.now() - started}ms)` : `ssr-patch generation=${result.update.generation} ${result.update.reload ? `reload (${result.update.reason})` : `patch modules=${result.update.changedIds.length}`} (${Date.now() - started}ms)`);
797
+ return false;
798
+ } catch (err) {
799
+ const message = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
800
+ if (IncrementalBuilder.#isCompileError(err)) {
801
+ this.#sendBuildStatus("ssr", { generation: batchGeneration, ok: false, files, message });
802
+ return false;
803
+ }
804
+ this.#logger.verbose(`ssr-patch threw; a build worker takes it: ${message}`);
805
+ patcher.forget();
806
+ return true;
807
+ } finally {
808
+ await rm(marker, { force: true });
809
+ }
810
+ }
811
+ async#runQueuedBatch({ discovery, ...work }) {
812
+ if (discovery)
813
+ await this.#refreshDiscovery(discovery);
814
+ this.#discoveredThrough = Math.max(this.#discoveredThrough, work.generation);
815
+ try {
816
+ if (work.needs.includes("ssr"))
817
+ await this.#withSsrLock(async () => {
818
+ try {
819
+ this.#noteSsrWorker(await this.#runBatch(work));
820
+ } finally {
821
+ this.#ssrPatcher?.forget();
822
+ }
823
+ });
824
+ else
825
+ await this.#runBatch(work);
826
+ } finally {
827
+ if (work.needs.includes("csr"))
828
+ this.#patcher?.forget();
829
+ }
830
+ }
831
+ async#refreshDiscovery({ files, refresh, generation }) {
832
+ const started = Date.now();
833
+ if (refresh)
834
+ this.#discovery = await GraphClientEntryDiscovery.create(this.#app);
835
+ else
836
+ this.#discovery.invalidate?.(files);
837
+ this.#discoveredThrough = Math.max(this.#discoveredThrough, generation);
838
+ this.#logger.verbose(`client-entry-discovery ${refresh ? "refreshed" : "invalidated"} (${Date.now() - started}ms)`);
839
+ }
840
+ async#runBatch(work) {
841
+ const { generation, needs, changedFiles } = work;
842
+ const started = Date.now();
843
+ const result = await this.#batchRunner.run(await this.#batchRequest(work), (msg) => BuilderChannel.emit(msg));
426
844
  if (result.optimizedFonts)
427
845
  this.#optimizedFonts = result.optimizedFonts;
428
846
  if (result.cssAssets)
429
847
  this.#artifact = { ...this.#artifact, cssAssets: result.cssAssets };
430
848
  if (result.crashed) {
431
- for (const need of needs)
849
+ const crashedNeeds = result.crashedNeeds ?? needs;
850
+ if (crashedNeeds.includes("csr"))
851
+ this.#patcher?.releaseHandedOver();
852
+ if (crashedNeeds.includes("ssr"))
853
+ this.#ssrPatcher?.releaseHandedOver();
854
+ for (const need of crashedNeeds)
432
855
  if (need !== "base")
433
- this.#sendBuildStatus(need, { generation, ok: false, files: changedFiles, message: result.errors[need] });
856
+ this.#sendBuildStatus(need, {
857
+ generation,
858
+ ok: false,
859
+ files: changedFiles,
860
+ message: `${result.errors[need] ?? "the build worker crashed"}; the next save builds it again`
861
+ });
434
862
  }
435
863
  if (needs.includes("css"))
436
864
  this.#logger.verbose(`css-rebuild checked (${Date.now() - started}ms)`);
437
865
  return result;
438
866
  }
439
- async#batchRequest({
440
- generation,
441
- needs,
442
- changedFiles
443
- }) {
867
+ async#batchRequest({ generation, needs, changedFiles, trace }) {
444
868
  return {
445
869
  appName: this.#app.name,
446
870
  workspaceRoot: this.#app.workspace.workspaceRoot,
@@ -451,7 +875,8 @@ class IncrementalBuilder {
451
875
  pageKeys: await this.#app.getPageKeys(),
452
876
  optimizedFonts: this.#optimizedFonts,
453
877
  cssAssets: this.#artifact.cssAssets ?? null,
454
- artifactDir: path2.resolve(this.#artifactDir)
878
+ artifactDir: path2.resolve(this.#artifactDir),
879
+ ...trace ? { trace } : {}
455
880
  };
456
881
  }
457
882
  async boot() {
@@ -462,7 +887,7 @@ class IncrementalBuilder {
462
887
  }
463
888
  async announceRecoveredState(changedFiles) {
464
889
  const generation = ++this.#generation;
465
- await this.#enqueueWork("boot-recovered", async () => {
890
+ await this.#workQueue.enqueue("boot-recovered", async () => {
466
891
  await this.#runBatch({ generation, needs: ["pages", "css"], changedFiles });
467
892
  });
468
893
  }
@@ -491,22 +916,47 @@ class IncrementalBuilder {
491
916
  this.#logger.verbose(`announced boot state after recycle generation=${generation}`);
492
917
  }
493
918
  async handleBuildCsr(msg) {
494
- await this.#enqueueWork("build-csr", async () => {
919
+ this.#beginCsrArm();
920
+ this.#csrActive = true;
921
+ const armed = this.#workQueue.enqueue("build-csr", async () => {
495
922
  const started = Date.now();
496
- const result = await this.#batchRunner.run(await this.#batchRequest({ generation: this.#generation, needs: ["csr"], changedFiles: [] }));
497
- const error = result.errors.csr;
923
+ let error;
924
+ try {
925
+ const result = await this.#batchRunner.run(await this.#batchRequest({ generation: this.#generation, needs: ["csr"], changedFiles: [] }));
926
+ error = result.errors.csr;
927
+ } catch (err) {
928
+ error = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
929
+ }
498
930
  if (error) {
499
931
  this.#logger.error(`csr-build failed: ${error}`);
500
932
  await BuilderChannel.send({ type: "build-csr-res", id: msg.id, ok: false, error });
501
933
  return;
502
934
  }
503
- this.#csrActive = true;
935
+ this.#csrArmSucceeded = true;
936
+ this.#patcher?.forget();
504
937
  this.#logger.info(`csr-build ok on demand (${Date.now() - started}ms); rebuilding CSR on every save now`);
505
938
  await BuilderChannel.send({ type: "build-csr-res", id: msg.id, ok: true });
506
939
  });
940
+ this.#csrGate = this.#armingCsr(armed);
941
+ await armed;
507
942
  }
508
- #shouldRebuildCsr() {
509
- return this.#csrActive;
943
+ #beginCsrArm() {
944
+ if (this.#csrArms === 0) {
945
+ this.#csrWasActive = this.#csrActive;
946
+ this.#csrArmSucceeded = false;
947
+ }
948
+ this.#csrArms += 1;
949
+ this.#csrArming = true;
950
+ }
951
+ #armingCsr(armed) {
952
+ const settle = () => {
953
+ this.#csrArms -= 1;
954
+ if (this.#csrArms > 0)
955
+ return;
956
+ this.#csrArming = false;
957
+ this.#csrActive = this.#csrWasActive || this.#csrArmSucceeded;
958
+ };
959
+ return armed.then(settle, settle);
510
960
  }
511
961
  static #csrArmedByEnv() {
512
962
  return process.env.AKAN_DEV_CSR_REBUILD === "1";
@@ -534,19 +984,24 @@ class IncrementalBuilder {
534
984
  async rearmCsrFromEnv() {
535
985
  if (!IncrementalBuilder.#csrArmedByEnv())
536
986
  return;
987
+ this.#beginCsrArm();
537
988
  this.#csrActive = true;
538
- await this.#enqueueWork("build-csr-rearm", async () => {
539
- const result = await this.#batchRunner.run(await this.#batchRequest({ generation: this.#generation, needs: ["csr"], changedFiles: [] }));
989
+ this.#csrArmSucceeded = true;
990
+ const rearmed = this.#workQueue.enqueue("build-csr-rearm", async () => {
991
+ const result = await this.#batchRunner.run(await this.#batchRequest({ generation: this.#generation, needs: ["csr"], changedFiles: [] }), (msg) => BuilderChannel.emit(msg));
992
+ this.#patcher?.forget();
540
993
  if (result.errors.csr)
541
994
  this.#logger.error(`csr-rearm failed: ${result.errors.csr}`);
542
995
  else
543
996
  this.#logger.verbose("csr-rearm ok; this session had CSR armed before the builder restarted");
544
997
  });
998
+ this.#csrGate = this.#armingCsr(rearmed);
999
+ await rearmed.catch((err) => this.#logger.error(`csr-rearm failed: ${ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot)}`));
545
1000
  }
546
- static #recoverBoot(app, bootError, logger, runner) {
1001
+ static #recoverBoot(app, bootError, logger, runner, initialGeneration) {
547
1002
  const firstMessage = bootError instanceof Error ? bootError.message : String(bootError);
548
1003
  logger.error(`boot build failed; entering degraded watch mode until the error is fixed: ${firstMessage}`);
549
- let generation = 0;
1004
+ let generation = initialGeneration;
550
1005
  const sendFailure = (files, message) => {
551
1006
  BuilderChannel.emit({
552
1007
  type: "build-status",
@@ -555,6 +1010,7 @@ class IncrementalBuilder {
555
1010
  };
556
1011
  sendFailure([], firstMessage);
557
1012
  BuilderChannel.emit({ type: "builder-ready" });
1013
+ BuilderChannel.emit({ type: "boot-armed" });
558
1014
  return new Promise((resolve, reject) => {
559
1015
  (async () => {
560
1016
  const roots = await new WatchRootResolver(app).resolve();
@@ -606,23 +1062,15 @@ class IncrementalBuilder {
606
1062
  builder.shutdown(msg.reason);
607
1063
  return;
608
1064
  }
609
- if (msg.type === "build-route") {
610
- const error = builder?.shuttingDown ? recyclingError : bootingError;
611
- if (!builder || builder.shuttingDown) {
612
- BuilderChannel.emit({ type: "build-route-res", id: msg.id, ok: false, error });
613
- return;
614
- }
615
- builder.handleBuildRoute(msg);
1065
+ if (msg.type !== "build-route" && msg.type !== "build-csr")
616
1066
  return;
617
- }
618
- if (msg.type === "build-csr") {
1067
+ if (!builder || builder.shuttingDown) {
619
1068
  const error = builder?.shuttingDown ? recyclingError : bootingError;
620
- if (!builder || builder.shuttingDown) {
621
- BuilderChannel.emit({ type: "build-csr-res", id: msg.id, ok: false, error });
622
- return;
623
- }
624
- builder.handleBuildCsr(msg);
1069
+ BuilderChannel.emit({ type: `${msg.type}-res`, id: msg.id, ok: false, error });
1070
+ return;
625
1071
  }
1072
+ const handled = msg.type === "build-route" ? builder.handleBuildRoute(msg) : builder.handleBuildCsr(msg);
1073
+ handled.catch((err) => logger.error(`${msg.type} failed: ${err instanceof Error ? err.message : String(err)}`));
626
1074
  });
627
1075
  process.on("disconnect", () => {
628
1076
  logger.warn("host IPC channel closed; exiting builder");
@@ -630,12 +1078,14 @@ class IncrementalBuilder {
630
1078
  });
631
1079
  let recoveredFiles = null;
632
1080
  const bootRunner = new BuildBatchRunner({ workspaceRoot, cwd: app.cwdPath });
1081
+ const initialGeneration = IncrementalBuilder.#initialGeneration();
633
1082
  try {
634
- builder = new IncrementalBuilder({ app, watch, ...await IncrementalBuilder.#buildBootDeps(app, bootRunner) });
1083
+ const deps = await IncrementalBuilder.#buildBootDeps(app, bootRunner);
1084
+ builder = new IncrementalBuilder({ app, watch, initialGeneration, ...deps });
635
1085
  } catch (err) {
636
1086
  if (!watch)
637
1087
  throw err;
638
- const recovered = await IncrementalBuilder.#recoverBoot(app, err, logger, bootRunner);
1088
+ const recovered = await IncrementalBuilder.#recoverBoot(app, err, logger, bootRunner, initialGeneration);
639
1089
  builder = recovered.builder;
640
1090
  recoveredFiles = recovered.changedFiles;
641
1091
  }
@@ -644,10 +1094,15 @@ class IncrementalBuilder {
644
1094
  await builder.announceRecoveredState(recoveredFiles);
645
1095
  else if (process.env.AKAN_BUILDER_ANNOUNCE_BOOT === "1")
646
1096
  await builder.announceBootState();
647
- await builder.rearmCsrFromEnv();
1097
+ await Promise.all([builder.rearmCsrFromEnv(), builder.armSsrRegistry()]);
1098
+ BuilderChannel.emit({ type: "boot-armed" });
1099
+ }
1100
+ static #initialGeneration() {
1101
+ const generation = Number(process.env.AKAN_BUILDER_INITIAL_GENERATION);
1102
+ return Number.isInteger(generation) && generation > 0 ? generation + 1 : 0;
648
1103
  }
649
1104
  }
650
- IncrementalBuilder.main().catch((err) => {
1105
+ await IncrementalBuilder.main().catch((err) => {
651
1106
  console.error(err);
652
1107
  process.exit(1);
653
1108
  });