Skip to content

Key the runtime transpiler cache on remove_cjs_module_wrapper - #40979

Open
robobun wants to merge 1 commit into
mainfrom
robobun/2c0daf89/transpiler-cache-cjs-wrapper-hash
Open

robobun wants to merge 1 commit into
mainfrom
robobun/2c0daf89/transpiler-cache-cjs-wrapper-hash

Conversation

@robobun

@robobun robobun commented Aug 30, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • With the transpiler cache on, bun a.tsx then bun - < a.tsx (same CommonJS bytes, 4 KiB or more) prints nothing and exits 0. In the other order bun a.tsx fails with ReferenceError: module is not defined. Reproduces on 1.4.1 and main.
  • The stdin and -e entry point is transpiled with remove_cjs_module_wrapper (src/jsc/RuntimeTranspilerStore.rs:826), which drops the CommonJS wrapper (src/js_parser/p.rs:8209). Features::hash_for_runtime_transpiler (src/js_parser/parser.rs:355) skips the flag, so both transpiles share one cache key.

Fix

  • remove_cjs_module_wrapper joins the hashed feature bools, and EXPECTED_VERSION moves from 27 to 28 as with every earlier change to the hash inputs.
  • Cost: one re-transpile when the same bytes switch modes, the same delete-and-rewrite as a --feature change.
  • Verified: two new tests in test/cli/run/transpiler-cache.test.ts (both orders), both fail on the released binary. The rest of that file, run-eval.test.ts and regression tests 28159, 30887, 32686 pass.

Background

  • The runtime transpiler cache (src/jsc/RuntimeTranspilerCache.rs) keys transpiled output by a hash of the source bytes plus a features hash of the options that change the output. A features hash mismatch deletes the entry and re-transpiles.
  • A CommonJS file is printed as (function(exports, require, module, __filename, __dirname) {...}) and the loader calls that function. The eval entry point gets module, exports and require as globals and runs as a plain script, so the wrapper must be absent.
  • stdin and -e use the tsx loader (src/bundler/options.rs:508), which is part of the key. Only a .tsx file of the same bytes collides.
Notes

Repro on the released binary (cjsbig.tsx is console.log("ran", typeof module); module.exports = {} plus 5 KiB of comments):

export BUN_RUNTIME_TRANSPILER_CACHE_PATH=$PWD/tc
bun cjsbig.tsx          # ran object
bun - < cjsbig.tsx      # (nothing), exit 0
rm -rf tc
bun - < cjsbig.tsx      # ran object
bun cjsbig.tsx          # ran undefined, then ReferenceError: module is not defined

Both runs write the same .pile file name (same input hash) with the same features hash. The second run reads the first run's output. In the first order the wrapper function is evaluated and never called. In the second order the unwrapped statements run without a module binding.

The evaluation paths are both in evaluateCommonJSModuleOnce (src/jsc/bindings/JSCommonJSModule.cpp). The eval entry branch is selected by Bun__VM__specifierIsEvalEntryPoint. The sync transpile path sets the same flag in src/runtime/jsc_hooks.rs:2666.

A .js file does not collide: stdin is transpiled as tsx, and ParserOptions::hash_for_runtime_transpiler hashes the ts flag and the JSX options, so the .js entry already had another features hash and was replaced, not served.

Debug build sequence with the fix (BUN_DEBUG_cache=1): file run writes, file run again restored, stdin run MismatchedFeatureHash then write, stdin run again restored, file run MismatchedFeatureHash then write.

Why key the flag and not disable the cache for the eval entry: the features hash is where every other output-affecting switch is keyed, and a large piped script still benefits from a hit on a repeat run.

Related open PRs that touch the same list or the version constant:

hash_for_runtime_transpiler lists the hashed fields by hand. An exhaustive destructure of Features would turn a missing field into a compile error. Not done here, since three open PRs add a line to this list.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/run/transpiler-cache.test.ts

The `bun -` and `bun -e` entry point is printed without the CommonJS
wrapper and evaluated as a script. The same bytes run as a file are
printed wrapped. Both transpiles had the same cache key, so one run was
served the other's output: stdin printed nothing, or the file failed
with "ReferenceError: module is not defined".

`remove_cjs_module_wrapper` joins the hashed feature bools and the cache
version moves from 27 to 28.
@robobun

robobun commented Aug 30, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: fix and tests pushed. Both automated reviews reported nothing to change. Lints, clippy, miri and mordant are green. Buildkite CI (build 108586) is still running.

Reproduced on bun 1.4.1 with a 5 KiB CommonJS .tsx file and BUN_RUNTIME_TRANSPILER_CACHE_PATH set to an empty directory. bun a.tsx then bun - < a.tsx prints nothing. bun - < a.tsx then bun a.tsx fails with ReferenceError: module is not defined. The two new tests in test/cli/run/transpiler-cache.test.ts fail on the released binary and pass with this change.

