Skip to content

Conversation

robobun
Copy link
Collaborator

@robobun robobun commented Oct 13, 2025

Summary

Fixes an issue where Bun's watch mode would exit with an error when trying to import a file that doesn't exist yet. Now, watch mode polls for missing modules and automatically reloads when they appear.

Changes

  • New abstraction: MissingModulePollTimer.zig implements exponential backoff polling (2ms → 100ms)
  • EventLoopTimer integration: Added timer tag, type mapping, and fire handler
  • Error detection: Detects MODULE_NOT_FOUND and ERR_MODULE_NOT_FOUND errors in watch mode
  • Auto-reload: Triggers hot reload when missing file is detected

Integration Points

  1. VirtualMachine.uncaughtException() - Starts polling when module-not-found error occurs
  2. VirtualMachine.handlePendingInternalPromiseRejection() - Same for promise rejections
  3. Timer lifecycle managed through VM timer system

Test Plan

Added test that verifies:

  • Process doesn't exit when requiring missing module in watch mode
  • File creation is detected via polling
  • Hot reload is triggered automatically
  • Process successfully loads the module after it's created

Test passes: bun bd test test/cli/watch/watch-missing-external.test.ts

🤖 Generated with Claude Code

When a script tries to import a file that doesn't exist in watch mode,
Bun now polls for the missing module instead of exiting with an error.
This allows developers to create the file and have Bun automatically
reload once it appears.

## Implementation

- Created `MissingModulePollTimer.zig` that polls for missing files
  using exponential backoff (2ms to 100ms intervals)
- Added timer integration to EventLoopTimer enum and fire handler
- Integrated polling detection in `VirtualMachine.uncaughtException()`
  and `handlePendingInternalPromiseRejection()`
- Detects MODULE_NOT_FOUND and ERR_MODULE_NOT_FOUND error codes
- Triggers hot reload when the missing file appears

## Testing

Added tests that verify the module polling works correctly when
a missing module is created after the process starts.

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

Co-Authored-By: Claude <[email protected]>
@robobun
Copy link
Collaborator Author

robobun commented Oct 13, 2025

Updated 2:56 PM PT - Oct 13th, 2025

@autofix-ci[bot], your commit 054e5b2 has 6 failures in Build #29106 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 23618

That installs a local version of the PR into your bun-23618 executable, so you can run:

bun-23618 --bun

Copy link
Contributor

coderabbitai bot commented Oct 13, 2025

Walkthrough

Adds watch-mode handling for missing modules by introducing a polling timer that checks for file appearance and triggers hot reload. Integrates the timer into the VM’s error paths and the event loop. Extends Timer API to expose and manage the new timer. Adds CLI watch tests validating the behavior.

Changes

Cohort / File(s) Summary of Changes
VM watch-mode missing-module handling
src/bun.js/VirtualMachine.zig
Adds tryHandleMissingModuleInWatchMode; invoked in uncaughtException and handlePendingInternalPromiseRejection to start polling for missing modules when in watch mode.
Timer subsystem integration
src/bun.js/api/Timer.zig, src/bun.js/api/Timer/EventLoopTimer.zig, src/bun.js/api/Timer/MissingModulePollTimer.zig
Introduces MissingModulePollTimer, wires it into Timer.All, adds EventLoopTimer.Tag variant and fire handling, and implements polling with exponential backoff, start/stop/deinit, and onTimeout to trigger VM reload when file appears.
CLI watch tests
test/cli/watch/watch-missing-external.test.ts, test/cli/watch/watch-missing-module-simple.test.ts, test/cli/watch/watch-missing-module.test.ts
Adds tests verifying watch mode polls for missing modules, detects creation, and exits successfully; includes timeouts and process cleanup.

Suggested reviewers

  • nektro
  • dylan-conway
  • alii

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description does not follow the repository’s required template headings; it uses sections like “## Summary” and “## Test Plan” instead of the mandated “### What does this PR do?” and “### How did you verify your code works?” headings. Please update the description to match the template by adding a “### What does this PR do?” section summarizing the change and a “### How did you verify your code works?” section describing the verification steps.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title “Add missing module polling in watch mode” clearly summarizes the primary feature added by this changeset, is concise, and directly reflects the main change without extraneous detail.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 72900ec and e7c624e.

📒 Files selected for processing (7)
  • src/bun.js/VirtualMachine.zig (3 hunks)
  • src/bun.js/api/Timer.zig (2 hunks)
  • src/bun.js/api/Timer/EventLoopTimer.zig (3 hunks)
  • src/bun.js/api/Timer/MissingModulePollTimer.zig (1 hunks)
  • test/cli/watch/watch-missing-external.test.ts (1 hunks)
  • test/cli/watch/watch-missing-module-simple.test.ts (1 hunks)
  • test/cli/watch/watch-missing-module.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (11)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
