Skip to content

bundler: keep import.meta.main in iife output for bun - #38139

Open
robobun wants to merge 1 commit into
mainfrom
farm/c37de9ba/import-meta-main-bun-iife
Open

robobun wants to merge 1 commit into
mainfrom
farm/c37de9ba/import-meta-main-bun-iife

Conversation

@robobun

@robobun robobun commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator

Related: #24540 (that report is the dynamic require() case, fixed by #38077; this PR is the import.meta.main case, which #38077 leaves out). Does not close it on its own.

Problem

  • An entry point that uses import.meta.main (or require.main === module, which the parser folds to the same node) bundled with bun build --target=bun --format=iife throws when the output is loaded: ReferenceError: __require is not defined today, ReferenceError: module is not defined once bundler: define __require in iife output #38077 defines __require for iife output.
  • The printer (ExprData::EImportMetaMain in src/js_printer/lib.rs:2993) keeps import.meta.main only for esm output and lowers it to <require>.main == module for every other format. That lowering assumes the output is loaded as a CommonJS script. Output for bun starts with a // @bun pragma (src/bundler/linker_context/postProcessJSChunk.rs:352), and Bun loads a // @bun file as an ES module unless the pragma also says @bun-cjs (cjs output only): import.meta.main works in that file and neither module nor require exist.
  • The same happens to an entry point whose first line is #!/usr/bin/env bun when the bundle is built for another target: src/bundler/ParseTask.rs:2403 switches that file to target bun and its chunk gets the pragma, but the printer was given the bundle's target (src/bundler/LinkerContext.rs:2222). With --target=node the esm output is __require.main == __require.module with __require never defined, because the parser (which used the file's target) did not record the use that the printer (which used the bundle's) relied on.

