mirror of
https://github.com/oven-sh/bun
synced 2026-03-04 14:30:58 +01:00
Compare commits
3 Commits
main
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf42798d9d | ||
|
|
3cd29a93a0 | ||
|
|
d7616e1046 |
@@ -186,9 +186,15 @@ pub const PathWatcher = struct {
|
||||
.manager = manager,
|
||||
});
|
||||
|
||||
// uv_fs_event_init on Windows unconditionally returns 0 (vendor/libuv/src/win/fs-event.c).
|
||||
// bun.assert evaluates its argument before the inline early-return, so this runs in release too.
|
||||
bun.assert(uv.uv_fs_event_init(manager.vm.uvLoop(), &this.handle) == .zero);
|
||||
errdefer {
|
||||
_ = manager.watchers.swapRemove(event_path);
|
||||
this.manager = null;
|
||||
this.deinit();
|
||||
}
|
||||
|
||||
if (uv.uv_fs_event_init(manager.vm.uvLoop(), &this.handle).toError(.watch)) |err| {
|
||||
return .{ .err = err };
|
||||
}
|
||||
this.handle.data = this;
|
||||
|
||||
// UV_FS_EVENT_RECURSIVE only works for Windows and OSX
|
||||
@@ -198,12 +204,6 @@ pub const PathWatcher = struct {
|
||||
event_path.ptr,
|
||||
if (recursive) uv.UV_FS_EVENT_RECURSIVE else 0,
|
||||
).toError(.watch)) |err| {
|
||||
// `errdefer` doesn't fire on `return .{ .err = ... }` (that's a successful return of a
|
||||
// Maybe(T), not an error-union return). Clean up the map entry and the half-initialized
|
||||
// watcher inline. See #26254.
|
||||
_ = manager.watchers.swapRemove(event_path);
|
||||
this.manager = null; // prevent deinit() from re-entering unregisterWatcher
|
||||
this.deinit();
|
||||
return .{ .err = err };
|
||||
}
|
||||
// we handle this in node_fs_watcher
|
||||
|
||||
@@ -301,9 +301,10 @@ pub fn scheduleBarrelDeferredImports(this: *BundleV2, result: *ParseTask.Result.
|
||||
// Handle import records without named bindings (not in named_imports).
|
||||
// - `import "x"` (bare statement): tree-shakeable with sideEffects: false — skip.
|
||||
// - `require("x")`: synchronous, needs full module — always mark as .all.
|
||||
// - `import("x")`: returns the full module namespace at runtime — consumer
|
||||
// can destructure or access any export. Must mark as .all. We cannot
|
||||
// safely assume which exports will be used.
|
||||
// - `import("x")`: mark as .all ONLY if the barrel has no prior requests,
|
||||
// meaning this is the sole reference. If the barrel already has a .partial
|
||||
// entry from a static import, the dynamic import is likely a secondary
|
||||
// (possibly circular) reference and should not escalate requirements.
|
||||
for (file_import_records.slice(), 0..) |ir, idx| {
|
||||
const target = if (ir.source_index.isValid())
|
||||
ir.source_index.get()
|
||||
@@ -318,9 +319,10 @@ pub fn scheduleBarrelDeferredImports(this: *BundleV2, result: *ParseTask.Result.
|
||||
const gop = try this.requested_exports.getOrPut(this.allocator(), target);
|
||||
gop.value_ptr.* = .all;
|
||||
} else if (ir.kind == .dynamic) {
|
||||
// import() returns the full module namespace — must preserve all exports.
|
||||
const gop = try this.requested_exports.getOrPut(this.allocator(), target);
|
||||
gop.value_ptr.* = .all;
|
||||
// Only escalate to .all if no prior requests exist for this target.
|
||||
if (!this.requested_exports.contains(target)) {
|
||||
try this.requested_exports.put(this.allocator(), target, .all);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,8 +354,8 @@ pub fn scheduleBarrelDeferredImports(this: *BundleV2, result: *ParseTask.Result.
|
||||
}
|
||||
}
|
||||
|
||||
// Add bare require/dynamic-import targets to BFS as star imports — both
|
||||
// always need the full namespace.
|
||||
// Add bare require/dynamic-import targets to BFS as star imports (matching
|
||||
// the seeding logic above — require always, dynamic only when sole reference).
|
||||
for (file_import_records.slice(), 0..) |ir, idx| {
|
||||
const target = if (ir.source_index.isValid())
|
||||
ir.source_index.get()
|
||||
@@ -364,7 +366,8 @@ pub fn scheduleBarrelDeferredImports(this: *BundleV2, result: *ParseTask.Result.
|
||||
if (ir.flags.is_internal) continue;
|
||||
if (named_ir_indices.contains(@intCast(idx))) continue;
|
||||
if (ir.flags.was_originally_bare_import) continue;
|
||||
const should_add = ir.kind == .require or ir.kind == .dynamic;
|
||||
const is_all = if (this.requested_exports.get(target)) |re| re == .all else false;
|
||||
const should_add = ir.kind == .require or (ir.kind == .dynamic and is_all);
|
||||
if (should_add) {
|
||||
try queue.append(queue_alloc, .{ .barrel_source_index = target, .alias = "", .is_star = true });
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ pub const DeclarationBlock = struct {
|
||||
.declarations = &declarations,
|
||||
.options = options,
|
||||
};
|
||||
errdefer decl_parser.deinit();
|
||||
|
||||
var parser = css.RuleBodyParser(PropertyDeclarationParser).new(input, &decl_parser);
|
||||
|
||||
while (parser.next()) |res| {
|
||||
@@ -78,10 +80,6 @@ pub const DeclarationBlock = struct {
|
||||
options.warn(e);
|
||||
continue;
|
||||
}
|
||||
// errdefer doesn't fire on `return .{ .err = ... }` — Result(T) is a tagged
|
||||
// union, not an error union. Free any declarations accumulated so far.
|
||||
css.deepDeinit(css.Property, input.allocator(), &declarations);
|
||||
css.deepDeinit(css.Property, input.allocator(), &important_declarations);
|
||||
return .{ .err = e };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -951,23 +951,19 @@ pub const UnresolvedColor = union(enum) {
|
||||
options: *const css.ParserOptions,
|
||||
parser: *ComponentParser,
|
||||
pub fn parsefn(this: *@This(), input2: *css.Parser) Result(UnresolvedColor) {
|
||||
// errdefer doesn't fire on `return .{ .err = ... }` — Result(T) is a
|
||||
// tagged union, not an error union. Clean up `light` inline.
|
||||
var light = switch (input2.parseUntilBefore(css.Delimiters{ .comma = true }, TokenList, this, @This().parsefn2)) {
|
||||
const light = switch (input2.parseUntilBefore(css.Delimiters{ .comma = true }, TokenList, this, @This().parsefn2)) {
|
||||
.result => |vv| vv,
|
||||
.err => |e| return .{ .err = e },
|
||||
};
|
||||
if (input2.expectComma().asErr()) |e| {
|
||||
light.deinit(input2.allocator());
|
||||
return .{ .err = e };
|
||||
}
|
||||
// TODO: fix this
|
||||
errdefer light.deinit();
|
||||
if (input2.expectComma().asErr()) |e| return .{ .err = e };
|
||||
const dark = switch (TokenListFns.parse(input2, this.options, 0)) {
|
||||
.result => |vv| vv,
|
||||
.err => |e| {
|
||||
light.deinit(input2.allocator());
|
||||
return .{ .err = e };
|
||||
},
|
||||
.err => |e| return .{ .err = e },
|
||||
};
|
||||
// TODO: fix this
|
||||
errdefer dark.deinit();
|
||||
return .{ .result = UnresolvedColor{
|
||||
.light_dark = .{
|
||||
.light = light,
|
||||
|
||||
@@ -617,7 +617,14 @@ function parseOptions(
|
||||
username ||= options.user || options.username || decodeIfValid(url.username);
|
||||
password ||= options.pass || options.password || decodeIfValid(url.password);
|
||||
|
||||
path ||= options.path || url.pathname;
|
||||
// Only use url.pathname as the Unix socket path for unix:// URLs.
|
||||
// For postgres://, mysql://, etc., the pathname is the database name (e.g. "/mydb"),
|
||||
// not a socket path. Using it as the socket path causes FailedToOpenSocket (#27713).
|
||||
if (url.protocol === "unix:") {
|
||||
path ||= options.path || url.pathname;
|
||||
} else {
|
||||
path ||= options.path;
|
||||
}
|
||||
|
||||
const queryObject = url.searchParams.toJSON();
|
||||
for (const key in queryObject) {
|
||||
|
||||
@@ -541,9 +541,7 @@ describe("bundler", () => {
|
||||
});
|
||||
|
||||
// --- Ported from Rolldown: dynamic-import-entry ---
|
||||
// A submodule dynamically imports the barrel back. import() returns the full
|
||||
// module namespace — all barrel exports must be preserved, even if the
|
||||
// import() result is discarded (we can't statically prove it isn't used).
|
||||
// A submodule dynamically imports the barrel back
|
||||
|
||||
itBundled("barrel/DynamicImportInSubmodule", {
|
||||
files: {
|
||||
@@ -564,103 +562,17 @@ describe("bundler", () => {
|
||||
export const a = 'dyn-a';
|
||||
import('./index.js');
|
||||
`,
|
||||
// b.js has a syntax error — only a is imported, so b should be skipped
|
||||
"/node_modules/dynlib/b.js": /* js */ `
|
||||
export const b = 'dyn-b';
|
||||
export const b = <<<SYNTAX_ERROR>>>;
|
||||
`,
|
||||
},
|
||||
outdir: "/out",
|
||||
onAfterBundle(api) {
|
||||
api.expectFile("/out/entry.js").toContain("dyn-a");
|
||||
// b must be included — import() needs the full namespace
|
||||
api.expectFile("/out/entry.js").toContain("dyn-b");
|
||||
},
|
||||
});
|
||||
|
||||
// Dynamic import returns the full namespace at runtime — consumer can access any export.
|
||||
// When a file also has a static named import of the same barrel, the barrel
|
||||
// optimization must not drop exports the dynamic import might use.
|
||||
// Previously, the dynamic import was ignored if a static import already seeded
|
||||
// requested_exports, producing invalid JS (export clause referencing undeclared symbol).
|
||||
itBundled("barrel/DynamicImportWithStaticImportSameTarget", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import { a } from "barrel";
|
||||
console.log(a);
|
||||
const run = async () => {
|
||||
const { b } = await import("barrel");
|
||||
console.log(b);
|
||||
};
|
||||
run();
|
||||
`,
|
||||
"/node_modules/barrel/package.json": JSON.stringify({
|
||||
name: "barrel",
|
||||
main: "./index.js",
|
||||
sideEffects: false,
|
||||
}),
|
||||
"/node_modules/barrel/index.js": /* js */ `
|
||||
export { a } from "./a.js";
|
||||
export { b } from "./b.js";
|
||||
`,
|
||||
"/node_modules/barrel/a.js": /* js */ `
|
||||
export const a = "A";
|
||||
`,
|
||||
"/node_modules/barrel/b.js": /* js */ `
|
||||
export const b = "B";
|
||||
`,
|
||||
},
|
||||
splitting: true,
|
||||
format: "esm",
|
||||
target: "bun",
|
||||
outdir: "/out",
|
||||
run: {
|
||||
stdout: "A\nB",
|
||||
},
|
||||
});
|
||||
|
||||
// Same as above but static and dynamic importers are in separate files.
|
||||
// This was parse-order dependent — if the static importer's
|
||||
// scheduleBarrelDeferredImports ran first, it seeded .partial and the dynamic
|
||||
// importer's escalation was skipped. Now import() always escalates to .all.
|
||||
itBundled("barrel/DynamicImportWithStaticImportSeparateFiles", {
|
||||
files: {
|
||||
"/static-user.js": /* js */ `
|
||||
import { a } from "barrel2";
|
||||
console.log(a);
|
||||
`,
|
||||
"/dynamic-user.js": /* js */ `
|
||||
const run = async () => {
|
||||
const { b } = await import("barrel2");
|
||||
console.log(b);
|
||||
};
|
||||
run();
|
||||
`,
|
||||
"/node_modules/barrel2/package.json": JSON.stringify({
|
||||
name: "barrel2",
|
||||
main: "./index.js",
|
||||
sideEffects: false,
|
||||
}),
|
||||
"/node_modules/barrel2/index.js": /* js */ `
|
||||
export { a } from "./a.js";
|
||||
export { b } from "./b.js";
|
||||
`,
|
||||
"/node_modules/barrel2/a.js": /* js */ `
|
||||
export const a = "A";
|
||||
`,
|
||||
"/node_modules/barrel2/b.js": /* js */ `
|
||||
export const b = "B";
|
||||
`,
|
||||
},
|
||||
entryPoints: ["/static-user.js", "/dynamic-user.js"],
|
||||
splitting: true,
|
||||
format: "esm",
|
||||
target: "bun",
|
||||
outdir: "/out",
|
||||
run: [
|
||||
{ file: "/out/static-user.js", stdout: "A" },
|
||||
{ file: "/out/dynamic-user.js", stdout: "B" },
|
||||
],
|
||||
});
|
||||
|
||||
// --- Ported from Rolldown: multiple-entries ---
|
||||
// Multiple entry points that each import different things from barrels
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { pathToFileURL } from "bun";
|
||||
import { bunEnv, bunExe, bunRun, bunRunAsScript, isWindows, tempDir, tempDirWithFiles } from "harness";
|
||||
import { bunRun, bunRunAsScript, isWindows, tempDirWithFiles } from "harness";
|
||||
import fs, { FSWatcher } from "node:fs";
|
||||
import path from "path";
|
||||
|
||||
@@ -725,64 +725,3 @@ describe("immediately closing", () => {
|
||||
for (let i = 0; i < 100; i++) fs.watch(testDir, { persistent: false, recursive: false }).close();
|
||||
});
|
||||
});
|
||||
|
||||
// On Windows, if fs.watch() fails after getOrPut() inserts into the internal path->watcher
|
||||
// map (e.g. uv_fs_event_start fails on a dangling junction, an ACL-protected dir, or a
|
||||
// directory deleted mid-watch), an errdefer that was silently broken by a !*T -> Maybe(*T)
|
||||
// refactor left the entry in place with a dangling key and an uninitialized value. The next
|
||||
// fs.watch() on the same path collided with the poisoned entry, returned the garbage value
|
||||
// as a *PathWatcher, and segfaulted at 0xFFFFFFFFFFFFFFFF calling .handlers.put() on it.
|
||||
//
|
||||
// https://github.com/oven-sh/bun/issues/26254
|
||||
// https://github.com/oven-sh/bun/issues/20203
|
||||
// https://github.com/oven-sh/bun/issues/19635
|
||||
//
|
||||
// Must run in a subprocess: on an unpatched build this segfaults the whole runtime.
|
||||
test.skipIf(!isWindows)("retrying a failed fs.watch does not crash (windows)", async () => {
|
||||
using dir = tempDir("fswatch-retry-failed", { "index.js": "" });
|
||||
const base = String(dir);
|
||||
|
||||
const fixture = /* js */ `
|
||||
const { mkdirSync, rmdirSync, symlinkSync, watch } = require("node:fs");
|
||||
const { join } = require("node:path");
|
||||
|
||||
const base = ${JSON.stringify(base)};
|
||||
const target = join(base, "target");
|
||||
const link = join(base, "link");
|
||||
|
||||
mkdirSync(target);
|
||||
symlinkSync(target, link, "junction"); // junctions need no admin rights on Windows
|
||||
rmdirSync(target); // junction now dangles
|
||||
|
||||
// Call 1: readlink(link) SUCCEEDS (returns the vanished target path into
|
||||
// a stack-local buffer), then uv_fs_event_start(target) fails ENOENT.
|
||||
// On unpatched builds: map entry left with dangling key + uninit value.
|
||||
try { watch(link); throw new Error("expected first watch to fail"); }
|
||||
catch (e) { if (e.code !== "ENOENT") throw e; }
|
||||
|
||||
// Call 2: identical stack frame layout -> identical outbuf address ->
|
||||
// identical key slice -> getOrPut returns found_existing=true ->
|
||||
// returns uninitialized value as a *PathWatcher -> segfault on unpatched builds.
|
||||
// Correct behaviour: throw ENOENT again.
|
||||
try { watch(link); throw new Error("expected second watch to fail"); }
|
||||
catch (e) { if (e.code !== "ENOENT") throw e; }
|
||||
|
||||
// Call 3: a valid watch must still work (map must not be corrupted).
|
||||
watch(base).close();
|
||||
|
||||
console.log("OK");
|
||||
`;
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", fixture],
|
||||
cwd: base,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stderr).toBe("");
|
||||
expect(stdout.trim()).toBe("OK");
|
||||
expect(exitCode).toBe(0); // unpatched: exitCode is 3 (Windows segfault)
|
||||
});
|
||||
|
||||
128
test/regression/issue/27713.test.ts
Normal file
128
test/regression/issue/27713.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { SQL } from "bun";
|
||||
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
||||
import { isWindows } from "harness";
|
||||
|
||||
// Regression test for https://github.com/oven-sh/bun/issues/27713
|
||||
// Bun SQL was treating the Postgres URL path component (the database name)
|
||||
// as a Unix domain socket path, causing FailedToOpenSocket on any URL with
|
||||
// a database name.
|
||||
|
||||
describe("SQL should not treat URL pathname as Unix socket path (#27713)", () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
// prettier-ignore
|
||||
const SQL_ENV_VARS = [
|
||||
"DATABASE_URL", "DATABASEURL",
|
||||
"TLS_DATABASE_URL",
|
||||
"POSTGRES_URL", "PGURL", "PG_URL",
|
||||
"TLS_POSTGRES_DATABASE_URL",
|
||||
"MYSQL_URL", "MYSQLURL",
|
||||
"TLS_MYSQL_DATABASE_URL",
|
||||
"PGHOST", "PGUSER", "PGPASSWORD", "PGDATABASE", "PGPORT",
|
||||
"PG_HOST", "PG_USER", "PG_PASSWORD", "PG_DATABASE", "PG_PORT",
|
||||
"MYSQL_HOST", "MYSQL_USER", "MYSQL_PASSWORD", "MYSQL_DATABASE", "MYSQL_PORT",
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
for (const key of SQL_ENV_VARS) {
|
||||
delete process.env[key];
|
||||
delete Bun.env[key];
|
||||
delete import.meta.env[key];
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
for (const key of SQL_ENV_VARS) {
|
||||
if (key in originalEnv) {
|
||||
process.env[key] = originalEnv[key]!;
|
||||
Bun.env[key] = originalEnv[key]!;
|
||||
import.meta.env[key] = originalEnv[key]!;
|
||||
} else {
|
||||
delete process.env[key];
|
||||
delete Bun.env[key];
|
||||
delete import.meta.env[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("postgres URL with database name should not set path", () => {
|
||||
const sql = new SQL("postgres://user:pass@myhost:5432/mydb");
|
||||
expect(sql.options.hostname).toBe("myhost");
|
||||
expect(sql.options.port).toBe(5432);
|
||||
expect(sql.options.database).toBe("mydb");
|
||||
// path must not be the database name "/mydb"
|
||||
expect(sql.options.path).toBeUndefined();
|
||||
});
|
||||
|
||||
test("postgres URL passed via url option should not set path", () => {
|
||||
const sql = new SQL({
|
||||
url: "postgres://user:pass@myhost:5432/mydb",
|
||||
});
|
||||
expect(sql.options.hostname).toBe("myhost");
|
||||
expect(sql.options.port).toBe(5432);
|
||||
expect(sql.options.database).toBe("mydb");
|
||||
expect(sql.options.path).toBeUndefined();
|
||||
});
|
||||
|
||||
test("DATABASE_URL with database name should not set path when using explicit options", () => {
|
||||
process.env.DATABASE_URL = "postgres://user:pass@envhost:5432/envdb";
|
||||
|
||||
const sql = new SQL({
|
||||
hostname: "myhost",
|
||||
port: 5432,
|
||||
username: "user",
|
||||
password: "pass",
|
||||
database: "mydb",
|
||||
});
|
||||
|
||||
expect(sql.options.hostname).toBe("myhost");
|
||||
expect(sql.options.database).toBe("mydb");
|
||||
// path must not be "/envdb" from DATABASE_URL
|
||||
expect(sql.options.path).toBeUndefined();
|
||||
});
|
||||
|
||||
test("DATABASE_URL with database name should not set path when used implicitly", () => {
|
||||
process.env.DATABASE_URL = "postgres://user:pass@envhost:5432/envdb";
|
||||
|
||||
const sql = new SQL();
|
||||
expect(sql.options.hostname).toBe("envhost");
|
||||
expect(sql.options.port).toBe(5432);
|
||||
expect(sql.options.database).toBe("envdb");
|
||||
// path must not be "/envdb"
|
||||
expect(sql.options.path).toBeUndefined();
|
||||
});
|
||||
|
||||
test("postgres URL with database name matching existing directory should not set path", () => {
|
||||
// This is the actual bug: when the URL pathname matches an existing filesystem
|
||||
// path (like /tmp), the old code would pass it as a Unix socket path.
|
||||
// The database name in postgres://.../<dbname> is "/tmp" here, which exists.
|
||||
const sql = new SQL("postgres://user:pass@myhost:5432/tmp");
|
||||
expect(sql.options.hostname).toBe("myhost");
|
||||
expect(sql.options.port).toBe(5432);
|
||||
expect(sql.options.database).toBe("tmp");
|
||||
// Before the fix, this would be "/tmp" (or "/tmp/.s.PGSQL.5432" if that exists),
|
||||
// causing the connection to use Unix domain socket instead of TCP.
|
||||
expect(sql.options.path).toBeUndefined();
|
||||
});
|
||||
|
||||
test("mysql URL with database name should not set path", () => {
|
||||
const sql = new SQL("mysql://user:pass@myhost:3306/mydb");
|
||||
expect(sql.options.hostname).toBe("myhost");
|
||||
expect(sql.options.port).toBe(3306);
|
||||
expect(sql.options.database).toBe("mydb");
|
||||
expect(sql.options.path).toBeUndefined();
|
||||
});
|
||||
|
||||
test.skipIf(isWindows)("unix:// protocol should still use pathname as socket path", () => {
|
||||
const socketPath = `/tmp/bun-test-27713-${process.pid}.sock`;
|
||||
using sock = Bun.listen({
|
||||
unix: socketPath,
|
||||
socket: {
|
||||
data: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const sql = new SQL(`unix://${sock.unix}`, { adapter: "postgres" });
|
||||
expect(sql.options.path).toBe(socketPath);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user