Skip to content
Open
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
2 changes: 2 additions & 0 deletions cli/dev/resolve-dev-target.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as fs from "node:fs"
import * as path from "node:path"
import { globbySync } from "globby"
import { resolveProjectDirFromInputPath } from "lib/project-config"
import { findBoardFiles } from "lib/shared/find-board-files"
import { getEntrypoint } from "lib/shared/get-entrypoint"
import { DEFAULT_IGNORED_PATTERNS } from "lib/shared/should-ignore-path"
Expand Down Expand Up @@ -81,6 +82,7 @@ export const resolveDevTarget = async (
return null
}

projectDir = resolveProjectDirFromInputPath(resolvedPath)
return { absolutePath: resolvedPath, projectDir }
}

Expand Down
8 changes: 6 additions & 2 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 {
loadRuntimeProjectConfig,
resolveProjectDirFromInputPath,
} from "lib/project-config"
import { mergePlatformConfigs } from "lib/shared/platform-config-utils"

export const registerExport = (program: Command) => {
Expand All @@ -35,7 +38,8 @@ export const registerExport = (program: Command) => {
},
) => {
const formatOption = options.format ?? "json"
const projectConfig = await loadRuntimeProjectConfig(process.cwd())
const projectDir = resolveProjectDirFromInputPath(file)
const projectConfig = await loadRuntimeProjectConfig(projectDir)

const commandPlatformConfig: PlatformConfig | undefined =
options.disablePartsEngine === true
Expand Down
8 changes: 6 additions & 2 deletions cli/simulate/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ 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 {
loadRuntimeProjectConfig,
resolveProjectDirFromInputPath,
} from "lib/project-config"
import { mergePlatformConfigs } from "lib/shared/platform-config-utils"

export const registerSimulate = (program: Command) => {
Expand All @@ -18,7 +21,8 @@ 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 projectDir = resolveProjectDirFromInputPath(file)
const projectConfig = await loadRuntimeProjectConfig(projectDir)
const commandPlatformConfig: PlatformConfig | undefined =
options.disablePartsEngine === true
? { partsEngineDisabled: true }
Expand Down
21 changes: 17 additions & 4 deletions lib/dev/DevServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import type {
FileUpdatedEvent,
} from "lib/file-server/FileServerEvent"
import type { FileServerRoutes } from "lib/file-server/FileServerRoutes"
import { loadProjectConfig } from "lib/project-config"
import {
findRuntimeProjectConfigModulePath,
loadProjectConfig,
} from "lib/project-config"
import { EventsWatcher } from "lib/server/EventsWatcher"
import { createHttpServer } from "lib/server/createHttpServer"
import { addPackage } from "lib/shared/add-package"
Expand Down Expand Up @@ -43,6 +46,7 @@ export class DevServer {
projectDir: string
/** Paths or directory names to ignore when syncing files */
ignoredFiles: string[]
runtimeConfigModulePath?: string

/** Whether to enable the KiCad PCM proxy server */
kicadPcm: boolean
Expand Down Expand Up @@ -94,6 +98,9 @@ export class DevServer {
}

async start() {
this.runtimeConfigModulePath =
findRuntimeProjectConfigModulePath(this.projectDir) ?? undefined

const { server } = await createHttpServer({
port: this.port,
defaultMainComponentPath: path.relative(
Expand Down Expand Up @@ -367,9 +374,15 @@ export class DevServer {
private async uploadInitialNodeModules() {
try {
console.log(kleur.blue("Analyzing node_modules dependencies..."))
const nodeModuleFiles = getAllNodeModuleFilePaths(
this.componentFilePath,
this.projectDir,
const nodeModuleFiles = Array.from(
new Set(
[this.componentFilePath, this.runtimeConfigModulePath].flatMap(
(filePath) =>
filePath
? getAllNodeModuleFilePaths(filePath, this.projectDir)
: [],
),
),
)

console.log(
Expand Down
69 changes: 56 additions & 13 deletions lib/project-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,27 +123,70 @@ const loadProjectConfigModule = async (
): Promise<TscircuitRuntimeProjectConfig | null> => {
loadProjectEnv(projectDir)

const configPath = findRuntimeProjectConfigModulePath(projectDir)
if (!configPath) {
return null
}

try {
const moduleUrl = pathToFileURL(configPath)
const stat = fs.statSync(configPath)
moduleUrl.searchParams.set("tsci", String(stat.mtimeMs))
const importedModule = await import(moduleUrl.href)
const exportedConfig =
importedModule.default ?? importedModule.config ?? importedModule
return parseProjectConfigObject(exportedConfig)
} catch (error) {
console.error(`Error loading ${path.basename(configPath)}: ${error}`)
return null
}
}

export const findRuntimeProjectConfigModulePath = (
projectDir: string = process.cwd(),
): string | null => {
for (const configFileName of CONFIG_MODULE_FILENAMES) {
const configPath = path.join(projectDir, configFileName)
if (!fs.existsSync(configPath)) continue

try {
const moduleUrl = pathToFileURL(configPath)
const stat = fs.statSync(configPath)
moduleUrl.searchParams.set("tsci", String(stat.mtimeMs))
const importedModule = await import(moduleUrl.href)
const exportedConfig =
importedModule.default ?? importedModule.config ?? importedModule
return parseProjectConfigObject(exportedConfig)
} catch (error) {
console.error(`Error loading ${configFileName}: ${error}`)
return null
if (fs.existsSync(configPath)) {
return configPath
}
}

return null
}

export const resolveProjectDirFromInputPath = (
inputPath: string,
cwd: string = process.cwd(),
): string => {
const resolvedPath = path.isAbsolute(inputPath)
? inputPath
: path.resolve(cwd, inputPath)

const fallbackDir =
fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()
? resolvedPath
: path.dirname(resolvedPath)

let currentDir = fallbackDir

while (true) {
if (
findRuntimeProjectConfigModulePath(currentDir) ||
fs.existsSync(path.join(currentDir, CONFIG_FILENAME)) ||
fs.existsSync(path.join(currentDir, "package.json"))
) {
return currentDir
}

const parentDir = path.dirname(currentDir)
if (parentDir === currentDir) {
return fallbackDir
}
currentDir = parentDir
}
}

export const loadRuntimeProjectConfig = async (
projectDir: string = process.cwd(),
): Promise<TscircuitRuntimeProjectConfig | null> => {
Expand Down
125 changes: 125 additions & 0 deletions tests/cli/dev/dev-server-runtime-config-module-deps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { expect, test } from "bun:test"
import { DevServer } from "cli/dev/DevServer"
import getPort from "get-port"
import { mkdir, writeFile } from "node:fs/promises"
import { join } from "node:path"
import { getCliTestFixture } from "../../fixtures/get-cli-test-fixture"

test("dev server uploads node_modules dependencies imported by tscircuit.config.ts", async () => {
const fixture = await getCliTestFixture()
const projectDir = fixture.tmpDir

await writeFile(
join(projectDir, "index.circuit.tsx"),
`
export default () => (
<board width="10mm" height="10mm">
<resistor name="R1" resistance="10k" />
</board>
)
`,
)

await writeFile(
join(projectDir, "tscircuit.config.ts"),
`
import { createTiPlatformConfig } from "@tscircuit/ti-parts-engine"

export default {
platformConfig: createTiPlatformConfig({
partnerToken: "secret-token",
}),
}
`,
)

await writeFile(
join(projectDir, "package.json"),
JSON.stringify(
{
name: "test-project",
version: "1.0.0",
dependencies: {
"@tscircuit/ti-parts-engine": "1.0.0",
},
},
null,
2,
),
)

const pkgDir = join(
projectDir,
"node_modules",
"@tscircuit",
"ti-parts-engine",
)
const libDir = join(pkgDir, "lib", "ti-parts-engine")
await mkdir(libDir, { recursive: true })

await writeFile(
join(pkgDir, "package.json"),
JSON.stringify(
{
name: "@tscircuit/ti-parts-engine",
version: "1.0.0",
main: "./index.ts",
module: "./index.ts",
exports: {
".": {
import: "./index.ts",
types: "./index.ts",
},
},
},
null,
2,
),
)

await writeFile(
join(pkgDir, "index.ts"),
`
export { createTiPlatformConfig } from "./lib/ti-parts-engine/createTiPlatformConfig"
`,
)

await writeFile(
join(libDir, "createTiPlatformConfig.ts"),
`
export const createTiPlatformConfig = (options: { partnerToken: string }) => ({
footprintLibraryMap: {
ti: async () => ({
footprintCircuitJson: [],
partnerToken: options.partnerToken,
}),
},
})
`,
)

const devServer = new DevServer({
port: await getPort(),
componentFilePath: join(projectDir, "index.circuit.tsx"),
})

try {
await devServer.start()

const { file_list } = (await devServer.fsKy
.get("api/files/list")
.json()) as { file_list: Array<{ file_path: string }> }

const filePaths = file_list.map((f) => f.file_path)

expect(filePaths).toContain("tscircuit.config.ts")
expect(filePaths).toContain(
"node_modules/@tscircuit/ti-parts-engine/index.ts",
)
expect(filePaths).toContain(
"node_modules/@tscircuit/ti-parts-engine/lib/ti-parts-engine/createTiPlatformConfig.ts",
)
} finally {
await devServer.stop()
}
}, 30_000)
37 changes: 37 additions & 0 deletions tests/cli/dev/resolve-dev-target-project-dir.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, test } from "bun:test"
import { mkdir, writeFile } from "node:fs/promises"
import { join } from "node:path"
import { resolveDevTarget } from "cli/dev/resolve-dev-target"
import { getCliTestFixture } from "../../fixtures/get-cli-test-fixture"

test("resolveDevTarget uses the input file project directory", async () => {
const { tmpDir } = await getCliTestFixture()
const nestedProjectDir = join(tmpDir, "nested-project")
const circuitPath = join(nestedProjectDir, "index.circuit.tsx")

await mkdir(nestedProjectDir, { recursive: true })
await writeFile(
join(nestedProjectDir, "package.json"),
JSON.stringify({ name: "nested-project" }),
)
await writeFile(
join(nestedProjectDir, "tscircuit.config.ts"),
"export default { includeBoardFiles: ['**/*.circuit.tsx'] }\n",
)
await writeFile(
circuitPath,
'export default () => <board width="10mm" height="10mm" />\n',
)

const originalCwd = process.cwd()
process.chdir(tmpDir)

try {
const resolved = await resolveDevTarget(circuitPath)
expect(resolved).not.toBeNull()
expect(resolved?.absolutePath).toBe(circuitPath)
expect(resolved?.projectDir).toBe(nestedProjectDir)
} finally {
process.chdir(originalCwd)
}
})
35 changes: 35 additions & 0 deletions tests/cli/runtime-project-config-command-flows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,38 @@ test("simulate analog consumes runtime platformConfig from tscircuit.config.ts",
expect(stderr).toContain("source_port_id")
expect(stdout).toContain("Index time")
}, 30_000)

test("export resolves runtime project config from the input file project directory", async () => {
const { tmpDir, runCommand } = await getCliTestFixture()
const nestedProjectDir = join(tmpDir, "nested-project")
const circuitPath = join(nestedProjectDir, "index.circuit.tsx")

await mkdir(nestedProjectDir, { recursive: true })
await writeFile(
join(nestedProjectDir, "package.json"),
JSON.stringify({ name: "nested-project" }),
)
await writeFile(circuitPath, tiBoardCircuitCode)
await writeFile(
join(nestedProjectDir, "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(nestedProjectDir, "index.circuit.circuit.json"),
"utf-8",
),
)

expect(
circuitJson.some((element: any) => element.type === "pcb_smtpad"),
).toBe(true)
}, 30_000)
Loading