Fix

  • src/options_types/bundle_enums.rs: Format::keeps_import_meta_main(target) is the one place that says whether import.meta.main stays as written: esm output except for node (node has no import.meta.main), and iife output for bun. cjs and the dev server format are lowered as before.
  • src/js_printer/lib.rs decides with that predicate. The only output it changes is iife for bun, which now contains import.meta.main itself; esm, cjs and iife for browser/node print exactly what they printed before.
  • src/bundler/ParseTask.rs uses the same predicate to set the parser option that records the runtime __require use the lowering needs. The option is renamed from lower_import_meta_main_for_node_js to lower_import_meta_main because it is no longer node specific. Recording is a no-op unless the output imports the runtime's __require (auto_polyfill_require, esm only today), so on this branch the parser's output is unchanged; once bundler: define __require in iife output #38077 turns that on for iife, this keeps an iife for bun from carrying an unused var __require = import.meta.require; while browser/node iife get the __require they print. (bundler: define __require in iife output #38077 adds an || !output_format.is_esm() to the same if in p.rs; whichever lands second can drop that, since this option covers it.)
  • src/bundler/LinkerContext.rs passes the file's own ast.target to the printer instead of the bundle's. Inside the bundler the printer reads target only in the EImportMetaMain arm, so this changes nothing except for the hashbang case, where the file's target is the one the pragma (and the parser) already follow. The same function already passed ast.target as the positional target of print_with_writer (LinkerContext.rs:2276); the options struct was the one place still using the bundle's. The require.main == module lowering stays for cjs output for bun: that output is wrapped in a CommonJS function by the @bun-cjs pragma, where it is correct.
  • Verified with test/bundler/bundler_edgecase.test.ts: edgecase/ImportMetaMainIIFETargetBun (the report), edgecase/ImportMetaMainBunHashbangTargetNode and edgecase/ImportMetaMainIIFEBunHashbangTargetNode (the hashbang entry, esm and iife) check the printed expression, that the output contains no require/module at all, and run the bundle under bun expecting true true false false. All three fail on the released binary (they print __require.main == module / __require.main == __require.module). edgecase/ImportMetaMainCJSTargetBun pins the cjs boundary and passes before and after.
  • Also passing with the debug build: the rest of bundler_edgecase.test.ts (DeepImportDiamondDAG times out locally, a 20k-module stress test unrelated to this), bundler_cjs.test.ts, bundler_banner.test.ts (hashbang handling), minify/RequireMainToImportMetaMain, compile/ImportMetaMain, esbuild/default.test.ts -t "DefineImportMeta|IIFE", test/js/bun/resolve/import-meta.test.js and bake/dev/bundle.test.ts -t import.meta.main.
  • Not changed here: a CommonJS entry point in iife output is wrapped in __commonJS and never called (bundler: call the wrapped entry point in iife output #37843), and browser/node iife output still lacks the __require binding it prints (bundler: define __require in iife output #38077). With this change a bun-target iife prints import.meta.main inside such a wrapper as well, so it works once bundler: call the wrapped entry point in iife output #37843 calls it.

Background

  • import.meta.main is a Bun feature: true in the module the process was started with. Outside esm output the bundler lowers it to a require.main == module comparison, which only makes sense where the output is evaluated as a CommonJS module (so that require and module exist). Non-entry files are folded to false by the parser; --compile folds the entry point to true; only a plain bun build entry point reaches the printer.
  • // @bun pragma: the linker puts it at the top of every chunk built for bun. When Bun later runs such a file it skips transpiling it and loads it as is, as an ES module; // @bun @bun-cjs (cjs output) instead tells it the file is the CommonJS function wrapper that follows. This is why iife output for bun has to keep import.meta.main while cjs output for bun has to lower it.
  • Runtime __require: for output formats that import the bundler's runtime helper, references to a dynamic require (including this lowering) are printed as the runtime's __require symbol, and the part defining it is only kept in the output if some file recorded a use of it during parsing. The parser does not know the target, so ParseTask passes target-dependent decisions to it as options; lower_import_meta_main is such an option.
  • Hashbang target switch: ParseTask parses the first entry point with target bun when its first line is exactly #!/usr/bin/env bun, whatever --target says, and stores that on the file's AST; the pragma is emitted from the entry point's AST target.
Repro on the released binary
$ echo 'console.log(import.meta.main)' > entry.js
$ bun build entry.js --target=bun --format=iife --outfile=out.js && bun out.js
ReferenceError: __require is not defined
$ cat out.js
// @bun
(() => {
  console.log(__require.main == module);
})();

$ printf '#!/usr/bin/env bun\nconsole.log(import.meta.main)\n' > hb.js
$ bun build hb.js --target=node --outfile=hb.out.js && bun hb.out.js
ReferenceError: __require is not defined
$ cat hb.out.js
#!/usr/bin/env bun
// @bun
console.log(__require.main == __require.module);

Both print true with this change; the output is import.meta.main in each case. A // @bun file printing typeof module, typeof require gives undefined undefined.

Output for bun carries a `// @bun` pragma that makes bun load it as an ES
module in every format but cjs, so lowering an entry point's
`import.meta.main` to `__require.main == module` in an iife for bun
references bindings that do not exist there. Decide whether the lowering
applies with one (format, target) predicate shared by the parser (which
records the `__require` use the lowering needs) and the printer, and give
the printer the file's own target so an entry point whose
`#!/usr/bin/env bun` hashbang switched it to bun, and therefore got the
pragma, is printed the same way.
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d7104f2a-3c55-4e8c-a237-f8f5417a792b

📥 Commits

Reviewing files that changed from the base of the PR and between b7a0431 and ddfa91a.

📒 Files selected for processing (8)
  • src/bundler/LinkerContext.rs
  • src/bundler/ParseTask.rs
  • src/bundler/transpiler.rs
  • src/js_parser/p.rs
  • src/js_parser/parse/parse_entry.rs
  • src/js_printer/lib.rs
  • src/options_types/bundle_enums.rs
  • test/bundler/bundler_edgecase.test.ts

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

@robobun

robobun commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:00 AM PT - Aug 13th, 2026

❌ @robobun, your commit ddfa91a has 1 failures in Build #94507 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 38139

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

bun-38139 --bun

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on the released binary (bun build entry.js --target=bun --format=iife prints __require.main == module into a // @bun file, which throws on load; a #!/usr/bin/env bun entry built with --target=node prints __require.main == __require.module with __require undefined). Fix is in this PR; the three new edgecase/ImportMetaMain* tests in test/bundler/bundler_edgecase.test.ts fail on the released binary and pass with the debug build. Waiting on CI.

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. __require is not created when using 'iife' as output format #24540 - Reports --format=iife output referencing an undefined __require; this PR removes one source of that emission by keeping literal import.meta.main (and require.main === module) in iife output for --target=bun. Note this is a partial fix — the reporter's localforage repro is the dynamic-require half, covered by bundler: define __require in iife output #38077.

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #24540

🤖 Generated with Claude Code

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Not adding Fixes #24540: that report is the dynamic require() half of the iife __require problem, which #38077 fixes (and already references the issue). This PR only covers import.meta.main for the bun target, so the PR body links the issue as related instead of closing it.

@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.

I reviewed this PR and didn't find any bugs. The fix is well-reasoned and the tests are thorough, but since it changes the printer's per-file target source in LinkerContext (bundle target → ast.target) and adjusts codegen across output formats, a maintainer sign-off would be worthwhile.

Checked that printer.options.target is only read in the EImportMetaMain arm when bundling: true (the third read at lib.rs:7489 is gated on !bundling), so the ast.target swap is scoped to exactly this expression.
Verified !keeps_import_meta_main widens lower_import_meta_main to cjs/iife/bake-dev, but record_usage_of_runtime_require is a no-op there because auto_polyfill_require is esm-only today — parser output is unchanged on this branch.
Confirmed ast.target is set from topts.target for every non-hashbang file (ParseTask.rs:2662), and non-entry files inline import.meta.main before the printer, so only the entry point's target is observable.

Extended reasoning...

Overview

Fixes import.meta.main printing for iife-format output targeting bun, and for the entry-with-#!/usr/bin/env bun case when the bundle target is something else. Adds Format::keeps_import_meta_main(target) as the single source of truth, uses it in the printer's EImportMetaMain arm and in ParseTask where the parser option is set, renames lower_import_meta_main_for_node_js → lower_import_meta_main (5 mechanical sites), and switches LinkerContext's printer options from the bundle-wide target to the file's ast.target. Four new itBundled tests cover the reported iife+bun case, the hashbang esm/iife cases, and pin the cjs+bun boundary.

Security risks

None. Pure codegen decision for how a single AST node prints; no untrusted-input parsing, no allocation/lifetime changes, no FFI.

Level of scrutiny

Moderate-high. The bundler's linker and printer are hot, correctness-critical paths, and the self.options.target → ast.target swap in LinkerContext changes a per-bundle input to a per-file one. I verified the PR's claim that this only affects EImportMetaMain: the printer's other read of options.target (lib.rs:7489) is behind !printer.options.bundling, and LinkerContext sets bundling: true. print_with_writer's positional target argument (which drives IS_BUN_PLATFORM) was already ast.target before this PR, so the change actually removes an existing inconsistency between the two target sources rather than introducing one.

I also checked the widened lower_import_meta_main condition. It now fires for cjs, iife-non-bun, and bake-dev entry points (previously node-only), but record_usage_of_runtime_require is gated on auto_polyfill_require, which ParseTask sets only for esm output — so the parser produces identical output on this branch for every widened case. The predicate is exhaustive over Format, and the esm/node-esm/iife-browser/cjs behaviors all match the pre-PR truth table.

Other factors

Tests are strong: each new case both asserts the exact printed expression (capture), asserts absence of require/module in the output where they'd be undefined at runtime, checks the pragma prefix, and executes the bundle expecting true true false false. The cjs+bun test pins the unchanged boundary. The description lists which existing suites were re-run. Non-entry files can't reach this printer arm (their import.meta.main is inlined during visit), so per-file target divergence for non-entry files in the same chunk is not a concern. Given the scope touches the linker's per-file printer setup, deferring for a maintainer glance rather than auto-approving.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Confirmed the print_with_writer point: print_code_for_file_in_chunk_js already passed ast.target as the positional target (LinkerContext.rs:2276), so the options struct was the only remaining use of the bundle-wide target in that function. Added that to the description.

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