mirror of
https://github.com/oven-sh/bun
synced 2026-02-21 08:12:21 +00:00
Compare commits
4 Commits
claude/fix
...
claude/arr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6441149674 | ||
|
|
e5c8da3c99 | ||
|
|
55fe522910 | ||
|
|
0b13ba1b7e |
@@ -269,6 +269,12 @@ pub const Arrow = struct {
|
||||
has_rest_arg: bool = false,
|
||||
prefer_expr: bool = false, // Use shorthand if true and "Body" is a single return statement
|
||||
|
||||
/// When minify_syntax is enabled and this arrow is a candidate for the
|
||||
/// `() => obj.method()` -> `obj.method.bind(obj)` transformation, this
|
||||
/// stores the ref of the receiver identifier (obj). The printer will check
|
||||
/// if this symbol was_assigned_to and only apply the transformation if not.
|
||||
bind_call_target_ref: ?Ref = null,
|
||||
|
||||
pub const noop_return_undefined: Arrow = .{
|
||||
.args = &.{},
|
||||
.body = .{
|
||||
|
||||
@@ -122,6 +122,7 @@ pub fn NewParser_(
|
||||
pub const visitClass = astVisit.visitClass;
|
||||
pub const visitStmts = astVisit.visitStmts;
|
||||
pub const visitAndAppendStmt = astVisit.visitAndAppendStmt;
|
||||
pub const tryMarkArrowForBindCallTransform = astVisit.tryMarkArrowForBindCallTransform;
|
||||
|
||||
pub const BinaryExpressionVisitor = @import("./visitBinaryExpression.zig").CreateBinaryExpressionVisitor(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).BinaryExpressionVisitor;
|
||||
|
||||
|
||||
@@ -154,6 +154,31 @@ pub fn Visit(
|
||||
return decs;
|
||||
}
|
||||
|
||||
/// Check if this arrow is a candidate for the `() => obj.method()` to
|
||||
/// `obj.method.bind(obj)` transformation. If so, store the receiver ref
|
||||
/// in the arrow so the printer can check if the symbol was assigned to.
|
||||
///
|
||||
/// NOTE: This optimization is currently DISABLED because it can change
|
||||
/// semantics in cases where:
|
||||
/// 1. The property (method) is reassigned after the arrow is created
|
||||
/// 2. The property is a getter that returns different values on each access
|
||||
/// 3. Constructability differs: arrows are not constructable but bound
|
||||
/// functions derived from regular methods may be
|
||||
///
|
||||
/// To safely enable this optimization, we would need to track:
|
||||
/// - Property assignments to the receiver object
|
||||
/// - Whether the property is defined as a getter
|
||||
/// - Whether the object escapes to code that could modify it
|
||||
/// - Whether the arrow could be used with `new`
|
||||
///
|
||||
/// For now, we conservatively disable the transformation entirely.
|
||||
pub fn tryMarkArrowForBindCallTransform(p: *P, arrow: *E.Arrow) void {
|
||||
_ = p;
|
||||
_ = arrow;
|
||||
// Disabled - see comment above for rationale
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn visitDecls(noalias p: *P, decls: []G.Decl, was_const: bool, comptime is_possibly_decl_to_remove: bool) usize {
|
||||
var j: usize = 0;
|
||||
var out_decls = decls;
|
||||
|
||||
@@ -1571,6 +1571,18 @@ pub fn VisitExpr(
|
||||
p.fn_only_data_visit.is_inside_async_arrow_fn = old_inside_async_arrow_fn;
|
||||
p.fn_or_arrow_data_visit = std.mem.bytesToValue(@TypeOf(p.fn_or_arrow_data_visit), &old_fn_or_arrow_data);
|
||||
|
||||
// Mark arrows that are candidates for the `() => obj.method()` to
|
||||
// `obj.method.bind(obj)` transformation. The actual transformation is
|
||||
// deferred to the printer, which can check if the captured symbol was
|
||||
// assigned to anywhere in the code.
|
||||
if (p.options.features.minify_syntax and
|
||||
e_.args.len == 0 and
|
||||
!e_.is_async and
|
||||
e_.body.stmts.len == 1)
|
||||
{
|
||||
p.tryMarkArrowForBindCallTransform(e_);
|
||||
}
|
||||
|
||||
if (react_hook_data) |*hook| try_mark_hook: {
|
||||
const stmts = p.nearest_stmt_list orelse break :try_mark_hook;
|
||||
bun.handleOom(stmts.append(p.getReactRefreshHookSignalDecl(hook.signature_cb)));
|
||||
|
||||
@@ -412,42 +412,6 @@ const IncomingMessagePrototype = {
|
||||
set socket(value) {
|
||||
this[fakeSocketSymbol] = value;
|
||||
},
|
||||
// Used by HTTP parser for adding headers from raw socket parsing
|
||||
// This is needed for allowHTTP1 fallback in HTTP/2 servers
|
||||
_addHeaderLine(field, value, dest) {
|
||||
const lowercased = field.toLowerCase();
|
||||
if (lowercased === "set-cookie") {
|
||||
if (dest[lowercased] !== undefined) {
|
||||
dest[lowercased].push(value);
|
||||
} else {
|
||||
dest[lowercased] = [value];
|
||||
}
|
||||
} else {
|
||||
if (dest[lowercased] !== undefined) {
|
||||
dest[lowercased] += ", " + value;
|
||||
} else {
|
||||
dest[lowercased] = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
_addHeaderLines(headers, n) {
|
||||
if (headers?.length) {
|
||||
if (!this.headers) {
|
||||
this.headers = Object.create(null);
|
||||
}
|
||||
if (!this.rawHeaders) {
|
||||
this.rawHeaders = [];
|
||||
}
|
||||
// Add to rawHeaders
|
||||
for (let i = 0; i < n; i++) {
|
||||
this.rawHeaders.push(headers[i]);
|
||||
}
|
||||
// Parse and add to headers object
|
||||
for (let i = 0; i < n; i += 2) {
|
||||
this._addHeaderLine(headers[i], headers[i + 1], this.headers);
|
||||
}
|
||||
}
|
||||
},
|
||||
} satisfies typeof import("node:http").IncomingMessage.prototype;
|
||||
IncomingMessage.prototype = IncomingMessagePrototype;
|
||||
$setPrototypeDirect.$call(IncomingMessage, Readable);
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
const { isTypedArray } = require("node:util/types");
|
||||
const { hideFromStack, throwNotImplemented } = require("internal/shared");
|
||||
const { STATUS_CODES } = require("internal/http");
|
||||
const http = require("node:http");
|
||||
const { parsers, freeParser, kIncomingMessage, HTTPParser } = require("node:_http_common");
|
||||
const tls = require("node:tls");
|
||||
const net = require("node:net");
|
||||
const fs = require("node:fs");
|
||||
@@ -61,7 +59,6 @@ const RegExpPrototypeExec = RegExp.prototype.exec;
|
||||
const ObjectAssign = Object.assign;
|
||||
const ArrayIsArray = Array.isArray;
|
||||
const ObjectKeys = Object.keys;
|
||||
const ObjectEntries = Object.entries;
|
||||
const FunctionPrototypeBind = Function.prototype.bind;
|
||||
const StringPrototypeTrim = String.prototype.trim;
|
||||
const ArrayPrototypePush = Array.prototype.push;
|
||||
@@ -3714,278 +3711,13 @@ function closeAllSessions(server: Http2Server | Http2SecureServer) {
|
||||
}
|
||||
}
|
||||
|
||||
// Simple HTTP/1.1 ServerResponse for allowHTTP1 fallback
|
||||
// This writes directly to the socket since we don't have a native Bun handle
|
||||
class Http1FallbackResponse extends EventEmitter {
|
||||
socket;
|
||||
statusCode = 200;
|
||||
statusMessage = "OK";
|
||||
headersSent = false;
|
||||
finished = false;
|
||||
_headers = Object.create(null);
|
||||
req;
|
||||
sendDate = true;
|
||||
_hasBody = true;
|
||||
|
||||
constructor(req, socket) {
|
||||
super();
|
||||
this.req = req;
|
||||
this.socket = socket;
|
||||
if (req.method === "HEAD") this._hasBody = false;
|
||||
}
|
||||
|
||||
setHeader(name, value) {
|
||||
this._headers[StringPrototypeToLowerCase.$call(name)] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
getHeader(name) {
|
||||
return this._headers[StringPrototypeToLowerCase.$call(name)];
|
||||
}
|
||||
|
||||
removeHeader(name) {
|
||||
delete this._headers[StringPrototypeToLowerCase.$call(name)];
|
||||
return this;
|
||||
}
|
||||
|
||||
hasHeader(name) {
|
||||
return StringPrototypeToLowerCase.$call(name) in this._headers;
|
||||
}
|
||||
|
||||
getHeaderNames() {
|
||||
return ObjectKeys(this._headers);
|
||||
}
|
||||
|
||||
writeHead(statusCode, statusMessage?, headers?) {
|
||||
if (this.headersSent) return this;
|
||||
|
||||
if (typeof statusMessage === "object") {
|
||||
headers = statusMessage;
|
||||
statusMessage = undefined;
|
||||
}
|
||||
|
||||
this.statusCode = statusCode;
|
||||
if (statusMessage) this.statusMessage = statusMessage;
|
||||
else this.statusMessage = STATUS_CODES[statusCode] || "Unknown";
|
||||
|
||||
if (headers) {
|
||||
for (const key of ObjectKeys(headers)) {
|
||||
this.setHeader(key, headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
_flushHeaders() {
|
||||
if (this.headersSent) return;
|
||||
this.headersSent = true;
|
||||
|
||||
let head = `HTTP/1.1 ${this.statusCode} ${this.statusMessage}\r\n`;
|
||||
|
||||
if (this.sendDate && !this.hasHeader("date")) {
|
||||
head += `Date: ${utcDate()}\r\n`;
|
||||
}
|
||||
|
||||
for (const [name, value] of ObjectEntries(this._headers)) {
|
||||
if (ArrayIsArray(value)) {
|
||||
for (const v of value) {
|
||||
head += `${name}: ${v}\r\n`;
|
||||
}
|
||||
} else {
|
||||
head += `${name}: ${value}\r\n`;
|
||||
}
|
||||
}
|
||||
|
||||
head += "\r\n";
|
||||
this.socket.write(head);
|
||||
}
|
||||
|
||||
write(chunk, encoding?, callback?) {
|
||||
if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
|
||||
if (!this.headersSent) {
|
||||
if (!this.hasHeader("transfer-encoding") && !this.hasHeader("content-length")) {
|
||||
this.setHeader("transfer-encoding", "chunked");
|
||||
}
|
||||
this._flushHeaders();
|
||||
}
|
||||
|
||||
if (this._hasBody && chunk) {
|
||||
if (this.getHeader("transfer-encoding") === "chunked") {
|
||||
const len = typeof chunk === "string" ? Buffer.byteLength(chunk, encoding) : chunk.length;
|
||||
this.socket.write(len.toString(16) + "\r\n");
|
||||
this.socket.write(chunk, encoding);
|
||||
return this.socket.write("\r\n", undefined, callback);
|
||||
} else {
|
||||
return this.socket.write(chunk, encoding, callback);
|
||||
}
|
||||
} else if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
end(data?, encoding?, callback?) {
|
||||
if (typeof data === "function") {
|
||||
callback = data;
|
||||
data = undefined;
|
||||
} else if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
|
||||
if (this.finished) {
|
||||
if (callback) process.nextTick(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!this.headersSent) {
|
||||
if (data && !this.hasHeader("content-length") && !this.hasHeader("transfer-encoding")) {
|
||||
const len = typeof data === "string" ? Buffer.byteLength(data, encoding) : (data?.length ?? 0);
|
||||
this.setHeader("content-length", String(len));
|
||||
} else if (!data && !this.hasHeader("content-length") && !this.hasHeader("transfer-encoding")) {
|
||||
this.setHeader("content-length", "0");
|
||||
}
|
||||
this._flushHeaders();
|
||||
}
|
||||
|
||||
if (this._hasBody && data) {
|
||||
if (this.getHeader("transfer-encoding") === "chunked") {
|
||||
const len = typeof data === "string" ? Buffer.byteLength(data, encoding) : data.length;
|
||||
this.socket.write(len.toString(16) + "\r\n");
|
||||
this.socket.write(data, encoding);
|
||||
this.socket.write("\r\n0\r\n\r\n");
|
||||
} else {
|
||||
this.socket.write(data, encoding);
|
||||
}
|
||||
} else if (this.getHeader("transfer-encoding") === "chunked") {
|
||||
this.socket.write("0\r\n\r\n");
|
||||
}
|
||||
|
||||
this.finished = true;
|
||||
this.emit("finish");
|
||||
|
||||
if (callback) process.nextTick(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
writeContinue() {
|
||||
this.socket.write("HTTP/1.1 100 Continue\r\n\r\n");
|
||||
}
|
||||
|
||||
destroy(err?) {
|
||||
this.socket.destroy(err);
|
||||
return this;
|
||||
}
|
||||
|
||||
assignSocket(socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP/1.1 connection listener for allowHTTP1 fallback
|
||||
// This sets up an HTTP parser to handle HTTP/1.1 requests on the TLS socket
|
||||
function http1ConnectionListener(socket: Socket, options) {
|
||||
const server = this;
|
||||
|
||||
// Ensure socket has server property set
|
||||
socket.server = server;
|
||||
|
||||
// Get configurable classes from options (set by initializeOptions)
|
||||
// Note: We default to Http1FallbackResponse because http.ServerResponse requires
|
||||
// a native handle that isn't available when parsing raw sockets. Only use a custom
|
||||
// ServerResponse if explicitly provided by the user.
|
||||
const ServerResponse =
|
||||
options.Http1ServerResponse && options.Http1ServerResponse !== http.ServerResponse
|
||||
? options.Http1ServerResponse
|
||||
: Http1FallbackResponse;
|
||||
|
||||
// Get the HTTP parser from the pool
|
||||
const parser = parsers.alloc();
|
||||
parser.socket = socket;
|
||||
socket.parser = parser;
|
||||
|
||||
// Initialize parser for REQUEST mode
|
||||
parser.initialize(HTTPParser.REQUEST, socket);
|
||||
|
||||
// When the parser completes parsing headers, create a response and emit the request event
|
||||
parser.onIncoming = function onIncoming(req, shouldKeepAlive) {
|
||||
// Create a response using the configured ServerResponse class
|
||||
// For Http1FallbackResponse, pass (req, socket). For custom classes, just pass (req).
|
||||
const res = ServerResponse === Http1FallbackResponse ? new ServerResponse(req, socket) : new ServerResponse(req);
|
||||
|
||||
// Emit the request event on the server
|
||||
if (req.headers.expect !== undefined && req.httpVersionMajor === 1 && req.httpVersionMinor === 1) {
|
||||
if (req.headers.expect === "100-continue") {
|
||||
if (server.listenerCount("checkContinue") > 0) {
|
||||
server.emit("checkContinue", req, res);
|
||||
} else {
|
||||
res.writeContinue();
|
||||
server.emit("request", req, res);
|
||||
}
|
||||
} else {
|
||||
if (server.listenerCount("checkExpectation") > 0) {
|
||||
server.emit("checkExpectation", req, res);
|
||||
} else {
|
||||
res.writeHead(417);
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
server.emit("request", req, res);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Set up socket event handlers
|
||||
function onSocketData(data) {
|
||||
const ret = parser.execute(data);
|
||||
if (ret instanceof Error) {
|
||||
socket.destroy(ret);
|
||||
}
|
||||
}
|
||||
|
||||
function onSocketEnd() {
|
||||
const ret = parser.finish();
|
||||
if (ret instanceof Error) {
|
||||
socket.destroy(ret);
|
||||
}
|
||||
}
|
||||
|
||||
function onSocketClose() {
|
||||
// Clean up parser
|
||||
freeParser(parser, null, socket);
|
||||
socket.parser = null;
|
||||
}
|
||||
|
||||
function onSocketError(err) {
|
||||
if (!server.emit("clientError", err, socket)) {
|
||||
socket.destroy(err);
|
||||
}
|
||||
}
|
||||
|
||||
socket.on("data", onSocketData);
|
||||
socket.on("end", onSocketEnd);
|
||||
socket.on("close", onSocketClose);
|
||||
socket.on("error", onSocketError);
|
||||
|
||||
// Emit connection event
|
||||
server.emit("connection", socket);
|
||||
}
|
||||
|
||||
function connectionListener(socket: Socket) {
|
||||
const options = this[bunSocketServerOptions] || {};
|
||||
if (socket.alpnProtocol === false || socket.alpnProtocol === "http/1.1") {
|
||||
// Fallback to HTTP/1.1
|
||||
if (options.allowHTTP1 === true) {
|
||||
return http1ConnectionListener.$call(this, socket, options);
|
||||
}
|
||||
// TODO: Fallback to HTTP/1.1
|
||||
// if (options.allowHTTP1 === true) {
|
||||
|
||||
// }
|
||||
// Let event handler deal with the socket
|
||||
|
||||
if (!this.emit("unknownProtocol", socket)) {
|
||||
@@ -4056,8 +3788,8 @@ function initializeOptions(options) {
|
||||
else options.unknownProtocolTimeout = 10000;
|
||||
|
||||
// Used only with allowHTTP1
|
||||
options.Http1IncomingMessage ||= http.IncomingMessage;
|
||||
options.Http1ServerResponse ||= http.ServerResponse;
|
||||
// options.Http1IncomingMessage ||= http.IncomingMessage;
|
||||
// options.Http1ServerResponse ||= http.ServerResponse;
|
||||
|
||||
options.Http2ServerRequest ||= Http2ServerRequest;
|
||||
options.Http2ServerResponse ||= Http2ServerResponse;
|
||||
@@ -4156,10 +3888,10 @@ class Http2SecureServer extends tls.Server {
|
||||
timeout = 0;
|
||||
[kSessions] = new SafeSet();
|
||||
constructor(options, onRequestHandler) {
|
||||
//TODO: add 'http/1.1' on ALPNProtocols list after allowHTTP1 support
|
||||
if (typeof options !== "undefined") {
|
||||
if (options && typeof options === "object") {
|
||||
const ALPNProtocols = options.allowHTTP1 === true ? ["h2", "http/1.1"] : ["h2"];
|
||||
options = { ...options, ALPNProtocols };
|
||||
options = { ...options, ALPNProtocols: ["h2"] };
|
||||
} else {
|
||||
throw $ERR_INVALID_ARG_TYPE("options", "object", options);
|
||||
}
|
||||
@@ -4167,9 +3899,16 @@ class Http2SecureServer extends tls.Server {
|
||||
options = { ALPNProtocols: ["h2"] };
|
||||
}
|
||||
|
||||
// Initialize options with defaults (including Http1IncomingMessage/ServerResponse)
|
||||
options = initializeOptions(options);
|
||||
const settings = options.settings;
|
||||
if (typeof settings !== "undefined") {
|
||||
validateObject(settings, "options.settings");
|
||||
}
|
||||
if (options.maxSessionInvalidFrames !== undefined)
|
||||
validateUint32(options.maxSessionInvalidFrames, "maxSessionInvalidFrames");
|
||||
|
||||
if (options.maxSessionRejectedStreams !== undefined) {
|
||||
validateUint32(options.maxSessionRejectedStreams, "maxSessionRejectedStreams");
|
||||
}
|
||||
super(options, connectionListener);
|
||||
this[kSessions] = new SafeSet();
|
||||
this.setMaxListeners(0);
|
||||
|
||||
@@ -2565,6 +2565,26 @@ fn NewPrinter(
|
||||
}
|
||||
},
|
||||
.e_arrow => |e| {
|
||||
// Optimization: Convert `() => obj.method()` to `obj.method.bind(obj)`
|
||||
// when the receiver symbol was never assigned to.
|
||||
if (e.bind_call_target_ref) |target_ref| {
|
||||
if (p.symbols().get(target_ref)) |symbol| {
|
||||
// Only transform if the symbol was never assigned to.
|
||||
// For const/unbound, has_been_assigned_to is always false.
|
||||
// For hoisted (function params, var), it's true if assigned anywhere.
|
||||
if (!symbol.has_been_assigned_to) {
|
||||
// Get the call expression from the body
|
||||
const call = e.body.stmts[0].data.s_return.value.?.data.e_call;
|
||||
// Print: target.method.bind(target)
|
||||
p.printExpr(call.target, .postfix, ExprFlag.Set{});
|
||||
p.print(".bind(");
|
||||
p.printSymbol(target_ref);
|
||||
p.print(")");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrap = level.gte(.assign);
|
||||
|
||||
if (wrap) {
|
||||
|
||||
@@ -1192,4 +1192,391 @@ describe("bundler", () => {
|
||||
stdout: "object\nobject\nobject",
|
||||
},
|
||||
});
|
||||
|
||||
// Arrow to bind optimization tests
|
||||
// NOTE: The arrow-to-bind transformation is currently DISABLED because
|
||||
// it can change semantics when properties are reassigned or are getters.
|
||||
// These tests verify the transformation is NOT applied.
|
||||
itBundled("minify/ArrowToBindConstIdentifier", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { value: 42, method() { return this.value; } };
|
||||
const fn = () => obj.method();
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Optimization disabled - should NOT transform
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "42",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformUnboundGlobal", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const fn = () => console.log();
|
||||
fn();
|
||||
console.log("done");
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform unbound globals - they could be reassigned externally
|
||||
expect(code).not.toContain(".bind(");
|
||||
expect(code).toMatch(/\(\)\s*=>/);
|
||||
},
|
||||
run: {
|
||||
stdout: "\ndone",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindComputedProperty", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { myMethod() { return this.value; }, value: 99 };
|
||||
const key = "myMethod";
|
||||
const fn = () => obj[key]();
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Optimization disabled - should NOT transform
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "99",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformLetReassigned", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
let obj = { method() { return "first"; } };
|
||||
const fn = () => obj.method();
|
||||
obj = { method() { return "second"; } };
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because obj is reassigned
|
||||
expect(code).not.toContain(".bind(");
|
||||
expect(code).toMatch(/\(\)\s*=>/);
|
||||
},
|
||||
run: {
|
||||
stdout: "second",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindLetNotReassigned", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
let obj = { value: 42, method() { return this.value; } };
|
||||
const fn = () => obj.method();
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Optimization disabled - should NOT transform
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "42",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindFunctionParam", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
function fetch(init) {
|
||||
return () => init.signal();
|
||||
}
|
||||
console.log(fetch({ signal: () => 555 })());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Optimization disabled - should NOT transform
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "555",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformFunctionParamReassigned", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
function fetch(init) {
|
||||
const cb = () => init.signal();
|
||||
init = { signal: () => 666 };
|
||||
return cb;
|
||||
}
|
||||
console.log(fetch({ signal: () => 555 })());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because init is reassigned
|
||||
expect(code).not.toContain(".bind(");
|
||||
expect(code).toMatch(/\(\)\s*=>/);
|
||||
},
|
||||
run: {
|
||||
stdout: "666",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformWithArgs", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { method(x) { return x * 2; } };
|
||||
const fn = () => obj.method(21);
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because the call has arguments
|
||||
expect(code).not.toContain(".bind(");
|
||||
expect(code).toMatch(/\(\)\s*=>/);
|
||||
},
|
||||
run: {
|
||||
stdout: "42",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformArrowWithParams", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { method() { return 123; } };
|
||||
const fn = (x) => obj.method();
|
||||
console.log(fn(1));
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because the arrow has parameters
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "123",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformAsync", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { async method() { return 456; } };
|
||||
const fn = async () => obj.method();
|
||||
fn().then(console.log);
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because the arrow is async
|
||||
expect(code).not.toContain(".bind(");
|
||||
expect(code).toMatch(/async\s*\(\)\s*=>/);
|
||||
},
|
||||
run: {
|
||||
stdout: "456",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformOptionalChain", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { method() { return 789; } };
|
||||
const fn = () => obj?.method();
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because of optional chaining
|
||||
expect(code).not.toContain(".bind(");
|
||||
expect(code).toMatch(/\?\.\w+\(\)/);
|
||||
},
|
||||
run: {
|
||||
stdout: "789",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformOptionalCall", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { method() { return 321; } };
|
||||
const fn = () => obj.method?.();
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because of optional call
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "321",
|
||||
},
|
||||
});
|
||||
itBundled("minify/ArrowToBindNoTransformThisTarget", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
class MyClass {
|
||||
value = 42;
|
||||
method() { return this.value; }
|
||||
getMethod() {
|
||||
// Arrow captures 'this' from enclosing context
|
||||
// Cannot transform because 'this' is not an identifier
|
||||
return () => this.method();
|
||||
}
|
||||
}
|
||||
const obj = new MyClass();
|
||||
const fn = obj.getMethod();
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because target is 'this', not a bound identifier
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "42",
|
||||
},
|
||||
});
|
||||
itBundled("minify/ArrowToBindNoTransformArgumentsAccess", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { greet(name) { return "Hello, " + name; } };
|
||||
function test() {
|
||||
// Arrow that uses arguments from enclosing function
|
||||
const fn = () => obj.greet(arguments[0]);
|
||||
return fn();
|
||||
}
|
||||
console.log(test("World"));
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because call has arguments
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "Hello, World",
|
||||
},
|
||||
});
|
||||
itBundled("minify/ArrowToBindNoTransformNewTarget", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = {
|
||||
check() { return "method called"; }
|
||||
};
|
||||
function MyConstructor() {
|
||||
// Arrow that references new.target
|
||||
this.fn = () => {
|
||||
if (new.target) {
|
||||
return obj.check();
|
||||
}
|
||||
return "no new.target";
|
||||
};
|
||||
}
|
||||
const instance = new MyConstructor();
|
||||
console.log(instance.fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform - arrow body has multiple statements (if/else logic)
|
||||
// and references new.target which arrows inherit from enclosing scope
|
||||
expect(code).not.toContain("obj.check.bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "method called",
|
||||
},
|
||||
});
|
||||
itBundled("minify/ArrowToBindNoTransformPropertyReassigned", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { method() { return "original"; } };
|
||||
const fn = () => obj.method();
|
||||
obj.method = () => "reassigned";
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because obj.method is reassigned after the arrow is created
|
||||
// If we transform to obj.method.bind(obj), it would capture the original method
|
||||
// But the arrow should call the reassigned method
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "reassigned",
|
||||
},
|
||||
});
|
||||
itBundled("minify/ArrowToBindNoTransformGetterProperty", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
let callCount = 0;
|
||||
const obj = {
|
||||
get method() {
|
||||
callCount++;
|
||||
return () => "from getter " + callCount;
|
||||
}
|
||||
};
|
||||
const fn = () => obj.method();
|
||||
console.log(fn());
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because method is a getter
|
||||
// bind() would only call the getter once, but arrow calls it each time
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "from getter 1\nfrom getter 2",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
/**
|
||||
* Regression test for issue #26721
|
||||
*
|
||||
* HTTP/1.1 fallback is broken for `node:http2` secure server when
|
||||
* `allowHTTP1: true` is passed. The server only advertises `h2` in ALPN
|
||||
* negotiation, causing HTTP/1.1-only clients to fail.
|
||||
*
|
||||
* @see https://github.com/oven-sh/bun/issues/26721
|
||||
*/
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import http2 from "node:http2";
|
||||
import https from "node:https";
|
||||
import { join } from "node:path";
|
||||
|
||||
// TLS certificates for testing
|
||||
const fixturesDir = join(import.meta.dirname, "..", "fixtures");
|
||||
const tlsOptions = {
|
||||
cert: readFileSync(join(fixturesDir, "cert.pem")),
|
||||
key: readFileSync(join(fixturesDir, "cert.key")),
|
||||
};
|
||||
|
||||
interface TestContext {
|
||||
server: http2.Http2SecureServer;
|
||||
serverPort: number;
|
||||
serverUrl: string;
|
||||
}
|
||||
|
||||
describe("HTTP/2 allowHTTP1 option", () => {
|
||||
let ctx: TestContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
const server = http2.createSecureServer({
|
||||
...tlsOptions,
|
||||
allowHTTP1: true,
|
||||
});
|
||||
|
||||
// Handle HTTP/2 streams
|
||||
server.on("stream", (stream, headers) => {
|
||||
stream.respond({
|
||||
":status": 200,
|
||||
"content-type": "text/plain",
|
||||
"x-protocol": "h2",
|
||||
});
|
||||
stream.end("ok h2\n");
|
||||
});
|
||||
|
||||
// Handle HTTP/1.1 requests (via allowHTTP1 fallback)
|
||||
// Note: HTTP/2 compatibility also emits 'request' events, but those requests
|
||||
// will have already been handled by the 'stream' handler. We check if headers
|
||||
// have been sent to avoid double-responding.
|
||||
server.on("request", (req, res) => {
|
||||
// Skip if this is from HTTP/2 compat (headers already sent by stream handler)
|
||||
if (res.headersSent) return;
|
||||
res.writeHead(200, {
|
||||
"content-type": "text/plain",
|
||||
"x-protocol": "http1",
|
||||
});
|
||||
res.end("ok http1\n");
|
||||
});
|
||||
|
||||
const { promise: listenPromise, resolve: listenResolve, reject: listenReject } = Promise.withResolvers<number>();
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
listenReject(new Error("Failed to get server address"));
|
||||
return;
|
||||
}
|
||||
listenResolve(address.port);
|
||||
});
|
||||
server.once("error", listenReject);
|
||||
const serverPort = await listenPromise;
|
||||
|
||||
ctx = {
|
||||
server,
|
||||
serverPort,
|
||||
serverUrl: `https://127.0.0.1:${serverPort}`,
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (ctx?.server) {
|
||||
// Close all active connections first to ensure server.close() completes
|
||||
if (typeof ctx.server.closeAllConnections === "function") {
|
||||
ctx.server.closeAllConnections();
|
||||
}
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
ctx.server.close(() => resolve());
|
||||
await promise;
|
||||
}
|
||||
});
|
||||
|
||||
test("HTTP/2 client can connect and make request", async () => {
|
||||
const client = http2.connect(ctx.serverUrl, { rejectUnauthorized: false });
|
||||
|
||||
const response = await new Promise<{ status: number; body: string; protocol: string }>((resolve, reject) => {
|
||||
const req = client.request({ ":path": "/" });
|
||||
|
||||
let body = "";
|
||||
let protocol = "";
|
||||
|
||||
req.on("response", headers => {
|
||||
protocol = headers["x-protocol"] as string;
|
||||
});
|
||||
|
||||
req.on("data", chunk => {
|
||||
body += chunk;
|
||||
});
|
||||
|
||||
req.on("end", () => {
|
||||
resolve({ status: 200, body, protocol });
|
||||
});
|
||||
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
});
|
||||
|
||||
expect(response.body).toBe("ok h2\n");
|
||||
expect(response.protocol).toBe("h2");
|
||||
|
||||
const { promise: closePromise, resolve: closeResolve } = Promise.withResolvers<void>();
|
||||
client.close(closeResolve);
|
||||
await closePromise;
|
||||
});
|
||||
|
||||
test("HTTP/1.1 client can connect when allowHTTP1 is true (issue #26721)", async () => {
|
||||
// This test verifies that HTTP/1.1 clients can connect to an HTTP/2 server
|
||||
// with allowHTTP1: true. Before the fix, this would fail with:
|
||||
// "tlsv1 alert no application protocol" because the server only
|
||||
// advertised "h2" in ALPN, not "http/1.1".
|
||||
|
||||
const response = await new Promise<{ statusCode: number; body: string; protocol: string }>((resolve, reject) => {
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "127.0.0.1",
|
||||
port: ctx.serverPort,
|
||||
path: "/",
|
||||
method: "GET",
|
||||
rejectUnauthorized: false,
|
||||
headers: {
|
||||
Connection: "close", // Ensure connection is closed after request
|
||||
},
|
||||
// Force HTTP/1.1 by not specifying ALPNProtocols or by using https module
|
||||
},
|
||||
res => {
|
||||
let body = "";
|
||||
res.on("data", chunk => {
|
||||
body += chunk;
|
||||
});
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode!,
|
||||
body,
|
||||
protocol: res.headers["x-protocol"] as string,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toBe("ok http1\n");
|
||||
expect(response.protocol).toBe("http1");
|
||||
});
|
||||
|
||||
test("HTTP/1.1 POST request works with allowHTTP1", async () => {
|
||||
const postData = JSON.stringify({ message: "hello" });
|
||||
|
||||
// Use the shared server from ctx
|
||||
const response = await new Promise<{ statusCode: number; body: string }>((resolve, reject) => {
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "127.0.0.1",
|
||||
port: ctx.serverPort,
|
||||
path: "/post",
|
||||
method: "POST",
|
||||
rejectUnauthorized: false,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(postData),
|
||||
Connection: "close", // Ensure connection is closed after request
|
||||
},
|
||||
},
|
||||
res => {
|
||||
let body = "";
|
||||
res.on("data", chunk => {
|
||||
body += chunk;
|
||||
});
|
||||
res.on("end", () => {
|
||||
resolve({ statusCode: res.statusCode!, body });
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
req.on("error", reject);
|
||||
req.write(postData);
|
||||
req.end();
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toBe("ok http1\n");
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTTP/2 without allowHTTP1", () => {
|
||||
test("HTTP/1.1 client gets rejected when allowHTTP1 is false", async () => {
|
||||
const server = http2.createSecureServer({
|
||||
...tlsOptions,
|
||||
allowHTTP1: false,
|
||||
});
|
||||
|
||||
server.on("stream", (stream, _headers) => {
|
||||
stream.respond({ ":status": 200 });
|
||||
stream.end("ok");
|
||||
});
|
||||
|
||||
const { promise: listenPromise, resolve: listenResolve, reject: listenReject } = Promise.withResolvers<number>();
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
listenReject(new Error("Failed to get server address"));
|
||||
return;
|
||||
}
|
||||
listenResolve(address.port);
|
||||
});
|
||||
server.once("error", listenReject);
|
||||
const port = await listenPromise;
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "127.0.0.1",
|
||||
port,
|
||||
path: "/",
|
||||
method: "GET",
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
() => {
|
||||
reject(new Error("Expected connection to fail"));
|
||||
},
|
||||
);
|
||||
|
||||
req.on("error", err => {
|
||||
// We expect an ALPN negotiation error or similar
|
||||
// Note: Bun's https client may report different error messages
|
||||
expect(err.message).toMatch(/no application protocol|ECONNRESET|ECONNREFUSED|socket hang up/i);
|
||||
resolve();
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
} finally {
|
||||
// Force close all connections and the server
|
||||
// Use a short timeout to ensure this doesn't hang the test
|
||||
await Promise.race([
|
||||
new Promise<void>(resolve => server.close(() => resolve())),
|
||||
new Promise<void>(resolve => setTimeout(resolve, 500)),
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user