Skip to content

turbopack: move CSS module composes validation from code generation to resolving - #92582

Merged
sokra merged 3 commits into
canaryfrom
sokra/CssModuleComposesIssue-during-module-graph
Apr 10, 2026
Merged

turbopack: move CSS module composes validation from code generation to resolving#92582
sokra merged 3 commits into
canaryfrom
sokra/CssModuleComposesIssue-during-module-graph

Conversation

@sokra

@sokra sokra commented Apr 9, 2026

Copy link
Copy Markdown
Member

What?

Move the validation that can produce CssModuleComposesIssue from code generation (chunk_item_content) to the reference's own resolve_reference() method, so these errors are surfaced during resolving rather than only during bundling.

Two validation checks are moved into CssModuleComposeReference::resolve_reference():

  1. A composes: ... from "..."; target module can't be resolved (unresolvable reference)
  2. A composes: ... from "..."; target module is not a CSS module (e.g. composing from a .js or .txt file)

The IssueStage is updated from CodeGen to Resolve to reflect the new phase.

Code generation still handles the error cases gracefully (skipping broken references with continue) — it just no longer re-emits the issue since it is already emitted by resolve_reference().

Why?

get_compilation_issues (used by the MCP server and other tooling) only builds the module graph — it does not run code generation. Previously, both CssModuleComposesIssue variants were only emitted during chunk_item_content(), which meant they were invisible to get_compilation_issues. Developers using the MCP get_compilation_issues tool would not see errors from broken composes references until an actual build/bundle was triggered.

Since resolve_reference() is called as part of module graph traversal, emitting the issue there means it is captured by get_compilation_issues alongside other resolve-phase errors.

How?

turbopack-css/src/references/compose.rs

  • CssModuleComposeReference::resolve_reference() is changed from fn to async fn. It calls css_resolve as before, then awaits first_module() on the result and validates:
    • Resolved to nothing → emit "can't be resolved" issue
    • Resolved to a module that doesn't implement CssModuleComposable → emit "not a CSS module" issue
  • CssModuleComposesIssue and its Issue impl are moved here from module_asset.rs, now using ResolvedVc<FileSystemPath> (from origin_path()) as the issue location rather than the IssueSource of the composing file.
  • A new CssModuleComposable marker trait (#[turbo_tasks::value_trait]) is defined here. resolve_reference() uses a try_sidecast to this trait to check whether the resolved module is a valid compose target — avoiding a hard dependency on EcmascriptCssModule from within compose.rs.

turbopack-css/src/module_asset.rs

  • EcmascriptCssModule implements CssModuleComposable, marking it as a valid composes: target.
  • module_references() is reverted to its original simple form — it only collects references without any validation.
  • All issue-related imports (Issue, IssueExt, IssueSource, etc.) are removed.

Test fixtures (test/development/mcp-server/fixtures/compilation-errors-app/app/css-composes-error/)

  • styles.module.css — CSS module with composes: something from './not-a-css-module.txt'
  • not-a-css-module.txt — a plain text file (resolves but is not a CSS module)
  • page.tsx — page importing the broken CSS module

test/development/mcp-server/mcp-server-get-compilation-issues.test.ts

  • New test case should detect CSS module composes errors verifies the issue is surfaced via get_compilation_issues.

… phase

CssModuleComposesIssue was only emitted during chunk_item_content() (code
generation), which meant get_compilation_issues (module graph only) could
not surface these errors. Move the validation into module_references() so
it runs during graph building. Add e2e test with a CSS module composing
from a .txt file to verify the issue is detected.

Co-Authored-By: Claude <noreply@anthropic.com>
@nextjs-bot nextjs-bot added created-by: Turbopack team PRs by the Turbopack team. tests Turbopack Related to Turbopack with Next.js. labels Apr 9, 2026
@codspeed-hq

codspeed-hq Bot commented Apr 9, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 17 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing sokra/CssModuleComposesIssue-during-module-graph (2255710) with canary (ba33b4e)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@nextjs-bot

nextjs-bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 455ms 455ms █▁██▁
Cold (Ready in log) 444ms 444ms █▄▄▇▃
Cold (First Request) 1.104s 1.133s ▁▇███
Warm (Listen) 456ms 457ms ▁█▁▁█
Warm (Ready in log) 446ms 442ms ▆▄▅██
Warm (First Request) 347ms 346ms ▆▇▅▇▅
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 455ms 456ms ▁▁▁▅▁
Cold (Ready in log) 437ms 436ms ▅▄▄▃▄
Cold (First Request) 1.874s 1.881s ▂▂▇▁▂
Warm (Listen) 456ms 456ms █▁███
Warm (Ready in log) 437ms 438ms ▆▄▆▅▅
Warm (First Request) 1.868s 1.900s ▃▃▇▂▃

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 3.820s 3.960s ▄▃█▂▃
Cached Build 3.976s 3.935s ▄▃█▁▁
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.493s 14.438s ▅▄▁▂▃
Cached Build 14.548s 14.570s ▄▃▂▂▃
node_modules Size 492 MB 492 MB █████
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
0-xemccipzit0.js gzip 13 kB N/A -
00-sfwc7hh0nr.js gzip 12.9 kB N/A -
07njffbag0w__.js gzip 156 B N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0lnp3l1q03nf6.js gzip 157 B N/A -
0lsxv4bg541pw.js gzip 65.5 kB N/A -
0p33svf597zhz.js gzip 156 B N/A -
0s8zkgq8tbel7.js gzip 70.8 kB N/A -
0vkt_1wbttko4.js gzip 153 B N/A -
13_2qj6sfhcpr.js gzip 8.51 kB N/A -
13q15tdry7-jw.js gzip 9.81 kB N/A -
163s91ld-ejxd.js gzip 154 B N/A -
16jdy7mb2hpzo.js gzip 2.28 kB N/A -
16lhqjoqbznyg.js gzip 220 B 220 B
17c6iioxxyq_7.js gzip 8.51 kB N/A -
1e9hak60wi8_q.js gzip 10.1 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1lti_nqcijcmi.js gzip 225 B N/A -
1lu_zu666m3ae.js gzip 160 B N/A -
1m9l9vnf18-38.js gzip 49 kB N/A -
1rmcmivcklfqn.js gzip 155 B N/A -
1zq04q8id1dsq.js gzip 8.59 kB N/A -
22jf6c01ubp3o.js gzip 159 B N/A -
246le60fytek6.js gzip 1.46 kB N/A -
28nzibfilon22.js gzip 156 B N/A -
2g5l7iy9vkloq.js gzip 152 B N/A -
2ipc3se0d9mja.js gzip 7.61 kB N/A -
2rm1ibbmjhlgi.js gzip 8.57 kB N/A -
2tmy32jxo7uy5.js gzip 154 B N/A -
2uqfz1rd1h-yi.js gzip 166 B N/A -
30z650ayitjz3.js gzip 5.67 kB N/A -
33ur7c3w36-m-.js gzip 8.56 kB N/A -
396buwq-nlhir.js gzip 8.59 kB N/A -
39mk1fjm58e-4.js gzip 8.62 kB N/A -
3cq10epinkxrc.js gzip 450 B N/A -
3wc1tgfurjhi-.js gzip 9.23 kB N/A -
3ze9s70gat6n_.js gzip 8.56 kB N/A -
41obdnb4lqdgs.js gzip 13.3 kB N/A -
44dza2th1-3p_.js gzip 155 B N/A -
454bom347xpxj.js gzip 13.8 kB N/A -
457x5n-k0jr1x.js gzip 10.4 kB N/A -
turbopack-00..5k8f.js gzip 4.17 kB N/A -
turbopack-03..7xjr.js gzip 4.17 kB N/A -
turbopack-14..0w66.js gzip 4.17 kB N/A -
turbopack-1a..1idr.js gzip 4.17 kB N/A -
turbopack-1e..y1sm.js gzip 4.17 kB N/A -
turbopack-1e..jwc3.js gzip 4.17 kB N/A -
turbopack-2_..0v01.js gzip 4.17 kB N/A -
turbopack-29..1ie-.js gzip 4.17 kB N/A -
turbopack-2e..unjt.js gzip 4.16 kB N/A -
turbopack-3g..e631.js gzip 4.17 kB N/A -
turbopack-3h..0jbu.js gzip 4.17 kB N/A -
turbopack-3q..nhhi.js gzip 4.17 kB N/A -
turbopack-41..2kti.js gzip 4.18 kB N/A -
turbopack-42..ltb0.js gzip 4.18 kB N/A -
0_1u_xrpzaeaj.js gzip N/A 8.52 kB -
0-ua_-urjvdtw.js gzip N/A 8.56 kB -
04o0z49-uugql.js gzip N/A 157 B -
05_r_-_rf4w-n.js gzip N/A 7.61 kB -
0eihfygkvyao-.js gzip N/A 1.46 kB -
0g_88ua4o_jp-.js gzip N/A 9.24 kB -
0ua91j3aes80c.js gzip N/A 8.58 kB -
0zwsxw6xkvw9p.js gzip N/A 8.62 kB -
1_fyx0hi94qc-.js gzip N/A 49 kB -
1_n6uuxm5qn11.js gzip N/A 170 B -
10mvvt3xn1_3j.js gzip N/A 8.59 kB -
1a5tis5_e07-2.js gzip N/A 70.8 kB -
1b75ishu64v5s.js gzip N/A 13 kB -
1b83ah3nflxjf.js gzip N/A 8.52 kB -
1fd23spooi5r7.js gzip N/A 225 B -
1gx5035rzzsj3.js gzip N/A 157 B -
1jbicyv-mme-i.js gzip N/A 156 B -
1nw99o32asytf.js gzip N/A 450 B -
1y6qa6xp0i1nz.js gzip N/A 13.3 kB -
1yd8qavjpi2n-.js gzip N/A 65.5 kB -
1zs0a63ov-hco.js gzip N/A 153 B -
2-moqrpvsput7.js gzip N/A 161 B -
26i99-ztb0xke.js gzip N/A 157 B -
2c3zssa2_hfne.js gzip N/A 158 B -
2kxdvc3gr7nt9.js gzip N/A 8.59 kB -
2sgg_sxyixu_p.js gzip N/A 13.8 kB -
2sk4gp5rmalb0.js gzip N/A 10.1 kB -
2u87ln5_zfir_.js gzip N/A 5.67 kB -
32k3-ntl-0f4s.js gzip N/A 163 B -
33602db0fe2xi.js gzip N/A 9.81 kB -
34l-6efot4df9.js gzip N/A 156 B -
3e-_gp4rum99w.js gzip N/A 157 B -
3j3snr-ce7e0q.js gzip N/A 10.4 kB -
3wy2l4mynculd.js gzip N/A 155 B -
3yby446qbgls0.js gzip N/A 8.56 kB -
3yypm2pwzx0mq.js gzip N/A 12.9 kB -
41e88xrrb9-u8.js gzip N/A 158 B -
42lado0_6oegq.js gzip N/A 2.28 kB -
turbopack-07..5x3d.js gzip N/A 4.18 kB -
turbopack-09..xjwl.js gzip N/A 4.16 kB -
turbopack-0i..szdq.js gzip N/A 4.18 kB -
turbopack-0x..wzj5.js gzip N/A 4.18 kB -
turbopack-0x..46wb.js gzip N/A 4.18 kB -
turbopack-1r..b2gv.js gzip N/A 4.18 kB -
turbopack-1s..7844.js gzip N/A 4.18 kB -
turbopack-1v..x7eh.js gzip N/A 4.18 kB -
turbopack-2s..1ss4.js gzip N/A 4.18 kB -
turbopack-2t..ntff.js gzip N/A 4.18 kB -
turbopack-37..gyrj.js gzip N/A 4.19 kB -
turbopack-3k..uuv_.js gzip N/A 4.18 kB -
turbopack-3t..5b--.js gzip N/A 4.18 kB -
turbopack-3w..vc63.js gzip N/A 4.18 kB -
Total 464 kB 465 kB ⚠️ +164 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 715 B 717 B
Total 715 B 717 B ⚠️ +2 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 435 B 436 B
Total 435 B 436 B ⚠️ +1 B

📦 Webpack

Client

Main Bundles
Canary PR Change
1011-HASH.js gzip 5.58 kB N/A -
2168.HASH.js gzip 169 B N/A -
2225-HASH.js gzip 4.64 kB N/A -
61a8f394-HASH.js gzip 62.8 kB N/A -
850-HASH.js gzip 60.6 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 257 B 250 B 🟢 7 B (-3%)
main-HASH.js gzip 39.3 kB 39.6 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
36c7d9a6-HASH.js gzip N/A 62.8 kB -
3967-HASH.js gzip N/A 4.63 kB -
5025-HASH.js gzip N/A 5.58 kB -
634-HASH.js gzip N/A 60.9 kB -
7586.HASH.js gzip N/A 170 B -
Total 235 kB 235 kB ⚠️ +606 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 182 B 181 B
css-HASH.js gzip 334 B 333 B
dynamic-HASH.js gzip 1.8 kB 1.81 kB
edge-ssr-HASH.js gzip 255 B 254 B
head-HASH.js gzip 352 B 352 B
hooks-HASH.js gzip 384 B 384 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 259 B 259 B
link-HASH.js gzip 2.52 kB 2.52 kB
routerDirect..HASH.js gzip 320 B 317 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.98 kB 7.99 kB ⚠️ +4 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 125 kB 126 kB
page.js gzip 272 kB 273 kB
Total 398 kB 399 kB ⚠️ +1.12 kB
Middleware
Canary PR Change
middleware-b..fest.js gzip 613 B 617 B
middleware-r..fest.js gzip 156 B 156 B
middleware.js gzip 44.4 kB 44.1 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 46 kB 45.7 kB ✅ -313 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 719 B 718 B
Total 719 B 718 B ✅ -1 B
Build Cache
Canary PR Change
0.pack gzip 4.38 MB 4.38 MB
index.pack gzip 113 kB 114 kB 🔴 +1.22 kB (+1%)
index.pack.old gzip 113 kB 113 kB
Total 4.6 MB 4.61 MB ⚠️ +730 B

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 346 kB 346 kB
app-page-exp..prod.js gzip 191 kB 191 kB
app-page-tur...dev.js gzip 345 kB 345 kB
app-page-tur..prod.js gzip 191 kB 191 kB
app-page-tur...dev.js gzip 342 kB 342 kB
app-page-tur..prod.js gzip 189 kB 189 kB
app-page.run...dev.js gzip 342 kB 342 kB
app-page.run..prod.js gzip 190 kB 190 kB
app-route-ex...dev.js gzip 76.9 kB 76.9 kB
app-route-ex..prod.js gzip 52.5 kB 52.5 kB
app-route-tu...dev.js gzip 77 kB 77 kB
app-route-tu..prod.js gzip 52.5 kB 52.5 kB
app-route-tu...dev.js gzip 76.6 kB 76.6 kB
app-route-tu..prod.js gzip 52.2 kB 52.2 kB
app-route.ru...dev.js gzip 76.5 kB 76.5 kB
app-route.ru..prod.js gzip 52.2 kB 52.2 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.9 kB 43.9 kB
pages-api-tu..prod.js gzip 33.5 kB 33.5 kB
pages-api.ru...dev.js gzip 43.8 kB 43.8 kB
pages-api.ru..prod.js gzip 33.4 kB 33.4 kB
pages-turbo....dev.js gzip 53.3 kB 53.3 kB
pages-turbo...prod.js gzip 39.1 kB 39.1 kB
pages.runtim...dev.js gzip 53.2 kB 53.2 kB
pages.runtim..prod.js gzip 39 kB 39 kB
server.runti..prod.js gzip 62.8 kB 62.8 kB
Total 3.06 MB 3.06 MB
📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/2255710716af3eae2f0fef39fb995cafb0a4e8d3/next

…::resolve_reference

Instead of validating in module_references(), move the logic into the
reference itself. Introduce a CssModuleComposable marker trait in
compose.rs; EcmascriptCssModule implements it. resolve_reference() now
validates both cases (unresolvable + non-CSS-module target) and emits
CssModuleComposesIssue directly, making the issue available during
module graph traversal (e.g. get_compilation_issues) without coupling
the validation to the consumer (module_references or codegen).

Co-Authored-By: Claude <noreply@anthropic.com>
@sokra
sokra marked this pull request as ready for review April 10, 2026 10:47
@sokra
sokra requested a review from mischnic April 10, 2026 10:47
Comment thread turbopack/crates/turbopack-css/src/module_asset.rs Outdated
Both types live in the same crate, so compose.rs can import
EcmascriptCssModule directly. The marker trait and its impl were
unnecessary indirection.

Co-Authored-By: Claude <noreply@anthropic.com>
@sokra
sokra merged commit 4b3e4e8 into canary Apr 10, 2026
188 checks passed
@sokra
sokra deleted the sokra/CssModuleComposesIssue-during-module-graph branch April 10, 2026 19:11
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Apr 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

created-by: Turbopack team PRs by the Turbopack team. locked tests Turbopack Related to Turbopack with Next.js.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants