diff --git a/bun.lockb b/bun.lockb index 559cfccd..a20747c4 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/cli/lib/cmd-fns/dev/index.ts b/cli/lib/cmd-fns/dev/index.ts index 2889aa2e..3feb2e70 100644 --- a/cli/lib/cmd-fns/dev/index.ts +++ b/cli/lib/cmd-fns/dev/index.ts @@ -23,6 +23,7 @@ export const devCmd = async (ctx: AppContext, args: any) => { const params = z .object({ port: z.coerce.number().optional().default(3020), + core: z.boolean().optional(), }) .parse(args) diff --git a/cli/lib/cmd-fns/dev/soupify-and-upload-example-file.ts b/cli/lib/cmd-fns/dev/soupify-and-upload-example-file.ts index 9dc555b7..09ecaac6 100644 --- a/cli/lib/cmd-fns/dev/soupify-and-upload-example-file.ts +++ b/cli/lib/cmd-fns/dev/soupify-and-upload-example-file.ts @@ -30,6 +30,7 @@ export const soupifyAndUploadExampleFile = async ( { filePath: examplePath, exportName, + useCore: ctx.params.core, }, ctx, ) diff --git a/cli/lib/cmd-fns/publish/index.ts b/cli/lib/cmd-fns/publish/index.ts index 7dc5491e..b4d2e8ed 100644 --- a/cli/lib/cmd-fns/publish/index.ts +++ b/cli/lib/cmd-fns/publish/index.ts @@ -7,7 +7,7 @@ import { existsSync, readFileSync } from "fs" import { getAllPackageFiles } from "cli/lib/util/get-all-package-files" import prompts from "prompts" import { getGeneratedReadme } from "../init/get-generated-readme" -import { soupify } from "../../soupify" +import { soupify } from "../../soupify/soupify" import { inferExportNameFromSource } from "../dev/infer-export-name-from-source" import $ from "dax-sh" import semver from "semver" diff --git a/cli/lib/cmd-fns/soupify.ts b/cli/lib/cmd-fns/soupify.ts index 29b4c730..828d4622 100644 --- a/cli/lib/cmd-fns/soupify.ts +++ b/cli/lib/cmd-fns/soupify.ts @@ -11,6 +11,7 @@ export const soupifyCmd = async (ctx: AppContext, args: any) => { file: z.string(), export: z.string().optional(), output: z.string().optional(), + core: z.boolean().optional(), }) .parse(args) @@ -18,6 +19,7 @@ export const soupifyCmd = async (ctx: AppContext, args: any) => { { filePath: params.file, exportName: params.export, + useCore: params.core, }, ctx, ) diff --git a/cli/lib/cmd-fns/version.ts b/cli/lib/cmd-fns/version.ts index d58f2df2..16f96624 100644 --- a/cli/lib/cmd-fns/version.ts +++ b/cli/lib/cmd-fns/version.ts @@ -12,7 +12,7 @@ export const versionCmd = async (ctx: AppContext, args: any) => { table.push({ name: "@tscircuit/cli", current: cliPackageJson.version }) table.push({ name: "@tscircuit/react-fiber", - current: cliPackageJson.devDependencies["@tscircuit/react-fiber"], + current: cliPackageJson.dependencies["@tscircuit/react-fiber"], }) table.push({ name: "@tscircuit/schematic-viewer", diff --git a/cli/lib/get-program.ts b/cli/lib/get-program.ts index c6658564..e02a2946 100644 --- a/cli/lib/get-program.ts +++ b/cli/lib/get-program.ts @@ -12,6 +12,7 @@ export const getProgram = (ctx: AppContext) => { .description("Run development server in current directory") .option("--cwd ", "Current working directory") .option("--port ", "Port to run dev server on") + .option("--core", "Use @tscircuit/core beta") .option("--no-cleanup", "Don't cleanup temporary files (for debugging)") .action((args) => CMDFN.dev(ctx, args)) @@ -293,6 +294,7 @@ export const getProgram = (ctx: AppContext) => { .description("Convert an example file to tscircuit soup") .requiredOption("--file ", "Input example files") .option("--output ", "Output file") + .option("--core", "Use @tscircuit/core to build (future version)") .option( "--export ", "Name of export to soupify, if not specified, soupify the default/only export", diff --git a/cli/lib/soupify.ts b/cli/lib/soupify.ts deleted file mode 100644 index 200c46be..00000000 --- a/cli/lib/soupify.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { AppContext } from "./util/app-context" -import { z } from "zod" -import $ from "dax-sh" -import * as Path from "path" -import { unlink } from "node:fs/promises" -import kleur from "kleur" -import { writeFileSync } from "fs" -import { readFile } from "fs/promises" -import Debug from "debug" - -const debug = Debug("tscircuit:soupify") - -export const soupify = async ( - { - filePath, - exportName, - }: { - filePath: string - exportName?: string - }, - ctx: Pick, -) => { - debug(`reading ${filePath}`) - const targetFileContent = await readFile(filePath, "utf-8") - - if (!exportName) { - if (targetFileContent.includes("export default")) { - exportName = "default" - } else { - // Look for "export const " or "export function " - const exportRegex = /export\s+(?:const|function)\s+(\w+)/g - const match = exportRegex.exec(targetFileContent) - if (match) { - exportName = match[1] - } - } - } - - if (!exportName) { - throw new Error( - `Couldn't derive an export name and didn't find default export in "${filePath}"`, - ) - } - - const tmpFilePath = Path.join( - Path.dirname(filePath), - Path.basename(filePath).replace(/\.[^\.]+$/, "") + ".__tmp_entrypoint.tsx", - ) - - debug(`writing to ${tmpFilePath}`) - writeFileSync( - tmpFilePath, - ` -import React from "react" -import { createRoot } from "@tscircuit/react-fiber" -import { createProjectBuilder } from "@tscircuit/builder" - -import * as EXPORTS from "./${Path.basename(filePath)}" - -const Component = EXPORTS["${exportName}"] - -if (!Component) { - console.log(JSON.stringify({ - COMPILE_ERROR: 'Failed to find "${exportName}" export in "${filePath}"' - })) - process.exit(0) -} - -const projectBuilder = createProjectBuilder() -const elements = await createRoot().render(, projectBuilder) - -console.log(JSON.stringify(elements)) - -`.trim(), - ) - - debug(`using runtime ${ctx.runtime}`) - const processCmdPart1 = - ctx.runtime === "node" ? $`npx tsx ${tmpFilePath}` : $`bun ${tmpFilePath}` - - debug(`starting process....`) - const processResult = await processCmdPart1 - .stdout(debug.enabled ? "inheritPiped" : "piped") - .stderr(debug.enabled ? "inheritPiped" : "piped") - .noThrow() - - // TODO we should send it to a temporarty file rather than rely on stdout - // which can easily be polluted - const rawSoup = processResult.stdout.replace(/^[^\[]*/, "") - const errText = processResult.stderr - - if (ctx.params.cleanup !== false) { - debug(`deleting ${tmpFilePath}`) - await unlink(tmpFilePath) - } - - try { - debug(`parsing result of soupify...`) - const soup = JSON.parse(rawSoup) - - if (soup.COMPILE_ERROR) { - // console.log(kleur.red(`Failed to compile ${filePath}`)) - console.log(kleur.red(soup.COMPILE_ERROR)) - throw new Error(soup.COMPILE_ERROR) - } - - return soup - } catch (e: any) { - // console.log(kleur.red(`Failed to parse result of soupify: ${e.toString()}`)) - const t = Date.now() - console.log(`Dumping raw output to .tscircuit/err-${t}.log`) - writeFileSync(`.tscircuit/err-${t}.log`, rawSoup + "\n\n" + errText) - throw new Error(errText) - } -} diff --git a/cli/lib/soupify/get-export-name-from-file.ts b/cli/lib/soupify/get-export-name-from-file.ts new file mode 100644 index 00000000..20e0eaf3 --- /dev/null +++ b/cli/lib/soupify/get-export-name-from-file.ts @@ -0,0 +1,29 @@ +import { readFile } from "node:fs/promises" +import Debug from "debug" + +const debug = Debug("tscircuit:soupify") + +export const getExportNameFromFile = async (filePath: string) => { + debug(`reading ${filePath}`) + const targetFileContent = await readFile(filePath, "utf-8") + + let exportName: string | undefined + if (targetFileContent.includes("export default")) { + exportName = "default" + } else { + // Look for "export const " or "export function " + const exportRegex = /export\s+(?:const|function)\s+(\w+)/g + const match = exportRegex.exec(targetFileContent) + if (match) { + exportName = match[1] + } + } + + if (!exportName) { + throw new Error( + `Couldn't derive an export name and didn't find default export in "${filePath}"`, + ) + } + + return exportName +} diff --git a/cli/lib/soupify/get-tmp-entrpoint-filepath.ts b/cli/lib/soupify/get-tmp-entrpoint-filepath.ts new file mode 100644 index 00000000..8906560c --- /dev/null +++ b/cli/lib/soupify/get-tmp-entrpoint-filepath.ts @@ -0,0 +1,13 @@ +import Path from "node:path" + +export const getTmpEntrypointFilePath = (filePath: string) => { + const tmpEntrypointPath = Path.join( + Path.dirname(filePath), + Path.basename(filePath).replace(/\.[^\.]+$/, "") + ".__tmp_entrypoint.tsx", + ) + const tmpOutputPath = Path.join( + Path.dirname(filePath), + Path.basename(filePath).replace(/\.[^\.]+$/, "") + ".__tmp_output.json", + ) + return { tmpEntrypointPath, tmpOutputPath } +} diff --git a/cli/lib/soupify/index.ts b/cli/lib/soupify/index.ts new file mode 100644 index 00000000..d26850cb --- /dev/null +++ b/cli/lib/soupify/index.ts @@ -0,0 +1 @@ +export * from "./soupify" diff --git a/cli/lib/soupify/run-entrypoint-file.ts b/cli/lib/soupify/run-entrypoint-file.ts new file mode 100644 index 00000000..4032e33b --- /dev/null +++ b/cli/lib/soupify/run-entrypoint-file.ts @@ -0,0 +1,60 @@ +import Debug from "debug" +import { writeFileSync } from "fs" +import { readFile, unlink } from "node:fs/promises" +import $ from "dax-sh" +import kleur from "kleur" +import { AppContext } from "../util/app-context" + +const debug = Debug("tscircuit:soupify") + +/** + * Runs the entrypoint file to generate circuit json (soup) for a given file + */ +export const runEntrypointFile = async ( + { + tmpEntrypointPath, + tmpOutputPath, + }: { tmpEntrypointPath: string; tmpOutputPath: string }, + ctx: Pick, +) => { + debug(`using runtime ${ctx.runtime}`) + const processCmdPart1 = + ctx.runtime === "node" + ? $`npx tsx ${tmpEntrypointPath}` + : $`bun ${tmpEntrypointPath}` + + debug(`starting process....`) + const processResult = await processCmdPart1 + .stdout(debug.enabled ? "inheritPiped" : "piped") + .stderr(debug.enabled ? "inheritPiped" : "piped") + .noThrow() + + const rawSoup = await readFile(tmpOutputPath, "utf-8") + const errText = processResult.stderr + + if (ctx.params.cleanup !== false) { + debug(`deleting ${tmpEntrypointPath}`) + await unlink(tmpEntrypointPath) + debug(`deleting ${tmpOutputPath}`) + await unlink(tmpOutputPath) + } + + try { + debug(`parsing result of soupify...`) + const soup = JSON.parse(rawSoup) + + if (soup.COMPILE_ERROR) { + // console.log(kleur.red(`Failed to compile ${filePath}`)) + console.log(kleur.red(soup.COMPILE_ERROR)) + throw new Error(soup.COMPILE_ERROR) + } + + return soup + } catch (e: any) { + // console.log(kleur.red(`Failed to parse result of soupify: ${e.toString()}`)) + const t = Date.now() + console.log(`Dumping raw output to .tscircuit/err-${t}.log`) + writeFileSync(`.tscircuit/err-${t}.log`, rawSoup + "\n\n" + errText) + throw new Error(errText) + } +} diff --git a/cli/lib/soupify/soupify-with-core.ts b/cli/lib/soupify/soupify-with-core.ts new file mode 100644 index 00000000..85296dd3 --- /dev/null +++ b/cli/lib/soupify/soupify-with-core.ts @@ -0,0 +1,52 @@ +import { AppContext } from "../util/app-context" +import { z } from "zod" +import $ from "dax-sh" +import * as Path from "path" +import { unlink } from "node:fs/promises" +import kleur from "kleur" +import { writeFileSync } from "fs" +import { readFile } from "fs/promises" +import Debug from "debug" +import { getExportNameFromFile } from "./get-export-name-from-file" +import { getTmpEntrypointFilePath } from "./get-tmp-entrpoint-filepath" +import { runEntrypointFile } from "./run-entrypoint-file" + +const debug = Debug("tscircuit:soupify") + +export const soupifyWithCore = async ( + params: { + filePath: string + exportName?: string + }, + ctx: Pick, +) => { + let { filePath, exportName } = params + + exportName ??= await getExportNameFromFile(filePath) + + const { tmpEntrypointPath, tmpOutputPath } = + getTmpEntrypointFilePath(filePath) + + debug(`writing to ${tmpEntrypointPath}`) + writeFileSync( + tmpEntrypointPath, + ` +import React from "react" +import { Project } from "@tscircuit/core" +import * as EXPORTS from "./${Path.basename(filePath)}" +import { writeFileSync } from "node:fs" + +const Component = EXPORTS["${exportName}"] + +const project = new Project() + +project.add() + +project.render() + +writeFileSync("${tmpOutputPath}", JSON.stringify(project.getCircuitJson())) +`.trim(), + ) + + return await runEntrypointFile({ tmpEntrypointPath, tmpOutputPath }, ctx) +} diff --git a/cli/lib/soupify/soupify.ts b/cli/lib/soupify/soupify.ts new file mode 100644 index 00000000..8510044d --- /dev/null +++ b/cli/lib/soupify/soupify.ts @@ -0,0 +1,77 @@ +import { AppContext } from "../util/app-context" +import { z } from "zod" +import $ from "dax-sh" +import * as Path from "path" +import { unlink } from "node:fs/promises" +import kleur from "kleur" +import { writeFileSync } from "fs" +import { readFile } from "fs/promises" +import Debug from "debug" +import { soupifyWithCore } from "./soupify-with-core" +import { getExportNameFromFile } from "./get-export-name-from-file" +import { getTmpEntrypointFilePath } from "./get-tmp-entrpoint-filepath" +import { runEntrypointFile } from "./run-entrypoint-file" + +const debug = Debug("tscircuit:soupify") + +export const soupifyWithBuilder = async ( + params: { + filePath: string + exportName?: string + /** + * Use @tscircuit/core instead of @tscircuit/builder, this will be the + * default eventually + */ + useCore?: boolean + }, + ctx: Pick, +) => { + let { filePath, exportName, useCore } = params + if (useCore) return soupifyWithCore(params, ctx) + + exportName ??= await getExportNameFromFile(filePath) + + const { tmpEntrypointPath, tmpOutputPath } = + getTmpEntrypointFilePath(filePath) + + debug(`writing to ${tmpEntrypointPath}`) + writeFileSync( + tmpEntrypointPath, + ` +import React from "react" +import { createRoot } from "@tscircuit/react-fiber" +import { createProjectBuilder } from "@tscircuit/builder" +import { writeFileSync } from "node:fs" + +let Component +try { + const EXPORTS = await import("./${Path.basename(filePath)}") + Component = EXPORTS["${exportName}"] +} catch (e) { + writeFileSync("${tmpOutputPath}", JSON.stringify({ + COMPILE_ERROR: e.message + "\\n\\n" + e.stack, + })) +} + +if (!Component) { + console.log(JSON.stringify({ + COMPILE_ERROR: 'Failed to find "${exportName}" export in "${filePath}"' + })) + writeFileSync("${tmpOutputPath}", JSON.stringify({ + COMPILE_ERROR: e.message + "\\n\\n" + e.stack, + })) + process.exit(0) +} + +const projectBuilder = createProjectBuilder() +const elements = await createRoot().render(, projectBuilder) + +writeFileSync("${tmpOutputPath}", JSON.stringify(elements)) + +`.trim(), + ) + + return await runEntrypointFile({ tmpEntrypointPath, tmpOutputPath }, ctx) +} + +export const soupify = soupifyWithBuilder diff --git a/cli/tests/soupify.test.ts b/cli/tests/soupify-builder.test.ts similarity index 85% rename from cli/tests/soupify.test.ts rename to cli/tests/soupify-builder.test.ts index feee4410..1189f0b3 100644 --- a/cli/tests/soupify.test.ts +++ b/cli/tests/soupify-builder.test.ts @@ -1,7 +1,7 @@ import { test, expect, describe } from "bun:test" import { $ } from "bun" -test("soupify", async () => { +test("soupify (builder)", async () => { const result = await $`bun cli/cli.ts soupify -y --file ./example-project/examples/basic-chip.tsx`.text() diff --git a/cli/tests/soupify-core.test.ts b/cli/tests/soupify-core.test.ts new file mode 100644 index 00000000..44e85230 --- /dev/null +++ b/cli/tests/soupify-core.test.ts @@ -0,0 +1,9 @@ +import { test, expect, describe } from "bun:test" +import { $ } from "bun" + +test("soupify (core)", async () => { + const result = + await $`bun cli/cli.ts soupify --core -y --file ./example-project/examples/basic-chip.tsx`.text() + + expect(result).toContain("10000") // R1 resistor value +}) diff --git a/example-project/examples/basic-chip.tsx b/example-project/examples/basic-chip.tsx index 07eab06e..6b58f1f8 100644 --- a/example-project/examples/basic-chip.tsx +++ b/example-project/examples/basic-chip.tsx @@ -1,14 +1,15 @@ +import "@tscircuit/core" import { layout } from "@tscircuit/layout" import manual_edits from "../src/manual-edits" -export const BasicBug = () => ( +export const BasicChip = () => ( - ( }} /> - + ) diff --git a/frontend/views/HeaderMenu.tsx b/frontend/views/HeaderMenu.tsx index 024061f5..47a564de 100644 --- a/frontend/views/HeaderMenu.tsx +++ b/frontend/views/HeaderMenu.tsx @@ -288,7 +288,7 @@ export const HeaderMenu = () => { @tscircuit/react-fiber v - {cliPackageJson.devDependencies["@tscircuit/react-fiber"].replace( + {cliPackageJson.dependencies["@tscircuit/react-fiber"].replace( /\^/g, "", )} diff --git a/package.json b/package.json index c26d919a..c5975d26 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,9 @@ "dependencies": { "@edge-runtime/primitives": "^4.1.0", "@hono/node-server": "^1.8.2", + "@tscircuit/builder": "*", + "@tscircuit/core": "0.0.14", + "@tscircuit/react-fiber": "1.2.0", "archiver": "^7.0.1", "axios": "^1.6.7", "chokidar": "^3.6.0", @@ -66,8 +69,7 @@ "ts-morph": "^22.0.0", "tsup": "^8.0.2", "winterspec": "0.0.86", - "zod": "^3.22.4", - "@tscircuit/builder": "*" + "zod": "^3.22.4" }, "peerDependencies": { "@tscircuit/builder": "*", @@ -96,7 +98,6 @@ "@tscircuit/layout": "^0.0.25", "@tscircuit/manual-edit-events": "^0.0.4", "@tscircuit/pcb-viewer": "^1.4.4", - "@tscircuit/react-fiber": "*", "@tscircuit/schematic-viewer": "^1.2.14", "@tscircuit/soup-util": "^0.0.20", "@tscircuit/table-viewer": "0.0.8", diff --git a/tsconfig.json b/tsconfig.json index 4885fbcd..0f5a3614 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,8 +13,17 @@ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + "paths": { + "cli/*": ["cli/*"], + "frontend/*": ["frontend/*"], + "api/*": ["api/*"], + "scripts/*": ["scripts/*"], + }, + "strict": true /* Enable all strict type-checking options. */, "skipLibCheck": true, - "verbatimModuleSyntax": false + "verbatimModuleSyntax": false, }, + "include": ["src/**/*", "cli/**/*", "frontend/**/*", "api/**/*", "scripts/**/*", "example-project/**/*"], + "exclude": ["node_modules"] }