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 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-12 20:10:46 +00:00
parent beccd01647
commit f40bf045fd
2 changed files with 82 additions and 3 deletions

View File

@@ -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?
}

View File

@@ -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);
});