Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 13 additions & 6 deletions cli/export/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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
Expand All @@ -48,7 +55,7 @@ export const registerExport = (program: Command) => {

if (formatOption === "spice") {
const { circuitJson } = await generateCircuitJson({
filePath: file,
filePath: resolvedFilePath,
platformConfig,
})
if (circuitJson) {
Expand All @@ -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)
Expand All @@ -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,
Expand Down
14 changes: 11 additions & 3 deletions cli/simulate/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,7 +22,11 @@ export const registerSimulate = (program: Command) => {
.argument("<file>", "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 }
Expand All @@ -29,7 +37,7 @@ export const registerSimulate = (program: Command) => {
)

const { circuitJson } = await generateCircuitJson({
filePath: file,
filePath: resolvedFilePath,
saveToFile: false,
platformConfig,
})
Expand Down
33 changes: 33 additions & 0 deletions lib/project-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 => {
Expand Down
5 changes: 3 additions & 2 deletions lib/shared/convert-to-kicad-library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } =
Expand Down
5 changes: 4 additions & 1 deletion lib/shared/export-snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 7 additions & 6 deletions lib/shared/generate-circuit-json.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 15 additions & 14 deletions lib/shared/importFromUserLand.ts
Original file line number Diff line number Diff line change
@@ -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
}
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 28 additions & 0 deletions tests/cli/runtime-project-config-command-flows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@
`tsci export ${circuitPath} -f circuit-json`,
)

expect(exitCode).toBe(0)

Check failure on line 233 in tests/cli/runtime-project-config-command-flows.test.ts

View workflow job for this annotation

GitHub Actions / test (4)

error: expect(received).toBe(expected)

Expected: 0 Received: 1 at <anonymous> (/home/runner/work/cli/cli/tests/cli/runtime-project-config-command-flows.test.ts:233:20)
expect(stderr).toBe("")

const circuitJson = JSON.parse(
Expand All @@ -242,6 +242,34 @@
).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)

Check failure on line 261 in tests/cli/runtime-project-config-command-flows.test.ts

View workflow job for this annotation

GitHub Actions / test (4)

error: expect(received).toBe(expected)

Expected: 0 Received: 1 at <anonymous> (/home/runner/work/cli/cli/tests/cli/runtime-project-config-command-flows.test.ts:261:20)
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")
Expand All @@ -256,7 +284,7 @@
`tsci simulate analog ${circuitPath}`,
)

expect(exitCode).toBe(0)

Check failure on line 287 in tests/cli/runtime-project-config-command-flows.test.ts

View workflow job for this annotation

GitHub Actions / test (4)

error: expect(received).toBe(expected)

Expected: 0 Received: 1 at <anonymous> (/home/runner/work/cli/cli/tests/cli/runtime-project-config-command-flows.test.ts:287:20)
expect(stderr).toContain("source_port_id")
expect(stdout).toContain("Index time")
}, 30_000)
Loading