diff options
author | 2023-04-24 14:11:59 -0700 | |
---|---|---|
committer | 2023-04-24 14:11:59 -0700 | |
commit | 923ac39c0b718ac5d488f65232f0dcd7161423d4 (patch) | |
tree | c4a35c8e7c8f16a5139a4622b8a26ff2331f85b6 | |
parent | 98209b8e101c8c0199f1360f7c1781938f502ed8 (diff) | |
download | bun-923ac39c0b718ac5d488f65232f0dcd7161423d4.tar.gz bun-923ac39c0b718ac5d488f65232f0dcd7161423d4.tar.zst bun-923ac39c0b718ac5d488f65232f0dcd7161423d4.zip |
Support plugins in `Bun.build` (#2720)
* wip
* Implement `onLoad` plugins
* Support exceptions and async `onLoad` plugins
* Fix filtering
* Handle empty files
* Fix JSON loader
---------
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r-- | src/bun.js/api/JSBundler.zig | 444 | ||||
-rw-r--r-- | src/bun.js/bindings/JSBundlerPlugin.cpp | 526 | ||||
-rw-r--r-- | src/bun.js/bindings/JSBundlerPlugin.h | 103 | ||||
-rw-r--r-- | src/bun.js/bindings/ModuleLoader.cpp | 139 | ||||
-rw-r--r-- | src/bun.js/bindings/ModuleLoader.h | 29 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 4 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.h | 6 | ||||
-rw-r--r-- | src/bun.js/bindings/headers-handwritten.h | 36 | ||||
-rw-r--r-- | src/bun.js/module_loader.zig | 12 | ||||
-rw-r--r-- | src/bun.zig | 1 | ||||
-rw-r--r-- | src/bundler/bundle_v2.zig | 150 | ||||
-rw-r--r-- | src/logger.zig | 17 | ||||
-rw-r--r-- | src/options.zig | 11 |
13 files changed, 1372 insertions, 106 deletions
diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 4bb5cad3d..c53d21f12 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -41,13 +41,9 @@ const Mimalloc = @import("../../mimalloc_arena.zig"); const Runtime = @import("../../runtime.zig").Runtime; const JSLexer = bun.js_lexer; const Expr = JSAst.Expr; +const Index = @import("../../ast/base.zig").Index; pub const JSBundler = struct { - heap: Mimalloc.Arena, - allocator: std.mem.Allocator, - configs: Config.List = .{}, - has_pending_activity: std.atomic.Atomic(bool) = std.atomic.Atomic(bool).init(true), - const OwnedString = bun.MutableString; pub const Config = struct { @@ -62,7 +58,6 @@ pub const JSBundler = struct { code_splitting: bool = false, minify: Minify = .{}, server_components: ServerComponents = ServerComponents{}, - plugins: PluginDeclaration.List = .{}, names: Names = .{}, label: OwnedString = OwnedString.initEmpty(bun.default_allocator), @@ -71,21 +66,7 @@ pub const JSBundler = struct { pub const List = bun.StringArrayHashMapUnmanaged(Config); - /// - /// { name: "", setup: (build) {} } - pub const PluginDeclaration = struct { - name: OwnedString = OwnedString.initEmpty(bun.default_allocator), - setup: JSC.Strong = .{}, - - pub const List = std.ArrayListUnmanaged(PluginDeclaration); - - pub fn deinit(this: *PluginDeclaration) void { - this.name.deinit(); - this.setup.deinit(); - } - }; - - pub fn fromJS(globalThis: *JSC.JSGlobalObject, config: JSC.JSValue, allocator: std.mem.Allocator) !Config { + pub fn fromJS(globalThis: *JSC.JSGlobalObject, config: JSC.JSValue, plugins: *?*Plugin, allocator: std.mem.Allocator) !Config { var this = Config{ .entry_points = bun.StringSet.init(allocator), .external = bun.StringSet.init(allocator), @@ -99,6 +80,7 @@ pub const JSBundler = struct { }, }; errdefer this.deinit(allocator); + errdefer if (plugins.*) |plugin| plugin.deinit(); if (try config.getOptionalEnum(globalThis, "target", options.Platform)) |target| { this.target = target; @@ -194,12 +176,6 @@ pub const JSBundler = struct { if (try config.getArray(globalThis, "plugins")) |array| { var iter = array.arrayIterator(globalThis); while (iter.next()) |plugin| { - var decl = PluginDeclaration{ - .name = OwnedString.initEmpty(allocator), - .setup = .{}, - }; - errdefer decl.deinit(); - if (try plugin.getObject(globalThis, "SECRET_SERVER_COMPONENTS_INTERNALS")) |internals| { if (internals.get(globalThis, "router")) |router_value| { if (router_value.as(JSC.API.FileSystemRouter) != null) { @@ -244,21 +220,47 @@ pub const JSBundler = struct { globalThis.throwInvalidArguments("Expected directive.server to be an array of strings", .{}); return error.JSException; } - } - if (try plugin.getOptional(globalThis, "name", ZigString.Slice)) |slice| { - defer slice.deinit(); - decl.name.appendSliceExact(slice.slice()) catch unreachable; + continue; } - if (try plugin.getFunction(globalThis, "setup")) |setup| { - decl.setup.set(globalThis, setup); + // var decl = PluginDeclaration{ + // .name = OwnedString.initEmpty(allocator), + // .setup = .{}, + // }; + // defer decl.deinit(); + + // if (try plugin.getOptional(globalThis, "name", ZigString.Slice)) |slice| { + // defer slice.deinit(); + // decl.name.appendSliceExact(slice.slice()) catch unreachable; + // } + + if (try plugin.getFunction(globalThis, "setup")) |_| { + // decl.setup.set(globalThis, setup); } else { globalThis.throwInvalidArguments("Expected plugin to have a setup() function", .{}); return error.JSError; } - try this.plugins.append(allocator, decl); + var bun_plugins: *Plugin = plugins.* orelse brk: { + plugins.* = Plugin.create( + globalThis, + switch (this.target) { + .bun, .bun_macro => JSC.JSGlobalObject.BunPluginTarget.bun, + .node => JSC.JSGlobalObject.BunPluginTarget.node, + else => .browser, + }, + ); + break :brk plugins.*.?; + }; + + const plugin_result = bun_plugins.addPlugin(globalThis, plugin); + + if (plugin_result.toError()) |err| { + globalThis.throwValue(err); + bun_plugins.deinit(); + return error.JSError; + } } } @@ -312,7 +314,6 @@ pub const JSBundler = struct { self.define.deinit(); self.dir.deinit(); self.serve.deinit(allocator); - self.plugins.deinit(allocator); self.server_components.deinit(allocator); self.names.deinit(); self.label.deinit(); @@ -324,12 +325,14 @@ pub const JSBundler = struct { globalThis: *JSC.JSGlobalObject, arguments: []const JSC.JSValue, ) JSC.JSValue { - const config = Config.fromJS(globalThis, arguments[0], globalThis.allocator()) catch { + var plugins: ?*Plugin = null; + const config = Config.fromJS(globalThis, arguments[0], &plugins, globalThis.allocator()) catch { return JSC.JSValue.jsUndefined(); }; return bun.BundleV2.generateFromJavaScript( config, + plugins, globalThis, globalThis.bunVM().eventLoop(), bun.default_allocator, @@ -351,4 +354,373 @@ pub const JSBundler = struct { ) js.JSValueRef { return build(globalThis, @ptrCast([]const JSC.JSValue, arguments_)).asObjectRef(); } + + pub const Resolve = struct { + import_record: *bun.ImportRecord, + source_file: string = "", + default_namespace: string = "", + + /// Null means the Resolve is aborted + completion: ?*bun.BundleV2.JSBundleCompletionTask = null, + + value: Value, + + pub const Value = union(enum) { + err: logger.Msg, + success: struct { + path: []const u8 = "", + namespace: []const u8 = "", + + pub fn deinit(this: *@This()) void { + bun.default_allocator.destroy(this.path); + bun.default_allocator.destroy(this.namespace); + } + }, + no_match: void, + pending: JSC.JSPromise.Strong, + consumed: void, + + fn badPluginError() Value { + return .{ + .err = logger.Msg{ + .data = .{ + .text = bun.default_allocator.dupe(u8, "onResolve plugin returned an invalid value") catch unreachable, + }, + }, + }; + } + + pub fn consume(this: *Value) Value { + const result = this.*; + this.* = .{ .consumed = {} }; + return result; + } + + pub fn fromJS(globalObject: *JSC.JSGlobalObject, source_file: []const u8, default_namespace: string, value: JSC.JSValue) Value { + if (value.isEmptyOrUndefinedOrNull()) { + return .{ .no_match = {} }; + } + + if (value.toError(globalObject)) |err| { + return .{ .err = logger.Msg.fromJS(bun.default_allocator, globalObject, source_file, err) catch unreachable }; + } + + // I think we already do this check? + if (!value.isObject()) return badPluginError(); + + var namespace = ZigString.Slice.fromUTF8NeverFree(default_namespace); + + if (value.getOptional(globalObject, "namespace", ZigString.Slice) catch return badPluginError()) |namespace_slice| { + namespace = namespace_slice; + } + + const path = value.getOptional(globalObject, "path", ZigString.Slice) catch { + namespace.deinit(); + return badPluginError(); + }; + + return .{ + .success = .{ + .path = path.cloneWithAllocator(bun.default_allocator).slice(), + .namespace = namespace.slice(), + }, + }; + } + + pub fn deinit(this: *Resolve.Value) void { + switch (this.*) { + .pending => |*pending| { + pending.deinit(); + }, + .success => |*success| { + success.deinit(); + }, + .err => |*err| { + err.deinit(bun.default_allocator); + }, + .consumed => {}, + } + this.* = .{ .consumed = {} }; + } + }; + + pub fn deinit(this: *Resolve) void { + this.value.deinit(); + if (this.completion) |completion| + completion.deref(); + } + + const AnyTask = JSC.AnyTask.New(@This(), runOnJSThread); + + pub fn runOnJSThread(this: *Load) void { + var completion = this.completion orelse { + this.deinit(); + return; + }; + + const result = completion.plugins.?.matchOnResolve( + completion.globalThis, + this.path, + this.namespace, + this, + ); + + this.value = Value.fromJS(completion.globalThis, this.source_file, this.default_namespace, result); + completion.bundler.onResolveAsync(this); + } + }; + + pub const Load = struct { + source_index: Index, + default_loader: options.Loader, + path: []const u8 = "", + namespace: []const u8 = "", + + /// Null means the task was aborted. + completion: ?*bun.BundleV2.JSBundleCompletionTask = null, + + value: Value, + js_task: JSC.AnyTask = undefined, + task: JSC.AnyEventLoop.Task = undefined, + parse_task: *bun.ParseTask = undefined, + + pub fn create( + completion: *bun.BundleV2.JSBundleCompletionTask, + source_index: Index, + default_loader: options.Loader, + path: Fs.Path, + ) Load { + return Load{ + .source_index = source_index, + .default_loader = default_loader, + .completion = completion, + .value = .{ .pending = .{} }, + .path = path.text, + .namespace = path.namespace, + }; + } + + pub const Value = union(enum) { + err: logger.Msg, + success: struct { + source_code: []const u8 = "", + loader: options.Loader = options.Loader.file, + }, + pending: JSC.JSPromise.Strong, + consumed: void, + + pub fn deinit(this: *Value) void { + switch (this.*) { + .pending => |*pending| { + pending.strong.deinit(); + }, + .success => |success| { + bun.default_allocator.destroy(success.source_code); + }, + .err => |*err| { + err.deinit(bun.default_allocator); + }, + .consumed => {}, + } + this.* = .{ .consumed = {} }; + } + + pub fn consume(this: *Value) Value { + const result = this.*; + this.* = .{ .consumed = {} }; + return result; + } + }; + + pub fn deinit(this: *Load) void { + this.value.deinit(); + if (this.completion) |completion| + completion.deref(); + } + + const AnyTask = JSC.AnyTask.New(@This(), runOnJSThread); + + pub fn runOnJSThread(this: *Load) void { + var completion = this.completion orelse { + this.deinit(); + return; + }; + + const err = completion.plugins.?.matchOnLoad( + completion.globalThis, + this.path, + this.namespace, + this, + ); + + if (this.value == .pending) { + if (!err.isEmptyOrUndefinedOrNull()) { + var code = ZigString.Empty; + JSBundlerPlugin__OnLoadAsync(this, err, &code, .js); + } + } + } + + pub fn dispatch(this: *Load) void { + var completion = this.completion orelse { + this.deinit(); + return; + }; + completion.ref(); + + this.js_task = AnyTask.init(this); + var concurrent_task = bun.default_allocator.create(JSC.ConcurrentTask) catch { + completion.deref(); + this.deinit(); + return; + }; + concurrent_task.* = JSC.ConcurrentTask{ + .auto_delete = true, + .task = this.js_task.task(), + }; + completion.jsc_event_loop.enqueueTaskConcurrent(concurrent_task); + } + + export fn JSBundlerPlugin__getDefaultLoader(this: *Load) options.Loader { + return this.default_loader; + } + + export fn JSBundlerPlugin__OnLoadAsync( + this: *Load, + error_value: JSC.JSValue, + source_code: *ZigString, + loader: options.Loader, + ) void { + if (this.completion) |completion| { + if (error_value.toError()) |err| { + if (this.value == .pending) this.value.pending.strong.deinit(); + this.value = .{ + .err = logger.Msg.fromJS(bun.default_allocator, completion.globalThis, this.path, err) catch unreachable, + }; + } else if (!error_value.isEmptyOrUndefinedOrNull() and error_value.isCell() and error_value.jsType() == .JSPromise) { + this.value.pending.strong.set(completion.globalThis, error_value); + return; + } else { + if (this.value == .pending) this.value.pending.strong.deinit(); + this.value = .{ + .success = .{ + .source_code = source_code.toSliceClone(bun.default_allocator).slice(), + .loader = loader, + }, + }; + } + + completion.bundler.onLoadAsync(this); + } else { + this.deinit(); + } + } + + comptime { + _ = JSBundlerPlugin__getDefaultLoader; + _ = JSBundlerPlugin__OnLoadAsync; + } + }; + + pub const Plugin = opaque { + extern fn JSBundlerPlugin__create(*JSC.JSGlobalObject, JSC.JSGlobalObject.BunPluginTarget) *Plugin; + pub fn create(globalObject: *JSC.JSGlobalObject, target: JSC.JSGlobalObject.BunPluginTarget) *Plugin { + return JSBundlerPlugin__create(globalObject, target); + } + + extern fn JSBundlerPlugin__tombestone(*Plugin) void; + + extern fn JSBundlerPlugin__anyMatches( + *Plugin, + namespaceString: *const ZigString, + path: *const ZigString, + bool, + ) bool; + + extern fn JSBundlerPlugin__matchOnLoad( + *JSC.JSGlobalObject, + *Plugin, + namespaceString: *const ZigString, + path: *const ZigString, + context: *anyopaque, + ) JSValue; + + extern fn JSBundlerPlugin__matchOnResolve( + *JSC.JSGlobalObject, + *Plugin, + namespaceString: *const ZigString, + path: *const ZigString, + importer: *const ZigString, + context: *anyopaque, + ) JSValue; + + pub fn hasAnyMatches( + this: *Plugin, + path: *const Fs.Path, + is_onLoad: bool, + ) bool { + const namespace_string = if (strings.eqlComptime(path.namespace, "file")) + ZigString.Empty + else + ZigString.fromUTF8(path.namespace); + const path_string = ZigString.fromUTF8(path.text); + return JSBundlerPlugin__anyMatches(this, &namespace_string, &path_string, is_onLoad); + } + + pub fn matchOnLoad( + this: *Plugin, + globalThis: *JSC.JSGlobalObject, + path: []const u8, + namespace: []const u8, + context: *anyopaque, + ) JSC.JSValue { + const namespace_string = if (strings.eqlComptime(namespace, "file")) + ZigString.Empty + else + ZigString.fromUTF8(namespace); + const path_string = ZigString.fromUTF8(path); + return JSBundlerPlugin__matchOnLoad(globalThis, this, &namespace_string, &path_string, context); + } + + pub fn matchOnResolve( + this: *Plugin, + globalThis: *JSC.JSGlobalObject, + path: []const u8, + namespace: []const u8, + importer: []const u8, + context: *anyopaque, + ) JSC.JSValue { + const namespace_string = if (strings.eqlComptime(namespace, "file")) + ZigString.Empty + else + ZigString.fromUTF8(namespace); + const path_string = ZigString.fromUTF8(path); + const importer_string = ZigString.fromUTF8(importer); + return JSBundlerPlugin__matchOnResolve(globalThis, this, &namespace_string, &path_string, &importer_string, context); + } + + pub fn addPlugin( + this: *Plugin, + globalObject: *JSC.JSGlobalObject, + object: JSC.JSValue, + ) JSValue { + return setupJSBundlerPlugin(this, globalObject, object); + } + + pub fn deinit(this: *Plugin) void { + JSBundlerPlugin__tombestone(this); + } + + pub fn setConfig(this: *Plugin, config: *anyopaque) void { + JSBundlerPlugin__setConfig(this, config); + } + + extern fn JSBundlerPlugin__setConfig(*Plugin, *anyopaque) void; + + extern fn setupJSBundlerPlugin( + *Plugin, + *JSC.JSGlobalObject, + JSC.JSValue, + ) JSValue; + }; }; diff --git a/src/bun.js/bindings/JSBundlerPlugin.cpp b/src/bun.js/bindings/JSBundlerPlugin.cpp new file mode 100644 index 000000000..2aaa40270 --- /dev/null +++ b/src/bun.js/bindings/JSBundlerPlugin.cpp @@ -0,0 +1,526 @@ +#include "JSBundlerPlugin.h" + +#include "headers-handwritten.h" +#include "JavaScriptCore/CatchScope.h" +#include "JavaScriptCore/JSGlobalObject.h" +#include "JavaScriptCore/JSTypeInfo.h" +#include "JavaScriptCore/Structure.h" +#include "helpers.h" +#include "ZigGlobalObject.h" +#include "JavaScriptCore/JavaScript.h" +#include "JavaScriptCore/JSObjectInlines.h" +#include "wtf/text/WTFString.h" +#include "JavaScriptCore/JSCInlines.h" + +#include "JavaScriptCore/ObjectConstructor.h" +#include "JavaScriptCore/SubspaceInlines.h" +#include "JavaScriptCore/RegExpObject.h" +#include "JavaScriptCore/JSPromise.h" +#include "BunClientData.h" +#include "ModuleLoader.h" +#include "JavaScriptCore/RegularExpression.h" + +namespace Bun { + +#define WRAP_BUNDLER_PLUGIN(argName) JSValue(bitwise_cast<double>(reinterpret_cast<uintptr_t>(argName))) +#define UNWRAP_BUNDLER_PLUGIN(callFrame) reinterpret_cast<JSBundlerPlugin*>(bitwise_cast<uintptr_t>(callFrame->thisValue().asDouble())) + +WTF_MAKE_ISO_ALLOCATED_IMPL(JSBundlerPlugin); + +static bool isValidNamespaceString(String& namespaceString) +{ + static JSC::Yarr::RegularExpression* namespaceRegex = nullptr; + if (!namespaceRegex) { + namespaceRegex = new JSC::Yarr::RegularExpression("^([/@a-zA-Z0-9_\\-]+)$"_s); + } + return namespaceRegex->match(namespaceString) > -1; +} + +static EncodedJSValue jsFunctionAppendOnLoadPluginBody(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe, JSBundlerPlugin& plugin) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + Ref protect(plugin); + + if (callframe->argumentCount() < 2) { + throwException(globalObject, scope, createError(globalObject, "onLoad() requires at least 2 arguments"_s)); + return JSValue::encode(jsUndefined()); + } + + auto* filterObject = callframe->uncheckedArgument(0).toObject(globalObject); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + auto clientData = WebCore::clientData(vm); + auto& builtinNames = clientData->builtinNames(); + JSC::RegExpObject* filter = nullptr; + if (JSValue filterValue = filterObject->getIfPropertyExists(globalObject, builtinNames.filterPublicName())) { + if (filterValue.isCell() && filterValue.asCell()->inherits<JSC::RegExpObject>()) + filter = jsCast<JSC::RegExpObject*>(filterValue); + } + + if (!filter) { + throwException(globalObject, scope, createError(globalObject, "onLoad() expects first argument to be an object with a filter RegExp"_s)); + return JSValue::encode(jsUndefined()); + } + + String namespaceString = String(); + if (JSValue namespaceValue = filterObject->getIfPropertyExists(globalObject, Identifier::fromString(vm, "namespace"_s))) { + if (namespaceValue.isString()) { + namespaceString = namespaceValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + if (!isValidNamespaceString(namespaceString)) { + throwException(globalObject, scope, createError(globalObject, "namespace can only contain letters, numbers, dashes, or underscores"_s)); + return JSValue::encode(jsUndefined()); + } + } + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + } + + auto func = callframe->uncheckedArgument(1); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + + if (!func.isCell() || !func.isCallable()) { + throwException(globalObject, scope, createError(globalObject, "onLoad() expects second argument to be a function"_s)); + return JSValue::encode(jsUndefined()); + } + + plugin.onLoad.append(vm, filter->regExp(), jsCast<JSFunction*>(func), namespaceString); + + return JSValue::encode(jsUndefined()); +} + +static EncodedJSValue jsFunctionAppendOnResolvePluginBody(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe, JSBundlerPlugin& plugin) +{ + Ref protect(plugin); + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (callframe->argumentCount() < 2) { + throwException(globalObject, scope, createError(globalObject, "onResolve() requires at least 2 arguments"_s)); + return JSValue::encode(jsUndefined()); + } + + auto* filterObject = callframe->uncheckedArgument(0).toObject(globalObject); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + auto clientData = WebCore::clientData(vm); + auto& builtinNames = clientData->builtinNames(); + JSC::RegExpObject* filter = nullptr; + if (JSValue filterValue = filterObject->getIfPropertyExists(globalObject, builtinNames.filterPublicName())) { + if (filterValue.isCell() && filterValue.asCell()->inherits<JSC::RegExpObject>()) + filter = jsCast<JSC::RegExpObject*>(filterValue); + } + + if (!filter) { + throwException(globalObject, scope, createError(globalObject, "onResolve() expects first argument to be an object with a filter RegExp"_s)); + return JSValue::encode(jsUndefined()); + } + + String namespaceString = String(); + if (JSValue namespaceValue = filterObject->getIfPropertyExists(globalObject, Identifier::fromString(vm, "namespace"_s))) { + if (namespaceValue.isString()) { + namespaceString = namespaceValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + if (!isValidNamespaceString(namespaceString)) { + throwException(globalObject, scope, createError(globalObject, "namespace can only contain letters, numbers, dashes, or underscores"_s)); + return JSValue::encode(jsUndefined()); + } + } + + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + } + + auto func = callframe->uncheckedArgument(1); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + + if (!func.isCell() || !func.isCallable()) { + throwException(globalObject, scope, createError(globalObject, "onResolve() expects second argument to be a function"_s)); + return JSValue::encode(jsUndefined()); + } + + plugin.onResolve.append(vm, filter->regExp(), jsCast<JSFunction*>(func), namespaceString); + + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendOnLoadJSBundlerPlugin, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto& plugin = *UNWRAP_BUNDLER_PLUGIN(callframe); + return jsFunctionAppendOnLoadPluginBody(globalObject, callframe, plugin); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendOnResolveJSBundlerPlugin, (JSGlobalObject * globalObject, CallFrame* callframe)) +{ + auto& plugin = *UNWRAP_BUNDLER_PLUGIN(callframe); + return jsFunctionAppendOnResolvePluginBody(globalObject, callframe, plugin); +} + +extern "C" EncodedJSValue setupJSBundlerPlugin(JSBundlerPlugin* bundlerPlugin, JSC::JSGlobalObject* globalObject, JSValue objValue) +{ + JSC::VM& vm = globalObject->vm(); + auto clientData = WebCore::clientData(vm); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + if (!objValue || !objValue.isObject()) { + JSC::throwTypeError(globalObject, throwScope, "plugin needs to be an object"_s); + return JSValue::encode(throwScope.exception()); + } + + JSC::JSObject* obj = objValue.toObject(globalObject); + + JSC::JSValue setupFunctionValue = obj->getIfPropertyExists(globalObject, Identifier::fromString(vm, "setup"_s)); + if (!setupFunctionValue || setupFunctionValue.isUndefinedOrNull() || !setupFunctionValue.isCell() || !setupFunctionValue.isCallable()) { + JSC::throwTypeError(globalObject, throwScope, "plugin needs a setup() function"_s); + return JSValue::encode(throwScope.exception()); + } + + JSFunction* setupFunction = jsCast<JSFunction*>(setupFunctionValue); + JSObject* builderObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 3); + + JSC::JSFunction* onLoadFunction = JSC::JSFunction::create(vm, globalObject, 1, "onLoad"_s, jsFunctionAppendOnLoadJSBundlerPlugin, ImplementationVisibility::Public); + JSC::JSFunction* onResolveFunction = JSC::JSFunction::create(vm, globalObject, 1, "onResolve"_s, jsFunctionAppendOnResolveJSBundlerPlugin, ImplementationVisibility::Public); + JSC::JSBoundFunction* boundOnLoadFunction = JSC::JSBoundFunction::create( + vm, + globalObject, + onLoadFunction, + WRAP_BUNDLER_PLUGIN(bundlerPlugin), + JSC::ArgList(), + 1, + jsString(vm, String("onLoad"_s))); + + JSC::JSBoundFunction* boundOnResolveFunction = JSC::JSBoundFunction::create( + vm, + globalObject, + onResolveFunction, + WRAP_BUNDLER_PLUGIN(bundlerPlugin), + JSC::ArgList(), + 1, + jsString(vm, String("onResolve"_s))); + + bundlerPlugin->ref(); + vm.heap.addFinalizer(boundOnLoadFunction, [bundlerPlugin](JSC::JSCell* cell) { + bundlerPlugin->deref(); + }); + + bundlerPlugin->ref(); + vm.heap.addFinalizer(boundOnResolveFunction, [bundlerPlugin](JSC::JSCell* cell) { + bundlerPlugin->deref(); + }); + + builderObject->putDirect( + vm, + JSC::Identifier::fromString(vm, "onLoad"_s), + boundOnLoadFunction, + JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0); + builderObject->putDirect( + vm, + JSC::Identifier::fromString(vm, "onResolve"_s), + boundOnResolveFunction, + JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0); + + JSC::MarkedArgumentBuffer args; + args.append(builderObject); + + JSFunction* function = jsCast<JSFunction*>(setupFunctionValue); + JSC::CallData callData = JSC::getCallData(function); + JSValue result = call(globalObject, function, callData, JSC::jsUndefined(), args); + + RETURN_IF_EXCEPTION(throwScope, JSValue::encode(throwScope.exception())); + + if (auto* promise = JSC::jsDynamicCast<JSC::JSPromise*>(result)) { + RELEASE_AND_RETURN(throwScope, JSValue::encode(promise)); + } + + RELEASE_AND_RETURN(throwScope, JSValue::encode(jsUndefined())); +} + +void JSBundlerPlugin::Group::append(JSC::VM& vm, JSC::RegExp* filter, JSC::JSFunction* func) +{ + Yarr::RegularExpression regex( + StringView(filter->pattern()), + filter->flags().contains(Yarr::Flags::IgnoreCase) ? Yarr::TextCaseSensitivity::TextCaseInsensitive : Yarr::TextCaseSensitivity::TextCaseInsensitive, + filter->multiline() ? Yarr::MultilineMode::MultilineEnabled : Yarr::MultilineMode::MultilineDisabled, + filter->eitherUnicode() ? Yarr::UnicodeMode::UnicodeAwareMode : Yarr::UnicodeMode::UnicodeUnawareMode); + filters.append(WTFMove(regex)); + callbacks.append(JSC::Strong<JSC::JSFunction> { vm, func }); +} + +void JSBundlerPlugin::Base::append(JSC::VM& vm, JSC::RegExp* filter, JSC::JSFunction* func, String& namespaceString) +{ + if (namespaceString.isEmpty() || namespaceString == "file"_s) { + this->fileNamespace.append(vm, filter, func); + } else if (auto found = this->group(namespaceString)) { + found->append(vm, filter, func); + } else { + Group newGroup; + newGroup.append(vm, filter, func); + this->groups.append(WTFMove(newGroup)); + this->namespaces.append(namespaceString); + } +} + +JSFunction* JSBundlerPlugin::Group::find(String& path) +{ + size_t count = filters.size(); + for (size_t i = 0; i < count; i++) { + int matchLength = 0; + if (filters[i].match(path, 0, &matchLength)) { + return callbacks[i].get(); + } + } + + return nullptr; +} + +EncodedJSValue JSBundlerPlugin::OnResolve::run(const ZigString* namespaceString, const ZigString* path, const ZigString* importer, void* context) +{ + Group* groupPtr = this->group(namespaceString ? Zig::toString(*namespaceString) : String()); + if (groupPtr == nullptr) { + return JSValue::encode(jsUndefined()); + } + Group& group = *groupPtr; + + auto pathString = Zig::toString(*path); + + JSC::JSFunction* function = group.find(pathString); + if (!function) { + return JSValue::encode(JSC::jsUndefined()); + } + + JSC::MarkedArgumentBuffer arguments; + JSC::JSGlobalObject* globalObject = function->globalObject(); + auto& vm = globalObject->vm(); + + JSC::JSObject* paramsObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 2); + auto clientData = WebCore::clientData(vm); + auto& builtinNames = clientData->builtinNames(); + paramsObject->putDirect( + vm, clientData->builtinNames().pathPublicName(), + Zig::toJSStringValue(*path, globalObject)); + paramsObject->putDirect( + vm, clientData->builtinNames().importerPublicName(), + Zig::toJSStringValue(*importer, globalObject)); + arguments.append(paramsObject); + + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto scope = DECLARE_CATCH_SCOPE(vm); + scope.assertNoExceptionExceptTermination(); + + JSC::CallData callData = JSC::getCallData(function); + + auto result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); + if (UNLIKELY(scope.exception())) { + return JSValue::encode(scope.exception()); + } + + if (auto* promise = JSC::jsDynamicCast<JSPromise*>(result)) { + switch (promise->status(vm)) { + case JSPromise::Status::Pending: { + return JSValue::encode(promise); + } + case JSPromise::Status::Rejected: { + promise->internalField(JSC::JSPromise::Field::Flags).set(vm, promise, jsNumber(static_cast<unsigned>(JSC::JSPromise::Status::Fulfilled))); + result = promise->result(vm); + return JSValue::encode(result); + } + case JSPromise::Status::Fulfilled: { + result = promise->result(vm); + break; + } + } + } + + if (!result.isObject()) { + JSC::throwTypeError(globalObject, throwScope, "onLoad() expects an object returned"_s); + return JSValue::encode({}); + } + + RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); +} + +EncodedJSValue JSBundlerPlugin::OnLoad::run(const ZigString* namespaceString, const ZigString* path, void* context) +{ + Group* groupPtr = this->group(namespaceString ? Zig::toString(*namespaceString) : String()); + if (groupPtr == nullptr) { + return JSValue::encode(jsUndefined()); + } + Group& group = *groupPtr; + + auto pathString = Zig::toString(*path); + + JSC::JSFunction* function = group.find(pathString); + if (!function) { + return JSValue::encode(JSC::jsUndefined()); + } + + JSC::MarkedArgumentBuffer arguments; + JSC::JSGlobalObject* globalObject = function->globalObject(); + auto& vm = globalObject->vm(); + + auto& callbacks = group.callbacks; + + auto& filters = group.filters; + + for (size_t i = 0; i < filters.size(); i++) { + if (!filters[i].match(pathString)) { + continue; + } + JSC::JSFunction* function = callbacks[i].get(); + if (UNLIKELY(!function)) { + continue; + } + + JSC::MarkedArgumentBuffer arguments; + JSC::VM& vm = globalObject->vm(); + + JSC::JSObject* paramsObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 1); + auto clientData = WebCore::clientData(vm); + auto& builtinNames = clientData->builtinNames(); + paramsObject->putDirect( + vm, clientData->builtinNames().pathPublicName(), + Zig::toJSStringValue(*path, globalObject)); + arguments.append(paramsObject); + + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto scope = DECLARE_CATCH_SCOPE(vm); + scope.assertNoExceptionExceptTermination(); + + JSC::CallData callData = JSC::getCallData(function); + + auto result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); + + if (UNLIKELY(!scope.exception() && result && !result.isUndefinedOrNull() && !result.isCell())) { + throwTypeError(globalObject, throwScope, "onLoad() expects an object returned"_s); + } + + if (UNLIKELY(scope.exception())) { + JSC::Exception* exception = scope.exception(); + scope.clearException(); + return JSValue::encode(exception); + } + + result = Bun::handleVirtualModuleResultForJSBundlerPlugin( + reinterpret_cast<Zig::GlobalObject*>(globalObject), + result, + path, + nullptr, + context); + + if (UNLIKELY(scope.exception())) { + JSC::Exception* exception = scope.exception(); + scope.clearException(); + return JSValue::encode(exception); + } + + if (!result || result.isUndefined()) { + RELEASE_AND_RETURN(throwScope, JSValue::encode(jsUndefined())); + } + + if (auto* promise = JSC::jsDynamicCast<JSPromise*>(result)) { + switch (promise->status(vm)) { + case JSPromise::Status::Pending: { + RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); + } + case JSPromise::Status::Rejected: { + promise->internalField(JSC::JSPromise::Field::Flags).set(vm, promise, jsNumber(static_cast<unsigned>(JSC::JSPromise::Status::Fulfilled))); + result = promise->result(vm); + RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); + } + case JSPromise::Status::Fulfilled: { + result = promise->result(vm); + break; + } + } + } + + if (!result.isObject()) { + JSC::throwTypeError(globalObject, throwScope, "onResolve() expects an object returned"_s); + JSC::Exception* exception = scope.exception(); + scope.clearException(); + return JSValue::encode(exception); + } + + RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); + } + + return JSValue::encode(JSC::jsUndefined()); +} + +bool JSBundlerPlugin::anyMatchesCrossThread(const ZigString* namespaceStr, const ZigString* path, bool isOnLoad) +{ + auto namespaceString = namespaceStr ? Zig::toString(*namespaceStr) : String(); + auto pathString = Zig::toString(*path); + + if (isOnLoad) { + auto* group = this->onLoad.group(namespaceString); + if (group == nullptr) { + return false; + } + + auto& filters = group->filters; + + for (auto& filter : filters) { + if (filter.match(pathString) > -1) { + return true; + } + } + + } else { + auto* group = this->onResolve.group(namespaceString); + if (group == nullptr) { + return false; + } + + auto& filters = group->filters; + + for (auto& filter : filters) { + if (filter.match(pathString) > -1) { + return true; + } + } + } + + return false; +} + +} // namespace Bun + +extern "C" bool JSBundlerPlugin__anyMatches(Bun::JSBundlerPlugin* plugin, const ZigString* namespaceString, const ZigString* path, bool isOnLoad) +{ + return plugin->anyMatchesCrossThread(namespaceString, path, isOnLoad); +} + +extern "C" JSC::EncodedJSValue JSBundlerPlugin__matchOnLoad(JSC::JSGlobalObject* globalObject, Bun::JSBundlerPlugin* plugin, const ZigString* namespaceString, const ZigString* path, void* context) +{ + Ref protect(*plugin); + return plugin->onLoad.run( + namespaceString, + path, + context); +} + +extern "C" JSC::EncodedJSValue JSBundlerPlugin__matchOnResolve(JSC::JSGlobalObject* globalObject, Bun::JSBundlerPlugin* plugin, const ZigString* namespaceString, const ZigString* path, const ZigString* importer, void* context) +{ + Ref protect(*plugin); + return plugin->onResolve.run( + namespaceString, + path, + importer, + context); +} + +extern "C" Bun::JSBundlerPlugin* JSBundlerPlugin__create(Zig::GlobalObject* globalObject, BunPluginTarget target) +{ + RefPtr<Bun::JSBundlerPlugin> plugin = adoptRef(*new Bun::JSBundlerPlugin(target, nullptr)); + plugin->ref(); + return plugin.leakRef(); +} + +extern "C" void JSBundlerPlugin__setConfig(Bun::JSBundlerPlugin* plugin, void* config) +{ + plugin->config = config; +} + +extern "C" void JSBundlerPlugin__tombestone(Bun::JSBundlerPlugin* plugin) +{ + plugin->tombstone(); + plugin->deref(); +} diff --git a/src/bun.js/bindings/JSBundlerPlugin.h b/src/bun.js/bindings/JSBundlerPlugin.h new file mode 100644 index 000000000..fde124ccf --- /dev/null +++ b/src/bun.js/bindings/JSBundlerPlugin.h @@ -0,0 +1,103 @@ +#pragma once + +#include "root.h" +#include "headers-handwritten.h" +#include "JavaScriptCore/JSGlobalObject.h" +#include "JavaScriptCore/Strong.h" +#include "JavaScriptCore/RegularExpression.h" +#include "helpers.h" +#include <JavaScriptCore/Yarr.h> + +namespace Bun { + +using namespace JSC; + +class JSBundlerPlugin final : public WTF::RefCounted<JSBundlerPlugin> { +public: + WTF_MAKE_ISO_ALLOCATED(JSBundlerPlugin); + static JSBundlerPlugin* create(JSC::JSGlobalObject* globalObject, BunPluginTarget target, void* config = nullptr); + + // This is a list of pairs of regexps and functions to match against + class Group { + + public: + Vector<Yarr::RegularExpression> filters = {}; + Vector<JSC::Strong<JSC::JSFunction>> callbacks = {}; + + void append(JSC::VM& vm, JSC::RegExp* filter, JSC::JSFunction* func); + JSFunction* find(String& path); + void clear() + { + filters.clear(); + callbacks.clear(); + } + }; + + class Base { + public: + Group fileNamespace = {}; + Vector<String> namespaces = {}; + Vector<Group> groups = {}; + BunPluginTarget target { BunPluginTargetBun }; + + Group* group(const String& namespaceStr) + { + if (namespaceStr.isEmpty()) { + return &fileNamespace; + } + + for (size_t i = 0; i < namespaces.size(); i++) { + if (namespaces[i] == namespaceStr) { + return &groups[i]; + } + } + + return nullptr; + } + + void append(JSC::VM& vm, JSC::RegExp* filter, JSC::JSFunction* func, String& namespaceString); + }; + + class OnLoad final : public Base { + + public: + OnLoad() + : Base() + { + } + + EncodedJSValue run(const ZigString* namespaceString, const ZigString* path, void* context); + }; + + class OnResolve final : public Base { + + public: + OnResolve() + : Base() + { + } + + EncodedJSValue run(const ZigString* namespaceString, const ZigString* path, const ZigString* importer, void* context); + }; + +public: + bool anyMatchesCrossThread(const ZigString* namespaceStr, const ZigString* path, bool isOnLoad); + void tombstone() { tombstoned = true; } + + JSBundlerPlugin(BunPluginTarget target, void* config) + { + this->target = target; + this->config = config; + } + + OnLoad onLoad = {}; + OnResolve onResolve = {}; + BunPluginTarget target { BunPluginTargetBrowser }; + void* config { nullptr }; + bool tombstoned { false }; + + using RefCounted::deref; + using RefCounted::ref; +}; + +} // namespace Zig
\ No newline at end of file diff --git a/src/bun.js/bindings/ModuleLoader.cpp b/src/bun.js/bindings/ModuleLoader.cpp index 40e41b083..00b975d88 100644 --- a/src/bun.js/bindings/ModuleLoader.cpp +++ b/src/bun.js/bindings/ModuleLoader.cpp @@ -40,7 +40,56 @@ namespace Bun { using namespace Zig; using namespace WebCore; -extern "C" BunLoaderType Bun__getDefaultLoader(JSC::JSGlobalObject*, ZigString* specifier); +extern "C" BunLoaderType Bun__getDefaultLoader(JSC::JSGlobalObject*, const ZigString* specifier); +extern "C" BunLoaderType JSBundlerPlugin__getDefaultLoader(void* context); +extern "C" void JSBundlerPlugin__OnLoadAsync(void* ctx, EncodedJSValue errorValue, ZigString* sourceCode, BunLoaderType loader); +OnLoadResult handleOnLoadResult(Zig::GlobalObject* globalObject, JSC::JSValue objectValue, const ZigString* specifier, void* context); + +JSValue handleVirtualModuleResultForJSBundlerPlugin( + Zig::GlobalObject* globalObject, + JSValue virtualModuleResult, + const ZigString* specifier, + const ZigString* referrer, + void* bundlerPluginContext) +{ + auto onLoadResult = handleOnLoadResult(globalObject, virtualModuleResult, specifier, bundlerPluginContext); + JSC::VM& vm = globalObject->vm(); + + switch (onLoadResult.type) { + case OnLoadResultTypeCode: { + JSBundlerPlugin__OnLoadAsync(bundlerPluginContext, JSValue::encode({}), &onLoadResult.value.sourceText.string, onLoadResult.value.sourceText.loader); + return jsUndefined(); + } + case OnLoadResultTypeError: { + JSBundlerPlugin__OnLoadAsync(bundlerPluginContext, JSValue::encode(onLoadResult.value.error), nullptr, BunLoaderTypeNone); + return jsUndefined(); + } + + case OnLoadResultTypePromise: { + JSFunction* performPromiseThenFunction = globalObject->performPromiseThenFunction(); + auto callData = JSC::getCallData(performPromiseThenFunction); + ASSERT(callData.type != CallData::Type::None); + auto specifierString = Zig::toString(*specifier); + auto referrerString = referrer ? Zig::toString(*referrer) : String(); + PendingVirtualModuleResult* pendingModule = PendingVirtualModuleResult::create(globalObject, specifierString, referrerString, bundlerPluginContext); + pendingModule->internalField(2).set(vm, pendingModule, virtualModuleResult); + JSC::JSPromise* promise = pendingModule->promise(); + + MarkedArgumentBuffer arguments; + arguments.append(promise); + arguments.append(globalObject->thenable(jsFunctionOnLoadObjectResultResolveForJSBundlerPlugin)); + arguments.append(globalObject->thenable(jsFunctionOnLoadObjectResultRejectForJSBundlerPlugin)); + arguments.append(jsUndefined()); + arguments.append(pendingModule); + ASSERT(!arguments.hasOverflowed()); + JSC::call(globalObject, performPromiseThenFunction, callData, jsUndefined(), arguments); + return promise; + } + default: { + __builtin_unreachable(); + } + } +} static JSC::JSInternalPromise* rejectedInternalPromise(JSC::JSGlobalObject* globalObject, JSC::JSValue value) { @@ -88,11 +137,16 @@ JSC::JSInternalPromise* PendingVirtualModuleResult::internalPromise() return jsCast<JSC::JSInternalPromise*>(internalField(2).get()); } +JSC::JSPromise* PendingVirtualModuleResult::promise() +{ + return jsCast<JSC::JSPromise*>(internalField(2).get()); +} + const ClassInfo PendingVirtualModuleResult::s_info = { "PendingVirtualModule"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(PendingVirtualModuleResult) }; -PendingVirtualModuleResult* PendingVirtualModuleResult::create(VM& vm, Structure* structure) +PendingVirtualModuleResult* PendingVirtualModuleResult::create(VM& vm, Structure* structure, void* bundlerPluginContext) { - PendingVirtualModuleResult* mod = new (NotNull, allocateCell<PendingVirtualModuleResult>(vm)) PendingVirtualModuleResult(vm, structure); + PendingVirtualModuleResult* mod = new (NotNull, allocateCell<PendingVirtualModuleResult>(vm)) PendingVirtualModuleResult(vm, structure, bundlerPluginContext); return mod; } Structure* PendingVirtualModuleResult::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) @@ -100,8 +154,9 @@ Structure* PendingVirtualModuleResult::createStructure(VM& vm, JSGlobalObject* g return Structure::create(vm, globalObject, prototype, TypeInfo(CellType, StructureFlags), info()); } -PendingVirtualModuleResult::PendingVirtualModuleResult(VM& vm, Structure* structure) +PendingVirtualModuleResult::PendingVirtualModuleResult(VM& vm, Structure* structure, void* bundlerPluginContext) : Base(vm, structure) + , m_bundlerPluginContext(bundlerPluginContext) { } @@ -110,7 +165,9 @@ void PendingVirtualModuleResult::finishCreation(VM& vm, const WTF::String& speci Base::finishCreation(vm); Base::internalField(0).set(vm, this, JSC::jsString(vm, specifier)); Base::internalField(1).set(vm, this, JSC::jsString(vm, referrer)); - Base::internalField(2).set(vm, this, JSC::JSInternalPromise::create(vm, globalObject()->internalPromiseStructure())); + if (!this->m_bundlerPluginContext) { + Base::internalField(2).set(vm, this, JSC::JSInternalPromise::create(vm, globalObject()->internalPromiseStructure())); + } } template<typename Visitor> @@ -123,21 +180,22 @@ void PendingVirtualModuleResult::visitChildrenImpl(JSCell* cell, Visitor& visito DEFINE_VISIT_CHILDREN(PendingVirtualModuleResult); -PendingVirtualModuleResult* PendingVirtualModuleResult::create(JSC::JSGlobalObject* globalObject, const WTF::String& specifier, const WTF::String& referrer) +PendingVirtualModuleResult* PendingVirtualModuleResult::create(JSC::JSGlobalObject* globalObject, const WTF::String& specifier, const WTF::String& referrer, void* bundlerPluginContext) { - auto* virtualModule = create(globalObject->vm(), reinterpret_cast<Zig::GlobalObject*>(globalObject)->pendingVirtualModuleResultStructure()); + auto* virtualModule = create(globalObject->vm(), reinterpret_cast<Zig::GlobalObject*>(globalObject)->pendingVirtualModuleResultStructure(), bundlerPluginContext); virtualModule->finishCreation(globalObject->vm(), specifier, referrer); return virtualModule; } -OnLoadResult handleOnLoadResultNotPromise(Zig::GlobalObject* globalObject, JSC::JSValue objectValue, ZigString* specifier) +OnLoadResult handleOnLoadResultNotPromise(Zig::GlobalObject* globalObject, JSC::JSValue objectValue, const ZigString* specifier, void* bunPluginContext) { OnLoadResult result = {}; result.type = OnLoadResultTypeError; + result.bundlerPluginContext = bunPluginContext; JSC::VM& vm = globalObject->vm(); result.value.error = JSC::jsUndefined(); auto scope = DECLARE_THROW_SCOPE(vm); - BunLoaderType loader = Bun__getDefaultLoader(globalObject, specifier); + BunLoaderType loader = bunPluginContext ? JSBundlerPlugin__getDefaultLoader(bunPluginContext) : Bun__getDefaultLoader(globalObject, specifier); if (JSC::Exception* exception = JSC::jsDynamicCast<JSC::Exception*>(objectValue)) { result.value.error = exception->value(); @@ -211,16 +269,17 @@ OnLoadResult handleOnLoadResultNotPromise(Zig::GlobalObject* globalObject, JSC:: return result; } -static OnLoadResult handleOnLoadResult(Zig::GlobalObject* globalObject, JSC::JSValue objectValue, ZigString* specifier) +OnLoadResult handleOnLoadResult(Zig::GlobalObject* globalObject, JSC::JSValue objectValue, const ZigString* specifier, void* context) { if (JSC::JSPromise* promise = JSC::jsDynamicCast<JSC::JSPromise*>(objectValue)) { OnLoadResult result = {}; result.type = OnLoadResultTypePromise; result.value.promise = objectValue; + result.bundlerPluginContext = context; return result; } - return handleOnLoadResultNotPromise(globalObject, objectValue, specifier); + return handleOnLoadResultNotPromise(globalObject, objectValue, specifier, context); } template<bool allowPromise> @@ -228,10 +287,10 @@ static JSValue handleVirtualModuleResult( Zig::GlobalObject* globalObject, JSValue virtualModuleResult, ErrorableResolvedSource* res, - ZigString* specifier, - ZigString* referrer) + const ZigString* specifier, + const ZigString* referrer) { - auto onLoadResult = handleOnLoadResult(globalObject, virtualModuleResult, specifier); + auto onLoadResult = handleOnLoadResult(globalObject, virtualModuleResult, specifier, nullptr); JSC::VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -353,8 +412,8 @@ template<bool allowPromise> static JSValue fetchSourceCode( Zig::GlobalObject* globalObject, ErrorableResolvedSource* res, - ZigString* specifier, - ZigString* referrer) + const ZigString* specifier, + const ZigString* referrer) { void* bunVM = globalObject->bunVM(); auto& vm = globalObject->vm(); @@ -486,6 +545,46 @@ static JSValue fetchSourceCode( return rejectOrResolve(JSC::JSSourceCode::create(vm, JSC::SourceCode(WTFMove(provider)))); } +extern "C" JSC::EncodedJSValue jsFunctionOnLoadObjectResultResolveForJSBundlerPlugin(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) +{ + JSC::VM& vm = globalObject->vm(); + ErrorableResolvedSource res = {}; + res.success = false; + JSC::JSValue objectResult = callFrame->argument(0); + PendingVirtualModuleResult* pendingModule = JSC::jsCast<PendingVirtualModuleResult*>(callFrame->argument(1)); + JSC::JSValue specifierString = pendingModule->internalField(0).get(); + JSC::JSValue referrerString = pendingModule->internalField(1).get(); + pendingModule->internalField(0).set(vm, pendingModule, JSC::jsUndefined()); + pendingModule->internalField(1).set(vm, pendingModule, JSC::jsUndefined()); + void* bunPluginContext = pendingModule->m_bundlerPluginContext; + JSC::JSPromise* promise = pendingModule->promise(); + + ZigString specifier = Zig::toZigString(specifierString, globalObject); + ZigString referrer = Zig::toZigString(referrerString, globalObject); + return JSC::JSValue::encode( + handleVirtualModuleResultForJSBundlerPlugin(reinterpret_cast<Zig::GlobalObject*>(globalObject), objectResult, &specifier, &referrer, bunPluginContext)); +} + +extern "C" JSC::EncodedJSValue jsFunctionOnLoadObjectResultRejectForJSBundlerPlugin(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) +{ + JSC::VM& vm = globalObject->vm(); + ErrorableResolvedSource res = {}; + JSC::JSValue reason = callFrame->argument(0); + PendingVirtualModuleResult* pendingModule = JSC::jsCast<PendingVirtualModuleResult*>(callFrame->argument(1)); + JSC::JSValue specifierString = pendingModule->internalField(0).get(); + JSC::JSValue referrerString = pendingModule->internalField(1).get(); + pendingModule->internalField(0).set(vm, pendingModule, JSC::jsUndefined()); + pendingModule->internalField(1).set(vm, pendingModule, JSC::jsUndefined()); + + ZigString specifier = Zig::toZigString(specifierString, globalObject); + ZigString referrer = Zig::toZigString(referrerString, globalObject); + pendingModule->internalField(2).set(vm, pendingModule, JSC::jsUndefined()); + + JSBundlerPlugin__OnLoadAsync(pendingModule->m_bundlerPluginContext, JSValue::encode(reason), nullptr, BunLoaderTypeNone); + + return JSValue::encode(reason); +} + extern "C" JSC::EncodedJSValue jsFunctionOnLoadObjectResultResolve(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) { JSC::VM& vm = globalObject->vm(); @@ -544,8 +643,8 @@ extern "C" JSC::EncodedJSValue jsFunctionOnLoadObjectResultReject(JSC::JSGlobalO JSValue fetchSourceCodeSync( Zig::GlobalObject* globalObject, ErrorableResolvedSource* res, - ZigString* specifier, - ZigString* referrer) + const ZigString* specifier, + const ZigString* referrer) { return fetchSourceCode<false>(globalObject, res, specifier, referrer); } @@ -553,8 +652,8 @@ JSValue fetchSourceCodeSync( JSValue fetchSourceCodeAsync( Zig::GlobalObject* globalObject, ErrorableResolvedSource* res, - ZigString* specifier, - ZigString* referrer) + const ZigString* specifier, + const ZigString* referrer) { return fetchSourceCode<true>(globalObject, res, specifier, referrer); } diff --git a/src/bun.js/bindings/ModuleLoader.h b/src/bun.js/bindings/ModuleLoader.h index 9090abcdc..f24a8fdb1 100644 --- a/src/bun.js/bindings/ModuleLoader.h +++ b/src/bun.js/bindings/ModuleLoader.h @@ -37,6 +37,7 @@ union OnLoadResultValue { struct OnLoadResult { OnLoadResultValue value; OnLoadResultType type; + void* bundlerPluginContext { nullptr }; }; class PendingVirtualModuleResult : public JSC::JSInternalFieldObjectImpl<3> { @@ -55,12 +56,13 @@ public: [](auto& spaces, auto&& space) { spaces.m_subspaceForPendingVirtualModuleResult = std::forward<decltype(space)>(space); }); } - JS_EXPORT_PRIVATE static PendingVirtualModuleResult* create(VM&, Structure*); - static PendingVirtualModuleResult* create(JSC::JSGlobalObject* globalObject, const WTF::String& specifier, const WTF::String& referrer); + JS_EXPORT_PRIVATE static PendingVirtualModuleResult* create(VM&, Structure*, void* bundlerPluginContext = nullptr); + static PendingVirtualModuleResult* create(JSC::JSGlobalObject* globalObject, const WTF::String& specifier, const WTF::String& referrer, void* bundlerPluginContext = nullptr); static PendingVirtualModuleResult* createWithInitialValues(VM&, Structure*); static Structure* createStructure(VM&, JSGlobalObject*, JSValue); JSC::JSInternalPromise* internalPromise(); + JSC::JSPromise* promise(); static std::array<JSValue, numberOfInternalFields> initialValues() { @@ -74,21 +76,32 @@ public: DECLARE_EXPORT_INFO; DECLARE_VISIT_CHILDREN; - PendingVirtualModuleResult(JSC::VM&, JSC::Structure*); + void* m_bundlerPluginContext { nullptr }; + + PendingVirtualModuleResult(JSC::VM&, JSC::Structure*, void* bundlerPluginContext = nullptr); void finishCreation(JSC::VM&, const WTF::String& specifier, const WTF::String& referrer); }; -OnLoadResult handleOnLoadResultNotPromise(Zig::GlobalObject* globalObject, JSC::JSValue objectValue); +OnLoadResult handleOnLoadResult(Zig::GlobalObject* globalObject, JSC::JSValue objectValue, ZigString* specifier, void* bunPluginContext = nullptr); +OnLoadResult handleOnLoadResultNotPromise(Zig::GlobalObject* globalObject, JSC::JSValue objectValue, void* bunPluginContext = nullptr); + +JSValue handleVirtualModuleResultForJSBundlerPlugin( + Zig::GlobalObject* globalObject, + JSValue virtualModuleResult, + const ZigString* specifier, + const ZigString* referrer, + void* bundlerPluginContext); + JSValue fetchSourceCodeSync( Zig::GlobalObject* globalObject, ErrorableResolvedSource* res, - ZigString* specifier, - ZigString* referrer); + const ZigString* specifier, + const ZigString* referrer); JSValue fetchSourceCodeAsync( Zig::GlobalObject* globalObject, ErrorableResolvedSource* res, - ZigString* specifier, - ZigString* referrer); + const ZigString* specifier, + const ZigString* referrer); } // namespace Bun
\ No newline at end of file diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index b969acf4c..a00d49343 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -3944,6 +3944,10 @@ GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(EncodedJSValue (*h return GlobalObject::PromiseFunctions::Bun__TestScope__onReject; } else if (handler == Bun__TestScope__onResolve) { return GlobalObject::PromiseFunctions::Bun__TestScope__onResolve; + } else if (handler == jsFunctionOnLoadObjectResultRejectForJSBundlerPlugin) { + return GlobalObject::PromiseFunctions::jsFunctionOnLoadObjectResultResolveForJSBundlerPlugin; + } else if (handler == jsFunctionOnLoadObjectResultResolveForJSBundlerPlugin) { + return GlobalObject::PromiseFunctions::jsFunctionOnLoadObjectResultRejectForJSBundlerPlugin; } else if (handler == CallbackJob__onResolve) { return GlobalObject::PromiseFunctions::CallbackJob__onResolve; } else if (handler == CallbackJob__onReject) { diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 0bdfd1d82..e16230b32 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -48,6 +48,8 @@ extern "C" void Bun__reportUnhandledError(JSC__JSGlobalObject*, JSC::EncodedJSVa // defined in ModuleLoader.cpp extern "C" JSC::EncodedJSValue jsFunctionOnLoadObjectResultResolve(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame); extern "C" JSC::EncodedJSValue jsFunctionOnLoadObjectResultReject(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame); +extern "C" JSC::EncodedJSValue jsFunctionOnLoadObjectResultResolveForJSBundlerPlugin(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame); +extern "C" JSC::EncodedJSValue jsFunctionOnLoadObjectResultRejectForJSBundlerPlugin(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame); // #include "EventTarget.h" // namespace WebCore { @@ -292,6 +294,8 @@ public: jsFunctionOnLoadObjectResultResolve, jsFunctionOnLoadObjectResultReject, + jsFunctionOnLoadObjectResultResolveForJSBundlerPlugin, + jsFunctionOnLoadObjectResultRejectForJSBundlerPlugin, Bun__TestScope__onReject, Bun__TestScope__onResolve, @@ -299,7 +303,7 @@ public: CallbackJob__onResolve, CallbackJob__onReject, }; - static constexpr size_t promiseFunctionsSize = 22; + static constexpr size_t promiseFunctionsSize = 24; static PromiseFunctions promiseHandlerID(EncodedJSValue (*handler)(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1)); diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index d4e1e72af..24a540320 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -119,17 +119,17 @@ const JSErrorCode JSErrorCodeStackOverflow = 253; const JSErrorCode JSErrorCodeUserErrorCode = 254; typedef uint8_t BunLoaderType; -const BunLoaderType BunLoaderTypeNone = 0; -const BunLoaderType BunLoaderTypeJSX = 1; -const BunLoaderType BunLoaderTypeJS = 2; -const BunLoaderType BunLoaderTypeTS = 3; -const BunLoaderType BunLoaderTypeTSX = 4; -const BunLoaderType BunLoaderTypeCSS = 5; -const BunLoaderType BunLoaderTypeFILE = 6; -const BunLoaderType BunLoaderTypeJSON = 7; -const BunLoaderType BunLoaderTypeTOML = 8; -const BunLoaderType BunLoaderTypeWASM = 9; -const BunLoaderType BunLoaderTypeNAPI = 10; +const BunLoaderType BunLoaderTypeNone = 254; +const BunLoaderType BunLoaderTypeJSX = 0; +const BunLoaderType BunLoaderTypeJS = 1; +const BunLoaderType BunLoaderTypeTS = 2; +const BunLoaderType BunLoaderTypeTSX = 3; +const BunLoaderType BunLoaderTypeCSS = 4; +const BunLoaderType BunLoaderTypeFILE = 5; +const BunLoaderType BunLoaderTypeJSON = 6; +const BunLoaderType BunLoaderTypeTOML = 7; +const BunLoaderType BunLoaderTypeWASM = 8; +const BunLoaderType BunLoaderTypeNAPI = 9; #pragma mark - Stream @@ -220,21 +220,21 @@ extern "C" void Microtask__run_default(void* ptr, void* global); extern "C" bool Bun__transpileVirtualModule( JSC::JSGlobalObject* global, - ZigString* specifier, - ZigString* referrer, + const ZigString* specifier, + const ZigString* referrer, ZigString* sourceCode, BunLoaderType loader, ErrorableResolvedSource* result); extern "C" JSC::EncodedJSValue Bun__runVirtualModule( JSC::JSGlobalObject* global, - ZigString* specifier); + const ZigString* specifier); extern "C" void* Bun__transpileFile( void* bunVM, JSC::JSGlobalObject* global, - ZigString* specifier, - ZigString* referrer, + const ZigString* specifier, + const ZigString* referrer, ErrorableResolvedSource* result, bool allowPromise); extern "C" JSC::EncodedJSValue CallbackJob__onResolve(JSC::JSGlobalObject*, JSC::CallFrame*); @@ -243,8 +243,8 @@ extern "C" JSC::EncodedJSValue CallbackJob__onReject(JSC::JSGlobalObject*, JSC:: extern "C" bool Bun__fetchBuiltinModule( void* bunVM, JSC::JSGlobalObject* global, - ZigString* specifier, - ZigString* referrer, + const ZigString* specifier, + const ZigString* referrer, ErrorableResolvedSource* result); // Used in process.version diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index ede5622dc..7e20e61bc 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -849,7 +849,7 @@ pub const ModuleLoader = struct { ) void; }; - pub export fn Bun__getDefaultLoader(global: *JSC.JSGlobalObject, str: *ZigString) Api.Loader { + pub export fn Bun__getDefaultLoader(global: *JSC.JSGlobalObject, str: *const ZigString) Api.Loader { var jsc_vm = global.bunVM(); const filename = str.toSlice(jsc_vm.allocator); defer filename.deinit(); @@ -1468,8 +1468,8 @@ pub const ModuleLoader = struct { pub export fn Bun__transpileFile( jsc_vm: *VirtualMachine, globalObject: *JSC.JSGlobalObject, - specifier_ptr: *ZigString, - referrer: *ZigString, + specifier_ptr: *const ZigString, + referrer: *const ZigString, ret: *ErrorableResolvedSource, allow_promise: bool, ) ?*anyopaque { @@ -1522,7 +1522,7 @@ pub const ModuleLoader = struct { return promise; } - export fn Bun__runVirtualModule(globalObject: *JSC.JSGlobalObject, specifier_ptr: *ZigString) JSValue { + export fn Bun__runVirtualModule(globalObject: *JSC.JSGlobalObject, specifier_ptr: *const ZigString) JSValue { JSC.markBinding(@src()); if (globalObject.bunVM().plugin_runner == null) return JSValue.zero; @@ -2084,8 +2084,8 @@ pub const ModuleLoader = struct { export fn Bun__transpileVirtualModule( globalObject: *JSC.JSGlobalObject, - specifier_ptr: *ZigString, - referrer_ptr: *ZigString, + specifier_ptr: *const ZigString, + referrer_ptr: *const ZigString, source_code: *ZigString, loader_: Api.Loader, ret: *ErrorableResolvedSource, diff --git a/src/bun.zig b/src/bun.zig index e6349fee9..336757ded 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1348,6 +1348,7 @@ pub const StringMap = struct { pub const DotEnv = @import("./env_loader.zig"); pub const BundleV2 = @import("./bundler/bundle_v2.zig").BundleV2; +pub const ParseTask = @import("./bundler/bundle_v2.zig").ParseTask; pub const Lock = @import("./lock.zig").Lock; pub const UnboundedQueue = @import("./bun.js/unbounded_queue.zig").UnboundedQueue; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index edfbc50eb..335edc033 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -267,6 +267,11 @@ pub const BundleV2 = struct { graph: Graph = Graph{}, linker: LinkerContext = LinkerContext{ .loop = undefined }, bun_watcher: ?*Watcher.Watcher = null, + plugins: ?*JSC.API.JSBundler.Plugin = null, + completion: ?*JSBundleCompletionTask = null, + + /// Allocations not tracked by a threadlocal heap + free_list: std.ArrayList(string) = std.ArrayList(string).init(bun.default_allocator), const debug = Output.scoped(.Bundle, false); @@ -399,7 +404,12 @@ pub const BundleV2 = struct { task.jsx = this.bundler.options.jsx; task.task.node.next = null; task.tree_shaking = this.linker.options.tree_shaking; - batch.push(ThreadPoolLib.Batch.from(&task.task)); + + // Handle onLoad plugins as entry points + if (!this.enqueueOnLoadPluginIfNeeded(task)) { + batch.push(ThreadPoolLib.Batch.from(&task.task)); + } + return source_index.get(); } @@ -577,6 +587,7 @@ pub const BundleV2 = struct { 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, @@ -587,12 +598,16 @@ pub const BundleV2 = struct { .jsc_event_loop = event_loop, .promise = JSC.JSPromise.Strong.init(globalThis), .globalThis = globalThis, - .ref = JSC.PollRef.init(), + .poll_ref = JSC.PollRef.init(), .env = globalThis.bunVM().bundler.env, + .plugins = plugins, .log = Logger.Log.init(bun.default_allocator), .task = JSBundleCompletionTask.TaskCompletion.init(completion), }; + if (plugins) |plugin| + plugin.setConfig(completion); + // Ensure this exists before we spawn the thread to prevent any race // conditions from creating two _ = JSC.WorkPool.get(); @@ -613,7 +628,7 @@ pub const BundleV2 = struct { BundleThread.instance.waker.wake() catch {}; } - completion.ref.ref(globalThis.bunVM()); + completion.poll_ref.ref(globalThis.bunVM()); return completion.promise.value(); } @@ -628,13 +643,16 @@ pub const BundleV2 = struct { task: bun.JSC.AnyTask, globalThis: *JSC.JSGlobalObject, promise: JSC.JSPromise.Strong, - ref: JSC.PollRef = JSC.PollRef.init(), + poll_ref: JSC.PollRef = JSC.PollRef.init(), env: *bun.DotEnv.Loader, log: Logger.Log, result: Result = .{ .pending = {} }, next: ?*JSBundleCompletionTask = null, + bundler: *BundleV2 = undefined, + plugins: ?*bun.JSC.API.JSBundler.Plugin = null, + ref_count: std.atomic.Atomic(u32) = std.atomic.Atomic(u32).init(1), pub const Result = union(enum) { pending: void, @@ -644,15 +662,22 @@ pub const BundleV2 = struct { pub const TaskCompletion = bun.JSC.AnyTask.New(JSBundleCompletionTask, onComplete); - pub fn onComplete(this: *JSBundleCompletionTask) void { - var globalThis = this.globalThis; - - defer { + pub fn deref(this: *JSBundleCompletionTask) void { + if (this.ref_count.fetchSub(1, .Monotonic) == 1) { this.config.deinit(bun.default_allocator); bun.default_allocator.destroy(this); } + } + + pub fn ref(this: *JSBundleCompletionTask) void { + _ = this.ref_count.fetchAdd(1, .Monotonic); + } + + pub fn onComplete(this: *JSBundleCompletionTask) void { + var globalThis = this.globalThis; + defer this.deref(); - this.ref.unref(globalThis.bunVM()); + this.poll_ref.unref(globalThis.bunVM()); const promise = this.promise.swap(); const root_obj = JSC.JSValue.createEmptyObject(globalThis, 2); @@ -674,6 +699,10 @@ pub const BundleV2 = struct { .value => |*build| { var output_files: []options.OutputFile = build.output_files.items; const output_files_js = JSC.JSValue.createEmptyArray(globalThis, output_files.len); + if (output_files_js == .zero) { + @panic("Unexpected pending JavaScript exception in JSBundleCompletionTask.onComplete. This is a bug in Bun."); + } + defer build.output_files.deinit(); for (output_files, 0..) |*output_file, i| { var obj = JSC.JSValue.createEmptyObject(globalThis, 2); @@ -710,6 +739,70 @@ pub const BundleV2 = struct { } }; + pub fn onLoadAsync( + this: *BundleV2, + load: *bun.JSC.API.JSBundler.Load, + ) void { + this.loop().enqueueTaskConcurrent( + bun.JSC.API.JSBundler.Load, + BundleV2, + load, + BundleV2.onLoad, + .task, + ); + } + + pub fn onResolveAsync( + this: *BundleV2, + resolve: *bun.JSC.API.JSBundler.Resolve, + ) void { + this.loop().enqueueTaskConcurrent( + bun.JSC.API.JSBundler.Resolve, + BundleV2, + resolve, + BundleV2.onResolve, + .task, + ); + } + + pub fn onLoad( + load: *bun.JSC.API.JSBundler.Load, + this: *BundleV2, + ) void { + defer load.deinit(); + + switch (load.value.consume()) { + .success => |code| { + this.graph.input_files.items(.loader)[load.source_index.get()] = code.loader; + this.graph.input_files.items(.source)[load.source_index.get()].contents = code.source_code; + var parse_task = load.parse_task; + parse_task.loader = code.loader; + this.free_list.append(code.source_code) catch unreachable; + parse_task.contents_or_fd = .{ + .contents = code.source_code, + }; + this.graph.pool.pool.schedule(ThreadPoolLib.Batch.from(&parse_task.task)); + }, + .err => |err| { + this.bundler.log.msgs.append(err) catch unreachable; + this.bundler.log.errors += @as(usize, @boolToInt(err.kind == .err)); + this.bundler.log.warnings += @as(usize, @boolToInt(err.kind == .warn)); + + // An error ocurred, prevent spinning the event loop forever + this.graph.parse_pending -= 1; + }, + .pending, .consumed => unreachable, + } + } + + pub fn onResolve( + this: *BundleV2, + resolve: *bun.JSC.API.JSBundler.Resolve, + ) void { + _ = this; + defer resolve.deinit(); + } + pub fn generateInNewThreadWrap( instance: *BundleThread, ) void { @@ -793,6 +886,9 @@ pub const BundleV2 = struct { bundler.resolver.opts = bundler.options; var this = try BundleV2.init(bundler, allocator, JSC.AnyEventLoop.init(allocator), false, JSC.WorkPool.get(), heap); + this.plugins = completion.plugins; + this.completion = completion; + completion.bundler = this; defer { if (this.graph.pool.pool.threadpool_context == @ptrCast(?*anyopaque, this.graph.pool)) { @@ -831,6 +927,12 @@ pub const BundleV2 = struct { this.graph.pool.pool.wakeForIdleEvents(); } + + for (this.free_list.items) |free| { + bun.default_allocator.free(free); + } + + this.free_list.clearAndFree(); } pub fn runFromJSInNewThread(this: *BundleV2, config: *const bun.JSC.API.JSBundler.Config) !std.ArrayList(options.OutputFile) { @@ -879,6 +981,26 @@ pub const BundleV2 = struct { return try this.linker.generateChunksInParallel(chunks); } + pub fn enqueueOnLoadPluginIfNeeded(this: *BundleV2, parse: *ParseTask) bool { + if (this.plugins) |plugins| { + if (plugins.hasAnyMatches(&parse.path, true)) { + // This is where onLoad plugins are enqueued + var load = bun.default_allocator.create(JSC.API.JSBundler.Load) catch unreachable; + load.* = JSC.API.JSBundler.Load.create( + this.completion.?, + parse.source_index, + parse.path.loader(&this.bundler.options.loaders) orelse options.Loader.js, + parse.path, + ); + load.parse_task = parse; + load.dispatch(); + return true; + } + } + + return false; + } + pub fn onParseTaskComplete(parse_result: *ParseTask.Result, this: *BundleV2) void { defer bun.default_allocator.destroy(parse_result); @@ -979,9 +1101,13 @@ pub const BundleV2 = struct { new_task.ctx = this; graph.input_files.append(graph.allocator, new_input_file) catch unreachable; graph.ast.append(graph.allocator, js_ast.Ast.empty) catch unreachable; - batch.push(ThreadPoolLib.Batch.from(&new_task.task)); - diff += 1; + + if (this.enqueueOnLoadPluginIfNeeded(new_task)) { + continue; + } + + batch.push(ThreadPoolLib.Batch.from(&new_task.task)); } else { bun.default_allocator.destroy(value); } @@ -1041,7 +1167,7 @@ pub const BundleV2 = struct { const UseDirective = js_ast.UseDirective; -const ParseTask = struct { +pub const ParseTask = struct { path: Fs.Path, secondary_path_for_commonjs_interop: ?Fs.Path = null, contents_or_fd: union(enum) { diff --git a/src/logger.zig b/src/logger.zig index 13dd98f96..bee10ff31 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -204,7 +204,7 @@ pub const Data = struct { text: string, location: ?Location = null, pub fn deinit(d: *Data, allocator: std.mem.Allocator) void { - if (d.location) |loc| { + if (d.location) |*loc| { loc.deinit(allocator); } @@ -401,6 +401,19 @@ pub const Msg = struct { metadata: Metadata = .{ .build = 0 }, notes: ?[]Data = null, + pub fn fromJS(allocator: std.mem.Allocator, globalObject: *bun.JSC.JSGlobalObject, file: string, err: bun.JSC.JSValue) !Msg { + var zig_exception_holder: bun.JSC.ZigException.Holder = bun.JSC.ZigException.Holder.init(); + err.toZigException(globalObject, zig_exception_holder.zigException()); + return Msg{ + .data = .{ + .text = zig_exception_holder.zigException().message.toSliceClone(allocator).slice(), + .location = Location{ + .file = file, + }, + }, + }; + } + pub fn count(this: *const Msg, builder: *StringBuilder) void { this.data.count(builder); if (this.notes) |notes| { @@ -490,7 +503,7 @@ pub const Msg = struct { pub fn deinit(msg: *Msg, allocator: std.mem.Allocator) void { msg.data.deinit(allocator); if (msg.notes) |notes| { - for (notes) |note| { + for (notes) |*note| { note.deinit(allocator); } } diff --git a/src/options.zig b/src/options.zig index 26485eb70..48ef405a0 100644 --- a/src/options.zig +++ b/src/options.zig @@ -646,7 +646,7 @@ pub const Platform = enum { }; }; -pub const Loader = enum { +pub const Loader = enum(u8) { jsx, js, ts, @@ -2089,8 +2089,13 @@ pub const OutputFile = struct { .buffer => |buffer| brk: { var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable; blob.* = JSC.WebCore.Blob.init(@constCast(buffer.bytes), buffer.allocator, globalObject); - blob.store.?.mime_type = this.loader.toMimeType(); - blob.content_type = blob.store.?.mime_type.value; + 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.allocator = bun.default_allocator; const blob_jsvalue = blob.toJS(globalObject); blob_jsvalue.ensureStillAlive(); |