mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
### 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 <[email protected]> Co-authored-by: Claude <[email protected]>
102 lines
3.1 KiB
TypeScript
102 lines
3.1 KiB
TypeScript
import path from 'path'
|
|
import { nextTestSetup } from 'e2e-utils'
|
|
|
|
// get_compilation_issues is a Turbopack-only feature (requires NAPI project handle)
|
|
;(process.env.IS_TURBOPACK_TEST ? describe : describe.skip)(
|
|
'mcp-server get_compilation_issues tool',
|
|
() => {
|
|
const { next, skipped } = nextTestSetup({
|
|
files: path.join(__dirname, 'fixtures', 'compilation-errors-app'),
|
|
})
|
|
|
|
if (skipped) {
|
|
return
|
|
}
|
|
|
|
async function callMcpTool() {
|
|
const response = await fetch(`${next.url}/_next/mcp`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json, text/event-stream',
|
|
},
|
|
body: JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
id: 'get-compilation-issues',
|
|
method: 'tools/call',
|
|
params: { name: 'get_compilation_issues', arguments: {} },
|
|
}),
|
|
})
|
|
|
|
const text = await response.text()
|
|
const match = text.match(/data: ({.*})/s)
|
|
expect(match).toBeTruthy()
|
|
const result = JSON.parse(match![1])
|
|
return JSON.parse(result.result?.content?.[0]?.text)
|
|
}
|
|
|
|
type Issue = { severity: string; filePath: string; title: string }
|
|
|
|
let compilationResult: { issues: Issue[] }
|
|
|
|
beforeAll(async () => {
|
|
compilationResult = await callMcpTool()
|
|
})
|
|
|
|
function errorIssues() {
|
|
return compilationResult.issues.filter(
|
|
(issue) => issue.severity === 'error' || issue.severity === 'fatal'
|
|
)
|
|
}
|
|
|
|
it('should return compilation issues without requiring a browser session', () => {
|
|
expect(compilationResult).toHaveProperty('issues')
|
|
expect(Array.isArray(compilationResult.issues)).toBe(true)
|
|
})
|
|
|
|
it('should detect module-not-found errors', () => {
|
|
const errors = errorIssues()
|
|
expect(errors.length).toBeGreaterThan(0)
|
|
|
|
const moduleNotFoundIssue = errors.find(
|
|
(issue) =>
|
|
issue.filePath.includes('missing-module') ||
|
|
issue.title.includes('non-existent-module')
|
|
)
|
|
expect(moduleNotFoundIssue).toBeDefined()
|
|
})
|
|
|
|
it('should detect syntax errors', () => {
|
|
const errors = errorIssues()
|
|
const syntaxErrorIssue = errors.find((issue) =>
|
|
issue.filePath.includes('syntax-error')
|
|
)
|
|
expect(syntaxErrorIssue).toBeDefined()
|
|
})
|
|
|
|
it('should detect CSS module composes errors', () => {
|
|
const errors = errorIssues()
|
|
const composesIssue = errors.find(
|
|
(issue) =>
|
|
issue.filePath.includes('css-composes-error') ||
|
|
issue.title.includes('composes')
|
|
)
|
|
expect(composesIssue).toBeDefined()
|
|
})
|
|
|
|
it('should include issue metadata fields', () => {
|
|
const errors = errorIssues()
|
|
expect(errors.length).toBeGreaterThan(0)
|
|
|
|
const issue = errors[0]
|
|
expect(issue).toHaveProperty('severity')
|
|
expect(issue).toHaveProperty('filePath')
|
|
expect(issue).toHaveProperty('title')
|
|
expect(typeof issue.severity).toBe('string')
|
|
expect(typeof issue.filePath).toBe('string')
|
|
// title must be a plain string, not a StyledString object
|
|
expect(typeof issue.title).toBe('string')
|
|
})
|
|
}
|
|
)
|