Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b82d32f
Add GPT5.2 version
skovhus Jan 1, 2026
cb53a75
Fixes
skovhus Jan 1, 2026
fd71864
Remove CLI
skovhus Jan 1, 2026
6a0a0ec
Adjust
skovhus Jan 2, 2026
1158a6e
Update
skovhus Jan 2, 2026
1c3e322
Apply fixes
skovhus Jan 3, 2026
2199c76
Fixes
skovhus Jan 3, 2026
c0fe1b7
Update test-cases
skovhus Jan 3, 2026
4ac1f84
Fixes
skovhus Jan 3, 2026
e369a4d
Full coverage
skovhus Jan 3, 2026
c8a8fc4
Fix lint
skovhus Jan 3, 2026
0567224
Type-aware linting
skovhus Jan 3, 2026
cdbc2b7
Fixes
skovhus Jan 3, 2026
812e4d5
Make hook required
skovhus Jan 3, 2026
ef92236
Improve transforms
skovhus Jan 3, 2026
c57300f
Fix all failing transforms
skovhus Jan 3, 2026
5f49965
Add support for comments
skovhus Jan 3, 2026
3808e6d
Update test-cases
skovhus Jan 4, 2026
af382a2
Rename hook to adapter
skovhus Jan 4, 2026
cf2c5d9
Simplify build
skovhus Jan 4, 2026
5385b13
Add failing test
skovhus Jan 4, 2026
f9bdf8b
Improve adapter
skovhus Jan 4, 2026
eda3e67
Add settings.json
skovhus Jan 4, 2026
9f8203b
Split builtin-handlers out of adapter
skovhus Jan 4, 2026
769a4e3
Fix oxlint
skovhus Jan 4, 2026
b86485f
Fix dropping comments
skovhus Jan 4, 2026
1b257b9
Remove deslop
skovhus Jan 4, 2026
6a2ff20
Fix custom styles
skovhus Jan 4, 2026
2be678a
Remove unnessary lint af part of test
skovhus Jan 4, 2026
5a0a57e
Fix universal-selector
skovhus Jan 4, 2026
440ea08
Fixes
skovhus Jan 4, 2026
b5ee9e1
Fix test
skovhus Jan 4, 2026
a96a9b6
Update exports
skovhus Jan 4, 2026
eb91f56
Skip universal selectors
skovhus Jan 4, 2026
a322d52
Refactor
skovhus Jan 5, 2026
d3d4edd
Address code review
skovhus Jan 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions oxlint.json → .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
"rules": {
"no-unused-vars": "warn",
"no-console": "warn",
"no-unused-vars": "error",
"no-console": "error",
"eqeqeq": "error"
},
"ignorePatterns": [
Expand Down
9 changes: 9 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
}
}
56 changes: 19 additions & 37 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ pnpm storybook # Start Storybook dev server (port 6006)
src/
├── index.ts # Main exports
├── transform.ts # Transform implementation
├── transform.test.ts # Test runner (auto-discovers test cases)
├── __tests__/transform.test.ts # Test runner (auto-discovers test cases)
├── run.ts # Programmatic runner (runTransform)
└── adapter.ts # Adapter interface and default adapter
└── adapter.ts # Adapter API (value resolution + dynamic handlers)

test-cases/
├── *.input.tsx # Input files (styled-components)
Expand All @@ -60,13 +60,13 @@ test-cases/

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.

Unsupported fixtures can be named `_unsupported.<case>.input.tsx` and should NOT have an output file.

**Test categories:**

- **File pairing**: Verifies all test cases have matching input/output files
- **Output linting**: Runs oxlint on all output files to ensure valid code
- **Transform tests** (skipped): Will verify transform produces expected output once implemented

**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.
- **Transform tests**: Verifies transform produces expected output fixtures for supported cases

## Storybook Visual Testing

Expand All @@ -88,52 +88,34 @@ Use the Playwright MCP to inspect test case rendering:

The "All" story shows every test case side-by-side, making it easy to compare styled-components input with StyleX output.

## Adapter System
## Adapter API

The codemod uses an adapter system for value transformations, allowing customization of how styled-components values are converted to StyleX.
The codemod exposes an adapter-based API for customization (value resolution + dynamic interpolation handling).

### Programmatic Usage

Create a script to run the transform with a custom adapter:

```typescript
// run-transform.ts
```ts
import { runTransform } from "styled-components-to-stylex-codemod";
import type { Adapter } from "styled-components-to-stylex-codemod";

const myAdapter: Adapter = {
transformValue({ path, defaultValue, valueType }) {
// Return StyleX-compatible value
// valueType is 'theme' | 'helper' | 'interpolation'
return `themeVars.${path.replace(/\./g, "_")}`;
},
getImports() {
return ["import { themeVars } from './theme.stylex';"];
},
getDeclarations() {
return [];
import { defineAdapter } from "styled-components-to-stylex-codemod";

const adapter = defineAdapter({
resolveValue(ctx) {
if (ctx.kind !== "theme") return null;
return {
expr: `themeVars.${ctx.path.replace(/\./g, "_")}`,
imports: ["import { themeVars } from './theme.stylex';"],
};
},
};
});

await runTransform({
files: "src/**/*.tsx",
adapter: myAdapter,
adapter,
dryRun: true, // Set to false to write changes
parser: "tsx",
});
```

Run with: `npx tsx run-transform.ts`

### Default Adapter

The default adapter converts theme values to CSS custom properties:

```typescript
// Input: props.theme.colors.primary
// Output: 'var(--colors-primary, #defaultValue)'
```

## StyleX Requirements

Output files must use valid StyleX syntax:
Expand Down
234 changes: 231 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,241 @@

Transform styled-components to StyleX.

## Usage
## Installation

```bash
npx styled-components-to-stylex src/
npx styled-components-to-stylex --dry src/ # dry run
npm install styled-components-to-stylex-codemod
# or
pnpm add styled-components-to-stylex-codemod
```

## Usage

Use `runTransform` to transform files matching a glob pattern:

```ts
import {
runTransform,
defineAdapter,
} from "styled-components-to-stylex-codemod";

const adapter = defineAdapter({
resolveValue(ctx) {
if (ctx.kind !== "theme") return null;
return {
expr: `tokens.${ctx.path.replace(/\./g, "_")}`,
imports: ["import { tokens } from './design-system.stylex';"],
};
},
});

const result = await runTransform({
files: "src/**/*.tsx",
adapter,
});

console.log(`Transformed ${result.transformed} files`);
```

### Options

```ts
interface RunTransformOptions {
/** Glob pattern(s) for files to transform */
files: string | string[];

/**
* Adapter for customizing the transform.
* Controls value resolution (and resolver-provided imports) and custom handlers.
*/
adapter: Adapter;

/** Dry run - don't write changes to files (default: false) */
dryRun?: boolean;

/** Print transformed output to stdout (default: false) */
print?: boolean;

/** Parser to use (default: "tsx") */
parser?: "babel" | "babylon" | "flow" | "ts" | "tsx";
}
```

### Dry Run

Preview changes without modifying files:

```ts
await runTransform({
files: "src/**/*.tsx",
adapter,
dryRun: true,
print: true, // prints transformed output to stdout
});
```

### Custom Adapter

Adapters are the main extension point. They let you control:

- how theme paths and CSS variables are turned into StyleX-compatible JS values (`resolveValue`)
- what extra imports to inject into transformed files (returned from `resolveValue`)
- how to handle dynamic interpolations inside template literals (`handlers`)

#### `Adapter` interface (what you can customize)

```ts
export interface Adapter {
/**
* Resolve theme paths and CSS variables to StyleX-compatible values.
*
* Called by built-in handlers for patterns like:
* ${(props) => props.theme.colors.primary}
*
* Also used for CSS `var(--...)` tokens inside static CSS values.
*
* Return an object containing:
* - `expr`: JS expression string to inline into output
* - `imports`: import statements required by `expr`
* - `dropDefinition?`: (CSS variables only) drop local `--x: ...` definitions when true
*/
resolveValue: (
context:
| { kind: "theme"; path: string }
| {
kind: "cssVariable";
name: string;
fallback?: string;
definedValue?: string;
}
) => { expr: string; imports: string[]; dropDefinition?: boolean } | null;

/**
* Custom handlers for dynamic expressions (template interpolations).
* These run BEFORE built-in handlers.
*/
handlers?: Array<{
name: string;
handle: (node: unknown, ctx: unknown) => unknown | null;
}>;
}
```

#### How handler ordering works

When the codemod encounters an interpolation inside a styled template literal, it tries handlers in this order:

- `adapter.handlers` (your custom handlers, in array order)
- internal built-in handlers (always enabled), which cover common cases like:
- theme access (`props.theme...`)
- prop access (`props.foo`)
- conditionals (`props.foo ? "a" : "b"`, `props.foo && "color: red;"`)

If no handler can resolve an interpolation:

- for `withConfig({ shouldForwardProp })` wrappers, the transform preserves the value as an inline style so output keeps visual parity
- otherwise, the declaration containing that interpolation is **dropped** and a warning is produced (manual follow-up required)

#### Create a custom adapter (theme path → tokens)

```ts
import {
runTransform,
defineAdapter,
} from "styled-components-to-stylex-codemod";

const adapter = defineAdapter({
resolveValue(ctx) {
if (ctx.kind === "theme") {
// Example: theme.colors.primary -> tokens.colors_primary
const varName = ctx.path.replace(/\./g, "_");
return {
expr: `tokens.${varName}`,
imports: ["import { tokens } from './design-system.stylex';"],
};
}
if (ctx.kind === "cssVariable") {
// Example: var(--spacing-sm) -> vars.spacingSm
if (ctx.name === "--spacing-sm") {
return {
expr: "vars.spacingSm",
imports: ['import { vars } from "./tokens.stylex";'],
};
}
}
return null;
},
});

await runTransform({
files: "src/**/*.tsx",
adapter,
});
```

#### Create a custom handler (advanced)

Most projects won’t need custom handlers. If you do, handlers let you convert an interpolation into:

- a resolved value (inline into the generated style object)
- a style function (generated helper + call site wiring)
- split variants (turn a conditional into base + conditional style keys)

If you want to implement handlers, start by importing the types:

```ts
import type {
DynamicHandler,
DynamicNode,
HandlerContext,
} from "styled-components-to-stylex-codemod";
```

Then add handlers to your adapter:

```ts
import { defineAdapter } from "styled-components-to-stylex-codemod";

export default defineAdapter({
handlers: [
{
name: "my-handler",
handle(node: any, ctx: any) {
// Return null to let the next handler try.
// Return a handler result object to tell the transform what to emit.
return null;
},
},
],
});
```

### Notes / Limitations

- **ThemeProvider**: if a file imports and uses `ThemeProvider` from `styled-components`, the transform **skips the entire file** (theming strategy is project-specific).
- **createGlobalStyle**: detected usage is reported as an **unsupported-feature** warning (StyleX does not support global styles in the same way).

### Transform Result

```ts
interface RunTransformResult {
errors: number; // Files that had errors
unchanged: number; // Files that were unchanged
skipped: number; // Files that were skipped
transformed: number; // Files that were transformed
timeElapsed: number; // Total time in seconds
}
```

## Public API

This package intentionally exposes a small public API:

- **`defineAdapter`**: define how theme paths / CSS variables resolve, plus custom handlers
- **`runTransform`**: run the codemod over a set of files (uses jscodeshift under the hood)

The underlying jscodeshift transform function is considered **internal** and is not a supported public entrypoint.

## License

MIT
Loading