Compare commits

...

2 Commits

Author SHA1 Message Date
RiskyMH
341fa14d01 Improve test to cover both --hot and --watch modes
- Use test.each to test both --hot and --watch flags
- Rename file to plugin-onload.test.ts (more generic name)
- Use tempDir with 'using' for automatic cleanup
- Remove unnecessary beforeEach hook
- Simplify counter logic
2025-11-07 10:06:03 +11:00
Claude Bot
4462468ad2 Fix hot reload for files loaded via Bun.plugin onLoad
When using `bun --hot` with plugins that use `onLoad`, only the entrypoint's
changes would trigger hot reloads. Files imported and transformed by plugins
were not being watched, so changes to them wouldn't trigger reloads.

The issue was that `Bun__runVirtualModule` (called for all module imports)
would run plugin `onLoad` callbacks but never added the source files to the
hot reloader's watch list.

This fix adds file watching for plugin-loaded modules when:
1. A plugin's onLoad callback successfully handles the module
2. Hot reloading is enabled
3. The file is in the "file" namespace (real filesystem)
4. The path is absolute and not in node_modules

Added test to verify files loaded through plugins now trigger hot reloads.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 06:56:20 +00:00
2 changed files with 185 additions and 1 deletions

View File

@@ -1114,9 +1114,28 @@ export fn Bun__runVirtualModule(globalObject: *JSGlobalObject, specifier_ptr: *c
else
specifier[@min(namespace.len + 1, specifier.len)..];
return globalObject.runOnLoadPlugins(bun.String.init(namespace), bun.String.init(after_namespace), .bun) catch {
const result = globalObject.runOnLoadPlugins(bun.String.init(namespace), bun.String.init(after_namespace), .bun) catch {
return JSValue.zero;
} orelse return .zero;
// Only add file to watcher if a plugin actually handled it (result is non-zero)
if (!result.isEmptyOrUndefinedOrNull()) {
// Add the file to the hot reloader's watch list if hot reloading is enabled
// and the file is in the "file" namespace (real filesystem files).
// This ensures that changes to files loaded through plugins trigger hot reloads.
const jsc_vm = globalObject.bunVM();
if (jsc_vm.isWatcherEnabled()) {
const is_file_namespace = namespace.len == 0 or bun.strings.eqlComptime(namespace, "file");
if (is_file_namespace and std.fs.path.isAbsolute(after_namespace)) {
if (!bun.strings.contains(after_namespace, "node_modules")) {
const loader = jsc_vm.transpiler.options.loader(std.fs.path.extension(after_namespace));
_ = jsc_vm.bun_watcher.addFileByPathSlow(after_namespace, loader);
}
}
}
}
return result;
}
fn getHardcodedModule(jsc_vm: *VirtualMachine, specifier: bun.String, hardcoded: HardcodedModule) ?ResolvedSource {

View File

@@ -0,0 +1,165 @@
import { spawn } from "bun";
import { expect, test } from "bun:test";
import { writeFileSync } from "fs";
import { bunEnv, bunExe, isDebug, tempDir } from "harness";
import { join } from "path";
const timeout = isDebug ? Infinity : 10_000;
test.each(["--hot", "--watch"])(
"should reload imported files when using Bun.plugin onLoad with %s",
async flag => {
using dir = tempDir("plugin-onload", {
"plugin.ts": `
import { plugin } from "bun";
plugin({
name: "test-plugin",
setup(build) {
build.onLoad({ filter: /\\.custom$/ }, (args) => {
const fs = require("fs");
const contents = fs.readFileSync(args.path, "utf8");
return {
contents: \`export const value = "\${contents.trim()}";\`,
loader: "ts",
};
});
},
});
`,
"data.custom": "value-0",
"index.ts": `
import { value } from "./data.custom";
console.log("[#!root] Value:", value);
`,
});
const pluginFile = join(String(dir), "plugin.ts");
const customFile = join(String(dir), "data.custom");
const entryFile = join(String(dir), "index.ts");
try {
var runner = spawn({
cmd: [bunExe(), "--preload", pluginFile, flag, entryFile],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
});
var reloadCounter = 0;
var finished = false;
async function onReload() {
writeFileSync(customFile, `value-${reloadCounter}`);
}
const killTimeout = setTimeout(() => {
finished = true;
runner.kill(9);
}, 5000);
var str = "";
for await (const line of runner.stdout) {
if (finished) break;
str += new TextDecoder().decode(line);
var any = false;
if (!/\[#!root\] Value:/g.test(str)) continue;
for (let line of str.split("\n")) {
if (!line.includes("[#!root]")) continue;
reloadCounter++;
str = "";
if (reloadCounter === 3) {
clearTimeout(killTimeout);
runner.unref();
runner.kill();
finished = true;
break;
}
expect(line).toContain(`[#!root] Value: value-${reloadCounter - 1}`);
any = true;
}
if (any) await onReload();
}
// Plugin-loaded files should trigger reloads when they change
expect(reloadCounter).toBeGreaterThanOrEqual(3);
} finally {
// @ts-ignore
runner?.unref?.();
// @ts-ignore
runner?.kill?.(9);
}
},
timeout,
);
test.each(["--hot", "--watch"])(
"should reload imported files when NOT using Bun.plugin (control test) with %s",
async flag => {
using dir = tempDir("plugin-onload-control", {
"data.js": `export const value = "value-0";`,
"index.ts": `
import { value } from "./data.js";
console.log("[#!root] Value:", value);
`,
});
const dataFile = join(String(dir), "data.js");
const entryFile = join(String(dir), "index.ts");
try {
var runner = spawn({
cmd: [bunExe(), flag, entryFile],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "inherit",
stdin: "ignore",
});
var reloadCounter = 0;
async function onReload() {
writeFileSync(dataFile, `export const value = "value-${reloadCounter}";`);
}
var str = "";
for await (const line of runner.stdout) {
str += new TextDecoder().decode(line);
var any = false;
if (!/\[#!root\] Value:/g.test(str)) continue;
for (let line of str.split("\n")) {
if (!line.includes("[#!root]")) continue;
reloadCounter++;
str = "";
if (reloadCounter === 3) {
runner.unref();
runner.kill();
break;
}
expect(line).toContain(`[#!root] Value: value-${reloadCounter - 1}`);
any = true;
}
if (any) await onReload();
}
expect(reloadCounter).toBeGreaterThanOrEqual(3);
} finally {
// @ts-ignore
runner?.unref?.();
// @ts-ignore
runner?.kill?.(9);
}
},
timeout,
);