Skip to content

bundler: resolve __dirname/__filename at runtime for --target=bun/node - #35470

Closed
robobun wants to merge 11 commits into
mainfrom
farm/192dcd65/bundler-dirname-filename-runtime
Closed

robobun wants to merge 11 commits into
mainfrom
farm/192dcd65/bundler-dirname-filename-runtime

Conversation

@robobun

@robobun robobun commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator

What

Bundling a file that references __dirname or __filename with --target=bun (or --target=node) previously inlined the build machine's absolute source path as a string literal:

$ echo 'console.log(__dirname)' > example.js
$ bun build example.js --target=bun
// @bun
var __dirname = "/home/user/project";
console.log(__dirname);

This leaked the build environment into the output, broke the bundle as soon as it was run from any other location, and made builds non-reproducible.

Fix

The parser options now carry the bundle target. The post-visit __dirname/__filename handling in parse_entry.rs branches on it:

target format emitted for __dirname / __filename
bun esm import.meta.dir / import.meta.path
node esm import.meta.dirname / import.meta.filename (Node 20.11+)
bun, node cjs nothing (falls through to the module wrapper's params)
browser / iife / bake any unchanged: build-time string literal

Since --compile implies --target=bun, compiled executables now get the virtual $bunfs path for __dirname/__filename, matching import.meta.dir/.path.

For CJS output on those targets, user-written import.meta.dir/.dirname/.path/.filename are rewritten to the wrapper's __dirname/__filename instead of being inlined as build-time strings, so they stay consistent with the now runtime-resolved __dirname. import.meta.url and import.meta.file keep their existing build-time inlining in CJS output: replacing them needs synthesized node:url/node:path calls rather than an identifier substitution, and that pre-existing behavior is intentionally out of scope here. Bake keeps its per-source-file inlining in both formats.

Deferring to the wrapper exposed a Windows bug in JSCommonJSModule: standalone-executable module keys are forward-slashed (B:/~BUN/root/app.js), but the wrapper's __dirname was computed by splitting on backslash only, so compiled CJS executables (--bytecode implies CJS) got __dirname === "" on Windows. The split now honors both separators (lastPathSeparatorIndex in PathInlines.h); __filename stays identical to the require-cache key.

After

$ bun build example.js --target=bun
// @bun
var __dirname = import.meta.dir;
console.log(__dirname);

$ bun build example.js --target=node
var __dirname = import.meta.dirname;
console.log(__dirname);

$ bun build example.js --target=bun --format=cjs
// @bun @bun-cjs
(function(exports, require, module, __filename, __dirname) {
console.log(__dirname);
})

Why this is the right behavior

The non-bundling runtime transpiler already does exactly this (emits var __dirname = import.meta.dir for ESM sources); this change brings bundled output in line. esbuild's --platform=node leaves __dirname unbound in ESM output (resolved by the host runtime); the effect is the same but Bun's // @bun bundles skip retranspilation, so we spell the import.meta access out.

A bundle collapses many source files into one output chunk, so there is no meaningful per-source-file __dirname anymore; every reference now resolves to the output chunk's directory. That is the only value a relocatable bundle can give.

Tests

A new suite in test/bundler/cli.test.ts bundles to a temp dir, copies the output to an unrelated location, runs it, and asserts __dirname/__filename report the runtime location (for both --target=bun/node, both ESM and CJS). The --target=node bundles run under a real node binary so the emitted import.meta.dirname/filename are exercised on the runtime they target. Also asserts the build-time path never appears in the output. Fails on main, passes with this change. (The suite lives in the bundler test file, not test/regression/, since the old behavior was never correct.)

Updated the existing "__dirname and __filename are printed correctly" test to expect the $bunfs path for --compile --bytecode --format=esm (ESM bytecode, covering the module-record import.meta flag), and preserved its UTF-8 path-literal coverage via a --target=browser build. A separate test covers --compile --bytecode without --format (implied CJS), where the wrapper exposes the forward-slashed standalone graph key on Windows; that test fails without the JSCommonJSModule separator fix. Both verified on Windows x64 and linux.

Fixes #4216
Fixes #17188


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

Bundling a file that references __dirname or __filename with --target=bun
(or --target=node) previously inlined the build machine's absolute source
path as a string literal:

    var __dirname = "/home/user/project/src";

This leaked the build environment into the output, broke the bundle as
soon as it was run from any other location, and made builds
non-reproducible.

The bundler now threads the target through to the parser and, for ESM
output, emits `var __dirname = import.meta.dir` / `import.meta.path`
(Bun) or `import.meta.dirname` / `import.meta.filename` (Node) so the
values resolve to the output file's location at runtime. For CJS output
on those targets the declaration is dropped entirely, letting references
fall through to the module wrapper's __dirname/__filename parameters.
Browser and IIFE output are unchanged (no runtime equivalent exists).

Since --compile implies --target=bun this also gives compiled executables
the virtual $bunfs path for __dirname/__filename, matching
import.meta.dir.

Fixes #4216.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Parser options now carry the resolved target into bundle-time parsing. Bun and Node bundles use runtime-aware __dirname/__filename handling, while browser builds retain literal inlining. Tests cover ESM, CJS, compiled executables, moved bundles, and browser output.

Target-aware dirname and filename bundling

Layer / File(s) Summary
Propagate target into parser options
src/bundler/ParseTask.rs, src/bundler/transpiler.rs
Bundler and transpiler parser configurations now forward the resolved target.
Apply target-aware dirname and filename lowering
src/js_parser/parse/parse_entry.rs
Parser options preserve the target, and bundle-time lowering selects import.meta values, CJS-wrapper handling, or browser-compatible literals.
Validate target-specific runtime behavior
test/bundler/cli.test.ts, test/regression/issue/04216.test.ts
Tests validate compiled paths, moved-runtime behavior, CJS bundles, and browser-target inlining.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address #4216 and #17188 by using target-aware runtime resolution for __dirname/__filename and adding regression tests.
Out of Scope Changes check ✅ Passed No unrelated code changes stand out; the browser-target test coverage is still directly tied to preserving existing behavior.
Title check ✅ Passed The title accurately summarizes the main change: runtime resolution of __dirname/__filename for bun/node targets.
Description check ✅ Passed The description covers the change, behavior by target/format, caveats, and verification steps, even though the headings differ from the template.

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

@github-actions

Copy link
Copy Markdown
Contributor

Found 4 issues this PR may fix:

  1. Bundling target=node should define __dirname, __filename, import.meta.dir, etc #2865 - Requests that bundling with --target=node emit import.meta.dirname/import.meta.filename instead of hardcoded paths, which this PR implements
  2. Bun doesn't correctly build import.meta.path #15994 - Reports import.meta.path being incorrectly resolved to the entry point's path during bundling, which this PR fixes by emitting runtime references
  3. Allow Build API define to map to non strings #6404 - Requested non-string define mappings as a workaround for hardcoded __dirname; this PR fixes the root cause directly
  4. JS loader caues unnecessary/unwanted/unexpected transformations #15996 - Reports user-defined __dirname/__filename variables being renamed due to Bun injecting conflicting definitions; this PR stops injecting hardcoded literals

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

Fixes #2865
Fixes #15994
Fixes #6404
Fixes #15996

🤖 Generated with Claude Code

The bytecode module-info path in postProcessJSChunk derives
contains_import_meta from the AST flag rather than the printer, so set
it when the post-visit __dirname/__filename lowering introduces an
import.meta reference.
@robobun

robobun commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review

Reproduced on main with echo 'console.log(__dirname)' | bun build - --target=bun → output contains var __dirname = "/abs/build/path".

Verification:

  • bun bd test test/bundler/cli.test.ts -t "resolve at runtime" → 5 pass (4 fail on main)
  • bun bd test test/bundler/bundler_edgecase.test.ts -t 4216 → 3 pass (3 fail on main)
  • --target=node bundles verified under real Node 26.3.0 for both ESM and CJS
  • --compile / --bytecode / --minify / --splitting all produce working output

Self-review concerns addressed: Bake production gated out (framework.is_some()), import.meta.dir in --format=cjs now agrees with __dirname, per-target output shape locked via itBundled.

@robobun

robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Checked the four suggested issues against the diff:

Leaving the PR description as is (#4216 and #17188 only); none of the four should be auto-closed by this change.

@github-actions

github-actions Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Use the virtual $bunfs path for __dirname/__filename in --compile executables #29066 - Same approach: lowers __dirname/__filename to import.meta.dir/import.meta.path in the bundler instead of inlining build-time paths; fixes the same issue 17188

🤖 Generated with Claude Code

@robobun

robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Looked at #29066: it is an overlapping earlier PR, not an exact duplicate. It adds a compile flag to the parser and lowers __dirname/__filename to import.meta.dir/path only for --compile builds, fixing #17188.

This PR keys off the bundle target instead, so it covers plain --target=bun and --target=node bundles as well as --compile (which implies target=bun), and emits the Node spelling (import.meta.dirname/filename) for node output. Merging this PR makes #29066 redundant; merging #29066 alone would still leave #4216 open for non-compile bundles.

@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: 6

🤖 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 `@src/js_parser/parse/parse_entry.rs`:
- Around line 1062-1163: Extract the shared __dirname/__filename part
construction into a helper reused by this block and the runtime injection near
the existing dirname/filename builder. Have the helper accept the dirname and
filename value expressions, while owning the shared count, declaration indexing,
DeclaredSymbolList, S::Local statement, and PartTag::DirnameFilename setup;
preserve each caller’s distinct value-expression generation and ensure both
paths apply identical declaration metadata.

In `@test/bundler/cli.test.ts`:
- Around line 223-233: Drain and assert all subprocess streams concurrently: in
test/bundler/cli.test.ts lines 223-233, add browserBuild.stderr.text() to the
Promise.all results and assert it does not contain "error:" before checking
browserExit; in test/regression/issue/04216.test.ts lines 30-45 and 89-105, add
build.stdout.text() to each Promise.all alongside the existing stderr and exited
awaits, preserving the combined-result assertions.

In `@test/regression/issue/04216.test.ts`:
- Around line 30-45: Drain the piped stdout from the Bun build spawned in the
regression test by awaiting build.stdout.text() alongside build.stderr.text()
and build.exited. Keep the existing stderr and exit-status handling unchanged
while ensuring the stdout pipe is consumed before the process completes.
- Around line 1-11: Move the __dirname/__filename runtime-resolution cases from
the regression suite into the existing bundler test file, such as cli.test.ts,
because issue `#4216` was never a correct behavior and is not a regression. Remove
the regression-only header explanation; do not retain this test under
test/regression/issue/.
- Around line 89-105: Drain the piped stdout from the Bun.spawn build in the
CommonJS build test by reading build.stdout alongside build.stderr and
build.exited in the Promise.all call. Preserve the existing build command and
exit-status handling.
- Around line 59-66: Update the process setup around Bun.spawn to select
nodeExe() when testing the --target=node bundle, while retaining bunExe() for
other targets. If nodeExe() is unavailable, skip that branch with an explicit
reason; preserve the existing stdout, stderr, and exit-code collection.
🪄 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: 6eeb75d0-dfaf-429b-84d5-4a2b41b846a0

📥 Commits

Reviewing files that changed from the base of the PR and between 028f7a3 and f4f4621.

📒 Files selected for processing (5)
  • src/bundler/ParseTask.rs
  • src/bundler/transpiler.rs
  • src/js_parser/parse/parse_entry.rs
  • test/bundler/cli.test.ts
  • test/regression/issue/04216.test.ts

Comment thread src/js_parser/parse/parse_entry.rs Outdated
Comment thread test/bundler/cli.test.ts
Comment thread test/regression/issue/04216.test.ts Outdated
Comment thread test/regression/issue/04216.test.ts Outdated
Comment thread test/regression/issue/04216.test.ts Outdated
Comment thread test/regression/issue/04216.test.ts Outdated
…regression dir

- Extract import_meta_dot and inject_dirname_filename_part helpers shared
  by the bundle-time and runtime lowering paths so declaration metadata
  stays identical in both
- Move the issue 4216 suite into test/bundler/cli.test.ts: the hardcoded
  path behavior was never correct, so it is not a regression
- Run --target=node bundles under a real node binary (skip if absent)
- Drain stdout/stderr on every build spawn
@robobun

robobun commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:31 PM PT - Jul 24th, 2026

@robobun, your commit b412c36 is building: #80137

@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):

  • 🟡 test/regression/issue/04216.test.ts:42-45 — Several new subprocess spawns set a pipe they never drain: the two build spawns in test/regression/issue/04216.test.ts (lines ~42-45 and ~106-108) set stdout: "pipe" but only await stderr.text() + exited, and the browserBuild spawn in test/bundler/cli.test.ts:223-230 sets stderr: "pipe" but only awaits stdout.text() + exited. REVIEW.md's harness convention requires draining all pipes concurrently — add the missing .text() to each Promise.all, or use "ignore" for the unused stream.

    Extended reasoning...

    What's happening

    Three newly-added Bun.spawn calls configure a pipe that is never read:

    test/regression/issue/04216.test.ts — both the ESM test (lines ~30-45) and the CJS test (lines ~93-108) spawn bun build --outfile with stdout: "pipe" and stderr: "pipe", but the subsequent Promise.all only contains [build.stderr.text(), build.exited]. bun build --outfile writes its "Bundled N modules in Xms" summary to stdout (the "log case" tests in cli.test.ts snapshot exactly this output), so the stdout pipe receives data that is never consumed.

    test/bundler/cli.test.ts:223-230 — the new browserBuild spawn sets stderr: "pipe", but the Promise.all only contains [browserBuild.stdout.text(), browserBuild.exited]. Any warnings or errors written to stderr are silently discarded.

    Why this violates the harness convention

    REVIEW.md is explicit under Tests reviewers reject:

    Subprocess tests: drain pipes concurrently. Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]) — an unread pipe fills the ~64KB OS buffer and deadlocks the child.

    The convention exists for two reasons: (1) an unread pipe eventually fills the OS pipe buffer (~64KB) and blocks the child on write(2), deadlocking the test; (2) discarding a piped stream throws away diagnostic output that would explain a failure — if the browserBuild exit-code assertion ever fails in CI, whatever bun build wrote to stderr is gone.

    Step-by-step trace

    Take the ESM build spawn at 04216.test.ts:30-47:

    1. Bun.spawn is called with stdout: "pipe", stderr: "pipe".
    2. The child (bun build --outfile …) writes ~100 bytes of "Bundled 2 modules in Xms\n\n bundle.mjs …" to stdout and exits 0.
    3. The test awaits Promise.all([build.stderr.text(), build.exited]) — stderr is drained, exit is observed, stdout is never touched.
    4. Because ~100 bytes ≪ 64KB, the child does not block; the test passes. But if a future change makes bun build chattier on stdout (verbose logging, many modules), this same pattern would hang.

    The browserBuild case is symmetric with stderr: a two-line browser build won't emit 64KB of stderr, so no deadlock in practice, but any error output is lost.

    Impact

    No concrete failure or flake will occur with the current tiny outputs — this is a harness-convention violation, not a correctness bug. The practical cost is diagnosability: when one of these assertions fails in CI, the discarded stream is exactly what you'd want to see.

    Fix

    Add the missing pipe to each Promise.all:

    // 04216.test.ts (both build spawns)
    const [, buildStderr, buildExit] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]);
    
    // cli.test.ts browserBuild
    const [browserOut, browserErr, browserExit] = await Promise.all([
      browserBuild.stdout.text(),
      browserBuild.stderr.text(),
      browserBuild.exited,
    ]);
    expect(browserErr).not.toContain("error:");

    Alternatively, if the stream is intentionally unused, set it to "ignore" instead of "pipe".

