diff --git a/cli/build/register.ts b/cli/build/register.ts index c1b71c96..674eda27 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 c2858b38..761b62af 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 28ee40c5..ff30ef7d 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")) +})