Skip to content

🚨 [WB-2324.4] Phase 0.1 β€” Rollup CSS emission + migrate Icon to CSS Modules#3071

Open
jandrade wants to merge 5 commits into
WB-2324.4from
WB-2324.1
Open

🚨 [WB-2324.4] Phase 0.1 β€” Rollup CSS emission + migrate Icon to CSS Modules#3071
jandrade wants to merge 5 commits into
WB-2324.4from
WB-2324.1

Conversation

@jandrade
Copy link
Copy Markdown
Member

Summary

Wires the build side of the CSS Modules pipeline and migrates the first real component (wonder-blocks-icon) as the end-to-end proof. Closes the last remaining sub-task of Phase 0.

Builds on top of #3069 (Phase 0.4 β€” Stylelint). First PR in the chain that has visible runtime behavior changes.

Issue: WB-2324

Test plan

  • pnpm build succeeds end-to-end.
  • cat packages/wonder-blocks-icon/dist/index.css shows the size rules wrapped in @layer shared { … } with hashed selectors (.wbIcon_moduleSmall<hash>, etc.).
  • head -3 packages/wonder-blocks-icon/dist/es/index.js shows import "./index.css"; as the first line; head -3 …/dist/index.js shows require("./index.css"); after 'use strict'.
  • pnpm jest packages/wonder-blocks-icon passes β€” Icon tests assert behavior (ref forwarding, ARIA, accessibility), not class names.
  • pnpm typecheck passes; the new icon.module.css.d.ts is generated by the existing pretypecheck hook.
  • pnpm lint:css clean β€” the new icon.module.css passes max-nesting-depth, color-no-hex, selector-class-pattern, and value-no-unknown-custom-properties from PR 🚨 [WB-2324.3] Phase 0.4 β€” Stylelint with token-strict-value + CI wiringΒ #3069.
  • (Manual) Open Storybook Icon stories: small / medium / large / xlarge variants render at the expected sizes, no visual regression vs. main.

Review plan

Please review these risky changes:

  1. 🚨 build-settings/rollup.config.mjs: adds rollup-plugin-styles to the plugin chain before swc. Emits per-package dist/index.css for any package that contains a *.module.css file. Injects a side-effect import "./index.css" (ESM) / require("./index.css") (CJS) at the top of those packages' JS bundles so npm consumers get styles automatically. SSR consumers that can't process CSS imports may need a loader / mock β€” standard webpack / Vite / Next.js setups handle this out of the box.
  2. 🚨 postcss.config.cjs: adds a wrap-in-layer PostCSS plugin that moves every top-level rule into @layer shared { … }. Detects already-wrapped CSS and skips to avoid @layer shared { @layer shared { … } } nesting. Shared by Vite (Storybook) and Rollup (build) so authoring stays consistent.
  3. ⚠️ packages/wonder-blocks-icon/package.json: adds exports map (. + ./css) and sideEffects: ["**/*.css"]. Published-surface change patterned after wonder-blocks-tokens.
  4. ⚠️ packages/wonder-blocks-icon/src/components/icon.tsx: migrates from StyleSheet.create + getSize to className={styles[size]}. The component still uses addStyle("div") so the style prop continues to accept Aphrodite StyleType values; only the internal size styles moved to CSS Modules.
  5. πŸ”· packages/wonder-blocks-icon/src/components/icon.module.css (new): the size classes themselves. Authors don't write @layer shared { … } here β€” the build wraps it.
  6. πŸ”· __docs__/css-modules-spike/spike.module.css: dropped the manual @layer shared { … } wrap since the build now does it. The PostCSS plugin's already-wrapped detection would have skipped re-wrapping anyway, but cleaning the source keeps authoring consistent across the codebase.

cc: @Khan/frontend-infra-web

πŸ€– Built using Claude Code #ai-generated

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 22, 2026

πŸ¦‹ Changeset detected

Latest commit: 6fe971c

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

