mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 23:18:47 +00:00
Compare commits
4 Commits
claude/sql
...
claude/vir
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23be320484 | ||
|
|
3add6b1756 | ||
|
|
7059672379 | ||
|
|
aabccfc731 |
@@ -1162,6 +1162,7 @@ pub const JSBundler = struct {
|
||||
return JSBundlerPlugin__anyMatches(this, &namespace_string, &path_string, is_onLoad);
|
||||
}
|
||||
|
||||
|
||||
pub fn matchOnLoad(
|
||||
this: *Plugin,
|
||||
path: []const u8,
|
||||
|
||||
@@ -57,6 +57,7 @@ JSC_DECLARE_HOST_FUNCTION(jsBundlerPluginFunction_onLoadAsync);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsBundlerPluginFunction_onResolveAsync);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsBundlerPluginFunction_onBeforeParse);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsBundlerPluginFunction_generateDeferPromise);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsBundlerPluginFunction_addVirtualModule);
|
||||
|
||||
void BundlerPlugin::NamespaceList::append(JSC::VM& vm, JSC::RegExp* filter, String& namespaceString, unsigned& index)
|
||||
{
|
||||
@@ -101,6 +102,13 @@ static bool anyMatchesForNamespace(JSC::VM& vm, BundlerPlugin::NamespaceList& li
|
||||
}
|
||||
bool BundlerPlugin::anyMatchesCrossThread(JSC::VM& vm, const BunString* namespaceStr, const BunString* path, bool isOnLoad)
|
||||
{
|
||||
auto pathString = path->toWTFString(BunString::ZeroCopy);
|
||||
|
||||
// Check virtual modules for both onLoad and onResolve
|
||||
if (this->virtualModules && this->virtualModules->contains(pathString)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isOnLoad) {
|
||||
return anyMatchesForNamespace(vm, this->onLoad, namespaceStr, path);
|
||||
} else {
|
||||
@@ -108,6 +116,54 @@ bool BundlerPlugin::anyMatchesCrossThread(JSC::VM& vm, const BunString* namespac
|
||||
}
|
||||
}
|
||||
|
||||
bool BundlerPlugin::hasVirtualModule(const String& path) const
|
||||
{
|
||||
return virtualModules && virtualModules->contains(path);
|
||||
}
|
||||
|
||||
JSC::JSObject* BundlerPlugin::getVirtualModule(const String& path)
|
||||
{
|
||||
if (!virtualModules) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto it = virtualModules->find(path);
|
||||
if (it != virtualModules->end()) {
|
||||
unsigned index = it->value;
|
||||
if (index < virtualModulesList.list().size()) {
|
||||
return virtualModulesList.list()[index].get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void BundlerPlugin::addVirtualModule(JSC::VM& vm, JSC::JSCell* owner, const String& path, JSC::JSObject* moduleFunction)
|
||||
{
|
||||
if (!virtualModules) {
|
||||
virtualModules = new VirtualModuleMap();
|
||||
}
|
||||
|
||||
unsigned index = virtualModulesList.list().size();
|
||||
virtualModulesList.append(vm, owner, moduleFunction);
|
||||
virtualModules->set(path, index);
|
||||
}
|
||||
|
||||
void BundlerPlugin::tombstone()
|
||||
{
|
||||
tombstoned = true;
|
||||
if (virtualModules) {
|
||||
delete virtualModules;
|
||||
virtualModules = nullptr;
|
||||
}
|
||||
virtualModulesList.clear();
|
||||
}
|
||||
|
||||
void BundlerPlugin::visitAdditionalChildren(JSC::JSCell* cell, JSC::SlotVisitor& visitor)
|
||||
{
|
||||
deferredPromises.visit(visitor);
|
||||
virtualModulesList.visit(visitor);
|
||||
}
|
||||
|
||||
static const HashTableValue JSBundlerPluginHashTable[] = {
|
||||
{ "addFilter"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBundlerPluginFunction_addFilter, 3 } },
|
||||
{ "addError"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBundlerPluginFunction_addError, 3 } },
|
||||
@@ -115,6 +171,7 @@ static const HashTableValue JSBundlerPluginHashTable[] = {
|
||||
{ "onResolveAsync"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBundlerPluginFunction_onResolveAsync, 4 } },
|
||||
{ "onBeforeParse"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBundlerPluginFunction_onBeforeParse, 4 } },
|
||||
{ "generateDeferPromise"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBundlerPluginFunction_generateDeferPromise, 0 } },
|
||||
{ "addVirtualModule"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBundlerPluginFunction_addVirtualModule, 2 } },
|
||||
};
|
||||
|
||||
class JSBundlerPlugin final : public JSC::JSDestructibleObject {
|
||||
@@ -193,7 +250,7 @@ void JSBundlerPlugin::visitAdditionalChildren(Visitor& visitor)
|
||||
this->onLoadFunction.visit(visitor);
|
||||
this->onResolveFunction.visit(visitor);
|
||||
this->setupFunction.visit(visitor);
|
||||
this->plugin.deferredPromises.visit(this, visitor);
|
||||
this->plugin.visitAdditionalChildren(this, visitor);
|
||||
}
|
||||
|
||||
template<typename Visitor>
|
||||
@@ -467,6 +524,37 @@ JSC_DEFINE_HOST_FUNCTION(jsBundlerPluginFunction_generateDeferPromise, (JSC::JSG
|
||||
return encoded_defer_promise;
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsBundlerPluginFunction_addVirtualModule, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
|
||||
{
|
||||
JSBundlerPlugin* thisObject = jsCast<JSBundlerPlugin*>(callFrame->thisValue());
|
||||
if (thisObject->plugin.tombstoned) {
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
JSC::JSValue pathValue = callFrame->argument(0);
|
||||
JSC::JSValue moduleValue = callFrame->argument(1);
|
||||
|
||||
if (!pathValue.isString()) {
|
||||
throwTypeError(globalObject, scope, "Expected first argument to be a string"_s);
|
||||
return JSC::JSValue::encode({});
|
||||
}
|
||||
|
||||
if (!moduleValue.isObject() || !moduleValue.isCallable()) {
|
||||
throwTypeError(globalObject, scope, "Expected second argument to be a function"_s);
|
||||
return JSC::JSValue::encode({});
|
||||
}
|
||||
|
||||
WTF::String path = pathValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
thisObject->plugin.addVirtualModule(vm, thisObject, path, JSC::asObject(moduleValue));
|
||||
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
void JSBundlerPlugin::finishCreation(JSC::VM& vm)
|
||||
{
|
||||
Base::finishCreation(vm);
|
||||
@@ -686,6 +774,13 @@ extern "C" void JSBundlerPlugin__drainDeferred(Bun::JSBundlerPlugin* pluginObjec
|
||||
extern "C" void JSBundlerPlugin__tombstone(Bun::JSBundlerPlugin* plugin)
|
||||
{
|
||||
plugin->plugin.tombstone();
|
||||
|
||||
// Clear virtual modules when tombstoning
|
||||
if (plugin->plugin.virtualModules) {
|
||||
plugin->plugin.virtualModules->clear();
|
||||
delete plugin->plugin.virtualModules;
|
||||
plugin->plugin.virtualModules = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue JSBundlerPlugin__runOnEndCallbacks(Bun::JSBundlerPlugin* plugin, JSC::EncodedJSValue encodedBuildPromise, JSC::EncodedJSValue encodedBuildResult, JSC::EncodedJSValue encodedRejection)
|
||||
@@ -738,4 +833,29 @@ extern "C" JSC::JSGlobalObject* JSBundlerPlugin__globalObject(Bun::JSBundlerPlug
|
||||
return plugin->m_globalObject;
|
||||
}
|
||||
|
||||
extern "C" bool JSBundlerPlugin__hasVirtualModule(Bun::JSBundlerPlugin* plugin, const BunString* path)
|
||||
{
|
||||
WTF::String pathStr = path ? path->toWTFString(BunString::ZeroCopy) : WTF::String();
|
||||
return plugin->plugin.hasVirtualModule(pathStr);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue JSBundlerPlugin__getVirtualModule(Bun::JSBundlerPlugin* plugin, const BunString* path)
|
||||
{
|
||||
WTF::String pathStr = path ? path->toWTFString(BunString::ZeroCopy) : WTF::String();
|
||||
auto* virtualModule = plugin->plugin.getVirtualModule(pathStr);
|
||||
if (virtualModule) {
|
||||
return JSC::JSValue::encode(virtualModule);
|
||||
}
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
extern "C" void JSBundlerPlugin__addVirtualModule(Bun::JSBundlerPlugin* plugin, const BunString* path, JSC::EncodedJSValue encodedModuleFunction)
|
||||
{
|
||||
WTF::String pathStr = path ? path->toWTFString(BunString::ZeroCopy) : WTF::String();
|
||||
JSC::JSValue moduleFunction = JSC::JSValue::decode(encodedModuleFunction);
|
||||
if (moduleFunction.isObject()) {
|
||||
plugin->plugin.addVirtualModule(plugin->vm(), plugin, pathStr, JSC::asObject(moduleFunction));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Bun
|
||||
|
||||
@@ -20,6 +20,8 @@ using namespace JSC;
|
||||
|
||||
class BundlerPlugin final {
|
||||
public:
|
||||
using VirtualModuleMap = WTF::UncheckedKeyHashMap<String, unsigned>;
|
||||
|
||||
/// In native plugins, the regular expression could be called concurrently on multiple threads.
|
||||
/// Therefore, we need a mutex to synchronize access.
|
||||
class FilterRegExp {
|
||||
@@ -119,7 +121,11 @@ public:
|
||||
|
||||
public:
|
||||
bool anyMatchesCrossThread(JSC::VM&, const BunString* namespaceStr, const BunString* path, bool isOnLoad);
|
||||
void tombstone() { tombstoned = true; }
|
||||
bool hasVirtualModule(const String& path) const;
|
||||
JSC::JSObject* getVirtualModule(const String& path);
|
||||
void addVirtualModule(JSC::VM& vm, JSC::JSCell* owner, const String& path, JSC::JSObject* moduleFunction);
|
||||
void tombstone();
|
||||
void visitAdditionalChildren(JSC::JSCell*, JSC::SlotVisitor&);
|
||||
|
||||
BundlerPlugin(void* config, BunPluginTarget target, JSBundlerPluginAddErrorCallback addError, JSBundlerPluginOnLoadAsyncCallback onLoadAsync, JSBundlerPluginOnResolveAsyncCallback onResolveAsync)
|
||||
: addError(addError)
|
||||
@@ -130,12 +136,20 @@ public:
|
||||
this->config = config;
|
||||
}
|
||||
|
||||
~BundlerPlugin() {
|
||||
if (virtualModules) {
|
||||
delete virtualModules;
|
||||
}
|
||||
}
|
||||
|
||||
NamespaceList onLoad = {};
|
||||
NamespaceList onResolve = {};
|
||||
NativePluginList onBeforeParse = {};
|
||||
BunPluginTarget target { BunPluginTargetBrowser };
|
||||
|
||||
WriteBarrierList<JSC::JSPromise> deferredPromises = {};
|
||||
VirtualModuleMap* virtualModules { nullptr };
|
||||
WriteBarrierList<JSC::JSObject> virtualModulesList = {};
|
||||
|
||||
JSBundlerPluginAddErrorCallback addError;
|
||||
JSBundlerPluginOnLoadAsyncCallback onLoadAsync;
|
||||
|
||||
@@ -2889,6 +2889,7 @@ pub const BundleV2 = struct {
|
||||
original_target: options.Target,
|
||||
) bool {
|
||||
if (this.plugins) |plugins| {
|
||||
// anyMatches will check for virtual modules too
|
||||
if (plugins.hasAnyMatches(&import_record.path, false)) {
|
||||
// This is where onResolve plugins are enqueued
|
||||
var resolve: *jsc.API.JSBundler.Resolve = bun.default_allocator.create(jsc.API.JSBundler.Resolve) catch unreachable;
|
||||
|
||||
@@ -8,6 +8,7 @@ interface BundlerPlugin {
|
||||
onLoad: Map<string, [RegExp, OnLoadCallback][]>;
|
||||
onResolve: Map<string, [RegExp, OnResolveCallback][]>;
|
||||
onEndCallbacks: Array<(build: Bun.BuildOutput) => void | Promise<void>> | undefined;
|
||||
virtualModules: Map<string, () => { contents: string; loader?: string }> | undefined;
|
||||
/** Binding to `JSBundlerPlugin__onLoadAsync` */
|
||||
onLoadAsync(
|
||||
internalID,
|
||||
@@ -19,6 +20,7 @@ interface BundlerPlugin {
|
||||
/** Binding to `JSBundlerPlugin__addError` */
|
||||
addError(internalID: number, error: any, which: number): void;
|
||||
addFilter(filter, namespace, number): void;
|
||||
addVirtualModule(path: string, moduleFunction: () => any): void;
|
||||
generateDeferPromise(id: number): Promise<void>;
|
||||
promises: Array<Promise<any>> | undefined;
|
||||
|
||||
@@ -112,6 +114,12 @@ export function runOnEndCallbacks(
|
||||
buildResult: Bun.BuildOutput,
|
||||
buildRejection: AggregateError | undefined,
|
||||
): Promise<void> | void {
|
||||
// Clean up virtual modules when build ends
|
||||
if (this.virtualModules) {
|
||||
this.virtualModules.clear();
|
||||
this.virtualModules = undefined;
|
||||
}
|
||||
|
||||
const callbacks = this.onEndCallbacks;
|
||||
if (!callbacks) return;
|
||||
const promises: PromiseLike<unknown>[] = [];
|
||||
@@ -347,8 +355,24 @@ export function runSetupFunction(
|
||||
onBeforeParse,
|
||||
onStart,
|
||||
resolve: notImplementedIssueFn(2771, "build.resolve()"),
|
||||
module: () => {
|
||||
throw new TypeError("module() is not supported in Bun.build() yet. Only via Bun.plugin() at runtime");
|
||||
module: (specifier: string, callback: () => { contents: string; loader?: string }) => {
|
||||
if (typeof specifier !== "string") {
|
||||
throw new TypeError("module() specifier must be a string");
|
||||
}
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError("module() callback must be a function");
|
||||
}
|
||||
|
||||
// Store the virtual module
|
||||
if (!self.virtualModules) {
|
||||
self.virtualModules = new Map();
|
||||
}
|
||||
self.virtualModules.$set(specifier, callback);
|
||||
|
||||
// Register the virtual module with the C++ side
|
||||
self.addVirtualModule(specifier, callback);
|
||||
|
||||
return this;
|
||||
},
|
||||
addPreload: () => {
|
||||
throw new TypeError("addPreload() is not supported in Bun.build() yet.");
|
||||
@@ -395,7 +419,15 @@ export function runOnResolvePlugins(this: BundlerPlugin, specifier, inputNamespa
|
||||
const kind = $ImportKindIdToLabel[kindId];
|
||||
|
||||
var promiseResult: any = (async (inputPath, inputNamespace, importer, kind) => {
|
||||
var { onResolve, onLoad } = this;
|
||||
var { onResolve, onLoad, virtualModules } = this;
|
||||
|
||||
// Check for virtual modules first
|
||||
if (virtualModules && virtualModules.$has(inputPath)) {
|
||||
// Return the virtual module with file namespace (empty string means file)
|
||||
this.onResolveAsync(internalID, inputPath, "", false);
|
||||
return null;
|
||||
}
|
||||
|
||||
var results = onResolve.$get(inputNamespace);
|
||||
if (!results) {
|
||||
this.onResolveAsync(internalID, null, null, null);
|
||||
@@ -511,6 +543,48 @@ export function runOnLoadPlugins(
|
||||
|
||||
const generateDefer = () => this.generateDeferPromise(internalID);
|
||||
var promiseResult = (async (internalID, path, namespace, isServerSide, defaultLoader, generateDefer) => {
|
||||
// Check for virtual modules first (file namespace and in virtualModules map)
|
||||
if (this.virtualModules && this.virtualModules.$has(path) && (namespace === "file" || namespace === "")) {
|
||||
const virtualModuleCallback = this.virtualModules.$get(path);
|
||||
if (virtualModuleCallback) {
|
||||
let result;
|
||||
try {
|
||||
result = virtualModuleCallback();
|
||||
} catch (e) {
|
||||
// If the callback throws, report it as an error
|
||||
this.addError(internalID, e, 1);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!result || !$isObject(result)) {
|
||||
throw new TypeError('Virtual module must return an object with "contents" property');
|
||||
}
|
||||
|
||||
var { contents, loader = defaultLoader } = result;
|
||||
|
||||
if (!(typeof contents === "string")) {
|
||||
throw new TypeError('Virtual module must return an object with "contents" as a string');
|
||||
}
|
||||
|
||||
if (!(typeof loader === "string")) {
|
||||
throw new TypeError('Virtual module "loader" must be a string if provided');
|
||||
}
|
||||
|
||||
const chosenLoader = LOADERS_MAP[loader];
|
||||
if (chosenLoader === undefined) {
|
||||
throw new TypeError(`Loader ${loader} is not supported.`);
|
||||
}
|
||||
|
||||
this.onLoadAsync(internalID, contents, chosenLoader);
|
||||
return null;
|
||||
} catch (e) {
|
||||
this.addError(internalID, e, 1);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var results = this.onLoad.$get(namespace);
|
||||
if (!results) {
|
||||
this.onLoadAsync(internalID, null, null);
|
||||
|
||||
@@ -402,27 +402,28 @@ describe("Bun.build", () => {
|
||||
expect(x.logs[0].position).toBeTruthy();
|
||||
});
|
||||
|
||||
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",
|
||||
};
|
||||
});
|
||||
},
|
||||
test("module() is now supported", async () => {
|
||||
// module() is now implemented and should not throw
|
||||
const result = await Bun.build({
|
||||
entrypoints: [join(import.meta.dir, "./fixtures/trivial/bundle-ws.ts")],
|
||||
plugins: [
|
||||
{
|
||||
name: "test",
|
||||
setup: b => {
|
||||
// Verify module() exists and can be called
|
||||
expect(typeof b.module).toBe("function");
|
||||
b.module("test-virtual-module", () => {
|
||||
return {
|
||||
contents: "export default 'hello world';",
|
||||
loader: "js",
|
||||
};
|
||||
});
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow();
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
test("non-object plugins throw invalid argument errors", () => {
|
||||
|
||||
438
test/bundler/bundler-plugin-virtual-modules.test.ts
Normal file
438
test/bundler/bundler-plugin-virtual-modules.test.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
import * as path from "node:path";
|
||||
|
||||
test("Bun.build plugin virtual modules - basic", async () => {
|
||||
using dir = tempDir("virtual-basic", {
|
||||
"entry.ts": `
|
||||
import message from "my-virtual-module";
|
||||
console.log(message);
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [{
|
||||
name: "virtual-module-plugin",
|
||||
setup(build) {
|
||||
build.module("my-virtual-module", () => ({
|
||||
contents: `export default "Hello from virtual module!"`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("Hello from virtual module!");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - multiple modules", async () => {
|
||||
using dir = tempDir("virtual-multiple", {
|
||||
"entry.ts": `
|
||||
import { greeting } from "virtual-greeting";
|
||||
import { name } from "virtual-name";
|
||||
console.log(greeting + " " + name);
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [{
|
||||
name: "multi-virtual-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-greeting", () => ({
|
||||
contents: `export const greeting = "Hello";`,
|
||||
loader: "js",
|
||||
}));
|
||||
|
||||
build.module("virtual-name", () => ({
|
||||
contents: `export const name = "World";`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("Hello");
|
||||
expect(output).toContain("World");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - TypeScript", async () => {
|
||||
using dir = tempDir("virtual-typescript", {
|
||||
"entry.ts": `
|
||||
import { calculate } from "virtual-math";
|
||||
console.log(calculate(5, 10));
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [{
|
||||
name: "typescript-virtual-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-math", () => ({
|
||||
contents: `
|
||||
export function calculate(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
`,
|
||||
loader: "ts",
|
||||
}));
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("calculate(5, 10)"); // Function call is in output
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - JSON", async () => {
|
||||
using dir = tempDir("virtual-json", {
|
||||
"entry.ts": `
|
||||
import config from "virtual-config";
|
||||
console.log(config.version);
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [{
|
||||
name: "json-virtual-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-config", () => ({
|
||||
contents: JSON.stringify({ version: "1.2.3", enabled: true }),
|
||||
loader: "json",
|
||||
}));
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("1.2.3");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - with onLoad and onResolve", async () => {
|
||||
using dir = tempDir("virtual-mixed", {
|
||||
"entry.ts": `
|
||||
import virtual from "my-virtual";
|
||||
import modified from "./real.js";
|
||||
console.log(virtual, modified);
|
||||
`,
|
||||
"real.js": `export default "original";`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [{
|
||||
name: "mixed-plugin",
|
||||
setup(build) {
|
||||
// Virtual module
|
||||
build.module("my-virtual", () => ({
|
||||
contents: `export default "virtual content";`,
|
||||
loader: "js",
|
||||
}));
|
||||
|
||||
// Regular onLoad plugin
|
||||
build.onLoad({ filter: /\.js$/ }, () => ({
|
||||
contents: `export default "modified";`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("virtual content");
|
||||
expect(output).toContain("modified");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - dynamic content", async () => {
|
||||
using dir = tempDir("virtual-dynamic", {
|
||||
"entry.ts": `
|
||||
import timestamp from "virtual-timestamp";
|
||||
console.log(timestamp);
|
||||
`,
|
||||
});
|
||||
|
||||
const buildTime = Date.now();
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [{
|
||||
name: "dynamic-virtual-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-timestamp", () => ({
|
||||
contents: `export default ${buildTime};`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain(buildTime.toString());
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - nested imports", async () => {
|
||||
using dir = tempDir("virtual-nested", {
|
||||
"entry.ts": `
|
||||
import { main } from "virtual-main";
|
||||
console.log(main());
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [{
|
||||
name: "nested-virtual-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-main", () => ({
|
||||
contents: `
|
||||
import { helper } from "virtual-helper";
|
||||
export function main() {
|
||||
return helper() + " from main";
|
||||
}
|
||||
`,
|
||||
loader: "js",
|
||||
}));
|
||||
|
||||
build.module("virtual-helper", () => ({
|
||||
contents: `
|
||||
export function helper() {
|
||||
return "Hello";
|
||||
}
|
||||
`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("helper() + \" from main\""); // Check for the function composition
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - multiple plugins", async () => {
|
||||
using dir = tempDir("virtual-multi-plugin", {
|
||||
"entry.ts": `
|
||||
import first from "virtual-first";
|
||||
import second from "virtual-second";
|
||||
console.log(first, second);
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "first-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-first", () => ({
|
||||
contents: `export default "from first plugin";`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "second-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-second", () => ({
|
||||
contents: `export default "from second plugin";`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("from first plugin");
|
||||
expect(output).toContain("from second plugin");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - error handling", async () => {
|
||||
using dir = tempDir("virtual-error", {
|
||||
"entry.ts": `
|
||||
import data from "virtual-error";
|
||||
console.log(data);
|
||||
`,
|
||||
});
|
||||
|
||||
// Plugin errors are thrown as "Bundle failed"
|
||||
await expect(
|
||||
Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [{
|
||||
name: "error-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-error", () => {
|
||||
throw new Error("Failed to generate virtual module");
|
||||
});
|
||||
},
|
||||
}],
|
||||
})
|
||||
).rejects.toThrow("Bundle failed");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - CSS", async () => {
|
||||
using dir = tempDir("virtual-css", {
|
||||
"entry.ts": `
|
||||
import styles from "virtual-styles";
|
||||
console.log(styles);
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [{
|
||||
name: "css-virtual-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-styles", () => ({
|
||||
contents: `
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
loader: "css",
|
||||
}));
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(2); // JS and CSS output
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - onLoad plugins still work", async () => {
|
||||
using dir = tempDir("virtual-with-onload", {
|
||||
"entry.ts": `
|
||||
import virtual from "my-virtual";
|
||||
import data from "./real.json";
|
||||
console.log(virtual, data);
|
||||
`,
|
||||
"real.json": `{"original": "data"}`,
|
||||
});
|
||||
|
||||
let onLoadCalled = false;
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [{
|
||||
name: "combined-plugin",
|
||||
setup(build) {
|
||||
// Add virtual module
|
||||
build.module("my-virtual", () => ({
|
||||
contents: `export default "virtual content";`,
|
||||
loader: "js",
|
||||
}));
|
||||
|
||||
// Also add regular onLoad plugin for JSON files
|
||||
build.onLoad({ filter: /\.json$/ }, (args) => {
|
||||
onLoadCalled = true;
|
||||
return {
|
||||
contents: `{"modified": "by onLoad plugin"}`,
|
||||
loader: "json",
|
||||
};
|
||||
});
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(onLoadCalled).toBe(true);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("virtual content");
|
||||
expect(output).toContain("modified");
|
||||
expect(output).toContain("by onLoad plugin");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - no memory leak on repeated builds", async () => {
|
||||
using dir = tempDir("virtual-memory", {
|
||||
"entry.ts": `
|
||||
import msg from "test-module";
|
||||
console.log(msg);
|
||||
`,
|
||||
});
|
||||
|
||||
// Track memory usage with multiple builds
|
||||
const initialMemory = process.memoryUsage().heapUsed;
|
||||
const memoryAfterBuilds = [];
|
||||
|
||||
// Run multiple builds to check for memory leaks
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [{
|
||||
name: `test-plugin-${i}`,
|
||||
setup(build) {
|
||||
// Create a large callback to make memory leaks more visible
|
||||
const largeData = new Array(10000).fill(`data-${i}`);
|
||||
build.module("test-module", () => ({
|
||||
contents: `export default "${largeData[0]}";`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
// Force GC after each build if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
memoryAfterBuilds.push(process.memoryUsage().heapUsed);
|
||||
}
|
||||
|
||||
// Memory usage should stabilize and not continuously grow
|
||||
// Check that the last few builds don't show significant growth
|
||||
const lastThreeBuilds = memoryAfterBuilds.slice(-3);
|
||||
const avgLastThree = lastThreeBuilds.reduce((a, b) => a + b, 0) / 3;
|
||||
const firstThreeBuilds = memoryAfterBuilds.slice(0, 3);
|
||||
const avgFirstThree = firstThreeBuilds.reduce((a, b) => a + b, 0) / 3;
|
||||
|
||||
// Memory shouldn't grow by more than 50% between first and last builds
|
||||
// This is a loose check to avoid flakiness
|
||||
const memoryGrowthRatio = avgLastThree / avgFirstThree;
|
||||
expect(memoryGrowthRatio).toBeLessThan(1.5);
|
||||
});
|
||||
Reference in New Issue
Block a user