Repository navigation
js_parser: run an ambiguous file with a label-body or if-body function declaration as CommonJS - #44803
Draft
robobun wants to merge 4 commits into
Conversation
Collaborator
Author
|
Status: draft, stacked on #44741. Reproduction ( foo:
function bar() { return "bar"; }
console.log(bar()); // node: bar, bun: ReferenceError: bar is not defined
|
…e makes an ambiguous file CommonJS
…ommonjs-no-export.test.ts
robobun
force-pushed
the
robobun/e7f32f41/label-if-fn-decl-commonjs
branch
from
October 14, 2026 03:25
1c139c3 to
66d4ff7
Compare
Collaborator
Author
|
Updated 2:04 AM PT - Oct 23rd, 2026
✅ @robobun, your commit 98b6f2f4166dc7a5d0b83a3d1abe5352743eddf7 passed in 🧪 To try this PR locally: bunx bun-pr 44803That installs a local version of the PR into your bun-44803 --bun |
…block-fn-runtime' into robobun/e7f32f41/label-if-fn-decl-commonjs # Conflicts: # src/jsc/RuntimeTranspilerCache.rs
…block-fn-runtime' into robobun/e7f32f41/label-if-fn-decl-commonjs # Conflicts: # src/jsc/RuntimeTranspilerCache.rs
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #25737
Stacked on #44741.
Problem
index.jsof function in label isn't accessible #25737 throwsReferenceError: bar is not defined. bun runs the file as a strict ES module, so the function stays block scoped (js_parser: keep a sloppy block-level function as a declaration when no renamer runs #44741). Node.js runs it as CommonJS.ModuleType::Unknownarm (src/js_parser/parse/parse_entry.rs:2065) defaults to ESM. A label-body orif-body function declaration was not a CommonJS hint.Fix
P::has_sloppy_only_syntaxfor either form in non-strict code, and theUnknownarm reads it. Runtime only, cache version 45."use strict"scope and animportkeep the flag off.test/cli/run/commonjs-no-export.test.ts(20 new tests, 12 fail on js_parser: keep a sloppy block-level function as a declaration when no renamer runs #44741 alone). Self-reviewed: 12 concerns raised, 12 addressed.Background
require,module,exports,with(Fix bug wherewithstatements do not trigger CommonJS #7167), top-level"use strict"(Make"use strict"become CommonJS if we don't know whether it's ESM or CJS #15868).importstrictness) and thewithcondition (importplus the form becomes an error).Downsides
Bun.pluginsource with the form (Bun.plugin: run CommonJScontentsfrom onLoad() and build.module() as CommonJS #34112).test.failingrows record them."use strict"(transpiler: preserve function-body "use strict" in CommonJS #31807)..jsxand imported.mjsfiles flip too.if (c) { function f(){} }and a read before the label stay wrong against Node. Two label shapes from js_parser: keep a sloppy block-level function as a declaration when no renamer runs #44741 become wrong. Other files: +2 to +5 instructions per parse.Notes
The lines that change the behaviour
src/js_parser/p.rs: the fieldhas_sloppy_only_syntaxand the settermark_sloppy_only_syntax. The setter writes only whenoptions.features.commonjs_at_runtimeis true andis_strict_mode()is false.src/js_parser/visit/mod.rs(visit_single_stmt, theif has_if_scopeblock): the body ofifand ofelse.src/js_parser/visit/visit_stmt.rs(s_label, a newStmtData::SFunctionarm): the body of a label.src/js_parser/parse/parse_entry.rs:|| p.has_sloppy_only_syntax, the last operand of the hint condition in theModuleType::Unknownarm. Thehas_with_scopearm does not change, soimportplus the form still loads.src/jsc/RuntimeTranspilerCache.rs:EXPECTED_VERSION44 to 45. An older entry stores the ESM kind. main has 43 and js_parser: keep a sloppy block-level function as a declaration when no renamer runs #44741 takes 44.Against node v26.3.0
Files with no module syntax. Debug builds, transpiler cache off.
index.jsof #25737 (entry,require(),import)barbarif (true) function f(){}thenf()function ffunction felse function g(){}theng()function gfunction g{ l: function h(){} }thenh()hhthisin a plain call, file with the label formif (true) { function k(){} }thentypeof kfunctionundefinedundefinedundefinedtypeof barbeforefoo: function bar(){}functionundefinedundefinedundefinedplain,Vplain,Vplain,Vlabelled,labelledThe route matrix (entry,
.ts,require(),import, default import,-e, stdin, abun testfile, and the strict contexts): 13 rows change against #44741 alone and all 13 now equal node. 11 go from strict to sloppy and 2 go from ReferenceError tobarandfunction f. 18 rows do not change. 2 runs per binary.What stays an ES module
Node rejects each of these files. bun runs them as ES modules, as on main.
import,exportor top-levelawait."use strict"directive or in a class body..jsor.tsfile in a package with"type": "module", and a.mjsentry point.The loader reports no module type for some files that have one in Node. There the form makes the file CommonJS, as
__dirnameand the other hints do on main. Measured: a.jsxor.tsxfile and a file with no extension in a"type": "module"package (src/runtime/jsc_hooks.rslets the file contents decide for them), and an imported.mjswith no package type. 6 of 20 rows of that matrix change. #40940 is open for the loader. One test pins that the form and__dirnamegive the same kind for these files.Sources that stop loading (7 probes: 7 run on main and on #44741 alone, 0 run on this branch)
.jsx, a legacy decorator in.ts, a standard decorator in.js, anaccessorfield in.js:SyntaxError: Unexpected token '{'. import call expects one or two arguments.A CommonJS file cannot take an import that the parser generates (Support import mod = require() in runtime #12812). The same file with"use strict"orwithfails in the same way on main. Bind parser-generated names with require() in CommonJS-wrapped modules, ahead of hoisted classes #38012 and Decide a file's module type with one rule on every runtime load path #40940 are open for it.Bun.pluginonLoadsource throughimportand throughrequire(), and abuild.module()source: the file does not run, with exit code 0 and no error. The module loader evaluates a plugin source as an ES module (Bun.plugin: run CommonJScontentsfrom onLoad() and build.module() as CommonJS #34112).What a flipped file gets from the CommonJS path
"use strict": the parser does not print it in a CommonJS file (transpiler: preserve function-body "use strict" in CommonJS #31807). 1 of 14 probe lines differ from node before, 11 of 14 after.Measurements (release builds of main at 5749c31 and of this change on it, taken before the restack. The code lines are the same. The comments are shorter and the cache version is 45.)
.text: +0 B bysize -A(65,618,773 B in both). The sum of text symbol sizes is +89 B bynm -S:_parsex2 +47 / +20 B,visit_single_stmtx2 +31 / +29 B,s_labelx2 +36 / +40 B,P::initx5 -15 B each,parser_snapshot-39 B.bloatyis not installed in the container._parse(2 for the new operand, 3 register reloads that the compiler duplicated), +2 in the TypeScript copy. Per label whose body is not a loop: +2 instructions. Every other path: +0 (objdump).sizeof(P): 4288 -> 4288 bytes (5 instantiations).ParserSnapshot: 568 -> 568 bytes. Allocations added: 0 (DWARF, debug builds).Not in this change
if (c) { function f(){} }and the other block-level functions are valid strict code, so they are not a hint. In a file with no CommonJS marker the function stays block scoped, as js_parser: keep a sloppy block-level function as a declaration when no renamer runs #44741 says.iforelse, or a label in a block. Those need the CommonJS kind.delete xor a strict-mode reserved word as a name does not load #44802:deleteof a plain name, and a strict-mode reserved word,evalorargumentsas a name. bun prints them and JavaScriptCore rejects the file as a module. Each can call the same setter where the visit pass meets it.returnis top-levelreturnshould hint to the transpiler that a module is commonjs / bundler can emit top-level return #8908 (PR js_parser: make the ESM/CJS classification and the module/exports bindings agree #40840).varof the same name is a different signal: module code rejects it and strict CommonJS accepts it.has_top_level_function_merged_with_varalready records it for the bundler. A runtime read needs its own operand."\1",eval = 1andvar await: the parser of bun rejects them in every kind (PR parser: report strict-mode early errors and accept legacy sloppy syntax #40827).Tests
test.failingrows.commonjs_at_runtimegate: thebun buildtest fails. No strictness test: the strict-context test fails. Dead branches skipped: the dead-branch row fails. The setter called for every label body and every single-statement body: the test for files without the two forms fails.commonjs-no-export.test.ts21 pass,runtime-transpiler.test.ts15 pass and 1 skip,preserve-use-strict-cjs.test.ts18 pass,transpiler.test.js272 pass,repl-transform.test.ts38 pass.test/bundler/transpiler/,test/cli/run/,test/js/bun/resolve/,test/js/node/module/,test/js/bun/plugin/. The only failures also fail on the base build: the 2 GiB printer test, 2 FUSE tests and 2require-cachetimeouts.[human-review] gate passed · iteration 1 · 6 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 3 passed · 1 rejected · iteration 1
evidence per changed file