@akanjs/cli 2.4.1-rc.2 → 2.4.1-rc.4

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 (44) hide show
  1. package/.build-stamp +1 -0
  2. package/{agent.command-h4afc69n.js → agent.command-t4346579.js} +6 -6
  3. package/{application.command-47mj9qsy.js → application.command-zkmeemdt.js} +5 -4
  4. package/{applicationBuildRunner-yz144508.js → applicationBuildRunner-5w8j9yp7.js} +2 -1
  5. package/buildBatch.proc.js +176 -0
  6. package/{capacitorApp-y0h6cgft.js → capacitorApp-wnxzrppx.js} +2 -2
  7. package/{cloud.command-hxcgsf80.js → cloud.command-5ztqxppm.js} +9 -8
  8. package/{context.command-nqbtak4f.js → context.command-smwxwgwf.js} +14 -14
  9. package/{guideline.command-ya0dh44f.js → guideline.command-1r0esdjs.js} +3 -3
  10. package/incrementalBuilder.proc.js +281 -160
  11. package/index-0fn1r7gg.js +466 -0
  12. package/{index-qhtr07v8.js → index-0jwvs8vp.js} +19 -19
  13. package/{index-a6sbyy0b.js → index-0t7pwff2.js} +3 -0
  14. package/{index-h6ca6qg0.js → index-2v10yr8g.js} +1 -1
  15. package/{index-73pr2cmy.js → index-2v6a4wd5.js} +4 -4
  16. package/{index-45aj5ry0.js → index-3sd2vape.js} +3 -3
  17. package/{index-y3hdhy4p.js → index-ahpcr9ss.js} +5 -5
  18. package/{index-8rc0bm04.js → index-eay4t1te.js} +2 -2
  19. package/{index-qaq13qk3.js → index-eqxmxan6.js} +1 -1
  20. package/{index-pmm9e2jf.js → index-fh990y67.js} +2 -2
  21. package/{index-sgmas1fc.js → index-gr4cjv99.js} +5 -5
  22. package/{index-1xdrsbry.js → index-hk58kr1m.js} +1 -1
  23. package/{index-85msc0wg.js → index-jbtn8h1y.js} +3 -3
  24. package/{index-a5rmdgy4.js → index-khzzttv1.js} +1362 -1447
  25. package/{index-77crfweb.js → index-n5y2gbf2.js} +478 -20
  26. package/{index-n6h3482q.js → index-nv7et4t3.js} +2 -2
  27. package/{index-hwzpw9c1.js → index-pya1h7wx.js} +3 -3
  28. package/{index-hdqztm58.js → index-qpz4csbs.js} +1 -1
  29. package/{index-mq6ns0f9.js → index-s9yajzkj.js} +9 -9
  30. package/{index-8pkbzj26.js → index-wh201a76.js} +7 -7
  31. package/{index-swf4bmbg.js → index-y4kg682s.js} +1 -1
  32. package/index.js +18 -18
  33. package/{library.command-r15zdqvp.js → library.command-jrew04m6.js} +3 -3
  34. package/{localRegistry.command-6z4s13mj.js → localRegistry.command-m08dkm1b.js} +8 -7
  35. package/{module.command-qrj3kmyz.js → module.command-p98t7522.js} +6 -6
  36. package/{package.command-5x5m0ej1.js → package.command-57dyrfmn.js} +3 -3
  37. package/package.json +2 -2
  38. package/{page.command-c6xdx0xm.js → page.command-tas6f3na.js} +3 -3
  39. package/{primitive.command-pv9ssmtf.js → primitive.command-bjrbsakw.js} +7 -7
  40. package/{quality.command-es67wvdp.js → quality.command-zavaafbw.js} +2 -2
  41. package/{repair.command-677675vw.js → repair.command-je57wx67.js} +5 -5
  42. package/{scalar.command-kabkd6wd.js → scalar.command-gqqyy1kr.js} +5 -5
  43. package/{workflow.command-64r6cw0w.js → workflow.command-d6mrc47s.js} +10 -10
  44. package/{workspace.command-875aj35r.js → workspace.command-bbg96z8k.js} +24 -23
@@ -5,41 +5,129 @@ import {
5
5
  import {
6
6
  AppExecutor,
7
7
  WorkspaceExecutor
8
- } from "./index-a6sbyy0b.js";
8
+ } from "./index-0t7pwff2.js";
9
+ import"./index-61keag0s.js";
9
10
  import {
10
11
  AutoImportSync,
11
- CsrArtifactBuilder,
12
12
  DevChangePlanner,
13
13
  DevGeneratedIndexSync,
14
- FontOptimizer,
15
14
  GraphClientEntryDiscovery,
16
- HmrWatcher,
17
- PagesBundleBuilder,
18
15
  RouteClientBuilder,
19
- SsrBaseArtifactBuilder,
16
+ SsrBaseArtifactBuilder
17
+ } from "./index-khzzttv1.js";
18
+ import {
19
+ HmrWatcher,
20
20
  WatchRootResolver
21
- } from "./index-a5rmdgy4.js";
22
- import"./index-61keag0s.js";
21
+ } from "./index-0fn1r7gg.js";
23
22
  import"./index-6pz1j0zj.js";
24
23
  import"./index-r24hmh0q.js";
25
24
 
26
25
  // pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts
26
+ import path2 from "path";
27
+ import { Logger as Logger2 } from "akanjs/common";
28
+
29
+ // pkgs/@akanjs/devkit/incrementalBuilder/buildBatchRunner.ts
27
30
  import path from "path";
28
31
  import { Logger } from "akanjs/common";
32
+
33
+ class BuildBatchRunner {
34
+ static #entryCandidates = (workspaceRoot) => [
35
+ path.join(workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
36
+ path.join(workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
37
+ path.join(import.meta.dir, "buildBatch.proc.js"),
38
+ path.join(import.meta.dir, "buildBatch.proc.ts")
39
+ ];
40
+ #logger = new Logger("BuildBatchRunner");
41
+ #entry = null;
42
+ #workspaceRoot;
43
+ #cwd;
44
+ constructor({ workspaceRoot, cwd }) {
45
+ this.#workspaceRoot = workspaceRoot;
46
+ this.#cwd = cwd;
47
+ }
48
+ async#resolveEntry() {
49
+ if (this.#entry)
50
+ return this.#entry;
51
+ const candidates = BuildBatchRunner.#entryCandidates(this.#workspaceRoot);
52
+ for (const candidate of candidates) {
53
+ if (!await Bun.file(candidate).exists())
54
+ continue;
55
+ this.#entry = candidate;
56
+ return candidate;
57
+ }
58
+ throw new Error(`[build-batch] worker entry not found; looked in: ${candidates.join(", ")}`);
59
+ }
60
+ async run(request, onMessage = () => {
61
+ return;
62
+ }) {
63
+ const started = Date.now();
64
+ const entry = await this.#resolveEntry();
65
+ let result = null;
66
+ const proc = Bun.spawn(["bun", entry, JSON.stringify(request)], {
67
+ cwd: this.#cwd,
68
+ env: process.env,
69
+ stdio: ["ignore", "inherit", "inherit"],
70
+ serialization: "advanced",
71
+ ipc: (message2) => {
72
+ if (!message2 || typeof message2 !== "object")
73
+ return;
74
+ if (message2.type === "build-batch-result")
75
+ result = message2.data;
76
+ else
77
+ onMessage(message2);
78
+ }
79
+ });
80
+ const exitCode = await proc.exited;
81
+ if (result) {
82
+ this.#logger.verbose(`[build-batch] generation=${request.generation} needs=${request.needs.join(",")} done in ${Date.now() - started}ms`);
83
+ return result;
84
+ }
85
+ const message = `build worker exited with code ${exitCode} before reporting a result`;
86
+ this.#logger.error(`[build-batch] generation=${request.generation} ${message}`);
87
+ return {
88
+ generation: request.generation,
89
+ errors: Object.fromEntries(request.needs.map((need) => [need, message])),
90
+ crashed: true
91
+ };
92
+ }
93
+ }
94
+
95
+ // pkgs/@akanjs/devkit/incrementalBuilder/builderReply.ts
96
+ class BuilderReply {
97
+ static #flushTimeoutMs = 5000;
98
+ static async send(res) {
99
+ const send = process.send;
100
+ if (!send)
101
+ return;
102
+ await new Promise((resolve) => {
103
+ const timer = setTimeout(resolve, BuilderReply.#flushTimeoutMs);
104
+ send.call(process, res, undefined, undefined, () => {
105
+ clearTimeout(timer);
106
+ resolve();
107
+ });
108
+ });
109
+ }
110
+ }
111
+
112
+ // pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts
29
113
  class IncrementalBuilder {
30
- #logger = new Logger("IncrementalBuilder");
114
+ #logger = new Logger2("IncrementalBuilder");
31
115
  #app;
32
116
  #artifact;
33
117
  #watch;
34
- #cssCompiler;
35
118
  #optimizedFonts;
36
119
  #discovery;
120
+ #batchRunner;
37
121
  #changePlanner;
38
122
  #generatedIndexSync;
39
123
  #autoImportSync;
124
+ #watcher = null;
40
125
  #generation = 0;
41
126
  #csrActive = IncrementalBuilder.#csrArmedByEnv();
42
127
  #workQueue = Promise.resolve();
128
+ #inFlight = 0;
129
+ #workCount = 0;
130
+ #shuttingDown = false;
43
131
  #cssRebuildQueue = Promise.resolve();
44
132
  #cssRebuildTimer = null;
45
133
  #pendingCssRebuild = null;
@@ -47,10 +135,13 @@ class IncrementalBuilder {
47
135
  this.#app = options.app;
48
136
  this.#artifact = options.artifact;
49
137
  this.#watch = options.watch;
50
- this.#cssCompiler = options.cssCompiler;
51
138
  this.#optimizedFonts = options.optimizedFonts;
52
139
  this.#discovery = options.discovery;
53
140
  this.#generation = options.initialGeneration ?? 0;
141
+ this.#batchRunner = new BuildBatchRunner({
142
+ workspaceRoot: options.app.workspace.workspaceRoot,
143
+ cwd: options.app.cwdPath
144
+ });
54
145
  this.#changePlanner = new DevChangePlanner({ workspaceRoot: options.app.workspace.workspaceRoot });
55
146
  this.#generatedIndexSync = new DevGeneratedIndexSync({ workspaceRoot: options.app.workspace.workspaceRoot });
56
147
  this.#autoImportSync = new AutoImportSync({ workspaceRoot: options.app.workspace.workspaceRoot });
@@ -59,7 +150,7 @@ class IncrementalBuilder {
59
150
  return `${this.#app.cwdPath}/.akan/artifact`;
60
151
  }
61
152
  async handleBuildRoute(msg) {
62
- return this.#enqueueWork(`build-route:${msg.routeId}`, async () => this.#handleBuildRoute(msg));
153
+ await this.#enqueueWork(`build-route:${msg.routeId}`, async () => BuilderReply.send(await this.#handleBuildRoute(msg)));
63
154
  }
64
155
  async#handleBuildRoute(msg) {
65
156
  try {
@@ -111,6 +202,7 @@ class IncrementalBuilder {
111
202
  }
112
203
  async#enqueueWork(label, fn) {
113
204
  const started = Date.now();
205
+ this.#inFlight += 1;
114
206
  const run = this.#workQueue.then(fn, fn);
115
207
  this.#workQueue = run.then(() => {
116
208
  return;
@@ -120,14 +212,51 @@ class IncrementalBuilder {
120
212
  try {
121
213
  return await run;
122
214
  } finally {
215
+ this.#inFlight -= 1;
216
+ this.#workCount += 1;
123
217
  this.#logger.verbose(`[work-queue] ${label} finished in ${Date.now() - started}ms`);
218
+ this.#reportMetrics();
124
219
  }
125
220
  }
221
+ get #idle() {
222
+ return this.#inFlight === 0 && this.#cssRebuildTimer === null;
223
+ }
224
+ #reportMetrics() {
225
+ if (!this.#idle || this.#shuttingDown)
226
+ return;
227
+ process.send?.({
228
+ type: "builder-metrics",
229
+ data: { rssBytes: process.memoryUsage.rss(), generation: this.#generation, workCount: this.#workCount }
230
+ });
231
+ }
232
+ async shutdown(reason) {
233
+ if (this.#shuttingDown)
234
+ return;
235
+ this.#shuttingDown = true;
236
+ const started = Date.now();
237
+ this.#logger.info(`shutdown requested (${reason}); draining ${this.#inFlight} work item(s)`);
238
+ if (this.#cssRebuildTimer) {
239
+ clearTimeout(this.#cssRebuildTimer);
240
+ this.#cssRebuildTimer = null;
241
+ this.#pendingCssRebuild = null;
242
+ }
243
+ await this.#workQueue.catch(() => {
244
+ return;
245
+ });
246
+ await this.#cssRebuildQueue.catch(() => {
247
+ return;
248
+ });
249
+ this.#logger.info(`drained in ${Date.now() - started}ms; exiting for recycle`);
250
+ process.exit(0);
251
+ }
252
+ get shuttingDown() {
253
+ return this.#shuttingDown;
254
+ }
126
255
  batchTouchesPagesTree(appDir, batch) {
127
- const absAppDir = path.resolve(appDir);
256
+ const absAppDir = path2.resolve(appDir);
128
257
  for (const f of batch.files) {
129
- const abs = path.resolve(f);
130
- if (!abs.startsWith(`${absAppDir}${path.sep}`) && abs !== absAppDir)
258
+ const abs = path2.resolve(f);
259
+ if (!abs.startsWith(`${absAppDir}${path2.sep}`) && abs !== absAppDir)
131
260
  continue;
132
261
  if (/\.(tsx|ts|jsx|js)$/.test(abs))
133
262
  return true;
@@ -135,67 +264,22 @@ class IncrementalBuilder {
135
264
  return false;
136
265
  }
137
266
  async batchMayChangePageKeys(appDir, batch) {
138
- const absAppDir = path.resolve(appDir);
139
- const pageKeys = new Set((await this.#app.getPageKeys()).map((key) => path.normalize(key)));
267
+ const absAppDir = path2.resolve(appDir);
268
+ const pageKeys = new Set((await this.#app.getPageKeys()).map((key) => path2.normalize(key)));
140
269
  for (const f of batch.files) {
141
- const abs = path.resolve(f);
142
- if (!abs.startsWith(`${absAppDir}${path.sep}`) && abs !== absAppDir)
270
+ const abs = path2.resolve(f);
271
+ if (!abs.startsWith(`${absAppDir}${path2.sep}`) && abs !== absAppDir)
143
272
  continue;
144
273
  if (!/\.(tsx|ts|jsx|js)$/.test(abs))
145
274
  continue;
146
- const rel = path.normalize(path.relative(absAppDir, abs));
275
+ const rel = path2.normalize(path2.relative(absAppDir, abs));
147
276
  if (!await Bun.file(abs).exists() || !pageKeys.has(rel))
148
277
  return true;
149
278
  }
150
279
  return false;
151
280
  }
152
- async rebuildCssArtifact(artifactDir, { refresh, generation, changedFiles }) {
153
- const cssStarted = Date.now();
154
- const cssByBasePathStarted = Date.now();
155
- const cssByBasePath = await this.#cssCompiler.getCssByBasePath({ refresh });
156
- this.#logger.verbose(`css-get-by-base-path ok (${Date.now() - cssByBasePathStarted}ms)`);
157
- const fontStarted = Date.now();
158
- const optimizedFonts = await this.#getOptimizedFonts(changedFiles ?? []);
159
- this.#logger.verbose(`font-assets ready (${Date.now() - fontStarted}ms)`);
160
- const cssAssetEntries = [];
161
- const cssBase64ByUrl = {};
162
- await Promise.all(Object.entries(cssByBasePath).flatMap(([basePath, baseCssText]) => {
163
- const cssText = [baseCssText, optimizedFonts.css].filter(Boolean).join(`
164
- `);
165
- if (!cssText)
166
- return [];
167
- return [
168
- (async () => {
169
- const cssAssetName = basePath || "root";
170
- const cssHash = Bun.hash(`${basePath}
171
- ${cssText}`).toString(36);
172
- const cssRelPath = `styles/${cssAssetName}-${cssHash}.css`;
173
- const cssUrl = `/_akan/styles/${cssAssetName}-${cssHash}.css`;
174
- await Bun.write(path.join(artifactDir, cssRelPath), cssText);
175
- cssAssetEntries.push([basePath, { cssUrl, cssRelPath }]);
176
- cssBase64ByUrl[cssUrl] = Buffer.from(new TextEncoder().encode(cssText)).toString("base64");
177
- })()
178
- ];
179
- }));
180
- const cssAssets = Object.fromEntries(cssAssetEntries);
181
- if (JSON.stringify(this.#artifact.cssAssets ?? {}) === JSON.stringify(cssAssets)) {
182
- this.#logger.verbose("css-rebuild unchanged assets; broadcast skipped");
183
- return;
184
- }
185
- this.#artifact = { ...this.#artifact, cssAssets };
186
- this.#logger.verbose(`css-compile ok assets=${Object.keys(cssAssets).length} (${Date.now() - cssStarted}ms)`);
187
- process.send?.({
188
- type: "css-updated",
189
- data: {
190
- cssAssets,
191
- cssBase64ByUrl,
192
- generation,
193
- changedFiles
194
- }
195
- });
196
- }
197
- scheduleCssRebuild(artifactDir, { refresh, generation, changedFiles }) {
198
- this.#pendingCssRebuild = { artifactDir, refresh, generation, changedFiles };
281
+ scheduleCssRebuild({ generation, changedFiles }) {
282
+ this.#pendingCssRebuild = { generation, changedFiles };
199
283
  if (this.#cssRebuildTimer)
200
284
  clearTimeout(this.#cssRebuildTimer);
201
285
  this.#cssRebuildTimer = setTimeout(() => {
@@ -204,15 +288,13 @@ ${cssText}`).toString(36);
204
288
  this.#pendingCssRebuild = null;
205
289
  if (!next)
206
290
  return;
291
+ this.#inFlight += 1;
207
292
  this.#cssRebuildQueue = this.#cssRebuildQueue.then(async () => {
208
- const started = Date.now();
209
- await this.rebuildCssArtifact(next.artifactDir, {
210
- refresh: next.refresh,
211
- generation: next.generation,
212
- changedFiles: next.changedFiles
293
+ await this.#runBatch({
294
+ generation: next.generation ?? this.#generation,
295
+ needs: ["css"],
296
+ changedFiles: next.changedFiles ?? []
213
297
  });
214
- this.#sendBuildStatus("css", { generation: next.generation, ok: true, files: next.changedFiles });
215
- this.#logger.verbose(`css-rebuild checked (${Date.now() - started}ms)`);
216
298
  }).catch((err) => {
217
299
  const message = err instanceof Error ? err.message : String(err);
218
300
  this.#logger.error(`css-rebuild failed: ${message}`);
@@ -222,29 +304,12 @@ ${cssText}`).toString(36);
222
304
  files: next.changedFiles,
223
305
  message
224
306
  });
307
+ }).finally(() => {
308
+ this.#inFlight -= 1;
309
+ this.#reportMetrics();
225
310
  });
226
311
  }, 150);
227
312
  }
228
- async#getOptimizedFonts(changedFiles) {
229
- if (!this.#shouldReoptimizeFonts(changedFiles)) {
230
- this.#logger.verbose(`font-optimize cached files=${this.#optimizedFonts.files.length}`);
231
- return this.#optimizedFonts;
232
- }
233
- const started = Date.now();
234
- this.#optimizedFonts = await new FontOptimizer(this.#app, "start").optimize();
235
- this.#logger.verbose(`font-optimize ok files=${this.#optimizedFonts.files.length} (${Date.now() - started}ms)`);
236
- return this.#optimizedFonts;
237
- }
238
- #shouldReoptimizeFonts(changedFiles) {
239
- if (changedFiles.length === 0)
240
- return false;
241
- return changedFiles.some((file) => {
242
- const normalized = path.resolve(file);
243
- if (/\.(woff2?|ttf|otf)$/i.test(normalized))
244
- return true;
245
- return this.#optimizedFonts.files.some((fontFile) => path.resolve(fontFile) === normalized);
246
- });
247
- }
248
313
  async installWatcher() {
249
314
  const [appDir, artifactDir] = [`${this.#app.cwdPath}/page`, this.#artifactDir];
250
315
  const roots = await new WatchRootResolver(this.#app).resolve();
@@ -255,7 +320,8 @@ ${cssText}`).toString(36);
255
320
  await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(appDir, artifactDir, batch));
256
321
  }
257
322
  });
258
- watcher.start();
323
+ await watcher.start();
324
+ this.#watcher = watcher;
259
325
  this.#logger.verbose(`watching ${roots.length} roots`);
260
326
  }
261
327
  async#handleWatchBatch(appDir, artifactDir, batch) {
@@ -269,6 +335,7 @@ ${cssText}`).toString(36);
269
335
  if (autoImport.changedFiles.length > 0)
270
336
  this.#logger.verbose(`[auto-import] inserted imports into ${autoImport.changedFiles.length} file(s)`);
271
337
  const indexSync = await this.#generatedIndexSync.syncForBatch(batch.files);
338
+ await this.#watcher?.absorb([...autoImport.changedFiles, ...indexSync.changedFiles]);
272
339
  const { files, kinds, expandedBatch, event, hasSyncErrors } = prepareDevWatchBatch({
273
340
  generation,
274
341
  batch,
@@ -306,42 +373,60 @@ ${cssText}`).toString(36);
306
373
  } else if (kinds.includes("code") && rebuildClient && this.batchTouchesPagesTree(appDir, expandedBatch)) {
307
374
  this.#logger.verbose("pageKeys refresh skipped; changed page source cannot add/remove a route key");
308
375
  }
309
- if (kinds.includes("code") && rebuildClient && this.#shouldRebuildCsr()) {
310
- try {
311
- const started = Date.now();
312
- await new CsrArtifactBuilder(this.#app).build();
313
- this.#sendBuildStatus("csr", { generation, ok: true, files });
314
- this.#logger.verbose(`csr-rebundle ok (${Date.now() - started}ms)`);
315
- } catch (err) {
316
- const message = err instanceof Error ? err.message : String(err);
317
- this.#logger.error(`csr-rebundle failed: ${message}`);
318
- this.#sendBuildStatus("csr", { generation, ok: false, files, message });
319
- }
320
- } else if (kinds.includes("code") && rebuildClient) {
321
- this.#logger.verbose(`csr-rebundle skipped; request /__csr or ?csr=true (or set AKAN_DEV_CSR_REBUILD=1) to enable per-save CSR rebuilds`);
322
- }
323
- process.send?.(event);
376
+ const needs = [];
324
377
  if (kinds.includes("code") && rebuildClient) {
325
- try {
326
- const started = Date.now();
327
- const next = await new PagesBundleBuilder(this.#app).build();
328
- process.send?.({
329
- type: "pages-updated",
330
- data: { bundlePath: next.bundlePath, buildId: next.buildId, generation, changedFiles: files }
331
- });
332
- this.#sendBuildStatus("pages", { generation, ok: true, files });
333
- this.#logger.verbose(`pages-rebundle ok buildId=${next.buildId} (${Date.now() - started}ms)`);
334
- } catch (err) {
335
- const message = err instanceof Error ? err.message : String(err);
336
- this.#logger.error(`pages-rebundle failed: ${message}`);
337
- this.#sendBuildStatus("pages", { generation, ok: false, files, message });
338
- }
378
+ if (this.#shouldRebuildCsr())
379
+ needs.push("csr");
380
+ else
381
+ this.#logger.verbose(`csr-rebundle skipped; request /__csr or ?csr=true (or set AKAN_DEV_CSR_REBUILD=1) to enable per-save CSR rebuilds`);
382
+ needs.push("pages");
383
+ needs.push("css");
339
384
  }
340
- if (kinds.includes("css") || kinds.includes("code") && rebuildClient) {
341
- this.scheduleCssRebuild(artifactDir, { refresh: true, generation, changedFiles: files });
385
+ process.send?.(event);
386
+ if (needs.length > 0)
387
+ await this.#runBatch({ generation, needs, changedFiles: files });
388
+ else if (kinds.includes("css")) {
389
+ this.scheduleCssRebuild({ generation, changedFiles: files });
342
390
  this.#logger.verbose(`css-rebuild scheduled generation=${generation}`);
343
391
  }
344
392
  }
393
+ async#runBatch({
394
+ generation,
395
+ needs,
396
+ changedFiles
397
+ }) {
398
+ const started = Date.now();
399
+ const result = await this.#batchRunner.run(await this.#batchRequest({ generation, needs, changedFiles }), (msg) => process.send?.(msg));
400
+ if (result.optimizedFonts)
401
+ this.#optimizedFonts = result.optimizedFonts;
402
+ if (result.cssAssets)
403
+ this.#artifact = { ...this.#artifact, cssAssets: result.cssAssets };
404
+ if (result.crashed) {
405
+ for (const need of needs)
406
+ this.#sendBuildStatus(need, { generation, ok: false, files: changedFiles, message: result.errors[need] });
407
+ }
408
+ if (needs.includes("css"))
409
+ this.#logger.verbose(`css-rebuild checked (${Date.now() - started}ms)`);
410
+ return result;
411
+ }
412
+ async#batchRequest({
413
+ generation,
414
+ needs,
415
+ changedFiles
416
+ }) {
417
+ return {
418
+ appName: this.#app.name,
419
+ workspaceRoot: this.#app.workspace.workspaceRoot,
420
+ repoName: this.#app.workspace.repoName,
421
+ generation,
422
+ needs,
423
+ changedFiles,
424
+ pageKeys: await this.#app.getPageKeys(),
425
+ optimizedFonts: this.#optimizedFonts,
426
+ cssAssets: this.#artifact.cssAssets ?? null,
427
+ artifactDir: path2.resolve(this.#artifactDir)
428
+ };
429
+ }
345
430
  async boot() {
346
431
  if (this.#watch)
347
432
  await this.installWatcher();
@@ -351,34 +436,46 @@ ${cssText}`).toString(36);
351
436
  async announceRecoveredState(changedFiles) {
352
437
  const generation = ++this.#generation;
353
438
  await this.#enqueueWork("boot-recovered", async () => {
354
- try {
355
- const next = await new PagesBundleBuilder(this.#app).build();
356
- process.send?.({
357
- type: "pages-updated",
358
- data: { bundlePath: next.bundlePath, buildId: next.buildId, generation, changedFiles }
359
- });
360
- this.#sendBuildStatus("pages", { generation, ok: true, files: changedFiles, message: "Boot build recovered" });
361
- } catch (err) {
362
- const message = err instanceof Error ? err.message : String(err);
363
- this.#logger.error(`recovered pages rebundle failed: ${message}`);
364
- this.#sendBuildStatus("pages", { generation, ok: false, files: changedFiles, message });
439
+ await this.#runBatch({ generation, needs: ["pages", "css"], changedFiles });
440
+ });
441
+ }
442
+ async announceBootState() {
443
+ const generation = ++this.#generation;
444
+ const reason = "builder-recycle";
445
+ process.send?.({
446
+ type: "pages-updated",
447
+ data: {
448
+ bundlePath: this.#artifact.pagesBundlePath,
449
+ buildId: this.#artifact.pagesBundleBuildId,
450
+ generation,
451
+ changedFiles: [],
452
+ reason
365
453
  }
366
- this.scheduleCssRebuild(this.#artifactDir, { refresh: true, generation, changedFiles });
367
454
  });
455
+ const cssAssets = this.#artifact.cssAssets ?? {};
456
+ const cssBase64ByUrl = Object.fromEntries(await Promise.all(Object.values(cssAssets).map(async ({ cssUrl, cssRelPath }) => [
457
+ cssUrl,
458
+ Buffer.from(await Bun.file(path2.join(this.#artifactDir, cssRelPath)).arrayBuffer()).toString("base64")
459
+ ])));
460
+ process.send?.({
461
+ type: "css-updated",
462
+ data: { cssAssets, cssBase64ByUrl, generation, changedFiles: [], reason }
463
+ });
464
+ this.#logger.verbose(`announced boot state after recycle generation=${generation}`);
368
465
  }
369
466
  async handleBuildCsr(msg) {
370
- return this.#enqueueWork("build-csr", async () => {
467
+ await this.#enqueueWork("build-csr", async () => {
371
468
  const started = Date.now();
372
- try {
373
- await new CsrArtifactBuilder(this.#app).build();
374
- this.#csrActive = true;
375
- this.#logger.info(`csr-build ok on demand (${Date.now() - started}ms); rebuilding CSR on every save now`);
376
- return { type: "build-csr-res", id: msg.id, ok: true };
377
- } catch (err) {
378
- const message = err instanceof Error ? err.message : String(err);
379
- this.#logger.error(`csr-build failed: ${message}`);
380
- return { type: "build-csr-res", id: msg.id, ok: false, error: message };
469
+ const result = await this.#batchRunner.run(await this.#batchRequest({ generation: this.#generation, needs: ["csr"], changedFiles: [] }));
470
+ const error = result.errors.csr;
471
+ if (error) {
472
+ this.#logger.error(`csr-build failed: ${error}`);
473
+ await BuilderReply.send({ type: "build-csr-res", id: msg.id, ok: false, error });
474
+ return;
381
475
  }
476
+ this.#csrActive = true;
477
+ this.#logger.info(`csr-build ok on demand (${Date.now() - started}ms); rebuilding CSR on every save now`);
478
+ await BuilderReply.send({ type: "build-csr-res", id: msg.id, ok: true });
382
479
  });
383
480
  }
384
481
  #shouldRebuildCsr() {
@@ -388,11 +485,21 @@ ${cssText}`).toString(36);
388
485
  return process.env.AKAN_DEV_CSR_REBUILD === "1";
389
486
  }
390
487
  static async#buildBootDeps(app) {
391
- const { artifact, cssCompiler, optimizedFonts } = await new SsrBaseArtifactBuilder(app).build();
392
- if (IncrementalBuilder.#csrArmedByEnv())
393
- await new CsrArtifactBuilder(app).build();
488
+ const { artifact, optimizedFonts } = await new SsrBaseArtifactBuilder(app).build();
394
489
  const discovery = await GraphClientEntryDiscovery.create(app);
395
- return { artifact, cssCompiler, optimizedFonts, discovery };
490
+ return { artifact, optimizedFonts, discovery };
491
+ }
492
+ async rearmCsrFromEnv() {
493
+ if (!IncrementalBuilder.#csrArmedByEnv())
494
+ return;
495
+ this.#csrActive = true;
496
+ await this.#enqueueWork("build-csr-rearm", async () => {
497
+ const result = await this.#batchRunner.run(await this.#batchRequest({ generation: this.#generation, needs: ["csr"], changedFiles: [] }));
498
+ if (result.errors.csr)
499
+ this.#logger.error(`csr-rearm failed: ${result.errors.csr}`);
500
+ else
501
+ this.#logger.verbose("csr-rearm ok; this session had CSR armed before the builder restarted");
502
+ });
396
503
  }
397
504
  static #recoverBoot(app, bootError, logger) {
398
505
  const firstMessage = bootError instanceof Error ? bootError.message : String(bootError);
@@ -430,13 +537,13 @@ ${cssText}`).toString(36);
430
537
  }
431
538
  }
432
539
  });
433
- watcher.start();
540
+ await watcher.start();
434
541
  logger.warn(`[degraded] watching ${roots.length} roots for a fix`);
435
542
  })().catch(reject);
436
543
  });
437
544
  }
438
545
  static async main() {
439
- const logger = new Logger("IncrementalBuilder");
546
+ const logger = new Logger2("IncrementalBuilder");
440
547
  const { appName, repoName, workspaceRoot } = WorkspaceExecutor.getBaseDevEnv();
441
548
  if (!workspaceRoot || !appName)
442
549
  throw new Error("AKAN_WORKSPACE_ROOT or AKAN_PUBLIC_APP_NAME is not set");
@@ -445,23 +552,34 @@ ${cssText}`).toString(36);
445
552
  const watch = process.env.AKAN_WATCH !== "0";
446
553
  let builder = null;
447
554
  const bootingError = "builder is recovering from a failed boot build; retry after the build error is fixed";
555
+ const recyclingError = "builder is recycling to release bundler memory; retry after it restarts";
448
556
  process.on("message", (msg) => {
449
557
  if (!msg || typeof msg !== "object")
450
558
  return;
451
- if (msg.type === "build-route") {
559
+ if (msg.type === "builder-shutdown") {
452
560
  if (!builder) {
453
- process.send?.({ type: "build-route-res", id: msg.id, ok: false, error: bootingError });
561
+ logger.warn(`ignoring shutdown request (${msg.reason}); builder is still recovering from a failed boot`);
454
562
  return;
455
563
  }
456
- builder.handleBuildRoute(msg).then((res) => process.send?.(res));
564
+ builder.shutdown(msg.reason);
565
+ return;
566
+ }
567
+ if (msg.type === "build-route") {
568
+ const error = builder?.shuttingDown ? recyclingError : bootingError;
569
+ if (!builder || builder.shuttingDown) {
570
+ process.send?.({ type: "build-route-res", id: msg.id, ok: false, error });
571
+ return;
572
+ }
573
+ builder.handleBuildRoute(msg);
457
574
  return;
458
575
  }
459
576
  if (msg.type === "build-csr") {
460
- if (!builder) {
461
- process.send?.({ type: "build-csr-res", id: msg.id, ok: false, error: bootingError });
577
+ const error = builder?.shuttingDown ? recyclingError : bootingError;
578
+ if (!builder || builder.shuttingDown) {
579
+ process.send?.({ type: "build-csr-res", id: msg.id, ok: false, error });
462
580
  return;
463
581
  }
464
- builder.handleBuildCsr(msg).then((res) => process.send?.(res));
582
+ builder.handleBuildCsr(msg);
465
583
  }
466
584
  });
467
585
  process.on("disconnect", () => {
@@ -481,6 +599,9 @@ ${cssText}`).toString(36);
481
599
  await builder.boot();
482
600
  if (recoveredFiles)
483
601
  await builder.announceRecoveredState(recoveredFiles);
602
+ else if (process.env.AKAN_BUILDER_ANNOUNCE_BOOT === "1")
603
+ await builder.announceBootState();
604
+ await builder.rearmCsrFromEnv();
484
605
  }
485
606
  }
486
607
  IncrementalBuilder.main().catch((err) => {