Repository navigation
bake: consolidate the open robobun dev-server, HMR runtime, production build and router fixes - #39488
Jarred-Sumner wants to merge 178 commits into
Conversation
…h the VM (#39302, #38241) Replace the Global::crash() and fail_with_build_error exits in production.rs with a BakeBuildFailed error the command tail turns into exit code 1, name the output directory in the open failure, and route the success path through on_exit + global_exit so 'exit' handlers and process.exitCode apply to a finished build.
#38233) TranspilerSlot lives in bake/mod.rs so the dev server can share it.
Rust calls BakeLoadModuleByKey/BakeGetModuleNamespace/BakeGetDefaultExportFromModule/ BakeGetOnModuleNamespace through from_js_host_call; the module loader hooks release their throw scopes; BakeRegisterProductionChunk (no callers) is deleted.
…Path (#39092) Also drops the Windows skip on the out-of-bundle import exception-check test and gives the transpiler-leak tests the bake harness's production build budget.
…nal_value shims (#39150)
…ath buffer instead of panicking (#39196)
…ng options to be strings (#39188)
…pported route files (#39174)
…failure is inside try/catch (#37899)
…a client file The gate is the one from #39488: a production build only, `Loader::Css` only, the file namespace only. The dev server tracks a stylesheet per importing graph, and a plugin namespace can turn the same path text into anything. The shared stylesheet is always parsed as a client file. Before, the graph that registered it first parsed it, so the class names of a CSS module and the export condition of its `@import` differed between identical builds (2 outputs in 32 builds of one fixture). `css_target()` reads `framework`, which every bake transpiler has. The ParseTask condition is main's again: it picked the transpiler that `transpiler_for_target(task.known_target)` had already picked. Co-authored-by: Alistair Smith <hi@alistair.sh>
|
The output write item of this PR (from #39295) is re-cut on current main as #47194. It has the same |
|
The CSS target item of this PR (b1e8210, with the sharing gate of 6a23d31 and 5451190) is on current main in two PRs.
Neither PR has the shared source index. In #37051 that sharing failed two builds that pass without it: two routes that share a server component whose stylesheet has a local |
React takes any other return value of `write()` as a full buffer. It then waits for a "drain" event before it writes the next completed Suspense boundary. The two streams of the React framework never emit that event. - `StaticRscInjectionStream` (`bun build --app`): with react-dom 0.0.0-experimental-dc9aed36-20261027, a page with several large Suspense boundaries never gave a result. - `RscInjectionStream` (the dev server): a streamed response could stop after one Suspense boundary and never send the next one. The two lines and the dev server test are from #39488. In `production.test.ts`: - The React cases run one at a time, and `build()` kills a build that does not exit in 60 s. - The `getStaticPaths` case that throws runs on a react-dom that does not call `onAllReady` after a shell error. - A new case builds a page with large Suspense boundaries on a react-dom that waits for "drain". - The case for a server component that throws inside `<Suspense>` is gone. That page keeps its fallback, so a test must not call it a recovery. Co-authored-by: Alistair Smith <hi@alistair.sh>
|
The prerender failure item of this PR (80892c6, aeed1f2, 97db92b, and the #47356 has the
The notes of #47356 ask which result a maintainer wants for each of the two. After #47356, the |
Consolidates the open robobun Bake PRs from Aug 12–17 (71 of them) into one branch, re-thought per theme so that each root cause is fixed once instead of once per symptom. Every PR was checked against current main, its review threads read, and its fix either taken, reworked, or deferred; the details per PR are in the commit that carries its number.
What changed, by theme
HMR runtime (
hmr-module.ts) — one module-loading state machine:Pending/Stale/Loaded/Errorplus a per-modulegenerationand an in-flightloading {promise, asyncId};beginEvaluationis the only Stale→Pending point; every failure path goes throughthrowLoadFailure;require()throws a fresh Error instead of mutating the loader's;dynamicImportis async and theimport(id, opts)fallback returns its promise;replaceModulesre-evaluates the modules between a changed module and its accepting boundary (Vite semantics), runs dispose/accept once per module per update, lets failed roots load again, and settles all in-flight reloads before it rejects;bun:wrapexports the wholeruntime.bunhelper namespace (decorators work in dev);vite:*event names are aliased. The server runtime observes the update promise instead of dying on an unhandled rejection.Dev server incremental graph / failure bookkeeping — three invariants implemented once: (1)
File.failed ⇔ bundling_failures contains it, and every flip is published (IncrementalGraph::retract_failure, used on demotion, deletion and re-bundle); (2) a file's parts are emptied only when a failure is recorded on a node reachable from its importers, and a failed file never gets a chunk (single CSS-root discovery loop; failed-root removals are final; plugin fall-through inside try/catch keeps the module bundled; an onLoad plugin that does not answer fails the route; a "use client" file whose import fails keeps its route failed); (3)check_route_failurestraces into a scratch list and re-bundles a failing route once per file change instead of on every request. Also: CSS roots get edges to every import and stylesheets are served in source order; a framework route'smeta.stylesfollows its imports; a changed file is re-bundled with the loader it was bundled with (text-imported HTML no longer panics); the first hot-reload event of a batch is processed once;server.reload()reuses an HTML file's route bundle;relative_pathhandles a root of/; parse-opened fds the watcher declines are closed; a failed onLoad releases its request before giving the scan unit back.Dev server lifetime —
Box<DevServer>is fully initialized before anything fallible runs andDropis the single teardown path (init failures no longer leak, and a never-started watcher closes its fd); the app plugin cell is an RAIIOwnedPlugin; a pending[serve.static]plugin load holds a pending request sostop()cannot free the server under it; deferred requests are unlinked inDrop; JS-thread bundle allocations go into the bundle's arena through an RAII scope guard. The memory-visualizer timer is re-armed from its tick handler (it corrupted the timer heap on canary); the HMR socket unsubscribes topics dropped by a re-subscribe; the incremental/memory visualizer pages and graph feed are restored (bitset, one serializer), with.bake-debugsource dumps behindBUN_BAKE_DUMP_SOURCES=1. The error-overlay wire format handles line-only and column-less locations.bun build --appfailure/exit model —build_with_vmnever exits the process; it returnsOk,JSError, or the newError::BakeBuildFailed("already printed");build_commandhas one tail that sets the exit code and exits throughon_exit()+global_exit()for success too, soprocess.on("exit")andprocess.exitCodework; a bundlerBuildFailedis converted at the bundle call (log printed, exit 1, no "internal error"); an unwritable output directory or file fails the build before prerendering; route scan errors (aliases, unsupported files) fail the build; allGlobal::crash()sites are gone. The production transpilers and framework projection are dropped (TranspilerSlot); the C++ module helpers check exceptions, takeJSC::JSGlobalObject*, and release the strings production.rs hands them;BakeProdResolvehandles Windows disk paths.Production linker — AstBuilder modules get a real
exports_ref/wrapper_ref(namespace/require()of a "use client" proxy orbun:bake/serverworks); server-component boundaries are redirected per importer side, so client-side imports of a "use client" file bind to its client build; route classification runs beforeimport()records are cleared and uses one reverse BFS from the client proxies (cycle-safe; skipped in dev), soimport()of a "use client" module works in production; routes with layouts prerender and stay static only if every layout is.Router / config — one route-matching contract:
[x]matches exactly one non-empty segment,[...x]one or more,[[...x]]zero or more, and among matching dynamic patterns the winner is by Next.js precedence (text < param < catch-all < optional catch-all; same rule #39291 givesBun.FileSystemRouter). Scan/config errors are reported instead of panicking (functionstyle, over-long roots and entries, aliased/unsupported route files,ignoreDirsactually honored,pluginsmust be arrays);app.rootand router roots go through one checked directory resolver; the localget_optional_valueshims are replaced byJSValue::get_optional::<JSValue>.Module ids / paths — dev-server module ids are relative to the dev server root (
app.rootor the cwd at creation), soapp.root != cwdandprocess.chdir()afterBun.servework; over-long unresolvable imports and root-absolute<script src>are reported instead of panicking.Found while consolidating —
Path::dupe_allocputtextin the per-bundle arena whenprettywas disjoint from it (everyssr:path); the watcher borrows that slice, so a hot reload after the bundle was freed read freed memory (intermittent ASAN BUS on the watcher thread, present on main).Tests
countBundles,watchSynchronizationMessagesFor,expectErrorPage,expectElemText.bundle.test.tsandcss.test.tsshare dev servers and assert served output.test/bake/framework-router.test.tscovers matching viaFrameworkRouter.match()frombun:internal-for-testing;test/bake/app-options.test.tsandtest/bake/dev/route-matching.test.tsare new;production.test.tsnow covers failure and exit paths and runs underBUN_JSC_validateExceptionChecks.bundle,css,productionstay listed for one CI cycle (css needs watcher: free the owned path of evicted watchlist entries #39197).test/bake+bun-serve-html+BunFrontendDevServer+ related bundler files: 758 pass, 0 fail apart from the pre-existing--compiletimeouts incli.test.tsand an occasional flake of the pre-existingdeinitializationfixture (stop()thenfetchraces a 250 ms timer; unchanged here);rust:check-allclean.Behaviour changes
bun build --apprunsprocess.on("exit")handlers and exits withprocess.exitCode; an unwritable output or a route scan error exits 1 without prerendering.pages/[user]/posts.tsxnow matches/joe/posts(1.3 only matched/joe/posts/); one trailing/is ignored for every route (static ones too, likeBun.FileSystemRouter);pages/[...slug].tsxstops matching/anddocs/[...rest].tsxstops matching/docs; missing trailing params (/a/bagainst[c]/[y]/[s]) stop matching instead of binding empty strings;//inside a catch-all no longer matches; overlapping dynamic routes resolve by Next.js precedence, not directory-scan order; requests are matched on the pathname (query string ignored);fileSystemRouterTypes[n].rootmay live outsideapp.root;ignoreDirsactually applies (main iterated the wrong array) and replaces the defaults; colliding routes failbun build --app; non-arraypluginsis an error.server.reload()instead of on every request; a file's import attribute (with { type }) is honoured on rebundle.import.meta.hotdocs promise). Concretely forbun ./index.htmlReact apps: editing a leaf component re-runs the modules that import it up to the nearestimport.meta.hot.acceptboundary (in React apps that is the Fast Refresh boundary, so component modules only), and module-level state in intermediate non-component files (store.ts,context.tsstyle singletons that are not accepted themselves) is re-created on such an edit where 1.3 kept the old instance and only patched bindings. Modules that callimport.meta.hot.accept()/hot.datakeep their state as before. An unaccepted client update reloads the page without running dispose first. This wants a docs note.server.reload()with an unchanged HTML route serves the cached bundle.style: () => …infileSystemRouterTypesis rejected withERR_INVALID_ARG_TYPE(it always panicked);app.root,plugins[n].name,serverRuntimeImportSource,serverRegisterClientReferenceExportmust be strings..bake-debugdumps requireBUN_BAKE_DUMP_SOURCES=1.Deferred (own PRs)
#38714 (UTF-8 module text; part of a stack — rebases in one line), #38617 (crash-dump; touches four non-bake crates), #37872 (overlay underline; needs #37848), #39197 (watcher eviction leak).
Supersedes
#37120 #37727 #37734 #37742 #37759 #37765 #37773 #37779 #37785 #37786 #37794 #37795 #37827 #37833 #37837 #37838 #37844 #37845 #37850 #37859 #37862 #37863 #37866 #37867 #37868 #37871 #37878 #37885 #37886 #37888 #37890 #37892 #37897 #37899 #37902 #37929 #37936 #38004 #38041 #38227 #38233 #38234 #38238 #38241 #38252 #38323 #38336 #38340 #38664 #38737 #38902 #38949 #38960 #38973 #39050 #39092 #39150 #39174 #39188 #39196 #39202 #39203 #39209 #39293 #39295 #39301 #39302 #39315 #39344 #39432 #39447 (and #38462)
Fixes #22533
Fixes #23299
Fixes #24893
Fixes #28117
Fixes #30488