@tscircuit/cli 0.1.1052 → 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.1051";
71667
+ var version = "0.1.1052";
71668
71668
  var package_default = {
71669
71669
  name: "@tscircuit/cli",
71670
71670
  version,
@@ -81871,42 +81871,27 @@ var exitBuild = (code, reason) => {
81871
81871
  // cli/build/worker-pool.ts
81872
81872
  import fs36 from "node:fs";
81873
81873
  import path38 from "node:path";
81874
+
81875
+ // lib/shared/thread-worker-pool.ts
81874
81876
  import { Worker } from "node:worker_threads";
81875
- var getWorkerEntrypointPath = () => {
81876
- const tsPath = path38.join(import.meta.dir, "build-worker-entrypoint.ts");
81877
- if (fs36.existsSync(tsPath)) {
81878
- return tsPath;
81879
- }
81880
- const jsBundledPath = path38.join(import.meta.dir, "build", "build-worker-entrypoint.js");
81881
- if (fs36.existsSync(jsBundledPath)) {
81882
- return jsBundledPath;
81883
- }
81884
- return path38.join(import.meta.dir, "build-worker-entrypoint.js");
81885
- };
81886
81877
 
81887
- class WorkerPool {
81878
+ class ThreadWorkerPool {
81888
81879
  workers = [];
81889
81880
  jobQueue = [];
81890
81881
  concurrency;
81891
- onLog;
81892
- workerEntrypointPath;
81882
+ options;
81893
81883
  initialized = false;
81894
81884
  stopped = false;
81895
81885
  stopReason = null;
81896
- stopOnFatal = false;
81897
- cancellationError = null;
81898
81886
  constructor(options) {
81887
+ this.options = options;
81899
81888
  this.concurrency = options.concurrency;
81900
- this.onLog = options.onLog;
81901
- this.workerEntrypointPath = getWorkerEntrypointPath();
81902
- this.stopOnFatal = options.stopOnFatal ?? false;
81903
- this.cancellationError = options.cancellationError ?? null;
81904
81889
  }
81905
81890
  async initWorkers() {
81906
81891
  if (this.initialized)
81907
81892
  return;
81908
81893
  for (let i = 0;i < this.concurrency; i++) {
81909
- const worker = new Worker(this.workerEntrypointPath);
81894
+ const worker = new Worker(this.options.workerEntrypointPath);
81910
81895
  const threadWorker = {
81911
81896
  worker,
81912
81897
  busy: false,
@@ -81920,38 +81905,24 @@ class WorkerPool {
81920
81905
  }
81921
81906
  setupWorkerMessageHandling(threadWorker) {
81922
81907
  threadWorker.worker.on("message", (message) => {
81923
- if (message.message_type === "worker_log") {
81924
- const logMsg = message;
81925
- if (this.onLog) {
81926
- this.onLog(logMsg.log_lines);
81927
- }
81928
- } else if (message.message_type === "build_completed") {
81929
- const completedMsg = message;
81930
- const job = threadWorker.currentJob;
81931
- if (job) {
81932
- if (this.stopOnFatal && completedMsg.isFatalError && this.cancellationError) {
81933
- this.stop(this.cancellationError);
81934
- }
81935
- job.resolve({
81936
- filePath: completedMsg.file_path,
81937
- outputPath: completedMsg.output_path,
81938
- glbOutputPath: completedMsg.glb_output_path,
81939
- previewOutputDir: completedMsg.preview_output_dir,
81940
- glbOk: completedMsg.glb_ok,
81941
- glbError: completedMsg.glb_error,
81942
- previewOk: completedMsg.preview_ok,
81943
- previewError: completedMsg.preview_error,
81944
- ok: completedMsg.ok,
81945
- isFatalError: completedMsg.isFatalError,
81946
- errors: completedMsg.errors,
81947
- warnings: completedMsg.warnings,
81948
- durationMs: completedMsg.durationMs
81949
- });
81950
- threadWorker.currentJob = null;
81951
- threadWorker.busy = false;
81952
- this.processQueue();
81953
- }
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;
81954
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);
81921
+ }
81922
+ job.resolve(this.options.getResult(message));
81923
+ threadWorker.currentJob = null;
81924
+ threadWorker.busy = false;
81925
+ this.processQueue();
81955
81926
  });
81956
81927
  }
81957
81928
  setupWorkerErrorHandling(threadWorker) {
@@ -81961,11 +81932,9 @@ class WorkerPool {
81961
81932
  threadWorker.currentJob = null;
81962
81933
  threadWorker.busy = false;
81963
81934
  }
81964
- if (this.onLog) {
81965
- this.onLog([
81966
- `Worker error: ${error instanceof Error ? error.message : String(error)}`
81967
- ]);
81968
- }
81935
+ this.options.onLog?.([
81936
+ `Worker error: ${error instanceof Error ? error.message : String(error)}`
81937
+ ]);
81969
81938
  });
81970
81939
  threadWorker.worker.on("exit", (code) => {
81971
81940
  if (code !== 0 && threadWorker.currentJob) {
@@ -81976,28 +81945,20 @@ class WorkerPool {
81976
81945
  });
81977
81946
  }
81978
81947
  processQueue() {
81979
- if (this.stopped)
81948
+ if (this.stopped || this.jobQueue.length === 0) {
81980
81949
  return;
81981
- if (this.jobQueue.length === 0)
81982
- return;
81983
- const availableWorker = this.workers.find((w) => !w.busy);
81984
- if (!availableWorker)
81950
+ }
81951
+ const availableWorker = this.workers.find((worker) => !worker.busy);
81952
+ if (!availableWorker) {
81985
81953
  return;
81986
- const job = this.jobQueue.shift();
81987
- if (!job)
81954
+ }
81955
+ const queuedJob = this.jobQueue.shift();
81956
+ if (!queuedJob) {
81988
81957
  return;
81958
+ }
81989
81959
  availableWorker.busy = true;
81990
- availableWorker.currentJob = job;
81991
- const message = {
81992
- message_type: "build_file",
81993
- file_path: job.filePath,
81994
- output_path: job.outputPath,
81995
- glb_output_path: job.glbOutputPath,
81996
- preview_output_dir: job.previewOutputDir,
81997
- project_dir: job.projectDir,
81998
- options: job.options
81999
- };
82000
- availableWorker.worker.postMessage(message);
81960
+ availableWorker.currentJob = queuedJob;
81961
+ availableWorker.worker.postMessage(this.options.createMessage(queuedJob.job));
82001
81962
  }
82002
81963
  async queueJob(job) {
82003
81964
  if (this.stopped) {
@@ -82005,12 +81966,7 @@ class WorkerPool {
82005
81966
  }
82006
81967
  await this.initWorkers();
82007
81968
  return new Promise((resolve4, reject) => {
82008
- const queuedJob = {
82009
- ...job,
82010
- resolve: resolve4,
82011
- reject
82012
- };
82013
- this.jobQueue.push(queuedJob);
81969
+ this.jobQueue.push({ job, resolve: resolve4, reject });
82014
81970
  this.processQueue();
82015
81971
  });
82016
81972
  }
@@ -82024,35 +81980,70 @@ class WorkerPool {
82024
81980
  }
82025
81981
  this.jobQueue = [];
82026
81982
  }
82027
- async runUntilComplete() {
82028
- return new Promise((resolve4) => {
82029
- const checkComplete = () => {
82030
- const allIdle = this.workers.every((w) => !w.busy);
82031
- const queueEmpty = this.jobQueue.length === 0;
82032
- if (allIdle && queueEmpty) {
82033
- resolve4();
82034
- } else {
82035
- setTimeout(checkComplete, 50);
82036
- }
82037
- };
82038
- checkComplete();
82039
- });
82040
- }
82041
81983
  async terminate() {
82042
- const terminatePromises = this.workers.map((w) => w.worker.terminate());
82043
- await Promise.all(terminatePromises);
81984
+ await Promise.all(this.workers.map((worker) => worker.worker.terminate()));
82044
81985
  this.workers = [];
82045
81986
  this.initialized = false;
82046
81987
  }
82047
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
+ };
82048
82002
  async function buildFilesWithWorkerPool(options) {
82049
82003
  const cancellationError = new Error("Build cancelled due fatal error");
82050
82004
  const poolConcurrency = Math.max(1, Math.min(options.concurrency, options.files.length));
82051
- const pool = new WorkerPool({
82005
+ const pool = new ThreadWorkerPool({
82052
82006
  concurrency: poolConcurrency,
82053
- onLog: options.onLog,
82054
- stopOnFatal: options.stopOnFatal,
82055
- 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
82056
82047
  });
82057
82048
  const results = [];
82058
82049
  const promises = [];
@@ -177560,8 +177551,89 @@ function applyCameraPreset(preset, cam) {
177560
177551
  }
177561
177552
 
177562
177553
  // lib/shared/snapshot-project.ts
177563
- import fs61 from "node:fs";
177554
+ import path64 from "node:path";
177555
+
177556
+ // cli/snapshot/worker-pool.ts
177557
+ import fs60 from "node:fs";
177564
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";
177565
177637
  import {
177566
177638
  convertCircuitJsonToGltf as convertCircuitJsonToGltf5,
177567
177639
  getBestCameraPosition
@@ -177570,12 +177642,11 @@ import {
177570
177642
  convertCircuitJsonToPcbSvg as convertCircuitJsonToPcbSvg3,
177571
177643
  convertCircuitJsonToSchematicSvg as convertCircuitJsonToSchematicSvg3
177572
177644
  } from "circuit-to-svg";
177573
- import looksSame2 from "looks-same";
177574
177645
  import { renderGLTFToPNGBufferFromGLBBuffer as renderGLTFToPNGBufferFromGLBBuffer2 } from "poppygl";
177575
177646
 
177576
177647
  // lib/shared/compare-images.ts
177577
177648
  import looksSame from "looks-same";
177578
- import fs60 from "node:fs/promises";
177649
+ import fs61 from "node:fs/promises";
177579
177650
  var compareAndCreateDiff = async (buffer1, buffer2, diffPath, createDiff = true) => {
177580
177651
  const { equal: equal2 } = await looksSame(buffer1, buffer2, {
177581
177652
  strict: false,
@@ -177591,12 +177662,200 @@ var compareAndCreateDiff = async (buffer1, buffer2, diffPath, createDiff = true)
177591
177662
  tolerance: 2
177592
177663
  });
177593
177664
  } else {
177594
- await fs60.writeFile(diffPath, buffer2);
177665
+ await fs61.writeFile(diffPath, buffer2);
177595
177666
  }
177596
177667
  }
177597
177668
  return { equal: equal2 };
177598
177669
  };
177599
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
+
177600
177859
  // lib/shared/snapshot-project.ts
177601
177860
  var snapshotProject = async ({
177602
177861
  update = false,
@@ -177611,7 +177870,8 @@ var snapshotProject = async ({
177611
177870
  onSuccess = (msg) => console.log(msg),
177612
177871
  platformConfig: platformConfig2,
177613
177872
  createDiff = false,
177614
- cameraPreset
177873
+ cameraPreset,
177874
+ concurrency = 1
177615
177875
  } = {}) => {
177616
177876
  if (cameraPreset) {
177617
177877
  threeD = true;
@@ -177621,7 +177881,7 @@ var snapshotProject = async ({
177621
177881
  ...DEFAULT_IGNORED_PATTERNS,
177622
177882
  ...ignored.map(normalizeIgnorePattern)
177623
177883
  ];
177624
- const resolvedPaths = filePaths.map((f2) => path62.resolve(projectDir, f2));
177884
+ const resolvedPaths = filePaths.map((f2) => path64.resolve(projectDir, f2));
177625
177885
  const boardFiles = findBoardFiles({
177626
177886
  projectDir,
177627
177887
  ignore,
@@ -177634,141 +177894,78 @@ var snapshotProject = async ({
177634
177894
  const snapshotsDirName = getSnapshotsDir(projectDir);
177635
177895
  const mismatches = [];
177636
177896
  let didUpdate = false;
177637
- const isCircuitJsonFile = (filePath) => {
177638
- const normalizedPath = filePath.toLowerCase().replaceAll("\\", "/");
177639
- return normalizedPath.endsWith(".circuit.json") || normalizedPath.endsWith("/circuit.json");
177640
- };
177641
- for (const file of boardFiles) {
177642
- const relativeFilePath = path62.relative(projectDir, file);
177643
- let circuitJson;
177644
- let pcbSvg;
177645
- let schSvg;
177646
- try {
177647
- if (isCircuitJsonFile(file)) {
177648
- const parsed = JSON.parse(fs61.readFileSync(file, "utf-8"));
177649
- circuitJson = Array.isArray(parsed) ? parsed : [];
177650
- } else {
177651
- const completePlatformConfig = getCompletePlatformConfig(platformConfig2);
177652
- const result = await generateCircuitJson({
177653
- filePath: file,
177654
- platformConfig: completePlatformConfig
177655
- });
177656
- circuitJson = result.circuitJson;
177657
- }
177658
- } catch (error) {
177659
- const errorMessage = error instanceof Error ? error.message : String(error);
177660
- onError(kleur_default.red(`
177661
- ❌ Failed to generate circuit JSON for ${relativeFilePath}:
177662
- `) + kleur_default.red(` ${errorMessage}
177663
- `));
177664
- 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);
177665
177901
  }
177666
- try {
177667
- pcbSvg = convertCircuitJsonToPcbSvg3(circuitJson);
177668
- } catch (error) {
177669
- const errorMessage = error instanceof Error ? error.message : String(error);
177670
- onError(kleur_default.red(`
177671
- ❌ Failed to generate PCB SVG for ${relativeFilePath}:
177672
- `) + kleur_default.red(` ${errorMessage}
177673
- `));
177674
- return onExit2(1);
177902
+ for (const successPath of result.successPaths) {
177903
+ console.log("✅", kleur_default.gray(successPath));
177675
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;
177676
177911
  try {
177677
- 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
+ });
177678
177941
  } catch (error) {
177679
177942
  const errorMessage = error instanceof Error ? error.message : String(error);
177680
- onError(kleur_default.red(`
177681
- ❌ Failed to generate schematic SVG for ${relativeFilePath}:
177682
- `) + kleur_default.red(` ${errorMessage}
177683
- `));
177943
+ onError(kleur_default.red(errorMessage));
177684
177944
  return onExit2(1);
177685
177945
  }
177686
- let png3d = null;
177687
- if (threeD) {
177688
- try {
177689
- const glbBuffer = await convertCircuitJsonToGltf5(circuitJson, getCircuitJsonToGltfOptions({ format: "glb" }));
177690
- if (!(glbBuffer instanceof ArrayBuffer)) {
177691
- throw new Error("Expected ArrayBuffer from convertCircuitJsonToGltf with glb format");
177692
- }
177693
- let cameraOptions = getBestCameraPosition(circuitJson);
177694
- if (cameraPreset) {
177695
- cameraOptions = applyCameraPreset(cameraPreset, cameraOptions);
177696
- }
177697
- png3d = await renderGLTFToPNGBufferFromGLBBuffer2(glbBuffer, cameraOptions);
177698
- } catch (error) {
177699
- const errorMessage = error instanceof Error ? error.message : String(error);
177700
- if (errorMessage.includes("No pcb_board found in circuit JSON")) {
177701
- const fileDir = path62.dirname(file);
177702
- const relativeDir = path62.relative(projectDir, fileDir);
177703
- const snapDir2 = snapshotsDirName ? path62.join(projectDir, snapshotsDirName, relativeDir) : path62.join(fileDir, "__snapshots__");
177704
- const base2 = path62.basename(file).replace(/\.[^.]+$/, "");
177705
- const snap3dPath = path62.join(snapDir2, `${base2}-3d.snap.png`);
177706
- const existing3dSnapshot = fs61.existsSync(snap3dPath);
177707
- if (existing3dSnapshot) {
177708
- onError(kleur_default.red(`
177709
- ❌ Failed to generate 3D snapshot for ${relativeFilePath}:
177710
- `) + kleur_default.red(` No pcb_board found in circuit JSON
177711
- `) + kleur_default.red(` Existing snapshot: ${path62.relative(projectDir, snap3dPath)}
177712
- `));
177713
- return onExit2(1);
177714
- } else {
177715
- console.log(kleur_default.red(`⚠️ Skipping 3D snapshot for ${relativeFilePath}:`) + kleur_default.red(` No pcb_board found in circuit JSON`));
177716
- png3d = null;
177717
- }
177718
- } else {
177719
- onError(kleur_default.red(`
177720
- ❌ Failed to generate 3D snapshot for ${relativeFilePath}:
177721
- `) + kleur_default.red(` ${errorMessage}
177722
- `));
177723
- return onExit2(1);
177724
- }
177725
- }
177726
- }
177727
- const snapDir = snapshotsDirName ? path62.join(projectDir, snapshotsDirName, path62.relative(projectDir, path62.dirname(file))) : path62.join(path62.dirname(file), "__snapshots__");
177728
- fs61.mkdirSync(snapDir, { recursive: true });
177729
- const base = path62.basename(file).replace(/\.[^.]+$/, "");
177730
- const snapshots = [];
177731
- if (pcbOnly || !schematicOnly) {
177732
- snapshots.push({ type: "pcb", content: pcbSvg, isBinary: false });
177733
- }
177734
- if (schematicOnly || !pcbOnly) {
177735
- snapshots.push({ type: "schematic", content: schSvg, isBinary: false });
177736
- }
177737
- if (threeD && png3d) {
177738
- snapshots.push({ type: "3d", content: png3d, isBinary: true });
177739
- }
177740
- if (!looksSame2) {
177741
- console.error("looks-same is required. Install it with 'bun add -d looks-same'");
177946
+ if (firstErrorMessage) {
177947
+ onError(firstErrorMessage);
177742
177948
  return onExit2(1);
177743
177949
  }
177744
- for (const snapshot of snapshots) {
177745
- const { type } = snapshot;
177746
- const is3d = type === "3d";
177747
- const snapPath = path62.join(snapDir, `${base}-${type}.snap.${is3d ? "png" : "svg"}`);
177748
- const existing = fs61.existsSync(snapPath);
177749
- const newContentBuffer = snapshot.isBinary ? snapshot.content : Buffer.from(snapshot.content, "utf8");
177750
- const newContentForFile = snapshot.content;
177751
- if (!existing) {
177752
- fs61.writeFileSync(snapPath, newContentForFile);
177753
- console.log("✅", kleur_default.gray(path62.relative(projectDir, snapPath)));
177754
- didUpdate = true;
177755
- continue;
177756
- }
177757
- const oldContentBuffer = fs61.readFileSync(snapPath);
177758
- const diffPath = snapPath.replace(is3d ? ".snap.png" : ".snap.svg", is3d ? ".diff.png" : ".diff.svg");
177759
- const { equal: equal2 } = await compareAndCreateDiff(oldContentBuffer, newContentBuffer, diffPath, createDiff);
177760
- if (update) {
177761
- if (!forceUpdate && equal2) {
177762
- console.log("✅", kleur_default.gray(path62.relative(projectDir, snapPath)));
177763
- } else {
177764
- fs61.writeFileSync(snapPath, newContentForFile);
177765
- console.log("✅", kleur_default.gray(path62.relative(projectDir, snapPath)));
177766
- didUpdate = true;
177767
- }
177768
- } else if (!equal2) {
177769
- mismatches.push(createDiff ? `${snapPath} (diff: ${diffPath})` : snapPath);
177770
- } else {
177771
- 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);
177772
177969
  }
177773
177970
  }
177774
177971
  }
@@ -177790,12 +177987,13 @@ Run with --update to fix.`);
177790
177987
 
177791
177988
  // cli/snapshot/register.ts
177792
177989
  var registerSnapshot = (program3) => {
177793
- 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) => {
177794
177991
  if (options.cameraPreset && !CAMERA_PRESET_NAMES.includes(options.cameraPreset)) {
177795
177992
  console.error(`Unknown camera preset "${options.cameraPreset}". Valid presets: ${CAMERA_PRESET_NAMES.join(", ")}`);
177796
177993
  process.exit(1);
177797
177994
  }
177798
177995
  await snapshotProject({
177996
+ concurrency: Math.max(1, Number.parseInt(options.concurrency || "1", 10)),
177799
177997
  update: options.update ?? false,
177800
177998
  threeD: options["3d"] ?? false,
177801
177999
  pcbOnly: options.pcbOnly ?? false,
@@ -177813,7 +178011,7 @@ var registerSnapshot = (program3) => {
177813
178011
  };
177814
178012
 
177815
178013
  // cli/transpile/register.ts
177816
- import path63 from "node:path";
178014
+ import path65 from "node:path";
177817
178015
  var registerTranspile = (program3) => {
177818
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) => {
177819
178017
  try {
@@ -177821,7 +178019,7 @@ var registerTranspile = (program3) => {
177821
178019
  fileOrDir: file,
177822
178020
  includeBoardFiles: false
177823
178021
  });
177824
- const distDir = path63.join(projectDir, "dist");
178022
+ const distDir = path65.join(projectDir, "dist");
177825
178023
  validateMainInDist(projectDir, distDir);
177826
178024
  console.log("Transpiling entry file...");
177827
178025
  const entryFile = mainEntrypoint || circuitFiles[0];