test/cli/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/cli/**/*.{js,ts}: Place CLI command tests (e.g., bun install, bun init) under test/cli/
When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must live under test/ and end with .test.ts or .test.tsx
In tests, always use port: 0; do not hardcode ports or roll your own random port
Prefer normalizeBunSnapshot for snapshotting test output instead of asserting raw strings
Do not write tests that assert absence of crashes (e.g., 'no panic' or 'no uncaught exception')
Use Bun’s Jest-compatible runner (import { test, expect } from "bun:test") for tests
Avoid shell commands like find or grep in tests; use Bun’s Glob and built-in tools instead
Prefer running tests via bun bd test and use provided harness utilities (bunEnv, bunExe, tempDir)
Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests

Files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
test/cli/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place CLI command tests under test/cli/

Files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun:test for files ending with *.test.{ts,js,jsx,tsx,mjs,cjs}
Prefer concurrent tests (test.concurrent/describe.concurrent) over sequential when feasible
Organize tests with describe blocks to group related tests
Use utilities like describe.each, toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach) and track resources for cleanup

Files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
test/**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

For large/repetitive strings, use Buffer.alloc(count, fill).toString() instead of "A".repeat(count)

Files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/bun.js/api/Timer/EventLoopTimer.zig
  • src/bun.js/api/Timer.zig
  • src/bun.js/api/Timer/MissingModulePollTimer.zig
  • src/bun.js/VirtualMachine.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/bun.js/api/Timer/EventLoopTimer.zig
  • src/bun.js/api/Timer.zig
  • src/bun.js/api/Timer/MissingModulePollTimer.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/api/Timer/EventLoopTimer.zig
  • src/bun.js/api/Timer.zig
  • src/bun.js/api/Timer/MissingModulePollTimer.zig
  • src/bun.js/VirtualMachine.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/bun.js/api/Timer/EventLoopTimer.zig
  • src/bun.js/api/Timer.zig
  • src/bun.js/api/Timer/MissingModulePollTimer.zig
  • src/bun.js/VirtualMachine.zig
🧠 Learnings (12)
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : Do not set explicit test timeouts; Bun already has timeouts

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
📚 Learning: 2025-10-08T13:48:02.430Z
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23373
File: test/js/bun/tarball/extract.test.ts:107-111
Timestamp: 2025-10-08T13:48:02.430Z
Learning: In Bun's test runner, use `expect(async () => { await ... }).toThrow()` to assert async rejections. Unlike Jest/Vitest, Bun does not require `await expect(...).rejects.toThrow()` - the async function wrapper with `.toThrow()` is the correct pattern for async error assertions in Bun tests.

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Prefer running tests via bun bd test <file> and use provided harness utilities (bunEnv, bunExe, tempDir)

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : Always check exit codes and error scenarios in tests (e.g., spawned processes should assert non-zero on failure)

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js,test/**/*-fixture.ts} : Use `using`/`await using` for resource cleanup with Bun APIs (e.g., `Bun.spawn`, `Bun.listen`, `Bun.serve`)

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/js/{bun,node}/** : Organize unit tests by module under `/test/js/bun/` and `/test/js/node/`

Applied to files:

  • test/cli/watch/watch-missing-module.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending with `*.test.{ts,js,jsx,tsx,mjs,cjs}`

Applied to files:

  • test/cli/watch/watch-missing-module.test.ts
  • test/cli/watch/watch-missing-external.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Use Bun’s Jest-compatible runner (import { test, expect } from "bun:test") for tests

Applied to files:

  • test/cli/watch/watch-missing-module.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : Import common utilities from `harness` (e.g., `bunExe`, `bunEnv`, `tempDirWithFiles`, platform helpers, GC helpers)

Applied to files:

  • test/cli/watch/watch-missing-module.test.ts
🧬 Code graph analysis (3)
test/cli/watch/watch-missing-module-simple.test.ts (1)
test/harness.ts (2)
  • tempDir (277-284)
  • bunExe (102-105)
test/cli/watch/watch-missing-module.test.ts (1)
test/harness.ts (2)
  • tempDir (277-284)
  • bunExe (102-105)
test/cli/watch/watch-missing-external.test.ts (1)
test/harness.ts (2)
  • tempDir (277-284)
  • bunExe (102-105)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (11)
src/bun.js/api/Timer.zig (2)

55-56: LGTM: Clean integration of MissingModulePollTimer.

The field is properly initialized via MissingModulePollTimer.init() and the comment clearly describes its purpose.


608-608: LGTM: Public import added.

src/bun.js/api/Timer/EventLoopTimer.zig (1)

72-72: LGTM: EventLoopTimer integration follows existing patterns.

The MissingModulePollTimer is correctly integrated into the timer system:

  • Added to the Tag enum
  • Type mapping references the correct type
  • fire() handler properly retrieves the parent pointer and invokes onTimeout

Also applies to: 98-98, 193-196

src/bun.js/VirtualMachine.zig (2)

647-690: LGTM: Watch-mode missing module handler is well-guarded.

The implementation correctly:

  • Guards execution to watch mode only (line 650)
  • Validates error structure and code (lines 652-668)
  • Extracts the specifier safely (lines 671-676)
  • Handles allocation failures gracefully with catch return
  • Provides clear user feedback (line 688)

The UTF-8 conversion at lines 679-680 is immediately freed and then startPolling dupes it again at line 683. While slightly inefficient, this is acceptable for error paths and keeps the ownership model clear.


714-716: LGTM: Integration points are correct.

Calling tryHandleMissingModuleInWatchMode before the standard exception handling in both uncaughtException and handlePendingInternalPromiseRejection ensures that watch mode can intercept module-not-found errors before they terminate the process.

Also applies to: 732-736

test/cli/watch/watch-missing-external.test.ts (2)

23-24: Timing assumption may be fragile on slower CI systems.

The 500ms wait assumes the process has started and hit the error. On slower CI systems or under load, this might not be sufficient.

Consider adding a more robust synchronization mechanism:

  • Parse stderr for the "Waiting for module to exist:" message before writing the file
  • Or increase the wait time to 1000ms for more margin
// Wait for the error message in stderr
await new Promise(resolve => setTimeout(resolve, 1000));

34-44: LGTM: Good use of stdout assertions.

Unlike the other tests, this one validates stdout contains the expected message, which provides stronger verification of the behavior.

src/bun.js/api/Timer/MissingModulePollTimer.zig (4)

39-59: LGTM: startPolling correctly manages state and memory.

The function properly:

  • Stops existing polling before starting new (lines 41-43)
  • Frees old path and dupes new one (lines 46-49)
  • Resets interval to minimum (line 52)
  • Schedules first poll (line 56)

106-114: Exponential backoff is correctly implemented.

The interval doubling with max cap ensures responsive polling for quickly-created files while avoiding excessive CPU usage for slow operations.


127-138: LGTM: deinit properly cleans up resources.

The function removes the active timer and frees the allocated path, ensuring no leaks.


98-103: No changes needed: initEmpty(undefined) is correct.
The reload method’s Task parameter is unused (named _), so supplying an empty Task via undefined is intentional.

Comment on lines +36 to +50
const timeout = setTimeout(() => {
proc.kill();
}, 5000);

try {
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

clearTimeout(timeout);

console.log("STDOUT:", stdout);
console.log("STDERR:", stderr);
console.log("Exit code:", exitCode);

// The test passes if the process exits with code 0
expect(exitCode).toBe(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider validating the fallback timeout behavior.

The test captures stdout/stderr but only logs them without assertions. While the current assertion on exitCode is sufficient for the happy path, consider:

  1. Adding assertions on stdout/stderr to validate that the error was caught and the polling message appeared.
  2. Testing the fallback case where the file is never created (though this would require a separate test with a shorter timeout).

Example:

     const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

     clearTimeout(timeout);

-    console.log("STDOUT:", stdout);
-    console.log("STDERR:", stderr);
-    console.log("Exit code:", exitCode);
+    // Validate that watch mode detected the error and started polling
+    expect(stderr).toContain("Waiting for module to exist:");

     // The test passes if the process exits with code 0
     expect(exitCode).toBe(0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const timeout = setTimeout(() => {
proc.kill();
}, 5000);
try {
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
clearTimeout(timeout);
console.log("STDOUT:", stdout);
console.log("STDERR:", stderr);
console.log("Exit code:", exitCode);
// The test passes if the process exits with code 0
expect(exitCode).toBe(0);
const timeout = setTimeout(() => {
proc.kill();
}, 5000);
try {
const [stdout, stderr, exitCode] = await Promise.all([
proc.stdout.text(),
proc.stderr.text(),
proc.exited,
]);
clearTimeout(timeout);
// Validate that watch mode detected the error and started polling
expect(stderr).toContain("Waiting for module to exist:");
// The test passes if the process exits with code 0
expect(exitCode).toBe(0);
🤖 Prompt for AI Agents
In test/cli/watch/watch-missing-module-simple.test.ts around lines 36 to 50, the
test only logs stdout/stderr and asserts exitCode; add assertions that stdout
and/or stderr contain the expected polling/fallback messages (e.g., the error
caught and the "waiting" or "polling" text) to validate normal fallback
behavior, and add a separate short-timeout test case that simulates the file
never being created to assert the fallback timeout path (assert that the polling
message repeats or that a specific timeout message appears) — implement these
assertions and the additional short timeout test to ensure both happy and
fallback paths are validated.

Comment on lines +4 to +50
test("watch mode should poll for missing modules", async () => {
using dir = tempDir("watch-missing-module", {
"entry.js": `
try {
require("./missing.js");
} catch (err) {
// Write the missing file
const fs = require("fs");
const path = require("path");
fs.writeFileSync(path.join(__dirname, "missing.js"), "process.exit(0);");
// Re-throw so watch mode sees the error and starts polling
throw err;
}
// If we get here without the file existing, something went wrong
setTimeout(() => {
process.exit(1);
}, 5000);
`,
});

const proc = Bun.spawn({
cmd: [bunExe(), "--watch", "entry.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const timeout = setTimeout(() => {
proc.kill();
}, 10000);

try {
const exitCode = await proc.exited;
clearTimeout(timeout);

// The test passes if the process exits with code 0
// This means the missing file was created and executed successfully
expect(exitCode).toBe(0);
} catch (err) {
clearTimeout(timeout);
proc.kill();
throw err;
}
}, 15000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider consolidating test structure or adding more assertions.

These tests cover both CommonJS and ES module scenarios, which is valuable. However:

  1. Similar to watch-missing-module-simple.test.ts, consider adding assertions on stdout/stderr to validate the polling messages.
  2. The two tests have very similar structure - consider using test.each to reduce duplication.

Example using test.each:

test.each([
  { name: "require", ext: "js", code: `require("./missing.js")` },
  { name: "import", ext: "mjs", code: `await import("./missing.mjs")` }
])("watch mode should poll for missing modules with $name", async ({ ext, code }) => {
  // shared test logic
});

Based on learnings

Also applies to: 52-99

Copy link
Contributor

coderabbitai bot commented Oct 13, 2025

Walkthrough

Adds a MissingModulePollTimer and integrates it into the event loop. VirtualMachine invokes watch-mode missing-module handling on uncaught exceptions and pending promise rejections. Timer API gains a new timer field and public import. New timer implementation polls for file appearance and triggers reload. Tests cover watch-mode behavior for missing modules.

Changes

Cohort / File(s) Summary of changes
VirtualMachine watch-mode handling
src/bun.js/VirtualMachine.zig
Added helper to detect module-not-found in watch mode and start polling. Invoked this path in uncaughtException and pending internal promise rejection flows.
Timer API wiring
src/bun.js/api/Timer.zig
Added missing_module_poll_timer field to All and exposed MissingModulePollTimer via public import.
Event loop dispatch integration
src/bun.js/api/Timer/EventLoopTimer.zig
Added MissingModulePollTimer to Tag, updated Tag→type mapping, and dispatch in fire to call onTimeout for the new timer.
MissingModulePollTimer implementation
src/bun.js/api/Timer/MissingModulePollTimer.zig
New timer with exponential backoff polling for missing module paths; APIs: init, startPolling, stopPolling, deinit; schedules polls and triggers VM reload upon file detection.
CLI watch tests
test/cli/watch/watch-missing-external.test.ts, test/cli/watch/watch-missing-module-simple.test.ts, test/cli/watch/watch-missing-module.test.ts
Added tests validating watch mode detects missing modules, polls until creation, and reloads successfully for CJS/ESM and external creation scenarios.

Possibly related PRs

  • Unify timer enum #23228: Also modifies src/bun.js/api/Timer/EventLoopTimer.zig Tag enum and Tag→type mapping, overlapping with this PR’s timer dispatch changes.

Suggested reviewers

  • nektro
  • dylan-conway

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The description provides a detailed summary, changes list, integration points, and test plan, but it does not use the required template headings “What does this PR do?” and “How did you verify your code works?”, making it non-compliant with the repository’s description template. Update the PR description to include the exact template sections “### What does this PR do?” with a concise overview and “### How did you verify your code works?” with the test plan and verification steps to align with the repository’s required format.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title “Add missing module polling in watch mode” accurately and concisely captures the core change of introducing polling for absent modules in watch mode without extraneous detail, making it clear to reviewers what the PR implements.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/bun.js/VirtualMachine.zig (2)

766-786: Stop poller on reload to avoid stale timers

Before reloading, stop any active missing-module polling to prevent leftover timers from firing post-reload.

 pub fn reload(this: *VirtualMachine, _: *HotReloader.Task) void {
+    // Ensure missing-module polling is stopped before reload
+    this.timer.missing_module_poll_timer.stopPolling(this);

1983-1996: Deinit MissingModulePollTimer to free memory

Call deinit() during VM teardown to release duplicated path memory and cancel timers.

 pub fn deinit(this: *VirtualMachine) void {
     this.auto_killer.deinit();
+    // Cleanup missing-module poller
+    this.timer.missing_module_poll_timer.deinit(this);
 
     if (source_code_printer) |print| {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 72900ec and 054e5b2.

📒 Files selected for processing (7)
  • src/bun.js/VirtualMachine.zig (3 hunks)
  • src/bun.js/api/Timer.zig (2 hunks)
  • src/bun.js/api/Timer/EventLoopTimer.zig (3 hunks)
  • src/bun.js/api/Timer/MissingModulePollTimer.zig (1 hunks)
  • test/cli/watch/watch-missing-external.test.ts (1 hunks)
  • test/cli/watch/watch-missing-module-simple.test.ts (1 hunks)
  • test/cli/watch/watch-missing-module.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (11)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
test/cli/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/cli/**/*.{js,ts}: Place CLI command tests (e.g., bun install, bun init) under test/cli/
When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must live under test/ and end with .test.ts or .test.tsx
In tests, always use port: 0; do not hardcode ports or roll your own random port
Prefer normalizeBunSnapshot for snapshotting test output instead of asserting raw strings
Do not write tests that assert absence of crashes (e.g., 'no panic' or 'no uncaught exception')
Use Bun’s Jest-compatible runner (import { test, expect } from "bun:test") for tests
Avoid shell commands like find or grep in tests; use Bun’s Glob and built-in tools instead
Prefer running tests via bun bd test and use provided harness utilities (bunEnv, bunExe, tempDir)
Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests

Files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
test/cli/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place CLI command tests under test/cli/

Files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun:test for files ending with *.test.{ts,js,jsx,tsx,mjs,cjs}
Prefer concurrent tests (test.concurrent/describe.concurrent) over sequential when feasible
Organize tests with describe blocks to group related tests
Use utilities like describe.each, toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach) and track resources for cleanup

Files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
test/**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

For large/repetitive strings, use Buffer.alloc(count, fill).toString() instead of "A".repeat(count)

Files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/bun.js/api/Timer.zig
  • src/bun.js/api/Timer/MissingModulePollTimer.zig
  • src/bun.js/VirtualMachine.zig
  • src/bun.js/api/Timer/EventLoopTimer.zig
src/{**/js_*.zig,bun.js/api/**/*.zig}

📄 CodeRabbit inference engine (.cursor/rules/registering-bun-modules.mdc)

Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Files:

  • src/bun.js/api/Timer.zig
  • src/bun.js/api/Timer/MissingModulePollTimer.zig
  • src/bun.js/api/Timer/EventLoopTimer.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/api/Timer.zig
  • src/bun.js/api/Timer/MissingModulePollTimer.zig
  • src/bun.js/VirtualMachine.zig
  • src/bun.js/api/Timer/EventLoopTimer.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/bun.js/api/Timer.zig
  • src/bun.js/api/Timer/MissingModulePollTimer.zig
  • src/bun.js/VirtualMachine.zig
  • src/bun.js/api/Timer/EventLoopTimer.zig
