diff options
author | 2023-09-29 16:34:20 -0700 | |
---|---|---|
committer | 2023-09-29 16:34:20 -0700 | |
commit | a97847a49475e774695c38cff07a71eadf608c05 (patch) | |
tree | 26867f9be2eddaa0b752189a27810ed4db6ed902 | |
parent | eddb0078b5c9ff49bf67c0f1b1c2c623f0480b77 (diff) | |
download | bun-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.md | 87 | ||||
-rw-r--r-- | packages/bun-types/bun.d.ts | 31 | ||||
-rw-r--r-- | src/bun.js/bindings/BunPlugin.cpp | 254 | ||||
-rw-r--r-- | src/bun.js/bindings/BunPlugin.h | 22 | ||||
-rw-r--r-- | src/bun.js/bindings/ImportMetaObject.cpp | 32 | ||||
-rw-r--r-- | src/bun.js/bindings/JSBundlerPlugin.cpp | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/ModuleLoader.cpp | 4 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 30 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.h | 5 | ||||
-rw-r--r-- | src/bun.js/bindings/isBuiltinModule.cpp | 98 | ||||
-rw-r--r-- | src/bun.js/bindings/isBuiltinModule.h | 5 | ||||
-rw-r--r-- | src/bun.js/modules/NodeModuleModule.h | 17 | ||||
-rw-r--r-- | src/js/builtins/BundlerPlugin.ts | 3 | ||||
-rw-r--r-- | src/js/out/WebCoreJSBuiltins.cpp | 4 | ||||
-rw-r--r-- | test/bundler/bun-build-api.test.ts | 23 | ||||
-rw-r--r-- | test/js/bun/plugin/module-plugins.ts | 38 | ||||
-rw-r--r-- | test/js/bun/plugin/plugins.test.ts | 78 |
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"); |