This PR includes changesets to release 20 packages
Name Type
@khanacademy/wonder-blocks-icon Patch
@khanacademy/wonder-blocks-accordion Patch
@khanacademy/wonder-blocks-badge Patch
@khanacademy/wonder-blocks-banner Patch
@khanacademy/wonder-blocks-birthday-picker Patch
@khanacademy/wonder-blocks-button Patch
@khanacademy/wonder-blocks-date-picker Patch
@khanacademy/wonder-blocks-dropdown Patch
@khanacademy/wonder-blocks-form Patch
@khanacademy/wonder-blocks-icon-button Patch
@khanacademy/wonder-blocks-link Patch
@khanacademy/wonder-blocks-search-field Patch
@khanacademy/wonder-blocks-switch Patch
@khanacademy/wonder-blocks-tabs Patch
eslint-plugin-wonder-blocks-demo Patch
@khanacademy/wonder-blocks-card Patch
@khanacademy/wonder-blocks-modal Patch
@khanacademy/wonder-blocks-popover Patch
@khanacademy/wonder-blocks-pill Patch
@khanacademy/wonder-blocks-tooltip Patch

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

@khan-actions-bot khan-actions-bot requested a review from a team May 22, 2026 20:57
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

Size Change: +51 B (+0.04%)

Total Size: 129 kB

πŸ“¦ View Changed
Filename Size Change
packages/wonder-blocks-icon/dist/es/index.js 1.95 kB +38 B (+1.99%)
packages/wonder-blocks-labeled-field/dist/es/index.js 3.48 kB +13 B (+0.37%)
ℹ️ 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-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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

npm Snapshot: Published

πŸŽ‰ Good news!! We've packaged up the latest commit from this PR (40d5e83) and published all packages with changesets to npm.

You can install the packages in frontend by running:

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

Packages can also be installed manually by running:

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

A new build was pushed to Chromatic! πŸš€

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

Chromatic results:

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

@jandrade jandrade changed the title 🚨 [WB-2324] Phase 0.1 β€” Rollup CSS emission + migrate Icon to CSS Modules 🚨 [WB-2324.4] Phase 0.1 β€” Rollup CSS emission + migrate Icon to CSS Modules May 22, 2026
@jandrade jandrade requested a review from a team May 22, 2026 21:06
Comment thread build-settings/rollup.config.mjs Outdated
@beaesguerra
Copy link
Copy Markdown
Member

@claude review once

