diff --git a/packages/bun-uws/src/HttpContext.h b/packages/bun-uws/src/HttpContext.h index 73384a5af8..439b8cdf87 100644 --- a/packages/bun-uws/src/HttpContext.h +++ b/packages/bun-uws/src/HttpContext.h @@ -466,12 +466,12 @@ public: /* Register an HTTP route handler acording to URL pattern */ void onHttp(std::string method, std::string pattern, MoveOnlyFunction *, HttpRequest *)> &&handler, bool upgrade = false) { - HttpContextData *httpContextData = getSocketContextData(); + HttpContextData *httpContextData = getSocketContextData(); /* Todo: This is ugly, fix */ std::vector methods; if (method == "*") { - methods = httpContextData->currentRouter->upperCasedMethods; + methods = {"*"}; } else { methods = {method}; } diff --git a/packages/bun-uws/src/HttpRouter.h b/packages/bun-uws/src/HttpRouter.h index 0215006f84..80bf4cc094 100644 --- a/packages/bun-uws/src/HttpRouter.h +++ b/packages/bun-uws/src/HttpRouter.h @@ -35,43 +35,7 @@ namespace uWS { template struct HttpRouter { - /* These are public for now */ - std::vector upperCasedMethods = { - "ACL", - "BIND", - "CHECKOUT", - "CONNECT", - "COPY", - "DELETE", - "GET", - "HEAD", - "LINK", - "LOCK", - "M-SEARCH" , - "MERGE", - "MKACTIVITY", - "MKCALENDAR", - "MKCOL", - "MOVE", - "NOTIFY", - "OPTIONS", - "PATCH", - "POST", - "PROPFIND", - "PROPPATCH", - "PURGE", - "PUT", - "REBIND", - "REPORT", - "SEARCH", - "SOURCE", - "SUBSCRIBE", - "TRACE", - "UNBIND", - "UNLINK", - "UNLOCK", - "UNSUBSCRIBE", - }; + static constexpr std::string_view ANY_METHOD_TOKEN = "*"; static const uint32_t HIGH_PRIORITY = 0xd0000000, MEDIUM_PRIORITY = 0xe0000000, LOW_PRIORITY = 0xf0000000; private: @@ -81,23 +45,20 @@ private: /* Handler ids are 32-bit */ static const uint32_t HANDLER_MASK = 0x0fffffff; - /* Methods and their respective priority */ - std::map priority; - /* List of handlers */ std::vector> handlers; /* Current URL cache */ - std::string_view currentUrl; - std::string_view urlSegmentVector[MAX_URL_SEGMENTS]; - int urlSegmentTop; + std::string_view currentUrl = {}; + std::string_view urlSegmentVector[MAX_URL_SEGMENTS] = {}; + int urlSegmentTop = -1; /* The matching tree */ struct Node { - std::string name; - std::vector> children; - std::vector handlers; - bool isHighPriority; + std::string name = {}; + std::vector> children = {}; + std::vector handlers = {}; + bool isHighPriority = false; Node(std::string name) : name(name) {} } root = {"rootNode"}; @@ -141,8 +102,8 @@ private: struct RouteParameters { friend struct HttpRouter; private: - std::string_view params[MAX_URL_SEGMENTS]; - int paramsTop; + std::string_view params[MAX_URL_SEGMENTS] = {}; + int paramsTop = -1; void reset() { paramsTop = -1; @@ -173,7 +134,7 @@ private: inline std::pair getUrlSegment(int urlSegment) { if (urlSegment > urlSegmentTop) { /* Signal as STOP when we have no more URL or stack space */ - if (!currentUrl.length() || urlSegment > 99) { + if (!currentUrl.length() || urlSegment > int(MAX_URL_SEGMENTS - 1)) { return {{}, true}; } @@ -280,10 +241,8 @@ private: public: HttpRouter() { - int p = 0; - for (std::string &method : upperCasedMethods) { - priority[method] = p++; - } + /* Always have ANY route */ + getNode(&root, std::string(ANY_METHOD_TOKEN.data(), ANY_METHOD_TOKEN.length()), false); } std::pair getParameters() { @@ -323,14 +282,19 @@ public: void add(std::vector methods, std::string pattern, MoveOnlyFunction &&handler, uint32_t priority = MEDIUM_PRIORITY) { /* First remove existing handler */ remove(methods[0], pattern, priority); - + for (std::string method : methods) { /* Lookup method */ Node *node = getNode(&root, method, false); /* Iterate over all segments */ setUrl(pattern); for (int i = 0; !getUrlSegment(i).second; i++) { - node = getNode(node, std::string(getUrlSegment(i).first), priority == HIGH_PRIORITY); + std::string strippedSegment(getUrlSegment(i).first); + if (strippedSegment.length() && strippedSegment[0] == ':') { + /* Parameter routes must be named only : */ + strippedSegment = ":"; + } + node = getNode(node, strippedSegment, priority == HIGH_PRIORITY); } /* Insert handler in order sorted by priority (most significant 1 byte) */ node->handlers.insert(std::upper_bound(node->handlers.begin(), node->handlers.end(), (uint32_t) (priority | handlers.size())), (uint32_t) (priority | handlers.size())); @@ -339,11 +303,20 @@ public: /* Alloate this handler */ handlers.emplace_back(std::move(handler)); - /* Assume can find this handler again */ - if (((handlers.size() - 1) | priority) != findHandler(methods[0], pattern, priority)) { - std::cerr << "Error: Internal routing error" << std::endl; - std::abort(); - } + /* ANY method must be last, GET must be first */ + std::sort(root.children.begin(), root.children.end(), [](const auto &a, const auto &b) { + if (a->name == "GET" && b->name != "GET") { + return true; + } else if (b->name == "GET" && a->name != "GET") { + return false; + } else if (a->name == ANY_METHOD_TOKEN && b->name != ANY_METHOD_TOKEN) { + return false; + } else if (b->name == ANY_METHOD_TOKEN && a->name != ANY_METHOD_TOKEN) { + return true; + } else { + return a->name < b->name; + } + }); } bool cullNode(Node *parent, Node *node, uint32_t handler) { @@ -408,4 +381,4 @@ public: } -#endif // UWS_HTTPROUTER_HPP +#endif // UWS_HTTPROUTER_HPP \ No newline at end of file diff --git a/src/OutputFile.zig b/src/OutputFile.zig new file mode 100644 index 0000000000..c36f42ae72 --- /dev/null +++ b/src/OutputFile.zig @@ -0,0 +1,539 @@ +// Instead of keeping files in-memory, we: +// 1. Write directly to disk +// 2. (Optional) move the file to the destination +// This saves us from allocating a buffer + +loader: Loader, +input_loader: Loader = .js, +src_path: Fs.Path, +value: Value, +size: usize = 0, +size_without_sourcemap: usize = 0, +hash: u64 = 0, +is_executable: bool = false, +source_map_index: u32 = std.math.maxInt(u32), +bytecode_index: u32 = std.math.maxInt(u32), +output_kind: JSC.API.BuildArtifact.OutputKind, +/// Relative +dest_path: []const u8 = "", +side: ?bun.bake.Side, +/// This is only set for the JS bundle, and not files associated with an +/// entrypoint like sourcemaps and bytecode +entry_point_index: ?u32, +referenced_css_files: []const Index = &.{}, + +pub const Index = bun.GenericIndex(u32, OutputFile); + +pub fn deinit(this: *OutputFile) void { + this.value.deinit(); + + bun.default_allocator.free(this.src_path.text); + bun.default_allocator.free(this.dest_path); + bun.default_allocator.free(this.referenced_css_files); +} + +// Depending on: +// - The target +// - The number of open file handles +// - Whether or not a file of the same name exists +// We may use a different system call +pub const FileOperation = struct { + pathname: string, + fd: FileDescriptorType = bun.invalid_fd, + dir: FileDescriptorType = bun.invalid_fd, + is_tmpdir: bool = false, + is_outdir: bool = false, + close_handle_on_complete: bool = false, + autowatch: bool = true, + + pub fn fromFile(fd: anytype, pathname: string) FileOperation { + return .{ + .pathname = pathname, + .fd = bun.toFD(fd), + }; + } + + pub fn getPathname(file: *const FileOperation) string { + if (file.is_tmpdir) { + return resolve_path.joinAbs(@TypeOf(Fs.FileSystem.instance.fs).tmpdir_path, .auto, file.pathname); + } else { + return file.pathname; + } + } +}; + +pub const Value = union(Kind) { + move: FileOperation, + copy: FileOperation, + noop: u0, + buffer: struct { + allocator: std.mem.Allocator, + bytes: []const u8, + }, + pending: resolver.Result, + saved: SavedFile, + + pub fn deinit(this: *Value) void { + switch (this.*) { + .buffer => |buf| { + buf.allocator.free(buf.bytes); + }, + .saved => {}, + .move => {}, + .copy => {}, + .noop => {}, + .pending => {}, + } + } + + pub fn toBunString(v: Value) bun.String { + return switch (v) { + .noop => bun.String.empty, + .buffer => |buf| { + // Use ExternalStringImpl to avoid cloning the string, at + // the cost of allocating space to remember the allocator. + const FreeContext = struct { + allocator: std.mem.Allocator, + + fn onFree(uncast_ctx: *anyopaque, buffer: *anyopaque, len: u32) callconv(.C) void { + const ctx: *@This() = @alignCast(@ptrCast(uncast_ctx)); + ctx.allocator.free(@as([*]u8, @ptrCast(buffer))[0..len]); + bun.destroy(ctx); + } + }; + return bun.String.createExternal( + buf.bytes, + true, + bun.new(FreeContext, .{ .allocator = buf.allocator }), + FreeContext.onFree, + ); + }, + .pending => unreachable, + else => |tag| bun.todoPanic(@src(), "handle .{s}", .{@tagName(tag)}), + }; + } +}; + +pub const SavedFile = struct { + pub fn toJS( + globalThis: *JSC.JSGlobalObject, + path: []const u8, + byte_size: usize, + ) JSC.JSValue { + const mime_type = globalThis.bunVM().mimeType(path); + const store = JSC.WebCore.Blob.Store.initFile( + JSC.Node.PathOrFileDescriptor{ + .path = JSC.Node.PathLike{ + .string = JSC.PathString.init(path), + }, + }, + mime_type, + bun.default_allocator, + ) catch unreachable; + + var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable; + blob.* = JSC.WebCore.Blob.initWithStore(store, globalThis); + if (mime_type) |mime| { + blob.content_type = mime.value; + } + blob.size = @as(JSC.WebCore.Blob.SizeType, @truncate(byte_size)); + blob.allocator = bun.default_allocator; + return blob.toJS(globalThis); + } +}; + +pub const Kind = enum { move, copy, noop, buffer, pending, saved }; + +pub fn initPending(loader: Loader, pending: resolver.Result) OutputFile { + return .{ + .loader = loader, + .src_path = pending.pathConst().?.*, + .size = 0, + .value = .{ .pending = pending }, + }; +} + +pub fn initFile(file: std.fs.File, pathname: string, size: usize) OutputFile { + return .{ + .loader = .file, + .src_path = Fs.Path.init(pathname), + .size = size, + .value = .{ .copy = FileOperation.fromFile(file.handle, pathname) }, + }; +} + +pub fn initFileWithDir(file: std.fs.File, pathname: string, size: usize, dir: std.fs.Dir) OutputFile { + var res = initFile(file, pathname, size); + res.value.copy.dir_handle = bun.toFD(dir.fd); + return res; +} + +pub const Options = struct { + loader: Loader, + input_loader: Loader, + hash: ?u64 = null, + source_map_index: ?u32 = null, + bytecode_index: ?u32 = null, + output_path: string, + size: ?usize = null, + input_path: []const u8 = "", + display_size: u32 = 0, + output_kind: JSC.API.BuildArtifact.OutputKind, + is_executable: bool, + data: union(enum) { + buffer: struct { + allocator: std.mem.Allocator, + data: []const u8, + }, + file: struct { + file: std.fs.File, + size: usize, + dir: std.fs.Dir, + }, + saved: usize, + }, + side: ?bun.bake.Side, + entry_point_index: ?u32, + referenced_css_files: []const Index = &.{}, +}; + +pub fn init(options: Options) OutputFile { + return .{ + .loader = options.loader, + .input_loader = options.input_loader, + .src_path = Fs.Path.init(options.input_path), + .dest_path = options.output_path, + .size = options.size orelse switch (options.data) { + .buffer => |buf| buf.data.len, + .file => |file| file.size, + .saved => 0, + }, + .size_without_sourcemap = options.display_size, + .hash = options.hash orelse 0, + .output_kind = options.output_kind, + .bytecode_index = options.bytecode_index orelse std.math.maxInt(u32), + .source_map_index = options.source_map_index orelse std.math.maxInt(u32), + .is_executable = options.is_executable, + .value = switch (options.data) { + .buffer => |buffer| Value{ .buffer = .{ .allocator = buffer.allocator, .bytes = buffer.data } }, + .file => |file| Value{ + .copy = brk: { + var op = FileOperation.fromFile(file.file.handle, options.output_path); + op.dir = bun.toFD(file.dir.fd); + break :brk op; + }, + }, + .saved => Value{ .saved = .{} }, + }, + .side = options.side, + .entry_point_index = options.entry_point_index, + .referenced_css_files = options.referenced_css_files, + }; +} + +pub fn writeToDisk(f: OutputFile, root_dir: std.fs.Dir, longest_common_path: []const u8) ![]const u8 { + switch (f.value) { + .saved => { + var rel_path = f.dest_path; + if (f.dest_path.len > longest_common_path.len) { + rel_path = resolve_path.relative(longest_common_path, f.dest_path); + } + return rel_path; + }, + .buffer => |value| { + var rel_path = f.dest_path; + + if (f.dest_path.len > longest_common_path.len) { + rel_path = resolve_path.relative(longest_common_path, f.dest_path); + if (std.fs.path.dirname(rel_path)) |parent| { + if (parent.len > longest_common_path.len) { + try root_dir.makePath(parent); + } + } + } + + var handled_file_not_found = false; + while (true) { + var path_buf: bun.PathBuffer = undefined; + JSC.Node.NodeFS.writeFileWithPathBuffer(&path_buf, .{ + .data = .{ .buffer = .{ + .buffer = .{ + .ptr = @constCast(value.bytes.ptr), + .len = value.bytes.len, + .byte_len = value.bytes.len, + }, + } }, + .encoding = .buffer, + .mode = if (f.is_executable) 0o755 else 0o644, + .dirfd = bun.toFD(root_dir.fd), + .file = .{ .path = .{ + .string = JSC.PathString.init(rel_path), + } }, + }).unwrap() catch |err| switch (err) { + error.FileNotFound, error.ENOENT => { + if (handled_file_not_found) return err; + handled_file_not_found = true; + try root_dir.makePath( + std.fs.path.dirname(rel_path) orelse + return err, + ); + continue; + }, + else => return err, + }; + break; + } + + return rel_path; + }, + .move => |value| { + _ = value; + // var filepath_buf: bun.PathBuffer = undefined; + // filepath_buf[0] = '.'; + // filepath_buf[1] = '/'; + // const primary = f.dest_path[root_dir_path.len..]; + // bun.copy(u8, filepath_buf[2..], primary); + // var rel_path: []const u8 = filepath_buf[0 .. primary.len + 2]; + // rel_path = value.pathname; + + // try f.moveTo(root_path, @constCast(rel_path), bun.toFD(root_dir.fd)); + { + @panic("TODO: Regressed behavior"); + } + + // return primary; + }, + .copy => |value| { + _ = value; + // rel_path = value.pathname; + + // try f.copyTo(root_path, @constCast(rel_path), bun.toFD(root_dir.fd)); + { + @panic("TODO: Regressed behavior"); + } + }, + .noop => { + return f.dest_path; + }, + .pending => unreachable, + } +} + +pub fn moveTo(file: *const OutputFile, _: string, rel_path: []u8, dir: FileDescriptorType) !void { + try bun.C.moveFileZ(file.value.move.dir, bun.sliceTo(&(try std.posix.toPosixPath(file.value.move.getPathname())), 0), dir, bun.sliceTo(&(try std.posix.toPosixPath(rel_path)), 0)); +} + +pub fn copyTo(file: *const OutputFile, _: string, rel_path: []u8, dir: FileDescriptorType) !void { + const file_out = (try dir.asDir().createFile(rel_path, .{})); + + const fd_out = file_out.handle; + var do_close = false; + const fd_in = (try std.fs.openFileAbsolute(file.src_path.text, .{ .mode = .read_only })).handle; + + if (Environment.isWindows) { + Fs.FileSystem.setMaxFd(fd_out); + Fs.FileSystem.setMaxFd(fd_in); + do_close = Fs.FileSystem.instance.fs.needToCloseFiles(); + + // use paths instead of bun.getFdPathW() + @panic("TODO windows"); + } + + defer { + if (do_close) { + _ = bun.sys.close(bun.toFD(fd_out)); + _ = bun.sys.close(bun.toFD(fd_in)); + } + } + + try bun.copyFile(fd_in, fd_out).unwrap(); +} + +pub fn toJS( + this: *OutputFile, + owned_pathname: ?[]const u8, + globalObject: *JSC.JSGlobalObject, +) bun.JSC.JSValue { + return switch (this.value) { + .move, .pending => @panic("Unexpected pending output file"), + .noop => JSC.JSValue.undefined, + .copy => |copy| brk: { + const file_blob = JSC.WebCore.Blob.Store.initFile( + if (copy.fd != .zero) + JSC.Node.PathOrFileDescriptor{ + .fd = copy.fd, + } + else + JSC.Node.PathOrFileDescriptor{ + .path = JSC.Node.PathLike{ .string = bun.PathString.init(globalObject.allocator().dupe(u8, copy.pathname) catch unreachable) }, + }, + this.loader.toMimeType(), + globalObject.allocator(), + ) catch |err| { + Output.panic("error: Unable to create file blob: \"{s}\"", .{@errorName(err)}); + }; + + var build_output = bun.new(JSC.API.BuildArtifact, .{ + .blob = JSC.WebCore.Blob.initWithStore(file_blob, globalObject), + .hash = this.hash, + .loader = this.input_loader, + .output_kind = this.output_kind, + .path = bun.default_allocator.dupe(u8, copy.pathname) catch @panic("Failed to allocate path"), + }); + + this.value = .{ + .buffer = .{ + .allocator = bun.default_allocator, + .bytes = &.{}, + }, + }; + + break :brk build_output.toJS(globalObject); + }, + .saved => brk: { + var build_output = bun.default_allocator.create(JSC.API.BuildArtifact) catch @panic("Unable to allocate Artifact"); + const path_to_use = owned_pathname orelse this.src_path.text; + + const file_blob = JSC.WebCore.Blob.Store.initFile( + JSC.Node.PathOrFileDescriptor{ + .path = JSC.Node.PathLike{ .string = bun.PathString.init(owned_pathname orelse (bun.default_allocator.dupe(u8, this.src_path.text) catch unreachable)) }, + }, + this.loader.toMimeType(), + globalObject.allocator(), + ) catch |err| { + Output.panic("error: Unable to create file blob: \"{s}\"", .{@errorName(err)}); + }; + + this.value = .{ + .buffer = .{ + .allocator = bun.default_allocator, + .bytes = &.{}, + }, + }; + + build_output.* = JSC.API.BuildArtifact{ + .blob = JSC.WebCore.Blob.initWithStore(file_blob, globalObject), + .hash = this.hash, + .loader = this.input_loader, + .output_kind = this.output_kind, + .path = bun.default_allocator.dupe(u8, path_to_use) catch @panic("Failed to allocate path"), + }; + + break :brk build_output.toJS(globalObject); + }, + .buffer => |buffer| brk: { + var blob = JSC.WebCore.Blob.init(@constCast(buffer.bytes), buffer.allocator, globalObject); + if (blob.store) |store| { + store.mime_type = this.loader.toMimeType(); + blob.content_type = store.mime_type.value; + } else { + blob.content_type = this.loader.toMimeType().value; + } + + blob.size = @as(JSC.WebCore.Blob.SizeType, @truncate(buffer.bytes.len)); + + var build_output = bun.default_allocator.create(JSC.API.BuildArtifact) catch @panic("Unable to allocate Artifact"); + build_output.* = JSC.API.BuildArtifact{ + .blob = blob, + .hash = this.hash, + .loader = this.input_loader, + .output_kind = this.output_kind, + .path = owned_pathname orelse bun.default_allocator.dupe(u8, this.src_path.text) catch unreachable, + }; + + this.value = .{ + .buffer = .{ + .allocator = bun.default_allocator, + .bytes = &.{}, + }, + }; + + break :brk build_output.toJS(globalObject); + }, + }; +} + +pub fn toBlob( + this: *OutputFile, + allocator: std.mem.Allocator, + globalThis: *JSC.JSGlobalObject, +) !JSC.WebCore.Blob { + return switch (this.value) { + .move, .pending => @panic("Unexpected pending output file"), + .noop => @panic("Cannot convert noop output file to blob"), + .copy => |copy| brk: { + const file_blob = try JSC.WebCore.Blob.Store.initFile( + if (copy.fd != .zero) + JSC.Node.PathOrFileDescriptor{ + .fd = copy.fd, + } + else + JSC.Node.PathOrFileDescriptor{ + .path = JSC.Node.PathLike{ .string = bun.PathString.init(allocator.dupe(u8, copy.pathname) catch unreachable) }, + }, + this.loader.toMimeType(), + allocator, + ); + + this.value = .{ + .buffer = .{ + .allocator = bun.default_allocator, + .bytes = &.{}, + }, + }; + + break :brk JSC.WebCore.Blob.initWithStore(file_blob, globalThis); + }, + .saved => brk: { + const file_blob = try JSC.WebCore.Blob.Store.initFile( + JSC.Node.PathOrFileDescriptor{ + .path = JSC.Node.PathLike{ .string = bun.PathString.init(allocator.dupe(u8, this.src_path.text) catch unreachable) }, + }, + this.loader.toMimeType(), + allocator, + ); + + this.value = .{ + .buffer = .{ + .allocator = bun.default_allocator, + .bytes = &.{}, + }, + }; + + break :brk JSC.WebCore.Blob.initWithStore(file_blob, globalThis); + }, + .buffer => |buffer| brk: { + var blob = JSC.WebCore.Blob.init(@constCast(buffer.bytes), buffer.allocator, globalThis); + if (blob.store) |store| { + store.mime_type = this.loader.toMimeType(); + blob.content_type = store.mime_type.value; + } else { + blob.content_type = this.loader.toMimeType().value; + } + + this.value = .{ + .buffer = .{ + .allocator = bun.default_allocator, + .bytes = &.{}, + }, + }; + + blob.size = @as(JSC.WebCore.Blob.SizeType, @truncate(buffer.bytes.len)); + break :brk blob; + }, + }; +} + +const OutputFile = @This(); +const string = []const u8; +const FileDescriptorType = bun.FileDescriptor; + +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; +const Fs = bun.fs; +const Loader = @import("./options.zig").Loader; +const resolver = @import("./resolver/resolver.zig"); +const resolve_path = @import("./resolver/resolve_path.zig"); +const Output = @import("./global.zig").Output; +const Environment = bun.Environment; diff --git a/src/allocators/memory_allocator.zig b/src/allocators/memory_allocator.zig index 07b84ef7b3..244f15544b 100644 --- a/src/allocators/memory_allocator.zig +++ b/src/allocators/memory_allocator.zig @@ -86,10 +86,11 @@ const CAllocator = struct { }; pub const c_allocator = Allocator{ - .ptr = undefined, - .vtable = &c_allocator_vtable, + // This ptr can be anything. But since it's not nullable, we should set it to something. + .ptr = @constCast(c_allocator_vtable), + .vtable = c_allocator_vtable, }; -const c_allocator_vtable = Allocator.VTable{ +const c_allocator_vtable = &Allocator.VTable{ .alloc = &CAllocator.alloc, .resize = &CAllocator.resize, .free = &CAllocator.free, diff --git a/src/bun.js/api/server.classes.ts b/src/bun.js/api/server.classes.ts index 6b9f205693..34b08171dd 100644 --- a/src/bun.js/api/server.classes.ts +++ b/src/bun.js/api/server.classes.ts @@ -3,6 +3,7 @@ import { define } from "../../codegen/class-definitions"; function generate(name) { return define({ name, + memoryCost: true, proto: { fetch: { fn: "doFetch", @@ -205,4 +206,17 @@ export default [ construct: true, klass: {}, }), + + define({ + name: "HTMLBundle", + noConstructor: true, + finalize: true, + proto: { + index: { + getter: "getIndex", + cache: true, + }, + }, + klass: {}, + }), ]; diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index a841498d3e..7530e95d9c 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -164,7 +164,7 @@ fn writeHeaders( } } -fn writeStatus(comptime ssl: bool, resp_ptr: ?*uws.NewApp(ssl).Response, status: u16) void { +pub fn writeStatus(comptime ssl: bool, resp_ptr: ?*uws.NewApp(ssl).Response, status: u16) void { if (resp_ptr) |resp| { if (HTTPStatusText.get(status)) |text| { resp.writeStatus(text); @@ -175,242 +175,54 @@ fn writeStatus(comptime ssl: bool, resp_ptr: ?*uws.NewApp(ssl).Response, status: } } -const StaticRoute = struct { - server: ?AnyServer = null, - status_code: u16, - blob: AnyBlob, - cached_blob_size: u64 = 0, - has_content_disposition: bool = false, - headers: Headers = .{ - .allocator = bun.default_allocator, - }, - ref_count: u32 = 1, +const StaticRoute = @import("./server/StaticRoute.zig"); +const HTMLBundle = JSC.API.HTMLBundle; +const HTMLBundleRoute = HTMLBundle.HTMLBundleRoute; +pub const AnyStaticRoute = union(enum) { + StaticRoute: *StaticRoute, + HTMLBundleRoute: *HTMLBundleRoute, - const HTTPResponse = uws.AnyResponse; - const Route = @This(); - - pub usingnamespace bun.NewRefCounted(@This(), deinit); - - fn deinit(this: *Route) void { - this.blob.detach(); - this.headers.deinit(); - - this.destroy(); + pub fn memoryCost(this: AnyStaticRoute) usize { + return switch (this) { + .StaticRoute => |static_route| static_route.memoryCost(), + .HTMLBundleRoute => |html_bundle_route| html_bundle_route.memoryCost(), + }; } - pub fn fromJS(globalThis: *JSC.JSGlobalObject, argument: JSC.JSValue) bun.JSError!*Route { - if (argument.as(JSC.WebCore.Response)) |response| { + pub fn setServer(this: AnyStaticRoute, server: ?AnyServer) void { + switch (this) { + .StaticRoute => |static_route| static_route.server = server, + .HTMLBundleRoute => |html_bundle_route| html_bundle_route.server = server, + } + } - // The user may want to pass in the same Response object multiple endpoints - // Let's let them do that. - response.body.value.toBlobIfPossible(); + pub fn deref(this: AnyStaticRoute) void { + switch (this) { + .StaticRoute => |static_route| static_route.deref(), + .HTMLBundleRoute => |html_bundle_route| html_bundle_route.deref(), + } + } - var blob: AnyBlob = brk: { - switch (response.body.value) { - .Used => { - return globalThis.throwInvalidArguments("Response body has already been used", .{}); - }, + pub fn ref(this: AnyStaticRoute) void { + switch (this) { + .StaticRoute => |static_route| static_route.ref(), + .HTMLBundleRoute => |html_bundle_route| html_bundle_route.ref(), + } + } - else => { - return globalThis.throwInvalidArguments("Body must be fully buffered before it can be used in a static route. Consider calling new Response(await response.blob()) to buffer the body.", .{}); - }, - .Null, .Empty => { - break :brk AnyBlob{ - .InternalBlob = JSC.WebCore.InternalBlob{ - .bytes = std.ArrayList(u8).init(bun.default_allocator), - }, - }; - }, - - .Blob, .InternalBlob, .WTFStringImpl => { - if (response.body.value == .Blob and response.body.value.Blob.needsToReadFile()) { - return globalThis.throwTODO("TODO: support Bun.file(path) in static routes"); - } - var blob = response.body.value.use(); - blob.globalThis = globalThis; - blob.allocator = null; - response.body.value = .{ .Blob = blob.dupe() }; - - break :brk .{ .Blob = blob }; - }, - } - }; - - var has_content_disposition = false; - - if (response.init.headers) |headers| { - has_content_disposition = headers.fastHas(.ContentDisposition); - headers.fastRemove(.TransferEncoding); - headers.fastRemove(.ContentLength); + pub fn fromJS(globalThis: *JSC.JSGlobalObject, argument: JSC.JSValue, dedupe_html_bundle_map: *std.AutoHashMap(*HTMLBundle, *HTMLBundleRoute)) bun.JSError!AnyStaticRoute { + if (argument.as(HTMLBundle)) |html_bundle| { + const entry = try dedupe_html_bundle_map.getOrPut(html_bundle); + if (!entry.found_existing) { + entry.value_ptr.* = HTMLBundleRoute.init(html_bundle); + } else { + entry.value_ptr.*.ref(); } - const headers: Headers = if (response.init.headers) |headers| - Headers.from(headers, bun.default_allocator, .{ - .body = &blob, - }) catch { - blob.detach(); - globalThis.throwOutOfMemory(); - return error.JSError; - } - else - .{ - .allocator = bun.default_allocator, - }; - - return Route.new(.{ - .blob = blob, - .cached_blob_size = blob.size(), - .has_content_disposition = has_content_disposition, - .headers = headers, - .server = null, - .status_code = response.statusCode(), - }); + return .{ .HTMLBundleRoute = entry.value_ptr.* }; } - return globalThis.throwInvalidArguments("Expected a Response object", .{}); - } - - // HEAD requests have no body. - pub fn onHEADRequest(this: *Route, req: *uws.Request, resp: HTTPResponse) void { - req.setYield(false); - this.ref(); - if (this.server) |server| { - server.onPendingRequest(); - resp.timeout(server.config().idleTimeout); - } - resp.corked(renderMetadataAndEnd, .{ this, resp }); - this.onResponseComplete(resp); - } - - fn renderMetadataAndEnd(this: *Route, resp: HTTPResponse) void { - this.renderMetadata(resp); - resp.writeHeaderInt("Content-Length", this.cached_blob_size); - resp.endWithoutBody(resp.shouldCloseConnection()); - } - - pub fn onRequest(this: *Route, req: *uws.Request, resp: HTTPResponse) void { - req.setYield(false); - this.ref(); - if (this.server) |server| { - server.onPendingRequest(); - resp.timeout(server.config().idleTimeout); - } - var finished = false; - this.doRenderBlob(resp, &finished); - if (finished) { - this.onResponseComplete(resp); - return; - } - - this.toAsync(resp); - } - - fn toAsync(this: *Route, resp: HTTPResponse) void { - resp.onAborted(*Route, onAborted, this); - resp.onWritable(*Route, onWritableBytes, this); - } - - fn onAborted(this: *Route, resp: HTTPResponse) void { - this.onResponseComplete(resp); - } - - fn onResponseComplete(this: *Route, resp: HTTPResponse) void { - resp.clearAborted(); - resp.clearOnWritable(); - resp.clearTimeout(); - - if (this.server) |server| { - server.onStaticRequestComplete(); - } - - this.deref(); - } - - pub fn doRenderBlob(this: *Route, resp: HTTPResponse, did_finish: *bool) void { - // We are not corked - // The body is small - // Faster to do the memcpy than to do the two network calls - // We are not streaming - // This is an important performance optimization - if (this.blob.fastSize() < 16384 - 1024) { - resp.corked(doRenderBlobCorked, .{ this, resp, did_finish }); - } else { - this.doRenderBlobCorked(resp, did_finish); - } - } - - pub fn doRenderBlobCorked(this: *Route, resp: HTTPResponse, did_finish: *bool) void { - this.renderMetadata(resp); - this.renderBytes(resp, did_finish); - } - - fn onWritable(this: *Route, write_offset: u64, resp: HTTPResponse) void { - if (this.server) |server| { - resp.timeout(server.config().idleTimeout); - } - - if (!this.onWritableBytes(write_offset, resp)) { - this.toAsync(resp); - return; - } - - this.onResponseComplete(resp); - } - - fn onWritableBytes(this: *Route, write_offset: u64, resp: HTTPResponse) bool { - const blob = this.blob; - const all_bytes = blob.slice(); - - const bytes = all_bytes[@min(all_bytes.len, @as(usize, @truncate(write_offset)))..]; - - if (!resp.tryEnd( - bytes, - all_bytes.len, - resp.shouldCloseConnection(), - )) { - return false; - } - - return true; - } - - fn doWriteStatus(_: *StaticRoute, status: u16, resp: HTTPResponse) void { - switch (resp) { - .SSL => |r| writeStatus(true, r, status), - .TCP => |r| writeStatus(false, r, status), - } - } - - fn doWriteHeaders(this: *StaticRoute, resp: HTTPResponse) void { - switch (resp) { - inline .SSL, .TCP => |s| { - const entries = this.headers.entries.slice(); - const names: []const Api.StringPointer = entries.items(.name); - const values: []const Api.StringPointer = entries.items(.value); - const buf = this.headers.buf.items; - - for (names, values) |name, value| { - s.writeHeader(name.slice(buf), value.slice(buf)); - } - }, - } - } - - fn renderBytes(this: *Route, resp: HTTPResponse, did_finish: *bool) void { - did_finish.* = this.onWritableBytes(0, resp); - } - - fn renderMetadata(this: *Route, resp: HTTPResponse) void { - var status = this.status_code; - const size = this.cached_blob_size; - - status = if (status == 200 and size == 0 and !this.blob.isDetached()) - 204 - else - status; - - this.doWriteStatus(status, resp); - this.doWriteHeaders(resp); + return .{ .StaticRoute = try StaticRoute.fromJS(globalThis, argument) }; } }; @@ -463,36 +275,120 @@ pub const ServerConfig = struct { bake: ?bun.bake.UserOptions = null, + pub fn memoryCost(this: *const ServerConfig) usize { + // ignore @sizeOf(ServerConfig), assume already included. + var cost: usize = 0; + for (this.static_routes.items) |*entry| { + cost += entry.memoryCost(); + } + cost += this.id.len; + cost += this.base_url.href.len; + return cost; + } pub const StaticRouteEntry = struct { path: []const u8, - route: *StaticRoute, + route: AnyStaticRoute, + + pub fn memoryCost(this: *const StaticRouteEntry) usize { + return this.path.len + this.route.memoryCost(); + } + + /// Clone the path buffer and increment the ref count + /// This doesn't actually clone the route, it just increments the ref count + pub fn clone(this: StaticRouteEntry) !StaticRouteEntry { + this.route.ref(); + + return .{ + .path = try bun.default_allocator.dupe(u8, this.path), + .route = this.route, + }; + } pub fn deinit(this: *StaticRouteEntry) void { bun.default_allocator.free(this.path); this.route.deref(); } + + pub fn isLessThan(_: void, this: StaticRouteEntry, other: StaticRouteEntry) bool { + return strings.cmpStringsDesc({}, this.path, other.path); + } }; - pub fn applyStaticRoutes(this: *ServerConfig, comptime ssl: bool, server: AnyServer, app: *uws.NewApp(ssl)) void { - for (this.static_routes.items) |entry| { - entry.route.server = server; - const handler_wrap = struct { - pub fn handler(route: *StaticRoute, req: *uws.Request, resp: *uws.NewApp(ssl).Response) void { - route.onRequest(req, switch (comptime ssl) { - true => .{ .SSL = resp }, - false => .{ .TCP = resp }, - }); - } + pub fn cloneForReloadingStaticRoutes(this: *ServerConfig) !ServerConfig { + var that = this.*; + this.ssl_config = null; + this.sni = null; + this.address = .{ .tcp = .{} }; + this.websocket = null; + this.bake = null; - pub fn HEAD(route: *StaticRoute, req: *uws.Request, resp: *uws.NewApp(ssl).Response) void { - route.onHEADRequest(req, switch (comptime ssl) { - true => .{ .SSL = resp }, - false => .{ .TCP = resp }, - }); + var static_routes_dedupe_list = bun.StringHashMap(void).init(bun.default_allocator); + try static_routes_dedupe_list.ensureTotalCapacity(@truncate(this.static_routes.items.len)); + defer static_routes_dedupe_list.deinit(); + + // Iterate through the list of static routes backwards + // Later ones added override earlier ones + var static_routes = this.static_routes; + this.static_routes = std.ArrayList(StaticRouteEntry).init(bun.default_allocator); + if (static_routes.items.len > 0) { + var index = static_routes.items.len - 1; + while (true) { + const route = &static_routes.items[index]; + const entry = static_routes_dedupe_list.getOrPut(route.path) catch unreachable; + if (entry.found_existing) { + var item = static_routes.orderedRemove(index); + item.deinit(); } - }; - app.head(entry.path, *StaticRoute, entry.route, handler_wrap.HEAD); - app.any(entry.path, *StaticRoute, entry.route, handler_wrap.handler); + if (index == 0) break; + index -= 1; + } + } + + // sort the cloned static routes by name for determinism + std.mem.sort(StaticRouteEntry, static_routes.items, {}, StaticRouteEntry.isLessThan); + + that.static_routes = static_routes; + return that; + } + + pub fn appendStaticRoute(this: *ServerConfig, path: []const u8, route: AnyStaticRoute) !void { + try this.static_routes.append(StaticRouteEntry{ + .path = try bun.default_allocator.dupe(u8, path), + .route = route, + }); + } + + fn applyStaticRoute(server: AnyServer, comptime ssl: bool, app: *uws.NewApp(ssl), comptime T: type, entry: T, path: []const u8) void { + entry.server = server; + const handler_wrap = struct { + pub fn handler(route: T, req: *uws.Request, resp: *uws.NewApp(ssl).Response) void { + route.onRequest(req, switch (comptime ssl) { + true => .{ .SSL = resp }, + false => .{ .TCP = resp }, + }); + } + + pub fn HEAD(route: T, req: *uws.Request, resp: *uws.NewApp(ssl).Response) void { + route.onHEADRequest(req, switch (comptime ssl) { + true => .{ .SSL = resp }, + false => .{ .TCP = resp }, + }); + } + }; + app.head(path, T, entry, handler_wrap.HEAD); + app.any(path, T, entry, handler_wrap.handler); + } + + pub fn applyStaticRoutes(this: *ServerConfig, comptime ssl: bool, server: AnyServer, app: *uws.NewApp(ssl)) void { + for (this.static_routes.items) |*entry| { + switch (entry.route) { + .StaticRoute => |static_route| { + applyStaticRoute(server, ssl, app, *StaticRoute, static_route, entry.path); + }, + .HTMLBundleRoute => |html_bundle_route| { + applyStaticRoute(server, ssl, app, *HTMLBundleRoute, html_bundle_route, entry.path); + }, + } } } @@ -1227,6 +1123,16 @@ pub const ServerConfig = struct { }).init(global, static); defer iter.deinit(); + var dedupe_html_bundle_map = std.AutoHashMap(*HTMLBundle, *HTMLBundleRoute).init(bun.default_allocator); + defer dedupe_html_bundle_map.deinit(); + + errdefer { + for (args.static_routes.items) |*static_route| { + static_route.deinit(); + } + args.static_routes.clearAndFree(); + } + while (try iter.next()) |key| { const path, const is_ascii = key.toOwnedSliceReturningAllASCII(bun.default_allocator) catch bun.outOfMemory(); @@ -1242,7 +1148,7 @@ pub const ServerConfig = struct { return global.throwInvalidArguments("Invalid static route \"{s}\". Please encode all non-ASCII characters in the path.", .{path}); } - const route = try StaticRoute.fromJS(global, value); + const route = try AnyStaticRoute.fromJS(global, value, &dedupe_html_bundle_map); args.static_routes.append(.{ .path = path, .route = route, @@ -5972,6 +5878,12 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp JSValue.jsNull(); } + pub fn memoryCost(this: *ThisServer) usize { + return @sizeOf(ThisServer) + + this.base_url_string_for_joining.len + + this.config.memoryCost(); + } + pub fn timeout(this: *ThisServer, request: *JSC.WebCore.Request, seconds: JSValue) bun.JSError!JSC.JSValue { if (!seconds.isNumber()) { return this.globalThis.throw("timeout() requires a number", .{}); @@ -5985,6 +5897,10 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp this.config.idleTimeout = @truncate(@min(seconds, 255)); } + pub fn appendStaticRoute(this: *ThisServer, path: []const u8, route: AnyStaticRoute) !void { + try this.config.appendStaticRoute(path, route); + } + pub fn publish(this: *ThisServer, globalThis: *JSC.JSGlobalObject, topic: ZigString, message_value: JSValue, compress_value: ?JSValue) bun.JSError!JSValue { if (this.config.websocket == null) return JSValue.jsNumber(0); @@ -6255,6 +6171,17 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp this.setRoutes(); } + pub fn reloadStaticRoutes(this: *ThisServer) !bool { + if (this.app == null) { + // Static routes will get cleaned up when the server is stopped + return false; + } + this.config = try this.config.cloneForReloadingStaticRoutes(); + this.app.?.clearRoutes(); + this.setRoutes(); + return true; + } + pub fn onReload(this: *ThisServer, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(1).slice(); if (arguments.len < 1) { @@ -7506,6 +7433,24 @@ pub const AnyServer = union(enum) { DebugHTTPServer: *DebugHTTPServer, DebugHTTPSServer: *DebugHTTPSServer, + pub fn reloadStaticRoutes(this: AnyServer) !bool { + return switch (this) { + inline else => |server| server.reloadStaticRoutes(), + }; + } + + pub fn appendStaticRoute(this: AnyServer, path: []const u8, route: AnyStaticRoute) !void { + return switch (this) { + inline else => |server| server.appendStaticRoute(path, route), + }; + } + + pub fn globalThis(this: AnyServer) *JSC.JSGlobalObject { + return switch (this) { + inline else => |server| server.globalThis, + }; + } + pub fn config(this: AnyServer) *const ServerConfig { return switch (this) { inline else => |server| &server.config, diff --git a/src/bun.js/api/server/HTMLBundle.zig b/src/bun.js/api/server/HTMLBundle.zig new file mode 100644 index 0000000000..860357f124 --- /dev/null +++ b/src/bun.js/api/server/HTMLBundle.zig @@ -0,0 +1,442 @@ +// This is a description of what the build will be. +// It doesn't do the build. + +ref_count: u32 = 1, +globalObject: *JSGlobalObject, +path: []const u8, +config: bun.JSC.API.JSBundler.Config, +plugins: ?*bun.JSC.API.JSBundler.Plugin, + +pub fn init(globalObject: *JSGlobalObject, path: []const u8) !*HTMLBundle { + var config = bun.JSC.API.JSBundler.Config{}; + try config.entry_points.insert(path); + config.experimental.html = true; + config.experimental.css = true; + config.target = .browser; + try config.public_path.appendChar('/'); + return HTMLBundle.new(.{ + .globalObject = globalObject, + .path = try bun.default_allocator.dupe(u8, path), + .config = config, + .plugins = null, + }); +} + +pub fn finalize(this: *HTMLBundle) void { + this.deref(); +} + +pub fn deinit(this: *HTMLBundle) void { + bun.default_allocator.free(this.path); + this.config.deinit(bun.default_allocator); + if (this.plugins) |plugin| { + plugin.deinit(); + } + this.destroy(); +} + +pub fn getIndex(this: *HTMLBundle, globalObject: *JSGlobalObject) JSValue { + var str = bun.String.createUTF8(this.path); + return str.transferToJS(globalObject); +} + +pub const HTMLBundleRoute = struct { + html_bundle: *HTMLBundle, + pending_responses: std.ArrayListUnmanaged(*PendingResponse) = .{}, + ref_count: u32 = 1, + server: ?AnyServer = null, + value: Value = .pending, + + pub fn memoryCost(this: *const HTMLBundleRoute) usize { + var cost: usize = 0; + cost += @sizeOf(HTMLBundleRoute); + cost += this.pending_responses.items.len * @sizeOf(PendingResponse); + cost += this.value.memoryCost(); + return cost; + } + + pub fn init(html_bundle: *HTMLBundle) *HTMLBundleRoute { + return HTMLBundleRoute.new(.{ + .html_bundle = html_bundle, + .pending_responses = .{}, + .ref_count = 1, + .server = null, + .value = .pending, + }); + } + + pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); + + pub const Value = union(enum) { + pending: void, + building: *bun.BundleV2.JSBundleCompletionTask, + err: bun.logger.Log, + html: *StaticRoute, + + pub fn deinit(this: *Value) void { + switch (this.*) { + .err => |*log| { + log.deinit(); + }, + .building => |completion| { + completion.cancelled = true; + completion.deref(); + }, + .html => { + this.html.deref(); + }, + .pending => {}, + } + } + + pub fn memoryCost(this: *const Value) usize { + return switch (this.*) { + .pending => 0, + .building => 0, + .err => |log| log.memoryCost(), + .html => |html| html.memoryCost(), + }; + } + }; + + pub fn deinit(this: *HTMLBundleRoute) void { + for (this.pending_responses.items) |pending_response| { + pending_response.deref(); + } + this.pending_responses.deinit(bun.default_allocator); + this.html_bundle.deref(); + this.value.deinit(); + this.destroy(); + } + + pub fn onRequest(this: *HTMLBundleRoute, req: *uws.Request, resp: HTTPResponse) void { + this.onAnyRequest(req, resp, false); + } + + pub fn onHEADRequest(this: *HTMLBundleRoute, req: *uws.Request, resp: HTTPResponse) void { + this.onAnyRequest(req, resp, true); + } + + fn onAnyRequest(this: *HTMLBundleRoute, req: *uws.Request, resp: HTTPResponse, is_head: bool) void { + this.ref(); + defer this.deref(); + const server: AnyServer = this.server orelse { + resp.endWithoutBody(true); + return; + }; + + if (server.config().development) { + // TODO: actually implement proper watch mode instead of "rebuild on every request" + if (this.value == .html) { + this.value.html.deref(); + this.value = .pending; + } else if (this.value == .err) { + this.value.err.deinit(); + this.value = .pending; + } + } + + if (this.value == .pending) { + if (bun.Environment.enable_logs) + debug("onRequest: {s} - pending", .{req.url()}); + + const globalThis = server.globalThis(); + + const vm = globalThis.bunVM(); + + var config = this.html_bundle.config; + config.entry_points = config.entry_points.clone() catch bun.outOfMemory(); + config.public_path = config.public_path.clone() catch bun.outOfMemory(); + config.define = config.define.clone() catch bun.outOfMemory(); + if (!server.config().development) { + config.minify.syntax = true; + config.minify.whitespace = true; + config.minify.identifiers = true; + config.define.put("process.env.NODE_ENV", "\"production\"") catch bun.outOfMemory(); + } + config.source_map = .linked; + + const completion_task = bun.BundleV2.createAndScheduleCompletionTask( + config, + this.html_bundle.plugins, + globalThis, + vm.eventLoop(), + bun.default_allocator, + ) catch { + resp.endWithoutBody(true); + bun.outOfMemory(); + return; + }; + completion_task.started_at_ns = bun.getRoughTickCount().ns(); + completion_task.html_build_task = this; + this.value = .{ .building = completion_task }; + + // While we're building, ensure this doesn't get freed. + this.ref(); + } + + switch (this.value) { + .pending => unreachable, + .building => { + if (bun.Environment.enable_logs) + debug("onRequest: {s} - building", .{req.url()}); + // create the PendingResponse, add it to the list + var pending = PendingResponse.new(.{ + .method = bun.http.Method.which(req.method()) orelse { + resp.writeStatus("405 Method Not Allowed"); + resp.endWithoutBody(true); + return; + }, + .resp = resp, + .server = this.server, + .route = this, + .ref_count = 1, + }); + + this.pending_responses.append(bun.default_allocator, pending) catch { + pending.deref(); + resp.endWithoutBody(true); + bun.outOfMemory(); + return; + }; + + this.ref(); + pending.ref(); + resp.onAborted(*PendingResponse, PendingResponse.onAborted, pending); + req.setYield(false); + }, + .err => |log| { + if (bun.Environment.enable_logs) + debug("onRequest: {s} - err", .{req.url()}); + _ = log; // autofix + // use the code from server.zig to render the error + resp.endWithoutBody(true); + }, + .html => |html| { + if (bun.Environment.enable_logs) + debug("onRequest: {s} - html", .{req.url()}); + // we already have the html, so we can just serve it + if (is_head) { + html.onHEADRequest(req, resp); + } else { + html.onRequest(req, resp); + } + }, + } + } + + pub fn onComplete(this: *HTMLBundleRoute, completion_task: *bun.BundleV2.JSBundleCompletionTask) void { + // To ensure it stays alive for the deuration of this function. + this.ref(); + defer this.deref(); + + // For the build task. + defer this.deref(); + + switch (completion_task.result) { + .err => |err| { + if (bun.Environment.enable_logs) + debug("onComplete: err - {s}", .{@errorName(err)}); + this.value = .{ .err = bun.logger.Log.init(bun.default_allocator) }; + completion_task.log.cloneToWithRecycled(&this.value.err, true) catch bun.outOfMemory(); + + if (this.server) |server| { + if (server.config().development) { + switch (bun.Output.enable_ansi_colors_stderr) { + inline else => |enable_ansi_colors| { + var writer = bun.Output.errorWriterBuffered(); + this.value.err.printWithEnableAnsiColors(&writer, enable_ansi_colors) catch {}; + writer.context.flush() catch {}; + }, + } + } + } + }, + .value => |bundle| { + if (bun.Environment.enable_logs) + debug("onComplete: success", .{}); + // Find the HTML entry point and create static routes + const server: AnyServer = this.server orelse return; + const globalThis = server.globalThis(); + const output_files = bundle.output_files.items; + + if (server.config().development) { + const now = bun.getRoughTickCount().ns(); + const duration = now - completion_task.started_at_ns; + var duration_f64: f64 = @floatFromInt(duration); + duration_f64 /= std.time.ns_per_s; + + bun.Output.printElapsed(duration_f64); + var byte_length: u64 = 0; + for (output_files) |*output_file| { + byte_length += output_file.size_without_sourcemap; + } + + bun.Output.prettyErrorln(" bundle {s} {d:.2} KB", .{ std.fs.path.basename(this.html_bundle.path), @as(f64, @floatFromInt(byte_length)) / 1000.0 }); + bun.Output.flush(); + } + + var this_html_route: ?*StaticRoute = null; + + // Create static routes for each output file + for (output_files) |*output_file| { + const blob = JSC.WebCore.AnyBlob{ .Blob = output_file.toBlob(bun.default_allocator, globalThis) catch bun.outOfMemory() }; + var headers = JSC.WebCore.Headers{ .allocator = bun.default_allocator }; + headers.append("Content-Type", blob.Blob.contentTypeOrMimeType() orelse output_file.loader.toMimeType().value) catch bun.outOfMemory(); + // Do not apply etags to html. + if (output_file.loader != .html and output_file.value == .buffer) { + var hashbuf: [64]u8 = undefined; + const etag_str = std.fmt.bufPrint(&hashbuf, "{}", .{bun.fmt.hexIntLower(output_file.hash)}) catch bun.outOfMemory(); + headers.append("ETag", etag_str) catch bun.outOfMemory(); + if (!server.config().development and (output_file.output_kind == .chunk)) + headers.append("Cache-Control", "public, max-age=31536000") catch bun.outOfMemory(); + } + + // Add a SourceMap header if we have a source map index + // and it's in development mode. + if (server.config().development) { + if (output_file.source_map_index != std.math.maxInt(u32)) { + var route_path = output_files[output_file.source_map_index].dest_path; + if (strings.hasPrefixComptime(route_path, "./") or strings.hasPrefixComptime(route_path, ".\\")) { + route_path = route_path[1..]; + } + headers.append("SourceMap", route_path) catch bun.outOfMemory(); + } + } + + const static_route = StaticRoute.new(.{ + .blob = blob, + .server = server, + .status_code = 200, + .headers = headers, + .cached_blob_size = blob.size(), + }); + + if (this_html_route == null and output_file.output_kind == .@"entry-point") { + if (output_file.loader == .html) { + this_html_route = static_route; + } + } + + var route_path = output_file.dest_path; + + // The route path gets cloned inside of appendStaticRoute. + if (strings.hasPrefixComptime(route_path, "./") or strings.hasPrefixComptime(route_path, ".\\")) { + route_path = route_path[1..]; + } + + server.appendStaticRoute(route_path, .{ .StaticRoute = static_route }) catch bun.outOfMemory(); + } + + const html_route: *StaticRoute = this_html_route orelse @panic("Internal assertion failure: HTML entry point not found in HTMLBundle."); + const html_route_clone = html_route.clone(globalThis) catch bun.outOfMemory(); + this.value = .{ .html = html_route_clone }; + + if (!(server.reloadStaticRoutes() catch bun.outOfMemory())) { + // Server has shutdown, so it won't receive any new requests + // TODO: handle this case + } + }, + .pending => unreachable, + } + + // Handle pending responses + var pending = this.pending_responses; + defer pending.deinit(bun.default_allocator); + this.pending_responses = .{}; + for (pending.items) |pending_response| { + // for the list of pending responses + defer pending_response.deref(); + + const resp = pending_response.resp; + const method = pending_response.method; + + if (!pending_response.is_response_pending) { + // request already aborted + continue; + } + + pending_response.is_response_pending = false; + resp.clearAborted(); + + switch (this.value) { + .html => |html| { + if (method == .HEAD) { + html.onHEAD(resp); + } else { + html.on(resp); + } + }, + .err => |log| { + _ = log; // autofix + resp.writeStatus("500 Build Failed"); + resp.endWithoutBody(false); + }, + else => { + resp.endWithoutBody(false); + }, + } + + // for the HTTP response. + pending_response.deref(); + } + } + + // Represents an in-flight response before the bundle has finished building. + pub const PendingResponse = struct { + method: bun.http.Method, + resp: HTTPResponse, + ref_count: u32 = 1, + is_response_pending: bool = true, + server: ?AnyServer = null, + route: *HTMLBundleRoute, + + pub usingnamespace bun.NewRefCounted(@This(), @This().deinit); + + pub fn deinit(this: *PendingResponse) void { + if (this.is_response_pending) { + this.resp.clearAborted(); + this.resp.clearOnWritable(); + this.resp.endWithoutBody(true); + } + this.route.deref(); + this.destroy(); + } + + pub fn onAborted(this: *PendingResponse, resp: HTTPResponse) void { + _ = resp; // autofix + bun.debugAssert(this.is_response_pending == true); + this.is_response_pending = false; + + // Technically, this could be the final ref count, but we don't want to risk it + this.route.ref(); + defer this.route.deref(); + + while (std.mem.indexOfScalar(*PendingResponse, this.route.pending_responses.items, this)) |index| { + _ = this.route.pending_responses.orderedRemove(index); + this.route.deref(); + } + + this.deref(); + } + }; +}; + +pub usingnamespace JSC.Codegen.JSHTMLBundle; +pub usingnamespace bun.NewRefCounted(HTMLBundle, deinit); +const bun = @import("root").bun; +const std = @import("std"); +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const JSString = JSC.JSString; +const JSValueRef = JSC.JSValueRef; +const HTMLBundle = @This(); +const JSBundler = JSC.API.JSBundler; +const HTTPResponse = bun.uws.AnyResponse; +const uws = bun.uws; +const AnyServer = JSC.API.AnyServer; +const StaticRoute = @import("./StaticRoute.zig"); + +const debug = bun.Output.scoped(.HTMLBundle, true); +const strings = bun.strings; diff --git a/src/bun.js/api/server/StaticRoute.zig b/src/bun.js/api/server/StaticRoute.zig new file mode 100644 index 0000000000..6c4d7eccd8 --- /dev/null +++ b/src/bun.js/api/server/StaticRoute.zig @@ -0,0 +1,274 @@ +const std = @import("std"); + +server: ?AnyServer = null, +status_code: u16, +blob: AnyBlob, +cached_blob_size: u64 = 0, +has_content_disposition: bool = false, +headers: Headers = .{ + .allocator = bun.default_allocator, +}, +ref_count: u32 = 1, + +pub usingnamespace bun.NewRefCounted(@This(), deinit); + +fn deinit(this: *StaticRoute) void { + this.blob.detach(); + this.headers.deinit(); + + this.destroy(); +} + +pub fn clone(this: *StaticRoute, globalThis: *JSC.JSGlobalObject) !*StaticRoute { + var blob = this.blob.toBlob(globalThis); + this.blob = .{ .Blob = blob }; + + return StaticRoute.new(.{ + .blob = .{ .Blob = blob.dupe() }, + .cached_blob_size = this.cached_blob_size, + .has_content_disposition = this.has_content_disposition, + .headers = try this.headers.clone(), + .server = this.server, + .status_code = this.status_code, + }); +} + +pub fn memoryCost(this: *const StaticRoute) usize { + return @sizeOf(StaticRoute) + this.blob.memoryCost() + this.headers.memoryCost(); +} + +pub fn fromJS(globalThis: *JSC.JSGlobalObject, argument: JSC.JSValue) bun.JSError!*StaticRoute { + if (argument.as(JSC.WebCore.Response)) |response| { + + // The user may want to pass in the same Response object multiple endpoints + // Let's let them do that. + response.body.value.toBlobIfPossible(); + + var blob: AnyBlob = brk: { + switch (response.body.value) { + .Used => { + return globalThis.throwInvalidArguments("Response body has already been used", .{}); + }, + + else => { + return globalThis.throwInvalidArguments("Body must be fully buffered before it can be used in a static route. Consider calling new Response(await response.blob()) to buffer the body.", .{}); + }, + .Null, .Empty => { + break :brk AnyBlob{ + .InternalBlob = JSC.WebCore.InternalBlob{ + .bytes = std.ArrayList(u8).init(bun.default_allocator), + }, + }; + }, + + .Blob, .InternalBlob, .WTFStringImpl => { + if (response.body.value == .Blob and response.body.value.Blob.needsToReadFile()) { + return globalThis.throwTODO("TODO: support Bun.file(path) in static routes"); + } + var blob = response.body.value.use(); + blob.globalThis = globalThis; + blob.allocator = null; + response.body.value = .{ .Blob = blob.dupe() }; + + break :brk .{ .Blob = blob }; + }, + } + }; + + var has_content_disposition = false; + + if (response.init.headers) |headers| { + has_content_disposition = headers.fastHas(.ContentDisposition); + headers.fastRemove(.TransferEncoding); + headers.fastRemove(.ContentLength); + } + + const headers: Headers = if (response.init.headers) |headers| + Headers.from(headers, bun.default_allocator, .{ + .body = &blob, + }) catch { + blob.detach(); + globalThis.throwOutOfMemory(); + return error.JSError; + } + else + .{ + .allocator = bun.default_allocator, + }; + + return StaticRoute.new(.{ + .blob = blob, + .cached_blob_size = blob.size(), + .has_content_disposition = has_content_disposition, + .headers = headers, + .server = null, + .status_code = response.statusCode(), + }); + } + + return globalThis.throwInvalidArguments("Expected a Response object", .{}); +} + +// HEAD requests have no body. +pub fn onHEADRequest(this: *StaticRoute, req: *uws.Request, resp: HTTPResponse) void { + req.setYield(false); + this.onHEAD(resp); +} + +pub fn onHEAD(this: *StaticRoute, resp: HTTPResponse) void { + this.ref(); + if (this.server) |server| { + server.onPendingRequest(); + resp.timeout(server.config().idleTimeout); + } + resp.corked(renderMetadataAndEnd, .{ this, resp }); + this.onResponseComplete(resp); +} + +fn renderMetadataAndEnd(this: *StaticRoute, resp: HTTPResponse) void { + this.renderMetadata(resp); + resp.writeHeaderInt("Content-Length", this.cached_blob_size); + resp.endWithoutBody(resp.shouldCloseConnection()); +} + +pub fn onRequest(this: *StaticRoute, req: *uws.Request, resp: HTTPResponse) void { + req.setYield(false); + this.on(resp); +} + +pub fn on(this: *StaticRoute, resp: HTTPResponse) void { + this.ref(); + if (this.server) |server| { + server.onPendingRequest(); + resp.timeout(server.config().idleTimeout); + } + var finished = false; + this.doRenderBlob(resp, &finished); + if (finished) { + this.onResponseComplete(resp); + return; + } + + this.toAsync(resp); +} + +fn toAsync(this: *StaticRoute, resp: HTTPResponse) void { + resp.onAborted(*StaticRoute, onAborted, this); + resp.onWritable(*StaticRoute, onWritableBytes, this); +} + +fn onAborted(this: *StaticRoute, resp: HTTPResponse) void { + this.onResponseComplete(resp); +} + +fn onResponseComplete(this: *StaticRoute, resp: HTTPResponse) void { + resp.clearAborted(); + resp.clearOnWritable(); + resp.clearTimeout(); + + if (this.server) |server| { + server.onStaticRequestComplete(); + } + + this.deref(); +} + +pub fn doRenderBlob(this: *StaticRoute, resp: HTTPResponse, did_finish: *bool) void { + // We are not corked + // The body is small + // Faster to do the memcpy than to do the two network calls + // We are not streaming + // This is an important performance optimization + if (this.blob.fastSize() < 16384 - 1024) { + resp.corked(doRenderBlobCorked, .{ this, resp, did_finish }); + } else { + this.doRenderBlobCorked(resp, did_finish); + } +} + +pub fn doRenderBlobCorked(this: *StaticRoute, resp: HTTPResponse, did_finish: *bool) void { + this.renderMetadata(resp); + this.renderBytes(resp, did_finish); +} + +fn onWritable(this: *StaticRoute, write_offset: u64, resp: HTTPResponse) void { + if (this.server) |server| { + resp.timeout(server.config().idleTimeout); + } + + if (!this.onWritableBytes(write_offset, resp)) { + this.toAsync(resp); + return; + } + + this.onResponseComplete(resp); +} + +fn onWritableBytes(this: *StaticRoute, write_offset: u64, resp: HTTPResponse) bool { + const blob = this.blob; + const all_bytes = blob.slice(); + + const bytes = all_bytes[@min(all_bytes.len, @as(usize, @truncate(write_offset)))..]; + + if (!resp.tryEnd( + bytes, + all_bytes.len, + resp.shouldCloseConnection(), + )) { + return false; + } + + return true; +} + +fn doWriteStatus(_: *StaticRoute, status: u16, resp: HTTPResponse) void { + switch (resp) { + .SSL => |r| writeStatus(true, r, status), + .TCP => |r| writeStatus(false, r, status), + } +} + +fn doWriteHeaders(this: *StaticRoute, resp: HTTPResponse) void { + switch (resp) { + inline .SSL, .TCP => |s| { + const entries = this.headers.entries.slice(); + const names: []const Api.StringPointer = entries.items(.name); + const values: []const Api.StringPointer = entries.items(.value); + const buf = this.headers.buf.items; + + for (names, values) |name, value| { + s.writeHeader(name.slice(buf), value.slice(buf)); + } + }, + } +} + +fn renderBytes(this: *StaticRoute, resp: HTTPResponse, did_finish: *bool) void { + did_finish.* = this.onWritableBytes(0, resp); +} + +fn renderMetadata(this: *StaticRoute, resp: HTTPResponse) void { + var status = this.status_code; + const size = this.cached_blob_size; + + status = if (status == 200 and size == 0 and !this.blob.isDetached()) + 204 + else + status; + + this.doWriteStatus(status, resp); + this.doWriteHeaders(resp); +} + +const StaticRoute = @This(); + +const bun = @import("root").bun; + +const Api = @import("../../../api/schema.zig").Api; +const JSC = bun.JSC; +const uws = bun.uws; +const Headers = JSC.WebCore.Headers; +const AnyServer = JSC.API.AnyServer; +const AnyBlob = JSC.WebCore.AnyBlob; +const writeStatus = @import("../server.zig").writeStatus; +const HTTPResponse = uws.AnyResponse; diff --git a/src/bun.js/bindings/ModuleLoader.cpp b/src/bun.js/bindings/ModuleLoader.cpp index bb0653d1e8..bd91fce4a5 100644 --- a/src/bun.js/bindings/ModuleLoader.cpp +++ b/src/bun.js/bindings/ModuleLoader.cpp @@ -642,7 +642,7 @@ JSValue fetchCommonJSModule( } // TOML and JSONC may go through here - else if (res->result.value.tag == SyntheticModuleType::ExportsObject) { + else if (res->result.value.tag == SyntheticModuleType::ExportsObject || res->result.value.tag == SyntheticModuleType::ExportDefaultObject) { JSC::JSValue value = JSC::JSValue::decode(res->result.value.jsvalue_for_export); if (!value) { JSC::throwException(globalObject, scope, JSC::createSyntaxError(globalObject, "Failed to parse Object"_s)); @@ -853,6 +853,21 @@ static JSValue fetchESMSourceCode( JSC::SourceOrigin(), specifier->toWTFString(BunString::ZeroCopy))); JSC::ensureStillAliveHere(value); return rejectOrResolve(JSSourceCode::create(globalObject->vm(), WTFMove(source))); + } else if (res->result.value.tag == SyntheticModuleType::ExportDefaultObject) { + JSC::JSValue value = JSC::JSValue::decode(res->result.value.jsvalue_for_export); + if (!value) { + return reject(JSC::JSValue(JSC::createSyntaxError(globalObject, "Failed to parse Object"_s))); + } + + // JSON can become strings, null, numbers, booleans so we must handle "export default 123" + auto function = generateJSValueExportDefaultObjectSourceCode( + globalObject, + value); + auto source = JSC::SourceCode( + JSC::SyntheticSourceProvider::create(WTFMove(function), + JSC::SourceOrigin(), specifier->toWTFString(BunString::ZeroCopy))); + JSC::ensureStillAliveHere(value); + return rejectOrResolve(JSSourceCode::create(globalObject->vm(), WTFMove(source))); } return rejectOrResolve(JSC::JSSourceCode::create(vm, diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index 2f6acfc87e..d123aa26f2 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -81,4 +81,5 @@ pub const Classes = struct { pub const S3Client = JSC.WebCore.S3Client; pub const S3Stat = JSC.WebCore.S3Stat; + pub const HTMLBundle = JSC.API.HTMLBundle; }; diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 8831304b53..ff63039f88 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -2091,6 +2091,33 @@ pub const ModuleLoader = struct { }; }, + .html => { + if (flags.disableTranspiling()) { + return ResolvedSource{ + .allocator = null, + .source_code = bun.String.empty, + .specifier = input_specifier, + .source_url = input_specifier.createIfDifferent(path.text), + .hash = 0, + .tag = .esm, + }; + } + + if (globalObject == null) { + return error.NotSupported; + } + + const html_bundle = try JSC.API.HTMLBundle.init(globalObject.?, path.text); + return ResolvedSource{ + .allocator = &jsc_vm.allocator, + .jsvalue_for_export = html_bundle.toJS(globalObject.?), + .specifier = input_specifier, + .source_url = input_specifier.createIfDifferent(path.text), + .hash = 0, + .tag = .export_default_object, + }; + }, + else => { if (virtual_source == null) { if (comptime !disable_transpilying) { @@ -2326,11 +2353,16 @@ pub const ModuleLoader = struct { loader = .ts; } else if (attribute.eqlComptime("tsx")) { loader = .tsx; + } else if (jsc_vm.transpiler.options.experimental.html and attribute.eqlComptime("html")) { + loader = .html; } } - if (strings.eqlComptime(path.name.filename, "bun.lock")) { - loader = .json; + // If we were going to choose file loader, see if it's a bun.lock + if (loader == null) { + if (strings.eqlComptime(path.name.filename, "bun.lock")) { + loader = .json; + } } // We only run the transpiler concurrently when we can. diff --git a/src/bun.js/modules/ObjectModule.cpp b/src/bun.js/modules/ObjectModule.cpp index 9d3a5fe9e9..fddf20cf0a 100644 --- a/src/bun.js/modules/ObjectModule.cpp +++ b/src/bun.js/modules/ObjectModule.cpp @@ -84,6 +84,13 @@ generateJSValueModuleSourceCode(JSC::JSGlobalObject* globalObject, value.getObject()); } + return generateJSValueExportDefaultObjectSourceCode(globalObject, value); +} + +JSC::SyntheticSourceProvider::SyntheticSourceGenerator +generateJSValueExportDefaultObjectSourceCode(JSC::JSGlobalObject* globalObject, + JSC::JSValue value) +{ if (value.isCell()) gcProtectNullTolerant(value.asCell()); return [value](JSC::JSGlobalObject* lexicalGlobalObject, diff --git a/src/bun.js/modules/ObjectModule.h b/src/bun.js/modules/ObjectModule.h index 6988e9a94e..9e4807a8c4 100644 --- a/src/bun.js/modules/ObjectModule.h +++ b/src/bun.js/modules/ObjectModule.h @@ -16,4 +16,8 @@ JSC::SyntheticSourceProvider::SyntheticSourceGenerator generateJSValueModuleSourceCode(JSC::JSGlobalObject* globalObject, JSC::JSValue value); +JSC::SyntheticSourceProvider::SyntheticSourceGenerator +generateJSValueExportDefaultObjectSourceCode(JSC::JSGlobalObject* globalObject, + JSC::JSValue value); + } // namespace Zig diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 1e1341081f..c17bbbc6b5 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -3483,6 +3483,39 @@ pub const Headers = struct { buf: std.ArrayListUnmanaged(u8) = .{}, allocator: std.mem.Allocator, + pub fn memoryCost(this: *const Headers) usize { + return this.buf.items.len + this.entries.memoryCost(); + } + + pub fn clone(this: *Headers) !Headers { + return Headers{ + .entries = try this.entries.clone(this.allocator), + .buf = try this.buf.clone(this.allocator), + .allocator = this.allocator, + }; + } + + pub fn append(this: *Headers, name: []const u8, value: []const u8) !void { + var offset: u32 = @truncate(this.buf.items.len); + try this.buf.ensureUnusedCapacity(this.allocator, name.len + value.len); + const name_ptr = Api.StringPointer{ + .offset = offset, + .length = @truncate(name.len), + }; + this.buf.appendSliceAssumeCapacity(name); + offset = @truncate(this.buf.items.len); + this.buf.appendSliceAssumeCapacity(value); + + const value_ptr = Api.StringPointer{ + .offset = offset, + .length = @truncate(value.len), + }; + try this.entries.append(this.allocator, .{ + .name = name_ptr, + .value = value_ptr, + }); + } + pub fn deinit(this: *Headers) void { this.entries.deinit(this.allocator); this.buf.clearAndFree(this.allocator); diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 9cfa4dc51e..e4b436a713 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -4560,7 +4560,7 @@ pub const FileReader = struct { pub fn memoryCost(this: *const FileReader) usize { // ReadableStreamSource covers @sizeOf(FileReader) - return this.reader.memoryCost(); + return this.reader.memoryCost() + this.buffered.capacity; } pub const Source = ReadableStreamSource( diff --git a/src/bun.zig b/src/bun.zig index 36251997c3..806cf42ae7 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1855,6 +1855,17 @@ pub const StringSet = struct { pub const Map = StringArrayHashMap(void); + pub fn clone(self: StringSet) !StringSet { + var new_map = Map.init(self.map.allocator); + try new_map.ensureTotalCapacity(self.map.count()); + for (self.map.keys()) |key| { + new_map.putAssumeCapacity(try self.map.allocator.dupe(u8, key), {}); + } + return StringSet{ + .map = new_map, + }; + } + pub fn init(allocator: std.mem.Allocator) StringSet { return StringSet{ .map = Map.init(allocator), @@ -1897,6 +1908,13 @@ pub const StringMap = struct { pub const Map = StringArrayHashMap(string); + pub fn clone(self: StringMap) !StringMap { + return StringMap{ + .map = try self.map.clone(), + .dupe_keys = self.dupe_keys, + }; + } + pub fn init(allocator: std.mem.Allocator, dupe_keys: bool) StringMap { return StringMap{ .map = Map.init(allocator), diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 00eef30e77..81f3bdc2a1 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -1583,25 +1583,25 @@ pub const BundleV2 = struct { pub const JSBundleThread = BundleThread(JSBundleCompletionTask); - pub fn generateFromJavaScript( + pub fn createAndScheduleCompletionTask( config: bun.JSC.API.JSBundler.Config, plugins: ?*bun.JSC.API.JSBundler.Plugin, globalThis: *JSC.JSGlobalObject, event_loop: *bun.JSC.EventLoop, allocator: std.mem.Allocator, - ) OOM!bun.JSC.JSValue { - var completion = try allocator.create(JSBundleCompletionTask); - completion.* = JSBundleCompletionTask{ + ) OOM!*JSBundleCompletionTask { + _ = allocator; // autofix + const completion = JSBundleCompletionTask.new(.{ .config = config, .jsc_event_loop = event_loop, - .promise = JSC.JSPromise.Strong.init(globalThis), .globalThis = globalThis, .poll_ref = Async.KeepAlive.init(), .env = globalThis.bunVM().transpiler.env, .plugins = plugins, .log = Logger.Log.init(bun.default_allocator), - .task = JSBundleCompletionTask.TaskCompletion.init(completion), - }; + .task = undefined, + }); + completion.task = JSBundleCompletionTask.TaskCompletion.init(completion); if (plugins) |plugin| { plugin.setConfig(completion); @@ -1615,17 +1615,46 @@ pub const BundleV2 = struct { completion.poll_ref.ref(globalThis.bunVM()); + return completion; + } + + pub fn generateFromJavaScript( + config: bun.JSC.API.JSBundler.Config, + plugins: ?*bun.JSC.API.JSBundler.Plugin, + globalThis: *JSC.JSGlobalObject, + event_loop: *bun.JSC.EventLoop, + allocator: std.mem.Allocator, + ) OOM!bun.JSC.JSValue { + const completion = try createAndScheduleCompletionTask(config, plugins, globalThis, event_loop, allocator); + completion.promise = JSC.JSPromise.Strong.init(globalThis); return completion.promise.value(); } pub const BuildResult = struct { output_files: std.ArrayList(options.OutputFile), + + pub fn deinit(this: *BuildResult) void { + for (this.output_files.items) |*output_file| { + output_file.deinit(); + } + + this.output_files.clearAndFree(); + } }; pub const Result = union(enum) { pending: void, err: anyerror, value: BuildResult, + + pub fn deinit(this: *Result) void { + switch (this.*) { + .value => |*value| { + value.deinit(); + }, + else => {}, + } + } }; pub const JSBundleCompletionTask = struct { @@ -1633,10 +1662,13 @@ pub const BundleV2 = struct { jsc_event_loop: *bun.JSC.EventLoop, task: bun.JSC.AnyTask, globalThis: *JSC.JSGlobalObject, - promise: JSC.JSPromise.Strong, + promise: JSC.JSPromise.Strong = .{}, poll_ref: Async.KeepAlive = Async.KeepAlive.init(), env: *bun.DotEnv.Loader, log: Logger.Log, + cancelled: bool = false, + + html_build_task: ?*JSC.API.HTMLBundle.HTMLBundleRoute = null, result: Result = .{ .pending = {} }, @@ -1644,6 +1676,9 @@ pub const BundleV2 = struct { transpiler: *BundleV2 = undefined, plugins: ?*bun.JSC.API.JSBundler.Plugin = null, ref_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(1), + started_at_ns: u64 = 0, + + pub usingnamespace bun.NewThreadSafeRefCounted(JSBundleCompletionTask, @This().deinit); pub fn configureBundler( completion: *JSBundleCompletionTask, @@ -1723,16 +1758,16 @@ pub const BundleV2 = struct { pub const TaskCompletion = bun.JSC.AnyTask.New(JSBundleCompletionTask, onComplete); - pub fn deref(this: *JSBundleCompletionTask) void { - if (this.ref_count.fetchSub(1, .monotonic) == 1) { - this.config.deinit(bun.default_allocator); - debug("Deinit JSBundleCompletionTask(0{x})", .{@intFromPtr(this)}); - bun.default_allocator.destroy(this); + pub fn deinit(this: *JSBundleCompletionTask) void { + this.result.deinit(); + this.log.deinit(); + this.poll_ref.disable(); + if (this.plugins) |plugin| { + plugin.deinit(); } - } - - pub fn ref(this: *JSBundleCompletionTask) void { - _ = this.ref_count.fetchAdd(1, .monotonic); + this.config.deinit(bun.default_allocator); + this.promise.deinit(); + this.destroy(); } pub fn onComplete(this: *JSBundleCompletionTask) void { @@ -1740,6 +1775,15 @@ pub const BundleV2 = struct { defer this.deref(); this.poll_ref.unref(globalThis.bunVM()); + if (this.cancelled) { + return; + } + + if (this.html_build_task) |html_build_task| { + html_build_task.onComplete(this); + return; + } + const promise = this.promise.swap(); switch (this.result) { @@ -1772,11 +1816,8 @@ pub const BundleV2 = struct { @panic("Unexpected pending JavaScript exception in JSBundleCompletionTask.onComplete. This is a bug in Bun."); } - defer build.output_files.deinit(); var to_assign_on_sourcemap: JSC.JSValue = .zero; for (output_files, 0..) |*output_file, i| { - defer bun.default_allocator.free(output_file.src_path.text); - defer bun.default_allocator.free(output_file.dest_path); const result = output_file.toJS( if (!this.config.outdir.isEmpty()) if (std.fs.path.isAbsolute(this.config.outdir.list.items)) diff --git a/src/cli.zig b/src/cli.zig index ef679798ba..19794c7191 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -241,6 +241,7 @@ pub const Arguments = struct { clap.parseParam("--no-deprecation Suppress all reporting of the custom deprecation.") catch unreachable, clap.parseParam("--throw-deprecation Determine whether or not deprecation warnings result in errors.") catch unreachable, clap.parseParam("--title Set the process title") catch unreachable, + clap.parseParam("--experimental-html Bundle .html imports as JavaScript & CSS") catch unreachable, }; const auto_or_run_params = [_]ParamType{ @@ -1163,6 +1164,7 @@ pub const Arguments = struct { } opts.resolve = Api.ResolveMode.lazy; + ctx.bundler_options.experimental.html = args.flag("--experimental-html"); if (jsx_factory != null or jsx_fragment != null or diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index a69d579a38..4e8dc7edaa 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -387,8 +387,12 @@ pub const ResolvedSourceTag = enum(u32) { file = 4, esm = 5, json_for_object_loader = 6, + /// Generate an object with "default" set to all the exports, including a "default" propert exports_object = 7, + /// Generate a module that only exports default the input JSValue + export_default_object = 8, + // Built in modules are loaded through InternalModuleRegistry by numerical ID. // In this enum are represented as \`(1 << 9) & id\` ${moduleList.map((id, n) => ` @"${idToPublicSpecifierOrEnumName(id)}" = ${(1 << 9) | n},`).join("\n")} @@ -412,6 +416,7 @@ writeIfNotChanged( ESM = 5, JSONForObjectLoader = 6, ExportsObject = 7, + ExportDefaultObject = 8, // Built in modules are loaded through InternalModuleRegistry by numerical ID. // In this enum are represented as \`(1 << 9) & id\` InternalModuleRegistryFlag = 1 << 9, diff --git a/src/http/headers.zig b/src/http/headers.zig index 4c76f3b4a5..fe1f08e98b 100644 --- a/src/http/headers.zig +++ b/src/http/headers.zig @@ -1,8 +1,8 @@ const Api = @import("../api/schema.zig").Api; const std = @import("std"); - +const bun = @import("root").bun; pub const Kv = struct { name: Api.StringPointer, value: Api.StringPointer, }; -pub const Entries = std.MultiArrayList(Kv); +pub const Entries = bun.MultiArrayList(Kv); diff --git a/src/jsc.zig b/src/jsc.zig index 7fa5521390..ebb70db42f 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -53,6 +53,7 @@ pub const API = struct { pub const H2FrameParser = @import("./bun.js/api/bun/h2_frame_parser.zig").H2FrameParser; pub const NativeZlib = @import("./bun.js/node/node_zlib_binding.zig").SNativeZlib; pub const NativeBrotli = @import("./bun.js/node/node_zlib_binding.zig").SNativeBrotli; + pub const HTMLBundle = @import("./bun.js/api/server/HTMLBundle.zig"); }; pub const Postgres = @import("./sql/postgres.zig"); pub const DNS = @import("./bun.js/api/bun/dns_resolver.zig"); diff --git a/src/logger.zig b/src/logger.zig index dd6459162f..cddd6d75e9 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -116,6 +116,15 @@ pub const Location = struct { // TODO: document or remove offset: usize = 0, + pub fn memoryCost(this: *const Location) usize { + var cost: usize = 0; + cost += this.file.len; + cost += this.namespace.len; + if (this.line_text) |text| cost += text.len; + if (this.suggestion) |text| cost += text.len; + return cost; + } + pub fn count(this: Location, builder: *StringBuilder) void { builder.count(this.file); builder.count(this.namespace); @@ -214,6 +223,15 @@ pub const Data = struct { text: string, location: ?Location = null, + pub fn memoryCost(this: *const Data) usize { + var cost: usize = 0; + cost += this.text.len; + if (this.location) |*loc| { + cost += loc.memoryCost(); + } + return cost; + } + pub fn deinit(d: *Data, allocator: std.mem.Allocator) void { if (d.location) |*loc| { loc.deinit(allocator); @@ -399,6 +417,15 @@ pub const Msg = struct { notes: []Data = &.{}, redact_sensitive_information: bool = false, + pub fn memoryCost(this: *const Msg) usize { + var cost: usize = 0; + cost += this.data.memoryCost(); + for (this.notes) |*note| { + cost += note.memoryCost(); + } + return cost; + } + pub fn fromJS(allocator: std.mem.Allocator, globalObject: *bun.JSC.JSGlobalObject, file: string, err: bun.JSC.JSValue) OOM!Msg { var zig_exception_holder: bun.JSC.ZigException.Holder = bun.JSC.ZigException.Holder.init(); if (err.toError()) |value| { @@ -608,6 +635,14 @@ pub const Log = struct { clone_line_text: bool = false, + pub fn memoryCost(this: *const Log) usize { + var cost: usize = 0; + for (this.msgs.items) |msg| { + cost += msg.memoryCost(); + } + return cost; + } + pub inline fn hasErrors(this: *const Log) bool { return this.errors > 0; } diff --git a/src/multi_array_list.zig b/src/multi_array_list.zig index 8a85525034..46fbe6a936 100644 --- a/src/multi_array_list.zig +++ b/src/multi_array_list.zig @@ -562,6 +562,10 @@ pub fn MultiArrayList(comptime T: type) type { return self.bytes[0..capacityInBytes(self.capacity)]; } + pub fn memoryCost(self: Self) usize { + return capacityInBytes(self.capacity); + } + pub fn zero(self: Self) void { @memset(self.allocatedBytes(), 0); } diff --git a/src/options.zig b/src/options.zig index 3e965a81b9..add68926f8 100644 --- a/src/options.zig +++ b/src/options.zig @@ -1378,6 +1378,10 @@ pub fn loadersFromTransformOptions(allocator: std.mem.Allocator, _loaders: ?Api. inline for (default_loader_ext_bun) |ext| { _ = try loaders.getOrPutValue(ext, defaultLoaders.get(ext).?); } + + if (bun.CLI.Command.get().bundler_options.experimental.html) { + _ = try loaders.getOrPutValue(".html", .html); + } } if (target == .browser) { @@ -1879,435 +1883,7 @@ pub const TransformOptions = struct { } }; -// Instead of keeping files in-memory, we: -// 1. Write directly to disk -// 2. (Optional) move the file to the destination -// This saves us from allocating a buffer -pub const OutputFile = struct { - loader: Loader, - input_loader: Loader = .js, - src_path: Fs.Path, - value: Value, - size: usize = 0, - size_without_sourcemap: usize = 0, - hash: u64 = 0, - is_executable: bool = false, - source_map_index: u32 = std.math.maxInt(u32), - bytecode_index: u32 = std.math.maxInt(u32), - output_kind: JSC.API.BuildArtifact.OutputKind, - /// Relative - dest_path: []const u8 = "", - side: ?bun.bake.Side, - /// This is only set for the JS bundle, and not files associated with an - /// entrypoint like sourcemaps and bytecode - entry_point_index: ?u32, - referenced_css_files: []const Index = &.{}, - - pub const Index = bun.GenericIndex(u32, OutputFile); - - // Depending on: - // - The target - // - The number of open file handles - // - Whether or not a file of the same name exists - // We may use a different system call - pub const FileOperation = struct { - pathname: string, - fd: FileDescriptorType = bun.invalid_fd, - dir: FileDescriptorType = bun.invalid_fd, - is_tmpdir: bool = false, - is_outdir: bool = false, - close_handle_on_complete: bool = false, - autowatch: bool = true, - - pub fn fromFile(fd: anytype, pathname: string) FileOperation { - return .{ - .pathname = pathname, - .fd = bun.toFD(fd), - }; - } - - pub fn getPathname(file: *const FileOperation) string { - if (file.is_tmpdir) { - return resolve_path.joinAbs(@TypeOf(Fs.FileSystem.instance.fs).tmpdir_path, .auto, file.pathname); - } else { - return file.pathname; - } - } - }; - - pub const Value = union(Kind) { - move: FileOperation, - copy: FileOperation, - noop: u0, - buffer: struct { - allocator: std.mem.Allocator, - bytes: []const u8, - }, - pending: resolver.Result, - saved: SavedFile, - - pub fn toBunString(v: Value) bun.String { - return switch (v) { - .noop => bun.String.empty, - .buffer => |buf| { - // Use ExternalStringImpl to avoid cloning the string, at - // the cost of allocating space to remember the allocator. - const FreeContext = struct { - allocator: std.mem.Allocator, - - fn onFree(uncast_ctx: *anyopaque, buffer: *anyopaque, len: u32) callconv(.C) void { - const ctx: *@This() = @alignCast(@ptrCast(uncast_ctx)); - ctx.allocator.free(@as([*]u8, @ptrCast(buffer))[0..len]); - bun.destroy(ctx); - } - }; - return bun.String.createExternal( - buf.bytes, - true, - bun.new(FreeContext, .{ .allocator = buf.allocator }), - FreeContext.onFree, - ); - }, - .pending => unreachable, - else => |tag| bun.todoPanic(@src(), "handle .{s}", .{@tagName(tag)}), - }; - } - }; - - pub const SavedFile = struct { - pub fn toJS( - globalThis: *JSC.JSGlobalObject, - path: []const u8, - byte_size: usize, - ) JSC.JSValue { - const mime_type = globalThis.bunVM().mimeType(path); - const store = JSC.WebCore.Blob.Store.initFile( - JSC.Node.PathOrFileDescriptor{ - .path = JSC.Node.PathLike{ - .string = JSC.PathString.init(path), - }, - }, - mime_type, - bun.default_allocator, - ) catch unreachable; - - var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable; - blob.* = JSC.WebCore.Blob.initWithStore(store, globalThis); - if (mime_type) |mime| { - blob.content_type = mime.value; - } - blob.size = @as(JSC.WebCore.Blob.SizeType, @truncate(byte_size)); - blob.allocator = bun.default_allocator; - return blob.toJS(globalThis); - } - }; - - pub const Kind = enum { move, copy, noop, buffer, pending, saved }; - - pub fn initPending(loader: Loader, pending: resolver.Result) OutputFile { - return .{ - .loader = loader, - .src_path = pending.pathConst().?.*, - .size = 0, - .value = .{ .pending = pending }, - }; - } - - pub fn initFile(file: std.fs.File, pathname: string, size: usize) OutputFile { - return .{ - .loader = .file, - .src_path = Fs.Path.init(pathname), - .size = size, - .value = .{ .copy = FileOperation.fromFile(file.handle, pathname) }, - }; - } - - pub fn initFileWithDir(file: std.fs.File, pathname: string, size: usize, dir: std.fs.Dir) OutputFile { - var res = initFile(file, pathname, size); - res.value.copy.dir_handle = bun.toFD(dir.fd); - return res; - } - - pub const Options = struct { - loader: Loader, - input_loader: Loader, - hash: ?u64 = null, - source_map_index: ?u32 = null, - bytecode_index: ?u32 = null, - output_path: string, - size: ?usize = null, - input_path: []const u8 = "", - display_size: u32 = 0, - output_kind: JSC.API.BuildArtifact.OutputKind, - is_executable: bool, - data: union(enum) { - buffer: struct { - allocator: std.mem.Allocator, - data: []const u8, - }, - file: struct { - file: std.fs.File, - size: usize, - dir: std.fs.Dir, - }, - saved: usize, - }, - side: ?bun.bake.Side, - entry_point_index: ?u32, - referenced_css_files: []const Index = &.{}, - }; - - pub fn init(options: Options) OutputFile { - return .{ - .loader = options.loader, - .input_loader = options.input_loader, - .src_path = Fs.Path.init(options.input_path), - .dest_path = options.output_path, - .size = options.size orelse switch (options.data) { - .buffer => |buf| buf.data.len, - .file => |file| file.size, - .saved => 0, - }, - .size_without_sourcemap = options.display_size, - .hash = options.hash orelse 0, - .output_kind = options.output_kind, - .bytecode_index = options.bytecode_index orelse std.math.maxInt(u32), - .source_map_index = options.source_map_index orelse std.math.maxInt(u32), - .is_executable = options.is_executable, - .value = switch (options.data) { - .buffer => |buffer| Value{ .buffer = .{ .allocator = buffer.allocator, .bytes = buffer.data } }, - .file => |file| Value{ - .copy = brk: { - var op = FileOperation.fromFile(file.file.handle, options.output_path); - op.dir = bun.toFD(file.dir.fd); - break :brk op; - }, - }, - .saved => Value{ .saved = .{} }, - }, - .side = options.side, - .entry_point_index = options.entry_point_index, - .referenced_css_files = options.referenced_css_files, - }; - } - - pub fn initBuf(buf: []const u8, allocator: std.mem.Allocator, pathname: string, loader: Loader, hash: ?u64, source_map_index: ?u32) OutputFile { - return .{ - .loader = loader, - .src_path = Fs.Path.init(pathname), - .size = buf.len, - .hash = hash orelse 0, - .source_map_index = source_map_index orelse std.math.maxInt(u32), - .value = .{ - .buffer = .{ - .bytes = buf, - .allocator = allocator, - }, - }, - }; - } - - /// Given the `--outdir` as root_dir, this will return the relative path to display in terminal - pub fn writeToDisk(f: OutputFile, root_dir: std.fs.Dir, longest_common_path: []const u8) ![]const u8 { - switch (f.value) { - .saved => { - var rel_path = f.dest_path; - if (f.dest_path.len > longest_common_path.len) { - rel_path = resolve_path.relative(longest_common_path, f.dest_path); - } - return rel_path; - }, - .buffer => |value| { - var rel_path = f.dest_path; - - if (f.dest_path.len > longest_common_path.len) { - rel_path = resolve_path.relative(longest_common_path, f.dest_path); - if (std.fs.path.dirname(rel_path)) |parent| { - if (parent.len > longest_common_path.len) { - try root_dir.makePath(parent); - } - } - } - - var handled_file_not_found = false; - while (true) { - var path_buf: bun.PathBuffer = undefined; - JSC.Node.NodeFS.writeFileWithPathBuffer(&path_buf, .{ - .data = .{ .buffer = .{ - .buffer = .{ - .ptr = @constCast(value.bytes.ptr), - .len = value.bytes.len, - .byte_len = value.bytes.len, - }, - } }, - .encoding = .buffer, - .mode = if (f.is_executable) 0o755 else 0o644, - .dirfd = bun.toFD(root_dir.fd), - .file = .{ .path = .{ - .string = JSC.PathString.init(rel_path), - } }, - }).unwrap() catch |err| switch (err) { - error.FileNotFound, error.ENOENT => { - if (handled_file_not_found) return err; - handled_file_not_found = true; - try root_dir.makePath( - std.fs.path.dirname(rel_path) orelse - return err, - ); - continue; - }, - else => return err, - }; - break; - } - - return rel_path; - }, - .move => |value| { - _ = value; - // var filepath_buf: bun.PathBuffer = undefined; - // filepath_buf[0] = '.'; - // filepath_buf[1] = '/'; - // const primary = f.dest_path[root_dir_path.len..]; - // bun.copy(u8, filepath_buf[2..], primary); - // var rel_path: []const u8 = filepath_buf[0 .. primary.len + 2]; - // rel_path = value.pathname; - - // try f.moveTo(root_path, @constCast(rel_path), bun.toFD(root_dir.fd)); - { - @panic("TODO: Regressed behavior"); - } - - // return primary; - }, - .copy => |value| { - _ = value; - // rel_path = value.pathname; - - // try f.copyTo(root_path, @constCast(rel_path), bun.toFD(root_dir.fd)); - { - @panic("TODO: Regressed behavior"); - } - }, - .noop => { - return f.dest_path; - }, - .pending => unreachable, - } - } - - pub fn moveTo(file: *const OutputFile, _: string, rel_path: []u8, dir: FileDescriptorType) !void { - try bun.C.moveFileZ(file.value.move.dir, bun.sliceTo(&(try std.posix.toPosixPath(file.value.move.getPathname())), 0), dir, bun.sliceTo(&(try std.posix.toPosixPath(rel_path)), 0)); - } - - pub fn copyTo(file: *const OutputFile, _: string, rel_path: []u8, dir: FileDescriptorType) !void { - const file_out = (try dir.asDir().createFile(rel_path, .{})); - - const fd_out = file_out.handle; - var do_close = false; - const fd_in = (try std.fs.openFileAbsolute(file.src_path.text, .{ .mode = .read_only })).handle; - - if (Environment.isWindows) { - Fs.FileSystem.setMaxFd(fd_out); - Fs.FileSystem.setMaxFd(fd_in); - do_close = Fs.FileSystem.instance.fs.needToCloseFiles(); - - // use paths instead of bun.getFdPathW() - @panic("TODO windows"); - } - - defer { - if (do_close) { - _ = bun.sys.close(bun.toFD(fd_out)); - _ = bun.sys.close(bun.toFD(fd_in)); - } - } - - try bun.copyFile(fd_in, fd_out).unwrap(); - } - - pub fn toJS( - this: *OutputFile, - owned_pathname: ?[]const u8, - globalObject: *JSC.JSGlobalObject, - ) bun.JSC.JSValue { - return switch (this.value) { - .move, .pending => @panic("Unexpected pending output file"), - .noop => JSC.JSValue.undefined, - .copy => |copy| brk: { - const file_blob = JSC.WebCore.Blob.Store.initFile( - if (copy.fd != .zero) - JSC.Node.PathOrFileDescriptor{ - .fd = copy.fd, - } - else - JSC.Node.PathOrFileDescriptor{ - .path = JSC.Node.PathLike{ .string = bun.PathString.init(globalObject.allocator().dupe(u8, copy.pathname) catch unreachable) }, - }, - this.loader.toMimeType(), - globalObject.allocator(), - ) catch |err| { - Output.panic("error: Unable to create file blob: \"{s}\"", .{@errorName(err)}); - }; - - var build_output = bun.new(JSC.API.BuildArtifact, .{ - .blob = JSC.WebCore.Blob.initWithStore(file_blob, globalObject), - .hash = this.hash, - .loader = this.input_loader, - .output_kind = this.output_kind, - .path = bun.default_allocator.dupe(u8, copy.pathname) catch @panic("Failed to allocate path"), - }); - - break :brk build_output.toJS(globalObject); - }, - .saved => brk: { - var build_output = bun.default_allocator.create(JSC.API.BuildArtifact) catch @panic("Unable to allocate Artifact"); - const path_to_use = owned_pathname orelse this.src_path.text; - - const file_blob = JSC.WebCore.Blob.Store.initFile( - JSC.Node.PathOrFileDescriptor{ - .path = JSC.Node.PathLike{ .string = bun.PathString.init(owned_pathname orelse (bun.default_allocator.dupe(u8, this.src_path.text) catch unreachable)) }, - }, - this.loader.toMimeType(), - globalObject.allocator(), - ) catch |err| { - Output.panic("error: Unable to create file blob: \"{s}\"", .{@errorName(err)}); - }; - - build_output.* = JSC.API.BuildArtifact{ - .blob = JSC.WebCore.Blob.initWithStore(file_blob, globalObject), - .hash = this.hash, - .loader = this.input_loader, - .output_kind = this.output_kind, - .path = bun.default_allocator.dupe(u8, path_to_use) catch @panic("Failed to allocate path"), - }; - - break :brk build_output.toJS(globalObject); - }, - .buffer => |buffer| brk: { - var blob = JSC.WebCore.Blob.init(@constCast(buffer.bytes), buffer.allocator, globalObject); - if (blob.store) |store| { - store.mime_type = this.loader.toMimeType(); - blob.content_type = store.mime_type.value; - } else { - blob.content_type = this.loader.toMimeType().value; - } - - blob.size = @as(JSC.WebCore.Blob.SizeType, @truncate(buffer.bytes.len)); - - var build_output = bun.default_allocator.create(JSC.API.BuildArtifact) catch @panic("Unable to allocate Artifact"); - build_output.* = JSC.API.BuildArtifact{ - .blob = blob, - .hash = this.hash, - .loader = this.input_loader, - .output_kind = this.output_kind, - .path = owned_pathname orelse bun.default_allocator.dupe(u8, this.src_path.text) catch unreachable, - }; - break :brk build_output.toJS(globalObject); - }, - }; - } -}; +pub const OutputFile = @import("./OutputFile.zig"); pub const TransformResult = struct { errors: []logger.Msg = &([_]logger.Msg{}), diff --git a/src/string_mutable.zig b/src/string_mutable.zig index 0a78d7335f..6b3b08aa4c 100644 --- a/src/string_mutable.zig +++ b/src/string_mutable.zig @@ -19,6 +19,10 @@ pub const MutableString = struct { return MutableString.init(allocator, 2048); } + pub fn clone(self: *MutableString) !MutableString { + return MutableString.initCopy(self.allocator, self.list.items); + } + pub const Writer = std.io.Writer(*@This(), OOM, MutableString.writeAll); pub fn writer(self: *MutableString) Writer { return Writer{