diff options
-rw-r--r-- | src/bun.js/api/JSBundler.zig | 42 | ||||
-rw-r--r-- | src/bun.js/event_loop.zig | 16 | ||||
-rw-r--r-- | src/bun.js/webcore/blob.zig | 8 | ||||
-rw-r--r-- | src/bun.zig | 89 | ||||
-rw-r--r-- | src/bundler/bundle_v2.zig | 240 | ||||
-rw-r--r-- | src/options.zig | 51 |
6 files changed, 427 insertions, 19 deletions
diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index c16cc8897..a89001868 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -52,9 +52,9 @@ pub const JSBundler = struct { pub const Config = struct { target: options.Platform = options.Platform.browser, - entry_points: std.BufSet = std.BufSet.init(bun.default_allocator), + entry_points: bun.StringSet = bun.StringSet.init(bun.default_allocator), hot: bool = false, - define: std.BufMap = std.BufMap.init(bun.default_allocator), + define: bun.StringMap = bun.StringMap.init(bun.default_allocator), dir: OwnedString = OwnedString.initEmpty(bun.default_allocator), outdir: OwnedString = OwnedString.initEmpty(bun.default_allocator), serve: Serve = .{}, @@ -66,6 +66,8 @@ pub const JSBundler = struct { names: Names = .{}, label: OwnedString = OwnedString.initEmpty(bun.default_allocator), + external: bun.StringSet = bun.StringSet.init(bun.default_allocator), + sourcemap: options.SourceMapOption = .none, pub const List = bun.StringArrayHashMapUnmanaged(Config); @@ -85,8 +87,9 @@ pub const JSBundler = struct { pub fn fromJS(globalThis: *JSC.JSGlobalObject, config: JSC.JSValue, allocator: std.mem.Allocator) !Config { var this = Config{ - .entry_points = std.BufSet.init(allocator), - .define = std.BufMap.init(allocator), + .entry_points = bun.StringSet.init(allocator), + .external = bun.StringSet.init(allocator), + .define = bun.StringMap.init(allocator), .dir = OwnedString.initEmpty(allocator), .label = OwnedString.initEmpty(allocator), .outdir = OwnedString.initEmpty(allocator), @@ -118,6 +121,7 @@ pub const JSBundler = struct { if (hot.isBoolean()) { this.minify.whitespace = hot.coerce(bool, globalThis); this.minify.syntax = this.minify.whitespace; + this.minify.identifiers = this.minify.identifiers; } else if (hot.isObject()) { if (try hot.getOptional(globalThis, "whitespace", bool)) |whitespace| { this.minify.whitespace = whitespace; @@ -125,6 +129,9 @@ pub const JSBundler = struct { if (try hot.getOptional(globalThis, "syntax", bool)) |syntax| { this.minify.syntax = syntax; } + if (try hot.getOptional(globalThis, "identifiers", bool)) |syntax| { + this.minify.identifiers = syntax; + } } else { globalThis.throwInvalidArguments("Expected minify to be a boolean or an object", .{}); return error.JSException; @@ -146,6 +153,18 @@ pub const JSBundler = struct { return error.JSException; } + if (try config.getArray(globalThis, "external")) |externals| { + var iter = externals.arrayIterator(globalThis); + while (iter.next()) |entry_point| { + var slice = entry_point.toSliceOrNull(globalThis) orelse { + globalThis.throwInvalidArguments("Expected external to be an array of strings", .{}); + return error.JSException; + }; + defer slice.deinit(); + try this.external.insert(slice.slice()); + } + } + if (try config.getOptional(globalThis, "label", ZigString.Slice)) |slice| { defer slice.deinit(); this.label.appendSliceExact(slice.slice()) catch unreachable; @@ -272,6 +291,7 @@ pub const JSBundler = struct { pub const Minify = struct { whitespace: bool = false, + identifiers: bool = false, syntax: bool = false, }; @@ -288,6 +308,7 @@ pub const JSBundler = struct { pub fn deinit(self: *Config, allocator: std.mem.Allocator) void { self.entry_points.deinit(); + self.external.deinit(); self.define.deinit(); self.dir.deinit(); self.serve.deinit(allocator); @@ -303,11 +324,18 @@ pub const JSBundler = struct { globalThis: *JSC.JSGlobalObject, arguments: []const JSC.JSValue, ) JSC.JSValue { - _ = Config.fromJS(globalThis, arguments[0], globalThis.allocator()) catch { + const config = Config.fromJS(globalThis, arguments[0], globalThis.allocator()) catch { + return JSC.JSValue.jsUndefined(); + }; + + return bun.BundleV2.generateFromJavaScript( + config, + globalThis, + globalThis.bunVM().eventLoop(), + bun.default_allocator, + ) catch { return JSC.JSValue.jsUndefined(); }; - globalThis.throw("Not implemented", .{}); - return JSC.JSValue.jsUndefined(); } pub fn buildFn( diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 83f36922a..ea7551b7f 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -152,6 +152,10 @@ pub const AnyTask = struct { ctx: ?*anyopaque, callback: *const (fn (*anyopaque) void), + pub fn task(this: *AnyTask) Task { + return Task.init(this); + } + pub fn run(this: *AnyTask) void { @setRuntimeSafety(false); var callback = this.callback; @@ -827,18 +831,6 @@ pub const AnyEventLoop = union(enum) { return .{ .mini = MiniEventLoop.init(allocator) }; } - // pub fn enqueueTask( - // this: *AnyEventLoop, - // comptime Context: type, - // ctx: *Context, - // comptime Callback: fn (*Context) void, - // comptime field: std.meta.FieldEnum(Context), - // ) void { - // const TaskType = MiniEventLoop.Task.New(Context, Callback); - // @field(ctx, field) = TaskType.init(ctx); - // this.enqueueTaskConcurrent(&@field(ctx, field)); - // } - pub fn tick( this: *AnyEventLoop, context: *anyopaque, diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index a3650755f..4ac48f25f 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -2518,6 +2518,14 @@ pub const Blob = struct { return blob_.toJS(globalThis); } + pub fn getMimeType(this: *const Blob) ?bun.HTTP.MimeType { + if (this.store) |store| { + return store.mime_type; + } + + return null; + } + pub fn getType( this: *Blob, globalThis: *JSC.JSGlobalObject, diff --git a/src/bun.zig b/src/bun.zig index 1999e43d5..c6fce74de 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1245,3 +1245,92 @@ pub fn reloadProcess( pub var auto_reload_on_crash = false; pub const options = @import("./options.zig"); +pub const StringSet = struct { + map: Map, + + pub const Map = StringArrayHashMap(void); + + pub fn init(allocator: std.mem.Allocator) StringSet { + return StringSet{ + .map = Map.init(allocator), + }; + } + + pub fn keys(self: StringSet) []const string { + return self.map.keys(); + } + + pub fn insert(self: *StringSet, key: []const u8) !void { + var entry = try self.map.getOrPut(key); + if (!entry.found_existing) { + entry.key_ptr.* = try self.map.allocator.dupe(u8, key); + } + } + + pub fn deinit(self: *StringSet) void { + for (self.map.keys()) |key| { + self.map.allocator.free(key); + } + + self.map.deinit(); + } +}; + +pub const Schema = @import("./api/schema.zig"); + +pub const StringMap = struct { + map: Map, + + pub const Map = StringArrayHashMap(string); + + pub fn init(allocator: std.mem.Allocator) StringMap { + return StringMap{ + .map = Map.init(allocator), + }; + } + + pub fn keys(self: StringMap) []const string { + return self.map.keys(); + } + + pub fn values(self: StringMap) []const string { + return self.map.values(); + } + + pub fn count(self: StringMap) usize { + return self.map.count(); + } + + pub fn toAPI(self: StringMap) Schema.Api.StringMap { + return Schema.Api.StringMap{ + .keys = self.keys(), + .values = self.values(), + }; + } + + pub fn insert(self: *StringMap, key: []const u8, value: []const u8) !void { + var entry = try self.map.getOrPut(key); + if (!entry.found_existing) { + entry.key_ptr.* = try self.map.allocator.dupe(u8, key); + } else { + self.map.allocator.free(entry.value_ptr.*); + } + + entry.value_ptr.* = try self.map.allocator.dupe(u8, value); + } + + pub fn deinit(self: *StringMap) void { + for (self.map.values()) |value| { + self.map.allocator.free(value); + } + + for (self.map.keys()) |key| { + self.map.allocator.free(key); + } + + self.map.deinit(); + } +}; + +pub const DotEnv = @import("./env_loader.zig"); +pub const BundleV2 = @import("./bundler/bundle_v2.zig").BundleV2; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index dda7d9659..140229dc1 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -146,6 +146,11 @@ pub const ThreadPool = struct { has_notify_started: bool = false, has_created: bool = false, + pub fn deinit(this: *Worker) void { + this.data.deinit(this.allocator); + this.heap.deinit(); + } + pub fn get() *Worker { var worker = @ptrCast( *ThreadPool.Worker, @@ -545,6 +550,241 @@ pub const BundleV2 = struct { return try this.linker.generateChunksInParallel(chunks); } + pub fn generateFromJavaScript( + config: bun.JSC.API.JSBundler.Config, + globalThis: *JSC.JSGlobalObject, + event_loop: *bun.JSC.EventLoop, + allocator: std.mem.Allocator, + ) !bun.JSC.JSValue { + var completion = try allocator.create(JSBundleCompletionTask); + completion.* = JSBundleCompletionTask{ + .config = config, + .jsc_event_loop = event_loop, + .promise = JSC.JSPromise.Strong.init(globalThis), + .globalThis = globalThis, + .ref = JSC.Ref.init(), + .env = globalThis.bunVM().bundler.env, + .log = Logger.Log.init(bun.default_allocator), + .task = JSBundleCompletionTask.TaskCompletion.init(completion), + }; + + // Ensure this exists before we spawn the thread to prevent any race + // conditions from creating two + _ = JSC.WorkPool.get(); + + var thread = try std.Thread.spawn(.{}, generateInNewThreadWrap, .{completion}); + thread.detach(); + + completion.ref.ref(globalThis.bunVM()); + + return completion.promise.value(); + } + + pub const BuildResult = struct { + output_files: std.ArrayList(options.OutputFile), + }; + + pub const JSBundleCompletionTask = struct { + config: bun.JSC.API.JSBundler.Config, + jsc_event_loop: *bun.JSC.EventLoop, + task: bun.JSC.AnyTask, + globalThis: *JSC.JSGlobalObject, + promise: JSC.JSPromise.Strong, + ref: JSC.Ref = JSC.Ref.init(), + env: *bun.DotEnv.Loader, + log: Logger.Log, + + result: Result = .{ .pending = {} }, + + pub const Result = union(enum) { + pending: void, + err: anyerror, + value: BuildResult, + }; + + pub const TaskCompletion = bun.JSC.AnyTask.New(JSBundleCompletionTask, onComplete); + + pub fn onComplete(this: *JSBundleCompletionTask) void { + var globalThis = this.globalThis; + + defer { + this.config.deinit(bun.default_allocator); + } + + this.ref.unref(globalThis.bunVM()); + const promise = this.promise.swap(); + const root_obj = JSC.JSValue.createEmptyObject(globalThis, 2); + + switch (this.result) { + .pending => unreachable, + .err => { + root_obj.put( + globalThis, + JSC.ZigString.static("outputs"), + JSC.JSValue.createEmptyArray(globalThis, 0), + ); + + root_obj.put( + globalThis, + JSC.ZigString.static("logs"), + this.log.toJS(globalThis, bun.default_allocator, "Errors while building"), + ); + }, + .value => |*build| { + var output_files: []options.OutputFile = build.output_files.items; + const output_files_js = JSC.JSValue.createEmptyArray(globalThis, output_files.len); + defer build.output_files.deinit(); + for (output_files, 0..) |*output_file, i| { + var obj = JSC.JSValue.createEmptyObject(globalThis, 2); + obj.put( + globalThis, + JSC.ZigString.static("path"), + JSC.ZigString.fromUTF8(output_file.input.text).toValueGC(globalThis), + ); + + obj.put( + globalThis, + JSC.ZigString.static("result"), + output_file.toJS(globalThis), + ); + output_files_js.putIndex(globalThis, @intCast(u32, i), obj); + } + + root_obj.put( + globalThis, + JSC.ZigString.static("outputs"), + output_files_js, + ); + + root_obj.put( + globalThis, + JSC.ZigString.static("logs"), + this.log.toJS(globalThis, bun.default_allocator, "Errors while building"), + ); + }, + } + + promise.resolve(globalThis, root_obj); + } + }; + + pub fn generateInNewThreadWrap( + completion: *JSBundleCompletionTask, + ) void { + Output.Source.configureNamedThread("Bundler"); + generateInNewThread(completion) catch |err| { + completion.result = .{ .err = err }; + var concurrent_task = bun.default_allocator.create(JSC.ConcurrentTask) catch unreachable; + concurrent_task.* = JSC.ConcurrentTask{ + .auto_delete = true, + .task = completion.task.task(), + .next = null, + }; + completion.jsc_event_loop.enqueueTaskConcurrent(concurrent_task); + }; + } + + fn generateInNewThread( + completion: *JSBundleCompletionTask, + ) !void { + const allocator = bun.default_allocator; + + const config = &completion.config; + var bundler = try allocator.create(bun.Bundler); + errdefer allocator.destroy(bundler); + + bundler.* = try bun.Bundler.init( + allocator, + &completion.log, + Api.TransformOptions{ + .define = if (config.define.count() > 0) config.define.toAPI() else null, + .entry_points = config.entry_points.keys(), + .platform = config.target.toAPI(), + .absolute_working_dir = if (config.dir.list.items.len > 0) config.dir.toOwnedSliceLeaky() else null, + .inject = &.{}, + .external = config.external.keys(), + .main_fields = &.{}, + .extension_order = &.{}, + }, + null, + completion.env, + ); + bundler.options.jsx = config.jsx; + bundler.options.entry_names = config.names.entry_point.data; + bundler.options.output_dir = config.outdir.toOwnedSliceLeaky(); + bundler.options.minify_syntax = config.minify.syntax; + bundler.options.minify_whitespace = config.minify.whitespace; + bundler.options.minify_identifiers = config.minify.identifiers; + bundler.options.inlining = config.minify.syntax; + bundler.options.sourcemap = config.sourcemap; + + try bundler.configureDefines(); + bundler.configureLinker(); + + bundler.resolver.opts = bundler.options; + + var event_loop = try allocator.create(JSC.AnyEventLoop); + defer allocator.destroy(event_loop); + + // Ensure uWS::Loop is initialized + _ = bun.uws.Loop.get().?; + + var this = try BundleV2.init(bundler, allocator, JSC.AnyEventLoop.init(allocator), false, JSC.WorkPool.get()); + defer this.deinit(); + + completion.result = .{ + .value = .{ + .output_files = try this.runFromJSInNewThread(config), + }, + }; + + var concurrent_task = try bun.default_allocator.create(JSC.ConcurrentTask); + concurrent_task.* = JSC.ConcurrentTask{ + .auto_delete = true, + .task = completion.task.task(), + .next = null, + }; + completion.jsc_event_loop.enqueueTaskConcurrent(concurrent_task); + } + + pub fn deinit(this: *BundleV2) void { + for (this.graph.pool.workers[0..this.graph.pool.workers_used.loadUnchecked()]) |*worker| { + worker.deinit(); + } + this.graph.heap.deinit(); + } + + pub fn runFromJSInNewThread(this: *BundleV2, config: *const bun.JSC.API.JSBundler.Config) !std.ArrayList(options.OutputFile) { + if (this.bundler.log.errors > 0) { + return error.BuildFailed; + } + + this.graph.pool.pool.schedule(try this.enqueueEntryPoints(config.entry_points.keys())); + + // We must wait for all the parse tasks to complete, even if there are errors. + this.waitForParse(); + + if (this.bundler.log.errors > 0) { + return error.BuildFailed; + } + + try this.cloneAST(); + + var chunks = try this.linker.link( + this, + this.graph.entry_points.items, + this.graph.use_directive_entry_points, + try this.findReachableFiles(), + std.crypto.random.int(u64), + ); + + if (this.bundler.log.errors > 0) { + return error.BuildFailed; + } + + return try this.linker.generateChunksInParallel(chunks); + } + pub fn onParseTaskComplete(parse_result: *ParseTask.Result, this: *BundleV2) void { var graph = &this.graph; var batch = ThreadPoolLib.Batch{}; diff --git a/src/options.zig b/src/options.zig index 48b580746..59b814894 100644 --- a/src/options.zig +++ b/src/options.zig @@ -661,6 +661,16 @@ pub const Loader = enum { dataurl, text, + pub fn toMimeType(this: Loader) bun.HTTP.MimeType { + return switch (this) { + .jsx, .js, .ts, .tsx => bun.HTTP.MimeType.javascript, + .css => bun.HTTP.MimeType.css, + .toml, .json => bun.HTTP.MimeType.json, + .wasm => bun.HTTP.MimeType.wasm, + else => bun.HTTP.MimeType.other, + }; + } + pub const HashTable = bun.StringArrayHashMap(Loader); pub fn canHaveSourceMap(this: Loader) bool { @@ -1948,6 +1958,27 @@ pub const OutputFile = struct { close_handle_on_complete: bool = false, autowatch: bool = true, + pub fn toJS(this: FileOperation, globalObject: *JSC.JSGlobalObject, loader: Loader) JSC.JSValue { + var file_blob = JSC.WebCore.Blob.Store.initFile( + if (this.fd != 0) JSC.Node.PathOrFileDescriptor{ + .fd = this.fd, + } else JSC.Node.PathOrFileDescriptor{ + .path = JSC.Node.PathLike{ .string = bun.PathString.init(globalObject.allocator().dupe(u8, this.pathname) catch unreachable) }, + }, + loader.toMimeType(), + globalObject.allocator(), + ) catch |err| { + Output.panic("error: Unable to create file blob: \"{s}\"", .{@errorName(err)}); + }; + + var blob = globalObject.allocator().create(JSC.WebCore.Blob) catch unreachable; + blob.* = JSC.WebCore.Blob.initWithStore(file_blob, globalObject); + blob.allocator = globalObject.allocator(); + blob.content_type = loader.toMimeType().value; + + return blob.toJS(globalObject); + } + pub fn fromFile(fd: FileDescriptorType, pathname: string) FileOperation { return .{ .pathname = pathname, @@ -2035,6 +2066,26 @@ pub const OutputFile = struct { try bun.copyFile(fd_in, fd_out); } + + pub fn toJS( + this: *OutputFile, + globalObject: *JSC.JSGlobalObject, + ) bun.JSC.JSValue { + return switch (this.value) { + .pending => @panic("Unexpected pending output file"), + .noop => JSC.JSValue.undefined, + .move => this.value.move.toJS(globalObject, this.loader), + .copy => this.value.copy.toJS(globalObject, this.loader), + .buffer => |buffer| brk: { + var blob = globalObject.allocator().create(JSC.WebCore.Blob) catch unreachable; + blob.* = JSC.WebCore.Blob.init(@constCast(buffer), bun.default_allocator, globalObject); + blob.store.?.mime_type = this.loader.toMimeType(); + blob.content_type = blob.store.?.mime_type.value; + blob.allocator = globalObject.allocator(); + break :brk blob.toJS(globalObject); + }, + }; + } }; pub const TransformResult = struct { |