Comment thread src/js_parser/parse/parse_entry.rs Outdated
@robobun

robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Both findings were against an earlier revision:

  • The undrained pipes were fixed in 6cc8318: the suite moved into test/bundler/cli.test.ts and every build spawn now drains stdout and stderr concurrently, with the browser build asserting stderr.
  • p.has_import_meta = true is set when import_meta_names is Some since f4f4621, so the bundled AST carries HAS_IMPORT_META and the bytecode module record links ImportMetaFeature.

The bytecode gap in test coverage was real though: 4cedb52 adds --bytecode to the --compile dirname test, so the only import.meta reference in that module is the injected import.meta.dir and the cached-bytecode link path is exercised. Verified locally that the compiled executable prints the $bunfs paths.

- Gate the import.meta.dir lowering on framework.is_none() so Bake
  production (which inlines import.meta.dir as a per-source string) keeps
  __dirname consistent with it.
- fold.rs: for --format=cjs on Bun/Node targets (outside Bake), rewrite
  import.meta.dir/.dirname/.path/.filename to the wrapper's
  __dirname/__filename params instead of inlining the build-time path, so
  they agree with __dirname now that it falls through to the wrapper.
- Add itBundled output-shape tests next to ImportMetaMain that pin the
  per-target property names, and extend the cjs move-and-run test to
  cover import.meta.dir === __dirname.
Comment thread src/js_parser/fold.rs Outdated
Comment thread test/bundler/bundler_edgecase.test.ts
@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed both follow-up nits:

  • 7987308 tightens the Bun-target output-shape test: toMatch(/import\.meta\.dir\b/) and \.path\b word-boundary matches plus negative checks for the Node spellings, mirroring the Node-target test.
  • import.meta.url/.file in CJS output keep their pre-existing build-time inlining; replacing them needs synthesized node:url/node:path calls rather than the identifier substitution used for .dir/.path, so that is intentionally out of scope. Noted in the PR description.

Verified locally: the runtime suite (bun and node, ESM and CJS) and the three output-shape tests all pass with the current head.

Comment thread test/bundler/cli.test.ts Outdated
Standalone graph keys are forward-slashed (B:/~BUN/root/app.js), but the
CJS wrapper computed __dirname by splitting on backslash only, yielding
an empty string in compiled executables on Windows. Split on either
separator via lastPathSeparatorIndex.

Also split the compile test: --bytecode alone implies CJS output, so the
previous test never exercised the ESM bytecode import.meta flag. The
existing test now passes --format=esm explicitly, and a new test covers
the implied-CJS wrapper values with the forward-slashed graph key.

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

No bugs found in this pass; all earlier findings (the has_import_meta flag, the --bytecode implied-CJS test gap, and the Windows CJS-wrapper separator) have been addressed. That said, this changes user-visible bundler semantics for __dirname/__filename under --target=bun|node and widens the Windows __dirname separator handling in JSCommonJSModule for every CJS module — worth a maintainer sign-off.

