@tscircuit/cli 0.0.3 → 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 +0 -0
- package/dist/cli.js +339 -102
- package/lib/cmd-fns/dev/dev-server-request-handler.ts +54 -0
- package/lib/cmd-fns/dev/get-dev-server-axios.ts +25 -0
- package/lib/cmd-fns/dev/index.ts +17 -149
- package/lib/cmd-fns/dev/soupify-and-upload-example-file.ts +53 -0
- package/lib/cmd-fns/dev/start-dev-server.ts +34 -0
- package/lib/cmd-fns/dev/start-watcher.ts +48 -0
- package/lib/cmd-fns/dev/upload-examples-from-directory.ts +26 -0
- package/lib/cmd-fns/index.ts +2 -1
- package/lib/cmd-fns/init.ts +175 -0
- package/lib/get-program.ts +13 -0
- package/lib/param-handlers/index.ts +1 -0
- package/package.json +7 -4
- package/tests/assets/example-project/src/MyCircuit.tsx +1 -1
- package/tests/init.test.ts +9 -0
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.
|
|
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,11 +221,13 @@ 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",
|
|
228
|
-
tsci: "./dist/cli.js"
|
|
229
|
+
tsci: "./dist/cli.js",
|
|
230
|
+
tsck: "./dist/cli.js"
|
|
229
231
|
},
|
|
230
232
|
keywords: [],
|
|
231
233
|
author: "",
|
|
@@ -237,6 +239,7 @@ var package_default = {
|
|
|
237
239
|
"@tscircuit/react-fiber": "^1.0.6",
|
|
238
240
|
axios: "^1.6.7",
|
|
239
241
|
"better-sqlite3": "^9.4.3",
|
|
242
|
+
chokidar: "^3.6.0",
|
|
240
243
|
commander: "^12.0.0",
|
|
241
244
|
configstore: "^6.0.0",
|
|
242
245
|
"dax-sh": "^0.39.2",
|
|
@@ -250,13 +253,14 @@ var package_default = {
|
|
|
250
253
|
minimist: "^1.2.8",
|
|
251
254
|
"node-persist": "^4.0.1",
|
|
252
255
|
open: "^10.1.0",
|
|
253
|
-
"perfect-cli": "1.0.
|
|
256
|
+
"perfect-cli": "1.0.13",
|
|
254
257
|
prompts: "^2.4.2",
|
|
255
258
|
react: "^18.2.0",
|
|
256
259
|
zod: "latest"
|
|
257
260
|
},
|
|
258
261
|
devDependencies: {
|
|
259
262
|
"@types/bun": "^1.0.8",
|
|
263
|
+
"@types/chokidar": "^2.1.3",
|
|
260
264
|
"@types/configstore": "^6.0.2",
|
|
261
265
|
"@types/lodash": "^4.14.202",
|
|
262
266
|
"@types/mime-types": "^2.1.4",
|
|
@@ -273,7 +277,7 @@ var package_default = {
|
|
|
273
277
|
|
|
274
278
|
// lib/cmd-fns/config-reveal-location.ts
|
|
275
279
|
var configRevealLocation = async (ctx, args) => {
|
|
276
|
-
console.log(`
|
|
280
|
+
console.log(`tsck config path: ${ctx.global_config.path}`);
|
|
277
281
|
};
|
|
278
282
|
// lib/cmd-fns/config-set-registry.ts
|
|
279
283
|
import {z} from "zod";
|
|
@@ -619,10 +623,9 @@ var soupifyCmd = async (ctx, args) => {
|
|
|
619
623
|
console.log(JSON.stringify(soup, null, 2));
|
|
620
624
|
}
|
|
621
625
|
};
|
|
622
|
-
// lib/cmd-fns/dev.ts
|
|
626
|
+
// lib/cmd-fns/dev/index.ts
|
|
623
627
|
import {z as z16} from "zod";
|
|
624
|
-
import
|
|
625
|
-
import {join as joinPath} from "path";
|
|
628
|
+
import kleur7 from "kleur";
|
|
626
629
|
import prompts from "prompts";
|
|
627
630
|
import open from "open";
|
|
628
631
|
|
|
@@ -844,66 +847,54 @@ var bundle_default = {
|
|
|
844
847
|
"index.html": Buffer.from("PCFkb2N0eXBlIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KICA8aGVhZD4KICAgIDxtZXRhIGNoYXJzZXQ9IlVURi04IiAvPgogICAgPCEtLSA8bGluayByZWw9Imljb24iIHR5cGU9ImltYWdlL3N2Zyt4bWwiIGhyZWY9Ii92aXRlLnN2ZyIgLz4gLS0+CiAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEuMCIgLz4KICAgIDx0aXRsZT5bZGV2XSB0c2NpcmN1aXQ8L3RpdGxlPgogICAgPHNjcmlwdCB0eXBlPSJtb2R1bGUiIGNyb3Nzb3JpZ2luIHNyYz0iL3ByZXZpZXcvYXNzZXRzL2luZGV4LUNpRUdEejdkLmpzIj48L3NjcmlwdD4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgY3Jvc3NvcmlnaW4gaHJlZj0iL3ByZXZpZXcvYXNzZXRzL2luZGV4LUNNNkk3TGhOLmNzcyI+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPGRpdiBpZD0icm9vdCI+PC9kaXY+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==", "base64")
|
|
845
848
|
};
|
|
846
849
|
|
|
847
|
-
// lib/cmd-fns/dev.ts
|
|
848
|
-
import defaultAxios from "axios";
|
|
849
|
-
import {readdirSync, readFileSync} from "fs";
|
|
850
|
+
// lib/cmd-fns/dev/dev-server-request-handler.ts
|
|
850
851
|
import EdgeRuntimePrimitives from "@edge-runtime/primitives";
|
|
851
852
|
import mime from "mime-types";
|
|
852
|
-
var
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
});
|
|
877
|
-
const response = await stdin_default.makeRequest(req, {});
|
|
878
|
-
return response;
|
|
879
|
-
} else if (requestType === "preview") {
|
|
880
|
-
let frontendPath = url.pathname.replace("/preview", "");
|
|
881
|
-
if (frontendPath === "/" || frontendPath === "") {
|
|
882
|
-
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"
|
|
883
877
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
878
|
+
});
|
|
879
|
+
} else {
|
|
880
|
+
return new Response(null, {
|
|
881
|
+
status: 302,
|
|
882
|
+
headers: {
|
|
883
|
+
Location: "/preview"
|
|
888
884
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
Location: "/preview"
|
|
899
|
-
}
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
};
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
// lib/cmd-fns/dev/start-dev-server.ts
|
|
890
|
+
var startDevServer = async ({
|
|
891
|
+
port,
|
|
892
|
+
devServerAxios
|
|
893
|
+
}) => {
|
|
903
894
|
let server;
|
|
904
895
|
if (typeof Bun !== "undefined") {
|
|
905
896
|
server = Bun.serve({
|
|
906
|
-
fetch:
|
|
897
|
+
fetch: devServerRequestHandler,
|
|
907
898
|
development: false,
|
|
908
899
|
port
|
|
909
900
|
});
|
|
@@ -911,49 +902,155 @@ var devCmd = async (ctx, args) => {
|
|
|
911
902
|
const { Hono } = await import("hono");
|
|
912
903
|
const { serve } = await import("@hono/node-server");
|
|
913
904
|
const honoApp = new Hono;
|
|
914
|
-
honoApp.all("/*", (c) =>
|
|
905
|
+
honoApp.all("/*", (c) => devServerRequestHandler(c.req.raw));
|
|
915
906
|
server = serve({
|
|
916
907
|
fetch: honoApp.fetch,
|
|
917
908
|
port
|
|
918
909
|
});
|
|
919
910
|
}
|
|
920
911
|
console.log("Running health check against dev server...");
|
|
921
|
-
await
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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);
|
|
925
982
|
for (const exampleFileName of exampleFileNames) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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 });
|
|
939
1024
|
}
|
|
940
|
-
|
|
941
|
-
const soup = await soupify({
|
|
942
|
-
filePath: examplePath,
|
|
943
|
-
exportName
|
|
944
|
-
});
|
|
945
|
-
console.log(`Soupified ${exampleFileName}!`);
|
|
946
|
-
console.log(`Uploading ${exampleFileName}...`);
|
|
947
|
-
await axios.post("/api/dev_package_examples/create", {
|
|
948
|
-
tscircuit_soup: soup,
|
|
949
|
-
file_path: examplePath,
|
|
950
|
-
export_name: exportName
|
|
951
|
-
});
|
|
952
|
-
console.log(`Uploaded ${exampleFileName}!`);
|
|
953
|
-
} catch (e) {
|
|
954
|
-
console.log(kleur4.red(e.toString()));
|
|
1025
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
955
1026
|
}
|
|
956
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 });
|
|
957
1054
|
while (true) {
|
|
958
1055
|
const { action } = await prompts({
|
|
959
1056
|
type: "select",
|
|
@@ -977,13 +1074,151 @@ var devCmd = async (ctx, args) => {
|
|
|
977
1074
|
server.stop();
|
|
978
1075
|
if (server.close)
|
|
979
1076
|
server.close();
|
|
1077
|
+
watcher.stop();
|
|
980
1078
|
break;
|
|
981
1079
|
}
|
|
982
1080
|
}
|
|
983
1081
|
};
|
|
1082
|
+
// lib/cmd-fns/init.ts
|
|
1083
|
+
import {z as z17} from "zod";
|
|
1084
|
+
import {
|
|
1085
|
+
readFileSync as readFileSync3,
|
|
1086
|
+
writeFileSync as writeFileSync3,
|
|
1087
|
+
existsSync,
|
|
1088
|
+
mkdirSync as mkdirSync2,
|
|
1089
|
+
appendFileSync
|
|
1090
|
+
} from "fs";
|
|
1091
|
+
import * as Path4 from "node:path";
|
|
1092
|
+
import $2 from "dax-sh";
|
|
1093
|
+
var initCmd = async (ctx, args) => {
|
|
1094
|
+
const params = z17.object({
|
|
1095
|
+
name: z17.string().optional(),
|
|
1096
|
+
dir: z17.string().optional(),
|
|
1097
|
+
runtime: z17.enum(["bun", "node"]).optional()
|
|
1098
|
+
}).parse(args);
|
|
1099
|
+
if (params.name && !params.dir) {
|
|
1100
|
+
params.dir = `./${params.name.split("/").pop()}`;
|
|
1101
|
+
} else if (!params.dir) {
|
|
1102
|
+
params.dir = `.`;
|
|
1103
|
+
}
|
|
1104
|
+
let runtime = params.runtime;
|
|
1105
|
+
if (!runtime) {
|
|
1106
|
+
const bunExists = $2.commandExistsSync("bun");
|
|
1107
|
+
if (bunExists) {
|
|
1108
|
+
runtime = "bun";
|
|
1109
|
+
} else {
|
|
1110
|
+
runtime = "node";
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
const pkm = runtime === "bun" ? "bun" : "npm";
|
|
1114
|
+
const runner = runtime === "bun" ? "bun" : "tsx";
|
|
1115
|
+
const packageJsonExists = existsSync(Path4.join(params.dir, "package.json"));
|
|
1116
|
+
const dirExists = existsSync(params.dir);
|
|
1117
|
+
if (!dirExists) {
|
|
1118
|
+
console.log(`Creating directory ${params.dir}`);
|
|
1119
|
+
mkdirSync2(params.dir, { recursive: true });
|
|
1120
|
+
}
|
|
1121
|
+
process.chdir(params.dir);
|
|
1122
|
+
if (!packageJsonExists) {
|
|
1123
|
+
console.log(`Initializing ${pkm} project...`);
|
|
1124
|
+
await $2`${pkm} init -y`;
|
|
1125
|
+
}
|
|
1126
|
+
if (runtime !== "bun") {
|
|
1127
|
+
console.log(`Setting up project for typescript...`);
|
|
1128
|
+
await $2`${pkm} add -D typescript tsx`;
|
|
1129
|
+
}
|
|
1130
|
+
await $2`${pkm} add -D @tscircuit/react-fiber @tscircuit/builder`;
|
|
1131
|
+
console.log("Changing package name...");
|
|
1132
|
+
const packageJson = JSON.parse(readFileSync3("package.json", "utf-8"));
|
|
1133
|
+
packageJson.name = params.name;
|
|
1134
|
+
writeFileSync3("package.json", JSON.stringify(packageJson, null, 2));
|
|
1135
|
+
console.log(`Adding ".tscircuit" to .gitignore`);
|
|
1136
|
+
appendFileSync(".gitignore", "\n.tscircuit\n*.__tmp_entrypoint.tsx", {
|
|
1137
|
+
encoding: "utf-8",
|
|
1138
|
+
flag: "a+"
|
|
1139
|
+
});
|
|
1140
|
+
console.log("Creating lib and examples directories...");
|
|
1141
|
+
mkdirSync2("examples", { recursive: true });
|
|
1142
|
+
mkdirSync2("lib", { recursive: true });
|
|
1143
|
+
writeFileSync3(Path4.join("lib", "MyCircuit.tsx"), `
|
|
1144
|
+
export const MyCircuit = () => (
|
|
1145
|
+
<resistor
|
|
1146
|
+
resistance="10kohm"
|
|
1147
|
+
name="R1"
|
|
1148
|
+
footprint="0805"
|
|
1149
|
+
/>
|
|
1150
|
+
)
|
|
1151
|
+
`.trim());
|
|
1152
|
+
writeFileSync3("index.ts", `export * from "./lib/MyCircuit"`);
|
|
1153
|
+
writeFileSync3(Path4.join("examples", "MyExample.tsx"), `
|
|
1154
|
+
import { MyCircuit } from "lib/MyCircuit"
|
|
1155
|
+
|
|
1156
|
+
export const MyExample = () => (
|
|
1157
|
+
<MyCircuit />
|
|
1158
|
+
)
|
|
1159
|
+
`.trim());
|
|
1160
|
+
writeFileSync3("README.md", `
|
|
1161
|
+
# ${params.name}
|
|
1162
|
+
|
|
1163
|
+
> This project was generated using [tsck](https://github.com/tscircuit/tscircuit)
|
|
1164
|
+
|
|
1165
|
+
To develop and view the examples, run \`tsck dev\` and open [http://localhost:3020](http://localhost:3020) in your browser.
|
|
1166
|
+
|
|
1167
|
+
## Developing
|
|
1168
|
+
|
|
1169
|
+
You should install an editor like [VS Code](https://code.visualstudio.com/) with a typescript extension. Many web developers already have this.
|
|
1170
|
+
|
|
1171
|
+
Usually, you'll want to develop some named circuits inside of the \`lib\` directory,
|
|
1172
|
+
export them in the \`index.ts\` file, then show how to use them in the \`examples\` directory.
|
|
1173
|
+
|
|
1174
|
+
Any file in \`examples\` will be automatically loaded and appear in the browser preview when you run \`tsck dev\`. It auto-reloads when you make changes, no need to reload the page or re-run the command.
|
|
1175
|
+
|
|
1176
|
+
> [!TIP] Make sure to replace this README with some details about your project and how to use it.
|
|
1177
|
+
|
|
1178
|
+
## Publishing
|
|
1179
|
+
|
|
1180
|
+
After you're satisfied with your project, publish it on the [tscircuit registry](https://registry.tscircuit.com) with \`tsck publish\`
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
`.trim());
|
|
1185
|
+
writeFileSync3("tsconfig.json", `
|
|
1186
|
+
{
|
|
1187
|
+
"compilerOptions": {
|
|
1188
|
+
// Enable latest features
|
|
1189
|
+
"lib": ["ESNext"],
|
|
1190
|
+
"target": "ESNext",
|
|
1191
|
+
"module": "ESNext",
|
|
1192
|
+
"moduleDetection": "force",
|
|
1193
|
+
"jsx": "react-jsx",
|
|
1194
|
+
"allowJs": true,
|
|
1195
|
+
"types": ["@tscircuit/react-fiber"],
|
|
1196
|
+
"baseUrl": ".",
|
|
1197
|
+
|
|
1198
|
+
// Bundler mode
|
|
1199
|
+
"moduleResolution": "bundler",
|
|
1200
|
+
"allowImportingTsExtensions": true,
|
|
1201
|
+
"verbatimModuleSyntax": true,
|
|
1202
|
+
"noEmit": true,
|
|
1203
|
+
|
|
1204
|
+
// Best practices
|
|
1205
|
+
"strict": true,
|
|
1206
|
+
"skipLibCheck": true,
|
|
1207
|
+
"noFallthroughCasesInSwitch": true,
|
|
1208
|
+
|
|
1209
|
+
// Some stricter flags (disabled by default)
|
|
1210
|
+
"noUnusedLocals": false,
|
|
1211
|
+
"noUnusedParameters": false,
|
|
1212
|
+
"noPropertyAccessFromIndexSignature": false
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
`.trim());
|
|
1217
|
+
console.log("Done! Run `tsck dev` to start developing!");
|
|
1218
|
+
};
|
|
984
1219
|
// lib/get-program.ts
|
|
985
1220
|
var getProgram = (ctx) => {
|
|
986
|
-
const cmd = new Command("
|
|
1221
|
+
const cmd = new Command("tsck");
|
|
987
1222
|
cmd.version(package_default.version);
|
|
988
1223
|
const authCmd = cmd.command("auth");
|
|
989
1224
|
authCmd.command("login").action((args) => authLogin(ctx, args));
|
|
@@ -1017,11 +1252,12 @@ var getProgram = (ctx) => {
|
|
|
1017
1252
|
packageExamples.command("get").requiredOption("--package-example-id <package_example_id>").action((args) => packageExamplesGet(ctx, args));
|
|
1018
1253
|
packageExamples.command("create").requiredOption("--package-name-with-version <package_name_with_version>").requiredOption("--file <file>").option("--export <export>", "Name of export to soupify").action((args) => packageExamplesCreate(ctx, args));
|
|
1019
1254
|
cmd.command("publish").action((args) => publish(ctx, args));
|
|
1020
|
-
cmd.command("version").action(() => console.log(`
|
|
1255
|
+
cmd.command("version").action(() => console.log(`tsck v${package_default.version}`));
|
|
1021
1256
|
cmd.command("login").action((args) => authLogin(ctx, args));
|
|
1022
1257
|
cmd.command("logout").action((args) => authLogout(ctx, args));
|
|
1023
1258
|
cmd.command("soupify").requiredOption("--file <file>", "Input example files").option("--export <export_name>", "Name of export to soupify, if not specified, soupify the default/only export").action((args) => soupifyCmd(ctx, args));
|
|
1024
1259
|
cmd.command("dev").option("--cwd <cwd>", "Current working directory").option("--port <port>", "Port to run dev server on").action((args) => devCmd(ctx, args));
|
|
1260
|
+
cmd.command("init").option("--name <name>", "Name of the project").option("--runtime <runtime>", "Runtime to use (attempts to bun, otherwise node/tsx)").option("--dir <dir>", "Directory to initialize (defaults to ./<name> or . if name not provided)").action((args) => initCmd(ctx, args));
|
|
1025
1261
|
cmd.command("add").action((args) => {
|
|
1026
1262
|
});
|
|
1027
1263
|
cmd.command("remove").action((args) => {
|
|
@@ -1033,7 +1269,7 @@ var getProgram = (ctx) => {
|
|
|
1033
1269
|
|
|
1034
1270
|
// lib/util/create-context-and-run-program.ts
|
|
1035
1271
|
import defaultAxios2 from "axios";
|
|
1036
|
-
import
|
|
1272
|
+
import kleur8 from "kleur";
|
|
1037
1273
|
|
|
1038
1274
|
// lib/param-handlers/interact-for-local-directory.ts
|
|
1039
1275
|
import * as fs4 from "fs/promises";
|
|
@@ -1246,6 +1482,7 @@ var interactForPackageName = async ({
|
|
|
1246
1482
|
var PARAM_HANDLERS_BY_PARAM_NAME = {
|
|
1247
1483
|
file: interactForLocalFile,
|
|
1248
1484
|
cwd: interactForLocalDirectory,
|
|
1485
|
+
dir: interactForLocalDirectory,
|
|
1249
1486
|
package_release_id: interactForPackageReleaseId,
|
|
1250
1487
|
package_name: interactForPackageName,
|
|
1251
1488
|
package_name_with_version: interactForPackageNameWithVersion,
|
|
@@ -1255,7 +1492,7 @@ var PARAM_HANDLERS_BY_PARAM_NAME = {
|
|
|
1255
1492
|
// lib/util/create-context-and-run-program.ts
|
|
1256
1493
|
var createContextAndRunProgram = async (process_args) => {
|
|
1257
1494
|
const args = minimist(process_args);
|
|
1258
|
-
const global_config = new Configstore("
|
|
1495
|
+
const global_config = new Configstore("tsck");
|
|
1259
1496
|
const current_profile = args.profile ?? global_config.get("current_profile") ?? "default";
|
|
1260
1497
|
const profile_config = {
|
|
1261
1498
|
get: (key) => global_config.get(`profiles.${current_profile}.${key}`),
|
|
@@ -1278,17 +1515,17 @@ var createContextAndRunProgram = async (process_args) => {
|
|
|
1278
1515
|
if (global_config.get("log_requests")) {
|
|
1279
1516
|
console.log(`Using registry_url: ${registry_url}`);
|
|
1280
1517
|
axios.interceptors.request.use((req) => {
|
|
1281
|
-
console.log(
|
|
1518
|
+
console.log(kleur8.grey(`[REQ] ${req.method?.toUpperCase()} ${req.url}`));
|
|
1282
1519
|
return req;
|
|
1283
1520
|
});
|
|
1284
1521
|
axios.interceptors.response.use((res) => {
|
|
1285
|
-
console.log(
|
|
1522
|
+
console.log(kleur8.grey(`[RES] ${res.status} ${res.config.method?.toUpperCase()} ${res.config.url}`));
|
|
1286
1523
|
return res;
|
|
1287
1524
|
});
|
|
1288
1525
|
}
|
|
1289
1526
|
axios.interceptors.response.use((res) => res, (err) => {
|
|
1290
|
-
console.log(
|
|
1291
|
-
console.log(
|
|
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);
|
|
1292
1529
|
return Promise.reject(err);
|
|
1293
1530
|
});
|
|
1294
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
|
+
}
|
package/lib/cmd-fns/dev/index.ts
CHANGED
|
@@ -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
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
+
}
|
package/lib/cmd-fns/index.ts
CHANGED
|
@@ -24,4 +24,5 @@ export { packageExamplesGet } from "./package-examples-get"
|
|
|
24
24
|
export { packageExamplesCreate } from "./package-examples-create"
|
|
25
25
|
export { publish } from "./publish"
|
|
26
26
|
export { soupifyCmd as soupify } from "./soupify"
|
|
27
|
-
export { devCmd as dev } from "./dev
|
|
27
|
+
export { devCmd as dev } from "./dev"
|
|
28
|
+
export { initCmd as init } from "./init"
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { AppContext } from "../util/app-context"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import {
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
writeFile,
|
|
9
|
+
appendFileSync,
|
|
10
|
+
} from "fs"
|
|
11
|
+
import * as Path from "node:path"
|
|
12
|
+
import $ from "dax-sh"
|
|
13
|
+
|
|
14
|
+
export const initCmd = async (ctx: AppContext, args: any) => {
|
|
15
|
+
const params = z
|
|
16
|
+
.object({
|
|
17
|
+
name: z.string().optional(),
|
|
18
|
+
dir: z.string().optional(),
|
|
19
|
+
runtime: z.enum(["bun", "node"]).optional(),
|
|
20
|
+
})
|
|
21
|
+
.parse(args)
|
|
22
|
+
|
|
23
|
+
if (params.name && !params.dir) {
|
|
24
|
+
params.dir = `./${params.name.split("/").pop()}`
|
|
25
|
+
} else if (!params.dir) {
|
|
26
|
+
params.dir = `.`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let runtime = params.runtime
|
|
30
|
+
if (!runtime) {
|
|
31
|
+
const bunExists = $.commandExistsSync("bun")
|
|
32
|
+
|
|
33
|
+
if (bunExists) {
|
|
34
|
+
runtime = "bun"
|
|
35
|
+
} else {
|
|
36
|
+
runtime = "node"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const pkm = runtime === "bun" ? "bun" : "npm"
|
|
41
|
+
const runner = runtime === "bun" ? "bun" : "tsx"
|
|
42
|
+
|
|
43
|
+
const packageJsonExists = existsSync(Path.join(params.dir, "package.json"))
|
|
44
|
+
const dirExists = existsSync(params.dir)
|
|
45
|
+
|
|
46
|
+
if (!dirExists) {
|
|
47
|
+
console.log(`Creating directory ${params.dir}`)
|
|
48
|
+
mkdirSync(params.dir, { recursive: true })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
process.chdir(params.dir)
|
|
52
|
+
if (!packageJsonExists) {
|
|
53
|
+
console.log(`Initializing ${pkm} project...`)
|
|
54
|
+
await $`${pkm} init -y`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (runtime !== "bun") {
|
|
58
|
+
console.log(`Setting up project for typescript...`)
|
|
59
|
+
await $`${pkm} add -D typescript tsx`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// TODO just allow adding "tscircuit" in the future
|
|
63
|
+
await $`${pkm} add -D @tscircuit/react-fiber @tscircuit/builder`
|
|
64
|
+
|
|
65
|
+
console.log("Changing package name...")
|
|
66
|
+
// Change package.json "name" to params.name
|
|
67
|
+
const packageJson = JSON.parse(readFileSync("package.json", "utf-8"))
|
|
68
|
+
packageJson.name = params.name
|
|
69
|
+
writeFileSync("package.json", JSON.stringify(packageJson, null, 2))
|
|
70
|
+
|
|
71
|
+
console.log(`Adding ".tscircuit" to .gitignore`)
|
|
72
|
+
appendFileSync(".gitignore", "\n.tscircuit\n*.__tmp_entrypoint.tsx", {
|
|
73
|
+
encoding: "utf-8",
|
|
74
|
+
flag: "a+",
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
console.log("Creating lib and examples directories...")
|
|
78
|
+
mkdirSync("examples", { recursive: true })
|
|
79
|
+
mkdirSync("lib", { recursive: true })
|
|
80
|
+
|
|
81
|
+
writeFileSync(
|
|
82
|
+
Path.join("lib", "MyCircuit.tsx"),
|
|
83
|
+
`
|
|
84
|
+
export const MyCircuit = () => (
|
|
85
|
+
<resistor
|
|
86
|
+
resistance="10kohm"
|
|
87
|
+
name="R1"
|
|
88
|
+
footprint="0805"
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
`.trim()
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
writeFileSync("index.ts", `export * from "./lib/MyCircuit"`)
|
|
95
|
+
|
|
96
|
+
writeFileSync(
|
|
97
|
+
Path.join("examples", "MyExample.tsx"),
|
|
98
|
+
`
|
|
99
|
+
import { MyCircuit } from "lib/MyCircuit"
|
|
100
|
+
|
|
101
|
+
export const MyExample = () => (
|
|
102
|
+
<MyCircuit />
|
|
103
|
+
)
|
|
104
|
+
`.trim()
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// Override the README file
|
|
108
|
+
writeFileSync(
|
|
109
|
+
"README.md",
|
|
110
|
+
`
|
|
111
|
+
# ${params.name}
|
|
112
|
+
|
|
113
|
+
> This project was generated using [tsck](https://github.com/tscircuit/tscircuit)
|
|
114
|
+
|
|
115
|
+
To develop and view the examples, run \`tsck dev\` and open [http://localhost:3020](http://localhost:3020) in your browser.
|
|
116
|
+
|
|
117
|
+
## Developing
|
|
118
|
+
|
|
119
|
+
You should install an editor like [VS Code](https://code.visualstudio.com/) with a typescript extension. Many web developers already have this.
|
|
120
|
+
|
|
121
|
+
Usually, you'll want to develop some named circuits inside of the \`lib\` directory,
|
|
122
|
+
export them in the \`index.ts\` file, then show how to use them in the \`examples\` directory.
|
|
123
|
+
|
|
124
|
+
Any file in \`examples\` will be automatically loaded and appear in the browser preview when you run \`tsck dev\`. It auto-reloads when you make changes, no need to reload the page or re-run the command.
|
|
125
|
+
|
|
126
|
+
> [!TIP] Make sure to replace this README with some details about your project and how to use it.
|
|
127
|
+
|
|
128
|
+
## Publishing
|
|
129
|
+
|
|
130
|
+
After you're satisfied with your project, publish it on the [tscircuit registry](https://registry.tscircuit.com) with \`tsck publish\`
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
`.trim()
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
// Open tsconfig.json and modify it to import the tscircuit types
|
|
138
|
+
writeFileSync(
|
|
139
|
+
"tsconfig.json",
|
|
140
|
+
`
|
|
141
|
+
{
|
|
142
|
+
"compilerOptions": {
|
|
143
|
+
// Enable latest features
|
|
144
|
+
"lib": ["ESNext"],
|
|
145
|
+
"target": "ESNext",
|
|
146
|
+
"module": "ESNext",
|
|
147
|
+
"moduleDetection": "force",
|
|
148
|
+
"jsx": "react-jsx",
|
|
149
|
+
"allowJs": true,
|
|
150
|
+
"types": ["@tscircuit/react-fiber"],
|
|
151
|
+
"baseUrl": ".",
|
|
152
|
+
|
|
153
|
+
// Bundler mode
|
|
154
|
+
"moduleResolution": "bundler",
|
|
155
|
+
"allowImportingTsExtensions": true,
|
|
156
|
+
"verbatimModuleSyntax": true,
|
|
157
|
+
"noEmit": true,
|
|
158
|
+
|
|
159
|
+
// Best practices
|
|
160
|
+
"strict": true,
|
|
161
|
+
"skipLibCheck": true,
|
|
162
|
+
"noFallthroughCasesInSwitch": true,
|
|
163
|
+
|
|
164
|
+
// Some stricter flags (disabled by default)
|
|
165
|
+
"noUnusedLocals": false,
|
|
166
|
+
"noUnusedParameters": false,
|
|
167
|
+
"noPropertyAccessFromIndexSignature": false
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
`.trim()
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
console.log("Done! Run `tsck dev` to start developing!")
|
|
175
|
+
}
|
package/lib/get-program.ts
CHANGED
|
@@ -183,6 +183,19 @@ export const getProgram = (ctx: AppContext) => {
|
|
|
183
183
|
.option("--port <port>", "Port to run dev server on")
|
|
184
184
|
.action((args) => CMDFN.dev(ctx, args))
|
|
185
185
|
|
|
186
|
+
cmd
|
|
187
|
+
.command("init")
|
|
188
|
+
.option("--name <name>", "Name of the project")
|
|
189
|
+
.option(
|
|
190
|
+
"--runtime <runtime>",
|
|
191
|
+
"Runtime to use (attempts to bun, otherwise node/tsx)"
|
|
192
|
+
)
|
|
193
|
+
.option(
|
|
194
|
+
"--dir <dir>",
|
|
195
|
+
"Directory to initialize (defaults to ./<name> or . if name not provided)"
|
|
196
|
+
)
|
|
197
|
+
.action((args) => CMDFN.init(ctx, args))
|
|
198
|
+
|
|
186
199
|
cmd.command("add").action((args) => {})
|
|
187
200
|
cmd.command("remove").action((args) => {})
|
|
188
201
|
cmd.command("install").action((args) => {})
|
|
@@ -9,6 +9,7 @@ import { ParamHandler } from "./param-handler-type"
|
|
|
9
9
|
export const PARAM_HANDLERS_BY_PARAM_NAME: Record<string, ParamHandler> = {
|
|
10
10
|
file: interactForLocalFile,
|
|
11
11
|
cwd: interactForLocalDirectory,
|
|
12
|
+
dir: interactForLocalDirectory,
|
|
12
13
|
package_release_id: interactForPackageReleaseId,
|
|
13
14
|
package_name: interactForPackageName,
|
|
14
15
|
package_name_with_version: interactForPackageNameWithVersion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/cli",
|
|
3
|
-
"version": "0.0.
|
|
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",
|
|
@@ -41,13 +43,14 @@
|
|
|
41
43
|
"minimist": "^1.2.8",
|
|
42
44
|
"node-persist": "^4.0.1",
|
|
43
45
|
"open": "^10.1.0",
|
|
44
|
-
"perfect-cli": "1.0.
|
|
46
|
+
"perfect-cli": "1.0.13",
|
|
45
47
|
"prompts": "^2.4.2",
|
|
46
48
|
"react": "^18.2.0",
|
|
47
49
|
"zod": "latest"
|
|
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",
|
|
@@ -60,4 +63,4 @@
|
|
|
60
63
|
"tsx": "^4.7.1",
|
|
61
64
|
"typescript": "^5.3.3"
|
|
62
65
|
}
|
|
63
|
-
}
|
|
66
|
+
}
|