Comment on lines +23 to +29
"**/*.css"
],
"exports": {
".": {
"import": "./dist/es/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

πŸ”΄ The new conditional exports map lists the types condition last, but per the TypeScript handbook it must come first β€” Node-style condition resolution is order-sensitive and the first match wins, so TS consumers on moduleResolution: "node16"/"nodenext" will resolve import β†’ ./dist/es/index.js and never reach the types key. The top-level types field is ignored once exports is set under modern resolution, so this is a real published-surface bug. wonder-blocks-tokens has the same broken pattern (which is presumably where this was copied from), but since this PR introduces the first exports map of the migration and more packages will be migrated using it as the template, it's worth fixing the precedent here before it propagates. Trivial fix: move "types" to be the first key inside the "." object.

Extended reasoning...

What the bug is

packages/wonder-blocks-icon/package.json adds a new conditional exports map:

"exports": {
  ".": {
    "import": "./dist/es/index.js",
    "require": "./dist/index.js",
    "types": "./dist/index.d.ts"
  },
  "./css": "./dist/index.css"
}

The types condition is listed last. Per the TypeScript handbook on package.json exports:

...the "types" condition should always come first in "exports".

This is because Node-style conditional resolution is order-sensitive β€” the resolver walks the object's keys in declaration order and returns on the first matching condition. import and require are both "always-on" for the relevant module systems, so once one of them matches the resolver returns the .js path and never reaches types.

Step-by-step proof

Consider a downstream consumer with tsconfig.json moduleResolution: "node16" (or "nodenext") running import {Icon} from '@khanacademy/wonder-blocks-icon':

  1. The TS resolver reads packages/wonder-blocks-icon/package.json and finds the exports key. Per modern resolution, once exports is set, the top-level types field is ignored for this subpath.
  2. The resolver enters the "." entry and walks the conditions in declaration order: import, require, types.
  3. The consumer's module system is ESM, so the import condition matches first β†’ resolves to ./dist/es/index.js.
  4. Resolution returns immediately. The types condition is never evaluated.
  5. TS now looks for declarations adjacent to ./dist/es/index.js β€” i.e. ./dist/es/index.d.ts. But tsconfig-build.json sets outDir: "./dist" / rootDir: "src", so declarations are actually emitted to ./dist/index.d.ts, not ./dist/es/index.d.ts.
  6. Result: under strict node16/nodenext resolution, the consumer fails to find types for @khanacademy/wonder-blocks-icon.

If the order were types β†’ import β†’ require, step 2 would return ./dist/index.d.ts immediately and the consumer would get correct typings.

Why existing code doesn't prevent it

The top-level "types": "dist/index.d.ts" field is still present, which rescues old resolvers (moduleResolution: "node"/"node10") β€” but modern resolution modes (node16, nodenext, bundler) prefer the exports map and skip the top-level types field once exports is defined. This is exactly the failure mode publint / @arethetypeswrong/cli flag.

Why wonder-blocks-tokens having the same pattern isn't a defense

wonder-blocks-tokens/package.json (lines 21-28) has the same broken order, and yes β€” the new code was clearly patterned after it (the PR description even calls this out: "Published-surface change patterned after wonder-blocks-tokens."). But propagating a bug isn't a reason to keep doing it. Two reasons to fix here:

  1. This PR is Phase 0.1 of a multi-phase migration. The description and changeset both make clear that many more packages will get exports maps copied from this template. Fixing the precedent now means future packages get it right by default; leaving it means the same bug ships in every future PR in the chain.
  2. wonder-blocks-tokens having shipped without obvious breakage doesn't prove the issue is harmless β€” it proves either (a) most downstream consumers happen to use the lenient bundler resolution mode which falls back gracefully, or (b) consumers who hit it work around it silently. Neither is a reason to enshrine the pattern.

Impact

For TS consumers on moduleResolution: "node16" or "nodenext" (the recommended modes for modern Node-targeted packages), types may fail to resolve, producing implicit-any imports or hard errors depending on strict settings. Bundler-mode consumers are mostly OK because that resolver is lenient about types, but they'd still flag this with publint/are-the-types-wrong.

Fix

Move "types" to be the first key inside the "." exports object:

"exports": {
  ".": {
    "types": "./dist/index.d.ts",
    "import": "./dist/es/index.js",
    "require": "./dist/index.js"
  },
  "./css": "./dist/index.css"
}

Worth applying the same fix to wonder-blocks-tokens/package.json in a follow-up so the pattern is consistent across the codebase.

Comment thread postcss.config.cjs
Comment on lines 60 to 65

return (
<StyledDiv
style={[getSize(size), style]}
className={styles[size]}
style={style}
id={id}
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: className={styles[size]} returns undefined for any size outside the IconSize union, leaving the icon with no width/height class and collapsing it to 0Γ—0. The previous getSize used case "small": default: so invalid values silently fell back to small. TypeScript blocks this for typed consumers, but JS-only consumers (or anyone casting to any) lose the fallback β€” a one-line preservation of prior behavior would be className={styles[size] ?? styles.small}.

Extended reasoning...

What changes

The old getSize had a case "small": default: arm, so any size value that wasn't one of the four recognized strings silently rendered as small. The new code uses direct property access:

<StyledDiv className={styles[size]} ... />

For any size not in {small, medium, large, xlarge}, styles[size] is undefined. React strips an undefined className, so the div renders with no class. Since width/height come only from the size classes (the cloned child gets width: 100%; height: 100%), the icon collapses to 0Γ—0.

Why the existing safeguards don't catch this

  • The destructuring default size = "small" (icon.tsx:45) only fires when size === undefined. It does not fire for null, "", or any other invalid string.
  • The IconSize union in packages/wonder-blocks-icon/src/types.ts:11 blocks invalid values at compile time for TypeScript consumers, but is erased at runtime β€” a JS consumer or anyone using as any / @ts-ignore reaches the bad path.

Step-by-step proof

  1. JS consumer calls <Icon size={someDynamicValue}> where someDynamicValue is "xxlarge" (perhaps a refactor leftover, a value from a server response, or a typo).
  2. The destructuring size = "small" does not apply because "xxlarge" is not undefined.
  3. styles["xxlarge"] evaluates to undefined (no such export from the CSS module).
  4. React renders <div className={undefined}> β†’ <div> with no width/height class.
  5. The child element has width: 100%; height: 100% β€” but the parent has no intrinsic size, so the percentage resolves to 0.
  6. Icon renders as 0Γ—0 instead of the pre-PR behavior (silently small).

Addressing the refutation

The refutation argues this is intentional and that visible failure beats silent fallback. Both are reasonable in isolation, but two things weaken that case:

  1. The PR's own changeset explicitly says "same props, same DOM output" β€” invalid runtime input is one place where that claim no longer holds, and that's worth either fixing or noting in the changeset.
  2. The fix is a single character (?? and a fallback): className={styles[size] ?? styles.small}. Preserving the prior contract costs almost nothing.

If the team prefers the new visible-failure behavior as a deliberate design choice, that's a defensible call β€” but it should be documented as an intentional behavior change in the changeset rather than presented as a pure DOM-equivalent refactor.

Severity

nit β€” TypeScript users are protected; only affects JS consumers or type-bypass cases. Not blocking, but flagging because the fix is trivial and the PR description currently overclaims behavioral equivalence.

Comment thread postcss.config.cjs Outdated
Comment on lines +1 to +19
// Root PostCSS config β€” picked up by Vite (Storybook) and Rollup
// (`rollup-plugin-postcss` in `build-settings/rollup.config.mjs`).
//
// Plugin order matters:
// 1. postcss-import β€” inline @import (incl. cross-package paths
// resolved through the workspace).
// 2. @csstools/postcss-mixins β€” expand @define-mixin / @mixin before any
// downstream transform sees them.
// 3. wrap-in-layer β€” move every remaining top-level rule into
// `@layer shared { … }` so authors never
// write the wrap by hand. Layer order is
// declared once in
// `.storybook/preview-head.html`
// (`@layer reset, tokens, shared, overrides`).
//
// CSS Modules class-name hashing is handled by the consumer:
// - Vite handles `*.module.css` natively (its built-in CSS Modules pass
// runs *after* this PostCSS chain).
// - Rollup uses `rollup-plugin-postcss`'s `modules` option.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟑 Comments on lines 2 and 19 of postcss.config.cjs reference rollup-plugin-postcss, but build-settings/rollup.config.mjs imports a different package β€” rollup-plugin-styles β€” and that's also what's listed in package.json devDependencies. Pure docs nit, no runtime impact, but will mislead a maintainer grepping for the plugin name. Suggested fix: s/rollup-plugin-postcss/rollup-plugin-styles/g in those two comment lines.

Extended reasoning...

What the bug is

The comment block at the top of postcss.config.cjs (introduced in this PR) names the wrong Rollup plugin in two places:

  • Line 2: // (rollup-plugin-postcssinbuild-settings/rollup.config.mjs).
  • Line 19: // - Rollup uses rollup-plugin-postcss's modules option.

The plugin actually wired in by this PR is rollup-plugin-styles, not rollup-plugin-postcss. These are two different npm packages by different authors (Anidetrix/rollup-plugin-styles vs. egoist/rollup-plugin-postcss) with different APIs and option shapes.

Proof β€” walk through the repo

  1. build-settings/rollup.config.mjs line 8: import styles from "rollup-plugin-styles";
  2. package.json line 161 (in the diff): "rollup-plugin-styles": "^4.0.0" β€” listed in devDependencies.
  3. pnpm-lock.yaml resolves rollup-plugin-styles@4.0.0 (not rollup-plugin-postcss).
  4. Grepping the repo for rollup-plugin-postcss turns up only the two comment lines in postcss.config.cjs β€” no actual import, no dependency entry, no usage.

So the comment is internally inconsistent with the very file (build-settings/rollup.config.mjs) it points at.

Impact

Zero runtime impact β€” it's a comment block. The harm is purely cognitive: a future maintainer reading postcss.config.cjs to understand how the chain is wired will look for rollup-plugin-postcss in the rollup config, won't find it, and will waste time. The line 19 reference (rollup-plugin-postcss's modules option) is also slightly misleading because both plugins happen to expose a modules option but with different shapes, so a maintainer cross-referencing docs could end up reading the wrong project's README.

Fix

s/rollup-plugin-postcss/rollup-plugin-styles/g in those two lines of postcss.config.cjs. No code change needed.

Copy link
Copy Markdown
Member

@beaesguerra beaesguerra left a comment

Choose a reason for hiding this comment

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

First component to use CSS Modules, nice! Left some more questions πŸ˜„

Comment thread packages/wonder-blocks-icon/src/components/icon.module.css
return (
<StyledDiv
style={[getSize(size), style]}
className={styles[size]}
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 was curious to see if the typecheck would catch if there was a mismatch between the class names and size names, and it did πŸŽ‰

I tested this by renaming the xlarge typed string to extraLarge and also tried renaming the class name. When I renamed the class name though, I had to re-run pnpm gen:css-types in order to update the types for the stlyes (which makes sense!).

Would it make sense to include pnpm gen:css-types as part of the pnpm dev script so that the css types are updated automatically? (Let me know too if there's already something that should be doing that!)

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.

It is already running as part of the prebuild script, which is invoked at some point by dev :)

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.

Oh cool! I was going to try it out again, but am running into errors now when running pnpm dev:

turbo 2.5.3

β€’ Packages in scope: //, @khanacademy/eslint-plugin-wonder-blocks, @khanacademy/wb-codemod, @khanacademy/wb-dev-build-settings, @khanacademy/wonder-blocks-accordion, @khanacademy/wonder-blocks-announcer, @khanacademy/wonder-blocks-badge, @khanacademy/wonder-blocks-banner, @khanacademy/wonder-blocks-birthday-picker, @khanacademy/wonder-blocks-breadcrumbs, @khanacademy/wonder-blocks-button, @khanacademy/wonder-blocks-card, @khanacademy/wonder-blocks-cell, @khanacademy/wonder-blocks-clickable, @khanacademy/wonder-blocks-core, @khanacademy/wonder-blocks-data, @khanacademy/wonder-blocks-date-picker, @khanacademy/wonder-blocks-dropdown, @khanacademy/wonder-blocks-form, @khanacademy/wonder-blocks-grid, @khanacademy/wonder-blocks-icon, @khanacademy/wonder-blocks-icon-button, @khanacademy/wonder-blocks-labeled-field, @khanacademy/wonder-blocks-layout, @khanacademy/wonder-blocks-link, @khanacademy/wonder-blocks-modal, @khanacademy/wonder-blocks-pill, @khanacademy/wonder-blocks-popover, @khanacademy/wonder-blocks-progress-spinner, @khanacademy/wonder-blocks-search-field, @khanacademy/wonder-blocks-styles, @khanacademy/wonder-blocks-switch, @khanacademy/wonder-blocks-tabs, @khanacademy/wonder-blocks-testing, @khanacademy/wonder-blocks-testing-core, @khanacademy/wonder-blocks-theming, @khanacademy/wonder-blocks-timing, @khanacademy/wonder-blocks-tokens, @khanacademy/wonder-blocks-toolbar, @khanacademy/wonder-blocks-tooltip, @khanacademy/wonder-blocks-typography, eslint-plugin-wonder-blocks-demo
β€’ Running build:css in 42 packages
β€’ Remote caching disabled
  Γ— Failed to connect to daemon.
  ╰─▢ unable to connect to daemon after 3 retries

I made sure to run pnpm i first to make sure deps are up to date!

Comment thread packages/wonder-blocks-icon/package.json
Comment thread package.json Outdated
Comment thread build-settings/rollup.config.mjs
Comment thread build-settings/rollup.config.mjs Outdated
Comment thread build-settings/rollup.config.mjs Outdated
Comment on lines +8 to +10
output. Consumers using `@khanacademy/wonder-blocks-icon` automatically
pick up the bundled `dist/index.css` via a side-effect import in the
JS entry; SSR consumers that can't process CSS imports may need a CSS
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.

Do consuming apps/libraries need to update anything in their project for these changes to work? If so, should this be considered a breaking change?

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.

Good question! this should be transparent for consumers :) as they are still importing the JS bundles that reference these CSS files.

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.

Sounds good!

Will there be a major version release for something in WB? Since consuming apps would need to set up postcss/bundling before using new versions of certain packages? Maybe just the styles package? Or maybe not since the new css mixin is additive!

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.

My plan is the following:

  1. Configure WB tooling
  2. Start migrating WB components
  3. Configure consumer repos tooling
  4. Install WB versions with CSS modules support in consumer repos

#3 can be done before #2, so I think minor/patch versions would work transparently for consumer apps as I'd expect that at that point (#4) all the tooling/infra should be configured in advance. Agree about the styles package, as mixins are additive, and I'm planning to configure mixins in consumer repos as part of #3 as well.

@jandrade
Copy link
Copy Markdown
Member Author

The integration with consumers can be seen here: https://github.com/Khan/frontend/pull/11572

This includes the WB Icon component using CSS modules. The mixins part (--wb-focus-visible) can only be integrated once the consumer repo defines a postcss pipeline (and includes a config file that is capable of processing the postcss-mixins plugin).

For reference on the config, see: https://github.com/Khan/wonder-blocks/blob/WB-2324.1/postcss.config.cjs#L76

Screenshot 2026-05-26 at 12 26 55β€―PM

cc @Khan/wonder-blocks @Khan/frontend-infra-web

@jandrade jandrade requested a review from beaesguerra May 27, 2026 14:31
Copy link
Copy Markdown
Member

@jeresig jeresig left a comment

Choose a reason for hiding this comment

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

Looks good, thank you!

return (
<StyledDiv
style={[getSize(size), style]}
className={sizeStyles}
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 wonder if you're going to want to add support for something like clsx https://www.npmjs.com/package/clsx to our addStyle logic so that we can handle cases like className={["class1", condition && "class2"]}.

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.

oh yeah, thanks for reminding me of that! I've considered clsx for that, but I'm also exploring other alternatives around that problem.

As a short term solution around the addStyle logic, my goal is to refactor the internals of that function so we can incrementally migrate to CSS modules using the existing style prop. You can see how this is going to work here: b1cb814#diff-56c6348fe785a54784ccb372f1a9f9f946e2520366035f7957f2b2177544fcd5

As for supporting libraries like clsx, I'm planning to write a new ADR to evaluate different tools: so far, I've contemplated clsx, classnames and cva.

You can see some research I quickly did with ChatGPT some weeks ago: https://chatgpt.com/share/e/69f3ab89-9440-800f-b0fc-a5ee94613fd5

Juan Andrade added 5 commits May 29, 2026 14:51
…n to CSS Modules

Wires the build side of the CSS Modules pipeline and migrates the first real component as the end-to-end proof. Closes the last remaining sub-task of Phase 0.

Builds on top of WB-2324.4 (PR #3069); first PR in the chain that has visible runtime behavior changes.

Reviewers: #frontend-infra-web

Issue: WB-2324

1. `pnpm build` succeeds end-to-end.
2. `cat packages/wonder-blocks-icon/dist/index.css` shows the size rules wrapped in `@layer shared { … }` with hashed selectors (`wb-icon_module-small-<hash>`, etc.).
3. `head -3 packages/wonder-blocks-icon/dist/es/index.js` shows `import "./index.css";` as the first line; `head -3 …/dist/index.js` shows `require("./index.css");` after `'use strict'`.
4. `pnpm jest packages/wonder-blocks-icon` passes β€” Icon tests assert behavior (ref forwarding, ARIA, accessibility), not class names.
5. `pnpm typecheck` passes; the new `icon.module.css.d.ts` is generated by the existing `pretypecheck` hook.
6. `pnpm lint:css` clean (the spike CSS continues to pass; the new `icon.module.css` passes too).
7. (Manual) Open Storybook `Icon` stories: small / medium / large / xlarge variants render at the expected sizes, no visual regression vs. main.

Please review these risky changes

1. 🚨 `build-settings/rollup.config.mjs`: adds `rollup-plugin-styles` to the plugin chain before `swc`. Emits per-package `dist/index.css` for any package that contains a `*.module.css` file. Injects a side-effect `import "./index.css"` (ESM) / `require("./index.css")` (CJS) at the top of those packages' JS bundles so npm consumers get styles automatically. SSR consumers that can't process CSS imports may need a loader / mock β€” standard webpack / Vite / Next.js setups handle this out of the box.
2. 🚨 `postcss.config.cjs`: adds a `wrap-in-layer` PostCSS plugin that moves every top-level rule into `@layer shared { … }`. Detects already-wrapped CSS and skips to avoid `@layer shared { @layer shared { … } }` nesting. Shared by Vite (Storybook) and Rollup (build) so authoring stays consistent.
3. ⚠️ `packages/wonder-blocks-icon/package.json`: adds `exports` map (`.` + `./css`) and `sideEffects: ["**/*.css"]`. Published-surface change patterned after `wonder-blocks-tokens`.
4. ⚠️ `packages/wonder-blocks-icon/src/components/icon.tsx`: migrates from `StyleSheet.create` + `getSize` to `className={styles[size]}`. The component still uses `addStyle("div")` so the `style` prop continues to accept Aphrodite `StyleType` values; only the *internal* size styles moved to CSS Modules.
5. πŸ”· `packages/wonder-blocks-icon/src/components/icon.module.css` (new): the size classes themselves. Authors don't write `@layer shared { … }` here β€” the build wraps it.
6. πŸ”· `__docs__/css-modules-spike/spike.module.css`: dropped the manual `@layer shared { … }` wrap since the build now does it. The PostCSS plugin's already-wrapped detection would have skipped re-wrapping anyway, but cleaning the source keeps authoring consistent across the codebase.

πŸ€– Built using Claude Code #ai-generated
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.

3 participants