What was reviewed:

  • parse_entry.rs: the new import_meta_names/defer_to_cjs_wrapper branching and the shared inject_dirname_filename_part helper preserve the original part shape and declared_symbols.
  • fold.rs: the CJS import.meta.dir/.path → __dirname/__filename rewrite records usage on the wrapper refs and only fires outside Bake; import.meta.url/.file intentionally left as-is (noted in the description).
  • PathInlines.h / JSCommonJSModule.cpp: lastPathSeparatorIndex is identical to the old reverseFind(PLATFORM_SEP) on POSIX and matches Node's both-separator semantics on Windows; sibling reverseFind(PLATFORM_SEP) sites in ImportMetaObject.cpp are on the ESM import.meta path and unaffected by this fix.
Extended reasoning...

Overview

This PR threads the bundle target into parser options so __dirname/__filename in bundled output for --target=bun|node resolve at runtime (via import.meta.dir/.path for ESM, or the CJS wrapper params) instead of inlining the build machine's absolute source path. It refactors two near-identical declaration-injection blocks in parse_entry.rs into shared helpers, adds a CJS-target rewrite in fold.rs so user-written import.meta.dir/.path stay consistent with __dirname, and fixes a Windows bug in JSCommonJSModule.cpp where the CJS wrapper's __dirname was derived by splitting on backslash only (breaking forward-slashed standalone-executable keys). Eight files across parser, bundler, C++ bindings, and tests.

Security risks

None identified. No untrusted input parsing, no auth/crypto/permission surface. The path-separator change is a string-splitting refinement on trusted internal module keys.

Level of scrutiny

Moderate-to-high. The core logic is a targeted refactor of an existing lowering pass, and the C++ change is a small, well-scoped helper. But it is a user-visible semantic change to bundler output — every --target=bun|node bundle that references __dirname/__filename now emits different code and produces different runtime values (chunk location instead of per-source path). That's the right fix for #4216, but it's a product decision (e.g., a bundle that previously happened to work because it ran from the build directory will now report a different path). The Windows lastPathSeparatorIndex change also affects every CJS module's __dirname on Windows, not just standalone executables — behaviorally it now matches Node's path.win32.dirname (both separators), which is a correctness improvement, but it is broader than the standalone-executable case that motivated it.

Other factors

  • The PR has been through three prior review rounds from me with substantive findings each time (missing has_import_meta flag, --bytecode defaulting to CJS so the guard test wasn't exercising the ESM path, weak substring assertions), all addressed with follow-up commits and test coverage.
  • Test coverage is thorough: end-to-end runtime tests for both targets × both formats (Node-target run under real node), itBundled output-shape locks with word-boundary regexes, a dedicated --compile --bytecode (implied CJS) test that fails without the Windows separator fix, and the browser fallback preserved. All prior review threads are marked resolved.
  • The --target=node ESM lowering emits import.meta.dirname/.filename, which requires Node 20.11+; that's noted in the description but is a compatibility floor a maintainer may want to weigh.

Given the scope (parser + bundler + C++ runtime) and the user-visible behavior change, I'm deferring rather than approving.

Comment thread src/js_parser/fold.rs Outdated
Comment thread src/js_parser/fold.rs Outdated
Comment thread src/js_parser/parse/parse_entry.rs Outdated
Comment thread src/js_parser/parse/parse_entry.rs Outdated
Comment thread src/js_parser/parse/parse_entry.rs Outdated
Comment thread src/js_parser/parse/parse_entry.rs Outdated
Comment thread src/js_parser/parse/parse_entry.rs Outdated
Comment thread src/jsc/bindings/PathInlines.h Outdated

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

No new findings on this revision — all four earlier rounds of feedback are addressed (has_import_meta flag, --format=esm for the bytecode test, word-boundary output-shape assertions, and the comment-cop cleanups). Deferring to a human because this is a user-facing bundler output-semantics change plus a native-side Windows path-separator change applied to all four __dirname derivation sites in JSCommonJSModule.cpp, not just the standalone-executable one.

What was reviewed:

  • Traced the new target option through ParseTask/transpiler into parse_entry.rs and fold.rs; browser/iife and Bake keep the string-literal path, and the CJS defer-to-wrapper branch correctly skips part injection.
  • lastPathSeparatorIndex on non-Windows is byte-identical to the old reverseFind(PLATFORM_SEP); on Windows it now honors forward slashes, which the new implied-CJS --compile --bytecode test covers.
  • Confirmed the extracted inject_dirname_filename_part helper preserves the pre-existing decl/declared_symbols shape and part tag for both call sites.
Extended reasoning...

Overview

The PR threads the bundle target into parser options and uses it in two places: (1) parse_entry.rs post-visit, where __dirname/__filename now lower to import.meta.dir/.path (bun) or import.meta.dirname/.filename (node) for ESM output, defer to the CJS wrapper for CJS output, and keep the build-time string literal for browser/iife/Bake; and (2) fold.rs, where user-written import.meta.dir/.dirname/.path/.filename in CJS output on bun/node targets rewrite to the wrapper's __dirname/__filename refs instead of inlining. The two duplicated part-injection blocks in parse_entry.rs are collapsed into shared import_meta_dot / inject_dirname_filename_part helpers. PathInlines.h gains lastPathSeparatorIndex (both separators on Windows), replacing four reverseFind(PLATFORM_SEP) sites in JSCommonJSModule.cpp so the CJS wrapper's __dirname is derived correctly from forward-slashed standalone-graph keys. Tests: three itBundled output-shape locks in bundler_edgecase.test.ts and a full end-to-end suite in cli.test.ts (build → relocate → run under both Bun and real Node, ESM and CJS, plus --compile --bytecode in both formats).

Security risks

None identified. No new untrusted-input parsing, no auth/crypto/permissions surface. The lastPathSeparatorIndex helper only widens which separator is recognized when deriving __dirname from an already-resolved module key; it does not introduce a new path-traversal surface.

Level of scrutiny

High. This is a deliberate change to bundler output semantics: every existing bun build --target=bun|node consumer whose code references __dirname/__filename will now see the output chunk's runtime location instead of the source file's build-time path. That is almost certainly what users want (#4216, #17188), and it matches what the non-bundling runtime transpiler already does — but it is still a behavior change a maintainer should sign off on. Separately, the JSCommonJSModule.cpp change applies the both-separator split to all four __dirname derivation sites, not only the standalone-executable path that motivated it; on non-Windows it is a no-op, but on Windows it changes how any forward-slashed require-map key is split. That broadening looks correct but is worth a human glance.

Other factors

The PR has been through four rounds of automated review on earlier revisions (missing has_import_meta flag, import.meta.url/.file scope note, substring-vs-word-boundary assertion, and --bytecode implying CJS which also surfaced the Windows separator bug). All are addressed in the current head, the comment-cop threads are resolved by b412c36, and this run's bug hunt found nothing new. Test coverage is thorough — output-shape locks per target, relocated-bundle runtime checks under both Bun and real Node, and both --compile --bytecode formats — with the author reporting Windows x64 and Linux verification. The one intentionally-deferred sibling (import.meta.url/.file still inline in CJS) is documented in the description.

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Current CI state for reviewers: the latest run (build 80137) is red only from infrastructure, not the diff. Three build-bun compile jobs (linux x64, linux x64-asan, freebsd x64) timed out before producing a binary, which left the downstream test jobs stuck, and the handful of test annotations are flaky tests that passed on retry on unrelated lanes. The same lanes built fine on the previous run of this branch, where every test lane that ran my changed files was green, including both Windows lanes after the CJS wrapper fix. The diff itself is complete and verified locally on linux and Windows x64; it needs a CI re-run or a maintainer look rather than more code changes.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Consolidated into #29066, which now also carries the pieces from this PR that apply to compiled executables: the JSCommonJSModule.cpp / PathInlines.h separator fix (verified on Windows x64 that compiled CJS output prints an empty __dirname without it), the import.meta.filename rewrite for CJS output, and the ESM-with---bytecode coverage. Closing.

The part of this PR that is not carried over is the change to plain --target=bun / --target=node bundles. Inlining the source path there is the behavior the earlier review of #17222 described as intentional (native addons and other files located relative to __dirname in node_modules keep resolving when a bundle runs in place) and asked to put behind an option, so changing it unconditionally is not something this consolidation should decide; #4216 and #17188 stay open for that discussion. For --compile the question does not arise, since the executable is relocated by definition and /$bunfs/root is the only directory its embedded files can be found under, which is the scope #29066 keeps.

Nothing in this area has landed on main in the meantime: this PR's tests (bundler_edgecase DirnameFilenameTarget*, the cli.test.ts suite) still fail on current main, as do #29066's.

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.

__dirname handled incorrectly when building Bundling with --target=bun changes __dirname references to hardcoded values

1 participant