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) +})