Skip to content

bundler: fold import.meta.filename for cjs output; emit direct-eval build note - #35961

Open
robobun wants to merge 4 commits into
mainfrom
farm/9dadacfe/bundler-lowering-strict-and-filename
Open

robobun wants to merge 4 commits into
mainfrom
farm/9dadacfe/bundler-lowering-strict-and-filename

Conversation

@robobun

@robobun robobun commented Jul 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

$ printf 'console.log(import.meta.filename);\nconsole.log(import.meta.dirname);\n' > f.js
$ bun build --format=cjs --target=node f.js
// f.js
console.log(import.meta.filename);
console.log("/tmp/repro");
$ node -e "$(bun build --format=cjs --target=node f.js)"
SyntaxError: Cannot use 'import.meta' outside a module

The cjs/Bake fold pass in fold.rs already inlines import.meta.{dir,dirname,file,path,url} for --format=cjs, but filename (Node's __filename equivalent, same value as import.meta.path) was missing from the set, so the raw import.meta expression reached the output and Node rejected it.

Fix

  • fold.rs: treat import.meta.filename the same as import.meta.path in the cjs/Bake inlining branch.
  • visit_expr.rs: emit esbuild's debug-level note for direct eval() when bundling (replaces the TODO at that site).

Verification

edgecase/ImportMetaFilenameCjs in test/bundler/bundler_edgecase.test.ts asserts no import.meta remains in cjs output and that the result runs under node. It fails on the released binary and passes with this change. bundler_edgecase.test.ts, bundler_cjs.test.ts, bundler_minify.test.ts, transpiler/transpiler.test.js, transpiler/runtime-transpiler.test.ts and esbuild/default.test.ts -t DirectEval continue to pass.

Related


no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/bundler_edgecase.test.ts

…me for cjs

- parse_stmt: call mark_strict_mode_feature(WithStatement) in t_with so
  bundling a with-statement to --format=esm errors instead of emitting
  strict-mode-invalid output, and "use strict"; with(x){} is rejected.
  Sloppy cjs output and bun run of .cjs are unchanged.
- fold: include import.meta.filename in the cjs inlining set (same value
  as import.meta.path); previously emitted verbatim, which is a syntax
  error under Node.js CommonJS.
- visit_expr: emit esbuild's direct-eval debug note when bundling.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The bundler now inlines import.meta.filename like import.meta.path in CommonJS output, with a regression test covering generated code and runtime values. Bundled direct eval calls also emit a debug log note.

Import Meta CJS Handling

Layer / File(s) Summary
Import meta path inlining and validation
src/js_parser/fold.rs, test/bundler/bundler_edgecase.test.ts
import.meta.filename and import.meta.path inline to the full source path, and the CommonJS test verifies output and runtime behavior.

Bundled Eval Diagnostics

Layer / File(s) Summary
Direct eval debug logging
src/js_parser/visit/visit_expr.rs
Bundled direct eval handling records a debug message for the identifier range.

Possibly related PRs

  • oven-sh/bun#35967: Updates related path-like property inlining in the same parser logic.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the two main changes: import.meta.filename folding for CJS and a direct-eval bundling note.
Description check ✅ Passed The description covers what changed and how it was verified, though it does not use the repository's exact template headings.

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

@robobun

robobun commented Jul 26, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:53 PM PT - Jul 26th, 2026

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

  • 📦 Binary size — 12 over 0.50 MB
  • targetthis build canary: main #79916
    sizeΔ
    ❌ bun-darwin-aarch6458.13 MB57.58 MB+564.9 KB
    ❌ bun-darwin-x6463.48 MB62.95 MB+544.5 KB
    ❌ bun-linux-aarch6470.98 MB70.42 MB+576.0 KB
    ❌ bun-linux-x6472.47 MB71.95 MB+528.0 KB
    ❌ bun-linux-aarch64-musl64.88 MB64.32 MB+576.0 KB
    ❌ bun-linux-x64-musl66.98 MB66.45 MB+544.0 KB
    ❌ bun-linux-aarch64-android78.47 MB77.97 MB+512.0 KB
    ❌ bun-linux-x64-android80.62 MB80.10 MB+529.2 KB
    ❌ bun-freebsd-x6483.07 MB82.56 MB+528.0 KB
    ❌ bun-freebsd-aarch6484.84 MB84.31 MB+544.0 KB
    ❌ bun-windows-x6480.26 MB79.70 MB+570.5 KB
    ❌ bun-windows-aarch6470.86 MB70.34 MB+533.0 KB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 35961

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

bun-35961 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Bundling target=node should define __dirname, __filename, import.meta.dir, etc #2865 - PR folds import.meta.filename in CJS output (replacing it with __filename), directly addressing one of the sub-items requested in this feature issue

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

Fixes #2865

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. bundler: error on 'with' statements that would land in strict-mode ESM output #35959 - Both reject with statements in ESM output via mark_strict_mode_feature(StrictModeFeature::WithStatement, ...) with overlapping test cases; bundler: fold import.meta.filename for cjs output; emit direct-eval build note #35961 does it in the parse pass (t_with) while bundler: error on 'with' statements that would land in strict-mode ESM output #35959 does it in the visit pass (s_with)

🤖 Generated with Claude Code

@robobun robobun changed the title bundler: reject with-statement in ESM output; fold import.meta.filename for cjs bundler: fold import.meta.filename for cjs output; emit direct-eval build note Jul 26, 2026
@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Dropped the with-statement hunk: #35959 (opened a few minutes earlier) handles it in the visit pass, which also covers with(x){}; export{} bundled to non-ESM output (esbuild errors there too). This PR now only carries the import.meta.filename cjs fold and the direct-eval build note.

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

Additional findings (outside current diff — PR may have been updated during review):

  • 🔴 src/js_parser/parse/parse_stmt.rs:308-310 — The mark_strict_mode_feature(WithStatement, ...) call is placed in the parse pass, but esbuild performs this check in the visit pass (case *js_ast.SWith: in visitStmts). At parse time is_strict_mode() only sees ExplicitStrictMode — ImplicitStrictModeClass (visit/mod.rs:805) and ImplicitStrictModeImport/Export/TopLevelAwait (p.rs:2796–2805, prepare_for_visit_pass) are set after parsing — so class C { m() { with(x){} } } and export {}; with(x){} bundled to --format=cjs slip through and still produce output every engine rejects. Moving the call into s_with() at visit_stmt.rs:1620 (using js_lexer::range_of_identifier(p.source, stmt.loc) for the range) matches esbuild's control flow, aligns with every other mark_strict_mode_feature caller in this codebase, and covers the whole bug class in one place.

    Extended reasoning...

    What the bug is

    The PR adds p.mark_strict_mode_feature(StrictModeFeature::WithStatement, with_range, b"") inside t_with() in parse_stmt.rs — the parse pass. The reference implementation (esbuild) performs this check in the visit pass instead, under case *js_ast.SWith: in visitStmts (js_parser.go ~10885). Per REVIEW.md, "for ported code, the reference implementation (esbuild, Node) is the spec — diff control flow against it".

    Why the layer matters

    mark_strict_mode_feature reads self.current_scope().strict_mode via is_strict_mode() (p.rs:4331, 4383). Several strict-mode kinds are only populated after the parse pass completes:

    • ImplicitStrictModeClass is set at visit/mod.rs:805 via recursive_set_strict_mode(StrictModeKind::ImplicitStrictModeClass) — during class visit.
    • ImplicitStrictModeImport / Export / TopLevelAwait are set at p.rs:2796–2805 in prepare_for_visit_pass, after parsing completes.

    The only strict-mode kind set during parsing is ExplicitStrictMode (a leading "use strict" directive, parse/mod.rs:1486), inherited by child scopes at push time (p.rs:3261). So at parse time, is_strict_mode() inside t_with() returns false for every implicit-strict scope.

    Step-by-step proof

    Take bun build --format=cjs --target=node on:

    class C { m() { with (x) {} } }
    1. Parse pass hits t_with() at parse_stmt.rs:308. At this point current_scope().strict_mode == SloppyMode — the class body's ImplicitStrictModeClass won't be set until visit/mod.rs:805.
    2. is_strict_mode() (p.rs:4331) → false.
    3. is_strict_mode_output_format() (p.rs:4368, 4388) → bundle && output_format.is_esm() → false for --format=cjs.
    4. mark_strict_mode_feature returns Ok(()) with no diagnostic.
    5. Visit pass reaches s_with() at visit_stmt.rs:1620 — no check there.
    6. Output contains class C { m() { with (x) {} } }, which Node.js (and every engine) rejects: "SyntaxError: Strict mode code may not include a with statement".

    Same walkthrough for export {}; with (Math) {} under --format=cjs: ImplicitStrictModeExport is set at p.rs:2801 in prepare_for_visit_pass, after t_with() has already run and returned without erroring.

    esbuild rejects both of these; Bun with this patch silently emits unparseable output — the exact symptom the PR title says it fixes.

    Why existing code doesn't prevent it

    The two things that do work at parse time — the ESM-output-format check (is_strict_mode_output_format() reads p.options, populated before parsing) and explicit "use strict" (ExplicitStrictMode set at parse/mod.rs:1486) — happen to cover only the two test cases the PR added. Every other mark_strict_mode_feature caller in the codebase (visit/mod.rs:99, visit/mod.rs:620, visit_expr.rs:187, p.rs:4535 which is called from the visit pass) lives in the visit pass precisely so it sees the fully-resolved strict_mode.

    Impact

    Per REVIEW.md, "Fix the whole class in the same PR" and "Fix bugs at the layer that owns the violated invariant, never where the symptom appears". The PR's stated bug class is "bun build emits output Node.js cannot parse due to with", and the wrong-layer placement leaves that class open for class bodies and implicit-ESM modules bundled to non-ESM output.

    Fix

    Move the call from t_with() (parse_stmt.rs:310) into s_with() at visit_stmt.rs:1620, before p.visit_expr(&mut data.value):

    fn s_with(p: &mut Self, stmts: &mut StmtList<'a>, stmt: &mut Stmt, data: &mut S::With) -> Result<(), Error> {
        p.mark_strict_mode_feature(
            StrictModeFeature::WithStatement,
            js_lexer::range_of_identifier(p.source, stmt.loc),
            b"",
        )?;
        p.visit_expr(&mut data.value);
        ...

    This is the same one-line change, matches esbuild exactly, and covers ExplicitStrictMode, ImplicitStrictModeClass, and ImplicitStrictModeImport/Export/TopLevelAwait in one place. Worth adding a test for the class-body case (class C { m() { with(x){} } } under --format=cjs should error) alongside the existing three.

Comment thread src/js_parser/fold.rs
@robobun

robobun commented Jul 26, 2026 •

Copy link
Copy Markdown
Collaborator Author

Re the parse_stmt.rs:308-310 finding: that hunk was dropped in e5eb286 before the review ran; #35959 handles with in the visit pass for exactly the reason described (class bodies, implicit-ESM under --format=cjs).

CI on build 82576 (fad5bfd, finished): 195/196 jobs passed. The only failure is the binary-size check, which compares against a stale canary baseline (main #79916, ~2600 builds back) and shows the same +~550 KB on unrelated PR builds 82552 and 82554; this diff is three source lines. The four test annotations (test-fastutf8stream-reopen, test-https-timeout, no-orphans, fetch-leak) are all [flaky] and passed on retry. The new edgecase/ImportMetaFilenameCjs test passed on every lane.

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

LGTM — the two inline notes are non-blocking test-strength nits; the change itself is correct.

What was reviewed:

  • fold.rs: import.meta.filename joins the existing .path arm and inlines to p.source.path.text (full path), matching Node's __filename semantics; the sibling .dir/.dirname/.file/.url arms already covered the rest of the set.
  • visit_expr.rs: the direct-eval note follows the identical range_of_identifier → add_range_debug pattern used ~150 lines below for the unbundled-require note, and is gated on p.options.bundle like esbuild.
  • The pre-existing module.filename basename fold flagged last run has been acknowledged and handed off; the CI failure (worker-transfer-terminate-stress SIGABRT) is unrelated to the bundler.
Extended reasoning...

Overview

Two hunks in the JS parser plus one bundler test:

  • src/js_parser/fold.rs (+1/-1): adds || name == b"filename" to the existing name == b"path" arm inside the EImportMeta cjs/Bake fold block, so import.meta.filename inlines to p.source.path.text alongside import.meta.path. This closes the last hole in the {dir,dirname,file,path,url} set that already prevented raw import.meta from reaching --format=cjs output.
  • src/js_parser/visit/visit_expr.rs (+8/-1): replaces a TODO inside the direct-eval detection block with esbuild's debug-level note, gated on p.options.bundle. The call shape (js_lexer::range_of_identifier(p.source, e_.target.loc) → p.log().add_range_debug(Some(p.source), r, b"...")) is byte-for-byte the pattern already used at visit_expr.rs:2056-2062 for the unbundled-require note.
  • test/bundler/bundler_edgecase.test.ts (+20): edgecase/ImportMetaFilenameCjs bundles import.meta.{filename,dirname,path} with format: "cjs", target: "node", asserts no import.meta token remains, and runs the output under Node checking typeof === "string" for each.

Security risks

None. No untrusted-input parsing, no allocation, no lifetime changes. The fold arm reuses an existing e_string_init(p.source.path.text) expression already emitted for the sibling .path case; the eval note is a debug-level log message with a static byte literal.

Level of scrutiny

Low. The fold change is a one-token addition to an existing conditional whose consequent is unchanged; correctness reduces to "is import.meta.filename semantically the full path?" — yes, per Node's docs it equals __filename. The eval note is a TODO-to-implementation swap that copies an in-file pattern verbatim and only fires at debug log level, so worst-case regression is a spurious build note.

Other factors

  • The two inline findings this run are both nits about test strength (no coverage for the debug note; typeof rather than value equality), not correctness. The primary fix — raw import.meta no longer reaches cjs output — is covered by the new test's .not.toContain("import.meta") + runtime: "node" assertions.
  • My prior-run comment on the pre-existing module.filename → basename fold (fold.rs:364) was acknowledged; the author is handling it (and module.path) in a follow-up. That thread is resolved and not merge-blocking here.
  • The with-statement hunk was already dropped in favor of #35959, so no overlap remains.
  • CI shows worker-transfer-terminate-stress.test.ts SIGABRT on x64-asan and a binary-size delta vs an older canary; neither is attributable to a 9-line bundler diff.

Comment thread src/js_parser/visit/visit_expr.rs
Comment thread test/bundler/bundler_edgecase.test.ts

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/bundler/bundler_edgecase.test.ts`:
- Around line 2155-2156: Update the assertions around import.meta.filename and
import.meta.path to compare the actual full source path against the fixture’s
expected absolute /entry.ts path, using platform-safe path construction. Replace
the current shape-only length and equality checks while preserving the existing
test context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 43c310cd-c8bc-4426-9ea8-a0de4e709e75

📥 Commits

Reviewing files that changed from the base of the PR and between 44f6469 and fad5bfd.

📒 Files selected for processing (3)
  • src/js_parser/fold.rs
  • src/js_parser/visit/visit_expr.rs
  • test/bundler/bundler_edgecase.test.ts

Comment thread test/bundler/bundler_edgecase.test.ts

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

LGTM — the test-strength nit is addressed in fad5bfd (.filename === .path and .length > .file.length pin the full-path value), and the direct-eval note gap is acknowledged with a reasonable rationale (debug-level, no harness knob, matches the untested sibling at :2058).

Extended reasoning...

Overview

Three small changes: fold.rs adds || name == b"filename" to the existing import.meta.path fold arm so cjs/Bake output inlines import.meta.filename to the full path instead of leaving raw import.meta (a Node syntax error); visit_expr.rs replaces a TODO with an add_range_debug note for direct eval() under p.options.bundle, matching esbuild and the existing add_range_debug pattern at :2058; bundler_edgecase.test.ts gains edgecase/ImportMetaFilenameCjs.

Security risks

None. This is a compile-time constant-folding tweak in the parser plus a debug-level diagnostic string. No user input reaches new allocation or side-effecting paths.

Level of scrutiny

Low. The fold.rs change is a one-token addition to an existing branch that already handles path with the correct value (p.source.path.text). The visit_expr.rs change is a non-load-bearing advisory diagnostic guarded by p.options.bundle, using the same helper (js_lexer::range_of_identifier + p.log().add_range_debug) as neighboring code.

Other factors

I reviewed this PR twice previously. All three inline threads are resolved: the pre-existing module.filename basename bug was handed off separately (correct — different failure class); the test-strength nit was fixed in fad5bfd; the direct-eval-note coverage gap was acknowledged with the harness-limitation explanation. The new test asserts no import.meta token remains in cjs output, runs the result under Node, and now pins .filename to the full path via equality with .path and a length check against .file. No new findings from the bug-hunting system this run.

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.

2 participants