build: type CSS-module class names in the apps - #3113
Merged
Conversation
A CSS-module import in `apps/docs` and `apps/remote-dom-demo` was typed by
Next's ambient wildcard (`next/types/global`) as
`{ readonly [key: string]: string }`. A mistyped key, or one taken from the
wrong module, therefore type-checked fine and evaluated to `undefined` at
runtime — React drops the `className` with no error anywhere.
`packages/components` already solved this with `build:scss-types`. Its
generator moves to `packages/core/src/scssTypes/generateScssTypes.ts`,
parameterized on package root, source directory and Vite config, and
generalized from `.module.scss` to every flavour `vite-plugin-sass-dts`
handles — `.module.sass` and `.module.css` included, which is what the three
plain CSS modules in the docs app needed. The components declarations come out
byte-identical after the move.
Both apps get a thin entry point. Neither has a Vite setup to borrow, so the
plugin gets an inline config: `configFile: false`, the `@/…` alias from
tsconfig, and deliberately no `css.modules.localsConvention` — Next hands class
names through as authored, which the plugin's default reproduces.
`allowArbitraryExtensions` in each app's tsconfig is what makes TypeScript
resolve `./X.module.scss` to `./X.module.d.scss.ts`. Without it the
declarations are inert and the ambient wildcard silently wins again. Verified
against the compiler the apps actually use (TypeScript 7 native, per the
`>typescript` overrides in pnpm-workspace.yaml).
nx wiring:
- `docs`: `build:scss-types` runs before `build`, `dev` and `test:unit`. The
`test:unit` edge is what puts the generator in front of CI's
`git diff --exit-code`; the type check itself rides on `nx build docs` in
build-previews.yml.
- `remote-dom-demo`: only before `dev`. This app has no build, test or compile
target and appears in no workflow, so its declarations are an editor and
`next dev` guard — CI neither type-checks them nor notices a stale one.
- Both targets list the shared generator under `inputs`, so a change to it
invalidates their cache and marks the apps affected.
The new class-name types surface seven `styles.<key>` reads that resolve to
`undefined`, so the element never got the class:
- `MobileNavigation` read `mobileNavigationOffCanvas` off `layout.module.scss`;
the class lives in `Header.module.scss`.
- `MdxFileView` read `liveCodeEditor` off `customComponents.module.css`; the
class lives in `LiveCodeEditor.module.css` — and `LiveCodeEditor` already
applies it to its own root, so the pass-through was redundant on top of being
wrong.
- `Footer.feedbackLink`, `ComponentCard.title`, `ComponentCard.description`,
`LiveCodeEditor.error` and `ExampleTile.mobileCode` never existed in their
modules at all.
All seven were `className={undefined}` at runtime, so removing them changes
nothing that renders.
`.mobileNavigationOffCanvas` in `Header.module.scss` goes with it. Wiring the
import up instead would hide the off-canvas modal's "Menü" heading — a visible
change to a design that has shipped with the heading since #2338, and not one
to make without UX.
The reverse direction of the previous commit, which the class-name types do not catch: a class no TSX ever names is not a type error. `Footer.module.scss`'s `.feedback` and `.feedbackImage` and `customComponents.module.css`'s `.preContainer` and `.preCopyButton` are referenced from nowhere, so none of them has ever applied.
Contributor
Coverage Report for ./packages/components/
File CoverageNo changed files found. |
Contributor
🚀 Preview DeploymentPreview environments are ready:
Images:
|
Lisa18289
approved these changes
Sep 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A CSS-module import in
apps/docsandapps/remote-dom-demowas typed by Next's ambient wildcard (next/types/global) as{ readonly [key: string]: string }. A mistyped key — or one taken from the wrong module — therefore type-checked fine and evaluated toundefinedat runtime, so React dropped theclassNamewith no error anywhere.packages/componentsalready solved this. This PR extends the same solution to both apps and fixes the seven mismatches it surfaced.The generator
packages/components/dev/scss-types/generateScssTypes.tsmoves topackages/core/src/scssTypes/generateScssTypes.ts, parameterized on package root, source directory and Vite config, and generalized from.module.scssto every flavourvite-plugin-sass-dtshandles —.module.sassand.module.cssincluded, which is what the three plain CSS modules in the docs app needed. The components declarations come out byte-identical after the move.Both apps get a thin entry point. Neither has a Vite setup to borrow, so the plugin gets an inline config:
configFile: false, the@/…alias from tsconfig, and deliberately nocss.modules.localsConvention— Next hands class names through as authored, which the plugin's default reproduces.allowArbitraryExtensionsin each app's tsconfig is what makes TypeScript resolve./X.module.scssto./X.module.d.scss.ts. Without it the declarations are inert and the ambient wildcard silently wins again. Verified against the compiler the apps actually use — TypeScript 7 native, per the>typescriptoverrides inpnpm-workspace.yaml— both with the flag (typo is an error) and without (typo passes).nx wiring
docs—build:scss-typesruns beforebuild,devandtest:unit. Thetest:unitedge is what puts the generator in front of CI'sgit diff --exit-code; the type check itself rides onpnpm nx build docsinbuild-previews.yml.remote-dom-demo— only beforedev. Worth knowing: this app has no build, test or compile target and appears in no workflow, so its declarations are an editor andnext devguard only — CI neither type-checks them nor notices a stale one. Regenerating ondevis what keeps them current. Giving the demo a real build/compile target is a separate question.inputs, so a change to it invalidates their cache and marks the apps affected (nx show projects --affectedconfirms).What it caught
MobileNavigation.tsx:55mobileNavigationOffCanvaslayout.module.scss, class is inHeader.module.scssMdxFileView.tsx:82liveCodeEditorcustomComponents.module.css, class is inLiveCodeEditor.module.cssFooter.tsx:85feedbackLinkComponentCard.tsx:29,33title,descriptionLiveCodeEditor.tsx:315errorExampleTile.tsx:53mobileCodeAll seven were
className={undefined}at runtime, so removing them changes nothing that renders.LiveCodeEditoralready applies its own.liveCodeEditorto its root, soMdxFileView's pass-through was redundant on top of being wrong.One decision worth a second opinion
.mobileNavigationOffCanvas h3 { display: none }inHeader.module.scssis deleted rather than wired up. Repointing the import instead would hide the off-canvas modal's "Menü" heading — a visible change to a design that has shipped with the heading since #2338. If UX confirms the heading was meant to be hidden, the fix is to importHeader.module.scssinMobileNavigation.tsxand restore the rule.The third commit removes four classes no TSX ever names (
Footer.module.scss's.feedback,.feedbackImage;customComponents.module.css's.preContainer,.preCopyButton). That direction is the one the types do not catch — an unused class is not a type error.Verification
pnpm nx build docsgreen (Next type check),pnpm nx run-many --targets=test:unit,test:compile,test:links --projects=docs,components,coregreenpnpm lint— 0 errors, format check cleanstyles.cradinComponentCard.tsxandstyles.rootContanerin the demo'slayout.tsx: both now fail the type check🤖 Generated with Claude Code