Skip to content

⚠️ [WB-2324.2] Phase 0.3 — Type generation + IDE autocomplete for CSS Modules#3063

Merged
jandrade merged 2 commits into
deploy/css-modulesfrom
WB-2324.3
May 29, 2026
Merged

⚠️ [WB-2324.2] Phase 0.3 — Type generation + IDE autocomplete for CSS Modules#3063
jandrade merged 2 commits into
deploy/css-modulesfrom
WB-2324.3

Conversation

@jandrade

@jandrade jandrade commented May 21, 2026

Copy link
Copy Markdown
Member

Summary:

Adds the static-typing half of the CSS Modules migration tooling. tsc --noEmit now catches styles.<missingClassName> references, and VS Code / WebStorm get live autocomplete + Go-to-Definition on *.module.css imports.

Builds on top of WB-2324 (PR #3059); no Rollup or runtime behavior changes.

Reviewers: #frontend-infra-web

Issue: WB-2324

Test plan:

  1. pnpm typecheck — passes; prebuild/pretypecheck regenerates .d.ts files first.
  2. Introduce styles.tpyo in __docs__/css-modules-spike/spike.tsxpnpm typecheck fails with Property 'tpyo' does not exist on type '{ readonly pill: string; readonly root: string; }'. Did you mean 'root'?.
  3. Revert the typo → pnpm typecheck passes again.
  4. pnpm build — runs gen:css-types first via the prebuild hook, then Rollup; build succeeds.
  5. Open __docs__/css-modules-spike/spike.tsx in VS Code with the recommended extensions installed: styles. autocomplete shows pill and root; Go-to-Definition jumps to the matching CSS rule.

Review plan:

Please review these risky changes — each links to its inline comment:

  1. ⚠️ package.json
  2. ⚠️ tsconfig.json
  3. 🔷 build-settings/gen-css-types.cjs

Common patterns:

2 Files — pre-script hooks ensuring *.module.css.d.ts exists before tsc / Rollup run:

// package.json
+ "prebuild": "pnpm gen:css-types",
+ "gen:css-types": "node ./build-settings/gen-css-types.cjs",
...
+ "pretypecheck": "pnpm gen:css-types",

1 File — gitignore the generated declarations (regenerated on every build):

# Generated by `pnpm gen:css-types` (typed-css-modules) via the `prebuild` /
# `pretypecheck` hooks. Never checked in — fresh checkouts regenerate them.
**/*.module.css.d.ts

🤖 Built using Claude Code #ai-generated

@jandrade jandrade self-assigned this May 21, 2026
@jandrade jandrade requested a review from a team May 21, 2026 20:08
@changeset-bot

changeset-bot Bot commented May 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c06d53d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 129 kB

ℹ️ View Unchanged
Filename Size
packages/eslint-plugin-wonder-blocks/dist/es/index.js 7.18 kB
packages/wonder-blocks-accordion/dist/es/index.js 3.02 kB
packages/wonder-blocks-announcer/dist/es/index.js 2.43 kB
packages/wonder-blocks-badge/dist/es/index.js 2.03 kB
packages/wonder-blocks-banner/dist/es/index.js 2.01 kB
packages/wonder-blocks-birthday-picker/dist/es/index.js 1.93 kB
packages/wonder-blocks-breadcrumbs/dist/es/index.js 761 B
packages/wonder-blocks-button/dist/es/index.js 4.28 kB
packages/wonder-blocks-card/dist/es/index.js 1.09 kB
packages/wonder-blocks-cell/dist/es/index.js 2.19 kB
packages/wonder-blocks-clickable/dist/es/index.js 2.61 kB
packages/wonder-blocks-core/dist/es/index.js 2.6 kB
packages/wonder-blocks-data/dist/es/index.js 5.48 kB
packages/wonder-blocks-date-picker/dist/es/index.js 8.06 kB
packages/wonder-blocks-dropdown/dist/es/index.js 19.7 kB
packages/wonder-blocks-form/dist/es/index.js 6.32 kB
packages/wonder-blocks-grid/dist/es/index.js 1.25 kB
packages/wonder-blocks-icon-button/dist/es/index.js 4.01 kB
packages/wonder-blocks-icon/dist/es/index.js 1.91 kB
packages/wonder-blocks-labeled-field/dist/es/index.js 3.47 kB
packages/wonder-blocks-layout/dist/es/index.js 1.69 kB
packages/wonder-blocks-link/dist/es/index.js 1.54 kB
packages/wonder-blocks-modal/dist/es/index.js 7.36 kB
packages/wonder-blocks-pill/dist/es/index.js 1.31 kB
packages/wonder-blocks-popover/dist/es/index.js 4.41 kB
packages/wonder-blocks-progress-spinner/dist/es/index.js 1.48 kB
packages/wonder-blocks-search-field/dist/es/index.js 1.1 kB
packages/wonder-blocks-styles/dist/es/index.js 464 B
packages/wonder-blocks-switch/dist/es/index.js 1.55 kB
packages/wonder-blocks-tabs/dist/es/index.js 5.46 kB
packages/wonder-blocks-testing-core/dist/es/index.js 3.25 kB
packages/wonder-blocks-testing/dist/es/index.js 978 B
packages/wonder-blocks-theming/dist/es/index.js 384 B
packages/wonder-blocks-timing/dist/es/index.js 1.53 kB
packages/wonder-blocks-tokens/dist/es/index.js 5.36 kB
packages/wonder-blocks-toolbar/dist/es/index.js 921 B
packages/wonder-blocks-tooltip/dist/es/index.js 6.06 kB
packages/wonder-blocks-typography/dist/es/index.js 1.58 kB

compressed-size-action

@jandrade jandrade removed the request for review from a team May 21, 2026 20:10
@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

npm Snapshot: Published

🎉 Good news!! We've packaged up the latest commit from this PR (6cd221f) and published all packages with changesets to npm.

You can install the packages in frontend by running:

./dev/tools/deploy_wonder_blocks.js --tag="PR3063"

Packages can also be installed manually by running:

pnpm add @khanacademy/wonder-blocks-<package-name>@PR3063

@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

A new build was pushed to Chromatic! 🚀

https://5e1bf4b385e3fb0020b7073c-kccwzddppx.chromatic.com/

Chromatic results:

Metric Total
Captured snapshots 0
Tests with visual changes 0
Total stories 847
Inherited (not captured) snapshots [TurboSnap] 479
Tests on the build 479

Comment thread package.json
"alex": "alex packages/",
"dev": "pnpm build && turbo watch build:css",
"build": "rollup -c ./build-settings/rollup.config.mjs && pnpm -r build && turbo build:css",
"prebuild": "pnpm gen:css-types",

@jandrade jandrade May 21, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 1 of 3: Adds typed-css-modules + typescript-plugin-css-modules devDeps and wires gen:css-types into both prebuild and pretypecheck (line 43). Hooks run for every pnpm build and every pnpm typecheck, so a regression in build-settings/gen-css-types.cjs would break CI. cc: @Khan/wonder-blocks

Comment thread tsconfig.json
// Provides live IDE autocomplete and "Go to Definition" for
// `*.module.css` imports. Build-time type safety still comes from
// the generated `*.module.css.d.ts` files (see `gen:css-types`).
"plugins": [{"name": "typescript-plugin-css-modules"}]

@jandrade jandrade May 21, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 2 of 3: Adds the typescript-plugin-css-modules TS language-service plugin. Doesn't affect tsc output (plugins only influence the language service), but everyone using a TS-aware editor on this repo will start seeing the plugin's behavior — live autocomplete + Go-to-Definition for styles.<className>. cc: @Khan/wonder-blocks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to include instructions for setting up this ts plugin in the getting started docs? https://khan.github.io/wonder-blocks/?path=/docs/get-started--docs

Or maybe somewhere else, since it isn't specific to WB?

@@ -0,0 +1,48 @@
// Generate `*.module.css.d.ts` files next to every `*.module.css` source.

@jandrade jandrade May 21, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔷 3 of 3: Root-level Node script that globs packages/**/*.module.css + __docs__/**/*.module.css (ignoring node_modules/dist) and invokes typed-css-modules to emit colocated .d.ts declarations. Camel-casing left disabled (camelCase: false) until 0.4 picks the project-wide naming convention — the spike's class names already happen to be camelCase, so this is a no-op for now. cc: @Khan/wonder-blocks

@jandrade jandrade marked this pull request as ready for review May 22, 2026 20:58
@khan-actions-bot khan-actions-bot requested a review from a team May 22, 2026 20:59
@codecov

codecov Bot commented May 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.00%. Comparing base (d39be33) to head (9022a89).

Additional details and impacted files

Impacted file tree graph

@@       Coverage Diff       @@
##   WB-2324   #3063   +/-   ##
===============================
===============================

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d39be33...9022a89. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jandrade jandrade changed the title ⚠️ [WB-2324] Phase 0.3 — Type generation + IDE autocomplete for CSS Modules ⚠️ [WB-2324.2] Phase 0.3 — Type generation + IDE autocomplete for CSS Modules May 22, 2026
@jandrade jandrade requested a review from a team May 25, 2026 14:55
jandrade pushed a commit that referenced this pull request May 25, 2026
Adds CSS-side hygiene to the migration. `pnpm lint:css` is now an explicit CI step alongside ESLint and Typecheck. Token-required properties (color / spacing / typography) raise a warning when authored as raw values — stays at warn level during the migration and tightens to error once Phase 5 finishes.

Builds on top of WB-2324.3 (PR #3063); no runtime behavior changes.

Reviewers: #frontend-infra-web

Issue: WB-2324

## Test plan:

1. `pnpm lint:css` — passes (0 errors, 1 pre-existing warning on `__docs__/wonder-blocks-typography/styles.css` for a hardcoded `color: red`).
2. Add a bogus at-rule body to `spike.module.css` (e.g. `@whatever-bogus-rule { color: red; }`): `pnpm lint:css` fails with `Unknown at-rule "@whatever-bogus-rule"` and warns about the raw color.
3. Revert → clean.
4. Watch CI on this branch: the new `Stylelint` step runs after `Eslint` and before `Typecheck` in node-ci-lint.yml.

## Review plan:

Please review these risky changes

1. 🚨 `.github/workflows/node-ci-lint.yml`: adds a new `Stylelint` job step (`pnpm lint:css`) between `Eslint` and `Typecheck`. Runs on every PR; a regression in the stylelint config or new false-positive rule could block unrelated work. `if: always()` so it doesn't short-circuit on an upstream failure.
2. ⚠️ `.stylelintrc.cjs` (new): extends `stylelint-config-standard` + `stylelint-config-css-modules`. Allow-lists `@apply`, `@mixin`, `@define-mixin`, `@include`, `@layer` so the PostCSS pipeline's at-rules pass. Disables `custom-property-pattern` (WB tokens use `--wb-camelCase_underscore` naming on purpose). Activates `scale-unlimited/declaration-strict-value` at *warning* level for color / background / border-color / padding / margin / font-size / font-family.
3. ⚠️ `package.json`: adds 4 external devDeps — `stylelint`, `stylelint-config-standard`, `stylelint-config-css-modules`, `stylelint-declaration-strict-value` — plus the `lint:css` script. New script becomes part of standard dev / CI workflow.

### Common patterns:

**1 File — declarative at-rule allow-list paired with the PostCSS pipeline:**

```js
// .stylelintrc.cjs — must match plugin order in postcss.config.cjs
"at-rule-no-unknown": [
    true,
    {
        ignoreAtRules: [
            "apply", "mixin", "define-mixin", "include", "layer",
        ],
    },
],
```

**1 File — CI job step that mirrors the existing Eslint / Typecheck shape:**

```yaml
# .github/workflows/node-ci-lint.yml
- name: Stylelint
  if: always()
  run: pnpm lint:css
```

🤖 Built using Claude Code #ai-generated
@beaesguerra

Copy link
Copy Markdown
Member

@claude review once

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — tooling-only PR, the inline nit about stale *.d.ts cleanup is non-blocking.

Extended reasoning...

Overview

This PR is the Phase 0.3 tooling for the CSS Modules migration. It adds two devDeps (typed-css-modules, typescript-plugin-css-modules), a small Node script at build-settings/gen-css-types.cjs that globs *.module.css and emits colocated *.module.css.d.ts files, wires that script into prebuild and pretypecheck in package.json, registers the TS language-service plugin in tsconfig.json, gitignores/eslintignores the generated declarations, and recommends a VS Code extension. No runtime or Rollup behavior changes.

Security risks

None apparent. No auth, crypto, network, or user-input surfaces are touched. The new devDeps run in build/typecheck context only and the generated .d.ts files are gitignored.

Level of scrutiny

Low-to-moderate. This is build tooling, not production runtime code, and the author has already self-annotated the three risky changes (package.json, tsconfig.json, gen-css-types.cjs) with clear reasoning. The bug hunter surfaced only one nit-level issue (stale *.d.ts files when CSS sources are renamed/deleted), which is bounded to local working trees — CI starts from a fresh checkout and is unaffected. It is a reasonable refinement to defer to Phase 0.4 as the migration surface grows.

Other factors

  • Self-contained, easily reversible change.
  • The CSS Modules spike currently has exactly one *.module.css file, so the blast radius for any regression in the generator is minimal today.
  • Reviewer requested approval explicitly (@claude review once) and no prior review from me exists on this PR.
  • Generated .d.ts files are correctly excluded from both .gitignore and .eslintignore.

Comment on lines +33 to +36
for (const file of files) {
const content = await creator.create(file);
await content.writeFile();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit: gen-css-types.cjs regenerates a .d.ts for each *.module.css it finds but never deletes orphaned *.module.css.d.ts files whose source has been renamed or removed. The stale declarations let import styles from './removed.module.css' keep type-checking locally (CI is unaffected since the files are gitignored). Consider unlinking *.module.css.d.ts files without a matching source at the start of each run.

Extended reasoning...

What the bug is

build-settings/gen-css-types.cjs globs every packages/**/*.module.css and __docs__/**/*.module.css source and writes a colocated <name>.module.css.d.ts via typed-css-modules. It only ever writes; it never enumerates existing .d.ts files to prune ones whose source has disappeared. So after a developer renames foo.module.cssbar.module.css (or deletes the CSS source entirely), foo.module.css.d.ts lingers in the working tree.

Why it matters (and the one real consequence)

Because *.module.css.d.ts acts as the ambient type declaration for *.module.css, TypeScript will still resolve import styles from './foo.module.css' against the orphan declaration rather than failing with Cannot find module. pnpm typecheck therefore reports green on a local checkout that has stale .d.ts files, even though a fresh checkout (CI) would correctly fail since the .d.ts files are gitignored. The realistic failure mode is: a dev renames/deletes a CSS module file, forgets to update one importer, local typecheck passes, and the diff lands in a PR where CI then fails — or worse, the importer is in code that doesn't get exercised until the build step.

Scope is narrow (hence nit)

The blast radius is limited and worth being upfront about:

  • CI / fresh checkouts are unaffected — .d.ts files are gitignored and regenerated by prebuild / pretypecheck.
  • Class-name renames within the same file are not affected — typed-css-modules' writeFile() overwrites the colocated .d.ts rather than merging, so removed classes do disappear on regeneration.
  • pnpm build (Rollup) loads the real CSS file and would fail with a missing-module error if the source is gone.
  • The IDE side uses typescript-plugin-css-modules, which reads CSS files live, so editors would also surface the missing source.

So the only window is pnpm typecheck against a stale local working tree, between the rename/delete and the next git clean / fresh checkout. Today only one CSS Module file exists (__docs__/css-modules-spike/spike.module.css), so the chance of hitting it in Phase 0.3 is low — but the surface grows as Phase 0.4+ migrates more files.

Step-by-step proof

  1. pnpm gen:css-types runs in a clean tree — __docs__/css-modules-spike/spike.module.css.d.ts is written declaring pill and root.
  2. Developer renames the file: git mv spike.module.css renamed.module.css and updates the import in spike.tsx to './renamed.module.css'. They forget to update one other hypothetical importer (call it other.tsx) that still has import styles from './spike.module.css'.
  3. pnpm typecheck invokes pretypecheckgen:css-types. The script globs sources, finds renamed.module.css, writes renamed.module.css.d.ts. It never inspects spike.module.css.d.ts, which is still on disk from step 1.
  4. tsc runs. other.tsx's import of ./spike.module.css resolves against the stale spike.module.css.d.ts, and styles.pill still type-checks. Typecheck passes locally.
  5. The PR lands on CI. CI starts from a fresh checkout (no orphan .d.ts), regenerates only renamed.module.css.d.ts, and now other.tsx's import fails with Cannot find module './spike.module.css'. Red build, surprised author.

How to fix

At the start of main(), glob **/*.module.css.d.ts (same roots, same ignores) and fs.unlink any whose corresponding *.module.css source no longer exists — or, equivalently, walk the source list to build a set of expected output paths and remove every *.module.css.d.ts not in the set. Either approach keeps prebuild / pretypecheck idempotent without affecting CI behavior.

Addressing the refutation

The refuter is correct that the original synthesis description over-claims — consequences #2 (class-name renames within a file) and #3 (new-contributor artifacts) are not real, for the reasons above. This comment is narrowed to the one real consequence (rename/delete of a CSS source). The refuter's other point — that this is a known limitation of typed-css-modules tooling and could be deferred to Phase 0.4 — is fair; that's why this is filed as nit rather than blocking. A ~10-line cleanup pass at the top of the script removes the local/CI divergence cheaply, but it's a reasonable refinement to land later.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude fix this

@beaesguerra beaesguerra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Confirmed type checking catches if I try using a style that doesn't exist:

Image

In terms of DX, do you know if there's a way to make it so if I cmd + click on the import file name (ie. ./spike.module.css), it'll take me to the css file, rather than the type declaration file? Before (in the WB-2324 branch), it gave me an option to choose the .css file. After, it'll take me to the spike.module.css.d.ts file directly.

Before After
Image Image

It would be nice if cmd + click on the styles variable took the user to the .css file too!

Update: Nvm! I had to install the recommended extensions and reload 😄 cmd+click on the file name gives me the option to go to the CSS file. cmd+click on the classname (the root part of styles.root), takes me to the css class definition, though cmd+click on the styles part takes me to the assets.d.ts file instead!

Comment thread tsconfig.json
// Provides live IDE autocomplete and "Go to Definition" for
// `*.module.css` imports. Build-time type safety still comes from
// the generated `*.module.css.d.ts` files (see `gen:css-types`).
"plugins": [{"name": "typescript-plugin-css-modules"}]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to include instructions for setting up this ts plugin in the getting started docs? https://khan.github.io/wonder-blocks/?path=/docs/get-started--docs

Or maybe somewhere else, since it isn't specific to WB?

@beaesguerra

Copy link
Copy Markdown
Member

Another DX thing I noticed, what's the best way to identify what uses a *.module.css file? (besides searching for the filename, since the filename might not be unique?)

With React components, I'm able to use the "Find File References" option. This option isn't available for the .css file though!

tsx file css file
image image

@khan-actions-bot khan-actions-bot requested a review from a team May 26, 2026 21:47
jandrade pushed a commit that referenced this pull request May 26, 2026
Adds CSS-side hygiene to the migration. `pnpm lint:css` is now an explicit CI step alongside ESLint and Typecheck. Token-required properties (color / spacing / typography) raise a warning when authored as raw values — stays at warn level during the migration and tightens to error once Phase 5 finishes.

Builds on top of WB-2324.3 (PR #3063); no runtime behavior changes.

Reviewers: #frontend-infra-web

Issue: WB-2324

## Test plan:

1. `pnpm lint:css` — passes (0 errors, 1 pre-existing warning on `__docs__/wonder-blocks-typography/styles.css` for a hardcoded `color: red`).
2. Add a bogus at-rule body to `spike.module.css` (e.g. `@whatever-bogus-rule { color: red; }`): `pnpm lint:css` fails with `Unknown at-rule "@whatever-bogus-rule"` and warns about the raw color.
3. Revert → clean.
4. Watch CI on this branch: the new `Stylelint` step runs after `Eslint` and before `Typecheck` in node-ci-lint.yml.

## Review plan:

Please review these risky changes

1. 🚨 `.github/workflows/node-ci-lint.yml`: adds a new `Stylelint` job step (`pnpm lint:css`) between `Eslint` and `Typecheck`. Runs on every PR; a regression in the stylelint config or new false-positive rule could block unrelated work. `if: always()` so it doesn't short-circuit on an upstream failure.
2. ⚠️ `.stylelintrc.cjs` (new): extends `stylelint-config-standard` + `stylelint-config-css-modules`. Allow-lists `@apply`, `@mixin`, `@define-mixin`, `@include`, `@layer` so the PostCSS pipeline's at-rules pass. Disables `custom-property-pattern` (WB tokens use `--wb-camelCase_underscore` naming on purpose). Activates `scale-unlimited/declaration-strict-value` at *warning* level for color / background / border-color / padding / margin / font-size / font-family.
3. ⚠️ `package.json`: adds 4 external devDeps — `stylelint`, `stylelint-config-standard`, `stylelint-config-css-modules`, `stylelint-declaration-strict-value` — plus the `lint:css` script. New script becomes part of standard dev / CI workflow.

### Common patterns:

**1 File — declarative at-rule allow-list paired with the PostCSS pipeline:**

```js
// .stylelintrc.cjs — must match plugin order in postcss.config.cjs
"at-rule-no-unknown": [
    true,
    {
        ignoreAtRules: [
            "apply", "mixin", "define-mixin", "include", "layer",
        ],
    },
],
```

**1 File — CI job step that mirrors the existing Eslint / Typecheck shape:**

```yaml
# .github/workflows/node-ci-lint.yml
- name: Stylelint
  if: always()
  run: pnpm lint:css
```

🤖 Built using Claude Code #ai-generated
@jandrade jandrade requested a review from beaesguerra May 27, 2026 14:32

@jeresig jeresig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm not understanding the changes in this PR. It seems like typescript-plugin-css-modules allows us to dynamically compute the types in Typescript - but then how are the generated type files used? They aren't checked in... are this distributed as part of the package? I thought we weren't exporting any CSS modules directly?

@jandrade

Copy link
Copy Markdown
Member Author

I think I'm not understanding the changes in this PR. It seems like typescript-plugin-css-modules allows us to dynamically compute the types in Typescript - but then how are the generated type files used? They aren't checked in... are this distributed as part of the package? I thought we weren't exporting any CSS modules directly?

Good questions @jeresig! This is more for internal WB consumption as we are not planning to distribute these typedefs as part of the package.

  • typescript-plugin-css-modules is included for integration with IDEs, so it allows autocompleting valid styles.
  • typed-css-modules is the CLI that generate the type files so we can enforce that TS errors are caught during build-time, so we avoid generating component bundles where an invalid style could be introduced at build-time. This package is the one we use on prebuild and pretypecheck scripts

My plan is to implement something similar in consumer repos in a way that folks can also use these tools to enforce that we are using valid css modules. I'm going to try to see if we can use a tool in frontend that doesn't create .d.ts files on disk, but open to any suggestions from your side.

@jeresig jeresig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jandrade Gotcha, ok that's helpful! So I think it may be worth confirming that in frontend when doing a build using Rspack that it doesn't error out anyway, as we may already have something that's able to catch failures. But it's possible that this would be "too late" and we'd want to catch it during typechecking anyway, as you noted.

jandrade pushed a commit that referenced this pull request May 29, 2026
Adds CSS-side hygiene to the migration. `pnpm lint:css` is now an explicit CI step alongside ESLint and Typecheck. Token-required properties (color / spacing / typography) raise a warning when authored as raw values — stays at warn level during the migration and tightens to error once Phase 5 finishes.

Builds on top of WB-2324.3 (PR #3063); no runtime behavior changes.

Reviewers: #frontend-infra-web

Issue: WB-2324

## Test plan:

1. `pnpm lint:css` — passes (0 errors, 1 pre-existing warning on `__docs__/wonder-blocks-typography/styles.css` for a hardcoded `color: red`).
2. Add a bogus at-rule body to `spike.module.css` (e.g. `@whatever-bogus-rule { color: red; }`): `pnpm lint:css` fails with `Unknown at-rule "@whatever-bogus-rule"` and warns about the raw color.
3. Revert → clean.
4. Watch CI on this branch: the new `Stylelint` step runs after `Eslint` and before `Typecheck` in node-ci-lint.yml.

## Review plan:

Please review these risky changes

1. 🚨 `.github/workflows/node-ci-lint.yml`: adds a new `Stylelint` job step (`pnpm lint:css`) between `Eslint` and `Typecheck`. Runs on every PR; a regression in the stylelint config or new false-positive rule could block unrelated work. `if: always()` so it doesn't short-circuit on an upstream failure.
2. ⚠️ `.stylelintrc.cjs` (new): extends `stylelint-config-standard` + `stylelint-config-css-modules`. Allow-lists `@apply`, `@mixin`, `@define-mixin`, `@include`, `@layer` so the PostCSS pipeline's at-rules pass. Disables `custom-property-pattern` (WB tokens use `--wb-camelCase_underscore` naming on purpose). Activates `scale-unlimited/declaration-strict-value` at *warning* level for color / background / border-color / padding / margin / font-size / font-family.
3. ⚠️ `package.json`: adds 4 external devDeps — `stylelint`, `stylelint-config-standard`, `stylelint-config-css-modules`, `stylelint-declaration-strict-value` — plus the `lint:css` script. New script becomes part of standard dev / CI workflow.

### Common patterns:

**1 File — declarative at-rule allow-list paired with the PostCSS pipeline:**

```js
// .stylelintrc.cjs — must match plugin order in postcss.config.cjs
"at-rule-no-unknown": [
    true,
    {
        ignoreAtRules: [
            "apply", "mixin", "define-mixin", "include", "layer",
        ],
    },
],
```

**1 File — CI job step that mirrors the existing Eslint / Typecheck shape:**

```yaml
# .github/workflows/node-ci-lint.yml
- name: Stylelint
  if: always()
  run: pnpm lint:css
```

🤖 Built using Claude Code #ai-generated
Base automatically changed from WB-2324 to deploy/css-modules May 29, 2026 18:22
Juan Andrade added 2 commits May 29, 2026 14:27
…odules

Adds the static-typing half of the CSS Modules migration tooling. `tsc --noEmit` now catches `styles.<missingClassName>` references, and VS Code / WebStorm get live autocomplete + Go-to-Definition on `*.module.css` imports.

Builds on top of `WB-2324` (PR #3059); no Rollup or runtime behavior changes.

Reviewers: #frontend-infra-web

Issue: WB-2324

## Test plan:

1. `pnpm typecheck` — passes; `prebuild`/`pretypecheck` regenerates `.d.ts` files first.
2. Introduce `styles.tpyo` in `__docs__/css-modules-spike/spike.tsx` → `pnpm typecheck` fails with `Property 'tpyo' does not exist on type '{ readonly pill: string; readonly root: string; }'. Did you mean 'root'?`.
3. Revert the typo → `pnpm typecheck` passes again.
4. `pnpm build` — runs `gen:css-types` first via the `prebuild` hook, then Rollup; build succeeds.
5. Open `__docs__/css-modules-spike/spike.tsx` in VS Code with the recommended extensions installed: `styles.` autocomplete shows `pill` and `root`; Go-to-Definition jumps to the matching CSS rule.

## Review plan:

Please review these risky changes

1. ⚠️ `package.json`: adds `typed-css-modules` + `typescript-plugin-css-modules` devDeps and wires `gen:css-types` into both `prebuild` and `pretypecheck`. Hooks run for every `pnpm build` and every `pnpm typecheck`, so a regression in `gen-css-types.cjs` would break CI.
2. ⚠️ `tsconfig.json`: adds the `typescript-plugin-css-modules` TS language-service plugin. Doesn't affect `tsc` output (plugins only influence the language service), but everyone using a TS-aware editor on this repo will start seeing the plugin's behavior.
3. 🔷 `build-settings/gen-css-types.cjs` (new): root-level script that globs `packages/**/*.module.css` + `__docs__/**/*.module.css` (ignoring `node_modules`/`dist`) and invokes `typed-css-modules`. Camel-casing left disabled until 0.4 picks the project-wide naming convention.

### Common patterns:

**2 Files — pre-script hooks ensuring `*.module.css.d.ts` exists before tsc / Rollup run:**

```jsonc
// package.json
+ "prebuild": "pnpm gen:css-types",
+ "gen:css-types": "node ./build-settings/gen-css-types.cjs",
...
+ "pretypecheck": "pnpm gen:css-types",
```

**1 File — gitignore the generated declarations (regenerated on every build):**

```
# Generated by `pnpm gen:css-types` (typed-css-modules) via the `prebuild` /
# `pretypecheck` hooks. Never checked in — fresh checkouts regenerate them.
**/*.module.css.d.ts
```

🤖 Built using Claude Code #ai-generated
Mirrors the .gitignore entry — these are codegen artifacts from `pnpm gen:css-types` and don't need to pass prettier or eslint checks. Without this, `pnpm lint` flags the generated declarations for inconsistent quoting + trailing newline.

🤖 Built using Claude Code #ai-generated
@jandrade jandrade merged commit 91827dc into deploy/css-modules May 29, 2026
14 of 15 checks passed
@jandrade jandrade deleted the WB-2324.3 branch May 29, 2026 18:35
jandrade pushed a commit that referenced this pull request May 29, 2026
Adds CSS-side hygiene to the migration. `pnpm lint:css` is now an explicit CI step alongside ESLint and Typecheck. Token-required properties (color / spacing / typography) raise a warning when authored as raw values — stays at warn level during the migration and tightens to error once Phase 5 finishes.

Builds on top of WB-2324.3 (PR #3063); no runtime behavior changes.

Reviewers: #frontend-infra-web

Issue: WB-2324

## Test plan:

1. `pnpm lint:css` — passes (0 errors, 1 pre-existing warning on `__docs__/wonder-blocks-typography/styles.css` for a hardcoded `color: red`).
2. Add a bogus at-rule body to `spike.module.css` (e.g. `@whatever-bogus-rule { color: red; }`): `pnpm lint:css` fails with `Unknown at-rule "@whatever-bogus-rule"` and warns about the raw color.
3. Revert → clean.
4. Watch CI on this branch: the new `Stylelint` step runs after `Eslint` and before `Typecheck` in node-ci-lint.yml.

## Review plan:

Please review these risky changes

1. 🚨 `.github/workflows/node-ci-lint.yml`: adds a new `Stylelint` job step (`pnpm lint:css`) between `Eslint` and `Typecheck`. Runs on every PR; a regression in the stylelint config or new false-positive rule could block unrelated work. `if: always()` so it doesn't short-circuit on an upstream failure.
2. ⚠️ `.stylelintrc.cjs` (new): extends `stylelint-config-standard` + `stylelint-config-css-modules`. Allow-lists `@apply`, `@mixin`, `@define-mixin`, `@include`, `@layer` so the PostCSS pipeline's at-rules pass. Disables `custom-property-pattern` (WB tokens use `--wb-camelCase_underscore` naming on purpose). Activates `scale-unlimited/declaration-strict-value` at *warning* level for color / background / border-color / padding / margin / font-size / font-family.
3. ⚠️ `package.json`: adds 4 external devDeps — `stylelint`, `stylelint-config-standard`, `stylelint-config-css-modules`, `stylelint-declaration-strict-value` — plus the `lint:css` script. New script becomes part of standard dev / CI workflow.

### Common patterns:

**1 File — declarative at-rule allow-list paired with the PostCSS pipeline:**

```js
// .stylelintrc.cjs — must match plugin order in postcss.config.cjs
"at-rule-no-unknown": [
    true,
    {
        ignoreAtRules: [
            "apply", "mixin", "define-mixin", "include", "layer",
        ],
    },
],
```

**1 File — CI job step that mirrors the existing Eslint / Typecheck shape:**

```yaml
# .github/workflows/node-ci-lint.yml
- name: Stylelint
  if: always()
  run: pnpm lint:css
```

🤖 Built using Claude Code #ai-generated
jandrade pushed a commit that referenced this pull request Jun 1, 2026
Adds CSS-side hygiene to the migration. `pnpm lint:css` is now an explicit CI step alongside ESLint and Typecheck. Token-required properties (color / spacing / typography) raise a warning when authored as raw values — stays at warn level during the migration and tightens to error once Phase 5 finishes.

Builds on top of WB-2324.3 (PR #3063); no runtime behavior changes.

Reviewers: #frontend-infra-web

Issue: WB-2324

## Test plan:

1. `pnpm lint:css` — passes (0 errors, 1 pre-existing warning on `__docs__/wonder-blocks-typography/styles.css` for a hardcoded `color: red`).
2. Add a bogus at-rule body to `spike.module.css` (e.g. `@whatever-bogus-rule { color: red; }`): `pnpm lint:css` fails with `Unknown at-rule "@whatever-bogus-rule"` and warns about the raw color.
3. Revert → clean.
4. Watch CI on this branch: the new `Stylelint` step runs after `Eslint` and before `Typecheck` in node-ci-lint.yml.

## Review plan:

Please review these risky changes

1. 🚨 `.github/workflows/node-ci-lint.yml`: adds a new `Stylelint` job step (`pnpm lint:css`) between `Eslint` and `Typecheck`. Runs on every PR; a regression in the stylelint config or new false-positive rule could block unrelated work. `if: always()` so it doesn't short-circuit on an upstream failure.
2. ⚠️ `.stylelintrc.cjs` (new): extends `stylelint-config-standard` + `stylelint-config-css-modules`. Allow-lists `@apply`, `@mixin`, `@define-mixin`, `@include`, `@layer` so the PostCSS pipeline's at-rules pass. Disables `custom-property-pattern` (WB tokens use `--wb-camelCase_underscore` naming on purpose). Activates `scale-unlimited/declaration-strict-value` at *warning* level for color / background / border-color / padding / margin / font-size / font-family.
3. ⚠️ `package.json`: adds 4 external devDeps — `stylelint`, `stylelint-config-standard`, `stylelint-config-css-modules`, `stylelint-declaration-strict-value` — plus the `lint:css` script. New script becomes part of standard dev / CI workflow.

### Common patterns:

**1 File — declarative at-rule allow-list paired with the PostCSS pipeline:**

```js
// .stylelintrc.cjs — must match plugin order in postcss.config.cjs
"at-rule-no-unknown": [
    true,
    {
        ignoreAtRules: [
            "apply", "mixin", "define-mixin", "include", "layer",
        ],
    },
],
```

**1 File — CI job step that mirrors the existing Eslint / Typecheck shape:**

```yaml
# .github/workflows/node-ci-lint.yml
- name: Stylelint
  if: always()
  run: pnpm lint:css
```

🤖 Built using Claude Code #ai-generated
jandrade added a commit that referenced this pull request Jun 1, 2026
…ng (#3069)

## Summary:
Adds CSS-side hygiene to the migration. `pnpm lint:css` is now an explicit CI step alongside ESLint and Typecheck. Highlights:

- **At-rule allow-list** lets `@apply`, `@mixin`, `@define-mixin`, `@include`, `@layer` pass through (matches `postcss.config.cjs`).
- **`selector-class-pattern`** enforces camelCase — `.root`, `.iconWrapper`, etc.
- **Token-required properties** (color / spacing / typography) raise a *warning* when authored as raw values; tightens to error once Phase 5 finishes.
- **`csstools/value-no-unknown-custom-properties`** rejects any `var(--foo)` that doesn't resolve to a token published by `@khanacademy/wonder-blocks-tokens` (reads `packages/wonder-blocks-tokens/dist/css/index.css`).
- **`prelint:css`** runs `turbo build:css` so the merged token CSS is fresh before stylelint reads it (CI already builds first; locally turbo's cache makes warm runs near-instant).

Builds on top of WB-2324.3 (PR #3063); no runtime behavior changes.

Reviewers: jandrade, claude[bot], beaesguerra, jeresig

Issue: WB-2324

## Test plan:

1. `pnpm lint:css` — passes (0 errors, 1 pre-existing warning on `__docs__/wonder-blocks-typography/styles.css` for a hardcoded `color: red`).
2. Add a bogus at-rule body to `spike.module.css` (e.g. `@whatever-bogus-rule { color: red; }`): `pnpm lint:css` fails with `Unknown at-rule "@whatever-bogus-rule"` and warns about the raw color.
3. Change a `var(--wb-sizing-size_080)` reference to `var(--wb-totally-bogus-token)` in `spike.module.css`: `pnpm lint:css` fails with `Unexpected custom property "--wb-totally-bogus-token"`.
4. Rename a CSS Modules class to kebab-case (e.g. `.pill-badge`): `pnpm lint:css` fails with `Expected ".pill-badge" to match pattern "^[a-z]+([A-Z][a-z0-9]+)*$"`.
5. Revert all of the above → clean.
6. Watch CI on this branch: the new `Stylelint` step runs after `Eslint` and before `Typecheck` in `node-ci-lint.yml`.

## Review plan:

Please review these risky changes — each links to its inline comment:

1. 🚨 [`.github/workflows/node-ci-lint.yml`](#3069 (comment))
2. ⚠️ [`.stylelintrc.cjs`](#3069 (comment))
3. ⚠️ [`package.json`](#3069 (comment))

### Common patterns:

**1 File — declarative at-rule allow-list paired with the PostCSS pipeline:**

```js
// .stylelintrc.cjs — must match plugin order in postcss.config.cjs
"at-rule-no-unknown": [
    true,
    {
        ignoreAtRules: [
            "apply", "mixin", "define-mixin", "include", "layer",
        ],
    },
],
```

**1 File — token-anchored `var()` validation:**

```js
// .stylelintrc.cjs
"csstools/value-no-unknown-custom-properties": [
    true,
    {
        importFrom: [
            "./packages/wonder-blocks-tokens/dist/css/index.css",
        ],
    },
],
```

**1 File — CI job step that mirrors the existing Eslint / Typecheck shape:**

```yaml
# .github/workflows/node-ci-lint.yml
- name: Stylelint
  if: always()
  run: pnpm lint:css
```

🤖 Built using Claude Code #ai-generated

Author: jandrade

Required Reviewers:

Approved By: jeresig, beaesguerra

Checks: ✅ 13 checks were successful, ⏭️  3 checks have been skipped

Pull Request URL: #3069
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants