Skip to content

Start the HTML server when the entry of the node shim resolves to an HTML file - #43495

Open
robobun wants to merge 1 commit into
robobun/b560be00/node-shim-resolve-entryfrom
robobun/0bd17bac/node-shim-html-entry
Open

robobun wants to merge 1 commit into
robobun/b560be00/node-shim-resolve-entryfrom
robobun/0bd17bac/node-shim-html-entry

Conversation

@robobun

@robobun robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #43409. The base of this PR is that branch.

Problem

  • bun as node (the shim that bun run puts in PATH) prints nothing and exits 0 for node ./site when site/package.json has "main": "index.html". bun ./site and node ./site/index.html start the HTML dev server.
  • RunCommand::boot (src/runtime/cli/run_command.rs:1111) sets vm.main_is_html_entrypoint from the extension of the given path. /x/site has none, so index.html loads as a module and nothing runs.
  • The reverse also fails: a symlink page.html to script.js runs the script, then starts an HTML server that never exits.

Fix

  • Run::start sets the flag from the path that Make the entry point of the node shim the main module #43409 resolves, as bun <entry> does. If the resolve fails, the flag from boot stays, so node './*.html' still works.
  • For an HTML entry point, process.argv[1] is the resolved file, as for bun ./site. src/js/internal/html.ts finds its pages in process.argv by the .html suffix.
  • Verified: test/cli/run/as-node.test.ts (3 new tests, all fail on the base branch and on release 1.4.3-canary). Also test/js/bun/http/bun-serve-html-entry.test.ts.
  • Self-reviewed: 2 concerns raised, 1 addressed. Rejected: a new test that times out leaves its server running. The existing HTML server tests do the same.

Background

  • An HTML entry point does not load as a module. reload_entry_point runs internal/html.ts, which serves the pages with Bun.serve.
  • Loaded as a module, an HTML file gives an HTMLBundle object. That is correct for import page from "./index.html".
  • Node has no HTML entry point. v26.3.0 throws a SyntaxError here. The shim has served a direct .html path since Introduce bun ./index.html #16993.
Notes

Why the dev server and not an error

  • The shim already serves node ./site/index.html. If node ./site failed, the spelling of the path would decide the result, not the file that runs.
  • bun ./site starts the server. exec_with_cfg resolves first and passes the loader of the resolved path to boot.
  • An error needs the same resolve, and the direct .html path would stay a special case.
  • The review of Make the entry point of the node shim the main module #43409 found this case and proposed the same remedy: take the flag from the resolved entry.

