Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
e1dcdb695c fix(bundler): enable tsconfig option in Bun.build() API
The `tsconfig` option in `Bun.build()` was not working - path aliases
defined in a custom tsconfig were not resolved. This fixes the issue by:

1. Parsing the `tsconfig` property from the JavaScript config object
2. Passing `tsconfig_override` to the TransformOptions during Transpiler init
3. Explicitly loading and attaching the tsconfig to the directory cache

The root cause was that the bundler's resolver shares a global directory
cache with the main process. When a build script loads, the main process
populates the cache without the tsconfig_override, and subsequent bundler
operations would use the cached entries that lack tsconfig information.

The fix ensures the tsconfig is loaded and attached to both the tsconfig
file's directory and any entry point directories that may have been
cached before the tsconfig was loaded.

Fixes #26793

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-07 19:50:22 +00:00
3 changed files with 153 additions and 2 deletions

View File

@@ -577,6 +577,23 @@ pub const JSBundler = struct {
try this.footer.appendSliceExact(slice.slice());
}
if (try config.getOptional(globalThis, "tsconfig", ZigString.Slice)) |slice| {
defer slice.deinit();
const tsconfig_path = slice.slice();
// Normalize relative tsconfig path to absolute
if (std.fs.path.isAbsolute(tsconfig_path)) {
try this.tsconfig_override.appendSliceExact(tsconfig_path);
} else {
var abs_buf: bun.PathBuffer = undefined;
const cwd = bun.getcwd(&abs_buf) catch {
return globalThis.throwPretty("failed to get current working directory for tsconfig path", .{});
};
var path_buf: bun.PathBuffer = undefined;
const abs_path = bun.path.joinAbsStringBuf(cwd, &path_buf, &.{tsconfig_path}, .auto);
try this.tsconfig_override.appendSliceExact(abs_path);
}
}
if (try config.getTruthy(globalThis, "sourcemap")) |source_map_js| {
if (source_map_js.isBoolean()) {
if (source_map_js == .true) {

View File

@@ -1921,6 +1921,10 @@ pub const BundleV2 = struct {
.drop = config.drop.map.keys(),
.bunfig_path = transpiler.options.bunfig_path,
.jsx = jsx_api,
.tsconfig_override = if (config.tsconfig_override.slice().len > 0)
config.tsconfig_override.slice()
else
null,
},
completion.env,
);
@@ -2004,14 +2008,56 @@ pub const BundleV2 = struct {
transpiler.options.emit_dce_annotations = false;
}
// Update resolver options before configureLinker, since configureLinker
// may read directory info (e.g., for auto-detecting JSX settings from tsconfig)
// and those reads cache results that depend on tsconfig_override being set.
transpiler.resolver.opts = transpiler.options;
transpiler.resolver.env_loader = transpiler.env;
// If tsconfig_override is set, we need to explicitly load it and attach it to the
// directory where the tsconfig file is located and to any entry point directories.
// This is necessary because the directory cache may have been populated by the main
// process (e.g., when loading the build script) without the tsconfig_override, and
// the bundler's resolver shares that global cache.
// Note: tsconfig_path is already normalized to absolute in JSBundler.zig
if (transpiler.options.tsconfig_override) |tsconfig_path| {
// Load the tsconfig
const tsconfig = transpiler.resolver.parseTSConfig(tsconfig_path, bun.invalid_fd) catch null;
if (tsconfig) |ts| {
// Get the directory where the tsconfig file is located
const tsconfig_dir = std.fs.path.dirname(tsconfig_path) orelse transpiler.fs.top_level_dir;
// Set the tsconfig on its containing directory, unconditionally overwriting
// any cached values to ensure the override takes effect
if (transpiler.resolver.readDirInfo(tsconfig_dir) catch null) |tsconfig_dir_info| {
tsconfig_dir_info.tsconfig_json = ts;
tsconfig_dir_info.enclosing_tsconfig_json = ts;
}
// Also update enclosing_tsconfig_json for any entry point directories that may have been
// cached before the tsconfig was loaded
var abs_path_buf: bun.PathBuffer = undefined;
for (config.entry_points.keys()) |entry_point| {
// entry_point might be relative, resolve it
const abs_entry = if (std.fs.path.isAbsolute(entry_point))
entry_point
else
transpiler.fs.absBuf(&.{ transpiler.fs.top_level_dir, entry_point }, &abs_path_buf);
const entry_dir = std.fs.path.dirname(abs_entry) orelse continue;
if (transpiler.resolver.readDirInfo(entry_dir) catch null) |dir_info| {
dir_info.enclosing_tsconfig_json = ts;
}
}
}
}
transpiler.configureLinker();
try transpiler.configureDefines();
if (!transpiler.options.production) {
try transpiler.options.conditions.appendSlice(&.{"development"});
}
transpiler.resolver.env_loader = transpiler.env;
transpiler.resolver.opts = transpiler.options;
}
pub fn completeOnBundleThread(completion: *JSBundleCompletionTask) void {

View File

@@ -0,0 +1,88 @@
import { expect, test } from "bun:test";
import { tempDir } from "harness";
// Regression test for https://github.com/oven-sh/bun/issues/26793
// Bun.build() API tsconfig option does not work - path aliases are not resolved
test("Bun.build() tsconfig option should resolve path aliases", async () => {
using dir = tempDir("issue-26793", {
"src/index.ts": `import { sum } from "@/utils";\nexport { sum };\n`,
"src/utils.ts": `export function sum(a: number, b: number) { return a + b; }\n`,
"tsconfig.custom.json": JSON.stringify({
compilerOptions: {
baseUrl: ".",
paths: {
"@/*": ["./src/*"],
},
},
}),
});
const result = await Bun.build({
entrypoints: [`${dir}/src/index.ts`],
outdir: `${dir}/dist`,
tsconfig: `${dir}/tsconfig.custom.json`,
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
const output = await result.outputs[0].text();
// The bundled output should contain the sum function
expect(output).toContain("sum");
});
test("Bun.build() tsconfig option should work with relative path in tsconfig", async () => {
using dir = tempDir("issue-26793-relative", {
"src/index.ts": `import { multiply } from "@lib/math";\nexport { multiply };\n`,
"lib/math.ts": `export function multiply(a: number, b: number) { return a * b; }\n`,
"tsconfig.json": JSON.stringify({
compilerOptions: {
baseUrl: ".",
paths: {
"@lib/*": ["./lib/*"],
},
},
}),
});
// Test that tsconfig with relative paths inside it (baseUrl, paths) works correctly
const result = await Bun.build({
entrypoints: [`${dir}/src/index.ts`],
outdir: `${dir}/dist`,
tsconfig: `${dir}/tsconfig.json`,
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
const output = await result.outputs[0].text();
expect(output).toContain("multiply");
});
test("Bun.build() without tsconfig option should not resolve custom aliases", async () => {
using dir = tempDir("issue-26793-no-tsconfig", {
"src/index.ts": `import { divide } from "@custom/math";\nexport { divide };\n`,
"custom/math.ts": `export function divide(a: number, b: number) { return a / b; }\n`,
// No tsconfig at root, custom tsconfig is not passed
"other/tsconfig.json": JSON.stringify({
compilerOptions: {
baseUrl: ".",
paths: {
"@custom/*": ["./custom/*"],
},
},
}),
});
const result = await Bun.build({
entrypoints: [`${dir}/src/index.ts`],
outdir: `${dir}/dist`,
// No tsconfig option - should fail to resolve the alias
throw: false, // Don't throw, just return success=false
});
// Without the tsconfig option, the path alias should not be resolved
expect(result.success).toBe(false);
expect(result.logs.some(log => log.message?.includes("@custom/math"))).toBe(true);
});