-
Notifications
You must be signed in to change notification settings - Fork 461
feat(ui): migrate Mosaic Button to StyleX #9208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
alexcarpenter
wants to merge
5
commits into
main
Choose a base branch
from
migrate-mosaic-to-stylex
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
d70595f
feat(ui): migrate Mosaic Button to StyleX
alexcarpenter 722577d
ci(repo): dedupe lockfile and ignore dist-mosaic in prettier
alexcarpenter d9b55a4
refactor(ui): use shadcn color token names in Mosaic
alexcarpenter 7b64fd0
fix(ui): build styles.css as part of build so its export resolves
alexcarpenter a8c6010
fix(ui): correct Button story source path and assert default type
alexcarpenter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
|
|
||
| # compiled output | ||
| dist | ||
| dist-mosaic | ||
| tmp | ||
| out-tsc | ||
| out | ||
|
|
||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ | |
| build | ||
| coverage | ||
| dist | ||
| dist-mosaic | ||
| /packages/nextjs/examples | ||
| node_modules | ||
| package-lock.json | ||
|
|
||
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,41 @@ | ||
| import { createRequire } from 'module'; | ||
| import { dirname, resolve } from 'path'; | ||
| import { fileURLToPath } from 'url'; | ||
|
|
||
| const require = createRequire(import.meta.url); | ||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| // StyleX CSS extraction. The `@stylexjs/postcss-plugin` scans the Mosaic source, runs the | ||
| // StyleX babel transform itself, and replaces the `@stylex;` directive in `globals.css` with | ||
| // the generated CSS (token `:root` defaults + atoms). This is the CSS half of the setup; the | ||
| // JS half is the unplugin in `next.config.mjs`. Both must use the SAME StyleX babel version | ||
| // and options (`dev`, `rootDir`) so the atom class hashes line up. | ||
| const uiRoot = resolve(__dirname, '../ui'); | ||
|
|
||
| export default { | ||
| plugins: { | ||
| '@stylexjs/postcss-plugin': { | ||
| // `../ui/src/mosaic` is consumed from source (aliased), so point the scanner at it | ||
| // explicitly — auto-discovery only walks the package's own tree and direct deps. | ||
| include: [resolve(uiRoot, 'src/mosaic/**/*.{ts,tsx}')], | ||
| useCSSLayers: false, | ||
| babelConfig: { | ||
| babelrc: false, | ||
| configFile: false, | ||
| presets: [require('@babel/preset-typescript')], | ||
| plugins: [ | ||
| require('@babel/plugin-syntax-jsx'), | ||
| [ | ||
| require('@stylexjs/babel-plugin'), | ||
| { | ||
| dev: process.env.NODE_ENV !== 'production', | ||
| runtimeInjection: false, | ||
| unstable_moduleResolution: { type: 'commonJS', rootDir: uiRoot }, | ||
| }, | ||
| ], | ||
| ], | ||
| }, | ||
| }, | ||
| '@tailwindcss/postcss': {}, | ||
| }, | ||
| }; |
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| import { mergeProps, themeProps } from '../props'; | ||
|
|
||
| // The public styling contract lives here, not in any one component: the stable | ||
| // `.cl-<slot>` class, `data-<axis>` variant reflection, and the class/style merge | ||
| // order that lets a consumer's `className`/`style` win. Components just wire their | ||
| // props into these helpers, so this is where the contract is exhaustively pinned. | ||
|
|
||
| describe('themeProps', () => { | ||
| it('returns the stable slot class', () => { | ||
| expect(themeProps('button')).toEqual({ className: 'cl-button' }); | ||
| }); | ||
|
|
||
| it('reflects each variant axis as a data-<axis> attribute', () => { | ||
| expect(themeProps('button', { intent: 'destructive', variant: 'outline' })).toEqual({ | ||
| className: 'cl-button', | ||
| 'data-intent': 'destructive', | ||
| 'data-variant': 'outline', | ||
| }); | ||
| }); | ||
|
|
||
| it('kebab-cases camelCase axis names', () => { | ||
| expect(themeProps('button', { fullWidth: 'yes' })).toHaveProperty('data-full-width', 'yes'); | ||
| }); | ||
|
|
||
| it('reflects a true boolean as an empty-string presence attribute', () => { | ||
| expect(themeProps('button', { fullWidth: true })).toHaveProperty('data-full-width', ''); | ||
| }); | ||
|
|
||
| it('skips false, null, and undefined axes', () => { | ||
| expect(themeProps('button', { a: false, b: null, c: undefined })).toEqual({ className: 'cl-button' }); | ||
| }); | ||
|
|
||
| it('stringifies numeric axis values', () => { | ||
| expect(themeProps('button', { size: 0 })).toHaveProperty('data-size', '0'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('mergeProps', () => { | ||
| it('concatenates className across bags, base first', () => { | ||
| const merged = mergeProps({ className: 'cl-button' }, { className: 'x1 x2' }); | ||
| expect(merged.className).toBe('cl-button x1 x2'); | ||
| }); | ||
|
|
||
| it('accepts a trailing string as an appended className', () => { | ||
| const merged = mergeProps({ className: 'cl-button' }, { className: 'x1' }, 'consumer'); | ||
| expect(merged.className).toBe('cl-button x1 consumer'); | ||
| }); | ||
|
|
||
| it('accepts a trailing object as a style and lets it win', () => { | ||
| const merged = mergeProps({ style: { marginTop: '2px', color: 'red' } }, undefined, { marginTop: '8px' }); | ||
| expect(merged.style).toEqual({ marginTop: '8px', color: 'red' }); | ||
| }); | ||
|
|
||
| it('disambiguates the trailing className and style pair by position', () => { | ||
| const merged = mergeProps({ className: 'cl-button' }, undefined, 'consumer', { marginTop: '8px' }); | ||
| expect(merged.className).toBe('cl-button consumer'); | ||
| expect(merged.style).toEqual({ marginTop: '8px' }); | ||
| }); | ||
|
|
||
| it('treats a string second argument as a className', () => { | ||
| expect(mergeProps({ className: 'cl-button' }, 'consumer').className).toBe('cl-button consumer'); | ||
| }); | ||
|
|
||
| it('drops className entirely when nothing contributes one', () => { | ||
| expect(mergeProps({ 'data-intent': 'primary' }, {})).not.toHaveProperty('className'); | ||
| }); | ||
|
|
||
| it('preserves non-class/style props from both bags', () => { | ||
| const merged = mergeProps({ 'data-intent': 'primary' }, { role: 'button' }); | ||
| expect(merged).toMatchObject({ 'data-intent': 'primary', role: 'button' }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: clerk/javascript
Length of output: 26821
🌐 Web query:
@stylexjs/unplugin webpack does it emit css runtimeInjection extraction mode postcss plugin💡 Result:
The
@stylexjs/unpluginfor Webpack is designed primarily to aggregate StyleX styles at build time and append them to a CSS asset emitted by the bundler (e.g., using MiniCssExtractPlugin) [1][2][3]. It does not operate via a "runtime injection" extraction mode in production; rather, it consolidates styles into a static CSS file [2][3]. Key details regarding your query: 1. CSS Extraction: The plugin works by intercepting the build process to aggregate CSS and injecting it into an existing CSS asset produced by Webpack [2][3]. If no CSS asset is produced by your Webpack configuration, the plugin will emit a fallback stylesheet named stylex.css [2][3]. It is highly recommended to use a CSS extraction plugin like MiniCssExtractPlugin to ensure a proper injection target exists [1][4][3]. 2. Runtime Injection: While the underlying babel-plugin has a runtimeInjection option (typically used for development or specific client-side needs), it is not the intended "extraction mode" for production builds [5]. Production builds should rely on static CSS extraction to ensure better performance and deterministic output [3]. 3. PostCSS Plugin: StyleX maintains a separate package,@stylexjs/postcss-plugin, which is used for processing the CSS generated by StyleX [2]. It is not a replacement for the unplugin's core role in compiling and aggregating styles, but it can be part of the pipeline to process the aggregated CSS [2]. In summary, for Webpack, you should configure the@stylexjs/unpluginalongside an extraction plugin like MiniCssExtractPlugin to achieve static CSS output [1][4][3]. Avoid using runtime injection in production to prevent unnecessary overhead [5].Citations:
🏁 Script executed:
Repository: clerk/javascript
Length of output: 2082
🌐 Web query:
@stylexjs/unplugin webpack css extraction asset source stylex docs💡 Result:
To extract CSS when using StyleX with Webpack, you should use the official
@stylexjs/unpluginpackage [1][2][3]. The older@stylexjs/webpack-pluginis largely deprecated in favor of this universal bundler plugin [4][5]. The process for CSS extraction involves two main requirements: integrating the StyleX plugin and configuring a CSS extraction plugin (like MiniCssExtractPlugin) to provide an injection target [1][6][4]. 1. Configure the Plugin: Use stylex.webpack from@stylexjs/unpluginin your webpack.config.js [1][2]. It is recommended to enable useCSSLayers: true for better CSS specificity management [6][7]. 2. Provide an Extraction Target: StyleX aggregates the CSS generated by your application and automatically appends it to an existing CSS asset produced by your build [2][4][3]. You must include a CSS extraction plugin (e.g., MiniCssExtractPlugin) and ensure at least one CSS file is imported in your application code (typically a global stylesheet or entry-point CSS file) so that Webpack creates the necessary CSS asset [1][6][4]. If no CSS assets are emitted by your bundle,@stylexjs/unpluginwill default to creating a standalone stylex.css file as a fallback [2][4][3]. Example configuration snippet: const stylex = require('@stylexjs/unplugin').default; const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { module: { rules: [ { test: /.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] }, ], }, plugins: [ stylex.webpack({ useCSSLayers: true }), new MiniCssExtractPlugin, ], }; You can use the cssInjectionTarget option (a function receiving the filename) within the stylex.webpack configuration if you need to specifically select which emitted CSS asset should receive the StyleX styles [4][3].Citations:
🏁 Script executed:
Repository: clerk/javascript
Length of output: 4357
Use one StyleX CSS extraction path for Mosaic.
packages/swingset/next.config.mjsandpackages/swingset/postcss.config.mjsboth processpackages/ui/src/mosaic, and the webpack unplugin also emits CSS into the bundle. Keep one owner for CSS output and leave the other path to JS transforms only, or the same atoms can be emitted twice.📍 Affects 2 files
packages/swingset/next.config.mjs#L74-L93(this comment)packages/swingset/postcss.config.mjs#L15-L38🤖 Prompt for AI Agents
Source: MCP tools
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Intentional split: unplugin = JS transform, postcss-plugin = CSS. Checked unplugin
webpack.js— it appends CSS only when a.cssasset exists, else skips. Swingset runsnext dev→ no.cssasset → unplugin no-ops, postcss sole CSS owner, no dup atoms. (prod build appends once more, but dev-only private tool + atomic CSS idempotent.) Leaving as-is.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexcarpenter, understood. The split is intentional: PostCSS is the CSS owner for Swingset development, while the webpack unplugin performs the StyleX JS transform. The production duplicate remains a known, accepted trade-off for this private dev tool, so I won’t press for a change here.✏️ Learnings added