🧠 Learnings (14)
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests

Applied to files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/cli/**/*.{js,ts} : When testing Bun as a CLI, use spawn with bunExe() and bunEnv from harness, and capture stdout/stderr via pipes

Applied to files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending with `*.test.{ts,js,jsx,tsx,mjs,cjs}`

Applied to files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/js/bun/**/*.{js,ts} : Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Applied to files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Prefer running tests via bun bd test <file> and use provided harness utilities (bunEnv, bunExe, tempDir)

Applied to files:

  • test/cli/watch/watch-missing-external.test.ts
  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : Do not set explicit test timeouts; Bun already has timeouts

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
📚 Learning: 2025-10-04T09:51:30.294Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-04T09:51:30.294Z
Learning: Applies to test/**/*.test.{ts,tsx} : Use Bun’s Jest-compatible runner (import { test, expect } from "bun:test") for tests

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/js/{bun,node}/** : Organize unit tests by module under `/test/js/bun/` and `/test/js/node/`

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
📚 Learning: 2025-10-08T13:48:02.430Z
Learnt from: Jarred-Sumner
PR: oven-sh/bun#23373
File: test/js/bun/tarball/extract.test.ts:107-111
Timestamp: 2025-10-08T13:48:02.430Z
Learning: In Bun's test runner, use `expect(async () => { await ... }).toThrow()` to assert async rejections. Unlike Jest/Vitest, Bun does not require `await expect(...).rejects.toThrow()` - the async function wrapper with `.toThrow()` is the correct pattern for async error assertions in Bun tests.

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : Always check exit codes and error scenarios in tests (e.g., spawned processes should assert non-zero on failure)

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js,test/**/*-fixture.ts} : Use `using`/`await using` for resource cleanup with Bun APIs (e.g., `Bun.spawn`, `Bun.listen`, `Bun.serve`)

Applied to files:

  • test/cli/watch/watch-missing-module-simple.test.ts
  • test/cli/watch/watch-missing-module.test.ts
📚 Learning: 2025-10-12T02:22:34.349Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.349Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : Import common utilities from `harness` (e.g., `bunExe`, `bunEnv`, `tempDirWithFiles`, platform helpers, GC helpers)

Applied to files:

  • test/cli/watch/watch-missing-module.test.ts
🧬 Code graph analysis (3)
test/cli/watch/watch-missing-external.test.ts (1)
test/harness.ts (2)
  • tempDir (277-284)
  • bunExe (102-105)
test/cli/watch/watch-missing-module-simple.test.ts (1)
test/harness.ts (2)
  • tempDir (277-284)
  • bunExe (102-105)
test/cli/watch/watch-missing-module.test.ts (1)
test/harness.ts (2)
  • tempDir (277-284)
  • bunExe (102-105)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (4)
src/bun.js/api/Timer.zig (1)

608-609: Public import looks good

Import placement and naming are consistent.

src/bun.js/api/Timer/EventLoopTimer.zig (1)

72-99: Timer tag and dispatch integration LGTM

Enum, Type mapping, and fire() path correctly wire MissingModulePollTimer.

Also applies to: 193-197

src/bun.js/api/Timer/MissingModulePollTimer.zig (1)

117-124: Path resolution note

checkFileExists() uses cwd-relative stat. Ensure the VM passes absolute or cwd-relative paths only; otherwise nested module imports may never be detected. The guard suggested in VirtualMachine.tryHandleMissingModuleInWatchMode helps. Please verify nested require/import cases.

src/bun.js/VirtualMachine.zig (1)

647-691: Harden specifier handling before polling

Strip any “?…” suffix and only poll file-like specifiers. For example:

const slice = specifier.toUTF8(default_allocator);
defer slice.deinit();
var spec = slice.slice();
// Strip query
if (std.mem.indexOf(u8, spec, '?')) |i| spec = spec[0..i];
// Only poll absolute or relative paths
if (!std.fs.path.isAbsolute(spec) and !(spec.len > 0 and (spec[0] == '.' or spec[0] == '/'))) return;
// Start polling
this.timer.missing_module_poll_timer.startPolling(this, spec) catch {};

Verify that missing_module_poll_timer is properly stopped in VirtualMachine.deinit (or on hot-reload).

Comment on lines +55 to +57
/// Polls for missing modules in watch mode
missing_module_poll_timer: MissingModulePollTimer = MissingModulePollTimer.init(),

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add lifecycle cleanup for MissingModulePollTimer

This timer duplicates strings; without calling its deinit() on VM teardown, memory can leak. Ensure VM.deinit invokes vm.timer.missing_module_poll_timer.deinit(vm).


🏁 Script executed:

#!/bin/bash
set -e
rg -n "MissingModulePollTimer" -n src/bun.js/api/Timer.zig
rg -n "fn deinit" -n src/bun.js/api/Timer.zig

Length of output: 175


🏁 Script executed:

#!/bin/bash
set -e
echo "=== Timer.zig (first 200 lines) ==="
sed -n '1,200p' src/bun.js/api/Timer.zig
echo "=== MissingModulePollTimer.zig (first 200 lines) ==="
sed -n '1,200p' src/bun.js/api/Timer/MissingModulePollTimer.zig
echo "=== Search for deinit in MissingModulePollTimer.zig ==="
rg -n "deinit" -n src/bun.js/api/Timer/MissingModulePollTimer.zig || true

Length of output: 11786


🏁 Script executed:

#!/bin/bash
set -e
# Find VirtualMachine.zig
fd --extension zig VirtualMachine.zig
# Display first 200 lines of its file(s)
for f in $(fd --extension zig VirtualMachine.zig); do
  echo "=== $f ==="
  sed -n '1,200p' "$f"
done
# Search for deinit or finalize
rg -n "fn deinit" -n $(fd --extension zig VirtualMachine.zig) || true
rg -n "fn finalize" -n $(fd --extension zig VirtualMachine.zig) || true

Length of output: 7515


🏁 Script executed:

#!/bin/bash
set -e
echo "=== VirtualMachine.deinit (lines 1980-2100) ==="
sed -n '1980,2100p' src/bun.js/VirtualMachine.zig
echo "=== References to missing_module_poll_timer in VirtualMachine.zig ==="
rg -n "missing_module_poll_timer" src/bun.js/VirtualMachine.zig || echo "no references"

Length of output: 3986


Invoke MissingModulePollTimer.deinit in VM teardown

Add a call to this.timer.missing_module_poll_timer.deinit(this); inside VirtualMachine.deinit to free the duplicated path string and stop the active poll timer.

🤖 Prompt for AI Agents
In src/bun.js/api/Timer.zig around lines 55-57, the MissingModulePollTimer
created at missing_module_poll_timer: MissingModulePollTimer =
MissingModulePollTimer.init() allocates a duplicated path string and starts an
active poll timer that isn't being cleaned up; add a call to
this.timer.missing_module_poll_timer.deinit(this); inside the
VirtualMachine.deinit method so the duplicated path is freed and the poll timer
is stopped as part of VM teardown.

Comment on lines +61 to +74
/// Stop polling for the missing module
pub fn stopPolling(this: *MissingModulePollTimer, vm: *VirtualMachine) void {
if (!this.is_polling) return;

if (this.event_loop_timer.state == .ACTIVE) {
vm.timer.remove(&this.event_loop_timer);
}

this.is_polling = false;
this.current_interval_ms = this.min_interval_ms;

log("Stopped polling for missing module: {s}", .{this.missing_path});
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Free duplicated path on stop to avoid leaks

missing_path is duped in startPolling but never freed on stopPolling(). Free it and reset to empty.

 pub fn stopPolling(this: *MissingModulePollTimer, vm: *VirtualMachine) void {
     if (!this.is_polling) return;
 
     if (this.event_loop_timer.state == .ACTIVE) {
         vm.timer.remove(&this.event_loop_timer);
     }
 
     this.is_polling = false;
     this.current_interval_ms = this.min_interval_ms;
 
-    log("Stopped polling for missing module: {s}", .{this.missing_path});
+    log("Stopped polling for missing module: {s}", .{this.missing_path});
+    if (this.missing_path.len > 0) {
+        bun.default_allocator.free(this.missing_path);
+        this.missing_path = &.{};
+    }
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/bun.js/api/Timer/MissingModulePollTimer.zig around lines 61-74,
stopPolling currently never frees the duplicated missing_path from startPolling;
call the same allocator that was used to dup missing_path to free that buffer
(guarding against empty/null), then reset missing_path to an empty value (e.g.,
null/empty string) to avoid a dangling pointer and prevent future double-free;
perform the free before clearing is_polling and log after cleanup.

Comment on lines +81 to +115
/// Timer callback that checks if the missing file exists
pub fn onTimeout(this: *MissingModulePollTimer, vm: *VirtualMachine) jsc.API.Timer.EventLoopTimer.Arm {
this.event_loop_timer.state = .FIRED;

if (!this.is_polling) {
return .disarm;
}

// Check if the file exists
const file_exists = this.checkFileExists();

if (file_exists) {
log("Missing module found: {s}. Triggering reload.", .{this.missing_path});

// Stop polling
this.is_polling = false;

// Trigger a hot reload by calling reload directly
const HotReloader = jsc.hot_reloader.HotReloader;
var task = HotReloader.Task.initEmpty(undefined);
vm.reload(&task);

return .disarm;
}

// File still doesn't exist, increase interval with exponential backoff
this.current_interval_ms = @min(this.current_interval_ms * 2, this.max_interval_ms);

log("Missing module not found yet: {s}. Next poll in {}ms", .{ this.missing_path, this.current_interval_ms });

// Schedule next poll
this.scheduleNextPoll(vm);

return .disarm;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Free duplicated path when file is found

When the module appears, release missing_path to prevent accumulation across reload cycles.

     if (file_exists) {
         log("Missing module found: {s}. Triggering reload.", .{this.missing_path});
 
         // Stop polling
         this.is_polling = false;
+        if (this.missing_path.len > 0) {
+            bun.default_allocator.free(this.missing_path);
+            this.missing_path = &.{};
+        }
 
         // Trigger a hot reload by calling reload directly
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Timer callback that checks if the missing file exists
pub fn onTimeout(this: *MissingModulePollTimer, vm: *VirtualMachine) jsc.API.Timer.EventLoopTimer.Arm {
this.event_loop_timer.state = .FIRED;
if (!this.is_polling) {
return .disarm;
}
// Check if the file exists
const file_exists = this.checkFileExists();
if (file_exists) {
log("Missing module found: {s}. Triggering reload.", .{this.missing_path});
// Stop polling
this.is_polling = false;
// Trigger a hot reload by calling reload directly
const HotReloader = jsc.hot_reloader.HotReloader;
var task = HotReloader.Task.initEmpty(undefined);
vm.reload(&task);
return .disarm;
}
// File still doesn't exist, increase interval with exponential backoff
this.current_interval_ms = @min(this.current_interval_ms * 2, this.max_interval_ms);
log("Missing module not found yet: {s}. Next poll in {}ms", .{ this.missing_path, this.current_interval_ms });
// Schedule next poll
this.scheduleNextPoll(vm);
return .disarm;
}
/// Timer callback that checks if the missing file exists
pub fn onTimeout(this: *MissingModulePollTimer, vm: *VirtualMachine) jsc.API.Timer.EventLoopTimer.Arm {
this.event_loop_timer.state = .FIRED;
if (!this.is_polling) {
return .disarm;
}
// Check if the file exists
const file_exists = this.checkFileExists();
if (file_exists) {
log("Missing module found: {s}. Triggering reload.", .{this.missing_path});
// Stop polling
this.is_polling = false;
if (this.missing_path.len > 0) {
bun.default_allocator.free(this.missing_path);
this.missing_path = &.{};
}
// Trigger a hot reload by calling reload directly
const HotReloader = jsc.hot_reloader.HotReloader;
var task = HotReloader.Task.initEmpty(undefined);
vm.reload(&task);
return .disarm;
}
// File still doesn't exist, increase interval with exponential backoff
this.current_interval_ms = @min(this.current_interval_ms * 2, this.max_interval_ms);
log("Missing module not found yet: {s}. Next poll in {}ms", .{ this.missing_path, this.current_interval_ms });
// Schedule next poll
this.scheduleNextPoll(vm);
return .disarm;
}
🤖 Prompt for AI Agents
In src/bun.js/api/Timer/MissingModulePollTimer.zig around lines 81 to 115, when
the missing file is found the duplicated path stored in this.missing_path is
never released; free the allocated memory for this.missing_path using the
appropriate allocator/free routine (e.g., call the timer/VM allocator's free
function) before or immediately after setting this.is_polling = false, then set
this.missing_path to null (or an empty/zero value) to avoid double-free on
subsequent reload cycles.

Comment on lines +38 to +41
console.log("STDOUT:", stdout);
console.log("STDERR:", stderr);
console.log("Exit code:", exitCode);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove debug logging in tests

Drop console logs to reduce CI noise; keep assertions only.

🤖 Prompt for AI Agents
In test/cli/watch/watch-missing-external.test.ts around lines 38 to 41, remove
the three console.log debug statements (console.log("STDOUT:", stdout);
console.log("STDERR:", stderr); console.log("Exit code:", exitCode);) so the
test does not emit noisy output in CI; leave the existing assertions intact and
only assert against stdout/stderr/exitCode as needed.

proc.kill();
throw err;
}
}, 10000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Avoid explicit test timeout

Let the runner manage timeouts; remove the 10000 override.

-}, 10000);
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}, 10000);
});
🤖 Prompt for AI Agents
In test/cli/watch/watch-missing-external.test.ts around line 50, the test has an
explicit timeout override "}, 10000);" which should be removed so the test
runner manages timeouts; delete the trailing numeric timeout argument from the
test closure (remove ", 10000") leaving the test definition without an explicit
timeout.

