turbopack: move CSS module composes validation from code generation to resolving (#92582)
### 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`.
<!-- NEXT_JS_LLM_PR -->
---------
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>