Repository navigation
Conversation
|
Status Reproduced on canary 1.4.3-canary.1+09bb54630 with a framework app (
With the React framework, a component with a license banner before The new tests fail on the base branch (#42826) and pass with the debug build of this branch: bun bd test test/bake/use-directive.test.ts
bun bd test test/bake/dev/bundle.test.ts -t "use directive|use server"
bun bd test test/js/bun/transpiler/source-too-large.test.ts -t "use directive"PR: #42959 |
|
Updated 3:05 PM PT - Sep 29th, 2026
✅ @robobun, your commit c39774be1e6eb119600d77850fe08301887aa6d6 passed in 🧪 To try this PR locally: bunx bun-pr 42959That installs a local version of the PR into your bun-42959 --bun |
There was a problem hiding this comment.
Beyond the inline findings, I also checked the bun:internal-for-testing scanUseDirective binding (owned Source + local Arena, nothing arena-allocated owns heap memory, so no leak on the test path), the lexer-error-after-a-valid-directive case in scan_use_directive (returns None and the subsequent real parse reports the syntax error, so nothing is silently swallowed), and the Source hoist in parse_worker (same arena, same borrow of entry.contents, only moved earlier — no behavior change).
Extended reasoning...
The inline findings cover the behavioral widening (newly detected client boundaries hitting existing todo_panic!/CommonJS-proxy gaps) and the continues_expression table that must track parse_suffix. Separately I read the new scanner, the ParseTask.rs hoist of the Source allocation, and the test-only host function in JSBundler.rs: the host function builds an owned Source from to_utf8 and a fresh Arena that drops normally, the Log in the scanner is a plain local, and the hoisted Source construction is byte-identical to the deleted block with the same bump arena, so those paths do not add new memory or correctness risk. The ("use client") / "use client" as any divergence from the parser is called out in the PR notes as intentional spec alignment and is a design call for a human, not a defect in the scanner logic.
Findings marked 🟡 are optional suggestions and need no follow-up push.
531a06a to
afc53e8
Compare
afc53e8 to
5afa20e
Compare
|
Review follow-up for 5afa20e. Each thread has its own reply.
|
5afa20e to
bb23eaf
Compare
…shbang The scan that runs before the parse compared the first bytes of the source with the quoted directive. A comment or a hashbang before "use client" hid the directive, and the file was bundled as a plain server module. A template literal, or a string that starts a larger expression, counted as a directive. scan_use_directive runs the lexer over the start of the file. The directive has to be the first statement, and the string literal has to be the whole statement. The scan returns the range of the literal, so the "use server" error points at the directive and not at the start of the file. A use directive after "use strict" still does not count. That is the shape compilers emit for CommonJS, and the client reference proxy cannot list CommonJS exports. `bun build --server-components` sets no framework. A "use client" module is a plain module there, so the modules that the scan now finds do not reach the abort that one with the directive on the first line reached. bun:internal-for-testing gets scanUseDirective for a table test of the rules.
bb23eaf to
c39774b
Compare
|
Update for c39774b.
The PR body has the measured tables and the costs. |
Problem
"use client"makes the module a plain server module. React builds then fail:"useState" is not available in a server component.UseDirective::parse(src/ast/use_directive.rs:19) trims white space, then compares 12 bytes. A template literal and"use client".lengthalso match.Fix
scan_use_directiveruns the lexer over the start of the file. The directive must be the whole first statement."use server"error from bundler: report a "use server" module as a build error instead of aborting #42826 (this PR's base) points at the directive.UseDirective::parseandrangeare deleted.bun build --server-components),"use client"is now a plain module, so newly found files do not reach the first-line abort.test/bake/use-directive.test.ts(new) and 3 devTests inbundle.test.tsfail on the base. Self-reviewed: 19 concerns raised, 15 addressed (Notes).Background
S::Directive(too late: the target is picked first).Downsides
"use server"file is now a build error.`use client`and"use client".lengthstop counting.export *or CommonJS exports givesundefinedserver exports, like its twin without the comment.Notes
Origin. No user reported this. It came from the review of #42826, which keeps the byte prefix match and leaves this change to a separate PR. Alone on main, this change makes a
"use server"file under a banner go from "bundled as a plain module" to thetodo_panic!abort. #42826 turns that abort into a build error, so this PR sits on top of it.What changes for a file (framework with server components)
// commentor/* comment */, then"use client"#!/usr/bin/env bun, then"use client""use client""use server""use client"or"use server", with no byte after it`use client`"use client".length,"use client" + x,"use client"then.lengthon the next line;"use client""use strict";then"use client";A file read from disk loses its BOM in the reader. A source from a plugin's
onLoadkeeps it."use strict"then"use client"stays a server module on purpose. It is the shape that compilers emit for CommonJS. WithseparateSSRGraph: truethe client reference proxy lists only ESM exports, so a CommonJS client module givesundefinedexports on the server. A CommonJS file that starts with"use client"already behaves that way. Detection of the"use strict"shape would turn a working server module from a compiled package intoundefinedexports. The scan can walk the whole prologue when the proxy can list CommonJS exports. React's reference loader and Next.js accept the directive after"use strict".bun build --server-componentswith no framework (bun build --server-components --target=bun entry.ts,entry.tsimportsComp.ts, release builds):Comp.ts"use client";on line 1"use client";"use client""use server";on line 1"use server";, or only the 12 bytesRows two and three are the reason for the guard in
ParseTask.rs. Without it they abort on this branch (called `Option::unwrap()` on a `None` value). 5c5e077 on #42826 reverted a change to the old scan for the same reason. The guard also ends the abort of row one. #38046 (open) makes a directive with no framework a build error, because the bundle does not do what the directive asks for. This PR keeps such a build working, because these files build today. The choice is one place, thefilteron the scan result inParseTask.rs. If #38046 lands after this PR, it replaces the guard.A newly found file takes the same paths as the same file without the comment. That includes the paths that do not work today. I measured these with
separateSSRGraph: true(typeof Buttonon the server):"use client"on line 1, canary"use client",export { Button } from "./Button""use client",export * from "./Button"(Button.tshas its own directive)"use client",exports.Button = ...(CommonJS)The last two rows are the proxy gap: it lists
named_exportsonly, soexport *and CommonJS exports are missing. A build error for every such module is not right: server code that uses only the named exports of a client barrel works today, and the client graph handlesexport *. The fix belongs in the proxy (passexport *through, list CommonJS exports).Other paths that a client module can reach today:
todo_panic!("unique_key here")inwrap_value_for_server_component_reference(production build,separateSSRGraph: false) andtodo_panic!("separate_ssr_graph=false")inbundle_v2.rs. bake: do not abort on a "use client" module without a separate SSR graph #42885 (also stacked on bundler: report a "use server" module as a build error instead of aborting #42826) covers this mode: the parser no longer reachesunique_key here, and a production build fails at the directive with a build error. It callsUseDirective::range, which this PR deletes, so the second of the two to land needs a rebase.finalize_bundlewith three or more client components in one bundle (bake: consolidate the open robobun dev-server, HMR runtime, production build and router fixes #39488 carries the fix). The devTest gives each fixture its own route for that reason.The
useStateerror has a second cause that this PR does not touch: a module with no directive that a client component imports is parsed for the SSR graph in a server mode.Where the scan and the parser differ. The parser is more lenient than the specification in three places, and the scan follows the specification (as TypeScript, Next.js and React's reference loader do):
("use client"),"use client" as any, and a directive after a type-only statement such astype A = 1;. The directive text is compared raw, so"use\x20client"is not a use directive. React's loader compares the raw text too.A source over 2 GiB. Every parser rejects one by length before it reads it (
Source::check_parseable_len), because positions arei32. The scan does the same and leaves the error to the parser. Without the check, a source that opens a comment and never closes it ends inpanic: int cast: TryFromIntError(PosOverflow).test/js/bun/transpiler/source-too-large.test.tshas the case.Tests.
test/bake/use-directive.test.tschecks 236 sources through the newscanUseDirectivehook inbun:internal-for-testing, and one build with no framework.continues_expression_after_line_breaksits next toparse_suffixand lists the arms of its match minus the three that stop at a line break (++,--, and the TypeScript!). The table test checks each of them, with LF and with CRLF. An earlier byte-level version of the same statement rule agreed with acorn'sdirectivefield on 200,000 random module sources (0 mismatches).Cost, measured. Release builds of the base (49bc7cf) and of this branch (c39774b), same build tree,
size bun-profile:bunfilePer process: one host function for the testing hook. Per parsed file with server components off: nothing, the scan does not run. With server components on and a JavaScript loader: the lexer reads the leading white space and comments, then one token, and a second token when the first is a use directive. No syscall and no file read. From the code, the scan does not allocate for an ordinary source. The lexer allocates when a leading comment starts with
/*!or//!(it keeps a list of those) and when a token needs decoding (an identifier with an escape).Time, release build. The scan alone through the testing hook: 2.1 µs for a 64-byte file with no comment, 3.2 µs for a file with a 1 KB banner. Those numbers include the call from JavaScript, a copy of the source and a new arena, which the bundler does not pay. A build of 2,000 modules with a 1 KB banner each, 15 interleaved runs for each binary, user plus system CPU time: base median 117 ms (96 to 137), this branch median 114 ms (99 to 149). The difference is inside the noise. Instruction counts are not measured:
valgrindandperfare not installed on the build machine.Suites run with the debug build:
test/bake/use-directive.test.ts,test/js/bun/transpiler/source-too-large.test.ts,test/bake/dev/bundle.test.ts,production.test.ts,ecosystem.test.ts,plugins.test.tsandresponse-to-bake-response.test.tsintest/bake/dev,test/bake/app-options.test.ts,test/internal/source-lints.Self-review items not done here
export *). See above for why it is not a blanket build error.export *support in the client reference proxy.[human-review] gate passed · iteration 5 · 10 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 2 rejected · iteration 5
evidence per changed file