Skip to content

build: type CSS-module class names in the apps - #3113

Merged
mfal merged 4 commits into
mainfrom
claude/inspiring-easley-473829
Sep 8, 2026
Merged

build: type CSS-module class names in the apps#3113
mfal merged 4 commits into
mainfrom
claude/inspiring-easley-473829

Conversation

@mfal

@mfal mfal commented Sep 3, 2026

Copy link
Copy Markdown
Member

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, so React dropped the className with no error anywhere.

packages/components already 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.ts 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 — both with the flag (typo is an error) and without (typo passes).

nx wiring

  • docsbuild: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 pnpm nx build docs in build-previews.yml.
  • remote-dom-demo — only before dev. Worth knowing: this app has no build, test or compile target and appears in no workflow, so its declarations are an editor and next dev guard only — CI neither type-checks them nor notices a stale one. Regenerating on dev is what keeps them current. Giving the demo a real build/compile target is a separate question.
  • Both targets list the shared generator under inputs, so a change to it invalidates their cache and marks the apps affected (nx show projects --affected confirms).

What it caught

Where Key Cause
MobileNavigation.tsx:55 mobileNavigationOffCanvas reads layout.module.scss, class is in Header.module.scss
MdxFileView.tsx:82 liveCodeEditor reads customComponents.module.css, class is in LiveCodeEditor.module.css
Footer.tsx:85 feedbackLink never existed
ComponentCard.tsx:29,33 title, description never existed
LiveCodeEditor.tsx:315 error never existed
ExampleTile.tsx:53 mobileCode never existed

All seven were className={undefined} at runtime, so removing them changes nothing that renders. LiveCodeEditor already applies its own .liveCodeEditor to its root, so MdxFileView's pass-through was redundant on top of being wrong.

One decision worth a second opinion

.mobileNavigationOffCanvas h3 { display: none } in Header.module.scss is 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 import Header.module.scss in MobileNavigation.tsx and 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 docs green (Next type check), pnpm nx run-many --targets=test:unit,test:compile,test:links --projects=docs,components,core green
  • pnpm lint — 0 errors, format check clean
  • No generated-code drift after a full build
  • Injected styles.crad in ComponentCard.tsx and styles.rootContaner in the demo's layout.tsx: both now fail the type check

🤖 Generated with Claude Code

mfal added 3 commits September 3, 2026 09:12
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.
@mfal
mfal requested a review from a team September 3, 2026 07:13
@mfal mfal self-assigned this Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for ./packages/components/

Status Category Percentage Covered / Total
🔵 Lines 78.84% 764 / 969
🔵 Statements 78.72% 781 / 992
🔵 Functions 80.28% 171 / 213
🔵 Branches 70.79% 400 / 565
File CoverageNo changed files found.
Generated in workflow #6692 for commit f92168c by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

Preview environments are ready:

Type URL
docs pr-3113.docs.review.flow-components.de
storybook pr-3113.storybook.review.flow-components.de

Images:

  • docs: ghcr.io/mittwald/flow/docs:pr-3113
  • storybook: ghcr.io/mittwald/flow/storybook:pr-3113

@mfal
mfal requested a review from Lisa18289 September 3, 2026 11:05
@mfal
mfal merged commit c1d16ca into main Sep 8, 2026
22 checks passed
@mfal
mfal deleted the claude/inspiring-easley-473829 branch September 8, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants