@blinkk/root 3.1.1 → 3.1.3

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.
@@ -47,7 +47,7 @@ import {
47
47
  } from "./chunk-TM6QRBGS.js";
48
48
 
49
49
  // src/cli/cli.ts
50
- import { Command, InvalidArgumentError } from "commander";
50
+ import { Command, InvalidArgumentError, Option } from "commander";
51
51
  import { bgGreen, black } from "kleur/colors";
52
52
 
53
53
  // src/cli/build.ts
@@ -55,7 +55,7 @@ import os from "node:os";
55
55
  import path2 from "node:path";
56
56
  import { fileURLToPath as fileURLToPath2 } from "node:url";
57
57
  import fsExtra from "fs-extra";
58
- import { dim as dim2, cyan } from "kleur/colors";
58
+ import { dim as dim3, cyan as cyan2 } from "kleur/colors";
59
59
  import glob from "tiny-glob";
60
60
  import { build as viteBuild } from "vite";
61
61
 
@@ -142,11 +142,253 @@ async function batchAsyncCalls(data, concurrency, asyncFunction) {
142
142
  return result;
143
143
  }
144
144
 
145
+ // src/cli/build-progress.ts
146
+ import { cyan, dim as dim2, green } from "kleur/colors";
147
+ function parseBuildLogMode(value) {
148
+ if (value === void 0 || value === null || value === "") {
149
+ return "progress";
150
+ }
151
+ if (value === "progress" || value === "verbose" || value === "quiet") {
152
+ return value;
153
+ }
154
+ throw new Error(
155
+ `invalid --log value: "${value}" (expected "progress", "verbose", or "quiet")`
156
+ );
157
+ }
158
+ function formatBytes(bytes) {
159
+ const k = 1024;
160
+ if (bytes < k) {
161
+ return (bytes / k).toFixed(2) + " kB";
162
+ }
163
+ const units = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
164
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
165
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + units[i];
166
+ }
167
+ function formatDuration(ms) {
168
+ const secs = ms / 1e3;
169
+ if (secs < 60) {
170
+ return `${secs.toFixed(1)}s`;
171
+ }
172
+ const mins = Math.floor(secs / 60);
173
+ const rem = Math.round(secs - mins * 60);
174
+ return `${mins}m${String(rem).padStart(2, "0")}s`;
175
+ }
176
+ var BAR_WIDTH = 20;
177
+ var BuildProgress = class {
178
+ total;
179
+ mode;
180
+ itemLabel;
181
+ outputDirLabel;
182
+ stream;
183
+ now;
184
+ intervalMs;
185
+ summaryTopN;
186
+ isTTY;
187
+ startedAt;
188
+ completed = 0;
189
+ totalBytes = 0;
190
+ files = [];
191
+ lastPrintAt = 0;
192
+ lastMilestone = 0;
193
+ ttyLineActive = false;
194
+ done = false;
195
+ /** Writes to `this.stream`, bypassing the log interceptor. */
196
+ streamWrite;
197
+ /** Restores the original `write` of each intercepted stream. */
198
+ restoreWrites = [];
199
+ /** True when intercepted output ended without a trailing newline. */
200
+ foreignLineOpen = false;
201
+ redrawTimer = null;
202
+ constructor(options) {
203
+ this.total = options.total;
204
+ this.mode = options.mode;
205
+ this.itemLabel = options.itemLabel ?? "pages";
206
+ this.outputDirLabel = options.outputDirLabel ?? "";
207
+ this.stream = options.stream ?? process.stdout;
208
+ this.now = options.now ?? Date.now;
209
+ this.isTTY = !!this.stream.isTTY;
210
+ this.intervalMs = options.intervalMs ?? (this.isTTY ? 80 : 5e3);
211
+ this.summaryTopN = options.summaryTopN ?? 5;
212
+ this.startedAt = this.now();
213
+ this.streamWrite = (str) => this.stream.write(str);
214
+ const interceptStreams = options.interceptStreams ?? (options.stream ? [] : [process.stdout, process.stderr]);
215
+ if (this.isTTY && this.mode === "progress") {
216
+ this.interceptLogs(interceptStreams);
217
+ }
218
+ }
219
+ /** Records a completed output file. */
220
+ add(outputFile, sizeBytes) {
221
+ this.completed += 1;
222
+ this.totalBytes += sizeBytes;
223
+ this.files.push({ path: outputFile, size: sizeBytes });
224
+ if (this.mode === "verbose") {
225
+ const paddedSize = formatBytes(sizeBytes).padStart(9, " ");
226
+ this.println(
227
+ ` ${dim2(paddedSize)} ${dim2(this.outputDirLabel)}${cyan(outputFile)}`
228
+ );
229
+ return;
230
+ }
231
+ if (this.mode === "progress") {
232
+ this.maybePrintProgress();
233
+ }
234
+ }
235
+ /** Clears any in-place progress line (call before printing errors). */
236
+ abort() {
237
+ this.clearTtyLine();
238
+ this.done = true;
239
+ this.restoreLogs();
240
+ }
241
+ /** Prints the final summary. */
242
+ finish() {
243
+ if (this.done) {
244
+ return;
245
+ }
246
+ this.done = true;
247
+ this.restoreLogs();
248
+ this.clearTtyLine();
249
+ const elapsed = formatDuration(this.now() - this.startedAt);
250
+ const summary = `${this.completed} ${this.itemLabel} (${formatBytes(
251
+ this.totalBytes
252
+ )}) in ${elapsed}`;
253
+ this.println(` ${green("\u2713")} ${summary}`);
254
+ if (this.mode === "progress" && this.summaryTopN > 0) {
255
+ const largest = [...this.files].sort((a, b) => b.size - a.size).slice(0, this.summaryTopN);
256
+ if (largest.length > 0) {
257
+ this.println(` ${dim2(`largest ${this.itemLabel}:`)}`);
258
+ for (const file of largest) {
259
+ const paddedSize = formatBytes(file.size).padStart(9, " ");
260
+ this.println(
261
+ ` ${dim2(paddedSize)} ${dim2(this.outputDirLabel)}${cyan(
262
+ file.path
263
+ )}`
264
+ );
265
+ }
266
+ }
267
+ }
268
+ }
269
+ maybePrintProgress() {
270
+ const now = this.now();
271
+ if (this.isTTY) {
272
+ if (now - this.lastPrintAt < this.intervalMs) {
273
+ return;
274
+ }
275
+ this.lastPrintAt = now;
276
+ this.renderTtyLine(now);
277
+ return;
278
+ }
279
+ const milestone = Math.floor(this.completed / this.total * 10);
280
+ if (milestone <= this.lastMilestone || this.completed === this.total) {
281
+ return;
282
+ }
283
+ if (now - this.lastPrintAt < this.intervalMs) {
284
+ return;
285
+ }
286
+ this.lastMilestone = milestone;
287
+ this.lastPrintAt = now;
288
+ const pct = Math.floor(this.completed / this.total * 100);
289
+ this.println(
290
+ ` ${this.completed}/${this.total} ${this.itemLabel} (${pct}%) \xB7 ${formatBytes(this.totalBytes)} \xB7 ${formatDuration(now - this.startedAt)}${this.eta(now)}`
291
+ );
292
+ }
293
+ renderTtyLine(now) {
294
+ this.closeForeignLine();
295
+ const ratio = this.total > 0 ? this.completed / this.total : 0;
296
+ const filled = Math.round(ratio * BAR_WIDTH);
297
+ const bar = "\u2595" + "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled) + "\u258F";
298
+ const pct = String(Math.floor(ratio * 100)).padStart(3, " ");
299
+ const line = ` ${bar} ${pct}% \xB7 ${this.completed}/${this.total} ${this.itemLabel} \xB7 ${formatBytes(this.totalBytes)} \xB7 ${formatDuration(now - this.startedAt)}${this.eta(now)}`;
300
+ this.streamWrite(`\r\x1B[2K${line}`);
301
+ this.ttyLineActive = true;
302
+ }
303
+ /** Returns an " · eta 12.3s" suffix once enough progress exists to estimate. */
304
+ eta(now) {
305
+ if (this.completed < 10 || this.completed / this.total < 0.05) {
306
+ return "";
307
+ }
308
+ const elapsed = now - this.startedAt;
309
+ const remaining = elapsed / this.completed * (this.total - this.completed);
310
+ return ` \xB7 eta ${formatDuration(remaining)}`;
311
+ }
312
+ clearTtyLine() {
313
+ if (this.ttyLineActive) {
314
+ this.streamWrite("\r\x1B[2K");
315
+ this.ttyLineActive = false;
316
+ }
317
+ }
318
+ println(line) {
319
+ this.clearTtyLine();
320
+ this.closeForeignLine();
321
+ this.streamWrite(line + "\n");
322
+ }
323
+ /**
324
+ * Patches `write` on each stream so foreign output (user logs) clears the
325
+ * progress bar line before printing and schedules a redraw after. The
326
+ * progress bar's own writes go through `streamWrite` and bypass the patch.
327
+ */
328
+ interceptLogs(streams) {
329
+ const self = this;
330
+ for (const stream of streams) {
331
+ const original = stream.write;
332
+ if (stream === this.stream) {
333
+ this.streamWrite = (str) => original.call(stream, str);
334
+ }
335
+ stream.write = function(...args) {
336
+ self.clearTtyLine();
337
+ const chunk = args[0];
338
+ const text = typeof chunk === "string" ? chunk : String(chunk ?? "");
339
+ self.foreignLineOpen = text.length > 0 && !text.endsWith("\n");
340
+ const result = original.apply(stream, args);
341
+ self.scheduleRedraw();
342
+ return result;
343
+ };
344
+ this.restoreWrites.push(() => {
345
+ stream.write = original;
346
+ });
347
+ }
348
+ }
349
+ /** Redraws the progress bar shortly after foreign output interrupts it. */
350
+ scheduleRedraw() {
351
+ if (this.done || this.redrawTimer !== null || this.completed === 0) {
352
+ return;
353
+ }
354
+ this.redrawTimer = setTimeout(() => {
355
+ this.redrawTimer = null;
356
+ if (!this.done) {
357
+ this.renderTtyLine(this.now());
358
+ }
359
+ }, 0);
360
+ this.redrawTimer.unref?.();
361
+ }
362
+ /**
363
+ * If intercepted output left a partial (unterminated) line, terminate it
364
+ * so the next progress bar render doesn't overwrite it with `\r`.
365
+ */
366
+ closeForeignLine() {
367
+ if (this.foreignLineOpen) {
368
+ this.streamWrite("\n");
369
+ this.foreignLineOpen = false;
370
+ }
371
+ }
372
+ restoreLogs() {
373
+ if (this.redrawTimer) {
374
+ clearTimeout(this.redrawTimer);
375
+ this.redrawTimer = null;
376
+ }
377
+ while (this.restoreWrites.length > 0) {
378
+ this.restoreWrites.pop()();
379
+ }
380
+ }
381
+ };
382
+
145
383
  // src/cli/build-worker-pool.ts
146
384
  import path from "node:path";
147
385
  import { fileURLToPath } from "node:url";
386
+ import v8 from "node:v8";
148
387
  import { Worker } from "node:worker_threads";
149
388
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
389
+ var WORKER_HEAP_LIMIT_MB = Math.ceil(
390
+ v8.getHeapStatistics().heap_size_limit / (1024 * 1024)
391
+ );
150
392
  var BuildPageError = class extends Error {
151
393
  urlPath;
152
394
  params;
@@ -173,7 +415,8 @@ var BuildWorker = class {
173
415
  this.pool = pool;
174
416
  this.concurrency = options.workerConcurrency;
175
417
  this.worker = new Worker(path.resolve(__dirname, "./build-worker.js"), {
176
- workerData: { rootDir: options.rootDir, mode: options.mode }
418
+ workerData: { rootDir: options.rootDir, mode: options.mode },
419
+ resourceLimits: { maxOldGenerationSizeMb: WORKER_HEAP_LIMIT_MB }
177
420
  });
178
421
  let onReady;
179
422
  let onFailed;
@@ -216,10 +459,30 @@ var BuildWorker = class {
216
459
  }
217
460
  });
218
461
  }
462
+ /**
463
+ * Rejects all in-flight tasks after a worker-level failure (fatal error,
464
+ * crash, or non-zero exit — e.g. an OOM kill).
465
+ *
466
+ * Each task is rejected with its own `BuildPageError` so the build log
467
+ * names every page that was in flight when the worker died. Without this,
468
+ * worker deaths surface as a single generic error with no route context,
469
+ * making OOM crashes impossible to trace back to pages. Note the in-flight
470
+ * pages are the OOM *suspects*, not necessarily the cause (memory pressure
471
+ * is cumulative), which is why the message says "while rendering".
472
+ */
219
473
  failAll(err) {
220
474
  const pendingTasks = Array.from(this.inFlight.values());
221
475
  this.inFlight.clear();
222
- pendingTasks.forEach((pending) => pending.reject(err));
476
+ pendingTasks.forEach(
477
+ (pending) => pending.reject(
478
+ new BuildPageError(
479
+ pending.task.urlPath,
480
+ pending.task.params,
481
+ pending.task.routeSrc,
482
+ `worker thread died while rendering this page (${pendingTasks.length} page(s) were in flight): ` + String(err.message || err)
483
+ )
484
+ )
485
+ );
223
486
  }
224
487
  /** Pulls tasks off the pool's queue until at max concurrency. */
225
488
  next() {
@@ -274,18 +537,22 @@ var BuildWorkerPool = class {
274
537
 
275
538
  // src/cli/build.ts
276
539
  var __dirname2 = path2.dirname(fileURLToPath2(import.meta.url));
540
+ var activeLogMode = "progress";
277
541
  async function build(rootProjectDir, options) {
278
542
  const mode = options?.mode || "production";
279
543
  process.env.NODE_ENV = mode;
544
+ activeLogMode = options?.quiet ? "quiet" : parseBuildLogMode(options?.log);
280
545
  const rootDir = path2.resolve(rootProjectDir || process.cwd());
281
546
  const rootConfig = await loadRootConfig(rootDir, { command: "build" });
282
547
  const distDir = path2.join(rootDir, "dist");
283
548
  const ssrOnly = options?.ssrOnly || false;
284
- console.log();
285
- console.log(`${dim2("\u2503")} project: ${rootDir}`);
286
- console.log(`${dim2("\u2503")} output: ${distDir}/html`);
287
- console.log(`${dim2("\u2503")} mode: ${mode}`);
288
- console.log();
549
+ if (activeLogMode !== "quiet") {
550
+ console.log();
551
+ console.log(`${dim3("\u2503")} project: ${rootDir}`);
552
+ console.log(`${dim3("\u2503")} output: ${distDir}/html`);
553
+ console.log(`${dim3("\u2503")} mode: ${mode}`);
554
+ console.log();
555
+ }
289
556
  await rmDir(distDir);
290
557
  await makeDir(distDir);
291
558
  const pods = await collectPods(rootConfig);
@@ -341,7 +608,9 @@ async function build(rootProjectDir, options) {
341
608
  ...viteConfig,
342
609
  root: rootDir,
343
610
  mode,
344
- plugins: vitePlugins
611
+ plugins: vitePlugins,
612
+ // In quiet mode, only surface vite warnings and errors.
613
+ logLevel: activeLogMode === "quiet" ? "warn" : viteConfig.logLevel
345
614
  };
346
615
  const ssrInput = {
347
616
  render: path2.resolve(__dirname2, "./render.js")
@@ -540,7 +809,9 @@ async function build(rootProjectDir, options) {
540
809
  await fsExtra.copy(assetFrom, assetTo);
541
810
  printFileOutput(fileSize(assetTo), "dist/html/", assetRelPath);
542
811
  }
543
- console.log("\njs/css output:");
812
+ if (activeLogMode !== "quiet") {
813
+ console.log("\njs/css output:");
814
+ }
544
815
  await Promise.all(
545
816
  Object.keys(rootManifest).map(async (src) => {
546
817
  const assetData = rootManifest[src];
@@ -609,10 +880,9 @@ async function build(rootProjectDir, options) {
609
880
  if (!result.staticContent) {
610
881
  addSitemapXmlItem(result.urlPath, sitemapItem, result.outFilePath);
611
882
  }
612
- printFileOutput(
613
- fileSize(path2.join(buildDir, result.outFilePath)),
614
- "dist/html/",
615
- result.outFilePath
883
+ progress.add(
884
+ result.outFilePath,
885
+ fileSizeBytes(path2.join(buildDir, result.outFilePath))
616
886
  );
617
887
  };
618
888
  const sitemapEntries = Object.entries(sitemap).filter(([, sitemapItem]) => {
@@ -624,76 +894,100 @@ async function build(rootProjectDir, options) {
624
894
  }
625
895
  return true;
626
896
  });
627
- console.log("\nhtml output:");
897
+ if (activeLogMode !== "quiet") {
898
+ console.log("\nhtml output:");
899
+ }
628
900
  const concurrency = Number(options?.concurrency || 10);
629
- const numThreads = resolveNumThreads(
901
+ const resolvedThreads = resolveNumThreads(
630
902
  options?.threads,
631
903
  sitemapEntries.length
632
904
  );
633
- if (numThreads > 0) {
634
- const pool = new BuildWorkerPool({
635
- numWorkers: numThreads,
636
- workerConcurrency: Math.max(Math.ceil(concurrency / numThreads), 1),
637
- rootDir,
638
- mode
639
- });
640
- try {
641
- await pool.ready();
642
- await Promise.all(
643
- sitemapEntries.map(async ([urlPath, sitemapItem]) => {
905
+ const numThreads = resolvedThreads.count;
906
+ const workerConcurrency = numThreads > 0 ? Math.max(Math.ceil(concurrency / numThreads), 1) : 0;
907
+ if (activeLogMode !== "quiet") {
908
+ const threadsDesc = numThreads > 0 ? `${numThreads} workers x ${workerConcurrency} pages/worker` : `in-process, ${concurrency} concurrent pages`;
909
+ console.log(
910
+ ` ${dim3(`threads: ${threadsDesc} (${resolvedThreads.source})`)}`
911
+ );
912
+ }
913
+ const progress = new BuildProgress({
914
+ total: sitemapEntries.length,
915
+ mode: activeLogMode,
916
+ itemLabel: "pages",
917
+ outputDirLabel: "dist/html/"
918
+ });
919
+ try {
920
+ if (numThreads > 0) {
921
+ const pool = new BuildWorkerPool({
922
+ numWorkers: numThreads,
923
+ workerConcurrency,
924
+ rootDir,
925
+ mode
926
+ });
927
+ try {
928
+ await pool.ready();
929
+ await Promise.all(
930
+ sitemapEntries.map(async ([urlPath, sitemapItem]) => {
931
+ try {
932
+ const result = await pool.run({
933
+ urlPath,
934
+ params: sitemapItem.params,
935
+ routeSrc: sitemapItem.route.src,
936
+ locale: sitemapItem.locale
937
+ });
938
+ onPageResult(sitemapItem, result);
939
+ } catch (e) {
940
+ if (e instanceof BuildPageError) {
941
+ progress.abort();
942
+ logBuildError(
943
+ {
944
+ route: sitemapItem.route,
945
+ params: sitemapItem.params,
946
+ urlPath
947
+ },
948
+ new Error(e.workerError)
949
+ );
950
+ }
951
+ throw e;
952
+ }
953
+ })
954
+ );
955
+ } finally {
956
+ await pool.terminate();
957
+ }
958
+ } else {
959
+ await batchAsyncCalls(
960
+ sitemapEntries,
961
+ concurrency,
962
+ async ([urlPath, sitemapItem]) => {
644
963
  try {
645
- const result = await pool.run({
646
- urlPath,
647
- params: sitemapItem.params,
648
- routeSrc: sitemapItem.route.src,
649
- locale: sitemapItem.locale
650
- });
964
+ const result = await buildPage(
965
+ renderer,
966
+ rootConfig,
967
+ buildDir,
968
+ sitemapItem.route,
969
+ { urlPath, params: sitemapItem.params }
970
+ );
651
971
  onPageResult(sitemapItem, result);
652
972
  } catch (e) {
653
- if (e instanceof BuildPageError) {
654
- logBuildError(
655
- {
656
- route: sitemapItem.route,
657
- params: sitemapItem.params,
658
- urlPath
659
- },
660
- new Error(e.workerError)
661
- );
662
- }
663
- throw e;
973
+ progress.abort();
974
+ logBuildError(
975
+ { route: sitemapItem.route, params: sitemapItem.params, urlPath },
976
+ e
977
+ );
978
+ throw new Error(
979
+ `BuildError: ${urlPath} (${sitemapItem.route.src}) failed to build.`,
980
+ { cause: e }
981
+ );
664
982
  }
665
- })
983
+ }
666
984
  );
667
- } finally {
668
- await pool.terminate();
669
985
  }
670
- } else {
671
- await batchAsyncCalls(
672
- sitemapEntries,
673
- concurrency,
674
- async ([urlPath, sitemapItem]) => {
675
- try {
676
- const result = await buildPage(
677
- renderer,
678
- rootConfig,
679
- buildDir,
680
- sitemapItem.route,
681
- { urlPath, params: sitemapItem.params }
682
- );
683
- onPageResult(sitemapItem, result);
684
- } catch (e) {
685
- logBuildError(
686
- { route: sitemapItem.route, params: sitemapItem.params, urlPath },
687
- e
688
- );
689
- throw new Error(
690
- `BuildError: ${urlPath} (${sitemapItem.route.src}) failed to build.`,
691
- { cause: e }
692
- );
693
- }
694
- }
695
- );
986
+ } catch (e) {
987
+ progress.abort();
988
+ throw e;
696
989
  }
990
+ progress.finish();
697
991
  if (rootConfig.sitemap) {
698
992
  const sitemapXmlBuilder = [];
699
993
  sitemapXmlItems.sort((a, b) => a.url.localeCompare(b.url));
@@ -734,42 +1028,51 @@ function isRouteFile(filepath) {
734
1028
  return filepath.startsWith("routes") && isJsFile(filepath);
735
1029
  }
736
1030
  var MIN_PAGES_PER_THREAD = 10;
1031
+ var MEMORY_PER_THREAD = 1024 * 1024 * 1024;
1032
+ function availableMemory() {
1033
+ if (typeof process.availableMemory === "function") {
1034
+ return process.availableMemory();
1035
+ }
1036
+ return os.freemem();
1037
+ }
737
1038
  function resolveNumThreads(threads, numPages) {
738
1039
  if (threads === void 0 || threads === false) {
739
- return 0;
1040
+ return { count: 0, source: "--threads not set" };
740
1041
  }
741
1042
  if (threads === true || threads === "auto") {
742
1043
  const maxByCpu = Math.max(os.availableParallelism() - 1, 1);
743
1044
  const maxByPages = Math.floor(numPages / MIN_PAGES_PER_THREAD);
744
- const numThreads = Math.min(maxByCpu, maxByPages);
1045
+ const maxByMemory = Math.max(
1046
+ Math.floor(availableMemory() / MEMORY_PER_THREAD) - 1,
1047
+ 1
1048
+ );
1049
+ const numThreads = Math.min(maxByCpu, maxByPages, maxByMemory);
1050
+ const source = `auto = min(cpu ${maxByCpu}, pages ${maxByPages}, memory ${maxByMemory})`;
745
1051
  if (numThreads <= 1) {
746
- return 0;
1052
+ return { count: 0, source };
747
1053
  }
748
- console.log(`rendering with ${numThreads} worker threads`);
749
- return numThreads;
1054
+ return { count: numThreads, source };
750
1055
  }
751
1056
  const num = parseInt(threads);
752
1057
  if (isNaN(num) || num < 0) {
753
1058
  throw new Error(`invalid --threads value: ${threads}`);
754
1059
  }
755
- return num;
1060
+ return { count: num, source: `--threads=${num}` };
1061
+ }
1062
+ function fileSizeBytes(filepath) {
1063
+ return fsExtra.statSync(filepath).size;
756
1064
  }
757
1065
  function fileSize(filepath) {
758
- const stats = fsExtra.statSync(filepath);
759
- const bytes = stats.size;
760
- const k = 1024;
761
- if (bytes < k) {
762
- return (bytes / k).toFixed(2) + " kB";
763
- }
764
- const units = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
765
- const i = Math.floor(Math.log(bytes) / Math.log(k));
766
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + units[i];
1066
+ return formatBytes(fileSizeBytes(filepath));
767
1067
  }
768
1068
  function printFileOutput(fileSize2, outputDir, outputFile) {
1069
+ if (activeLogMode === "quiet") {
1070
+ return;
1071
+ }
769
1072
  const indent = " ".repeat(2);
770
1073
  const paddedSize = fileSize2.padStart(9, " ");
771
1074
  console.log(
772
- `${indent}${dim2(paddedSize)} ${dim2(outputDir)}${cyan(outputFile)}`
1075
+ `${indent}${dim3(paddedSize)} ${dim3(outputDir)}${cyan2(outputFile)}`
773
1076
  );
774
1077
  }
775
1078
  function sanitizeFileName(name) {
@@ -1021,7 +1324,7 @@ import path11 from "node:path";
1021
1324
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1022
1325
  import cookieParser from "cookie-parser";
1023
1326
  import { default as express } from "express";
1024
- import { dim as dim5 } from "kleur/colors";
1327
+ import { dim as dim6 } from "kleur/colors";
1025
1328
  import sirv from "sirv";
1026
1329
  import glob3 from "tiny-glob";
1027
1330
 
@@ -1306,7 +1609,7 @@ import { spawn as spawn3 } from "node:child_process";
1306
1609
  import fs6 from "node:fs";
1307
1610
  import path9 from "node:path";
1308
1611
  import * as readline from "node:readline";
1309
- import { dim as dim3, green, red, yellow } from "kleur/colors";
1612
+ import { dim as dim4, green as green2, red, yellow } from "kleur/colors";
1310
1613
 
1311
1614
  // src/secrets/manifest.ts
1312
1615
  import fs3 from "node:fs";
@@ -2079,7 +2382,7 @@ function getGitEmail() {
2079
2382
  }
2080
2383
 
2081
2384
  // src/cli/secrets.ts
2082
- var BAR = dim3("\u2503");
2385
+ var BAR = dim4("\u2503");
2083
2386
  var DEV_SYNC_TIMEOUT_MS = 1e4;
2084
2387
  function registerSecretsCommands(program) {
2085
2388
  const secrets = program.command("secrets").description(
@@ -2140,13 +2443,13 @@ async function secretsInit(opts) {
2140
2443
  await writeManifest(target, manifest);
2141
2444
  const addedGitignore = await ensureRootGitignored(rootDir);
2142
2445
  console.log();
2143
- console.log(`${BAR} ${green("secrets:")} created ${rel(rootDir, target)}`);
2446
+ console.log(`${BAR} ${green2("secrets:")} created ${rel(rootDir, target)}`);
2144
2447
  if (addedGitignore) {
2145
- console.log(`${BAR} added ${dim3(".root/")} to .gitignore`);
2448
+ console.log(`${BAR} added ${dim4(".root/")} to .gitignore`);
2146
2449
  }
2147
2450
  await warnIfEnvNotIgnored(rootDir);
2148
2451
  console.log(
2149
- `${BAR} ${dim3("next: `root secrets set <NAME>` to store a value")}`
2452
+ `${BAR} ${dim4("next: `root secrets set <NAME>` to store a value")}`
2150
2453
  );
2151
2454
  console.log();
2152
2455
  }
@@ -2158,18 +2461,18 @@ async function secretsSet(name, opts) {
2158
2461
  throw new Error("no value provided on stdin");
2159
2462
  }
2160
2463
  await setSecret({ rootDir, manifestFilePath, name, value: secretValue });
2161
- console.log(`${BAR} ${green("secrets:")} set ${name}`);
2464
+ console.log(`${BAR} ${green2("secrets:")} set ${name}`);
2162
2465
  console.log(
2163
- `${BAR} ${dim3(`commit ${rel(rootDir, manifestFilePath)} to share this change`)}`
2466
+ `${BAR} ${dim4(`commit ${rel(rootDir, manifestFilePath)} to share this change`)}`
2164
2467
  );
2165
2468
  }
2166
2469
  async function secretsRm(name, opts) {
2167
2470
  const rootDir = process.cwd();
2168
2471
  const manifestFilePath = opts.manifest ? path9.resolve(rootDir, opts.manifest) : manifestPath(rootDir);
2169
2472
  await removeSecret({ rootDir, manifestFilePath, name });
2170
- console.log(`${BAR} ${green("secrets:")} removed ${name}`);
2473
+ console.log(`${BAR} ${green2("secrets:")} removed ${name}`);
2171
2474
  console.log(
2172
- `${BAR} ${dim3(`commit ${rel(rootDir, manifestFilePath)} to share this change`)}`
2475
+ `${BAR} ${dim4(`commit ${rel(rootDir, manifestFilePath)} to share this change`)}`
2173
2476
  );
2174
2477
  }
2175
2478
  async function secretsPush(opts) {
@@ -2184,7 +2487,7 @@ async function secretsPush(opts) {
2184
2487
  }
2185
2488
  console.log();
2186
2489
  console.log(
2187
- `${BAR} ${green("secrets:")} push ${names.length} value(s) from .env to "${gsmKey}":`
2490
+ `${BAR} ${green2("secrets:")} push ${names.length} value(s) from .env to "${gsmKey}":`
2188
2491
  );
2189
2492
  for (const name of names) {
2190
2493
  console.log(`${BAR} ${name}`);
@@ -2227,13 +2530,13 @@ async function secretsStatus() {
2227
2530
  }
2228
2531
  console.log();
2229
2532
  console.log(
2230
- `${BAR} ${green("secrets:")} ${resolved.site.gsmKey} ${dim3(`(${resolved.site.gcpProjectId})`)}`
2533
+ `${BAR} ${green2("secrets:")} ${resolved.site.gsmKey} ${dim4(`(${resolved.site.gcpProjectId})`)}`
2231
2534
  );
2232
2535
  if (resolved.shared) {
2233
- console.log(`${BAR} imports ${dim3(resolved.shared.gsmKey)}`);
2536
+ console.log(`${BAR} imports ${dim4(resolved.shared.gsmKey)}`);
2234
2537
  }
2235
2538
  if (keys.length === 0) {
2236
- console.log(`${BAR} ${dim3("no managed keys yet")}`);
2539
+ console.log(`${BAR} ${dim4("no managed keys yet")}`);
2237
2540
  }
2238
2541
  for (const key of keys) {
2239
2542
  console.log(`${BAR} ${formatStatus(key)}`);
@@ -2261,15 +2564,15 @@ async function syncSecretsOnDev(rootDir) {
2261
2564
  function formatStatus(key) {
2262
2565
  switch (key.kind) {
2263
2566
  case "in-sync":
2264
- return `${green("\u2713")} ${key.name}`;
2567
+ return `${green2("\u2713")} ${key.name}`;
2265
2568
  case "remote-newer":
2266
- return `${yellow("\u2193")} ${key.name} ${dim3("(update available \u2014 run `root secrets sync`)")}`;
2569
+ return `${yellow("\u2193")} ${key.name} ${dim4("(update available \u2014 run `root secrets sync`)")}`;
2267
2570
  case "locally-edited":
2268
- return `${dim3("\u2022")} ${key.name} ${dim3("(locally edited)")}`;
2571
+ return `${dim4("\u2022")} ${key.name} ${dim4("(locally edited)")}`;
2269
2572
  case "conflict":
2270
- return `${yellow("!")} ${key.name} ${dim3("(changed locally & remotely)")}`;
2573
+ return `${yellow("!")} ${key.name} ${dim4("(changed locally & remotely)")}`;
2271
2574
  case "not-pulled":
2272
- return `${yellow("\u2193")} ${key.name} ${dim3("(not pulled yet)")}`;
2575
+ return `${yellow("\u2193")} ${key.name} ${dim4("(not pulled yet)")}`;
2273
2576
  default:
2274
2577
  return key.name;
2275
2578
  }
@@ -2281,13 +2584,13 @@ function printSyncSummary(result, opts = {}) {
2281
2584
  }
2282
2585
  const parts = [];
2283
2586
  if (result.changed.length) {
2284
- parts.push(green(`${result.changed.length} updated`));
2587
+ parts.push(green2(`${result.changed.length} updated`));
2285
2588
  }
2286
2589
  if (result.removed.length) {
2287
2590
  parts.push(`${result.removed.length} removed`);
2288
2591
  }
2289
2592
  if (result.kept.length) {
2290
- parts.push(dim3(`${result.kept.length} local`));
2593
+ parts.push(dim4(`${result.kept.length} local`));
2291
2594
  }
2292
2595
  if (result.conflicts.length) {
2293
2596
  parts.push(yellow(`${result.conflicts.length} conflict`));
@@ -2297,22 +2600,22 @@ function printSyncSummary(result, opts = {}) {
2297
2600
  }
2298
2601
  console.log();
2299
2602
  console.log(
2300
- `${BAR} ${green("secrets:")} ${parts.length ? parts.join(", ") : "up to date"}`
2603
+ `${BAR} ${green2("secrets:")} ${parts.length ? parts.join(", ") : "up to date"}`
2301
2604
  );
2302
2605
  for (const name of result.changed) {
2303
- console.log(`${BAR} ${green("+")} ${name}`);
2606
+ console.log(`${BAR} ${green2("+")} ${name}`);
2304
2607
  }
2305
2608
  for (const name of result.removed) {
2306
- console.log(`${BAR} ${dim3("-")} ${name} ${dim3("(removed)")}`);
2609
+ console.log(`${BAR} ${dim4("-")} ${name} ${dim4("(removed)")}`);
2307
2610
  }
2308
2611
  for (const name of result.overwritten) {
2309
2612
  console.log(
2310
- `${BAR} ${yellow("!")} ${name} ${dim3("(local value replaced)")}`
2613
+ `${BAR} ${yellow("!")} ${name} ${dim4("(local value replaced)")}`
2311
2614
  );
2312
2615
  }
2313
2616
  for (const name of result.conflicts) {
2314
2617
  console.log(
2315
- `${BAR} ${yellow("!")} ${name} ${dim3(
2618
+ `${BAR} ${yellow("!")} ${name} ${dim4(
2316
2619
  "(changed locally & remotely; keeping yours \u2014 `root secrets pull` to take remote)"
2317
2620
  )}`
2318
2621
  );
@@ -2334,18 +2637,18 @@ function printPushResult(result, rootDir, manifestFilePath) {
2334
2637
  console.log(`${BAR} ${yellow("secrets:")} nothing to push`);
2335
2638
  } else {
2336
2639
  console.log(
2337
- `${BAR} ${green("secrets:")} pushed ${result.pushed.length} value(s) to "${result.gsmKey}"`
2640
+ `${BAR} ${green2("secrets:")} pushed ${result.pushed.length} value(s) to "${result.gsmKey}"`
2338
2641
  );
2339
2642
  for (const name of result.pushed) {
2340
- console.log(`${BAR} ${green("+")} ${name}`);
2643
+ console.log(`${BAR} ${green2("+")} ${name}`);
2341
2644
  }
2342
2645
  }
2343
2646
  for (const skip of result.skipped) {
2344
- console.log(`${BAR} ${dim3("-")} ${skip.name} ${dim3(`(${skip.reason})`)}`);
2647
+ console.log(`${BAR} ${dim4("-")} ${skip.name} ${dim4(`(${skip.reason})`)}`);
2345
2648
  }
2346
2649
  if (result.pushed.length > 0) {
2347
2650
  console.log(
2348
- `${BAR} ${dim3(`commit ${rel(rootDir, manifestFilePath)} to share`)}`
2651
+ `${BAR} ${dim4(`commit ${rel(rootDir, manifestFilePath)} to share`)}`
2349
2652
  );
2350
2653
  }
2351
2654
  console.log();
@@ -2371,7 +2674,7 @@ async function warnIfEnvNotIgnored(rootDir) {
2371
2674
  const ignored = await isPathGitIgnored(rootDir, ".env");
2372
2675
  if (ignored === false) {
2373
2676
  console.log(
2374
- `${BAR} ${yellow("warning:")} ${dim3(".env is not git-ignored \u2014 add it to .gitignore")}`
2677
+ `${BAR} ${yellow("warning:")} ${dim4(".env is not git-ignored \u2014 add it to .gitignore")}`
2375
2678
  );
2376
2679
  }
2377
2680
  }
@@ -2512,7 +2815,7 @@ import os2 from "node:os";
2512
2815
  import path10 from "node:path";
2513
2816
 
2514
2817
  // src/cli/startup/check-version.ts
2515
- import { dim as dim4, green as green2, yellow as yellow2 } from "kleur/colors";
2818
+ import { dim as dim5, green as green3, yellow as yellow2 } from "kleur/colors";
2516
2819
  var PACKAGE_NAME = "@blinkk/root";
2517
2820
  var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
2518
2821
  var REGISTRY_TIMEOUT_MS = 3e3;
@@ -2583,10 +2886,10 @@ function parseVersion(version) {
2583
2886
  return [toInt(parts[0]), toInt(parts[1]), toInt(parts[2])];
2584
2887
  }
2585
2888
  function printUpdateNotice(current, latest) {
2586
- const bar = dim4("\u2503");
2889
+ const bar = dim5("\u2503");
2587
2890
  const label = yellow2(`Update available for ${PACKAGE_NAME}:`);
2588
2891
  console.log();
2589
- console.log(`${bar} ${label} ${dim4(current)} \u2192 ${green2(latest)}`);
2892
+ console.log(`${bar} ${label} ${dim5(current)} \u2192 ${green3(latest)}`);
2590
2893
  console.log();
2591
2894
  }
2592
2895
 
@@ -2665,12 +2968,12 @@ async function dev(rootProjectDir, options) {
2665
2968
  const basePath = rootConfig.base || "";
2666
2969
  const homePagePath = rootConfig.server?.homePagePath || basePath;
2667
2970
  console.log();
2668
- console.log(`${dim5("\u2503")} project: ${rootDir}`);
2669
- console.log(`${dim5("\u2503")} server: http://${host}:${port}${homePagePath}`);
2971
+ console.log(`${dim6("\u2503")} project: ${rootDir}`);
2972
+ console.log(`${dim6("\u2503")} server: http://${host}:${port}${homePagePath}`);
2670
2973
  if (testCmsEnabled(rootConfig)) {
2671
- console.log(`${dim5("\u2503")} cms: http://${host}:${port}/cms/`);
2974
+ console.log(`${dim6("\u2503")} cms: http://${host}:${port}/cms/`);
2672
2975
  }
2673
- console.log(`${dim5("\u2503")} mode: development`);
2976
+ console.log(`${dim6("\u2503")} mode: development`);
2674
2977
  console.log();
2675
2978
  currentServer = server.listen(port, host);
2676
2979
  const rootConfigDependencies = server.get(
@@ -2685,7 +2988,7 @@ async function dev(rootProjectDir, options) {
2685
2988
  if (dependencies.includes(file)) {
2686
2989
  console.log(
2687
2990
  `
2688
- ${dim5("\u2503")} root.config.ts changed. restarting server...`
2991
+ ${dim6("\u2503")} root.config.ts changed. restarting server...`
2689
2992
  );
2690
2993
  await restart();
2691
2994
  }
@@ -3080,7 +3383,7 @@ function testManagedVersion(service, appInfo, prefix) {
3080
3383
  import path12 from "node:path";
3081
3384
  import cookieParser2 from "cookie-parser";
3082
3385
  import { default as express2 } from "express";
3083
- import { dim as dim6 } from "kleur/colors";
3386
+ import { dim as dim7 } from "kleur/colors";
3084
3387
  import sirv2 from "sirv";
3085
3388
  async function preview(rootProjectDir, options) {
3086
3389
  process.env.NODE_ENV = "development";
@@ -3089,9 +3392,9 @@ async function preview(rootProjectDir, options) {
3089
3392
  const port = parseInt(process.env.PORT || "4007");
3090
3393
  const host = options?.host || "localhost";
3091
3394
  console.log();
3092
- console.log(`${dim6("\u2503")} project: ${rootDir}`);
3093
- console.log(`${dim6("\u2503")} server: http://${host}:${port}`);
3094
- console.log(`${dim6("\u2503")} mode: preview`);
3395
+ console.log(`${dim7("\u2503")} project: ${rootDir}`);
3396
+ console.log(`${dim7("\u2503")} server: http://${host}:${port}`);
3397
+ console.log(`${dim7("\u2503")} mode: preview`);
3095
3398
  console.log();
3096
3399
  server.listen(port, host);
3097
3400
  }
@@ -3226,7 +3529,7 @@ function rootPreviewServer500Middleware() {
3226
3529
  import path13 from "node:path";
3227
3530
  import cookieParser3 from "cookie-parser";
3228
3531
  import { default as express3 } from "express";
3229
- import { dim as dim7 } from "kleur/colors";
3532
+ import { dim as dim8 } from "kleur/colors";
3230
3533
  import sirv3 from "sirv";
3231
3534
  async function start(rootProjectDir, options) {
3232
3535
  process.env.NODE_ENV = "production";
@@ -3235,9 +3538,9 @@ async function start(rootProjectDir, options) {
3235
3538
  const port = parseInt(process.env.PORT || "4007");
3236
3539
  const host = options?.host || "localhost";
3237
3540
  console.log();
3238
- console.log(`${dim7("\u2503")} project: ${rootDir}`);
3239
- console.log(`${dim7("\u2503")} server: http://${host}:${port}`);
3240
- console.log(`${dim7("\u2503")} mode: production`);
3541
+ console.log(`${dim8("\u2503")} project: ${rootDir}`);
3542
+ console.log(`${dim8("\u2503")} server: http://${host}:${port}`);
3543
+ console.log(`${dim8("\u2503")} mode: production`);
3241
3544
  console.log();
3242
3545
  server.listen(port, host);
3243
3546
  }
@@ -3403,7 +3706,14 @@ var CliRunner = class {
3403
3706
  "--filter <urlPathRegex>",
3404
3707
  'builds the url paths that match the given regex, e.g. "/products/.*"',
3405
3708
  ""
3406
- ).action(build);
3709
+ ).addOption(
3710
+ new Option(
3711
+ "--log <mode>",
3712
+ 'build log output: "progress" (default) shows a progress indicator and a final summary, "verbose" prints one line per output file, "quiet" prints only the final summary'
3713
+ ).choices(["progress", "verbose", "quiet"])
3714
+ ).action(
3715
+ (rootProjectDir, options, cmd) => build(rootProjectDir, cmd.optsWithGlobals())
3716
+ );
3407
3717
  program.command("codegen [type] [name]").description("generates boilerplate code").option("--out <outdir>", "output dir").action(codegen);
3408
3718
  program.command("create-package [path]").alias("package").description(
3409
3719
  "creates a standalone npm package for deployment to various hosting services"
@@ -3465,4 +3775,4 @@ export {
3465
3775
  createProdServer,
3466
3776
  CliRunner
3467
3777
  };
3468
- //# sourceMappingURL=chunk-E4F6CMO3.js.map
3778
+ //# sourceMappingURL=chunk-WGQNQPWG.js.map