Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/bun.js/api/JSBundler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
50 changes: 48 additions & 2 deletions src/bundler/bundle_v2.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down Expand Up @@ -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 {
Expand Down
88 changes: 88 additions & 0 deletions test/regression/issue/26793.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});