aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2023-09-29 16:34:20 -0700
committerGravatar GitHub <noreply@github.com> 2023-09-29 16:34:20 -0700
commita97847a49475e774695c38cff07a71eadf608c05 (patch)
tree26867f9be2eddaa0b752189a27810ed4db6ed902
parenteddb0078b5c9ff49bf67c0f1b1c2c623f0480b77 (diff)
downloadbun-a97847a49475e774695c38cff07a71eadf608c05.tar.gz
bun-a97847a49475e774695c38cff07a71eadf608c05.tar.zst
bun-a97847a49475e774695c38cff07a71eadf608c05.zip
Implement virtual module support in `Bun.plugin` (#6167)
* Add support for `build.module` in `Bun.plugin` * Another test * Update docs * Update isBuiltinModule.cpp --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
-rw-r--r--docs/runtime/plugins.md87
-rw-r--r--packages/bun-types/bun.d.ts31
-rw-r--r--src/bun.js/bindings/BunPlugin.cpp254
-rw-r--r--src/bun.js/bindings/BunPlugin.h22
-rw-r--r--src/bun.js/bindings/ImportMetaObject.cpp32
-rw-r--r--src/bun.js/bindings/JSBundlerPlugin.cpp2
-rw-r--r--src/bun.js/bindings/ModuleLoader.cpp4
-rw-r--r--src/bun.js/bindings/ZigGlobalObject.cpp30
-rw-r--r--src/bun.js/bindings/ZigGlobalObject.h5
-rw-r--r--src/bun.js/bindings/isBuiltinModule.cpp98
-rw-r--r--src/bun.js/bindings/isBuiltinModule.h5
-rw-r--r--src/bun.js/modules/NodeModuleModule.h17
-rw-r--r--src/js/builtins/BundlerPlugin.ts3
-rw-r--r--src/js/out/WebCoreJSBuiltins.cpp4
-rw-r--r--test/bundler/bun-build-api.test.ts23
-rw-r--r--test/js/bun/plugin/module-plugins.ts38
-rw-r--r--test/js/bun/plugin/plugins.test.ts78
17 files changed, 622 insertions, 111 deletions
diff --git a/docs/runtime/plugins.md b/docs/runtime/plugins.md
index f9921cbad..f2e0d7c77 100644
--- a/docs/runtime/plugins.md
+++ b/docs/runtime/plugins.md
@@ -45,7 +45,7 @@ plugin(
);
```
-Bun's plugin API is based on [esbuild](https://esbuild.github.io/plugins). Only [a subset](/docs/bundler/vs-esbuild#plugin-api) of the esbuild API is implemented, but some esbuild plugins "just work" in Bun, like the official [MDX loader](https://mdxjs.com/packages/esbuild/):
+Bun's plugin API is loosely based on [esbuild](https://esbuild.github.io/plugins). Only [a subset](/docs/bundler/vs-esbuild#plugin-api) of the esbuild API is implemented, but some esbuild plugins "just work" in Bun, like the official [MDX loader](https://mdxjs.com/packages/esbuild/):
```jsx
import { plugin } from "bun";
@@ -217,6 +217,91 @@ import MySvelteComponent from "./component.svelte";
console.log(mySvelteComponent.render());
```
+## Virtual Modules
+
+{% note %}
+
+This feature is currently only available at runtime with `Bun.plugin` and not yet supported in the bundler, but you can mimick the behavior using `onResolve` and `onLoad`.
+
+{% /note %}
+
+To create virtual modules at runtime, use `builder.module(specifier, callback)` in the `setup` function of a `Bun.plugin`.
+
+For example:
+
+```js
+import { plugin } from "bun";
+
+plugin({
+ name: "my-virtual-module",
+
+ setup(build) {
+ build.module(
+ // The specifier, which can be any string
+ "my-transpiled-virtual-module",
+ // The callback to run when the module is imported or required for the first time
+ () => {
+ return {
+ contents: "console.log('hello world!')",
+ loader: "js",
+ };
+ },
+ );
+
+ build.module("my-object-virtual-module", () => {
+ return {
+ exports: {
+ foo: "bar",
+ },
+ loader: "object",
+ };
+ });
+ },
+});
+
+// Sometime later
+// All of these work
+import "my-transpiled-virtual-module";
+require("my-transpiled-virtual-module");
+await import("my-transpiled-virtual-module");
+require.resolve("my-transpiled-virtual-module");
+
+import { foo } from "my-object-virtual-module";
+const object = require("my-object-virtual-module");
+await import("my-object-virtual-module");
+require.resolve("my-object-virtual-module");
+```
+
+### Overriding existing modules
+
+You can also override existing modules with `build.module`.
+
+```js
+import { plugin } from "bun";
+build.module("my-object-virtual-module", () => {
+ return {
+ exports: {
+ foo: "bar",
+ },
+ loader: "object",
+ };
+});
+
+require("my-object-virtual-module"); // { foo: "bar" }
+await import("my-object-virtual-module"); // { foo: "bar" }
+
+build.module("my-object-virtual-module", () => {
+ return {
+ exports: {
+ baz: "quix",
+ },
+ loader: "object",
+ };
+});
+require("my-object-virtual-module"); // { baz: "quix" }
+await import("my-object-virtual-module"); // { baz: "quix" }
+```
+
## Reading the config
Plugins can read and write to the [build config](/docs/bundler#api) with `build.config`.
diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts
index 514a6bbdb..9b2d314e8 100644
--- a/packages/bun-types/bun.d.ts
+++ b/packages/bun-types/bun.d.ts
@@ -3293,6 +3293,37 @@ declare module "bun" {
* The config object passed to `Bun.build` as is. Can be mutated.
*/
config: BuildConfig & { plugins: BunPlugin[] };
+
+ /**
+ * Create a lazy-loaded virtual module that can be `import`ed or `require`d from other modules
+ *
+ * @param specifier The module specifier to register the callback for
+ * @param callback The function to run when the module is imported or required
+ *
+ * ### Example
+ * @example
+ * ```ts
+ * Bun.plugin({
+ * setup(builder) {
+ * builder.module("hello:world", () => {
+ * return { exports: { foo: "bar" }, loader: "object" };
+ * });
+ * },
+ * });
+ *
+ * // sometime later
+ * const { foo } = await import("hello:world");
+ * console.log(foo); // "bar"
+ *
+ * // or
+ * const { foo } = require("hello:world");
+ * console.log(foo); // "bar"
+ * ```
+ */
+ module(
+ specifier: string,
+ callback: () => OnLoadResult | Promise<OnLoadResult>,
+ ): void;
}
interface BunPlugin {
diff --git a/src/bun.js/bindings/BunPlugin.cpp b/src/bun.js/bindings/BunPlugin.cpp
index 129d7816b..b53fcf313 100644
--- a/src/bun.js/bindings/BunPlugin.cpp
+++ b/src/bun.js/bindings/BunPlugin.cpp
@@ -17,8 +17,10 @@
#include "JavaScriptCore/RegExpObject.h"
#include "JavaScriptCore/JSPromise.h"
#include "BunClientData.h"
-
+#include "isBuiltinModule.h"
#include "JavaScriptCore/RegularExpression.h"
+#include "JavaScriptCore/JSMap.h"
+#include "JavaScriptCore/JSMapInlines.h"
namespace Zig {
@@ -86,6 +88,76 @@ static EncodedJSValue jsFunctionAppendOnLoadPluginBody(JSC::JSGlobalObject* glob
return JSValue::encode(jsUndefined());
}
+static EncodedJSValue jsFunctionAppendVirtualModulePluginBody(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe)
+{
+ JSC::VM& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+
+ if (callframe->argumentCount() < 2) {
+ throwException(globalObject, scope, createError(globalObject, "module() needs 2 arguments: a module ID and a function to call"_s));
+ return JSValue::encode(jsUndefined());
+ }
+
+ JSValue moduleIdValue = callframe->uncheckedArgument(0);
+ JSValue functionValue = callframe->uncheckedArgument(1);
+
+ if (!moduleIdValue.isString()) {
+ throwException(globalObject, scope, createError(globalObject, "module() expects first argument to be a string for the module ID"_s));
+ return JSValue::encode(jsUndefined());
+ }
+
+ if (!functionValue.isCallable()) {
+ throwException(globalObject, scope, createError(globalObject, "module() expects second argument to be a function"_s));
+ return JSValue::encode(jsUndefined());
+ }
+
+ String moduleId = moduleIdValue.toWTFString(globalObject);
+ RETURN_IF_EXCEPTION(scope, encodedJSValue());
+
+ if (moduleId.isEmpty()) {
+ throwException(globalObject, scope, createError(globalObject, "virtual module cannot be blank"_s));
+ return JSValue::encode(jsUndefined());
+ }
+
+ if (Bun::isBuiltinModule(moduleId)) {
+ throwException(globalObject, scope, createError(globalObject, makeString("module() cannot be used to override builtin module \""_s, moduleId, "\""_s)));
+ return JSValue::encode(jsUndefined());
+ }
+
+ if (moduleId.startsWith("."_s)) {
+ throwException(globalObject, scope, createError(globalObject, "virtual module cannot start with \".\""_s));
+ return JSValue::encode(jsUndefined());
+ }
+
+ Zig::GlobalObject* global = Zig::jsCast<Zig::GlobalObject*>(globalObject);
+ if (global->onLoadPlugins.virtualModules == nullptr) {
+ global->onLoadPlugins.virtualModules = new BunPlugin::VirtualModuleMap;
+ }
+ auto* virtualModules = global->onLoadPlugins.virtualModules;
+
+ virtualModules->set(moduleId, JSC::Strong<JSC::JSObject> { vm, jsCast<JSC::JSObject*>(functionValue) });
+
+ JSMap* esmRegistry;
+
+ if (auto loaderValue = global->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "Loader"_s))) {
+ if (auto registryValue = loaderValue.getObject()->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "registry"_s))) {
+ esmRegistry = jsCast<JSC::JSMap*>(registryValue);
+ }
+ }
+
+ global->requireMap()->remove(globalObject, moduleIdValue);
+ esmRegistry && esmRegistry->remove(globalObject, moduleIdValue);
+
+ // bool hasBeenRequired = global->requireMap()->has(globalObject, moduleIdValue);
+ // bool hasBeenImported = esmRegistry && esmRegistry->has(globalObject, moduleIdValue);
+ // if (hasBeenRequired || hasBeenImported) {
+ // // callAndReplaceModule(global, moduleIdValue, functionValue, global->requireMap(), esmRegistry, hasBeenRequired, hasBeenImported);
+ // // RETURN_IF_EXCEPTION(scope, encodedJSValue());
+ // }
+
+ return JSValue::encode(jsUndefined());
+}
+
static EncodedJSValue jsFunctionAppendOnResolvePluginBody(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe, BunPluginTarget target, BunPlugin::Base& plugin, void* ctx, OnAppendPluginCallback callback)
{
JSC::VM& vm = globalObject->vm();
@@ -143,7 +215,7 @@ static EncodedJSValue jsFunctionAppendOnResolvePluginGlobal(JSC::JSGlobalObject*
{
Zig::GlobalObject* global = Zig::jsCast<Zig::GlobalObject*>(globalObject);
- auto& plugins = global->onResolvePlugins[target];
+ auto& plugins = global->onResolvePlugins;
auto callback = Bun__onDidAppendPlugin;
return jsFunctionAppendOnResolvePluginBody(globalObject, callframe, target, plugins, global->bunVM(), callback);
}
@@ -152,7 +224,7 @@ static EncodedJSValue jsFunctionAppendOnLoadPluginGlobal(JSC::JSGlobalObject* gl
{
Zig::GlobalObject* global = Zig::jsCast<Zig::GlobalObject*>(globalObject);
- auto& plugins = global->onLoadPlugins[target];
+ auto& plugins = global->onLoadPlugins;
auto callback = Bun__onDidAppendPlugin;
return jsFunctionAppendOnLoadPluginBody(globalObject, callframe, target, plugins, global->bunVM(), callback);
}
@@ -182,6 +254,11 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendOnResolvePluginBun, (JSC::JSGlobalObjec
return jsFunctionAppendOnResolvePluginGlobal(globalObject, callframe, BunPluginTargetBun);
}
+JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendVirtualModule, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
+{
+ return jsFunctionAppendVirtualModulePluginBody(globalObject, callframe);
+}
+
JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendOnResolvePluginBrowser, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
{
return jsFunctionAppendOnResolvePluginGlobal(globalObject, callframe, BunPluginTargetBrowser);
@@ -190,12 +267,12 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendOnResolvePluginBrowser, (JSC::JSGlobalO
extern "C" EncodedJSValue jsFunctionBunPluginClear(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe)
{
Zig::GlobalObject* global = reinterpret_cast<Zig::GlobalObject*>(globalObject);
- for (uint8_t i = 0; i < BunPluginTargetMax + 1; i++) {
- global->onLoadPlugins[i].fileNamespace.clear();
- global->onResolvePlugins[i].fileNamespace.clear();
- global->onLoadPlugins[i].groups.clear();
- global->onResolvePlugins[i].namespaces.clear();
- }
+ global->onLoadPlugins.fileNamespace.clear();
+ global->onResolvePlugins.fileNamespace.clear();
+ global->onLoadPlugins.groups.clear();
+ global->onResolvePlugins.namespaces.clear();
+
+ delete global->onLoadPlugins.virtualModules;
return JSValue::encode(jsUndefined());
}
@@ -239,76 +316,37 @@ extern "C" EncodedJSValue setupBunPlugin(JSC::JSGlobalObject* globalObject, JSC:
}
JSFunction* setupFunction = jsCast<JSFunction*>(setupFunctionValue);
- JSObject* builderObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 3);
-
- switch (target) {
- case BunPluginTargetNode: {
- builderObject->putDirect(vm, Identifier::fromString(vm, "target"_s), jsString(vm, String("node"_s)), 0);
- builderObject->putDirectNativeFunction(
- vm,
- globalObject,
- JSC::Identifier::fromString(vm, "onLoad"_s),
- 1,
- jsFunctionAppendOnLoadPluginNode,
- ImplementationVisibility::Public,
- NoIntrinsic,
- JSC::PropertyAttribute::DontDelete | 0);
- builderObject->putDirectNativeFunction(
- vm,
- globalObject,
- JSC::Identifier::fromString(vm, "onResolve"_s),
- 1,
- jsFunctionAppendOnResolvePluginNode,
- ImplementationVisibility::Public,
- NoIntrinsic,
- JSC::PropertyAttribute::DontDelete | 0);
- break;
- }
- case BunPluginTargetBun: {
- builderObject->putDirect(vm, Identifier::fromString(vm, "target"_s), jsString(vm, String("bun"_s)), 0);
- builderObject->putDirectNativeFunction(
- vm,
- globalObject,
- JSC::Identifier::fromString(vm, "onLoad"_s),
- 1,
- jsFunctionAppendOnLoadPluginBun,
- ImplementationVisibility::Public,
- NoIntrinsic,
- JSC::PropertyAttribute::DontDelete | 0);
- builderObject->putDirectNativeFunction(
- vm,
- globalObject,
- JSC::Identifier::fromString(vm, "onResolve"_s),
- 1,
- jsFunctionAppendOnResolvePluginBun,
- ImplementationVisibility::Public,
- NoIntrinsic,
- JSC::PropertyAttribute::DontDelete | 0);
- break;
- }
- case BunPluginTargetBrowser: {
- builderObject->putDirect(vm, Identifier::fromString(vm, "target"_s), jsString(vm, String("browser"_s)), 0);
- builderObject->putDirectNativeFunction(
- vm,
- globalObject,
- JSC::Identifier::fromString(vm, "onLoad"_s),
- 1,
- jsFunctionAppendOnLoadPluginBrowser,
- ImplementationVisibility::Public,
- NoIntrinsic,
- JSC::PropertyAttribute::DontDelete | 0);
- builderObject->putDirectNativeFunction(
- vm,
- globalObject,
- JSC::Identifier::fromString(vm, "onResolve"_s),
- 1,
- jsFunctionAppendOnResolvePluginBrowser,
- ImplementationVisibility::Public,
- NoIntrinsic,
- JSC::PropertyAttribute::DontDelete | 0);
- break;
- }
- }
+ JSObject* builderObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 4);
+
+ builderObject->putDirect(vm, Identifier::fromString(vm, "target"_s), jsString(vm, String("bun"_s)), 0);
+ builderObject->putDirectNativeFunction(
+ vm,
+ globalObject,
+ JSC::Identifier::fromString(vm, "onLoad"_s),
+ 1,
+ jsFunctionAppendOnLoadPluginBun,
+ ImplementationVisibility::Public,
+ NoIntrinsic,
+ JSC::PropertyAttribute::DontDelete | 0);
+ builderObject->putDirectNativeFunction(
+ vm,
+ globalObject,
+ JSC::Identifier::fromString(vm, "onResolve"_s),
+ 1,
+ jsFunctionAppendOnResolvePluginBun,
+ ImplementationVisibility::Public,
+ NoIntrinsic,
+ JSC::PropertyAttribute::DontDelete | 0);
+
+ builderObject->putDirectNativeFunction(
+ vm,
+ globalObject,
+ JSC::Identifier::fromString(vm, "module"_s),
+ 1,
+ jsFunctionAppendVirtualModule,
+ ImplementationVisibility::Public,
+ NoIntrinsic,
+ JSC::PropertyAttribute::DontDelete | 0);
JSC::MarkedArgumentBuffer args;
args.append(builderObject);
@@ -329,9 +367,7 @@ extern "C" EncodedJSValue setupBunPlugin(JSC::JSGlobalObject* globalObject, JSC:
extern "C" EncodedJSValue jsFunctionBunPlugin(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe)
{
Zig::GlobalObject* global = reinterpret_cast<Zig::GlobalObject*>(globalObject);
- BunPluginTarget target = global->defaultBunPluginTarget;
-
- return setupBunPlugin(globalObject, callframe, target);
+ return setupBunPlugin(globalObject, callframe, BunPluginTargetBun);
}
void BunPlugin::Group::append(JSC::VM& vm, JSC::RegExp* filter, JSC::JSFunction* func)
@@ -513,10 +549,60 @@ EncodedJSValue BunPlugin::OnResolve::run(JSC::JSGlobalObject* globalObject, BunS
extern "C" JSC::EncodedJSValue Bun__runOnResolvePlugins(Zig::GlobalObject* globalObject, BunString* namespaceString, BunString* path, BunString* from, BunPluginTarget target)
{
- return globalObject->onResolvePlugins[target].run(globalObject, namespaceString, path, from);
+ return globalObject->onResolvePlugins.run(globalObject, namespaceString, path, from);
}
extern "C" JSC::EncodedJSValue Bun__runOnLoadPlugins(Zig::GlobalObject* globalObject, BunString* namespaceString, BunString* path, BunPluginTarget target)
{
- return globalObject->onLoadPlugins[target].run(globalObject, namespaceString, path);
+ return globalObject->onLoadPlugins.run(globalObject, namespaceString, path);
+}
+
+namespace Bun {
+JSC::JSValue runVirtualModule(Zig::GlobalObject* globalObject, BunString* specifier)
+{
+ auto fallback = [&]() -> JSC::JSValue {
+ return JSValue::decode(Bun__runVirtualModule(globalObject, specifier));
+ };
+
+ if (!globalObject->onLoadPlugins.virtualModules) {
+ return fallback();
+ }
+ auto& virtualModules = *globalObject->onLoadPlugins.virtualModules;
+ WTF::String specifierString = Bun::toWTFString(*specifier);
+ if (auto virtualModuleFn = virtualModules.get(specifierString)) {
+ auto& vm = globalObject->vm();
+ JSC::JSObject* function = virtualModuleFn.get();
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+
+ JSC::MarkedArgumentBuffer arguments;
+ JSC::CallData callData = JSC::getCallData(function);
+ RELEASE_ASSERT(callData.type != JSC::CallData::Type::None);
+
+ auto result = call(globalObject, function, callData, JSC::jsUndefined(), arguments);
+ RETURN_IF_EXCEPTION(throwScope, JSC::jsUndefined());
+
+ if (auto* promise = JSC::jsDynamicCast<JSPromise*>(result)) {
+ switch (promise->status(vm)) {
+ case JSPromise::Status::Rejected:
+ case JSPromise::Status::Pending: {
+ return promise;
+ }
+ case JSPromise::Status::Fulfilled: {
+ result = promise->result(vm);
+ break;
+ }
+ }
+ }
+
+ if (!result.isObject()) {
+ JSC::throwTypeError(globalObject, throwScope, "virtual module expects an object returned"_s);
+ return JSC::jsUndefined();
+ }
+
+ return result;
+ }
+
+ return fallback();
}
+
+} \ No newline at end of file
diff --git a/src/bun.js/bindings/BunPlugin.h b/src/bun.js/bindings/BunPlugin.h
index cf37b739b..f4d09883d 100644
--- a/src/bun.js/bindings/BunPlugin.h
+++ b/src/bun.js/bindings/BunPlugin.h
@@ -15,6 +15,8 @@ using namespace JSC;
class BunPlugin {
public:
+ using VirtualModuleMap = WTF::HashMap<String, JSC::Strong<JSC::JSObject>>;
+
// This is a list of pairs of regexps and functions to match against
class Group {
@@ -67,7 +69,15 @@ public:
{
}
- EncodedJSValue run(JSC::JSGlobalObject* globalObject, BunString* namespaceString, BunString* path);
+ VirtualModuleMap* virtualModules = nullptr;
+ JSC::EncodedJSValue run(JSC::JSGlobalObject* globalObject, BunString* namespaceString, BunString* path);
+
+ ~OnLoad()
+ {
+ if (virtualModules) {
+ delete virtualModules;
+ }
+ }
};
class OnResolve final : public Base {
@@ -78,8 +88,14 @@ public:
{
}
- EncodedJSValue run(JSC::JSGlobalObject* globalObject, BunString* namespaceString, BunString* path, BunString* importer);
+ JSC::EncodedJSValue run(JSC::JSGlobalObject* globalObject, BunString* namespaceString, BunString* path, BunString* importer);
};
};
-} // namespace Zig \ No newline at end of file
+class GlobalObject;
+
+} // namespace Zig
+
+namespace Bun {
+JSC::JSValue runVirtualModule(Zig::GlobalObject*, BunString* specifier);
+} \ No newline at end of file
diff --git a/src/bun.js/bindings/ImportMetaObject.cpp b/src/bun.js/bindings/ImportMetaObject.cpp
index 4160102a5..340c3319b 100644
--- a/src/bun.js/bindings/ImportMetaObject.cpp
+++ b/src/bun.js/bindings/ImportMetaObject.cpp
@@ -64,6 +64,12 @@ static EncodedJSValue functionRequireResolve(JSC::JSGlobalObject* globalObject,
JSC::JSValue moduleName = callFrame->argument(0);
auto doIt = [&](const WTF::String& fromStr) -> JSC::EncodedJSValue {
+ if (auto* virtualModules = jsCast<Zig::GlobalObject*>(globalObject)->onLoadPlugins.virtualModules) {
+ if (virtualModules->contains(fromStr)) {
+ return JSC::JSValue::encode(jsString(vm, fromStr));
+ }
+ }
+
BunString from = Bun::toString(fromStr);
auto result = Bun__resolveSyncWithSource(globalObject, JSC::JSValue::encode(moduleName), &from, false);
@@ -160,6 +166,14 @@ extern "C" EncodedJSValue functionImportMeta__resolveSync(JSC::JSGlobalObject* g
JSC__JSValue from;
bool isESM = true;
+ if (auto* virtualModules = jsCast<Zig::GlobalObject*>(globalObject)->onLoadPlugins.virtualModules) {
+ if (moduleName.isString()) {
+ if (virtualModules->contains(moduleName.toWTFString(globalObject))) {
+ return JSC::JSValue::encode(moduleName);
+ }
+ }
+ }
+
if (callFrame->argumentCount() > 1) {
if (callFrame->argumentCount() > 2) {
@@ -226,6 +240,7 @@ extern "C" EncodedJSValue functionImportMeta__resolveSyncPrivate(JSC::JSGlobalOb
{
JSC::VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
+ auto* global = jsDynamicCast<Zig::GlobalObject*>(globalObject);
JSC::JSValue moduleName = callFrame->argument(0);
JSValue from = callFrame->argument(1);
@@ -239,8 +254,15 @@ extern "C" EncodedJSValue functionImportMeta__resolveSyncPrivate(JSC::JSGlobalOb
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::JSValue {}));
+ if (auto* virtualModules = global->onLoadPlugins.virtualModules) {
+ if (moduleName.isString()) {
+ if (virtualModules->contains(moduleName.toWTFString(globalObject))) {
+ return JSC::JSValue::encode(moduleName);
+ }
+ }
+ }
+
if (!isESM) {
- auto* global = jsDynamicCast<Zig::GlobalObject*>(globalObject);
if (LIKELY(global)) {
auto overrideHandler = global->m_nodeModuleOverriddenResolveFilename.get();
if (UNLIKELY(overrideHandler)) {
@@ -289,6 +311,14 @@ JSC_DEFINE_HOST_FUNCTION(functionImportMeta__resolve,
JSC__JSValue from;
+ if (auto* virtualModules = jsCast<Zig::GlobalObject*>(globalObject)->onLoadPlugins.virtualModules) {
+ if (moduleName.isString()) {
+ if (virtualModules->contains(moduleName.toWTFString(globalObject))) {
+ return JSC::JSValue::encode(moduleName);
+ }
+ }
+ }
+
if (callFrame->argumentCount() > 1 && callFrame->argument(1).isString()) {
from = JSC::JSValue::encode(callFrame->argument(1));
} else {
diff --git a/src/bun.js/bindings/JSBundlerPlugin.cpp b/src/bun.js/bindings/JSBundlerPlugin.cpp
index 6ae266df7..d896d5b3d 100644
--- a/src/bun.js/bindings/JSBundlerPlugin.cpp
+++ b/src/bun.js/bindings/JSBundlerPlugin.cpp
@@ -31,6 +31,7 @@ namespace Bun {
extern "C" void JSBundlerPlugin__addError(void*, void*, JSC::EncodedJSValue, JSC::EncodedJSValue);
extern "C" void JSBundlerPlugin__onLoadAsync(void*, void*, JSC::EncodedJSValue, JSC::EncodedJSValue);
extern "C" void JSBundlerPlugin__onResolveAsync(void*, void*, JSC::EncodedJSValue, JSC::EncodedJSValue, JSC::EncodedJSValue);
+extern "C" void JSBundlerPlugin__onVirtualModulePlugin(void*, void*, JSC::EncodedJSValue, JSC::EncodedJSValue, JSC::EncodedJSValue);
JSC_DECLARE_HOST_FUNCTION(jsBundlerPluginFunction_addFilter);
JSC_DECLARE_HOST_FUNCTION(jsBundlerPluginFunction_addError);
@@ -154,6 +155,7 @@ public:
Bun::BundlerPlugin plugin;
JSC::LazyProperty<JSBundlerPlugin, JSC::JSFunction> onLoadFunction;
JSC::LazyProperty<JSBundlerPlugin, JSC::JSFunction> onResolveFunction;
+ JSC::LazyProperty<JSBundlerPlugin, JSC::JSFunction> moduleFunction;
JSC::LazyProperty<JSBundlerPlugin, JSC::JSFunction> setupFunction;
private:
diff --git a/src/bun.js/bindings/ModuleLoader.cpp b/src/bun.js/bindings/ModuleLoader.cpp
index acda70e0a..127fb6965 100644
--- a/src/bun.js/bindings/ModuleLoader.cpp
+++ b/src/bun.js/bindings/ModuleLoader.cpp
@@ -458,7 +458,7 @@ JSValue fetchCommonJSModule(
}
}
- if (JSC::JSValue virtualModuleResult = JSValue::decode(Bun__runVirtualModule(globalObject, specifier))) {
+ if (JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, specifier)) {
JSPromise* promise = jsCast<JSPromise*>(handleVirtualModuleResult<true>(globalObject, virtualModuleResult, res, specifier, referrer));
switch (promise->status(vm)) {
case JSPromise::Status::Rejected: {
@@ -633,7 +633,7 @@ static JSValue fetchESMSourceCode(
}
}
- if (JSC::JSValue virtualModuleResult = JSValue::decode(Bun__runVirtualModule(globalObject, specifier))) {
+ if (JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, specifier)) {
return handleVirtualModuleResult<allowPromise>(globalObject, virtualModuleResult, res, specifier, referrer);
}
diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp
index d54800ca6..b9f2e4e46 100644
--- a/src/bun.js/bindings/ZigGlobalObject.cpp
+++ b/src/bun.js/bindings/ZigGlobalObject.cpp
@@ -4036,12 +4036,24 @@ extern "C" void JSC__JSGlobalObject__queueMicrotaskCallback(Zig::GlobalObject* g
globalObject->queueMicrotask(function, JSValue(bitwise_cast<double>(reinterpret_cast<uintptr_t>(ptr))), JSValue(bitwise_cast<double>(reinterpret_cast<uintptr_t>(callback))), jsUndefined(), jsUndefined());
}
-JSC::Identifier GlobalObject::moduleLoaderResolve(JSGlobalObject* globalObject,
+JSC::Identifier GlobalObject::moduleLoaderResolve(JSGlobalObject* jsGlobalObject,
JSModuleLoader* loader, JSValue key,
JSValue referrer, JSValue origin)
{
+ Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(jsGlobalObject);
+
ErrorableString res;
res.success = false;
+
+ if (key.isString()) {
+ if (auto* virtualModules = globalObject->onLoadPlugins.virtualModules) {
+ auto keyString = key.toWTFString(globalObject);
+ if (virtualModules->contains(keyString)) {
+ return JSC::Identifier::fromString(globalObject->vm(), keyString);
+ }
+ }
+ }
+
BunString keyZ;
if (key.isString()) {
auto moduleName = jsCast<JSString*>(key)->value(globalObject);
@@ -4077,18 +4089,32 @@ JSC::Identifier GlobalObject::moduleLoaderResolve(JSGlobalObject* globalObject,
}
}
-JSC::JSInternalPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* globalObject,
+JSC::JSInternalPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalObject,
JSModuleLoader*,
JSString* moduleNameValue,
JSValue parameters,
const SourceOrigin& sourceOrigin)
{
+ auto* globalObject = reinterpret_cast<Zig::GlobalObject*>(jsGlobalObject);
JSC::VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* promise = JSC::JSInternalPromise::create(vm, globalObject->internalPromiseStructure());
RETURN_IF_EXCEPTION(scope, promise->rejectWithCaughtException(globalObject, scope));
+ if (auto* virtualModules = globalObject->onLoadPlugins.virtualModules) {
+ auto keyString = moduleNameValue->value(globalObject);
+ if (virtualModules->contains(keyString)) {
+ auto resolvedIdentifier = JSC::Identifier::fromString(vm, keyString);
+
+ auto result = JSC::importModule(globalObject, resolvedIdentifier,
+ JSC::jsUndefined(), parameters, JSC::jsUndefined());
+
+ RETURN_IF_EXCEPTION(scope, promise->rejectWithCaughtException(globalObject, scope));
+ return result;
+ }
+ }
+
auto sourceURL = sourceOrigin.url();
ErrorableString resolved;
BunString moduleNameZ;
diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h
index 26111725b..035d18b08 100644
--- a/src/bun.js/bindings/ZigGlobalObject.h
+++ b/src/bun.js/bindings/ZigGlobalObject.h
@@ -382,9 +382,8 @@ public:
return false;
}
- BunPlugin::OnLoad onLoadPlugins[BunPluginTargetMax + 1] {};
- BunPlugin::OnResolve onResolvePlugins[BunPluginTargetMax + 1] {};
- BunPluginTarget defaultBunPluginTarget = BunPluginTargetBun;
+ BunPlugin::OnLoad onLoadPlugins {};
+ BunPlugin::OnResolve onResolvePlugins {};
// This increases the cache hit rate for JSC::VM's SourceProvider cache
// It also avoids an extra allocation for the SourceProvider
diff --git a/src/bun.js/bindings/isBuiltinModule.cpp b/src/bun.js/bindings/isBuiltinModule.cpp
new file mode 100644
index 000000000..b8e69f479
--- /dev/null
+++ b/src/bun.js/bindings/isBuiltinModule.cpp
@@ -0,0 +1,98 @@
+#include "root.h"
+
+static constexpr ASCIILiteral builtinModuleNamesSortedLength[] = {
+ "fs"_s,
+ "os"_s,
+ "v8"_s,
+ "vm"_s,
+ "ws"_s,
+ "bun"_s,
+ "dns"_s,
+ "net"_s,
+ "sys"_s,
+ "tls"_s,
+ "tty"_s,
+ "url"_s,
+ "http"_s,
+ "path"_s,
+ "repl"_s,
+ "util"_s,
+ "wasi"_s,
+ "zlib"_s,
+ "dgram"_s,
+ "http2"_s,
+ "https"_s,
+ "assert"_s,
+ "buffer"_s,
+ "crypto"_s,
+ "domain"_s,
+ "events"_s,
+ "module"_s,
+ "stream"_s,
+ "timers"_s,
+ "undici"_s,
+ "bun:ffi"_s,
+ "bun:jsc"_s,
+ "cluster"_s,
+ "console"_s,
+ "process"_s,
+ "bun:wrap"_s,
+ "punycode"_s,
+ "bun:test"_s,
+ "bun:main"_s,
+ "readline"_s,
+ "_tls_wrap"_s,
+ "constants"_s,
+ "inspector"_s,
+ "bun:sqlite"_s,
+ "path/posix"_s,
+ "path/win32"_s,
+ "perf_hooks"_s,
+ "stream/web"_s,
+ "util/types"_s,
+ "_http_agent"_s,
+ "_tls_common"_s,
+ "async_hooks"_s,
+ "detect-libc"_s,
+ "fs/promises"_s,
+ "querystring"_s,
+ "_http_client"_s,
+ "_http_common"_s,
+ "_http_server"_s,
+ "_stream_wrap"_s,
+ "dns/promises"_s,
+ "trace_events"_s,
+ "assert/strict"_s,
+ "child_process"_s,
+ "_http_incoming"_s,
+ "_http_outgoing"_s,
+ "_stream_duplex"_s,
+ "string_decoder"_s,
+ "worker_threads"_s,
+ "stream/promises"_s,
+ "timers/promises"_s,
+ "_stream_readable"_s,
+ "_stream_writable"_s,
+ "stream/consumers"_s,
+ "_stream_transform"_s,
+ "readline/promises"_s,
+ "inspector/promises"_s,
+ "_stream_passthrough"_s,
+ "diagnostics_channel"_s,
+};
+
+namespace Bun {
+
+bool isBuiltinModule(const String &namePossiblyWithNodePrefix) {
+ String name = namePossiblyWithNodePrefix;
+ if (name.startsWith("node:"_s))
+ name = name.substringSharingImpl(5);
+
+ for (auto &builtinModule : builtinModuleNamesSortedLength) {
+ if (name == builtinModule)
+ return true;
+ }
+ return false;
+}
+
+} // namespace Bun \ No newline at end of file
diff --git a/src/bun.js/bindings/isBuiltinModule.h b/src/bun.js/bindings/isBuiltinModule.h
new file mode 100644
index 000000000..d66f025d1
--- /dev/null
+++ b/src/bun.js/bindings/isBuiltinModule.h
@@ -0,0 +1,5 @@
+#pragma once
+
+namespace Bun {
+bool isBuiltinModule(const String &namePossiblyWithNodePrefix);
+} // namespace Bun \ No newline at end of file
diff --git a/src/bun.js/modules/NodeModuleModule.h b/src/bun.js/modules/NodeModuleModule.h
index ddb273de4..0da967f65 100644
--- a/src/bun.js/modules/NodeModuleModule.h
+++ b/src/bun.js/modules/NodeModuleModule.h
@@ -1,8 +1,11 @@
+#pragma once
+
#include "CommonJSModuleRecord.h"
#include "ImportMetaObject.h"
#include "JavaScriptCore/JSBoundFunction.h"
#include "JavaScriptCore/ObjectConstructor.h"
#include "_NativeModule.h"
+#include "isBuiltinModule.h"
using namespace Zig;
using namespace JSC;
@@ -88,18 +91,6 @@ static constexpr ASCIILiteral builtinModuleNames[] = {
"zlib"_s,
};
-static bool isBuiltinModule(const String &namePossiblyWithNodePrefix) {
- String name = namePossiblyWithNodePrefix;
- if (name.startsWith("node:"_s))
- name = name.substringSharingImpl(5);
-
- for (auto &builtinModule : builtinModuleNames) {
- if (name == builtinModule)
- return true;
- }
- return false;
-}
-
JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeModuleModuleConstructor,
(JSC::JSGlobalObject * globalObject,
JSC::CallFrame *callFrame)) {
@@ -158,7 +149,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionIsBuiltinModule,
auto moduleStr = moduleName.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, JSValue::encode(jsBoolean(false)));
- return JSValue::encode(jsBoolean(isBuiltinModule(moduleStr)));
+ return JSValue::encode(jsBoolean(Bun::isBuiltinModule(moduleStr)));
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionWrap, (JSC::JSGlobalObject * globalObject,
diff --git a/src/js/builtins/BundlerPlugin.ts b/src/js/builtins/BundlerPlugin.ts
index 7be030ee8..d2c88b667 100644
--- a/src/js/builtins/BundlerPlugin.ts
+++ b/src/js/builtins/BundlerPlugin.ts
@@ -162,6 +162,9 @@ export function runSetupFunction(this: BundlerPlugin, setup: Setup, config: Buil
onResolve,
onStart: notImplementedIssueFn(2771, "On-start callbacks"),
resolve: notImplementedIssueFn(2771, "build.resolve()"),
+ module: () => {
+ throw new TypeError("module() is not supported in Bun.build() yet. Only via Bun.plugin() at runtime");
+ },
// esbuild's options argument is different, we provide some interop
initialOptions: {
...config,
diff --git a/src/js/out/WebCoreJSBuiltins.cpp b/src/js/out/WebCoreJSBuiltins.cpp
index a60dfc281..31246276b 100644
--- a/src/js/out/WebCoreJSBuiltins.cpp
+++ b/src/js/out/WebCoreJSBuiltins.cpp
@@ -30,9 +30,9 @@ const char* const s_bundlerPluginRunOnResolvePluginsCode = "(function (specifier
const JSC::ConstructAbility s_bundlerPluginRunSetupFunctionCodeConstructAbility = JSC::ConstructAbility::CannotConstruct;
const JSC::ConstructorKind s_bundlerPluginRunSetupFunctionCodeConstructorKind = JSC::ConstructorKind::None;
const JSC::ImplementationVisibility s_bundlerPluginRunSetupFunctionCodeImplementationVisibility = JSC::ImplementationVisibility::Public;
-const int s_bundlerPluginRunSetupFunctionCodeLength = 4001;
+const int s_bundlerPluginRunSetupFunctionCodeLength = 4133;
static const JSC::Intrinsic s_bundlerPluginRunSetupFunctionCodeIntrinsic = JSC::NoIntrinsic;
-const char* const s_bundlerPluginRunSetupFunctionCode = "(function (setup, config) {\"use strict\";\n var onLoadPlugins = new Map, onResolvePlugins = new Map;\n function validate(filterObject, callback, map) {\n if (!filterObject || !@isObject(filterObject))\n @throwTypeError('Expected an object with \"filter\" RegExp');\n if (!callback || !@isCallable(callback))\n @throwTypeError(\"callback must be a function\");\n var { filter, namespace = \"file\" } = filterObject;\n if (!filter)\n @throwTypeError('Expected an object with \"filter\" RegExp');\n if (!@isRegExpObject(filter))\n @throwTypeError(\"filter must be a RegExp\");\n if (namespace && typeof namespace !== \"string\")\n @throwTypeError(\"namespace must be a string\");\n if ((namespace\?.length \?\? 0) === 0)\n namespace = \"file\";\n if (!/^([/@a-zA-Z0-9_\\\\-]+)$/.test(namespace))\n @throwTypeError(\"namespace can only contain $a-zA-Z0-9_\\\\-\");\n var callbacks = map.@get(namespace);\n if (!callbacks)\n map.@set(namespace, [[filter, callback]]);\n else\n @arrayPush(callbacks, [filter, callback]);\n }\n function onLoad(filterObject, callback) {\n validate(filterObject, callback, onLoadPlugins);\n }\n function onResolve(filterObject, callback) {\n validate(filterObject, callback, onResolvePlugins);\n }\n const processSetupResult = () => {\n var anyOnLoad = !1, anyOnResolve = !1;\n for (var [namespace, callbacks] of onLoadPlugins.entries())\n for (var [filter] of callbacks)\n this.addFilter(filter, namespace, 1), anyOnLoad = !0;\n for (var [namespace, callbacks] of onResolvePlugins.entries())\n for (var [filter] of callbacks)\n this.addFilter(filter, namespace, 0), anyOnResolve = !0;\n if (anyOnResolve) {\n var onResolveObject = this.onResolve;\n if (!onResolveObject)\n this.onResolve = onResolvePlugins;\n else\n for (var [namespace, callbacks] of onResolvePlugins.entries()) {\n var existing = onResolveObject.@get(namespace);\n if (!existing)\n onResolveObject.@set(namespace, callbacks);\n else\n onResolveObject.@set(namespace, existing.concat(callbacks));\n }\n }\n if (anyOnLoad) {\n var onLoadObject = this.onLoad;\n if (!onLoadObject)\n this.onLoad = onLoadPlugins;\n else\n for (var [namespace, callbacks] of onLoadPlugins.entries()) {\n var existing = onLoadObject.@get(namespace);\n if (!existing)\n onLoadObject.@set(namespace, callbacks);\n else\n onLoadObject.@set(namespace, existing.concat(callbacks));\n }\n }\n return anyOnLoad || anyOnResolve;\n };\n var setupResult = setup({\n config,\n onDispose: () => @throwTypeError(\"@{@2} is not implemented yet. See https://github.com/oven-sh/bun/issues/@1\"),\n onEnd: () => @throwTypeError(\"@{@2} is not implemented yet. See https://github.com/oven-sh/bun/issues/@1\"),\n onLoad,\n onResolve,\n onStart: () => @throwTypeError(\"@{@2} is not implemented yet. See https://github.com/oven-sh/bun/issues/@1\"),\n resolve: () => @throwTypeError(\"@{@2} is not implemented yet. See https://github.com/oven-sh/bun/issues/@1\"),\n initialOptions: {\n ...config,\n bundle: !0,\n entryPoints: config.entrypoints \?\? config.entryPoints \?\? [],\n minify: typeof config.minify === \"boolean\" \? config.minify : !1,\n minifyIdentifiers: config.minify === !0 || config.minify\?.identifiers,\n minifyWhitespace: config.minify === !0 || config.minify\?.whitespace,\n minifySyntax: config.minify === !0 || config.minify\?.syntax,\n outbase: config.root,\n platform: config.target === \"bun\" \? \"node\" : config.target\n },\n esbuild: {}\n });\n if (setupResult && @isPromise(setupResult))\n if (@getPromiseInternalField(setupResult, @promiseFieldFlags) & @promiseStateFulfilled)\n setupResult = @getPromiseInternalField(setupResult, @promiseFieldReactionsOrResult);\n else\n return setupResult.@then(processSetupResult);\n return processSetupResult();\n})\n";
+const char* const s_bundlerPluginRunSetupFunctionCode = "(function (setup, config) {\"use strict\";\n var onLoadPlugins = new Map, onResolvePlugins = new Map;\n function validate(filterObject, callback, map) {\n if (!filterObject || !@isObject(filterObject))\n @throwTypeError('Expected an object with \"filter\" RegExp');\n if (!callback || !@isCallable(callback))\n @throwTypeError(\"callback must be a function\");\n var { filter, namespace = \"file\" } = filterObject;\n if (!filter)\n @throwTypeError('Expected an object with \"filter\" RegExp');\n if (!@isRegExpObject(filter))\n @throwTypeError(\"filter must be a RegExp\");\n if (namespace && typeof namespace !== \"string\")\n @throwTypeError(\"namespace must be a string\");\n if ((namespace\?.length \?\? 0) === 0)\n namespace = \"file\";\n if (!/^([/@a-zA-Z0-9_\\\\-]+)$/.test(namespace))\n @throwTypeError(\"namespace can only contain $a-zA-Z0-9_\\\\-\");\n var callbacks = map.@get(namespace);\n if (!callbacks)\n map.@set(namespace, [[filter, callback]]);\n else\n @arrayPush(callbacks, [filter, callback]);\n }\n function onLoad(filterObject, callback) {\n validate(filterObject, callback, onLoadPlugins);\n }\n function onResolve(filterObject, callback) {\n validate(filterObject, callback, onResolvePlugins);\n }\n const processSetupResult = () => {\n var anyOnLoad = !1, anyOnResolve = !1;\n for (var [namespace, callbacks] of onLoadPlugins.entries())\n for (var [filter] of callbacks)\n this.addFilter(filter, namespace, 1), anyOnLoad = !0;\n for (var [namespace, callbacks] of onResolvePlugins.entries())\n for (var [filter] of callbacks)\n this.addFilter(filter, namespace, 0), anyOnResolve = !0;\n if (anyOnResolve) {\n var onResolveObject = this.onResolve;\n if (!onResolveObject)\n this.onResolve = onResolvePlugins;\n else\n for (var [namespace, callbacks] of onResolvePlugins.entries()) {\n var existing = onResolveObject.@get(namespace);\n if (!existing)\n onResolveObject.@set(namespace, callbacks);\n else\n onResolveObject.@set(namespace, existing.concat(callbacks));\n }\n }\n if (anyOnLoad) {\n var onLoadObject = this.onLoad;\n if (!onLoadObject)\n this.onLoad = onLoadPlugins;\n else\n for (var [namespace, callbacks] of onLoadPlugins.entries()) {\n var existing = onLoadObject.@get(namespace);\n if (!existing)\n onLoadObject.@set(namespace, callbacks);\n else\n onLoadObject.@set(namespace, existing.concat(callbacks));\n }\n }\n return anyOnLoad || anyOnResolve;\n };\n var setupResult = setup({\n config,\n onDispose: () => @throwTypeError(\"@{@2} is not implemented yet. See https://github.com/oven-sh/bun/issues/@1\"),\n onEnd: () => @throwTypeError(\"@{@2} is not implemented yet. See https://github.com/oven-sh/bun/issues/@1\"),\n onLoad,\n onResolve,\n onStart: () => @throwTypeError(\"@{@2} is not implemented yet. See https://github.com/oven-sh/bun/issues/@1\"),\n resolve: () => @throwTypeError(\"@{@2} is not implemented yet. See https://github.com/oven-sh/bun/issues/@1\"),\n module: () => {\n @throwTypeError(\"module() is not supported in Bun.build() yet. Only via Bun.plugin() at runtime\");\n },\n initialOptions: {\n ...config,\n bundle: !0,\n entryPoints: config.entrypoints \?\? config.entryPoints \?\? [],\n minify: typeof config.minify === \"boolean\" \? config.minify : !1,\n minifyIdentifiers: config.minify === !0 || config.minify\?.identifiers,\n minifyWhitespace: config.minify === !0 || config.minify\?.whitespace,\n minifySyntax: config.minify === !0 || config.minify\?.syntax,\n outbase: config.root,\n platform: config.target === \"bun\" \? \"node\" : config.target\n },\n esbuild: {}\n });\n if (setupResult && @isPromise(setupResult))\n if (@getPromiseInternalField(setupResult, @promiseFieldFlags) & @promiseStateFulfilled)\n setupResult = @getPromiseInternalField(setupResult, @promiseFieldReactionsOrResult);\n else\n return setupResult.@then(processSetupResult);\n return processSetupResult();\n})\n";
#define DEFINE_BUILTIN_GENERATOR(codeName, functionName, overriddenName, argumentCount) \
JSC::FunctionExecutable* codeName##Generator(JSC::VM& vm) \
diff --git a/test/bundler/bun-build-api.test.ts b/test/bundler/bun-build-api.test.ts
index 3c2308d94..d1f1a10b8 100644
--- a/test/bundler/bun-build-api.test.ts
+++ b/test/bundler/bun-build-api.test.ts
@@ -290,4 +290,27 @@ describe("Bun.build", () => {
// depends on the ws package in the test/node_modules.
expect(content).toContain("var websocket = __toESM(require_websocket(), 1);");
});
+
+ test("module() throws error", async () => {
+ expect(() =>
+ Bun.build({
+ entrypoints: [join(import.meta.dir, "./fixtures/trivial/bundle-ws.ts")],
+ plugins: [
+ {
+ name: "test",
+ setup: b => {
+ b.module("ad", () => {
+ return {
+ exports: {
+ hello: "world",
+ },
+ loader: "object",
+ };
+ });
+ },
+ },
+ ],
+ }),
+ ).toThrow();
+ });
});
diff --git a/test/js/bun/plugin/module-plugins.ts b/test/js/bun/plugin/module-plugins.ts
new file mode 100644
index 000000000..d6034c5df
--- /dev/null
+++ b/test/js/bun/plugin/module-plugins.ts
@@ -0,0 +1,38 @@
+import { plugin } from "bun";
+plugin({
+ name: "i am virtual!",
+ setup(builder) {
+ builder.module("my-virtual-module-async", async () => {
+ // check
+ await Bun.sleep(1);
+ return {
+ exports: {
+ hello: "world",
+ },
+ loader: "object",
+ };
+ });
+
+ builder.module("my-virtual-module-sync", () => {
+ return {
+ exports: {
+ hello: "world",
+ },
+ loader: "object",
+ };
+ });
+
+ builder.onLoad({ filter: /.*/, namespace: "rejected-promise" }, async ({ path }) => {
+ throw new Error("Rejected Promise");
+ });
+
+ builder.onResolve({ filter: /.*/, namespace: "rejected-promise2" }, ({ path }) => ({
+ namespace: "rejected-promise2",
+ path,
+ }));
+
+ builder.onLoad({ filter: /.*/, namespace: "rejected-promise2" }, ({ path }) => {
+ return Promise.reject(new Error("Rejected Promise"));
+ });
+ },
+});
diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts
index c2827f600..2d3cfa1fa 100644
--- a/test/js/bun/plugin/plugins.test.ts
+++ b/test/js/bun/plugin/plugins.test.ts
@@ -185,6 +185,7 @@ plugin({
// This is to test that it works when imported from a separate file
import "../../third_party/svelte";
+import "./module-plugins";
describe("require", () => {
it("SSRs `<h1>Hello world!</h1>` with Svelte", () => {
@@ -210,6 +211,83 @@ describe("require", () => {
});
});
+describe("module", () => {
+ it("throws with require()", () => {
+ expect(() => require("my-virtual-module-async")).toThrow();
+ });
+
+ it("async module works with async import", async () => {
+ // @ts-expect-error
+ const { hello } = await import("my-virtual-module-async");
+
+ expect(hello).toBe("world");
+ delete require.cache["my-virtual-module-async"];
+ });
+
+ it("sync module module works with require()", async () => {
+ const { hello } = require("my-virtual-module-sync");
+
+ expect(hello).toBe("world");
+ delete require.cache["my-virtual-module-sync"];
+ });
+
+ it("sync module module works with require.resolve()", async () => {
+ expect(require.resolve("my-virtual-module-sync")).toBe("my-virtual-module-sync");
+ delete require.cache["my-virtual-module-sync"];
+ });
+
+ it("sync module module works with import", async () => {
+ // @ts-expect-error
+ const { hello } = await import("my-virtual-module-sync");
+
+ expect(hello).toBe("world");
+ delete require.cache["my-virtual-module-sync"];
+ });
+
+ it("modules are overridable", async () => {
+ // @ts-expect-error
+ let { hello, there } = await import("my-virtual-module-sync");
+ expect(there).toBeUndefined();
+ expect(hello).toBe("world");
+
+ Bun.plugin({
+ setup(builder) {
+ builder.module("my-virtual-module-sync", () => ({
+ exports: {
+ there: true,
+ },
+ loader: "object",
+ }));
+ },
+ });
+
+ {
+ const { there, hello } = require("my-virtual-module-sync");
+ expect(there).toBe(true);
+ expect(hello).toBeUndefined();
+ }
+
+ Bun.plugin({
+ setup(builder) {
+ builder.module("my-virtual-module-sync", () => ({
+ exports: {
+ yo: true,
+ },
+ loader: "object",
+ }));
+ },
+ });
+
+ {
+ // @ts-expect-error
+ const { there, hello, yo } = await import("my-virtual-module-sync");
+ expect(yo).toBe(true);
+ expect(hello).toBeUndefined();
+ expect(there).toBeUndefined();
+ }
+ });
+});
+
describe("dynamic import", () => {
it("SSRs `<h1>Hello world!</h1>` with Svelte", async () => {
const { default: App }: any = await import("./hello.svelte");