Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/p2-14-migrate-interpolations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@typestyles/migrate': minor
---

Migrate codemod: convert prop-based styled-components interpolations to `createVar` + `assignVars`, boolean prop ternaries to `styles.component` variants, destructured prop params, and `@media` blocks (P2.14).
33 changes: 29 additions & 4 deletions IMPROVEMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,40 @@ Bugs and credibility issues that lose evaluations on contact. Do these first.
`packages/migrate`, `packages/props`, `packages/open-props`, and
`packages/build-runner`; root README links the full package index.

- [ ] **P2.14 — Migrate codemod: handle interpolations** (PR: )
- The codemod skips any template literal with interpolations — i.e. most real
styled-components code. Convert prop-based interpolations to
`createVar` + `assignVars` so migration is genuinely automated.
- [x] **P2.14 — Migrate codemod: handle interpolations** (PR: #99)
- Convert prop-based interpolations (`props => props.color`, destructured
`({ color }) => color`, suffixes like `px`) to `createVar` + `assignVars`.
- Convert boolean prop ternaries (`props.primary ? A : B`) to `styles.component`
variants with JSX call-site rewrites.
- Parse `@media` (and other at-rules) in static templates into nested style objects.

- [ ] **P2.15 — Per-route critical CSS** (PR: )
- `getRegisteredCss()` returns everything ever registered. Use the extraction
manifest in `@typestyles/next/build` to emit route-level CSS.

- [ ] **P2.16 — Migrate codemod: Emotion css prop** (PR: )
- Hoist inline `css={css\`...\`}`JSX attributes to module-level`styles.class`/`styles.component`and rewrite to`className`.

- [ ] **P2.17 — Migrate codemod: attrs and css composition** (PR: )
- Parse `styled.*.attrs(…)` chains and merge static attrs onto rewritten JSX.
- Inline referenced `css\`...\`` fragments when composing styled templates.

- [ ] **P2.18 — Migrate codemod: export migration opt-in** (PR: )
- Add `--include-exports` to migrate exported styled components into named
function wrappers without silently changing public APIs by default.

- [ ] **P2.19 — Migrate codemod: theme → tokens heuristic** (PR: )
- Optional `--theme-prefix` rewrite for `props.theme…` interpolations to
`tokens.use(…)` references with warnings for unmatched paths.

- [ ] **P2.20 — Migrate CLI hardening** (PR: )
- `--strict` / non-zero exit on warnings; spread-prop detection (`{...props}`)
with actionable hints; optional partial migration for mixed templates.

- [ ] **P2.21 — Migrate codemod: class → component second pass** (PR: )
- Optional `--to-component` pass that groups related `styles.class` calls in a
file into `styles.component` recipes with a `base` slot.

## P3 — Later (not scheduled)

- VS Code extension: hover preview of emitted CSS, token autocomplete with swatches
Expand Down
11 changes: 8 additions & 3 deletions docs/content/docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -958,9 +958,12 @@ The `@typestyles/migrate` package includes an early CLI to help with static migr

### Scope in this first version

- Converts static tagged templates (`styled.*`, `styled(...)`, and `css\`...\``) into `styles.class(...)`.
- Converts static tagged templates (`styled.*`, `styled(...)`, and `css\`...\``) into `styles.class(...)`or`styles.component(...)` when boolean prop variants are detected.
- Rewrites JSX usage for safely transformable styled components.
- Skips dynamic template interpolations and emits warnings instead of doing unsafe rewrites.
- Skips unsupported template interpolations (theme access, non-literal ternaries, etc.) and emits warnings instead of doing unsafe rewrites.
- Converts prop-based interpolations such as `` `${props => props.color}` `` to `createVar` + `assignVars`.
- Converts boolean prop ternaries such as `` `${props => props.primary ? '#0066ff' : '#6b7280'}` `` to `styles.component` variants.
- Supports destructured prop params (`` `${({ color }) => color}` ``) and `@media` blocks in static templates.

### Usage

Expand All @@ -983,10 +986,12 @@ Useful options:

### Current limitations

- Dynamic interpolations (for example `${(props) => ...}`) are intentionally not auto-migrated.
- Theme access and other non-literal interpolations are not auto-migrated.
- Exported styled components are skipped to avoid accidental API-shape changes.
- Complex non-JSX references to styled component variables are skipped.

Prop-based interpolations (for example `` `${(props) => props.color}` `` or `` `${props => props.width}px` ``) are converted to `createVar` + `assignVars` at JSX call sites. Boolean prop ternaries become `styles.component` variants. Destructured prop params and `@media` blocks in static templates are supported.

## Troubleshooting migration issues

### Styles not applying
Expand Down
20 changes: 12 additions & 8 deletions packages/migrate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,28 @@ Full migration guide: [typestyles.dev/docs/migration](https://typestyles.dev/doc

## What it transforms

| Source | Becomes |
| ------------------------ | --------------------------------------------------- |
| `styled.div\`...\`` | `styles.class('div', { ... })` + `className` on JSX |
| `styled(Button)\`...\`` | `styles.class('button', { ... })` + `className` |
| `` css`...` `` (Emotion) | `styles.class(...)` |
| Static template literals | CSS object properties (via PostCSS parser) |
| Source | Becomes |
| ----------------------------------- | --------------------------------------------------- |
| `styled.div\`...\`` | `styles.class('div', { ... })` + `className` on JSX |
| `styled(Button)\`...\`` | `styles.class('button', { ... })` + `className` |
| `` css`...` `` (Emotion) | `styles.class(...)` |
| Static template literals | CSS object properties (via PostCSS parser) |
| `` `${props => props.x}` `` | `createVar` + `assignVars` + `styles.class` |
| `` `${props => props.x ? A : B}` `` | `styles.component` variants + JSX rewrite |
| `` `${({ x }) => x}` `` | `createVar` + `assignVars` (destructured params) |
| `@media` in templates | Nested `'@media (…)'` objects in style definitions |

The codemod rewrites JSX usage for styled components it can safely transform and adds the required `import { styles } from 'typestyles'`.

## What it skips (with warnings)

Honest automation beats silent breakage:

- **Template interpolations** — `` `${props => ...}` ``, theme access, and any dynamic expression inside backticks
- **Unsupported interpolations** — theme access (`props.theme…`), non-literal ternaries, and other non-prop expressions
- **Exported styled components** — avoids changing your public API shape without review
- **Non-JSX references** to styled component variables

Dynamic styles should become [`createVar` + `assignVars`](https://typestyles.dev/docs/css-variables) or inline styles. Interpolation support is planned ([P2.14](https://github.com/type-styles/typestyles/blob/main/IMPROVEMENTS.md)).
Prop-based patterns like `` `${props => props.color}` `` and `` `${(props) => props.width}px` `` are converted to [`createVar` + `assignVars`](https://typestyles.dev/docs/dynamic-styles). Boolean prop ternaries like `` `${props => props.primary ? '#0066ff' : '#6b7280'}` `` become `styles.component` variants. Suffix text after the interpolation (e.g. `px`) is applied at the call site.

## Installation

Expand Down
22 changes: 18 additions & 4 deletions packages/migrate/src/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
return property.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
}

function toValueNode(value: string): t.Expression {
function toValueNode(value: string, varReplacements?: Map<string, t.Identifier>): t.Expression {
const trimmed = value.trim();
if (varReplacements?.has(trimmed)) {
return varReplacements.get(trimmed)!;

Check warning on line 12 in packages/migrate/src/css.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
}
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
return t.numericLiteral(Number(trimmed));
}
Expand All @@ -24,22 +27,32 @@
function nodesToObject(
nodes: ChildNode[] | undefined,
warnings: MigrationWarning[],
varReplacements?: Map<string, t.Identifier>,
): t.ObjectExpression {
const properties: t.ObjectProperty[] = [];

for (const node of nodes ?? []) {
if (node.type === 'decl') {
const normalized = node.prop.startsWith('--') ? node.prop : camelCaseProperty(node.prop);
properties.push(t.objectProperty(toKeyNode(normalized), toValueNode(node.value)));
properties.push(
t.objectProperty(toKeyNode(normalized), toValueNode(node.value, varReplacements)),
);
continue;
}

if (node.type === 'rule') {
const nested = nodesToObject(node.nodes, warnings);
const nested = nodesToObject(node.nodes, warnings, varReplacements);
properties.push(t.objectProperty(t.stringLiteral(node.selector.trim()), nested));
continue;
}

if (node.type === 'atrule') {
const nested = nodesToObject(node.nodes, warnings, varReplacements);
const atRuleKey = `@${node.name}${node.params ? ` ${node.params}` : ''}`;
properties.push(t.objectProperty(t.stringLiteral(atRuleKey), nested));
continue;
}

warnings.push({
message: `Unsupported CSS node "${node.type}" skipped.`,
});
Expand All @@ -51,10 +64,11 @@
export function cssToObjectExpression(
cssText: string,
warnings: MigrationWarning[],
varReplacements?: Map<string, t.Identifier>,
): t.ObjectExpression | null {
try {
const root = postcss.parse(cssText);
return nodesToObject((root as unknown as Container).nodes, warnings);
return nodesToObject((root as unknown as Container).nodes, warnings, varReplacements);
} catch (error) {
warnings.push({
message: `Could not parse CSS template literal: ${(error as Error).message}`,
Expand Down
Loading
Loading