@tscircuit/cli 0.1.1051 → 0.1.1053

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.
package/dist/cli/main.js CHANGED
@@ -71664,7 +71664,7 @@ var registerStaticAssetLoaders = () => {
71664
71664
  // cli/main.ts
71665
71665
  var import_perfect_cli = __toESM2(require_dist2(), 1);
71666
71666
  // package.json
71667
- var version = "0.1.1050";
71667
+ var version = "0.1.1052";
71668
71668
  var package_default = {
71669
71669
  name: "@tscircuit/cli",
71670
71670
  version,
@@ -80669,15 +80669,11 @@ var buildFile = async (input, output, projectDir, options) => {
80669
80669
  }
80670
80670
  }
80671
80671
  }
80672
- if (errors.length > 0 && !options?.ignoreErrors) {
80673
- console.error(kleur_default.red(`Build failed with ${errors.length} error(s). Use --ignore-errors to continue.`));
80674
- return { ok: false };
80675
- } else {
80676
- return {
80677
- ok: true,
80678
- circuitJson
80679
- };
80680
- }
80672
+ return {
80673
+ ok: true,
80674
+ circuitJson,
80675
+ hasErrors: errors.length > 0 && !options?.ignoreErrors
80676
+ };
80681
80677
  } catch (err) {
80682
80678
  console.error(err);
80683
80679
  if (err instanceof Error) {
@@ -81875,42 +81871,27 @@ var exitBuild = (code, reason) => {
81875
81871
  // cli/build/worker-pool.ts
81876
81872
  import fs36 from "node:fs";
81877
81873
  import path38 from "node:path";
81874
+
81875
+ // lib/shared/thread-worker-pool.ts
81878
81876
  import { Worker } from "node:worker_threads";
81879
- var getWorkerEntrypointPath = () => {
81880
- const tsPath = path38.join(import.meta.dir, "build-worker-entrypoint.ts");
81881
- if (fs36.existsSync(tsPath)) {
81882
- return tsPath;
81883
- }
81884
- const jsBundledPath = path38.join(import.meta.dir, "build", "build-worker-entrypoint.js");
81885
- if (fs36.existsSync(jsBundledPath)) {
81886
- return jsBundledPath;
81887
- }
81888
- return path38.join(import.meta.dir, "build-worker-entrypoint.js");
81889
- };
81890
81877
 
81891
- class WorkerPool {
81878
+ class ThreadWorkerPool {
81892
81879
  workers = [];
81893
81880
  jobQueue = [];
81894
81881
  concurrency;
81895
- onLog;
81896
- workerEntrypointPath;
81882
+ options;
81897
81883
  initialized = false;
81898
81884
  stopped = false;
81899
81885
  stopReason = null;
81900
- stopOnFatal = false;
81901
- cancellationError = null;
81902
81886
  constructor(options) {
81887
+ this.options = options;
81903
81888
  this.concurrency = options.concurrency;
81904
- this.onLog = options.onLog;
81905
- this.workerEntrypointPath = getWorkerEntrypointPath();
81906
- this.stopOnFatal = options.stopOnFatal ?? false;
81907
- this.cancellationError = options.cancellationError ?? null;
81908
81889
  }
81909
81890
  async initWorkers() {
81910
81891
  if (this.initialized)
81911
81892
  return;
81912
81893
  for (let i = 0;i < this.concurrency; i++) {
81913
- const worker = new Worker(this.workerEntrypointPath);
81894
+ const worker = new Worker(this.options.workerEntrypointPath);
81914
81895
  const threadWorker = {
81915
81896
  worker,
81916
81897
  busy: false,
@@ -81924,38 +81905,24 @@ class WorkerPool {
81924
81905
  }
81925
81906
  setupWorkerMessageHandling(threadWorker) {
81926
81907
  threadWorker.worker.on("message", (message) => {
81927
- if (message.message_type === "worker_log") {
81928
- const logMsg = message;
81929
- if (this.onLog) {
81930
- this.onLog(logMsg.log_lines);
81931
- }
81932
- } else if (message.message_type === "build_completed") {
81933
- const completedMsg = message;
81934
- const job = threadWorker.currentJob;
81935
- if (job) {
81936
- if (this.stopOnFatal && completedMsg.isFatalError && this.cancellationError) {
81937
- this.stop(this.cancellationError);
81938
- }
81939
- job.resolve({
81940
- filePath: completedMsg.file_path,
81941
- outputPath: completedMsg.output_path,
81942
- glbOutputPath: completedMsg.glb_output_path,
81943
- previewOutputDir: completedMsg.preview_output_dir,
81944
- glbOk: completedMsg.glb_ok,
81945
- glbError: completedMsg.glb_error,
81946
- previewOk: completedMsg.preview_ok,
81947
- previewError: completedMsg.preview_error,
81948
- ok: completedMsg.ok,
81949
- isFatalError: completedMsg.isFatalError,
81950
- errors: completedMsg.errors,
81951
- warnings: completedMsg.warnings,
81952
- durationMs: completedMsg.durationMs
81953
- });
81954
- threadWorker.currentJob = null;
81955
- threadWorker.busy = false;
81956
- this.processQueue();
81957
- }
81908
+ if (this.options.isLogMessage(message)) {
81909
+ this.options.onLog?.(this.options.getLogLines(message));
81910
+ return;
81911
+ }
81912
+ if (!this.options.isCompletionMessage(message)) {
81913
+ return;
81914
+ }
81915
+ const job = threadWorker.currentJob;
81916
+ if (!job) {
81917
+ return;
81918
+ }
81919
+ if (this.options.shouldStopOnMessage?.(message) && this.options.cancellationError) {
81920
+ this.stop(this.options.cancellationError);
81958
81921
  }
81922
+ job.resolve(this.options.getResult(message));
81923
+ threadWorker.currentJob = null;
81924
+ threadWorker.busy = false;
81925
+ this.processQueue();
81959
81926
  });
81960
81927
  }
81961
81928
  setupWorkerErrorHandling(threadWorker) {
@@ -81965,11 +81932,9 @@ class WorkerPool {
81965
81932
  threadWorker.currentJob = null;
81966
81933
  threadWorker.busy = false;
81967
81934
  }
81968
- if (this.onLog) {
81969
- this.onLog([
81970
- `Worker error: ${error instanceof Error ? error.message : String(error)}`
81971
- ]);
81972
- }
81935
+ this.options.onLog?.([
81936
+ `Worker error: ${error instanceof Error ? error.message : String(error)}`
81937
+ ]);
81973
81938
  });
81974
81939
  threadWorker.worker.on("exit", (code) => {
81975
81940
  if (code !== 0 && threadWorker.currentJob) {
@@ -81980,28 +81945,20 @@ class WorkerPool {
81980
81945
  });
81981
81946
  }
81982
81947
  processQueue() {
81983
- if (this.stopped)
81984
- return;
81985
- if (this.jobQueue.length === 0)
81948
+ if (this.stopped || this.jobQueue.length === 0) {
81986
81949
  return;
81987
- const availableWorker = this.workers.find((w) => !w.busy);
81988
- if (!availableWorker)
81950
+ }
81951
+ const availableWorker = this.workers.find((worker) => !worker.busy);
81952
+ if (!availableWorker) {
81989
81953
  return;
81990
- const job = this.jobQueue.shift();
81991
- if (!job)
81954
+ }
81955
+ const queuedJob = this.jobQueue.shift();
81956
+ if (!queuedJob) {
81992
81957
  return;
81958
+ }
81993
81959
  availableWorker.busy = true;
81994
- availableWorker.currentJob = job;
81995
- const message = {
81996
- message_type: "build_file",
81997
- file_path: job.filePath,
81998
- output_path: job.outputPath,
81999
- glb_output_path: job.glbOutputPath,
82000
- preview_output_dir: job.previewOutputDir,
82001
- project_dir: job.projectDir,
82002
- options: job.options
82003
- };
82004
- availableWorker.worker.postMessage(message);
81960
+ availableWorker.currentJob = queuedJob;
81961
+ availableWorker.worker.postMessage(this.options.createMessage(queuedJob.job));
82005
81962
  }
82006
81963
  async queueJob(job) {
82007
81964
  if (this.stopped) {
@@ -82009,12 +81966,7 @@ class WorkerPool {
82009
81966
  }
82010
81967
  await this.initWorkers();
82011
81968
  return new Promise((resolve4, reject) => {
82012
- const queuedJob = {
82013
- ...job,
82014
- resolve: resolve4,
82015
- reject
82016
- };
82017
- this.jobQueue.push(queuedJob);
81969
+ this.jobQueue.push({ job, resolve: resolve4, reject });
82018
81970
  this.processQueue();
82019
81971
  });
82020
81972
  }
@@ -82028,35 +81980,70 @@ class WorkerPool {
82028
81980
  }
82029
81981
  this.jobQueue = [];
82030
81982
  }
82031
- async runUntilComplete() {
82032
- return new Promise((resolve4) => {
82033
- const checkComplete = () => {
82034
- const allIdle = this.workers.every((w) => !w.busy);
82035
- const queueEmpty = this.jobQueue.length === 0;
82036
- if (allIdle && queueEmpty) {
82037
- resolve4();
82038
- } else {
82039
- setTimeout(checkComplete, 50);
82040
- }
82041
- };
82042
- checkComplete();
82043
- });
82044
- }
82045
81983
  async terminate() {
82046
- const terminatePromises = this.workers.map((w) => w.worker.terminate());
82047
- await Promise.all(terminatePromises);
81984
+ await Promise.all(this.workers.map((worker) => worker.worker.terminate()));
82048
81985
  this.workers = [];
82049
81986
  this.initialized = false;
82050
81987
  }
82051
81988
  }
81989
+
81990
+ // cli/build/worker-pool.ts
81991
+ var getWorkerEntrypointPath = () => {
81992
+ const tsPath = path38.join(import.meta.dir, "build.worker.ts");
81993
+ if (fs36.existsSync(tsPath)) {
81994
+ return tsPath;
81995
+ }
81996
+ const jsBundledPath = path38.join(import.meta.dir, "build", "build.worker.js");
81997
+ if (fs36.existsSync(jsBundledPath)) {
81998
+ return jsBundledPath;
81999
+ }
82000
+ return path38.join(import.meta.dir, "build.worker.js");
82001
+ };
82052
82002
  async function buildFilesWithWorkerPool(options) {
82053
82003
  const cancellationError = new Error("Build cancelled due fatal error");
82054
82004
  const poolConcurrency = Math.max(1, Math.min(options.concurrency, options.files.length));
82055
- const pool = new WorkerPool({
82005
+ const pool = new ThreadWorkerPool({
82056
82006
  concurrency: poolConcurrency,
82057
- onLog: options.onLog,
82058
- stopOnFatal: options.stopOnFatal,
82059
- cancellationError
82007
+ workerEntrypointPath: getWorkerEntrypointPath(),
82008
+ createMessage: (job) => ({
82009
+ message_type: "build_file",
82010
+ file_path: job.filePath,
82011
+ output_path: job.outputPath,
82012
+ glb_output_path: job.glbOutputPath,
82013
+ preview_output_dir: job.previewOutputDir,
82014
+ project_dir: job.projectDir,
82015
+ options: job.options
82016
+ }),
82017
+ isLogMessage: (message) => message.message_type === "worker_log",
82018
+ getLogLines: (message) => message.message_type === "worker_log" ? message.log_lines : [],
82019
+ isCompletionMessage: (message) => message.message_type === "build_completed",
82020
+ getResult: (message) => {
82021
+ const completedMessage = message;
82022
+ return {
82023
+ filePath: completedMessage.file_path,
82024
+ outputPath: completedMessage.output_path,
82025
+ glbOutputPath: completedMessage.glb_output_path,
82026
+ previewOutputDir: completedMessage.preview_output_dir,
82027
+ glbOk: completedMessage.glb_ok,
82028
+ glbError: completedMessage.glb_error,
82029
+ previewOk: completedMessage.preview_ok,
82030
+ previewError: completedMessage.preview_error,
82031
+ ok: completedMessage.ok,
82032
+ hasErrors: completedMessage.hasErrors,
82033
+ isFatalError: completedMessage.isFatalError,
82034
+ errors: completedMessage.errors,
82035
+ warnings: completedMessage.warnings,
82036
+ durationMs: completedMessage.durationMs
82037
+ };
82038
+ },
82039
+ shouldStopOnMessage: (message) => {
82040
+ if (!options.stopOnFatal || message.message_type !== "build_completed") {
82041
+ return false;
82042
+ }
82043
+ return Boolean(message.isFatalError);
82044
+ },
82045
+ cancellationError,
82046
+ onLog: options.onLog
82060
82047
  });
82061
82048
  const results = [];
82062
82049
  const promises = [];
@@ -82235,6 +82222,9 @@ var registerBuild = (program2) => {
82235
82222
  outputPath,
82236
82223
  ok: buildOutcome.ok
82237
82224
  });
82225
+ if (buildOutcome.hasErrors) {
82226
+ hasErrors = true;
82227
+ }
82238
82228
  if (!buildOutcome.ok) {
82239
82229
  hasErrors = true;
82240
82230
  if (buildOutcome.isFatalError) {
@@ -82331,7 +82321,11 @@ var registerBuild = (program2) => {
82331
82321
  onJobComplete: async (result) => {
82332
82322
  const relative3 = path39.relative(projectDir, result.filePath);
82333
82323
  if (result.ok) {
82334
- console.log(kleur_default.green(`✓ ${relative3}`));
82324
+ if (result.hasErrors) {
82325
+ console.log(kleur_default.yellow(`⚠ ${relative3} (${result.errors.length} error(s))`));
82326
+ } else {
82327
+ console.log(kleur_default.green(`✓ ${relative3}`));
82328
+ }
82335
82329
  } else {
82336
82330
  console.log(kleur_default.red(`✗ ${relative3}`));
82337
82331
  for (const error of result.errors) {
@@ -82347,6 +82341,7 @@ var registerBuild = (program2) => {
82347
82341
  }
82348
82342
  await processBuildResult(result.filePath, result.outputPath, {
82349
82343
  ok: result.ok,
82344
+ hasErrors: result.hasErrors,
82350
82345
  isFatalError: result.isFatalError
82351
82346
  });
82352
82347
  if (resolvedOptions?.glbs && result.ok) {
@@ -82511,7 +82506,7 @@ var registerBuild = (program2) => {
82511
82506
  }
82512
82507
  }
82513
82508
  }
82514
- const shouldExitNonZero = hasFatalErrors || hasErrors && !resolvedOptions?.ignoreErrors;
82509
+ const shouldExitNonZero = hasFatalErrors;
82515
82510
  const successCount = builtFiles.filter((f) => f.ok).length;
82516
82511
  const failCount = builtFiles.length - successCount;
82517
82512
  const enabledOpts = [
@@ -82548,7 +82543,7 @@ var registerBuild = (program2) => {
82548
82543
  ⚠ Build completed with errors`) : kleur_default.green(`
82549
82544
  ✓ Done`));
82550
82545
  if (shouldExitNonZero) {
82551
- exitBuild(1, hasFatalErrors ? "fatal circuit build errors occurred" : "build errors occurred and --ignore-errors was not enabled");
82546
+ exitBuild(1, "fatal circuit build errors occurred");
82552
82547
  }
82553
82548
  exitBuild(0, "build finished successfully");
82554
82549
  } catch (error) {
@@ -177556,8 +177551,89 @@ function applyCameraPreset(preset, cam) {
177556
177551
  }
177557
177552
 
177558
177553
  // lib/shared/snapshot-project.ts
177559
- import fs61 from "node:fs";
177554
+ import path64 from "node:path";
177555
+
177556
+ // cli/snapshot/worker-pool.ts
177557
+ import fs60 from "node:fs";
177560
177558
  import path62 from "node:path";
177559
+ var getWorkerEntrypointPath2 = () => {
177560
+ const tsPath = path62.join(import.meta.dir, "snapshot.worker.ts");
177561
+ if (fs60.existsSync(tsPath)) {
177562
+ return tsPath;
177563
+ }
177564
+ const jsBundledPath = path62.join(import.meta.dir, "snapshot", "snapshot.worker.js");
177565
+ if (fs60.existsSync(jsBundledPath)) {
177566
+ return jsBundledPath;
177567
+ }
177568
+ return path62.join(import.meta.dir, "snapshot.worker.js");
177569
+ };
177570
+ var snapshotFilesWithWorkerPool = async (options) => {
177571
+ const cancellationError = new Error("Snapshot cancelled due to file failure");
177572
+ const poolConcurrency = Math.max(1, Math.min(options.concurrency, options.files.length));
177573
+ const pool = new ThreadWorkerPool({
177574
+ concurrency: poolConcurrency,
177575
+ workerEntrypointPath: getWorkerEntrypointPath2(),
177576
+ createMessage: (job) => ({
177577
+ message_type: "snapshot_file",
177578
+ file_path: job.filePath,
177579
+ project_dir: job.projectDir,
177580
+ snapshots_dir_name: job.snapshotsDirName,
177581
+ options: {
177582
+ update: job.options.update,
177583
+ threeD: job.options.threeD,
177584
+ pcbOnly: job.options.pcbOnly,
177585
+ schematicOnly: job.options.schematicOnly,
177586
+ forceUpdate: job.options.forceUpdate,
177587
+ platformConfig: job.options.platformConfig,
177588
+ createDiff: job.options.createDiff,
177589
+ cameraPreset: job.options.cameraPreset
177590
+ }
177591
+ }),
177592
+ isLogMessage: (message) => message.message_type === "worker_log",
177593
+ getLogLines: (message) => message.message_type === "worker_log" ? message.log_lines : [],
177594
+ isCompletionMessage: (message) => message.message_type === "snapshot_completed",
177595
+ getResult: (message) => {
177596
+ const completedMessage = message;
177597
+ return {
177598
+ filePath: completedMessage.file_path,
177599
+ result: completedMessage.result
177600
+ };
177601
+ },
177602
+ shouldStopOnMessage: (message) => {
177603
+ if (!options.stopOnFailure || message.message_type !== "snapshot_completed") {
177604
+ return false;
177605
+ }
177606
+ return !message.result.ok;
177607
+ },
177608
+ cancellationError,
177609
+ onLog: options.onLog
177610
+ });
177611
+ const results = [];
177612
+ const jobs = options.files.map((filePath) => pool.queueJob({
177613
+ filePath,
177614
+ projectDir: options.projectDir,
177615
+ snapshotsDirName: options.snapshotsDirName,
177616
+ options: options.snapshotOptions
177617
+ }).then(async (result) => {
177618
+ results.push(result);
177619
+ await options.onJobComplete?.(result);
177620
+ return result;
177621
+ }));
177622
+ const settledResults = await Promise.allSettled(jobs);
177623
+ for (const settledResult of settledResults) {
177624
+ if (settledResult.status === "rejected" && settledResult.reason !== cancellationError) {
177625
+ throw settledResult.reason;
177626
+ }
177627
+ }
177628
+ if (typeof Bun === "undefined") {
177629
+ await pool.terminate();
177630
+ }
177631
+ return results;
177632
+ };
177633
+
177634
+ // lib/shared/process-snapshot-file.ts
177635
+ import fs62 from "node:fs";
177636
+ import path63 from "node:path";
177561
177637
  import {
177562
177638
  convertCircuitJsonToGltf as convertCircuitJsonToGltf5,
177563
177639
  getBestCameraPosition
@@ -177566,12 +177642,11 @@ import {
177566
177642
  convertCircuitJsonToPcbSvg as convertCircuitJsonToPcbSvg3,
177567
177643
  convertCircuitJsonToSchematicSvg as convertCircuitJsonToSchematicSvg3
177568
177644
  } from "circuit-to-svg";
177569
- import looksSame2 from "looks-same";
177570
177645
  import { renderGLTFToPNGBufferFromGLBBuffer as renderGLTFToPNGBufferFromGLBBuffer2 } from "poppygl";
177571
177646
 
177572
177647
  // lib/shared/compare-images.ts
177573
177648
  import looksSame from "looks-same";
177574
- import fs60 from "node:fs/promises";
177649
+ import fs61 from "node:fs/promises";
177575
177650
  var compareAndCreateDiff = async (buffer1, buffer2, diffPath, createDiff = true) => {
177576
177651
  const { equal: equal2 } = await looksSame(buffer1, buffer2, {
177577
177652
  strict: false,
@@ -177587,12 +177662,200 @@ var compareAndCreateDiff = async (buffer1, buffer2, diffPath, createDiff = true)
177587
177662
  tolerance: 2
177588
177663
  });
177589
177664
  } else {
177590
- await fs60.writeFile(diffPath, buffer2);
177665
+ await fs61.writeFile(diffPath, buffer2);
177591
177666
  }
177592
177667
  }
177593
177668
  return { equal: equal2 };
177594
177669
  };
177595
177670
 
177671
+ // lib/shared/process-snapshot-file.ts
177672
+ var isCircuitJsonFile = (filePath) => {
177673
+ const normalizedPath = filePath.toLowerCase().replaceAll("\\", "/");
177674
+ return normalizedPath.endsWith(".circuit.json") || normalizedPath.endsWith("/circuit.json");
177675
+ };
177676
+ var processSnapshotFile = async ({
177677
+ file,
177678
+ projectDir,
177679
+ snapshotsDirName,
177680
+ update,
177681
+ threeD,
177682
+ pcbOnly,
177683
+ schematicOnly,
177684
+ forceUpdate,
177685
+ platformConfig: platformConfig2,
177686
+ createDiff,
177687
+ cameraPreset
177688
+ }) => {
177689
+ const relativeFilePath = path63.relative(projectDir, file);
177690
+ const successPaths = [];
177691
+ const warningMessages = [];
177692
+ const mismatches = [];
177693
+ let didUpdate = false;
177694
+ let circuitJson;
177695
+ let pcbSvg;
177696
+ let schSvg;
177697
+ try {
177698
+ if (isCircuitJsonFile(file)) {
177699
+ const parsed = JSON.parse(fs62.readFileSync(file, "utf-8"));
177700
+ circuitJson = Array.isArray(parsed) ? parsed : [];
177701
+ } else {
177702
+ const completePlatformConfig = getCompletePlatformConfig(platformConfig2);
177703
+ const result = await generateCircuitJson({
177704
+ filePath: file,
177705
+ platformConfig: completePlatformConfig
177706
+ });
177707
+ circuitJson = result.circuitJson;
177708
+ }
177709
+ } catch (error) {
177710
+ const errorMessage = error instanceof Error ? error.message : String(error);
177711
+ return {
177712
+ ok: false,
177713
+ didUpdate: false,
177714
+ successPaths,
177715
+ warningMessages,
177716
+ mismatches,
177717
+ errorMessage: kleur_default.red(`
177718
+ ❌ Failed to generate circuit JSON for ${relativeFilePath}:
177719
+ `) + kleur_default.red(` ${errorMessage}
177720
+ `)
177721
+ };
177722
+ }
177723
+ try {
177724
+ pcbSvg = convertCircuitJsonToPcbSvg3(circuitJson);
177725
+ } catch (error) {
177726
+ const errorMessage = error instanceof Error ? error.message : String(error);
177727
+ return {
177728
+ ok: false,
177729
+ didUpdate: false,
177730
+ successPaths,
177731
+ warningMessages,
177732
+ mismatches,
177733
+ errorMessage: kleur_default.red(`
177734
+ ❌ Failed to generate PCB SVG for ${relativeFilePath}:
177735
+ `) + kleur_default.red(` ${errorMessage}
177736
+ `)
177737
+ };
177738
+ }
177739
+ try {
177740
+ schSvg = convertCircuitJsonToSchematicSvg3(circuitJson);
177741
+ } catch (error) {
177742
+ const errorMessage = error instanceof Error ? error.message : String(error);
177743
+ return {
177744
+ ok: false,
177745
+ didUpdate: false,
177746
+ successPaths,
177747
+ warningMessages,
177748
+ mismatches,
177749
+ errorMessage: kleur_default.red(`
177750
+ ❌ Failed to generate schematic SVG for ${relativeFilePath}:
177751
+ `) + kleur_default.red(` ${errorMessage}
177752
+ `)
177753
+ };
177754
+ }
177755
+ let png3d = null;
177756
+ if (threeD) {
177757
+ try {
177758
+ const glbBuffer = await convertCircuitJsonToGltf5(circuitJson, getCircuitJsonToGltfOptions({ format: "glb" }));
177759
+ if (!(glbBuffer instanceof ArrayBuffer)) {
177760
+ throw new Error("Expected ArrayBuffer from convertCircuitJsonToGltf with glb format");
177761
+ }
177762
+ let cameraOptions = getBestCameraPosition(circuitJson);
177763
+ if (cameraPreset) {
177764
+ cameraOptions = applyCameraPreset(cameraPreset, cameraOptions);
177765
+ }
177766
+ png3d = await renderGLTFToPNGBufferFromGLBBuffer2(glbBuffer, cameraOptions);
177767
+ } catch (error) {
177768
+ const errorMessage = error instanceof Error ? error.message : String(error);
177769
+ if (errorMessage.includes("No pcb_board found in circuit JSON")) {
177770
+ const fileDir = path63.dirname(file);
177771
+ const relativeDir = path63.relative(projectDir, fileDir);
177772
+ const snapDir2 = snapshotsDirName ? path63.join(projectDir, snapshotsDirName, relativeDir) : path63.join(fileDir, "__snapshots__");
177773
+ const base2 = path63.basename(file).replace(/\.[^.]+$/, "");
177774
+ const snap3dPath = path63.join(snapDir2, `${base2}-3d.snap.png`);
177775
+ const existing3dSnapshot = fs62.existsSync(snap3dPath);
177776
+ if (existing3dSnapshot) {
177777
+ return {
177778
+ ok: false,
177779
+ didUpdate: false,
177780
+ successPaths,
177781
+ warningMessages,
177782
+ mismatches,
177783
+ errorMessage: kleur_default.red(`
177784
+ ❌ Failed to generate 3D snapshot for ${relativeFilePath}:
177785
+ `) + kleur_default.red(` No pcb_board found in circuit JSON
177786
+ `) + kleur_default.red(` Existing snapshot: ${path63.relative(projectDir, snap3dPath)}
177787
+ `)
177788
+ };
177789
+ }
177790
+ warningMessages.push(kleur_default.red(`⚠️ Skipping 3D snapshot for ${relativeFilePath}:`) + kleur_default.red(` No pcb_board found in circuit JSON`));
177791
+ png3d = null;
177792
+ } else {
177793
+ return {
177794
+ ok: false,
177795
+ didUpdate: false,
177796
+ successPaths,
177797
+ warningMessages,
177798
+ mismatches,
177799
+ errorMessage: kleur_default.red(`
177800
+ ❌ Failed to generate 3D snapshot for ${relativeFilePath}:
177801
+ `) + kleur_default.red(` ${errorMessage}
177802
+ `)
177803
+ };
177804
+ }
177805
+ }
177806
+ }
177807
+ const snapDir = snapshotsDirName ? path63.join(projectDir, snapshotsDirName, path63.relative(projectDir, path63.dirname(file))) : path63.join(path63.dirname(file), "__snapshots__");
177808
+ fs62.mkdirSync(snapDir, { recursive: true });
177809
+ const base = path63.basename(file).replace(/\.[^.]+$/, "");
177810
+ const snapshots = [];
177811
+ if (pcbOnly || !schematicOnly) {
177812
+ snapshots.push({ type: "pcb", content: pcbSvg, isBinary: false });
177813
+ }
177814
+ if (schematicOnly || !pcbOnly) {
177815
+ snapshots.push({ type: "schematic", content: schSvg, isBinary: false });
177816
+ }
177817
+ if (threeD && png3d) {
177818
+ snapshots.push({ type: "3d", content: png3d, isBinary: true });
177819
+ }
177820
+ for (const snapshot of snapshots) {
177821
+ const { type } = snapshot;
177822
+ const is3d = type === "3d";
177823
+ const snapPath = path63.join(snapDir, `${base}-${type}.snap.${is3d ? "png" : "svg"}`);
177824
+ const existing = fs62.existsSync(snapPath);
177825
+ const newContentBuffer = snapshot.isBinary ? snapshot.content : Buffer.from(snapshot.content, "utf8");
177826
+ const newContentForFile = snapshot.content;
177827
+ if (!existing) {
177828
+ fs62.writeFileSync(snapPath, newContentForFile);
177829
+ successPaths.push(path63.relative(projectDir, snapPath));
177830
+ didUpdate = true;
177831
+ continue;
177832
+ }
177833
+ const oldContentBuffer = fs62.readFileSync(snapPath);
177834
+ const diffPath = snapPath.replace(is3d ? ".snap.png" : ".snap.svg", is3d ? ".diff.png" : ".diff.svg");
177835
+ const { equal: equal2 } = await compareAndCreateDiff(oldContentBuffer, newContentBuffer, diffPath, createDiff);
177836
+ if (update) {
177837
+ if (!forceUpdate && equal2) {
177838
+ successPaths.push(path63.relative(projectDir, snapPath));
177839
+ } else {
177840
+ fs62.writeFileSync(snapPath, newContentForFile);
177841
+ successPaths.push(path63.relative(projectDir, snapPath));
177842
+ didUpdate = true;
177843
+ }
177844
+ } else if (!equal2) {
177845
+ mismatches.push(createDiff ? `${snapPath} (diff: ${diffPath})` : snapPath);
177846
+ } else {
177847
+ successPaths.push(path63.relative(projectDir, snapPath));
177848
+ }
177849
+ }
177850
+ return {
177851
+ ok: true,
177852
+ didUpdate,
177853
+ successPaths,
177854
+ warningMessages,
177855
+ mismatches
177856
+ };
177857
+ };
177858
+
177596
177859
  // lib/shared/snapshot-project.ts
177597
177860
  var snapshotProject = async ({
177598
177861
  update = false,
@@ -177607,7 +177870,8 @@ var snapshotProject = async ({
177607
177870
  onSuccess = (msg) => console.log(msg),
177608
177871
  platformConfig: platformConfig2,
177609
177872
  createDiff = false,
177610
- cameraPreset
177873
+ cameraPreset,
177874
+ concurrency = 1
177611
177875
  } = {}) => {
177612
177876
  if (cameraPreset) {
177613
177877
  threeD = true;
@@ -177617,7 +177881,7 @@ var snapshotProject = async ({
177617
177881
  ...DEFAULT_IGNORED_PATTERNS,
177618
177882
  ...ignored.map(normalizeIgnorePattern)
177619
177883
  ];
177620
- const resolvedPaths = filePaths.map((f2) => path62.resolve(projectDir, f2));
177884
+ const resolvedPaths = filePaths.map((f2) => path64.resolve(projectDir, f2));
177621
177885
  const boardFiles = findBoardFiles({
177622
177886
  projectDir,
177623
177887
  ignore,
@@ -177630,141 +177894,78 @@ var snapshotProject = async ({
177630
177894
  const snapshotsDirName = getSnapshotsDir(projectDir);
177631
177895
  const mismatches = [];
177632
177896
  let didUpdate = false;
177633
- const isCircuitJsonFile = (filePath) => {
177634
- const normalizedPath = filePath.toLowerCase().replaceAll("\\", "/");
177635
- return normalizedPath.endsWith(".circuit.json") || normalizedPath.endsWith("/circuit.json");
177636
- };
177637
- for (const file of boardFiles) {
177638
- const relativeFilePath = path62.relative(projectDir, file);
177639
- let circuitJson;
177640
- let pcbSvg;
177641
- let schSvg;
177642
- try {
177643
- if (isCircuitJsonFile(file)) {
177644
- const parsed = JSON.parse(fs61.readFileSync(file, "utf-8"));
177645
- circuitJson = Array.isArray(parsed) ? parsed : [];
177646
- } else {
177647
- const completePlatformConfig = getCompletePlatformConfig(platformConfig2);
177648
- const result = await generateCircuitJson({
177649
- filePath: file,
177650
- platformConfig: completePlatformConfig
177651
- });
177652
- circuitJson = result.circuitJson;
177653
- }
177654
- } catch (error) {
177655
- const errorMessage = error instanceof Error ? error.message : String(error);
177656
- onError(kleur_default.red(`
177657
- ❌ Failed to generate circuit JSON for ${relativeFilePath}:
177658
- `) + kleur_default.red(` ${errorMessage}
177659
- `));
177660
- return onExit2(1);
177897
+ const concurrencyValue = Math.max(1, concurrency);
177898
+ const processResult = (result) => {
177899
+ for (const warningMessage of result.warningMessages) {
177900
+ console.log(warningMessage);
177661
177901
  }
177662
- try {
177663
- pcbSvg = convertCircuitJsonToPcbSvg3(circuitJson);
177664
- } catch (error) {
177665
- const errorMessage = error instanceof Error ? error.message : String(error);
177666
- onError(kleur_default.red(`
177667
- ❌ Failed to generate PCB SVG for ${relativeFilePath}:
177668
- `) + kleur_default.red(` ${errorMessage}
177669
- `));
177670
- return onExit2(1);
177902
+ for (const successPath of result.successPaths) {
177903
+ console.log("✅", kleur_default.gray(successPath));
177671
177904
  }
177905
+ didUpdate = didUpdate || result.didUpdate;
177906
+ mismatches.push(...result.mismatches);
177907
+ };
177908
+ if (concurrencyValue > 1 && boardFiles.length > 1) {
177909
+ console.log(`Generating snapshots for ${boardFiles.length} file(s) with concurrency ${concurrencyValue}...`);
177910
+ let firstErrorMessage;
177672
177911
  try {
177673
- schSvg = convertCircuitJsonToSchematicSvg3(circuitJson);
177912
+ await snapshotFilesWithWorkerPool({
177913
+ files: boardFiles,
177914
+ projectDir,
177915
+ snapshotsDirName,
177916
+ concurrency: concurrencyValue,
177917
+ snapshotOptions: {
177918
+ update,
177919
+ threeD,
177920
+ pcbOnly,
177921
+ schematicOnly,
177922
+ forceUpdate,
177923
+ platformConfig: platformConfig2,
177924
+ createDiff,
177925
+ cameraPreset
177926
+ },
177927
+ stopOnFailure: true,
177928
+ onLog: (lines) => {
177929
+ for (const line2 of lines) {
177930
+ console.log(line2);
177931
+ }
177932
+ },
177933
+ onJobComplete: (jobResult) => {
177934
+ const { result } = jobResult;
177935
+ processResult(result);
177936
+ if (!result.ok && !firstErrorMessage) {
177937
+ firstErrorMessage = result.errorMessage ?? "Snapshot generation failed";
177938
+ }
177939
+ }
177940
+ });
177674
177941
  } catch (error) {
177675
177942
  const errorMessage = error instanceof Error ? error.message : String(error);
177676
- onError(kleur_default.red(`
177677
- ❌ Failed to generate schematic SVG for ${relativeFilePath}:
177678
- `) + kleur_default.red(` ${errorMessage}
177679
- `));
177943
+ onError(kleur_default.red(errorMessage));
177680
177944
  return onExit2(1);
177681
177945
  }
177682
- let png3d = null;
177683
- if (threeD) {
177684
- try {
177685
- const glbBuffer = await convertCircuitJsonToGltf5(circuitJson, getCircuitJsonToGltfOptions({ format: "glb" }));
177686
- if (!(glbBuffer instanceof ArrayBuffer)) {
177687
- throw new Error("Expected ArrayBuffer from convertCircuitJsonToGltf with glb format");
177688
- }
177689
- let cameraOptions = getBestCameraPosition(circuitJson);
177690
- if (cameraPreset) {
177691
- cameraOptions = applyCameraPreset(cameraPreset, cameraOptions);
177692
- }
177693
- png3d = await renderGLTFToPNGBufferFromGLBBuffer2(glbBuffer, cameraOptions);
177694
- } catch (error) {
177695
- const errorMessage = error instanceof Error ? error.message : String(error);
177696
- if (errorMessage.includes("No pcb_board found in circuit JSON")) {
177697
- const fileDir = path62.dirname(file);
177698
- const relativeDir = path62.relative(projectDir, fileDir);
177699
- const snapDir2 = snapshotsDirName ? path62.join(projectDir, snapshotsDirName, relativeDir) : path62.join(fileDir, "__snapshots__");
177700
- const base2 = path62.basename(file).replace(/\.[^.]+$/, "");
177701
- const snap3dPath = path62.join(snapDir2, `${base2}-3d.snap.png`);
177702
- const existing3dSnapshot = fs61.existsSync(snap3dPath);
177703
- if (existing3dSnapshot) {
177704
- onError(kleur_default.red(`
177705
- ❌ Failed to generate 3D snapshot for ${relativeFilePath}:
177706
- `) + kleur_default.red(` No pcb_board found in circuit JSON
177707
- `) + kleur_default.red(` Existing snapshot: ${path62.relative(projectDir, snap3dPath)}
177708
- `));
177709
- return onExit2(1);
177710
- } else {
177711
- console.log(kleur_default.red(`⚠️ Skipping 3D snapshot for ${relativeFilePath}:`) + kleur_default.red(` No pcb_board found in circuit JSON`));
177712
- png3d = null;
177713
- }
177714
- } else {
177715
- onError(kleur_default.red(`
177716
- ❌ Failed to generate 3D snapshot for ${relativeFilePath}:
177717
- `) + kleur_default.red(` ${errorMessage}
177718
- `));
177719
- return onExit2(1);
177720
- }
177721
- }
177722
- }
177723
- const snapDir = snapshotsDirName ? path62.join(projectDir, snapshotsDirName, path62.relative(projectDir, path62.dirname(file))) : path62.join(path62.dirname(file), "__snapshots__");
177724
- fs61.mkdirSync(snapDir, { recursive: true });
177725
- const base = path62.basename(file).replace(/\.[^.]+$/, "");
177726
- const snapshots = [];
177727
- if (pcbOnly || !schematicOnly) {
177728
- snapshots.push({ type: "pcb", content: pcbSvg, isBinary: false });
177729
- }
177730
- if (schematicOnly || !pcbOnly) {
177731
- snapshots.push({ type: "schematic", content: schSvg, isBinary: false });
177732
- }
177733
- if (threeD && png3d) {
177734
- snapshots.push({ type: "3d", content: png3d, isBinary: true });
177735
- }
177736
- if (!looksSame2) {
177737
- console.error("looks-same is required. Install it with 'bun add -d looks-same'");
177946
+ if (firstErrorMessage) {
177947
+ onError(firstErrorMessage);
177738
177948
  return onExit2(1);
177739
177949
  }
177740
- for (const snapshot of snapshots) {
177741
- const { type } = snapshot;
177742
- const is3d = type === "3d";
177743
- const snapPath = path62.join(snapDir, `${base}-${type}.snap.${is3d ? "png" : "svg"}`);
177744
- const existing = fs61.existsSync(snapPath);
177745
- const newContentBuffer = snapshot.isBinary ? snapshot.content : Buffer.from(snapshot.content, "utf8");
177746
- const newContentForFile = snapshot.content;
177747
- if (!existing) {
177748
- fs61.writeFileSync(snapPath, newContentForFile);
177749
- console.log("✅", kleur_default.gray(path62.relative(projectDir, snapPath)));
177750
- didUpdate = true;
177751
- continue;
177752
- }
177753
- const oldContentBuffer = fs61.readFileSync(snapPath);
177754
- const diffPath = snapPath.replace(is3d ? ".snap.png" : ".snap.svg", is3d ? ".diff.png" : ".diff.svg");
177755
- const { equal: equal2 } = await compareAndCreateDiff(oldContentBuffer, newContentBuffer, diffPath, createDiff);
177756
- if (update) {
177757
- if (!forceUpdate && equal2) {
177758
- console.log("✅", kleur_default.gray(path62.relative(projectDir, snapPath)));
177759
- } else {
177760
- fs61.writeFileSync(snapPath, newContentForFile);
177761
- console.log("✅", kleur_default.gray(path62.relative(projectDir, snapPath)));
177762
- didUpdate = true;
177763
- }
177764
- } else if (!equal2) {
177765
- mismatches.push(createDiff ? `${snapPath} (diff: ${diffPath})` : snapPath);
177766
- } else {
177767
- console.log("✅", kleur_default.gray(path62.relative(projectDir, snapPath)));
177950
+ } else {
177951
+ for (const file of boardFiles) {
177952
+ const result = await processSnapshotFile({
177953
+ file,
177954
+ projectDir,
177955
+ snapshotsDirName,
177956
+ update,
177957
+ threeD,
177958
+ pcbOnly,
177959
+ schematicOnly,
177960
+ forceUpdate,
177961
+ platformConfig: platformConfig2,
177962
+ createDiff,
177963
+ cameraPreset
177964
+ });
177965
+ processResult(result);
177966
+ if (!result.ok) {
177967
+ onError(result.errorMessage ?? "Snapshot generation failed");
177968
+ return onExit2(1);
177768
177969
  }
177769
177970
  }
177770
177971
  }
@@ -177786,12 +177987,13 @@ Run with --update to fix.`);
177786
177987
 
177787
177988
  // cli/snapshot/register.ts
177788
177989
  var registerSnapshot = (program3) => {
177789
- program3.command("snapshot").argument("[path]", "Path to file, directory, or glob pattern (e.g., 'examples/**/*.tsx')").description("Generate schematic and PCB snapshots (add --3d for 3d preview)").option("-u, --update", "Update snapshots on disk").option("--force-update", "Force update snapshots even if they match").option("--3d", "Generate 3d preview snapshots").option("--pcb-only", "Generate only PCB snapshots").option("--schematic-only", "Generate only schematic snapshots").option("--disable-parts-engine", "Disable the parts engine").option("--camera-preset <preset>", `Camera angle preset for 3D snapshots (implies --3d). Valid presets: ${CAMERA_PRESET_NAMES.join(", ")}`).option("--ci", "Enable CI mode with snapshot diff artifacts").option("--test", "Enable test mode with snapshot diff artifacts").action(async (target, options) => {
177990
+ program3.command("snapshot").argument("[path]", "Path to file, directory, or glob pattern (e.g., 'examples/**/*.tsx')").description("Generate schematic and PCB snapshots (add --3d for 3d preview)").option("-u, --update", "Update snapshots on disk").option("--force-update", "Force update snapshots even if they match").option("--3d", "Generate 3d preview snapshots").option("--pcb-only", "Generate only PCB snapshots").option("--schematic-only", "Generate only schematic snapshots").option("--disable-parts-engine", "Disable the parts engine").option("--camera-preset <preset>", `Camera angle preset for 3D snapshots (implies --3d). Valid presets: ${CAMERA_PRESET_NAMES.join(", ")}`).option("--ci", "Enable CI mode with snapshot diff artifacts").option("--test", "Enable test mode with snapshot diff artifacts").option("--concurrency <number>", "Number of files to snapshot in parallel (default: 1)", "1").action(async (target, options) => {
177790
177991
  if (options.cameraPreset && !CAMERA_PRESET_NAMES.includes(options.cameraPreset)) {
177791
177992
  console.error(`Unknown camera preset "${options.cameraPreset}". Valid presets: ${CAMERA_PRESET_NAMES.join(", ")}`);
177792
177993
  process.exit(1);
177793
177994
  }
177794
177995
  await snapshotProject({
177996
+ concurrency: Math.max(1, Number.parseInt(options.concurrency || "1", 10)),
177795
177997
  update: options.update ?? false,
177796
177998
  threeD: options["3d"] ?? false,
177797
177999
  pcbOnly: options.pcbOnly ?? false,
@@ -177809,7 +178011,7 @@ var registerSnapshot = (program3) => {
177809
178011
  };
177810
178012
 
177811
178013
  // cli/transpile/register.ts
177812
- import path63 from "node:path";
178014
+ import path65 from "node:path";
177813
178015
  var registerTranspile = (program3) => {
177814
178016
  program3.command("transpile").description("Transpile TypeScript/TSX to JavaScript (ESM, CommonJS, and type declarations)").argument("[file]", "Path to the entry file").action(async (file) => {
177815
178017
  try {
@@ -177817,7 +178019,7 @@ var registerTranspile = (program3) => {
177817
178019
  fileOrDir: file,
177818
178020
  includeBoardFiles: false
177819
178021
  });
177820
- const distDir = path63.join(projectDir, "dist");
178022
+ const distDir = path65.join(projectDir, "dist");
177821
178023
  validateMainInDist(projectDir, distDir);
177822
178024
  console.log("Transpiling entry file...");
177823
178025
  const entryFile = mainEntrypoint || circuitFiles[0];