Node v26.3.0, the base (#43409), and this PR (Linux x64, cwd /x)

command Node base this PR
node ./site ("main": "index.html") SyntaxError, exit 1 no output, exit 0 HTML server
node ./bin/link (symlink to index.html) SyntaxError, exit 1 no output, exit 0 HTML server
node ./site/index.html SyntaxError, exit 1 HTML server HTML server
node './site/*.html' MODULE_NOT_FOUND, exit 1 HTML server HTML server
node ./page.html (symlink to script.js) runs the script, argv[1] is /x/page.html, exit 0 runs the script, then an HTML server that never exits same as Node
node ./missing.html MODULE_NOT_FOUND, exit 1 error: Cannot find module, exit 1 same as the base

The released binary 1.4.3-canary.1+367d939d9 (main, without #43409) gives the same results as the base column.

What else I ran by hand (debug build)

  • node --hot ./site and node --watch ./site start the server.
  • node --require ./pre.js ./site and bun --preload ./pre.js ./site print the same process.argv[1] and Bun.main from the preload: /x/site/index.html.

Tests

  • New in test/cli/run/as-node.test.ts: an HTML file behind a directory with a package.json main, an HTML file behind a symlink, and a script behind a symlink named .html.
  • The new tests set argv0: "node" and do not go through bun --bun node. Then the process that the test stops is the HTML server, with no wrapper process between them. They do not touch the directory of the node shim, so they are concurrent.
  • Linux x64 debug build: as-node.test.ts 28 pass. With the src/ of the base branch: the 3 new tests fail. Released binary: the 3 new tests fail.
  • Also run on the debug build: test/js/bun/http/bun-serve-html-entry.test.ts (7 pass), test/cli/run/env.test.ts -t "node shim" (4 pass), test/cli/run/run-extensionless.test.ts.

Self-review

  • No reader of main_is_html_entrypoint runs between boot and the new assignment. Its readers are reload_entry_point and Bun__VM__entryRootKey.
  • process.argv is built on first access. Nothing reads it before reload_entry_point sets vm.main(), so internal/html.ts always sees the resolved path.
  • A --hot reload passes vm.main(), which is the resolved path. Cron mode and -e set eval_source, so they skip the block.
  • Addressed: the comment above the describe said that none of its tests is concurrent. It now names the tests that spawn bun --bun.
  • Rejected: bun:test stops the processes of a test that times out only when the test runs alone. A new test that hangs leaves its HTML server running. The tests in bun-serve-html-entry.test.ts are concurrent in the same way, and a hang is a failure in any case.
  • Known: node ./node_modules/pkg with an HTML main now fails with No HTML files found matching ... and exit 1, as bun ./node_modules/pkg does. internal/html.ts skips a path under node_modules. Before, the shim printed nothing and exited 0.

--hot after a delete (Linux x64, debug build of this PR, 3 saves of site/index.html)

Not in this PR

… runs

`node ./site` with a package.json "main" of `index.html` printed nothing
and exited 0. `boot` read the flag from the extension of the given path,
so the module loader imported the HTML file as a module.

`Run::start` now sets the flag from the resolved path. An HTML entry point
does not keep the given path in `process.argv[1]`, because
`internal/html.ts` reads its entry from there. A script behind a symlink
named `.html` now runs as a script.
@robobun

robobun commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Status: fix pushed (d56c366b9f), waiting for CI. PR: #43495 (stacked on #43409)

How I reproduced it (Linux x64, bun 1.4.3-canary.1+367d939d9, Node v26.3.0 for comparison):

mkdir -p /tmp/html/site /tmp/shim && cd /tmp/html
echo '{"main":"index.html"}' > site/package.json
echo '<html><body><script>console.log(1)</script></body></html>' > site/index.html
ln -sf "$(which bun)" /tmp/shim/node      # bun as node

/tmp/shim/node ./site ; echo "exit=$?"    # no output, exit=0 (before this PR)
bun ./site                                # Bun v1.4.3 dev server ready ... url: http://localhost:3000/
/tmp/shim/node ./site/index.html          # starts the dev server too
node ./site                               # Node: SyntaxError: Unexpected token '<', exit 1

The reverse case: ln -s script.js page.html, then /tmp/shim/node ./page.html runs the script and then starts an HTML server that never exits. Node and bun ./page.html run the script and exit 0.

bun bd test test/cli/run/as-node.test.ts fails the 3 new tests with the src/ of the base branch (#43409) and passes with this PR (28 tests). USE_SYSTEM_BUN=1 bun test test/cli/run/as-node.test.ts -t "HTML|html" fails all 3 on the released binary.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/runtime/cli/run_command.rs — Users running node --watch ./site or node --hot ./site with an HTML main get a file watcher whose main file is the directory /x/site, not the index.html that now runs. The reloader is enabled at src/runtime/cli/run_command.rs:1437 and :1447 with the unresolved entry, before resolve_entry_path at :1469 replaces it. MainFile::init (hot_reloader.rs:410) then hashes the directory and its parent; the rename/replace detection at hot_reloader.rs:1068 keys on that basename, so an editor that atomically replaces index.html may not trigger a restart. Fix: enable the reloader after the entry is resolved, or re-point MainFile at the resolved path once resolution succeeds, for both the Hot and Watch arms.

    Extended reasoning...

    Before this PR node --watch ./site with an HTML main did nothing useful (the base loads index.html as a module and exits), so the mismatch had no visible consequence. After this PR the same command starts the HTML dev server, so the watcher's behaviour is now user-visible for the first time. Steps: exec_as_if_node normalizes ./site to /x/site (run_command.rs:3036). Run::start enables the WatchReloader with Some(entry) = /x/site at :1447. MainFile::init stores file = /x/site, dir = /x/, hashes both. Only then does :1469 resolve to /x/site/index.html and :1472 mark it HTML. The module loader adds /x/site/index.html to the watch list when html.ts imports it, so in-place edits do reload, but the special main-file path at hot_reloader.rs:960-986 and :1068-1093 (which handles the main file being deleted and recreated by an atomic-save editor, and the kqueue directory fallback) is bound to /x/site, whose basename never changes. bun --watch ./site does not have this gap because exec_with_cfg resolves before boot and passes the HTML file. The PR only verified by hand that the…

    Verification: pre-existing — the mismatch the candidate describes is real, but the base branch (d62e10a, the #43409 ordering) already installs the same wrong MainFile for the same command and for every other EntryPath::Unresolved entry that resolves elsewhere (node --watch ./site with "main": "index.js", node --watch ./bin/link), where it is already user-visible; this PR does not touch the…

@robobun

robobun commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

MainFile under node --watch ./site and node --hot ./site

Confirmed in part. I ran it on Linux x64 with a debug build of this PR, 3 saves of site/index.html per row. A restart or a reload prints the server URL again.

command rename a new file over index.html delete index.html, then create it
bun --watch ./site 3 of 3 0 of 3 (the restart prints Module not found "./site")
node --watch ./site 3 of 3 2 of 3
bun --hot ./site 3 of 3 3 of 3
node --hot ./site 3 of 3 0 of 3
node --hot ./site/index.html not run 3 of 3

I do not change the reloader in this PR. The fix belongs with the resolve on #43409: the reloader has to get the resolved path, and the watcher thread reads MainFile, so it is more than an assignment after the resolve. When #43409 has the fix, this PR gets it with a rebase. The Notes of this PR now list the gap.

@robobun

robobun commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:25 PM PT - Sep 19th, 2026

✅ @robobun, your commit d56c366b9f30e01fb5a2afcc678abaac07ad6b70 passed in Build #118402! 🎉


🧪   To try this PR locally:

bunx bun-pr 43495

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

bun-43495 --bun

This branch has not been deployed

No deployments
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