From 6c7fac8c527665755b1afd3de693270caee2a307 Mon Sep 17 00:00:00 2001 From: techmannih Date: Wed, 3 Jun 2026 16:06:16 +0530 Subject: [PATCH 1/2] Fix file-based project context resolution --- cli/export/register.ts | 19 +++++++---- cli/simulate/register.ts | 14 ++++++-- lib/project-config/index.ts | 33 +++++++++++++++++++ lib/shared/convert-to-kicad-library.tsx | 5 +-- lib/shared/export-snippet.ts | 5 ++- lib/shared/generate-circuit-json.tsx | 13 ++++---- lib/shared/importFromUserLand.ts | 29 ++++++++-------- ...ntime-project-config-command-flows.test.ts | 28 ++++++++++++++++ 8 files changed, 114 insertions(+), 32 deletions(-) diff --git a/cli/export/register.ts b/cli/export/register.ts index 1a3d79b2d..9930cc0dd 100644 --- a/cli/export/register.ts +++ b/cli/export/register.ts @@ -9,7 +9,10 @@ import { resultToCsv } from "lib/shared/result-to-csv" import path from "node:path" import { promises as fs } from "node:fs" import type { PlatformConfig } from "@tscircuit/props" -import { loadRuntimeProjectConfig } from "lib/project-config" +import { + findNearestProjectConfigDir, + loadRuntimeProjectConfig, +} from "lib/project-config" import { mergePlatformConfigs } from "lib/shared/platform-config-utils" export const registerExport = (program: Command) => { @@ -35,7 +38,11 @@ export const registerExport = (program: Command) => { }, ) => { const formatOption = options.format ?? "json" - const projectConfig = await loadRuntimeProjectConfig(process.cwd()) + const resolvedFilePath = path.resolve(file) + const fileProjectDir = path.dirname(resolvedFilePath) + const projectConfigDir = + findNearestProjectConfigDir(fileProjectDir) ?? fileProjectDir + const projectConfig = await loadRuntimeProjectConfig(projectConfigDir) const commandPlatformConfig: PlatformConfig | undefined = options.disablePartsEngine === true @@ -48,7 +55,7 @@ export const registerExport = (program: Command) => { if (formatOption === "spice") { const { circuitJson } = await generateCircuitJson({ - filePath: file, + filePath: resolvedFilePath, platformConfig, }) if (circuitJson) { @@ -57,8 +64,8 @@ export const registerExport = (program: Command) => { const outputSpicePath = options.output ?? path.join( - path.dirname(file), - `${path.basename(file, path.extname(file))}.spice.cir`, + path.dirname(resolvedFilePath), + `${path.basename(resolvedFilePath, path.extname(resolvedFilePath))}.spice.cir`, ) await fs.writeFile(outputSpicePath, spiceString) @@ -83,7 +90,7 @@ export const registerExport = (program: Command) => { const format = formatOption as ExportFormat await exportSnippet({ - filePath: file, + filePath: resolvedFilePath, format, outputPath: options.output, platformConfig, diff --git a/cli/simulate/register.ts b/cli/simulate/register.ts index 24c5c84e9..1c4eb40d1 100644 --- a/cli/simulate/register.ts +++ b/cli/simulate/register.ts @@ -4,8 +4,12 @@ import { runSimulation } from "lib/eecircuit-engine/run-simulation" import { resultToTable } from "lib/shared/result-to-table" import { getSpiceWithPaddedSim } from "lib/shared/get-spice-with-sim" import type { PlatformConfig } from "@tscircuit/props" -import { loadRuntimeProjectConfig } from "lib/project-config" +import { + findNearestProjectConfigDir, + loadRuntimeProjectConfig, +} from "lib/project-config" import { mergePlatformConfigs } from "lib/shared/platform-config-utils" +import path from "node:path" export const registerSimulate = (program: Command) => { const simulateCommand = program @@ -18,7 +22,11 @@ export const registerSimulate = (program: Command) => { .argument("", "Path to tscircuit tsx or circuit json file") .option("--disable-parts-engine", "Disable the parts engine") .action(async (file: string, options: { disablePartsEngine?: boolean }) => { - const projectConfig = await loadRuntimeProjectConfig(process.cwd()) + const resolvedFilePath = path.resolve(file) + const fileProjectDir = path.dirname(resolvedFilePath) + const projectConfigDir = + findNearestProjectConfigDir(fileProjectDir) ?? fileProjectDir + const projectConfig = await loadRuntimeProjectConfig(projectConfigDir) const commandPlatformConfig: PlatformConfig | undefined = options.disablePartsEngine === true ? { partsEngineDisabled: true } @@ -29,7 +37,7 @@ export const registerSimulate = (program: Command) => { ) const { circuitJson } = await generateCircuitJson({ - filePath: file, + filePath: resolvedFilePath, saveToFile: false, platformConfig, }) diff --git a/lib/project-config/index.ts b/lib/project-config/index.ts index cf90c85c5..44d651f5b 100644 --- a/lib/project-config/index.ts +++ b/lib/project-config/index.ts @@ -22,6 +22,7 @@ const CONFIG_MODULE_FILENAMES = [ "tscircuit.config.ts", "tscircuit.config.js", ] as const +const CONFIG_FILENAMES = [CONFIG_FILENAME, ...CONFIG_MODULE_FILENAMES] as const const ENV_FILENAMES = [".env", ".env.local"] as const export const CONFIG_SCHEMA_URL = "https://cdn.jsdelivr.net/npm/@tscircuit/cli/types/tscircuit.config.schema.json" @@ -168,6 +169,38 @@ export const loadRuntimeProjectConfig = async ( } } +export const findNearestProjectConfigDir = ( + startPath: string = process.cwd(), +): string | null => { + const resolvedStartPath = path.resolve(startPath) + let currentDir = resolvedStartPath + + try { + if (!fs.statSync(resolvedStartPath).isDirectory()) { + currentDir = path.dirname(resolvedStartPath) + } + } catch { + currentDir = path.dirname(resolvedStartPath) + } + + while (true) { + if ( + CONFIG_FILENAMES.some((configFileName) => + fs.existsSync(path.join(currentDir, configFileName)), + ) + ) { + return currentDir + } + + const parentDir = path.dirname(currentDir) + if (parentDir === currentDir) { + return null + } + + currentDir = parentDir + } +} + export const loadProjectConfig = ( projectDir: string = process.cwd(), ): TscircuitProjectConfig | null => { diff --git a/lib/shared/convert-to-kicad-library.tsx b/lib/shared/convert-to-kicad-library.tsx index dfee202f2..66ebe5777 100644 --- a/lib/shared/convert-to-kicad-library.tsx +++ b/lib/shared/convert-to-kicad-library.tsx @@ -57,11 +57,12 @@ export async function convertToKicadLibrary({ const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath) + const projectDir = path.dirname(absoluteFilePath) // Import React and tscircuit from userland for rendering components - const React = await importFromUserLand("react") + const React = await importFromUserLand("react", projectDir) ;(globalThis as any).React = React - const userLandTscircuit = await importFromUserLand("tscircuit") + const userLandTscircuit = await importFromUserLand("tscircuit", projectDir) // Use provided module or default const { KicadLibraryConverter } = diff --git a/lib/shared/export-snippet.ts b/lib/shared/export-snippet.ts index ef5b9c6af..033beb58b 100644 --- a/lib/shared/export-snippet.ts +++ b/lib/shared/export-snippet.ts @@ -226,7 +226,10 @@ export const exportSnippet = async ({ break case "srj": { - const userLandTscircuit = await importFromUserLand("tscircuit") + const userLandTscircuit = await importFromUserLand( + "tscircuit", + projectDir, + ) const simpleRouteJson = unwrapSimpleRouteJson( userLandTscircuit.getSimpleRouteJsonFromCircuitJson({ circuitJson, diff --git a/lib/shared/generate-circuit-json.tsx b/lib/shared/generate-circuit-json.tsx index 8a76ece68..55d694195 100644 --- a/lib/shared/generate-circuit-json.tsx +++ b/lib/shared/generate-circuit-json.tsx @@ -52,19 +52,20 @@ export async function generateCircuitJson({ }: GenerateCircuitJsonOptions) { debug(`Generating circuit JSON for ${filePath}`) + const absoluteFilePath = path.isAbsolute(filePath) + ? filePath + : path.resolve(process.cwd(), filePath) + const projectDir = path.dirname(absoluteFilePath) + // Import React and make it globally available for packages referencing it - const React = await importFromUserLand("react") + const React = await importFromUserLand("react", projectDir) ;(globalThis as any).React = React registerStaticAssetLoaders(platformConfig) - const userLandTscircuit = await importFromUserLand("tscircuit") + const userLandTscircuit = await importFromUserLand("tscircuit", projectDir) const runner = new userLandTscircuit.RootCircuit({ platform: platformConfig, }) - const absoluteFilePath = path.isAbsolute(filePath) - ? filePath - : path.resolve(process.cwd(), filePath) - const projectDir = path.dirname(absoluteFilePath) const resolvedOutputDir = outputDir ?? projectDir // Get the relative path to the component from the project directory diff --git a/lib/shared/importFromUserLand.ts b/lib/shared/importFromUserLand.ts index da83ae2d2..9f0a5f00b 100644 --- a/lib/shared/importFromUserLand.ts +++ b/lib/shared/importFromUserLand.ts @@ -1,20 +1,21 @@ import { createRequire } from "node:module" -import fs from "node:fs" -import path, { relative, resolve } from "node:path" +import path from "node:path" -export async function importFromUserLand(moduleName: string) { +export async function importFromUserLand( + moduleName: string, + baseDir: string = process.cwd(), +) { // First try to resolve relative to the user's project without triggering - // Bun's auto-install (which can pull in inconsistent dependency versions) - const userModulePath = path.join(process.cwd(), "node_modules", moduleName) - if (fs.existsSync(userModulePath)) { - const userRequire = createRequire(path.join(process.cwd(), "noop.js")) - try { - const resolvedUserPath = userRequire.resolve(moduleName) - return await import(resolvedUserPath) - } catch (error: any) { - if (error?.code !== "MODULE_NOT_FOUND") { - throw error - } + // Bun's auto-install (which can pull in inconsistent dependency versions). + // `createRequire().resolve()` walks parent directories, so nested entrypoints + // still pick up dependencies from the nearest project root. + const userRequire = createRequire(path.join(path.resolve(baseDir), "noop.js")) + try { + const resolvedUserPath = userRequire.resolve(moduleName) + return await import(resolvedUserPath) + } catch (error: any) { + if (error?.code !== "MODULE_NOT_FOUND") { + throw error } } diff --git a/tests/cli/runtime-project-config-command-flows.test.ts b/tests/cli/runtime-project-config-command-flows.test.ts index 1255cedf7..8ef671245 100644 --- a/tests/cli/runtime-project-config-command-flows.test.ts +++ b/tests/cli/runtime-project-config-command-flows.test.ts @@ -242,6 +242,34 @@ test("export circuit-json consumes runtime platformConfig from tscircuit.config. ).toBe(true) }, 30_000) +test("export circuit-json resolves runtime config from the target file directory", async () => { + const { tmpDir, runCommand } = await getCliTestFixture() + const projectDir = join(tmpDir, "examples", "ti-parts-engine") + const circuitPath = join(projectDir, "index.circuit.tsx") + + await mkdir(projectDir, { recursive: true }) + await writeFile(circuitPath, tiBoardCircuitCode) + await writeFile( + join(projectDir, "tscircuit.config.ts"), + createTiPlatformConfigModule(), + ) + + const { stderr, exitCode } = await runCommand( + `tsci export ${circuitPath} -f circuit-json`, + ) + + expect(exitCode).toBe(0) + expect(stderr).toBe("") + + const circuitJson = JSON.parse( + await readFile(join(projectDir, "index.circuit.circuit.json"), "utf-8"), + ) + + expect( + circuitJson.some((element: any) => element.type === "pcb_smtpad"), + ).toBe(true) +}, 30_000) + test("simulate analog consumes runtime platformConfig from tscircuit.config.ts", async () => { const { tmpDir, runCommand } = await getCliTestFixture() const circuitPath = join(tmpDir, "analog.circuit.tsx") From 44e97af459a0f2de601fc59689704378dadbd003 Mon Sep 17 00:00:00 2001 From: techmannih Date: Wed, 3 Jun 2026 16:07:47 +0530 Subject: [PATCH 2/2] Add ti-parts-engine dependency --- bun.lock | 7 +++++-- package.json | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index 7eb9b9029..6e978cf7d 100644 --- a/bun.lock +++ b/bun.lock @@ -49,6 +49,7 @@ "easyeda": "^0.0.269", "fuse.js": "^7.1.0", "get-port": "^7.1.0", + "@tscircuit/ti-parts-engine": "github:tscircuit/ti-parts-engine", "globby": "^14.1.0", "jose": "^6.1.0", "jsonwebtoken": "^9.0.2", @@ -366,7 +367,7 @@ "@tscircuit/circuit-json-routing-analysis": ["@tscircuit/circuit-json-routing-analysis@0.0.1", "", { "dependencies": { "flatbush": "^4.5.1" }, "peerDependencies": { "typescript": "^5" } }, "sha512-vxXM5Vo92R4GjqYSuGrgRTU8jh3An8tUt4yvBvBALwkAswMWSXJIJFnA/n7wlV9S0uzv9uOvIwizKtbyUgNBpA=="], - "@tscircuit/circuit-json-schematic-placement-analysis": ["@tscircuit/circuit-json-schematic-placement-analysis@github:tscircuit/circuit-json-schematic-placement-analysis#700017d", { "dependencies": { "@tscircuit/circuit-json-util": "^0.0.94" }, "peerDependencies": { "circuit-json": "*", "typescript": "^5" } }, "tscircuit-circuit-json-schematic-placement-analysis-700017d", "sha512-kzB1R8Ah64EzI8/KqicpqHYYeOW6n6EIx4muAWbY/04pz053i4x7K047MQfK3ItAgWr+aARl/I6BBVHrDg290w=="], + "@tscircuit/circuit-json-schematic-placement-analysis": ["@tscircuit/circuit-json-schematic-placement-analysis@github:tscircuit/circuit-json-schematic-placement-analysis#700017d", { "dependencies": { "@tscircuit/circuit-json-util": "^0.0.94" }, "peerDependencies": { "circuit-json": "*", "typescript": "^5" } }, "tscircuit-circuit-json-schematic-placement-analysis-700017d"], "@tscircuit/circuit-json-util": ["@tscircuit/circuit-json-util@0.0.94", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "circuit-json": "*", "transformation-matrix": "*", "zod": "3" } }, "sha512-kEYV6LzcZbRuw43IxsZ1cZL2pUx4nF07MYAHHhY9s90UzKYaIYfZ1q11s+F2wNwKecCcSyTUoAwWeqazLQEyVQ=="], @@ -420,6 +421,8 @@ "@tscircuit/soup-util": ["@tscircuit/soup-util@0.0.41", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "circuit-json": "*", "transformation-matrix": "*", "zod": "*" } }, "sha512-47JKWBUKkRVHhad0HhBbdOJxB6v/eiac46beiKRBMlJqiZ1gPGI276v9iZfpF7c4hXR69cURcgiwuA5vowrFEg=="], + "@tscircuit/ti-parts-engine": ["@tscircuit/ti-parts-engine@github:tscircuit/ti-parts-engine#69b565a", { "dependencies": { "jszip": "^3.10.1" }, "peerDependencies": { "typescript": "^5" } }, "tscircuit-ti-parts-engine-69b565a"], + "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], @@ -558,7 +561,7 @@ "circuit-json-to-tscircuit": ["circuit-json-to-tscircuit@0.0.9", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-2B4E3kOU9zFbJ6SyCKcp9ktlay/Xf2gbLuGcWE8rBL3uuypJU3uX4MFjHVfwx8cbvB/0LTF5v3gHTYbxpiZMOg=="], - "circuit-json-trace-length-analysis": ["circuit-json-trace-length-analysis@github:tscircuit/circuit-json-trace-length-analysis#2b44792", { "peerDependencies": { "typescript": "^5" } }, "tscircuit-circuit-json-trace-length-analysis-2b44792", "sha512-CTFqTc+F66tflCKmXC+Ge7kD1K2rrEH4Z5vHhUJa0OxmtKh6L1gM80xCJL1YtAL+9f2p7i26U9fO+Pq22NEypQ=="], + "circuit-json-trace-length-analysis": ["circuit-json-trace-length-analysis@github:tscircuit/circuit-json-trace-length-analysis#2b44792", { "peerDependencies": { "typescript": "^5" } }, "tscircuit-circuit-json-trace-length-analysis-2b44792"], "circuit-to-svg": ["circuit-to-svg@0.0.345", "", { "dependencies": { "@types/node": "^22.5.5", "bun-types": "^1.1.40", "calculate-elbow": "0.0.12", "debug": "^4.4.3", "svg-path-commander": "^2.1.11", "svgson": "^5.3.1", "transformation-matrix": "^2.16.1" }, "peerDependencies": { "@tscircuit/alphabet": "*" } }, "sha512-d+P+AFJhWlt9Bdpk9/0zdBBjPxIRgnJaFsGqW/4CG0vEAY2QNqK/OqSl8i0zpFpM4+tiQdeR0n8h1tsvMMhvkA=="], diff --git a/package.json b/package.json index cfbd9a1c2..9715a6a35 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@tscircuit/props": "^0.0.536", "@tscircuit/runframe": "^0.0.2027", "@tscircuit/schematic-match-adapt": "^0.0.22", + "@tscircuit/ti-parts-engine": "github:tscircuit/ti-parts-engine", "@types/bun": "^1.2.2", "@types/configstore": "^6.0.2", "@types/debug": "^4.1.12",