|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +Codemod to transform styled-components to StyleX using jscodeshift. |
| 8 | + |
| 9 | +## Tech Stack |
| 10 | + |
| 11 | +- **Runtime**: Node.js >=22.20 |
| 12 | +- **Package Manager**: pnpm >=10.22.0 |
| 13 | +- **Language**: TypeScript (ESM) |
| 14 | +- **Build**: tsdown |
| 15 | +- **Test**: vitest |
| 16 | +- **Lint**: oxlint |
| 17 | +- **Visual Testing**: Storybook 10 |
| 18 | +- **Codemod Framework**: jscodeshift |
| 19 | + |
| 20 | +## Commands |
| 21 | + |
| 22 | +```bash |
| 23 | +pnpm install # Install dependencies |
| 24 | +pnpm build # Build with tsdown |
| 25 | +pnpm test # Run tests (watch mode) |
| 26 | +pnpm test:run # Run tests once |
| 27 | +pnpm typecheck # Type check with tsc |
| 28 | +pnpm lint # Lint with oxlint |
| 29 | +pnpm run ci # Run lint + typecheck + test |
| 30 | +pnpm storybook # Start Storybook dev server (port 6006) |
| 31 | +``` |
| 32 | + |
| 33 | +## Project Structure |
| 34 | + |
| 35 | +``` |
| 36 | +src/ |
| 37 | +├── index.ts # Main exports |
| 38 | +├── transform.ts # Transform implementation |
| 39 | +├── transform.test.ts # Test runner (auto-discovers test cases) |
| 40 | +├── run.ts # Programmatic runner (runTransform) |
| 41 | +└── adapter.ts # Adapter interface and default adapter |
| 42 | +
|
| 43 | +test-cases/ |
| 44 | +├── *.input.tsx # Input files (styled-components) |
| 45 | +├── *.output.tsx # Expected output files (StyleX) |
| 46 | +├── *.stylex.ts # StyleX theme variables (defineVars) |
| 47 | +├── *.warnings.json # Expected warnings for test cases |
| 48 | +├── lib/ # Helper files for test cases |
| 49 | +│ ├── helpers.ts # styled-components helpers (color, truncate) |
| 50 | +│ ├── helpers.stylex.ts # StyleX version of helpers |
| 51 | +│ └── colors.stylex.ts # StyleX color variables |
| 52 | +└── TestCases.stories.tsx # Auto-discovering Storybook stories |
| 53 | +
|
| 54 | +.storybook/ |
| 55 | +├── main.ts # Storybook config (Vite, React, StyleX) |
| 56 | +└── preview.ts # Storybook preview config |
| 57 | +``` |
| 58 | + |
| 59 | +## Adding Test Cases |
| 60 | + |
| 61 | +Create matching `.input.tsx` and `.output.tsx` files in `test-cases/`. Tests auto-discover all pairs and fail if any file is missing its counterpart. |
| 62 | + |
| 63 | +**Test categories:** |
| 64 | + |
| 65 | +- **File pairing**: Verifies all test cases have matching input/output files |
| 66 | +- **Output linting**: Runs oxlint on all output files to ensure valid code |
| 67 | +- **Transform tests** (skipped): Will verify transform produces expected output once implemented |
| 68 | + |
| 69 | +**Note**: The transform is currently a stub that adds TODO comments. The transform tests are skipped until implementation is complete. Output files represent the expected transformation results. |
| 70 | + |
| 71 | +## Storybook Visual Testing |
| 72 | + |
| 73 | +Storybook renders all test cases side-by-side (input with styled-components, output with StyleX) to visually verify the transformation produces identical styles. |
| 74 | + |
| 75 | +- **Auto-discovery**: Test cases are automatically discovered from `test-cases/*.input.tsx` and `*.output.tsx` files |
| 76 | +- **Side-by-side view**: Each test case shows "Input (styled-components)" and "Output (StyleX)" panels |
| 77 | +- **"All" story**: Shows all test cases on a single page at `http://localhost:6006/?path=/story/test-cases--all` |
| 78 | + |
| 79 | +Run `pnpm storybook` to start the dev server and visually compare transformations. |
| 80 | + |
| 81 | +## Visual Inspection with Playwright MCP |
| 82 | + |
| 83 | +Use the Playwright MCP to inspect test case rendering: |
| 84 | + |
| 85 | +1. Start Storybook: `pnpm storybook` |
| 86 | +2. Navigate to `http://localhost:6006/?path=/story/test-cases--all` |
| 87 | +3. Use Playwright MCP to take screenshots and verify input/output match visually |
| 88 | + |
| 89 | +The "All" story shows every test case side-by-side, making it easy to compare styled-components input with StyleX output. |
| 90 | + |
| 91 | +## Adapter System |
| 92 | + |
| 93 | +The codemod uses an adapter system for value transformations, allowing customization of how styled-components values are converted to StyleX. |
| 94 | + |
| 95 | +### Programmatic Usage |
| 96 | + |
| 97 | +Create a script to run the transform with a custom adapter: |
| 98 | + |
| 99 | +```typescript |
| 100 | +// run-transform.ts |
| 101 | +import { runTransform } from "styled-components-to-stylex-codemod"; |
| 102 | +import type { Adapter } from "styled-components-to-stylex-codemod"; |
| 103 | + |
| 104 | +const myAdapter: Adapter = { |
| 105 | + transformValue({ path, defaultValue, valueType }) { |
| 106 | + // Return StyleX-compatible value |
| 107 | + // valueType is 'theme' | 'helper' | 'interpolation' |
| 108 | + return `themeVars.${path.replace(/\./g, "_")}`; |
| 109 | + }, |
| 110 | + getImports() { |
| 111 | + return ["import { themeVars } from './theme.stylex';"]; |
| 112 | + }, |
| 113 | + getDeclarations() { |
| 114 | + return []; |
| 115 | + }, |
| 116 | +}; |
| 117 | + |
| 118 | +await runTransform({ |
| 119 | + files: "src/**/*.tsx", |
| 120 | + adapter: myAdapter, |
| 121 | + dryRun: true, // Set to false to write changes |
| 122 | + parser: "tsx", |
| 123 | +}); |
| 124 | +``` |
| 125 | + |
| 126 | +Run with: `npx tsx run-transform.ts` |
| 127 | + |
| 128 | +### Default Adapter |
| 129 | + |
| 130 | +The default adapter converts theme values to CSS custom properties: |
| 131 | + |
| 132 | +```typescript |
| 133 | +// Input: props.theme.colors.primary |
| 134 | +// Output: 'var(--colors-primary, #defaultValue)' |
| 135 | +``` |
| 136 | + |
| 137 | +## StyleX Requirements |
| 138 | + |
| 139 | +Output files must use valid StyleX syntax: |
| 140 | + |
| 141 | +- **No CSS shorthands**: Use `backgroundColor` not `background`, `borderWidth`/`borderStyle`/`borderColor` not `border` |
| 142 | +- **defineVars must be exported**: Theme variables must be in separate `.stylex.ts` files with named exports |
| 143 | +- **createTheme needs base vars**: `stylex.createTheme(baseVars, overrides)` requires two arguments |
| 144 | + |
| 145 | +## Test Cases (from styled-components docs) |
| 146 | + |
| 147 | +| Test Case | Pattern | |
| 148 | +| ------------------------------- | -------------------------------------------------------------------- | |
| 149 | +| `basic` | `styled.h1`, `styled.section` | |
| 150 | +| `adapting-props` | Props interpolation with `${props => ...}` | |
| 151 | +| `extending-styles` | `styled(Component)` inheritance | |
| 152 | +| `pseudo-selectors` | `&:hover`, `&:focus`, `&::before` | |
| 153 | +| `keyframes` | `keyframes` + animation | |
| 154 | +| `attrs` | `.attrs()` for default props | |
| 155 | +| `theming` | `ThemeProvider` + `props.theme` | |
| 156 | +| `css-helper` | `css` helper for shared styles | |
| 157 | +| `as-prop` | Polymorphic `as` prop | |
| 158 | +| `global-styles` | `createGlobalStyle` | |
| 159 | +| `media-queries` | `@media (min-width: ...)` responsive styles | |
| 160 | +| `nesting` | Child selectors `> *`, `&:not(:first-child)` | |
| 161 | +| `component-selector` | `${Link}:hover &` referencing other components | |
| 162 | +| `style-objects` | Object syntax `styled.div({...})` | |
| 163 | +| `conditional-styles` | Short-circuit `${props => props.x && '...'}` | |
| 164 | +| `styled-component` | `styled(CustomComponent)` with className | |
| 165 | +| `transient-props` | `$prefix` props to prevent DOM forwarding | |
| 166 | +| `refs` | Ref forwarding to styled components | |
| 167 | +| `use-theme` | `useTheme` hook for accessing theme | |
| 168 | +| `with-theme` | `withTheme` HOC for class components | |
| 169 | +| `sibling-selectors` | Adjacent `& + &` and general `& ~ &` sibling selectors | |
| 170 | +| `specificity` | Double ampersand `&&` and `&&&` for specificity boost | |
| 171 | +| `descendant-component-selector` | `${Child} { ... }` parent styling child component | |
| 172 | +| `forwarded-as` | `forwardedAs` prop for passing `as` through HOCs | |
| 173 | +| `function-theme` | `theme={fn}` function that receives parent theme | |
| 174 | +| `adhoc-theme` | Per-instance `theme` prop override | |
| 175 | +| `attribute-selectors` | `&[disabled]`, `&[type="text"]`, `&[href^="https"]` | |
| 176 | +| `css-variables` | `var(--custom-property)` CSS custom properties | |
| 177 | +| `css-calc` | `calc()` expressions in styles | |
| 178 | +| `multiple-animations` | Combining multiple keyframes animations | |
| 179 | +| `important` | `!important` declarations (removed in output) | |
| 180 | +| `universal-selector` | `& *` universal descendant selector | |
| 181 | +| `complex-selectors` | Multiple/compound selectors `&:hover, &:focus` | |
| 182 | +| `string-interpolation` | Static string interpolations `${variable}` | |
| 183 | +| `should-forward-prop` | `.withConfig({ shouldForwardProp })` prop filtering | |
| 184 | +| `with-config` | `.withConfig({ displayName, componentId })` | |
| 185 | +| `helpers` | Helper functions: `color()` theme accessor, `truncate()` CSS snippet | |
| 186 | + |
| 187 | +## Transformation Goals |
| 188 | + |
| 189 | +The codemod should handle conversions like: |
| 190 | + |
| 191 | +- `styled.div` / `styled(Component)` → `stylex.create()` + `stylex.props()` |
| 192 | +- Template literal CSS → StyleX object syntax |
| 193 | +- Dynamic props/interpolations → StyleX variants or dynamic styles |
| 194 | +- `keyframes` → `stylex.keyframes()` |
| 195 | +- Theme values → CSS variables or `stylex.createTheme()` |
| 196 | +- `css` helper → Plain style objects |
| 197 | +- `.attrs()` → Inline props on element |
0 commit comments