Compare commits

..

1 Commits

Author SHA1 Message Date
Claude Bot
5d661a4487 fix(sql): return JS number for PostgreSQL bigint values within safe integer range
When the `bigint` option is not set (the default), PostgreSQL int8/bigint
values that fit within Number.MAX_SAFE_INTEGER (±2^53-1) are now returned
as JavaScript numbers instead of strings. Values outside that range continue
to be returned as strings to avoid precision loss. This makes common
operations like COUNT(*) return numbers as users expect.

Fixes #22188

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-14 23:48:15 +00:00
22 changed files with 136 additions and 340 deletions

View File

@@ -1140,14 +1140,14 @@ export fn Bun__runVirtualModule(globalObject: *JSGlobalObject, specifier_ptr: *c
fn getHardcodedModule(jsc_vm: *VirtualMachine, specifier: bun.String, hardcoded: HardcodedModule) ?ResolvedSource {
analytics.Features.builtin_modules.insert(hardcoded);
return switch (hardcoded) {
.@"bun:main" => if (jsc_vm.entry_point.generated) .{
.@"bun:main" => .{
.allocator = null,
.source_code = bun.String.cloneUTF8(jsc_vm.entry_point.source.contents),
.specifier = specifier,
.source_url = specifier,
.tag = .esm,
.source_code_needs_deref = true,
} else null,
},
.@"bun:internal-for-testing" => {
if (!Environment.isDebug) {
if (!is_allowed_to_use_internal_testing_apis)

View File

@@ -1616,7 +1616,7 @@ fn _resolve(
if (strings.eqlComptime(std.fs.path.basename(specifier), Runtime.Runtime.Imports.alt_name)) {
ret.path = Runtime.Runtime.Imports.Name;
return;
} else if (strings.eqlComptime(specifier, main_file_name) and jsc_vm.entry_point.generated) {
} else if (strings.eqlComptime(specifier, main_file_name)) {
ret.result = null;
ret.path = jsc_vm.entry_point.source.path.text;
return;

View File

@@ -4,7 +4,7 @@ const TimerObjectInternals = @This();
/// Identifier for this timer that is exposed to JavaScript (by `+timer`)
id: i32 = -1,
interval: u31 = 0,
this_value: jsc.JSRef = .empty(),
strong_this: jsc.Strong.Optional = .empty,
flags: Flags = .{},
/// Used by:
@@ -76,41 +76,31 @@ pub fn runImmediateTask(this: *TimerObjectInternals, vm: *VirtualMachine) bool {
// loop alive other than setImmediates
(!this.flags.is_keeping_event_loop_alive and !vm.isEventLoopAliveExcludingImmediates()))
{
this.setEnableKeepingEventLoopAlive(vm, false);
this.this_value.downgrade();
this.deref();
return false;
}
const timer = this.this_value.tryGet() orelse {
const timer = this.strong_this.get() orelse {
if (Environment.isDebug) {
@panic("TimerObjectInternals.runImmediateTask: this_object is null");
}
this.setEnableKeepingEventLoopAlive(vm, false);
this.deref();
return false;
};
const globalThis = vm.global;
this.this_value.downgrade();
this.strong_this.deinit();
this.eventLoopTimer().state = .FIRED;
this.setEnableKeepingEventLoopAlive(vm, false);
timer.ensureStillAlive();
vm.eventLoop().enter();
const callback = ImmediateObject.js.callbackGetCached(timer).?;
const arguments = ImmediateObject.js.argumentsGetCached(timer).?;
this.ref();
const exception_thrown = this.run(globalThis, timer, callback, arguments, this.asyncID(), vm);
this.deref();
const exception_thrown = brk: {
this.ref();
defer {
if (this.eventLoopTimer().state == .FIRED) {
this.deref();
}
this.deref();
}
break :brk this.run(globalThis, timer, callback, arguments, this.asyncID(), vm);
};
// --- after this point, the timer is no longer guaranteed to be alive ---
if (this.eventLoopTimer().state == .FIRED) {
this.deref();
}
vm.eventLoop().exitMaybeDrainMicrotasks(!exception_thrown) catch return true;
@@ -130,13 +120,7 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
this.eventLoopTimer().state = .FIRED;
const globalThis = vm.global;
const this_object = this.this_value.tryGet() orelse {
this.setEnableKeepingEventLoopAlive(vm, false);
this.flags.has_cleared_timer = true;
this.this_value.downgrade();
this.deref();
return;
};
const this_object = this.strong_this.get().?;
const callback: JSValue, const arguments: JSValue, var idle_timeout: JSValue, var repeat: JSValue = switch (kind) {
.setImmediate => .{
@@ -159,7 +143,7 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
}
this.setEnableKeepingEventLoopAlive(vm, false);
this.flags.has_cleared_timer = true;
this.this_value.downgrade();
this.strong_this.deinit();
this.deref();
return;
@@ -168,7 +152,7 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
var time_before_call: timespec = undefined;
if (kind != .setInterval) {
this.this_value.downgrade();
this.strong_this.clearWithoutDeallocation();
} else {
time_before_call = timespec.msFromNow(.allow_mocked_time, this.interval);
}
@@ -255,7 +239,7 @@ fn convertToInterval(this: *TimerObjectInternals, global: *JSGlobalObject, timer
// https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L613
TimeoutObject.js.idleTimeoutSetCached(timer, global, repeat);
this.this_value.setStrong(timer, global);
this.strong_this.set(global, timer);
this.flags.kind = .setInterval;
this.interval = new_interval;
this.reschedule(timer, vm, global);
@@ -313,7 +297,7 @@ pub fn init(
this.reschedule(timer, vm, global);
}
this.this_value.setStrong(timer, global);
this.strong_this.set(global, timer);
}
pub fn doRef(this: *TimerObjectInternals, _: *jsc.JSGlobalObject, this_value: JSValue) JSValue {
@@ -343,7 +327,7 @@ pub fn doRefresh(this: *TimerObjectInternals, globalObject: *jsc.JSGlobalObject,
return this_value;
}
this.this_value.setStrong(this_value, globalObject);
this.strong_this.set(globalObject, this_value);
this.reschedule(this_value, VirtualMachine.get(), globalObject);
return this_value;
@@ -366,18 +350,12 @@ pub fn cancel(this: *TimerObjectInternals, vm: *VirtualMachine) void {
this.setEnableKeepingEventLoopAlive(vm, false);
this.flags.has_cleared_timer = true;
if (this.flags.kind == .setImmediate) {
// Release the strong reference so the GC can collect the JS object.
// The immediate task is still in the event loop queue and will be skipped
// by runImmediateTask when it sees has_cleared_timer == true.
this.this_value.downgrade();
return;
}
if (this.flags.kind == .setImmediate) return;
const was_active = this.eventLoopTimer().state == .ACTIVE;
this.eventLoopTimer().state = .CANCELLED;
this.this_value.downgrade();
this.strong_this.deinit();
if (was_active) {
vm.timer.remove(this.eventLoopTimer());
@@ -464,12 +442,12 @@ pub fn getDestroyed(this: *TimerObjectInternals) bool {
}
pub fn finalize(this: *TimerObjectInternals) void {
this.this_value.finalize();
this.strong_this.deinit();
this.deref();
}
pub fn deinit(this: *TimerObjectInternals) void {
this.this_value.deinit();
this.strong_this.deinit();
const vm = VirtualMachine.get();
const kind = this.flags.kind;

View File

@@ -68,16 +68,6 @@ pub const Chunk = struct {
}
pub fn getCSSChunkForHTML(this: *const Chunk, chunks: []Chunk) ?*Chunk {
// Look up the CSS chunk via the JS chunk's css_chunks indices.
// This correctly handles deduplicated CSS chunks that are shared
// across multiple HTML entry points (see issue #23668).
if (this.getJSChunkForHTML(chunks)) |js_chunk| {
const css_chunk_indices = js_chunk.content.javascript.css_chunks;
if (css_chunk_indices.len > 0) {
return &chunks[css_chunk_indices[0]];
}
}
// Fallback: match by entry_point_id for cases without a JS chunk.
const entry_point_id = this.entry_point.entry_point_id;
for (chunks) |*other| {
if (other.content == .css) {

View File

@@ -150,7 +150,6 @@ pub const ClientEntryPoint = struct {
pub const ServerEntryPoint = struct {
source: logger.Source = undefined,
generated: bool = false,
pub fn generate(
entry: *ServerEntryPoint,
@@ -231,7 +230,6 @@ pub const ServerEntryPoint = struct {
entry.source = logger.Source.initPathString(name, code);
entry.source.path.text = name;
entry.source.path.namespace = "server-entry";
entry.generated = true;
}
};

View File

@@ -22,6 +22,7 @@ pub noinline fn computeChunks(
const entry_source_indices = this.graph.entry_points.items(.source_index);
const css_asts = this.graph.ast.items(.css);
const css_chunking = this.options.css_chunking;
var html_chunks = bun.StringArrayHashMap(Chunk).init(temp_allocator);
const loaders = this.parse_graph.input_files.items(.loader);
const ast_targets = this.graph.ast.items(.target);
@@ -147,11 +148,10 @@ pub noinline fn computeChunks(
if (css_source_indices.len > 0) {
const order = this.findImportedFilesInCSSOrder(temp_allocator, css_source_indices.slice());
// Always use content-based hashing for CSS chunk deduplication.
// This ensures that when multiple JS entry points import the
// same CSS files, they share a single CSS output chunk rather
// than producing duplicates that collide on hash-based naming.
const hash_to_use = brk: {
const use_content_based_key = css_chunking or has_server_html_imports;
const hash_to_use = if (!use_content_based_key)
bun.hash(try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len)))
else brk: {
var hasher = std.hash.Wyhash.init(5);
bun.writeAnyToHasher(&hasher, order.len);
for (order.slice()) |x| x.hash(&hasher);
@@ -322,10 +322,7 @@ pub noinline fn computeChunks(
const remapped_css_indexes = try temp_allocator.alloc(u32, css_chunks.count());
const css_chunk_values = css_chunks.values();
// Use sorted_chunks.len as the starting index because HTML chunks
// may be interleaved with JS chunks, so js_chunks.count() would be
// incorrect when HTML entry points are present.
for (sorted_css_keys, sorted_chunks.len..) |key, sorted_index| {
for (sorted_css_keys, js_chunks.count()..) |key, sorted_index| {
const index = css_chunks.getIndex(key) orelse unreachable;
sorted_chunks.appendAssumeCapacity(css_chunk_values[index]);
remapped_css_indexes[index] = @intCast(sorted_index);

View File

@@ -1637,21 +1637,6 @@ pub const RunCommand = struct {
return;
}
// Support `node --run <script>` (Node.js v22+ feature).
// The --run flag is silently discarded by the arg parser since it's
// unrecognized, but the script name ends up as ctx.positionals[0].
// Scan the raw argv to detect if --run was present.
if (ctx.positionals.len > 0) {
for (bun.argv) |arg| {
if (strings.eqlComptime(arg, "--run")) {
if (exec(ctx, .{ .bin_dirs_only = false, .log_errors = true, .allow_fast_run_for_extensions = false })) |ok| {
if (ok) return;
} else |_| {}
Global.exit(1);
}
}
}
if (ctx.positionals.len == 0) {
Output.errGeneric("Missing script to execute. Bun's provided 'node' cli wrapper does not support a repl.", .{});
Global.exit(1);

View File

@@ -766,13 +766,19 @@ pub extern fn napi_type_tag_object(env: napi_env, _: napi_value, _: [*c]const na
pub extern fn napi_check_object_type_tag(env: napi_env, _: napi_value, _: [*c]const napi_type_tag, _: *bool) napi_status;
// do nothing for both of these
pub export fn napi_open_callback_scope(_: napi_env, _: napi_value, _: *anyopaque, _: *anyopaque) napi_status {
pub export fn napi_open_callback_scope(env_: napi_env, _: napi_value, _: *anyopaque, _: *anyopaque) napi_status {
log("napi_open_callback_scope", .{});
return @intFromEnum(NapiStatus.ok);
const env = env_ orelse {
return envIsNull();
};
return env.ok();
}
pub export fn napi_close_callback_scope(_: napi_env, _: *anyopaque) napi_status {
pub export fn napi_close_callback_scope(env_: napi_env, _: *anyopaque) napi_status {
log("napi_close_callback_scope", .{});
return @intFromEnum(NapiStatus.ok);
const env = env_ orelse {
return envIsNull();
};
return env.ok();
}
pub extern fn napi_throw(env: napi_env, @"error": napi_value) napi_status;
pub extern fn napi_throw_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status;

View File

@@ -12,32 +12,24 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __hasOwnProp = Object.prototype.hasOwnProperty;
// Shared getter/setter functions: .bind(obj, key) avoids creating a closure
// and JSLexicalEnvironment per property. BoundFunction is much cheaper.
// Must be regular functions (not arrows) so .bind() can set `this`.
function __accessProp(key) {
return this[key];
}
// This is used to implement "export * from" statements. It copies properties
// from the imported module to the current module's ESM export object. If the
// current module is an entry point and the target format is CommonJS, we
// also copy the properties to "module.exports" in addition to our module's
// internal ESM export object.
export var __reExport = (target, mod, secondTarget) => {
var keys = __getOwnPropNames(mod);
for (let key of keys)
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(target, key) && key !== "default")
__defProp(target, key, {
get: __accessProp.bind(mod, key),
get: () => mod[key],
enumerable: true,
});
if (secondTarget) {
for (let key of keys)
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(secondTarget, key) && key !== "default")
__defProp(secondTarget, key, {
get: __accessProp.bind(mod, key),
get: () => mod[key],
enumerable: true,
});
@@ -45,22 +37,11 @@ export var __reExport = (target, mod, secondTarget) => {
}
};
/*__PURE__*/
var __toESMCache_node;
/*__PURE__*/
var __toESMCache_esm;
// Converts the module from CommonJS to ESM. When in node mode (i.e. in an
// ".mjs" file, package.json has "type: module", or the "__esModule" export
// in the CommonJS file is falsy or missing), the "default" property is
// overridden to point to the original CommonJS exports object instead.
export var __toESM = (mod, isNodeMode, target) => {
var canCache = mod != null && typeof mod === "object";
if (canCache) {
var cache = isNodeMode ? (__toESMCache_node ??= new WeakMap()) : (__toESMCache_esm ??= new WeakMap());
var cached = cache.get(mod);
if (cached) return cached;
}
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to =
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
@@ -72,34 +53,34 @@ export var __toESM = (mod, isNodeMode, target) => {
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: __accessProp.bind(mod, key),
get: () => mod[key],
enumerable: true,
});
if (canCache) cache.set(mod, to);
return to;
};
// Converts the module from ESM to CommonJS. This clones the input module
// object with the addition of a non-enumerable "__esModule" property set
// to "true", which overwrites any existing export named "__esModule".
export var __toCommonJS = from => {
var entry = (__moduleCache ??= new WeakMap()).get(from),
var __moduleCache = /* @__PURE__ */ new WeakMap();
export var __toCommonJS = /* @__PURE__ */ from => {
var entry = __moduleCache.get(from),
desc;
if (entry) return entry;
entry = __defProp({}, "__esModule", { value: true });
if ((from && typeof from === "object") || typeof from === "function")
for (var key of __getOwnPropNames(from))
if (!__hasOwnProp.call(entry, key))
__getOwnPropNames(from).map(
key =>
!__hasOwnProp.call(entry, key) &&
__defProp(entry, key, {
get: __accessProp.bind(from, key),
get: () => from[key],
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable,
});
}),
);
__moduleCache.set(from, entry);
return entry;
};
/*__PURE__*/
var __moduleCache;
// When you do know the module is CJS
export var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -116,10 +97,6 @@ export var __name = (target, name) => {
// ESM export -> CJS export
// except, writable incase something re-exports
var __returnValue = v => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
export var __export = /* @__PURE__ */ (target, all) => {
for (var name in all)
@@ -127,19 +104,15 @@ export var __export = /* @__PURE__ */ (target, all) => {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name),
set: newValue => (all[name] = () => newValue),
});
};
function __exportValueSetter(name, newValue) {
this[name] = newValue;
}
export var __exportValue = (target, all) => {
for (var name in all) {
__defProp(target, name, {
get: __accessProp.bind(all, name),
set: __exportValueSetter.bind(all, name),
get: () => all[name],
set: newValue => (all[name] = newValue),
enumerable: true,
configurable: true,
});

View File

@@ -402,10 +402,11 @@ fn parseArray(bytes: []const u8, bigint: bool, comptime arrayType: types.Tag, gl
}
switch (arrayType) {
.int8_array => {
const int8_val = std.fmt.parseInt(i64, element, 0) catch return error.UnsupportedArrayFormat;
if (bigint) {
try array.append(bun.default_allocator, SQLDataCell{ .tag = .int8, .value = .{ .int8 = std.fmt.parseInt(i64, element, 0) catch return error.UnsupportedArrayFormat } });
try array.append(bun.default_allocator, SQLDataCell{ .tag = .int8, .value = .{ .int8 = int8_val } });
} else {
try array.append(bun.default_allocator, SQLDataCell{ .tag = .string, .value = .{ .string = if (element.len > 0) bun.String.cloneUTF8(element).value.WTFStringImpl else null }, .free_value = 1 });
try array.append(bun.default_allocator, int8ToSafeCell(int8_val));
}
slice = trySlice(slice, current_idx);
continue;
@@ -456,6 +457,25 @@ fn parseArray(bytes: []const u8, bigint: bool, comptime arrayType: types.Tag, gl
return SQLDataCell{ .tag = .array, .value = .{ .array = .{ .ptr = array.items.ptr, .len = @truncate(array.items.len), .cap = @truncate(array.capacity) } } };
}
/// Returns an int8 value as a JS-safe number when possible, or falls back to string.
/// Values within Number.MAX_SAFE_INTEGER range (±2^53 - 1) are returned as numbers;
/// values outside that range are returned as strings to avoid precision loss.
fn int8ToSafeCell(value: i64) SQLDataCell {
const max_safe_int = (1 << 53) - 1;
const min_safe_int = -max_safe_int;
if (value >= min_safe_int and value <= max_safe_int) {
// Value fits safely in a JS number (f64 without precision loss)
if (value >= std.math.minInt(i32) and value <= std.math.maxInt(i32)) {
return SQLDataCell{ .tag = .int4, .value = .{ .int4 = @intCast(value) } };
}
return SQLDataCell{ .tag = .float8, .value = .{ .float8 = @floatFromInt(value) } };
}
// Outside safe integer range: return as string to avoid precision loss
var buf: [21]u8 = undefined;
const str = std.fmt.bufPrint(&buf, "{d}", .{value}) catch unreachable;
return SQLDataCell{ .tag = .string, .value = .{ .string = bun.String.cloneUTF8(str).value.WTFStringImpl }, .free_value = 1 };
}
pub fn fromBytes(binary: bool, bigint: bool, oid: types.Tag, bytes: []const u8, globalObject: *jsc.JSGlobalObject) !SQLDataCell {
switch (oid) {
// TODO: .int2_array, .float8_array
@@ -530,13 +550,15 @@ pub fn fromBytes(binary: bool, bigint: bool, oid: types.Tag, bytes: []const u8,
return SQLDataCell{ .tag = .int4, .value = .{ .int4 = std.fmt.parseInt(i32, bytes, 0) catch 0 } };
}
},
// postgres when reading bigint as int8 it returns a string unless type: { bigint: postgres.BigInt is set
.int8 => {
const value: i64 = if (binary)
try parseBinary(.int8, i64, bytes)
else
std.fmt.parseInt(i64, bytes, 0) catch 0;
if (bigint) {
// .int8 is a 64-bit integer always string
return SQLDataCell{ .tag = .int8, .value = .{ .int8 = std.fmt.parseInt(i64, bytes, 0) catch 0 } };
return SQLDataCell{ .tag = .int8, .value = .{ .int8 = value } };
} else {
return SQLDataCell{ .tag = .string, .value = .{ .string = if (bytes.len > 0) bun.String.cloneUTF8(bytes).value.WTFStringImpl else null }, .free_value = 1 };
return int8ToSafeCell(value);
}
},
.float8 => {

View File

@@ -2,17 +2,13 @@
exports[`Bun.build Bun.write(BuildArtifact) 1`] = `
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
set: (newValue) => all[name] = () => newValue
});
};
@@ -35,17 +31,13 @@ NS.then(({ fn: fn2 }) => {
exports[`Bun.build outdir + reading out blobs works 1`] = `
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
set: (newValue) => all[name] = () => newValue
});
};
@@ -66,27 +58,23 @@ NS.then(({ fn: fn2 }) => {
"
`;
exports[`Bun.build BuildArtifact properties: hash 1`] = `"est79qzq"`;
exports[`Bun.build BuildArtifact properties: hash 1`] = `"d1c7nm6t"`;
exports[`Bun.build BuildArtifact properties + entry.naming: hash 1`] = `"7gfnt0h6"`;
exports[`Bun.build BuildArtifact properties + entry.naming: hash 1`] = `"rm7e36cf"`;
exports[`Bun.build BuildArtifact properties sourcemap: hash index.js 1`] = `"est79qzq"`;
exports[`Bun.build BuildArtifact properties sourcemap: hash index.js 1`] = `"d1c7nm6t"`;
exports[`Bun.build BuildArtifact properties sourcemap: hash index.js.map 1`] = `"00000000"`;
exports[`Bun.build new Response(BuildArtifact) sets content type: response text 1`] = `
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
set: (newValue) => all[name] = () => newValue
});
};

View File

@@ -1113,7 +1113,7 @@ describe("bundler", () => {
snapshotSourceMap: {
"entry.js.map": {
files: ["../node_modules/react/index.js", "../entry.js"],
mappingsExactMatch: "miBACA,WAAW,IAAQ,EAAE,ICDrB,eACA,QAAQ,IAAI,CAAK",
mappingsExactMatch: "qYACA,WAAW,IAAQ,EAAE,ICDrB,eACA,QAAQ,IAAI,CAAK",
},
},
});

View File

@@ -843,60 +843,4 @@ body {
api.expectFile("out/" + jsFile).toContain("sourceMappingURL");
},
});
// Test that multiple HTML entrypoints sharing the same CSS file both get
// the CSS link tag in production mode (css_chunking deduplication).
// Regression test for https://github.com/oven-sh/bun/issues/23668
itBundled("html/SharedCSSProductionMultipleEntries", {
outdir: "out/",
production: true,
files: {
"/entry1.html": `<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./global.css" />
</head>
<body>
<div id="root"></div>
<script src="./main1.tsx"></script>
</body>
</html>`,
"/entry2.html": `<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./global.css" />
</head>
<body>
<div id="root"></div>
<script src="./main2.tsx"></script>
</body>
</html>`,
"/global.css": `h1 { font-size: 24px; }`,
"/main1.tsx": `console.log("entry1");`,
"/main2.tsx": `console.log("entry2");`,
},
entryPoints: ["/entry1.html", "/entry2.html"],
onAfterBundle(api) {
const entry1Html = api.readFile("out/entry1.html");
const entry2Html = api.readFile("out/entry2.html");
// Both HTML files must contain a CSS link tag
const cssMatch1 = entry1Html.match(/href="(.*\.css)"/);
const cssMatch2 = entry2Html.match(/href="(.*\.css)"/);
expect(cssMatch1).not.toBeNull();
expect(cssMatch2).not.toBeNull();
// Both should reference the same deduplicated CSS chunk
expect(cssMatch1![1]).toBe(cssMatch2![1]);
// The CSS file should contain the shared styles
const cssContent = api.readFile("out/" + cssMatch1![1]);
expect(cssContent).toContain("font-size");
// Both HTML files should also have their respective JS bundles
expect(entry1Html).toMatch(/src=".*\.js"/);
expect(entry2Html).toMatch(/src=".*\.js"/);
},
});
});

View File

@@ -57,17 +57,17 @@ describe("bundler", () => {
"../entry.tsx",
],
mappings: [
["react.development.js:524:'getContextName'", "1:5567:Y1"],
["react.development.js:524:'getContextName'", "1:5412:Y1"],
["react.development.js:2495:'actScopeDepth'", "23:4082:GJ++"],
["react.development.js:696:''Component'", '1:7629:\'Component "%s"'],
["entry.tsx:6:'\"Content-Type\"'", '100:18808:"Content-Type"'],
["entry.tsx:11:'<html>'", "100:19062:void"],
["entry.tsx:23:'await'", "100:19161:await"],
["react.development.js:696:''Component'", '1:7474:\'Component "%s"'],
["entry.tsx:6:'\"Content-Type\"'", '100:18809:"Content-Type"'],
["entry.tsx:11:'<html>'", "100:19063:void"],
["entry.tsx:23:'await'", "100:19163:await"],
],
},
},
expectExactFilesize: {
"out/entry.js": 221895,
"out/entry.js": 221720,
},
run: {
stdout: "<!DOCTYPE html><html><body><h1>Hello World</h1><p>This is an example.</p></body></html>",

View File

@@ -76,17 +76,13 @@ describe("bundler", () => {
expect(bundled).toMatchInlineSnapshot(`
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
set: (newValue) => all[name] = () => newValue
});
};
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -164,7 +160,7 @@ describe("bundler", () => {
var { AsyncEntryPoint: AsyncEntryPoint2 } = await Promise.resolve().then(() => exports_AsyncEntryPoint);
AsyncEntryPoint2();
//# debugId=42062903F19477CF64756E2164756E21
//# debugId=5E85CC0956C6307964756E2164756E21
//# sourceMappingURL=out.js.map
"
`);
@@ -341,17 +337,13 @@ describe("bundler", () => {
expect(bundled).toMatchInlineSnapshot(`
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
set: (newValue) => all[name] = () => newValue
});
};
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -410,7 +402,7 @@ describe("bundler", () => {
var { AsyncEntryPoint: AsyncEntryPoint2 } = await Promise.resolve().then(() => exports_AsyncEntryPoint);
AsyncEntryPoint2();
//# debugId=BF876FBF618133C264756E2164756E21
//# debugId=C92CBF0103732ECC64756E2164756E21
//# sourceMappingURL=out.js.map
"
`);

View File

@@ -2150,7 +2150,10 @@ c {
toplevel-tilde.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
`, */
});
itBundled("css/MetafileCSSBundleTwoToOne", {
// TODO: Bun's bundler doesn't support multiple entry points generating CSS outputs
// with identical content hashes to the same output path. This test exposes that
// limitation. Skip until the bundler can deduplicate or handle this case.
itBundled.skip("css/MetafileCSSBundleTwoToOne", {
files: {
"/foo/entry.js": /* js */ `
import '../common.css'

View File

@@ -103,11 +103,11 @@ console.log(favicon);
"files": [
{
"input": "client.html",
"path": "./client-b5m4ng86.js",
"path": "./client-s249t5qg.js",
"loader": "js",
"isEntry": true,
"headers": {
"etag": "Ax71YVYyZQc",
"etag": "fxoJ6L-0X3o",
"content-type": "text/javascript;charset=utf-8"
}
},

View File

@@ -101,39 +101,4 @@ describe("fake node cli", () => {
const temp = tempDirWithFiles("fake-node", {});
expect(() => fakeNodeRun(temp, [])).toThrow();
});
describe("node --run", () => {
test("runs a package.json script", () => {
const temp = tempDirWithFiles("fake-node", {
"package.json": JSON.stringify({
scripts: {
echo_test: "echo pass",
},
}),
});
expect(fakeNodeRun(temp, ["--run", "echo_test"]).stdout).toBe("pass");
});
test("runs pre/post scripts", () => {
const temp = tempDirWithFiles("fake-node", {
"package.json": JSON.stringify({
scripts: {
premyscript: "echo pre",
myscript: "echo main",
postmyscript: "echo post",
},
}),
});
expect(fakeNodeRun(temp, ["--run", "myscript"]).stdout).toBe("pre\nmain\npost");
});
test("errors on missing script", () => {
const temp = tempDirWithFiles("fake-node", {
"package.json": JSON.stringify({
scripts: {},
}),
});
expect(() => fakeNodeRun(temp, ["--run", "nonexistent"])).toThrow();
});
});
});

View File

@@ -634,42 +634,3 @@ test.concurrent("bun serve files with correct Content-Type headers", async () =>
// The process will be automatically cleaned up by 'await using'
}
});
test("importing bun:main from HTML entry preload does not crash", async () => {
const dir = tempDirWithFiles("html-entry-bun-main", {
"index.html": /*html*/ `
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body><h1>Hello</h1></body>
</html>
`,
"preload.mjs": /*js*/ `
try {
await import("bun:main");
} catch {}
// Signal that preload ran successfully without crashing
console.log("PRELOAD_OK");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--preload", "./preload.mjs", "index.html", "--port=0"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const decoder = new TextDecoder();
let text = "";
for await (const chunk of proc.stdout) {
text += decoder.decode(chunk, { stream: true });
if (text.includes("http://")) break;
}
expect(text).toContain("PRELOAD_OK");
proc.kill();
await proc.exited;
});

View File

@@ -2775,8 +2775,35 @@ if (isDockerEnabled()) {
// return ['select 1', result]
// })
test("bigint is returned as String", async () => {
test("bigint outside safe integer range is returned as String", async () => {
expect(typeof (await sql`select 9223372036854777 as x`)[0].x).toBe("string");
expect((await sql`select 9223372036854777 as x`)[0].x).toBe("9223372036854777");
});
test("bigint within safe integer range is returned as Number", async () => {
// Values within Number.MAX_SAFE_INTEGER should be numbers, not strings
const result = (await sql`select 9007199254740991::int8 as x`)[0].x;
expect(typeof result).toBe("number");
expect(result).toBe(9007199254740991);
// Negative safe integer boundary
const result2 = (await sql`select (-9007199254740991)::int8 as x`)[0].x;
expect(typeof result2).toBe("number");
expect(result2).toBe(-9007199254740991);
// COUNT(*) returns bigint - should be a number for typical counts
const result3 = (await sql`select count(*) from information_schema.tables`)[0].count;
expect(typeof result3).toBe("number");
});
test("bigint just outside safe integer range is returned as String", async () => {
const result = (await sql`select 9007199254740992::int8 as x`)[0].x;
expect(typeof result).toBe("string");
expect(result).toBe("9007199254740992");
const result2 = (await sql`select (-9007199254740992)::int8 as x`)[0].x;
expect(typeof result2).toBe("string");
expect(result2).toBe("-9007199254740992");
});
test("bigint is returned as BigInt", async () => {

View File

@@ -1,29 +0,0 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "../../harness";
// https://github.com/oven-sh/bun/issues/27074
// `bun run --bun build` fails when a pre-script uses `node --run`
test("bun run --bun works with node --run in lifecycle scripts", () => {
const temp = tempDirWithFiles("issue-27074", {
"package.json": JSON.stringify({
scripts: {
echo_test: "echo echo_test_ran",
prebuild: "node --run echo_test",
build: "echo build_ran",
},
}),
});
const result = Bun.spawnSync({
cmd: [bunExe(), "run", "--bun", "build"],
cwd: temp,
env: bunEnv,
});
const stdout = result.stdout.toString("utf8").trim();
const stderr = result.stderr.toString("utf8").trim();
expect(stdout).toContain("echo_test_ran");
expect(stdout).toContain("build_ran");
expect(result.exitCode).toBe(0);
});

View File

@@ -92,17 +92,13 @@ test("cyclic imports with async dependencies should generate async wrappers", as
expect(bundled).toMatchInlineSnapshot(`
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
set: (newValue) => all[name] = () => newValue
});
};
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -180,7 +176,7 @@ test("cyclic imports with async dependencies should generate async wrappers", as
var { AsyncEntryPoint: AsyncEntryPoint2 } = await Promise.resolve().then(() => exports_AsyncEntryPoint);
AsyncEntryPoint2();
//# debugId=2020261114B67BB564756E2164756E21
//# debugId=986E7BD819E590FD64756E2164756E21
//# sourceMappingURL=entryBuild.js.map
"
`);