From f40bf045fd5adc39f7fe18e225d00529bbcb2a0f Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Mon, 12 Jan 2026 20:10:46 +0000 Subject: [PATCH] fix(resolver): child tsconfig paths should override parent paths, not merge Per TypeScript semantics, when a child tsconfig.json defines its own `paths` mapping, it should completely replace the parent's `paths` rather than merging them together. Fixes #25622 Co-Authored-By: Claude Opus 4.5 --- src/resolver/resolver.zig | 8 +-- test/regression/issue/25622.test.ts | 77 +++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 test/regression/issue/25622.test.ts diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index ec0ae1dc10..e531bb93d3 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -4235,9 +4235,11 @@ pub const Resolver = struct { merged_config.preserve_imports_not_used_as_values = value; } - var iter = parent_config.paths.iterator(); - while (iter.next()) |c| { - merged_config.paths.put(c.key_ptr.*, c.value_ptr.*) catch unreachable; + // TypeScript's behavior: if a child config defines `paths`, it completely + // overrides the parent's `paths` rather than merging. + if (parent_config.paths.count() > 0) { + merged_config.paths = parent_config.paths; + merged_config.base_url_for_paths = parent_config.base_url_for_paths; } // todo deinit these parent configs somehow? } diff --git a/test/regression/issue/25622.test.ts b/test/regression/issue/25622.test.ts new file mode 100644 index 0000000000..1f40f32f0d --- /dev/null +++ b/test/regression/issue/25622.test.ts @@ -0,0 +1,77 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +// https://github.com/oven-sh/bun/issues/25622 +// TypeScript's behavior: child tsconfig `paths` should completely override parent's `paths`, +// not merge with them. +test("child tsconfig paths should override parent paths, not merge", async () => { + using dir = tempDir("issue-25622", { + "tsconfig.base.json": JSON.stringify({ + compilerOptions: { + paths: { + "@helpers/*": ["./src/helpers/*"], + }, + }, + }), + "tsconfig.json": JSON.stringify({ + extends: "./tsconfig.base.json", + compilerOptions: { + paths: { + "@/*": ["./src/*"], + }, + }, + }), + "src/helpers/x.ts": `export const x = "from helpers";`, + "src/index.ts": `import "@helpers/x";`, + }); + + // This should fail because child's paths should override parent's paths + // (the @helpers/* mapping from the parent should not be present) + await using proc = Bun.spawn({ + cmd: [bunExe(), "src/index.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Should fail with a resolution error since @helpers/* should not be mapped + expect(stderr).toContain("@helpers/x"); + expect(exitCode).not.toBe(0); +}); + +test("child tsconfig inherits parent paths when child has no paths", async () => { + using dir = tempDir("issue-25622-inherit", { + "tsconfig.base.json": JSON.stringify({ + compilerOptions: { + paths: { + "@helpers/*": ["./src/helpers/*"], + }, + }, + }), + "tsconfig.json": JSON.stringify({ + extends: "./tsconfig.base.json", + compilerOptions: { + // No paths defined - should inherit from parent + }, + }), + "src/helpers/x.ts": `console.log("inherited path works");`, + "src/index.ts": `import "@helpers/x";`, + }); + + // This should succeed because child inherits parent's paths + await using proc = Bun.spawn({ + cmd: [bunExe(), "src/index.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toContain("inherited path works"); + expect(exitCode).toBe(0); +});