Compare commits

...

2 Commits

Author SHA1 Message Date
Claude
6e8a9996b1 fix: set dest_path for --no-bundle with --outdir
When using `bun build --no-bundle --outdir`, the output file's dest_path
was not being set, causing ENOENT error when trying to write to an empty
filename.

This fix computes dest_path by taking the input file's basename and
replacing the extension with the appropriate output extension (e.g.,
.ts -> .js).

Fixes #10370

https://claude.ai/code/session_01WTzMmxa1RL14NCnqw1scjn
2026-02-11 14:22:01 +00:00
Claude Bot
3a955bc824 fix(types): correct ArrayBuffer.resize return type and Promise.withResolvers resolve parameter
`ArrayBuffer.resize()` was typed as returning `ArrayBuffer` but per the
ECMAScript spec it returns `undefined` (`void` in TS). The parameter was
also made optional to match TypeScript's `lib.es2024.arraybuffer.d.ts`.

`Promise.withResolvers().resolve` had its `value` parameter marked as
optional, but it should be required per TypeScript's
`lib.es2024.promise.d.ts`. The optional parameter caused type
incompatibility errors with libraries like core-js that also declare
these types.

Adds a compatibility test that fetches type definitions from the upstream
core-js v4-types branch at runtime to catch future regressions.

Closes #26868

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-10 20:14:37 +00:00
5 changed files with 195 additions and 2 deletions

View File

@@ -1060,7 +1060,7 @@ interface ArrayBuffer {
/**
* Resize an ArrayBuffer in-place.
*/
resize(byteLength: number): ArrayBuffer;
resize(newByteLength?: number): void;
/**
* Returns a section of an ArrayBuffer.
@@ -1405,7 +1405,7 @@ interface PromiseConstructor {
*/
withResolvers<T>(): {
promise: Promise<T>;
resolve: (value?: T | PromiseLike<T>) => void;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
};

View File

@@ -616,6 +616,9 @@ pub const Transpiler = struct {
file_path.pretty = Linker.relative_paths_list.append(string, transpiler.fs.relativeTo(file_path.text)) catch unreachable;
const out_ext = transpiler.options.out_extensions.get(file_path.name.ext) orelse file_path.name.ext;
const dest_path = std.fmt.allocPrint(transpiler.allocator, "{s}{s}", .{ file_path.name.base, out_ext }) catch unreachable;
var output_file = options.OutputFile{
.src_path = file_path,
.loader = loader,
@@ -623,6 +626,7 @@ pub const Transpiler = struct {
.side = null,
.entry_point_index = null,
.output_kind = .chunk,
.dest_path = dest_path,
};
switch (loader) {

View File

@@ -512,6 +512,124 @@ describe("@types/bun integration test", () => {
});
});
describe("core-js type compatibility", () => {
// Tests that bun-types are compatible with core-js type patterns.
// core-js defines ponyfill constructors that extend global built-in
// interfaces, and these will fail with TS2430 if bun-types deviates
// from the spec signatures.
typeTest("bun-types signatures are compatible with core-js extends pattern", {
files: {
"core-js-extends-check.ts": `
// The resolve parameter must be non-optional (required) per the spec.
// core-js's CoreJSPromiseConstructor extends PromiseConstructor with
// this signature — if bun-types makes resolve optional, this fails with TS2430.
interface StrictPromiseWithResolvers<T> {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
}
interface StrictPromiseConstructor extends PromiseConstructor {
withResolvers<T>(): StrictPromiseWithResolvers<T>;
}
// ArrayBuffer.resize must return void per the spec.
// If bun-types returns ArrayBuffer instead, this fails with TS2430.
interface StrictArrayBuffer extends ArrayBuffer {
resize(newByteLength?: number): void;
}
`,
},
emptyInterfaces: expectedEmptyInterfacesWhenNoDOM,
diagnostics: diagnostics => {
const relevantDiagnostics = diagnostics.filter(d => d.line?.startsWith("core-js-extends-check.ts"));
expect(relevantDiagnostics).toEqual([]);
},
});
// Intentionally fetches type definitions from the upstream core-js v4-types
// branch at test time rather than vendoring them, so we always test against
// the latest core-js types and catch new incompatibilities early.
// https://github.com/zloirock/core-js/tree/v4-types/packages/core-js-types
const CORE_JS_TYPES_TREE_API = "https://api.github.com/repos/zloirock/core-js/git/trees/v4-types?recursive=1";
const CORE_JS_TYPES_RAW_BASE = "https://raw.githubusercontent.com/zloirock/core-js/v4-types";
const CORE_JS_TYPES_PREFIX = "packages/core-js-types/src/base/";
async function fetchWithRetry(url: string, retries = 3): Promise<Response> {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (response.ok) return response;
if (i === retries - 1) throw new Error(`Failed to fetch ${url}: ${response.status}`);
} catch (error) {
if (i === retries - 1) throw error;
}
await Bun.sleep(1000 * (i + 1));
}
throw new Error("unreachable");
}
test("no conflicts with upstream core-js-types", async () => {
// Discover all non-pure .d.ts files from the core-js-types package
const treeResponse = await fetchWithRetry(CORE_JS_TYPES_TREE_API);
const tree: { tree: { path: string; type: string }[] } = await treeResponse.json();
const typesFiles = tree.tree
.filter(
entry =>
entry.type === "blob" &&
entry.path.startsWith(CORE_JS_TYPES_PREFIX) &&
entry.path.endsWith(".d.ts") &&
!entry.path.includes("/pure/"),
)
.map(entry => entry.path.slice(CORE_JS_TYPES_PREFIX.length));
if (typesFiles.length === 0) throw new Error("No core-js type files found — API may have changed");
// Fetch all files in parallel
const files: Record<string, string> = {};
await Promise.all(
typesFiles.map(async file => {
const response = await fetchWithRetry(`${CORE_JS_TYPES_RAW_BASE}/${CORE_JS_TYPES_PREFIX}${file}`);
files[`core-js-types/${file}`] = await response.text();
}),
);
files["core-js-compat.ts"] =
typesFiles.map(file => `/// <reference path="core-js-types/${file}" />`).join("\n") +
`
// Verify usage works with both bun-types and core-js-types loaded
const buf = new ArrayBuffer(1024, { maxByteLength: 2048 });
buf.resize(2048);
const { promise, resolve, reject } = Promise.withResolvers<string>();
resolve("hello");
`;
const fixtureDir = await createIsolatedFixture();
const { diagnostics, emptyInterfaces } = await diagnose(fixtureDir, { files });
// core-js declares some DOM interfaces (Element, Node, etc.) for
// iterable-dom-collections — these are empty without lib.dom.d.ts.
// Just verify we're a superset of the base expected empty interfaces.
for (const name of expectedEmptyInterfacesWhenNoDOM) {
expect(emptyInterfaces).toContain(name);
}
// Filter out core-js internal issues (missing cross-references, circular types)
// that aren't caused by bun-types incompatibility.
const ignoredCodes = new Set([
2688, // "Cannot find type definition file" — core-js cross-references between its own files
2502, // "referenced directly or indirectly in its own type annotation" — circular refs in core-js
]);
const relevantDiagnostics = diagnostics.filter(
d =>
!ignoredCodes.has(d.code) &&
(d.line === null || d.line.startsWith("core-js-compat.ts") || d.line.startsWith("core-js-types/")),
);
expect(relevantDiagnostics).toEqual([]);
});
});
describe("lib configuration", () => {
typeTest("checks with no lib at all", {
options: {

View File

@@ -13,3 +13,10 @@ const buf = new SharedArrayBuffer(1024);
buf.grow(2048);
expectType(buffer[Symbol.toStringTag]).extends<string>();
// ArrayBuffer.resize() should return void per the ECMAScript spec
expectType(buffer.resize(2048)).is<void>();
// Promise.withResolvers resolve parameter should be non-optional
const { resolve } = Promise.withResolvers<string>();
expectType(resolve).is<(value: string | PromiseLike<string>) => void>();

View File

@@ -0,0 +1,64 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { readdirSync, existsSync } from "fs";
import { join } from "path";
test("bun build --no-bundle --outdir should output files correctly - issue #10370", async () => {
using dir = tempDir("10370-no-bundle-outdir", {
"index.ts": `console.log("hello from typescript");`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "./index.ts", "--no-bundle", "--outdir", "./dist"],
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(stderr).not.toContain("error");
expect(exitCode).toBe(0);
// Verify the output file exists
const distDir = join(String(dir), "dist");
expect(existsSync(distDir)).toBe(true);
const files = readdirSync(distDir);
expect(files).toContain("index.js");
});
test("bun build --no-bundle --outdir should handle .tsx extension - issue #10370", async () => {
using dir = tempDir("10370-no-bundle-outdir-tsx", {
"App.tsx": `export function App() { return <div>Hello</div>; }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "./App.tsx", "--no-bundle", "--outdir", "./out"],
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(stderr).not.toContain("error");
expect(exitCode).toBe(0);
// Verify the output file exists
const outDir = join(String(dir), "out");
expect(existsSync(outDir)).toBe(true);
const files = readdirSync(outDir);
expect(files).toContain("App.js");
});