From ef2a9efd11ab1ae078cbb6667f77943fe7db9f32 Mon Sep 17 00:00:00 2001 From: 64johnlee <64lamei@gmail.com> Date: Wed, 27 May 2026 02:09:55 +0800 Subject: [PATCH] fix: allow tsci push without entrypoint when circuit.json exists Closes #2797. When no configured `mainEntrypoint` and no standard `index.{circuit.,}tsx` are found, fall back to using the first discovered `circuit.json` / `*.circuit.json` file as an implicit entrypoint. This matches `tsci dev`, which already accepts a prebuilt circuit.json. - lib/shared/get-entrypoint.ts: recursive circuit.json scan with the same depth/results/boundary guards as findEntrypointsRecursively; skips node_modules, dist, and dot-dirs; runs only after all existing entrypoint detection paths have failed - cli/build/register.ts: when the chosen entrypoint isn't a .ts/.tsx file (e.g. a circuit.json), skip transpilation gracefully instead of failing the build - tests/get-entrypoint.test.ts: covers circuit.json fallback and tsx preference over circuit.json --- cli/build/register.ts | 10 ++++++ lib/shared/get-entrypoint.ts | 65 ++++++++++++++++++++++++++++++++++++ tests/get-entrypoint.test.ts | 46 +++++++++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/cli/build/register.ts b/cli/build/register.ts index c1b71c96d..674eda271 100644 --- a/cli/build/register.ts +++ b/cli/build/register.ts @@ -762,6 +762,10 @@ export const registerBuild = (program: Command) => { const entryFile = fileArgIsDirectFile ? resolvedFileArgPath : transpileEntrypoint + const isRealTsEntrypoint = Boolean( + entryFile && + (entryFile.endsWith(".ts") || entryFile.endsWith(".tsx")), + ) if (!entryFile) { if ( hasConfiguredIncludeBoardFiles && @@ -776,6 +780,12 @@ export const registerBuild = (program: Command) => { ) exitBuild(1, "transpile entry file not found") } + } else if (!isRealTsEntrypoint && !transpileExplicitlyRequested) { + console.log( + hasConfiguredIncludeBoardFiles + ? "Skipping transpilation because includeBoardFiles is configured and no library entrypoint was found." + : "Skipping transpilation because entrypoint is not a TypeScript file.", + ) } else { const transpileSuccess = await transpileFile({ input: entryFile, diff --git a/lib/shared/get-entrypoint.ts b/lib/shared/get-entrypoint.ts index c2858b38d..761b62afb 100644 --- a/lib/shared/get-entrypoint.ts +++ b/lib/shared/get-entrypoint.ts @@ -82,6 +82,58 @@ const findEntrypointsRecursively = ( return results } +const findCircuitJsonFiles = ( + dir: string, + projectDir: string, + maxDepth: number = MAX_SEARCH_DEPTH, +): string[] => { + if (maxDepth <= 0 || !isValidDirectory(dir, projectDir)) { + return [] + } + + const results: string[] = [] + + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + + for (const entry of entries) { + if (results.length >= MAX_RESULTS) break + + if ( + entry.isFile() && + (entry.name === "circuit.json" || entry.name.endsWith(".circuit.json")) + ) { + const filePath = path.resolve(dir, entry.name) + if (isValidDirectory(filePath, projectDir)) { + results.push(filePath) + } + } + } + + for (const entry of entries) { + if (results.length >= MAX_RESULTS) break + + if ( + entry.isDirectory() && + !entry.name.startsWith(".") && + entry.name !== "node_modules" && + entry.name !== "dist" + ) { + const subdirPath = path.resolve(dir, entry.name) + if (isValidDirectory(subdirPath, projectDir)) { + results.push( + ...findCircuitJsonFiles(subdirPath, projectDir, maxDepth - 1), + ) + } + } + } + } catch { + return [] + } + + return results +} + const validateProjectDir = (projectDir: string): string => { const resolvedDir = path.resolve(projectDir) if (!fs.existsSync(resolvedDir)) { @@ -202,6 +254,19 @@ export const getEntrypoint = async ({ } } + // No entrypoint found - check for circuit.json files as implicit entrypoints + // This allows `tsci push` to work the same as `tsci dev` which supports circuit.json files + const circuitJsonFiles = findCircuitJsonFiles( + validatedProjectDir, + validatedProjectDir, + ).sort() + + if (circuitJsonFiles.length > 0) { + const chosenFile = path.relative(validatedProjectDir, circuitJsonFiles[0]) + onSuccess(`Using circuit.json as implicit entrypoint: '${chosenFile}'`) + return circuitJsonFiles[0] + } + onError( kleur.red( "No entrypoint found. Run 'tsci init' to bootstrap a basic project or specify a file with 'tsci push '", diff --git a/tests/get-entrypoint.test.ts b/tests/get-entrypoint.test.ts index 28ee40c54..ff30ef7de 100644 --- a/tests/get-entrypoint.test.ts +++ b/tests/get-entrypoint.test.ts @@ -519,3 +519,49 @@ test("getEntrypoint warns when multiple common locations exist", async () => { expect(warnings[0]).toContain("Choosing 'index.tsx'") expect(warnings[0]).toContain("'src/index.tsx'") }) + +test("getEntrypoint returns circuit.json as implicit entrypoint when no tsx/ts files exist", async () => { + const { tmpDir } = await getCliTestFixture() + + // Create only a circuit.json file, no tsx/ts entrypoints + await fs.writeFile( + path.join(tmpDir, "prebuilt.circuit.json"), + JSON.stringify([{ type: "source_component", name: "U1" }]), + ) + + let onSuccessMessage = "" + const entrypoint = await getEntrypoint({ + projectDir: tmpDir, + onSuccess: (msg) => { + onSuccessMessage = msg + }, + }) + + expect(entrypoint).not.toBeNull() + expect(entrypoint).toBe(path.join(tmpDir, "prebuilt.circuit.json")) + expect(onSuccessMessage).toContain( + "Using circuit.json as implicit entrypoint", + ) +}) + +test("getEntrypoint prefers tsx entrypoint over circuit.json", async () => { + const { tmpDir } = await getCliTestFixture() + + // Create both a circuit.json and an index.tsx + await fs.writeFile( + path.join(tmpDir, "prebuilt.circuit.json"), + JSON.stringify([{ type: "source_component", name: "U1" }]), + ) + await fs.writeFile( + path.join(tmpDir, "index.tsx"), + 'export default () => ', + ) + + const entrypoint = await getEntrypoint({ + projectDir: tmpDir, + }) + + // Should prefer the tsx file since it comes first in ALLOWED_ENTRYPOINT_NAMES + expect(entrypoint).not.toBeNull() + expect(entrypoint).toBe(path.join(tmpDir, "index.tsx")) +})