@coderabbitai

coderabbitai Bot commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b97b997-8653-4c37-bbe1-5caace7cb424

📥 Commits

Reviewing files that changed from the base of the PR and between a6c4cc2 and e59d3b7.

📒 Files selected for processing (3)
  • src/js_parser/parser.rs
  • src/jsc/RuntimeTranspilerCache.rs
  • test/cli/run/transpiler-cache.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.


Walkthrough

The runtime transpiler cache key now includes remove_cjs_module_wrapper. Cache format version 28 distinguishes unwrapped stdin output from wrapped file output. Tests cover execution order, cache invalidation, cache hits, and exit behavior.

Changes

Transpiler cache isolation

Layer / File(s) Summary
Cache key and format updates
src/js_parser/parser.rs, src/jsc/RuntimeTranspilerCache.rs
The runtime transpiler hash includes remove_cjs_module_wrapper. The cache format version increases from 27 to 28.
Entry-point cache isolation tests
test/cli/run/transpiler-cache.test.ts
Tests verify separate cache entries for stdin and file execution in both orders, including invalidation, cache hits, output, and exit behavior.

Suggested reviewers: jarred-sumner, dylan-conway

Merge Risk: ⚪ Minimal · up to e59d3

The change prevents CommonJS and eval-style transpilation from reusing incompatible cached output by including the wrapper-removal option in the cache key. No actionable merge-blocking risk remains.

🚥 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 and concisely describes the main change: adding remove_cjs_module_wrapper to the runtime transpiler cache key.
Description check ✅ Passed The description explains the problem, fix, impact, verification, and relevant background. It does not use the exact template headings, but it provides the required change summary and verification deta…
Full details: Description check

Explanation

The description explains the problem, fix, impact, verification, and relevant background. It does not use the exact template headings, but it provides the required change summary and verification details.


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

@robobun

robobun commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

The automated review reported no actionable comments, so there is nothing to change in this push. The red claude-find-issues check is the action's own error (it exited after 494 ms with is_error: true and no findings), not a report about this diff. Buildkite CI is still pending.

@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 — targeted cache-key fix with a matching version bump and both-ordering coverage.

What was reviewed:

  • hash_for_runtime_transpiler: remove_cjs_module_wrapper appended to the bool array with the length bumped 17→18; the field exists on Features and the array is hashed via bytemuck::cast_slice, so no padding/layout concern.
  • EXPECTED_VERSION 27→28 follows the file's own rule that any hash-input change bumps the constant.
  • New tests reuse the file's env/temp_dir/newCacheCount/toSpawn scaffolding, pad past the 4 KiB threshold with Buffer.alloc, use .tsx to match the stdin loader, and drain stdout/stderr/exited concurrently in the stdin helper.
Extended reasoning...

Overview

The PR adds one entry (remove_cjs_module_wrapper) to the hand-maintained bool array in Features::hash_for_runtime_transpiler (src/js_parser/parser.rs), grows the array length annotation from 17 to 18, and bumps EXPECTED_VERSION from 27 to 28 in src/jsc/RuntimeTranspilerCache.rs with a doc comment in the same style as the previous version notes. Two tests are added to the existing test/cli/run/transpiler-cache.test.ts covering file→stdin and stdin→file orderings of the same CommonJS .tsx bytes, asserting correct output and the expected cache-entry deltas.

Security risks

None. This is a cache-key derivation change over parser feature flags; no user input parsing, no auth/crypto/permissions, no network or filesystem trust boundaries are touched. The only observable effect is that previously-colliding cache entries now get distinct feature hashes, and old entries are invalidated by the version bump.

Level of scrutiny

Low. The change is mechanical and mirrors the established pattern in the same function (a list of feature bools hashed as bytes). REVIEW.md's "cache keys cover every input that shapes the output" and "any change to cached/serialized output bumps the format version constant" are exactly what this PR does. The [bool; 18] explicit length means a miscount would fail to compile, and bytemuck::cast_slice::<bool, u8> keeps the hashing byte-for-byte with no alignment or uninit concerns.

Other factors

Tests are placed in the correct existing file, reuse the file's own beforeEach temp/cache setup and toSpawn/newCacheCount helpers, pad past the 4 KiB MINIMUM_CACHE_SIZE with Buffer.alloc(...).toString() per repo convention, and the stdin helper uses await using with concurrent Promise.all draining of stdout/stderr/exited. The .tsx extension is deliberately chosen so the file run and the stdin run agree on the loader portion of the key, which is the only case that could collide. CODEOWNERS does not cover any of the changed paths, and there are no prior reviews or outstanding objections on the timeline. The PR description's note about overlapping open PRs (#40838, #40971, #38683) touching the same list/constant is a rebase-ordering concern for humans landing them, not a defect in this change.

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