diff --git a/cli/dev/resolve-dev-target.ts b/cli/dev/resolve-dev-target.ts index abdcaf236..13c3ad947 100644 --- a/cli/dev/resolve-dev-target.ts +++ b/cli/dev/resolve-dev-target.ts @@ -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" @@ -81,6 +82,7 @@ export const resolveDevTarget = async ( return null } + projectDir = resolveProjectDirFromInputPath(resolvedPath) return { absolutePath: resolvedPath, projectDir } } diff --git a/cli/export/register.ts b/cli/export/register.ts index 1a3d79b2d..0f752c665 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 { + loadRuntimeProjectConfig, + resolveProjectDirFromInputPath, +} from "lib/project-config" import { mergePlatformConfigs } from "lib/shared/platform-config-utils" export const registerExport = (program: Command) => { @@ -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 diff --git a/cli/simulate/register.ts b/cli/simulate/register.ts index 24c5c84e9..745f3ca23 100644 --- a/cli/simulate/register.ts +++ b/cli/simulate/register.ts @@ -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) => { @@ -18,7 +21,8 @@ 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 projectDir = resolveProjectDirFromInputPath(file) + const projectConfig = await loadRuntimeProjectConfig(projectDir) const commandPlatformConfig: PlatformConfig | undefined = options.disablePartsEngine === true ? { partsEngineDisabled: true } diff --git a/lib/dev/DevServer.ts b/lib/dev/DevServer.ts index abf08bb48..c64711088 100644 --- a/lib/dev/DevServer.ts +++ b/lib/dev/DevServer.ts @@ -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" @@ -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 @@ -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( @@ -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( diff --git a/lib/project-config/index.ts b/lib/project-config/index.ts index cf90c85c5..b92088d06 100644 --- a/lib/project-config/index.ts +++ b/lib/project-config/index.ts @@ -123,27 +123,70 @@ const loadProjectConfigModule = async ( ): Promise => { 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 => { diff --git a/tests/cli/dev/dev-server-runtime-config-module-deps.test.ts b/tests/cli/dev/dev-server-runtime-config-module-deps.test.ts new file mode 100644 index 000000000..33cd0838f --- /dev/null +++ b/tests/cli/dev/dev-server-runtime-config-module-deps.test.ts @@ -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 () => ( + + + +) +`, + ) + + 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) diff --git a/tests/cli/dev/resolve-dev-target-project-dir.test.ts b/tests/cli/dev/resolve-dev-target-project-dir.test.ts new file mode 100644 index 000000000..868a9b209 --- /dev/null +++ b/tests/cli/dev/resolve-dev-target-project-dir.test.ts @@ -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 () => \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) + } +}) diff --git a/tests/cli/runtime-project-config-command-flows.test.ts b/tests/cli/runtime-project-config-command-flows.test.ts index 1255cedf7..2e0ee0da1 100644 --- a/tests/cli/runtime-project-config-command-flows.test.ts +++ b/tests/cli/runtime-project-config-command-flows.test.ts @@ -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)