@tscircuit/cli 0.0.4 → 0.0.5

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/bun.lockb CHANGED
Binary file
package/dist/cli.js CHANGED
@@ -210,7 +210,7 @@ import {Command} from "commander";
210
210
  // package.json
211
211
  var package_default = {
212
212
  name: "@tscircuit/cli",
213
- version: "0.0.4",
213
+ version: "0.0.5",
214
214
  private: false,
215
215
  type: "module",
216
216
  description: "Command line tool for developing, publishing and installing tscircuit circuits",
@@ -221,7 +221,8 @@ var package_default = {
221
221
  "start:dev-server": "bun build:dev-server && bun cli.ts dev -y --cwd ./tests/assets/example-project",
222
222
  "build:dev-server": "cd dev-server-api && bun run build && cd ../dev-server-frontend && bun run build",
223
223
  "build:cli": "bun build-cli.ts",
224
- build: "bun build:dev-server && npm run build:cli"
224
+ build: "bun build:dev-server && npm run build:cli",
225
+ "test:init": "bun cli.ts init --dir ./tmp/test --name test"
225
226
  },
226
227
  bin: {
227
228
  tscircuit: "./dist/cli.js",
@@ -238,6 +239,7 @@ var package_default = {
238
239
  "@tscircuit/react-fiber": "^1.0.6",
239
240
  axios: "^1.6.7",
240
241
  "better-sqlite3": "^9.4.3",
242
+ chokidar: "^3.6.0",
241
243
  commander: "^12.0.0",
242
244
  configstore: "^6.0.0",
243
245
  "dax-sh": "^0.39.2",
@@ -258,6 +260,7 @@ var package_default = {
258
260
  },
259
261
  devDependencies: {
260
262
  "@types/bun": "^1.0.8",
263
+ "@types/chokidar": "^2.1.3",
261
264
  "@types/configstore": "^6.0.2",
262
265
  "@types/lodash": "^4.14.202",
263
266
  "@types/mime-types": "^2.1.4",
@@ -622,8 +625,7 @@ var soupifyCmd = async (ctx, args) => {
622
625
  };
623
626
  // lib/cmd-fns/dev/index.ts
624
627
  import {z as z16} from "zod";
625
- import kleur4 from "kleur";
626
- import {join as joinPath} from "path";
628
+ import kleur7 from "kleur";
627
629
  import prompts from "prompts";
628
630
  import open from "open";
629
631
 
@@ -845,66 +847,54 @@ var bundle_default = {
845
847
  "index.html": Buffer.from("PCFkb2N0eXBlIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KICA8aGVhZD4KICAgIDxtZXRhIGNoYXJzZXQ9IlVURi04IiAvPgogICAgPCEtLSA8bGluayByZWw9Imljb24iIHR5cGU9ImltYWdlL3N2Zyt4bWwiIGhyZWY9Ii92aXRlLnN2ZyIgLz4gLS0+CiAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEuMCIgLz4KICAgIDx0aXRsZT5bZGV2XSB0c2NpcmN1aXQ8L3RpdGxlPgogICAgPHNjcmlwdCB0eXBlPSJtb2R1bGUiIGNyb3Nzb3JpZ2luIHNyYz0iL3ByZXZpZXcvYXNzZXRzL2luZGV4LUNpRUdEejdkLmpzIj48L3NjcmlwdD4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgY3Jvc3NvcmlnaW4gaHJlZj0iL3ByZXZpZXcvYXNzZXRzL2luZGV4LUNNNkk3TGhOLmNzcyI+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPGRpdiBpZD0icm9vdCI+PC9kaXY+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==", "base64")
846
848
  };
847
849
 
848
- // lib/cmd-fns/dev/index.ts
849
- import defaultAxios from "axios";
850
- import {readdirSync, readFileSync} from "fs";
850
+ // lib/cmd-fns/dev/dev-server-request-handler.ts
851
851
  import EdgeRuntimePrimitives from "@edge-runtime/primitives";
852
852
  import mime from "mime-types";
853
- var devCmd = async (ctx, args) => {
854
- const params = z16.object({
855
- cwd: z16.string().optional().default(process.cwd()),
856
- port: z16.coerce.number().optional().default(3020)
857
- }).parse(args);
858
- const { cwd, port } = params;
859
- console.log(kleur4.green(`\n--------------------------------------------\n\nStarting dev server http://localhost:${port}\n\n--------------------------------------------\n\n`));
860
- const serverUrl = `http://localhost:${port}`;
861
- const axios = defaultAxios.create({
862
- baseURL: serverUrl
863
- });
864
- axios.interceptors.response.use((res) => res, (err) => {
865
- console.log(kleur4.red(`[ERR] ${err.response?.status} ${err.config.method?.toUpperCase()} ${err.config.url}\n\n${JSON.stringify(err.response?.data, null, " ")}`.replace(/\\n/g, "\n").replace(/\\"/g, '"')));
866
- console.log(kleur4.yellow("[Request Body]:"), err.config.data);
867
- return Promise.reject(err);
868
- });
869
- const requestHandler = async (bunReq) => {
870
- const url = new URL(bunReq.url);
871
- const requestType = url.pathname.startsWith("/api") ? "api" : url.pathname.startsWith("/preview") ? "preview" : "other";
872
- if (requestType === "api") {
873
- const req = new EdgeRuntimePrimitives.Request(bunReq.url, {
874
- headers: bunReq.headers,
875
- method: bunReq.method,
876
- body: bunReq.body
877
- });
878
- const response = await stdin_default.makeRequest(req, {});
879
- return response;
880
- } else if (requestType === "preview") {
881
- let frontendPath = url.pathname.replace("/preview", "");
882
- if (frontendPath === "/" || frontendPath === "") {
883
- frontendPath = "index.html";
853
+ var devServerRequestHandler = async (bunReq) => {
854
+ const url = new URL(bunReq.url);
855
+ const requestType = url.pathname.startsWith("/api") ? "api" : url.pathname.startsWith("/preview") ? "preview" : "other";
856
+ if (requestType === "api") {
857
+ const req = new EdgeRuntimePrimitives.Request(bunReq.url, {
858
+ headers: bunReq.headers,
859
+ method: bunReq.method,
860
+ body: bunReq.body
861
+ });
862
+ const response = await stdin_default.makeRequest(req, {});
863
+ return response;
864
+ } else if (requestType === "preview") {
865
+ let frontendPath = url.pathname.replace("/preview", "");
866
+ if (frontendPath === "/" || frontendPath === "") {
867
+ frontendPath = "index.html";
868
+ }
869
+ frontendPath = frontendPath.replace(/^\//, "");
870
+ const fileContent = bundle_default[frontendPath];
871
+ if (!fileContent) {
872
+ return new Response("Not Found", { status: 404 });
873
+ }
874
+ return new Response(fileContent.toString("utf-8"), {
875
+ headers: {
876
+ "Content-Type": mime.lookup(frontendPath) || "text/plain"
884
877
  }
885
- frontendPath = frontendPath.replace(/^\//, "");
886
- const fileContent = bundle_default[frontendPath];
887
- if (!fileContent) {
888
- return new Response("Not Found", { status: 404 });
878
+ });
879
+ } else {
880
+ return new Response(null, {
881
+ status: 302,
882
+ headers: {
883
+ Location: "/preview"
889
884
  }
890
- return new Response(fileContent.toString("utf-8"), {
891
- headers: {
892
- "Content-Type": mime.lookup(frontendPath) || "text/plain"
893
- }
894
- });
895
- } else {
896
- return new Response(null, {
897
- status: 302,
898
- headers: {
899
- Location: "/preview"
900
- }
901
- });
902
- }
903
- };
885
+ });
886
+ }
887
+ };
888
+
889
+ // lib/cmd-fns/dev/start-dev-server.ts
890
+ var startDevServer = async ({
891
+ port,
892
+ devServerAxios
893
+ }) => {
904
894
  let server;
905
895
  if (typeof Bun !== "undefined") {
906
896
  server = Bun.serve({
907
- fetch: requestHandler,
897
+ fetch: devServerRequestHandler,
908
898
  development: false,
909
899
  port
910
900
  });
@@ -912,49 +902,155 @@ var devCmd = async (ctx, args) => {
912
902
  const { Hono } = await import("hono");
913
903
  const { serve } = await import("@hono/node-server");
914
904
  const honoApp = new Hono;
915
- honoApp.all("/*", (c) => requestHandler(c.req.raw));
905
+ honoApp.all("/*", (c) => devServerRequestHandler(c.req.raw));
916
906
  server = serve({
917
907
  fetch: honoApp.fetch,
918
908
  port
919
909
  });
920
910
  }
921
911
  console.log("Running health check against dev server...");
922
- await axios.get("/api/health");
923
- console.log(`Loading examples...`);
924
- const examplesDir = joinPath(cwd, "examples");
925
- const exampleFileNames = readdirSync(examplesDir);
912
+ await devServerAxios.get("/api/health");
913
+ return server;
914
+ };
915
+
916
+ // lib/cmd-fns/dev/get-dev-server-axios.ts
917
+ import kleur4 from "kleur";
918
+ import defaultAxios from "axios";
919
+ var getDevServerAxios = ({ serverUrl }) => {
920
+ const devServerAxios = defaultAxios.create({
921
+ baseURL: serverUrl
922
+ });
923
+ devServerAxios.interceptors.response.use((res) => res, (err) => {
924
+ console.log(kleur4.red(`[ERR] ${err.response?.status} ${err.config.method?.toUpperCase()} ${err.config.url}\n\n${JSON.stringify(err.response?.data, null, " ")}`.replace(/\\n/g, "\n").replace(/\\"/g, '"')));
925
+ console.log(kleur4.yellow("[Request Body]:"), err.config.data);
926
+ return Promise.reject(err);
927
+ });
928
+ return devServerAxios;
929
+ };
930
+
931
+ // lib/cmd-fns/dev/upload-examples-from-directory.ts
932
+ import {join as joinPath2} from "path";
933
+ import {readdirSync as readdirSync2} from "fs";
934
+
935
+ // lib/cmd-fns/dev/soupify-and-upload-example-file.ts
936
+ import kleur5 from "kleur";
937
+ import {join as joinPath} from "path";
938
+ import {readFileSync} from "fs";
939
+ var soupifyAndUploadExampleFile = async ({
940
+ examplesDir,
941
+ exampleFileName,
942
+ devServerAxios
943
+ }) => {
944
+ try {
945
+ const examplePath = joinPath(examplesDir, exampleFileName);
946
+ const exampleContent = readFileSync(examplePath).toString();
947
+ let exportName = "default";
948
+ if (!exampleContent.includes("export default")) {
949
+ const matches = Array.from(exampleContent.matchAll(/export\s+(const|function)\s+([A-Z]\w+)\s*=?/g)).map((m) => m[2]);
950
+ if (matches.length === 0) {
951
+ throw new Error(`No export detected in "${exampleFileName}"`);
952
+ }
953
+ if (matches.length > 1) {
954
+ throw new Error(`Multiple exports detected in "${exampleFileName}", only single exports currently working`);
955
+ }
956
+ exportName = matches[0];
957
+ }
958
+ console.log(kleur5.gray(`[soupifying] ${exampleFileName}...`));
959
+ const soup = await soupify({
960
+ filePath: examplePath,
961
+ exportName
962
+ });
963
+ console.log(kleur5.gray(`[uploading ] ${exampleFileName}...`));
964
+ await devServerAxios.post("/api/dev_package_examples/create", {
965
+ tscircuit_soup: soup,
966
+ file_path: examplePath,
967
+ export_name: exportName
968
+ });
969
+ console.log(kleur5.gray(`[ done ] ${exampleFileName}!`));
970
+ } catch (e) {
971
+ console.log(kleur5.red(e.toString()));
972
+ }
973
+ };
974
+
975
+ // lib/cmd-fns/dev/upload-examples-from-directory.ts
976
+ var uploadExamplesFromDirectory = async ({
977
+ cwd,
978
+ devServerAxios
979
+ }) => {
980
+ const examplesDir = joinPath2(cwd, "examples");
981
+ const exampleFileNames = readdirSync2(examplesDir);
926
982
  for (const exampleFileName of exampleFileNames) {
927
- try {
928
- const examplePath = joinPath(examplesDir, exampleFileName);
929
- const exampleContent = readFileSync(examplePath).toString();
930
- let exportName = "default";
931
- if (!exampleContent.includes("export default")) {
932
- const matches = Array.from(exampleContent.matchAll(/export\s+(const|function)\s+([A-Z]\w+)\s*=?/g)).map((m) => m[2]);
933
- if (matches.length === 0) {
934
- throw new Error(`No export detected in "${exampleFileName}"`);
935
- }
936
- if (matches.length > 1) {
937
- throw new Error(`Multiple exports detected in "${exampleFileName}", only single exports currently working`);
938
- }
939
- exportName = matches[0];
983
+ if (exampleFileName.endsWith(".__tmp_entrypoint.tsx"))
984
+ continue;
985
+ if (!exampleFileName.endsWith(".tsx"))
986
+ continue;
987
+ await soupifyAndUploadExampleFile({
988
+ devServerAxios,
989
+ examplesDir,
990
+ exampleFileName
991
+ });
992
+ }
993
+ };
994
+
995
+ // lib/cmd-fns/dev/index.ts
996
+ import {unlink as unlink2} from "fs/promises";
997
+ import * as Path3 from "path";
998
+
999
+ // lib/cmd-fns/dev/start-watcher.ts
1000
+ import chokidar from "chokidar";
1001
+ import kleur6 from "kleur";
1002
+ var startWatcher = async ({
1003
+ cwd,
1004
+ devServerAxios
1005
+ }) => {
1006
+ const watcher = chokidar.watch(`${cwd}/**/*.tsx`, {
1007
+ ignored: /node_modules/,
1008
+ persistent: true
1009
+ });
1010
+ const upload_queue_state = {
1011
+ dirty: false,
1012
+ should_run: true
1013
+ };
1014
+ watcher.on("change", async (path) => {
1015
+ console.log(`File ${path} has been changed`);
1016
+ upload_queue_state.dirty = true;
1017
+ });
1018
+ async function uploadInBackground() {
1019
+ while (upload_queue_state.should_run) {
1020
+ if (upload_queue_state.dirty) {
1021
+ console.log(kleur6.yellow("Changes detected, re-uploading examples..."));
1022
+ upload_queue_state.dirty = false;
1023
+ await uploadExamplesFromDirectory({ cwd, devServerAxios });
940
1024
  }
941
- console.log("Soupifying...");
942
- const soup = await soupify({
943
- filePath: examplePath,
944
- exportName
945
- });
946
- console.log(`Soupified ${exampleFileName}!`);
947
- console.log(`Uploading ${exampleFileName}...`);
948
- await axios.post("/api/dev_package_examples/create", {
949
- tscircuit_soup: soup,
950
- file_path: examplePath,
951
- export_name: exportName
952
- });
953
- console.log(`Uploaded ${exampleFileName}!`);
954
- } catch (e) {
955
- console.log(kleur4.red(e.toString()));
1025
+ await new Promise((resolve) => setTimeout(resolve, 100));
956
1026
  }
957
1027
  }
1028
+ const _backgroundUploaderPromise = uploadInBackground();
1029
+ return {
1030
+ _backgroundUploaderPromise,
1031
+ stop: () => {
1032
+ upload_queue_state.should_run = false;
1033
+ watcher.close();
1034
+ }
1035
+ };
1036
+ };
1037
+
1038
+ // lib/cmd-fns/dev/index.ts
1039
+ var devCmd = async (ctx, args) => {
1040
+ const params = z16.object({
1041
+ cwd: z16.string().optional().default(process.cwd()),
1042
+ port: z16.coerce.number().optional().default(3020)
1043
+ }).parse(args);
1044
+ const { cwd, port } = params;
1045
+ unlink2(Path3.join(cwd, ".tscircuit/dev-server.db")).catch(() => {
1046
+ });
1047
+ console.log(kleur7.green(`\n--------------------------------------------\n\nStarting dev server http://localhost:${port}\n\n--------------------------------------------\n\n`));
1048
+ const serverUrl = `http://localhost:${port}`;
1049
+ const devServerAxios = getDevServerAxios({ serverUrl });
1050
+ const server = await startDevServer({ port, devServerAxios });
1051
+ console.log(`Loading examples...`);
1052
+ await uploadExamplesFromDirectory({ devServerAxios, cwd });
1053
+ const watcher = await startWatcher({ cwd, devServerAxios });
958
1054
  while (true) {
959
1055
  const { action } = await prompts({
960
1056
  type: "select",
@@ -978,6 +1074,7 @@ var devCmd = async (ctx, args) => {
978
1074
  server.stop();
979
1075
  if (server.close)
980
1076
  server.close();
1077
+ watcher.stop();
981
1078
  break;
982
1079
  }
983
1080
  }
@@ -985,13 +1082,13 @@ var devCmd = async (ctx, args) => {
985
1082
  // lib/cmd-fns/init.ts
986
1083
  import {z as z17} from "zod";
987
1084
  import {
988
- readFileSync as readFileSync2,
1085
+ readFileSync as readFileSync3,
989
1086
  writeFileSync as writeFileSync3,
990
1087
  existsSync,
991
1088
  mkdirSync as mkdirSync2,
992
1089
  appendFileSync
993
1090
  } from "fs";
994
- import * as Path3 from "node:path";
1091
+ import * as Path4 from "node:path";
995
1092
  import $2 from "dax-sh";
996
1093
  var initCmd = async (ctx, args) => {
997
1094
  const params = z17.object({
@@ -1015,7 +1112,7 @@ var initCmd = async (ctx, args) => {
1015
1112
  }
1016
1113
  const pkm = runtime === "bun" ? "bun" : "npm";
1017
1114
  const runner = runtime === "bun" ? "bun" : "tsx";
1018
- const packageJsonExists = existsSync(Path3.join(params.dir, "package.json"));
1115
+ const packageJsonExists = existsSync(Path4.join(params.dir, "package.json"));
1019
1116
  const dirExists = existsSync(params.dir);
1020
1117
  if (!dirExists) {
1021
1118
  console.log(`Creating directory ${params.dir}`);
@@ -1030,19 +1127,20 @@ var initCmd = async (ctx, args) => {
1030
1127
  console.log(`Setting up project for typescript...`);
1031
1128
  await $2`${pkm} add -D typescript tsx`;
1032
1129
  }
1130
+ await $2`${pkm} add -D @tscircuit/react-fiber @tscircuit/builder`;
1033
1131
  console.log("Changing package name...");
1034
- const packageJson = JSON.parse(readFileSync2("package.json", "utf-8"));
1132
+ const packageJson = JSON.parse(readFileSync3("package.json", "utf-8"));
1035
1133
  packageJson.name = params.name;
1036
1134
  writeFileSync3("package.json", JSON.stringify(packageJson, null, 2));
1037
1135
  console.log(`Adding ".tscircuit" to .gitignore`);
1038
- appendFileSync(".gitignore", "\n.tscircuit", {
1136
+ appendFileSync(".gitignore", "\n.tscircuit\n*.__tmp_entrypoint.tsx", {
1039
1137
  encoding: "utf-8",
1040
- flag: "w+"
1138
+ flag: "a+"
1041
1139
  });
1042
1140
  console.log("Creating lib and examples directories...");
1043
1141
  mkdirSync2("examples", { recursive: true });
1044
1142
  mkdirSync2("lib", { recursive: true });
1045
- writeFileSync3(Path3.join("lib", "MyCircuit.tsx"), `
1143
+ writeFileSync3(Path4.join("lib", "MyCircuit.tsx"), `
1046
1144
  export const MyCircuit = () => (
1047
1145
  <resistor
1048
1146
  resistance="10kohm"
@@ -1052,7 +1150,7 @@ var initCmd = async (ctx, args) => {
1052
1150
  )
1053
1151
  `.trim());
1054
1152
  writeFileSync3("index.ts", `export * from "./lib/MyCircuit"`);
1055
- writeFileSync3(Path3.join("examples", "MyExample.tsx"), `
1153
+ writeFileSync3(Path4.join("examples", "MyExample.tsx"), `
1056
1154
  import { MyCircuit } from "lib/MyCircuit"
1057
1155
 
1058
1156
  export const MyExample = () => (
@@ -1171,7 +1269,7 @@ var getProgram = (ctx) => {
1171
1269
 
1172
1270
  // lib/util/create-context-and-run-program.ts
1173
1271
  import defaultAxios2 from "axios";
1174
- import kleur5 from "kleur";
1272
+ import kleur8 from "kleur";
1175
1273
 
1176
1274
  // lib/param-handlers/interact-for-local-directory.ts
1177
1275
  import * as fs4 from "fs/promises";
@@ -1417,17 +1515,17 @@ var createContextAndRunProgram = async (process_args) => {
1417
1515
  if (global_config.get("log_requests")) {
1418
1516
  console.log(`Using registry_url: ${registry_url}`);
1419
1517
  axios.interceptors.request.use((req) => {
1420
- console.log(kleur5.grey(`[REQ] ${req.method?.toUpperCase()} ${req.url}`));
1518
+ console.log(kleur8.grey(`[REQ] ${req.method?.toUpperCase()} ${req.url}`));
1421
1519
  return req;
1422
1520
  });
1423
1521
  axios.interceptors.response.use((res) => {
1424
- console.log(kleur5.grey(`[RES] ${res.status} ${res.config.method?.toUpperCase()} ${res.config.url}`));
1522
+ console.log(kleur8.grey(`[RES] ${res.status} ${res.config.method?.toUpperCase()} ${res.config.url}`));
1425
1523
  return res;
1426
1524
  });
1427
1525
  }
1428
1526
  axios.interceptors.response.use((res) => res, (err) => {
1429
- console.log(kleur5.red(`[ERR] ${err.response?.status} ${err.config.method?.toUpperCase()} ${err.config.url}\n\n${JSON.stringify(err.response?.data, null, " ")}`.replace(/\\n/g, "\n").replace(/\\"/g, '"')));
1430
- console.log(kleur5.yellow("[Request Body]:"), err.config.data);
1527
+ console.log(kleur8.red(`[ERR] ${err.response?.status} ${err.config.method?.toUpperCase()} ${err.config.url}\n\n${JSON.stringify(err.response?.data, null, " ")}`.replace(/\\n/g, "\n").replace(/\\"/g, '"')));
1528
+ console.log(kleur8.yellow("[Request Body]:"), err.config.data);
1431
1529
  return Promise.reject(err);
1432
1530
  });
1433
1531
  const ctx = {
@@ -0,0 +1,54 @@
1
+ import apiServer from "../../../dev-server-api/dist/bundle"
2
+ import frontendVfs from "../../../dev-server-frontend/dist/bundle"
3
+ import EdgeRuntimePrimitives from "@edge-runtime/primitives"
4
+ import mime from "mime-types"
5
+
6
+ export const devServerRequestHandler = async (bunReq: Request) => {
7
+ const url = new URL(bunReq.url)
8
+ const requestType = url.pathname.startsWith("/api")
9
+ ? "api"
10
+ : url.pathname.startsWith("/preview")
11
+ ? "preview"
12
+ : "other"
13
+
14
+ if (requestType === "api") {
15
+ // We have to shim Bun's Request until they fix the issue where
16
+ // .clone() doesn't clone the body
17
+ // https://github.com/oven-sh/bun/pull/8668
18
+ const req = new EdgeRuntimePrimitives.Request(bunReq.url, {
19
+ headers: bunReq.headers,
20
+ method: bunReq.method,
21
+ body: bunReq.body,
22
+ })
23
+
24
+ const response = await apiServer.makeRequest(req, {})
25
+ return response
26
+ } else if (requestType === "preview") {
27
+ let frontendPath = url.pathname.replace("/preview", "")
28
+ if (frontendPath === "/" || frontendPath === "") {
29
+ frontendPath = "index.html"
30
+ }
31
+ frontendPath = frontendPath.replace(/^\//, "")
32
+
33
+ const fileContent: Buffer = (frontendVfs as any)[frontendPath]
34
+ if (!fileContent) {
35
+ return new Response("Not Found", { status: 404 })
36
+ }
37
+ return new Response(
38
+ // Buffer.from(fileContent.toString(), "base64").toString("utf-8"),
39
+ fileContent.toString("utf-8"),
40
+ {
41
+ headers: {
42
+ "Content-Type": mime.lookup(frontendPath) || "text/plain",
43
+ },
44
+ }
45
+ )
46
+ } else {
47
+ return new Response(null, {
48
+ status: 302,
49
+ headers: {
50
+ Location: "/preview",
51
+ },
52
+ })
53
+ }
54
+ }
@@ -0,0 +1,25 @@
1
+ import kleur from "kleur"
2
+ import defaultAxios from "axios"
3
+
4
+ export const getDevServerAxios = ({ serverUrl }: { serverUrl: string }) => {
5
+ const devServerAxios = defaultAxios.create({
6
+ baseURL: serverUrl,
7
+ })
8
+ devServerAxios.interceptors.response.use(
9
+ (res) => res,
10
+ (err) => {
11
+ console.log(
12
+ kleur.red(
13
+ `[ERR] ${err.response?.status} ${err.config.method?.toUpperCase()} ${
14
+ err.config.url
15
+ }\n\n${JSON.stringify(err.response?.data, null, " ")}`
16
+ .replace(/\\n/g, "\n")
17
+ .replace(/\\"/g, '"')
18
+ )
19
+ )
20
+ console.log(kleur.yellow("[Request Body]:"), err.config.data)
21
+ return Promise.reject(err)
22
+ }
23
+ )
24
+ return devServerAxios
25
+ }
@@ -1,16 +1,15 @@
1
1
  import { AppContext } from "../../util/app-context"
2
2
  import { z } from "zod"
3
3
  import kleur from "kleur"
4
- import { join as joinPath } from "path"
5
4
  import prompts from "prompts"
6
5
  import open from "open"
7
- import apiServer from "../../../dev-server-api/dist/bundle"
8
- import frontendVfs from "../../../dev-server-frontend/dist/bundle"
9
- import defaultAxios from "axios"
10
- import { readdirSync, readFileSync } from "fs"
11
- import { soupify } from "lib/soupify"
12
- import EdgeRuntimePrimitives from "@edge-runtime/primitives"
13
- import mime from "mime-types"
6
+ import { startDevServer } from "./start-dev-server"
7
+ import { getDevServerAxios } from "./get-dev-server-axios"
8
+ import { uploadExamplesFromDirectory } from "./upload-examples-from-directory"
9
+ import { unlink } from "fs/promises"
10
+ import * as Path from "path"
11
+ import { appendFileSync } from "fs"
12
+ import { startWatcher } from "./start-watcher"
14
13
 
15
14
  export const devCmd = async (ctx: AppContext, args: any) => {
16
15
  const params = z
@@ -24,7 +23,8 @@ export const devCmd = async (ctx: AppContext, args: any) => {
24
23
 
25
24
  // Load package.json, install tscircuit if it's not installed. If any other
26
25
  // tscircuit dependency like @tscircuit/builder is installed, then don't
27
- // install anything
26
+ // install anything. If there is nothing in the current directory, ask to
27
+ // instead run "tsck init"
28
28
  // TODO
29
29
 
30
30
  // Check that examples directory exists, if not create it and put inside
@@ -37,13 +37,8 @@ export const devCmd = async (ctx: AppContext, args: any) => {
37
37
  // Add .tscircuit to .gitignore if it's not already there
38
38
  // TODO
39
39
 
40
- // Delete and recreate .tscircuit/dev-server.db
41
-
42
- // INSIDE BACKGROUND SERVICE
43
- // Soupify contents of examples directory, upload to sqlite db
44
- // TODO
45
- // Watch and re-soupify on file changes
46
- // TODO
40
+ // Delete old .tscircuit/dev-server.db
41
+ unlink(Path.join(cwd, ".tscircuit/dev-server.db")).catch(() => {})
47
42
 
48
43
  console.log(
49
44
  kleur.green(
@@ -51,144 +46,16 @@ export const devCmd = async (ctx: AppContext, args: any) => {
51
46
  )
52
47
  )
53
48
  const serverUrl = `http://localhost:${port}`
54
- const axios = defaultAxios.create({
55
- baseURL: serverUrl,
56
- })
57
- axios.interceptors.response.use(
58
- (res) => res,
59
- (err) => {
60
- console.log(
61
- kleur.red(
62
- `[ERR] ${err.response?.status} ${err.config.method?.toUpperCase()} ${
63
- err.config.url
64
- }\n\n${JSON.stringify(err.response?.data, null, " ")}`
65
- .replace(/\\n/g, "\n")
66
- .replace(/\\"/g, '"')
67
- )
68
- )
69
- console.log(kleur.yellow("[Request Body]:"), err.config.data)
70
- return Promise.reject(err)
71
- }
72
- )
73
-
74
- const requestHandler = async (bunReq: Request) => {
75
- const url = new URL(bunReq.url)
76
- const requestType = url.pathname.startsWith("/api")
77
- ? "api"
78
- : url.pathname.startsWith("/preview")
79
- ? "preview"
80
- : "other"
49
+ const devServerAxios = getDevServerAxios({ serverUrl })
81
50
 
82
- if (requestType === "api") {
83
- // We have to shim Bun's Request until they fix the issue where
84
- // .clone() doesn't clone the body
85
- // https://github.com/oven-sh/bun/pull/8668
86
- const req = new EdgeRuntimePrimitives.Request(bunReq.url, {
87
- headers: bunReq.headers,
88
- method: bunReq.method,
89
- body: bunReq.body,
90
- })
91
-
92
- const response = await apiServer.makeRequest(req, {})
93
- return response
94
- } else if (requestType === "preview") {
95
- let frontendPath = url.pathname.replace("/preview", "")
96
- if (frontendPath === "/" || frontendPath === "") {
97
- frontendPath = "index.html"
98
- }
99
- frontendPath = frontendPath.replace(/^\//, "")
100
-
101
- const fileContent: Buffer = (frontendVfs as any)[frontendPath]
102
- if (!fileContent) {
103
- return new Response("Not Found", { status: 404 })
104
- }
105
- return new Response(
106
- // Buffer.from(fileContent.toString(), "base64").toString("utf-8"),
107
- fileContent.toString("utf-8"),
108
- {
109
- headers: {
110
- "Content-Type": mime.lookup(frontendPath) || "text/plain",
111
- },
112
- }
113
- )
114
- } else {
115
- return new Response(null, {
116
- status: 302,
117
- headers: {
118
- Location: "/preview",
119
- },
120
- })
121
- }
122
- }
123
-
124
- let server: any
125
- if (typeof Bun !== "undefined") {
126
- server = Bun.serve({
127
- fetch: requestHandler,
128
- development: false,
129
- port,
130
- })
131
- } else {
132
- // Hono messes up the globals, only import it if we don't have Bun
133
- const { Hono } = await import("hono")
134
- const { serve } = await import("@hono/node-server")
135
- const honoApp = new Hono()
136
- honoApp.all("/*", (c) => requestHandler(c.req.raw))
137
- server = serve({
138
- fetch: honoApp.fetch,
139
- port,
140
- })
141
- }
142
-
143
- console.log("Running health check against dev server...")
144
- await axios.get("/api/health")
51
+ const server = await startDevServer({ port, devServerAxios })
145
52
 
146
53
  // Soupify all examples
147
54
  console.log(`Loading examples...`)
148
- const examplesDir = joinPath(cwd, "examples")
149
- const exampleFileNames = readdirSync(examplesDir)
150
- for (const exampleFileName of exampleFileNames) {
151
- try {
152
- const examplePath = joinPath(examplesDir, exampleFileName)
153
- const exampleContent = readFileSync(examplePath).toString()
154
-
155
- let exportName = "default"
156
-
157
- if (!exampleContent.includes("export default")) {
158
- // try to infer an export if possible
159
- const matches = Array.from(
160
- exampleContent.matchAll(
161
- /export\s+(const|function)\s+([A-Z]\w+)\s*=?/g
162
- )
163
- ).map((m) => m[2])
164
- if (matches.length === 0) {
165
- throw new Error(`No export detected in "${exampleFileName}"`)
166
- }
167
- if (matches.length > 1) {
168
- throw new Error(
169
- `Multiple exports detected in "${exampleFileName}", only single exports currently working`
170
- )
171
- }
172
- exportName = matches[0]
173
- }
55
+ await uploadExamplesFromDirectory({ devServerAxios, cwd })
174
56
 
175
- console.log("Soupifying...")
176
- const soup = await soupify({
177
- filePath: examplePath,
178
- exportName,
179
- })
180
- console.log(`Soupified ${exampleFileName}!`)
181
- console.log(`Uploading ${exampleFileName}...`)
182
- await axios.post("/api/dev_package_examples/create", {
183
- tscircuit_soup: soup,
184
- file_path: examplePath,
185
- export_name: exportName,
186
- })
187
- console.log(`Uploaded ${exampleFileName}!`)
188
- } catch (e: any) {
189
- console.log(kleur.red(e.toString()))
190
- }
191
- }
57
+ // Start watcher
58
+ const watcher = await startWatcher({ cwd, devServerAxios })
192
59
 
193
60
  while (true) {
194
61
  const { action } = await prompts({
@@ -211,6 +78,7 @@ export const devCmd = async (ctx: AppContext, args: any) => {
211
78
  } else if (!action || action === "stop") {
212
79
  if (server.stop) server.stop()
213
80
  if (server.close) server.close()
81
+ watcher.stop()
214
82
  break
215
83
  }
216
84
  }
@@ -0,0 +1,53 @@
1
+ import kleur from "kleur"
2
+ import { join as joinPath } from "path"
3
+ import { AxiosInstance } from "axios"
4
+ import { readdirSync, readFileSync } from "fs"
5
+ import { soupify } from "lib/soupify"
6
+
7
+ export const soupifyAndUploadExampleFile = async ({
8
+ examplesDir,
9
+ exampleFileName,
10
+ devServerAxios,
11
+ }: {
12
+ examplesDir: string
13
+ exampleFileName: string
14
+ devServerAxios: AxiosInstance
15
+ }) => {
16
+ try {
17
+ const examplePath = joinPath(examplesDir, exampleFileName)
18
+ const exampleContent = readFileSync(examplePath).toString()
19
+
20
+ let exportName = "default"
21
+
22
+ if (!exampleContent.includes("export default")) {
23
+ // try to infer an export if possible
24
+ const matches = Array.from(
25
+ exampleContent.matchAll(/export\s+(const|function)\s+([A-Z]\w+)\s*=?/g)
26
+ ).map((m) => m[2])
27
+ if (matches.length === 0) {
28
+ throw new Error(`No export detected in "${exampleFileName}"`)
29
+ }
30
+ if (matches.length > 1) {
31
+ throw new Error(
32
+ `Multiple exports detected in "${exampleFileName}", only single exports currently working`
33
+ )
34
+ }
35
+ exportName = matches[0]
36
+ }
37
+
38
+ console.log(kleur.gray(`[soupifying] ${exampleFileName}...`))
39
+ const soup = await soupify({
40
+ filePath: examplePath,
41
+ exportName,
42
+ })
43
+ console.log(kleur.gray(`[uploading ] ${exampleFileName}...`))
44
+ await devServerAxios.post("/api/dev_package_examples/create", {
45
+ tscircuit_soup: soup,
46
+ file_path: examplePath,
47
+ export_name: exportName,
48
+ })
49
+ console.log(kleur.gray(`[ done ] ${exampleFileName}!`))
50
+ } catch (e: any) {
51
+ console.log(kleur.red(e.toString()))
52
+ }
53
+ }
@@ -0,0 +1,34 @@
1
+ import { AxiosInstance } from "axios"
2
+ import { devServerRequestHandler } from "./dev-server-request-handler"
3
+
4
+ export const startDevServer = async ({
5
+ port,
6
+ devServerAxios,
7
+ }: {
8
+ port: number
9
+ devServerAxios: AxiosInstance
10
+ }) => {
11
+ let server: any
12
+ if (typeof Bun !== "undefined") {
13
+ server = Bun.serve({
14
+ fetch: devServerRequestHandler,
15
+ development: false,
16
+ port,
17
+ })
18
+ } else {
19
+ // Hono messes up the globals, only import it if we don't have Bun
20
+ const { Hono } = await import("hono")
21
+ const { serve } = await import("@hono/node-server")
22
+ const honoApp = new Hono()
23
+ honoApp.all("/*", (c) => devServerRequestHandler(c.req.raw))
24
+ server = serve({
25
+ fetch: honoApp.fetch,
26
+ port,
27
+ })
28
+ }
29
+
30
+ console.log("Running health check against dev server...")
31
+ await devServerAxios.get("/api/health")
32
+
33
+ return server
34
+ }
@@ -0,0 +1,48 @@
1
+ import { AxiosInstance } from "axios"
2
+ import chokidar from "chokidar"
3
+ import { uploadExamplesFromDirectory } from "./upload-examples-from-directory"
4
+ import kleur from "kleur"
5
+
6
+ export const startWatcher = async ({
7
+ cwd,
8
+ devServerAxios,
9
+ }: {
10
+ cwd: string
11
+ devServerAxios: AxiosInstance
12
+ }) => {
13
+ const watcher = chokidar.watch(`${cwd}/**/*.tsx`, {
14
+ ignored: /node_modules/,
15
+ persistent: true,
16
+ })
17
+
18
+ const upload_queue_state = {
19
+ dirty: false,
20
+ should_run: true,
21
+ }
22
+ watcher.on("change", async (path) => {
23
+ console.log(`File ${path} has been changed`)
24
+ // TODO analyze to determine which examples were impacted
25
+ upload_queue_state.dirty = true
26
+ })
27
+
28
+ async function uploadInBackground() {
29
+ while (upload_queue_state.should_run) {
30
+ if (upload_queue_state.dirty) {
31
+ console.log(kleur.yellow("Changes detected, re-uploading examples..."))
32
+ upload_queue_state.dirty = false
33
+ await uploadExamplesFromDirectory({ cwd, devServerAxios })
34
+ }
35
+ await new Promise((resolve) => setTimeout(resolve, 100))
36
+ }
37
+ }
38
+
39
+ const _backgroundUploaderPromise = uploadInBackground()
40
+
41
+ return {
42
+ _backgroundUploaderPromise,
43
+ stop: () => {
44
+ upload_queue_state.should_run = false
45
+ watcher.close()
46
+ },
47
+ }
48
+ }
@@ -0,0 +1,26 @@
1
+ import kleur from "kleur"
2
+ import { join as joinPath } from "path"
3
+ import { AxiosInstance } from "axios"
4
+ import { readdirSync, readFileSync } from "fs"
5
+ import { soupify } from "lib/soupify"
6
+ import { soupifyAndUploadExampleFile } from "./soupify-and-upload-example-file"
7
+
8
+ export const uploadExamplesFromDirectory = async ({
9
+ cwd,
10
+ devServerAxios,
11
+ }: {
12
+ cwd: string
13
+ devServerAxios: AxiosInstance
14
+ }) => {
15
+ const examplesDir = joinPath(cwd, "examples")
16
+ const exampleFileNames = readdirSync(examplesDir)
17
+ for (const exampleFileName of exampleFileNames) {
18
+ if (exampleFileName.endsWith(".__tmp_entrypoint.tsx")) continue
19
+ if (!exampleFileName.endsWith(".tsx")) continue
20
+ await soupifyAndUploadExampleFile({
21
+ devServerAxios,
22
+ examplesDir,
23
+ exampleFileName,
24
+ })
25
+ }
26
+ }
@@ -59,6 +59,9 @@ export const initCmd = async (ctx: AppContext, args: any) => {
59
59
  await $`${pkm} add -D typescript tsx`
60
60
  }
61
61
 
62
+ // TODO just allow adding "tscircuit" in the future
63
+ await $`${pkm} add -D @tscircuit/react-fiber @tscircuit/builder`
64
+
62
65
  console.log("Changing package name...")
63
66
  // Change package.json "name" to params.name
64
67
  const packageJson = JSON.parse(readFileSync("package.json", "utf-8"))
@@ -66,9 +69,9 @@ export const initCmd = async (ctx: AppContext, args: any) => {
66
69
  writeFileSync("package.json", JSON.stringify(packageJson, null, 2))
67
70
 
68
71
  console.log(`Adding ".tscircuit" to .gitignore`)
69
- appendFileSync(".gitignore", "\n.tscircuit", {
72
+ appendFileSync(".gitignore", "\n.tscircuit\n*.__tmp_entrypoint.tsx", {
70
73
  encoding: "utf-8",
71
- flag: "w+",
74
+ flag: "a+",
72
75
  })
73
76
 
74
77
  console.log("Creating lib and examples directories...")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Command line tool for developing, publishing and installing tscircuit circuits",
@@ -11,7 +11,8 @@
11
11
  "start:dev-server": "bun build:dev-server && bun cli.ts dev -y --cwd ./tests/assets/example-project",
12
12
  "build:dev-server": "cd dev-server-api && bun run build && cd ../dev-server-frontend && bun run build",
13
13
  "build:cli": "bun build-cli.ts",
14
- "build": "bun build:dev-server && npm run build:cli"
14
+ "build": "bun build:dev-server && npm run build:cli",
15
+ "test:init": "bun cli.ts init --dir ./tmp/test --name test"
15
16
  },
16
17
  "bin": {
17
18
  "tscircuit": "./dist/cli.js",
@@ -28,6 +29,7 @@
28
29
  "@tscircuit/react-fiber": "^1.0.6",
29
30
  "axios": "^1.6.7",
30
31
  "better-sqlite3": "^9.4.3",
32
+ "chokidar": "^3.6.0",
31
33
  "commander": "^12.0.0",
32
34
  "configstore": "^6.0.0",
33
35
  "dax-sh": "^0.39.2",
@@ -48,6 +50,7 @@
48
50
  },
49
51
  "devDependencies": {
50
52
  "@types/bun": "^1.0.8",
53
+ "@types/chokidar": "^2.1.3",
51
54
  "@types/configstore": "^6.0.2",
52
55
  "@types/lodash": "^4.14.202",
53
56
  "@types/mime-types": "^2.1.4",
@@ -1,5 +1,5 @@
1
1
  import "@tscircuit/react-fiber"
2
2
 
3
3
  export const MyCircuit = () => (
4
- <resistor name="R1" resistance="10kohm" footprint="0805" />
4
+ <resistor name="R2" resistance="20kohm" footprint="0805" />
5
5
  )