diff --git a/lib/runner/tsconfigPaths.ts b/lib/runner/tsconfigPaths.ts
index 80c9d88e..92e48999 100644
--- a/lib/runner/tsconfigPaths.ts
+++ b/lib/runner/tsconfigPaths.ts
@@ -1,4 +1,5 @@
import { normalizeFilePath } from "./normalizeFsMap"
+import { joinPath } from "../utils/pathJoin"
type RawTsConfig = {
compilerOptions?: {
@@ -40,6 +41,13 @@ export function resolveWithTsconfigPaths(opts: {
const { importPath, normalizedFilePathMap, extensions, tsConfig } = opts
if (!tsConfig) return null
const { baseUrl, paths } = tsConfig
+ const normalizedBaseUrl = baseUrl ? normalizeFilePath(baseUrl) : ""
+ const effectiveBaseUrl = normalizedBaseUrl === "." ? "" : normalizedBaseUrl
+
+ const resolveTargetWithBaseUrl = (target: string) => {
+ if (!baseUrl || target.startsWith("/")) return target
+ return joinPath(effectiveBaseUrl, target)
+ }
const tryResolveCandidate = (candidate: string) => {
const normalizedCandidate = normalizeFilePath(candidate)
@@ -72,20 +80,14 @@ export function resolveWithTsconfigPaths(opts: {
)
for (const target of targets) {
const replaced = target.replace("*", starMatch)
- const candidate =
- baseUrl && !replaced.startsWith("./") && !replaced.startsWith("/")
- ? `${baseUrl}/${replaced}`
- : replaced
+ const candidate = resolveTargetWithBaseUrl(replaced)
const resolved = tryResolveCandidate(candidate)
if (resolved) return resolved
}
} else {
if (importPath !== alias) continue
for (const target of targets) {
- const candidate =
- baseUrl && !target.startsWith("./") && !target.startsWith("/")
- ? `${baseUrl}/${target}`
- : target
+ const candidate = resolveTargetWithBaseUrl(target)
const resolved = tryResolveCandidate(candidate)
if (resolved) return resolved
}
diff --git a/lib/utils/pathJoin.ts b/lib/utils/pathJoin.ts
new file mode 100644
index 00000000..6316bc30
--- /dev/null
+++ b/lib/utils/pathJoin.ts
@@ -0,0 +1,38 @@
+export function joinPath(...parts: string[]): string {
+ const segments: string[] = []
+ let isAbsolute = false
+
+ for (const part of parts) {
+ if (!part) continue
+ const normalized = part.replace(/\\/g, "/")
+ if (!normalized) continue
+
+ if (normalized.startsWith("/")) {
+ segments.length = 0
+ isAbsolute = true
+ }
+
+ for (const segment of normalized.split("/")) {
+ if (!segment || segment === ".") {
+ continue
+ }
+ if (segment === "..") {
+ if (segments.length && segments[segments.length - 1] !== "..") {
+ segments.pop()
+ continue
+ }
+ if (!isAbsolute) {
+ segments.push("..")
+ }
+ continue
+ }
+ segments.push(segment)
+ }
+ }
+
+ const joined = segments.join("/")
+ if (isAbsolute) {
+ return joined ? `/${joined}` : "/"
+ }
+ return joined
+}
diff --git a/tests/features/tsconfig-paths-resolution.test.tsx b/tests/features/tsconfig-paths-resolution.test.tsx
index 725dc89b..89e7ba18 100644
--- a/tests/features/tsconfig-paths-resolution.test.tsx
+++ b/tests/features/tsconfig-paths-resolution.test.tsx
@@ -63,3 +63,34 @@ test("throws error when tsconfig path alias cannot be resolved (instead of tryin
'Import "@utils/missing" matches a tsconfig path alias but could not be resolved to an existing file',
)
})
+
+test("tsconfig paths honor baseUrl when targets use relative prefixes", async () => {
+ const circuitJson = await runTscircuitCode(
+ {
+ "tsconfig.json": JSON.stringify({
+ compilerOptions: {
+ baseUrl: "./src",
+ paths: {
+ "@components/*": ["./components/*"],
+ },
+ },
+ }),
+ "src/components/res.tsx": `
+ export default () => ()
+ `,
+ "user.tsx": `
+ import Resistor from "@components/res"
+ export default () => ()
+ `,
+ },
+ {
+ mainComponentPath: "user",
+ },
+ )
+
+ const resistor = circuitJson.find(
+ (el) => el.type === "source_component" && el.name === "Rbase",
+ ) as any
+ expect(resistor).toBeDefined()
+ expect(resistor.resistance).toBe(1000)
+})