css: implement handle_composes to build CssModuleReference entries

Closed
RO
14d ago
+230-13

What

CssModule::handle_composes in src/css/css_modules.rs was a validation-only stub: it checked that composes: appears under a single class selector but never built the composed references. CssModuleReference::{Local, Global} were never constructed anywhere (flagged by the dead-code sweep in #36184) and CssModuleExport.composes was always empty in the ToCssResult.exports map.

Fix

For each single-class selector and each name in the composes: list, construct the appropriate reference and append it to the selector's export entry (creating the entry on first use), de-duplicating by value:

composes: …reference
alphaLocal { name: pattern.write_to_string(.., "alpha") }
x from globalGlobal { name: "x" }
x from "./other.css"Dependency { name: "x", specifier: record.path.text }

This matches the lightningcss handle_composes behavior. Note that Bun's Specifier has no SourceIndex variant, so that lightningcss arm does not apply here.

The bundler's composes: support is a separate parser-level path (ast.composes consumed by generateCodeForLazyExport / scanImportsAndExports) and is unchanged; handle_composes populates the printer-side ToCssResult.exports map.

Testing

Added cssInternals.cssModulesTest(source) (internal-for-testing only) which parses with css_modules: Some(Config::default()), wires the parsed symbol list into to_css, and returns { code, exports } where each export carries its composes array. New tests in test/js/bun/css/css-modules-composes.test.ts cover the local / global / dependency / multi-name-dedupe cases plus the existing invalid-selector error.

bun bd test test/js/bun/css/css-modules-composes.test.ts
 5 pass, 0 fail

Existing test/bundler/css/css-modules.test.ts (6 pass) and test/bundler/esbuild/css.test.ts (56 pass) are unchanged.


[review] gate passed · iteration 1 · 5 files touched

fails on main (without fix)
ASAN without fix: 5 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/css/css-modules-composes.test.ts
bun test v1.4.0 (7f7c0f69e)

test/js/bun/css/css-modules-composes.test.ts:
3 | 
4 | const { cssModulesTest } = cssInternals as any;
5 | 
6 | describe("css_modules handle_composes", () => {
7 |   test("local composes builds a Local reference", () => {
8 |     const result = cssModulesTest(
                       ^
TypeError: cssModulesTest is not a function. (In 'cssModulesTest(`.alpha { color: red; }
       .beta { composes: alpha; color: blue; }`)', 'cssModulesTest' is undefined)
      at <anonymous> (/workspace/bun/test/js/bun/css/css-modules-composes.test.ts:8:20)
(fail) css_modules handle_composes > local composes builds a Local reference [11.01ms]
16 |     const hash = result.exports.beta.name.slice("beta_".length);
17 |     expect(result.exports.beta.composes[0].name).toBe(`alpha_${hash}`);
18 |   });
19 | 
20 |   test("global composes builds a Global reference", () => {
21 |     const result = cssModulesTest(`.alpha { composes: globalName from global; color: red; }`);
                
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (2de563454)

test/js/bun/css/css-modules-composes.test.ts:
(pass) css_modules handle_composes > local composes builds a Local reference [10.69ms]
(pass) css_modules handle_composes > global composes builds a Global reference [0.11ms]
(pass) css_modules handle_composes > composes from file builds a Dependency reference [0.06ms]
(pass) css_modules handle_composes > multiple names and dedupe [0.07ms]
(pass) css_modules handle_composes > invalid selector still errors [1.93ms]

 5 pass
 0 fail
 6 expect() calls
Ran 5 tests across 1 file. [179.00ms]
__F:0:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/css/css-modules-composes.test.ts
bun test v1.4.0 (7f7c0f69e)

test/js/bun/css/css-modules-composes.test.ts:
(pass) css_modules handle_composes > local composes builds a Local reference [19.03ms]
(pass) css_modules handle_composes > global composes builds a Global reference [3.71ms]
(pass) css_modules handle_composes > composes from file builds a Dependency reference [3.02ms]
(pass) css_modules handle_composes > multiple names and dedupe [4.28ms]
(pass) css_modules handle_composes > invalid selector still errors [5.25ms]

 5 pass
 0 fail
 6 expect() calls
Ran 5 tests across 1 file. [2.09s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 708ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/1] reconfigure
[1/28] install /workspace/bun
bun install v1.4.0-canary.1 (2de563454)

Checked 124 installs across 170 packages (no changes) [4.00ms]
[2/28] esbuild runtime.out.js

  build/release/codegen/runtime.out.js  5.3kb

⚡ Done in 5ms
[3/28] esbuild fallback-decoder.js

  build/release/codegen/fallback-decoder.js  8.8kb

⚡ Done in 8ms
[4/28] esbuild bun-error

  ../../build/release/codegen/bun-error/index.js       42.9kb
  ../../build/release/codegen/bun-error/bun-error.css  12.8kb

⚡ Done in 18ms
[5/28] gen cpp.rs (cppbind)
[6/28] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 240 extern-C blocks audited
[7/28] gen JS modules (bundle-modules)
Preprocess modules (9232ms)
Bundle modules (56ms)
Postprocesss modules (210ms)
Bundle Functions (739ms)
Generate Code (37ms)

[10.29s] Bundled "src/js" for production
  2570 kb
  193 internal modules
  13 native modules
  90 internal functions across 19 files
[7/14] cargo bun_bin → libbun_ru
... (truncated)
diff hotspot
src/css/css_modules.rs                       |  69 ++++++++++++---
 src/css_jsc/css_internals.rs                 | 121 +++++++++++++++++++++++++++
 src/js/internal-for-testing.ts               |   1 +
 src/runtime/dispatch_js2native.rs            |   5 +-
 test/js/bun/css/css-modules-composes.test.ts |  47 +++++++++++
 5 files changed, 230 insertions(+), 13 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                          reads  edits  tests
src/css/css_modules.rs                            1      1      0
src/css_jsc/css_internals.rs                      2      2      0
src/js/internal-for-testing.ts                    1      1      0
src/runtime/dispatch_js2native.rs                 1      1      0
test/js/bun/css/css-modules-composes.test.ts      0      3      0