Comment on lines +45 to +47
console.log("STDOUT:", stdout);
console.log("STDERR:", stderr);
console.log("Exit code:", exitCode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove debug logging in tests

These prints are unnecessary for assertions and bloat output.

🤖 Prompt for AI Agents
In test/cli/watch/watch-missing-module-simple.test.ts around lines 45 to 47, the
three console.log debug statements (printing STDOUT, STDERR, and Exit code)
should be removed because they are unnecessary for assertions and clutter test
output; delete these console.log calls and rely on existing assertions or add
focused assertions if visibility into outputs is needed.

proc.kill();
throw err;
}
}, 10000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Avoid explicit test timeout

Rely on default runner timeout.

-}, 10000);
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}, 10000);
});
🤖 Prompt for AI Agents
In test/cli/watch/watch-missing-module-simple.test.ts around line 56 there is an
explicit test timeout passed (", 10000"); remove the explicit timeout argument
from the test call so the test relies on the test runner's default timeout
(i.e., delete the ", 10000" token and keep the test function invocation
unchanged otherwise) and ensure no other hard-coded timeouts remain in this
file.

proc.kill();
throw err;
}
}, 15000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Avoid explicit test timeouts

Remove the 15000 overrides; rely on the runner defaults.

-}, 15000);
+});

Also applies to: 99-99

🤖 Prompt for AI Agents
In test/cli/watch/watch-missing-module.test.ts around lines 50 and 99, the
explicit Jest/Mocha test timeout overrides (e.g., ", 15000") should be removed
so tests rely on the runner's default timeout; delete the numeric timeout
arguments on those test() or it() calls and leave the function signature with
only the test callback so no explicit timeout is specified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant