Repository navigation
Conversation
…ults of its chunk A JS chunk prints the class names of a stylesheet that it uses. The stylesheet is not a file of the chunk, so the byte counter of the print worker had no key for it, and the metafile record of the JS output did not list it. The metafile now adds up the compile results of the chunk. The files of the chunk keep their place and their order. A source that is not one of them gets a record after them when the chunk has code of it. The print worker no longer counts, so files_with_parts_in_chunk is a set again.
… the order of the stylesheet records The comment on the filter named a difference from esbuild that the filter does not cause. The filter is for a wrapped stylesheet whose wrapper the chunk drops. New tests: the order of two stylesheet records, the wrapper that the chunk prints for a bare import in a wrapped file, and the 0-byte record of a file of the chunk with and without --splitting.
|
Updated 5:18 PM PT - Oct 30th, 2026
❌ @robobun, your commit e2ea6f9 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 47503That installs a local version of the PR into your bun-47503 --bun |
|
Status: ready for review. The diff is green in CI. Reproduced on the base, #47453 at 37777f6, and on main: 20 of the 25 new tests in The one red test of the CI run is This PR is stacked on #47453. |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline finding, I checked the two behaviors the deleted worker counter had that the new chunk_inputs no longer spells out: the runtime source is still dropped from the record by the existing Index::RUNTIME filter in generate_chunk_json (MetafileBuilder.rs:130), and a PrintResult::Err still counts as 0 bytes because CompileResult::code() returns an empty slice for it. The remaining readers of files_with_parts_in_chunk (findAllImportedPartsInJSOrder, computeCrossChunkDependencies, generateChunksInParallel) only use keys/contains/count, so the () value type affects none of them.
Extended reasoning...
The change replaces the atomic per-source byte counters in the JS codegen workers with a post-codegen sum over each JS chunk's compile results in MetafileBuilder.rs, turns files_with_parts_in_chunk into a plain set, updates the Send/Sync and prologue CONCURRENCY comments accordingly, and adds ~340 lines of metafile tests. It touches no security-sensitive surface. A confirmed inline finding on the bytes > 0 filter and the resulting esbuild divergence (no 0-byte stylesheet record for a bare CSS import) is a product decision the author explicitly asks a maintainer to settle, so a human look is needed.
|
Reply to the review: its one finding is the 0-byte record for a stylesheet that the JS output prints nothing of. That is the open question in the Notes of the description, so the code stays as it is until a maintainer answers. I ran the suggested source, |
|
A note on the merge order with #47624. That PR makes
|
Stacked on #47453.
Problem
import styles from "./a.module.css",outputs["./entry.js"].inputsis{"entry.js":{"bytesInOutput":35}}, and 48 of 112 bytes are in no record.Chunk.files_with_parts_in_chunk(generateCompileResultForJSChunk.rs:145). A stylesheet has no key there (computeChunks.rs:308).Fix
chunk_inputs(MetafileBuilder.rs) adds up the compile results of a JS chunk, as esbuild does. Each has the source and code of one printed piece.files_with_parts_in_chunkis a set again, as before feat(bundler): add metafile support matching esbuild format #25842.test/bundler/metafile.test.ts(25 new tests, 20 fail on bundler: write the metafile record of a CSS output from the compile results of its chunk #47453), 16 suites. Self-reviewed: 17 concerns raised, 16 addressed. Not done: a new tracker for the bugs of bundler: pull the runtime into chunks that print a CSS file's namespace object #38286, bundler: emit the module.exports body of a require()d CSS file #38307 and bundler: keep assets referenced by stylesheets and HTML documents out of JS chunks #38337.Background
--metafile) is esbuild's JSON build report. Each output lists its inputs withbytesInOutput.Downsides
--metafile-md.--metafilewith 1,000 CSS modules takes +0.87% instructions..textis equal.Notes
Where this comes from
bun.d.ts:4798documents the field as "Map of input files to their contribution in this output". bundler: report the emitted size of each chunk as metafile outputs[..].bytes #42760 fixedbytesagainst its doc line in the same way.#47453 also says: "Not in a PR: the counter of the JS worker. [...] The metafile can add up the same number from the compile results, as it does for CSS here. I have that as a commit [...] A one-line gate on the metafile option in the worker is the other way. Nothing asks for either, so it is not in a PR."
The missing record now asks for the first of the two. The gate keeps the lookup that finds no key for a stylesheet, so it cannot write the record. With the sums taken from the compile results, nothing reads the counters. So they go, and a build with no metafile no longer pays for the metafile.
Question for a maintainer: the 0-byte record
esbuild writes
"style.css": {"bytesInOutput": 0}in a JS output for a stylesheet that the output imports and prints nothing of. That is a bareimport "./style.css", or a binding that nothing reads. Should Bun write it too?--compile --aotmetafile writer (src/bundler/aot/metafile.rs) lists each input with no filter.bytes > 0filter inchunk_inputsis not what leaves the record out. A bare import gives the chunk no compile result for the stylesheet. The filter acts in one case: a wrapped stylesheet whose wrapper the chunk drops. In that case esbuild keeps the wrapper and lists 139 bytes.js.files_in_chunk_order. For a bare import it is[a.module.css, bare.js](from a run with a temporary print).[page.css, plain.js, plain.html]. So the rule needs a check of which file imports the stylesheet.bun initReact templates have one.Records of the JS output (
bun build --metafile, and esbuild 0.21.5 with--bundle --format=esm)import styles from "./a.module.css", readsstyles.fooentry.js35entry.js35,a.module.css48a.module.css36,entry.js28import { foo } from "./a.module.css"entry.js18entry.js18,a.module.css24a.module.css19,entry.js18import "./a.module.css"entry.js22entry.js22a.module.css0,entry.js22import("./route.js")with no--splitting, whereroute.jshasimport "./x.css"route.js68,entry.js82x.css23x.css43,route.js171,entry.js95--splitting:one.js,two.jsand their sharedshared.jsreadstyles.fooone.js50.shared.js41.two.js50a.module.css48 in each of the 3 outputsone.js43.two.js43.a.module.css36,shared.js34import "./style.css", wherestyle.csshasurl("./big.png")big.png0,entry.js22style.css0,entry.js22require()of a stylesheet of a package with no side effectsentry.js22entry.js22a.module.css139,own.css0,entry.js59In the fourth row
route.jsis a wrapped file. So the chunk prints a wrapper for the stylesheet (var init_x = () => {}and a semicolon), and the stylesheet has a record. A bare import can give a record.Size
100 components, and each one reads 20 class names of its own CSS module:
entry.jsis 241,917 bytes. On bundler: write the metafile record of a CSS output from the compile results of its chunk #47453, 63,700 of them (26.3%) are in no record. With this PR 3,010 are: path comments and blank lines.--minify, 50,668 of 163,380 bytes (31.0%) are in no record on bundler: write the metafile record of a CSS output from the compile results of its chunk #47453, and 19 with this PR.--metafile-mdand scripts that readbytesInOutput.What stays as on #47453
These rules came with the counters of #25842. This PR does not decide them.
--splitting, each live file that the entry point reaches is a file of the chunk. With--splitting, only a file with a compile result is one. So a file that only re-exports, or an empty file, keeps its 0-byte record. A file that only imports, or that tree shaking leaves nothing of, has none.bytesInOutputcounts the placeholder of an asset path, not the final path. esbuild counts the final path.inputs: {}.Weighed and not done: one rule for the three kinds of chunk, as esbuild has. That rule adds up the compile results in print order. It moves the records of today.
Bugs in what a JS chunk prints for a stylesheet
They are not in this PR. The metafile counts that code as it is, and seven of the new tests build such code.
import * as s from "./a.module.css": the chunk calls__exportand does not have it. The bundle throwsReferenceError: __export is not defined. bundler: pull the runtime into chunks that print a CSS file's namespace object #38286 fixes it.require("./a.module.css"), andimport()with no--splitting: the wrapper is empty, sostyles.fooisundefined. bundler: emit the module.exports body of a require()d CSS file #38307 is the open fix. In ESM output with no other use of the runtime, the bundle throwsReferenceError: __commonJS is not defined.files_with_parts_in_chunk. It does not see this code either (bundler: pull the runtime into chunks that print a CSS file's namespace object #38286).No issue holds these bugs. The three PRs hold them. They are open since August.
Deleted
AtomicUsizevalues of the map.// TODO: metafilemarker in the join loop ofpostProcessJSChunk.rs. esbuild collects this list there.chunk_inputsreads the same compile results.Numbers (release builds with LTO, of #47453 at 37777f6 and of this PR)
lock add(gdb hit counts). The worker of bundler: write the metafile record of a CSS output from the compile results of its chunk #47453 is the one of main.objdumpshows nolockinstruction in the worker of this PR.--metafile:chunk_inputsdoes the 2,001 lookups.compute_chunks, 2,001 modules: 138 -> 119 allocator calls (-32,736 bytes asked for).metafile_builder::generatewith--metafile: 42,052 calls in both for 2,001 modules, and 88 -> 98 for the example with one stylesheet (gdb)..text108,799,721 bytes and.rodata26,448,844 bytes in both. JS print worker 2,112 -> 1,718 bytes (427 -> 328 instructions).metafile_builder::generate10,873 -> 12,081 bytes.compute_chunks45,711 -> 45,598 bytes.files_with_parts_in_chunk: 16 -> 8 bytes per key.sizeof(Chunk)792 -> 792.--metafile-0.04% (194,912,374 -> 194,837,670). The spread of the runs is 0.22% to 0.37%, so both are inside it.--metafile+0.87% (160,617,099 -> 162,017,040). That build writes 1,000 new records.lock add.How the numbers were taken:
perf_event_open), with 2 pool threads.sizeof(Chunk)is the size of the chunk array of a build with one chunk.size -Aandnm -S.Records that change
23 builds with both binaries. 16 have no stylesheet code in a JS chunk: JS only,
--splitting,--format=cjs,--minify, stylesheet entry points,--css-chunking, HTML pages. In 7 a JS chunk prints code of a stylesheet.Tests
20 of the 25 new tests fail on #47453:
--splitting(2)--splitting(2)--metafile-md5 pass on #47453 and pin what must stay:
bytes > 0filter.--splitting(2)Suites run with the debug build
metafile104 pass. All pass:esbuild/metafile12,aot/cli/metafile4,css/css-modules46,esbuild/css68,esbuild/splitting26,bundler_html26,html-import-manifest10,bundler_compile_splitting30,bundler_loader62,bundler_bun22,bundler_naming27,bundler_splitting209,esbuild/default155,bundler_plugin68,bundler_html_server4,bake/dev-and-prod12.Self-review
17 concerns. 16 addressed:
--splitting.--splitting, which was falseNot done (1): a new tracker for those bugs. The three open PRs hold them, and #42758 was closed because no user hit what it listed.
Other open PRs on these lines (trial merges)
MetafileBuilder.rs. This diff adds no file to those conflicts. The loop of bundler: give each metafile record of a build with two graphs its own key #47083 reads the counters by index. After this PR it reads the list ofchunk_inputs.outputskey #47337.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/metafile.test.ts