Skip to content

Commit 9b0049b

Browse files
cursoragentskovhus
andcommitted
Support prop-based inline style theme access via useTheme() rewriting
When an interpolation accesses props.theme in a conditional expression that the adapter cannot resolve statically, the codemod now rewrites props.theme to theme (from useTheme() hook) and emits it as an inline style, instead of bailing with 'Unsupported prop-based inline style props.theme access is not supported'. This supports patterns like: border-left: ${props => props.$isHighlighted ? `2px solid ${props.theme.color.x}` : '2px solid transparent'}; The fix applies to arrow functions with a simple Identifier parameter (e.g., (props) => ...). Destructured ObjectPattern parameters (e.g., ({ theme }) => ...) retain the existing bail behavior to avoid closure variable confusion. Changes: - inline-style-props.ts: Rewrite props.theme to theme and mark needsUseThemeHook instead of bailing (both pseudo/media and base paths) - rule-interpolated-declaration.ts: Redirect emitStyleFunctionFromPropsObject with theme access to inline styles; handle shouldForwardProp path similarly - Filter 'theme' from collected props to avoid marking it as a forwarded prop Co-authored-by: Kenneth Skovhus <skovhus@users.noreply.github.com>
1 parent dc6a8e8 commit 9b0049b

File tree

3 files changed

+107
-40
lines changed

3 files changed

+107
-40
lines changed

src/__tests__/transform.test.ts

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3573,8 +3573,7 @@ export const App = () => <Box>Hello</Box>;
35733573
`);
35743574
});
35753575

3576-
it("should bail on complex conditions combining theme boolean with other expressions", () => {
3577-
// This tests that we bail when the condition is more complex than just theme.prop
3576+
it("should handle complex conditions combining theme boolean with prop access as inline style", () => {
35783577
const source = `
35793578
import styled from "styled-components";
35803579
@@ -3591,24 +3590,11 @@ export const App = () => <Box isActive>Hello</Box>;
35913590
{ adapter: fixtureAdapter },
35923591
);
35933592

3594-
// Should bail out - complex conditions are not supported
3595-
expect(result.code).toBeNull();
3596-
expect(result.warnings).toMatchInlineSnapshot(`
3597-
[
3598-
{
3599-
"context": {
3600-
"localName": "Box",
3601-
"propLabel": "color",
3602-
},
3603-
"loc": {
3604-
"column": 11,
3605-
"line": 5,
3606-
},
3607-
"severity": "warning",
3608-
"type": "Unsupported prop-based inline style props.theme access is not supported",
3609-
},
3610-
]
3611-
`);
3593+
// Now handled via inline style with useTheme() hook
3594+
expect(result.code).not.toBeNull();
3595+
expect(result.warnings).toEqual([]);
3596+
expect(result.code).toContain("useTheme");
3597+
expect(result.code).toContain("theme.isDark");
36123598
});
36133599

36143600
it("should not treat closure variables as destructured props in theme conditionals", () => {

src/internal/lower-rules/inline-style-props.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
hasThemeAccessInArrowFn,
1515
hasUnsupportedConditionalTest,
1616
inlineArrowFunctionBody,
17+
rewritePropsThemeToThemeVar,
1718
unwrapArrowFunctionToPropsExpr,
1819
} from "./inline-styles.js";
1920
import { buildPseudoMediaPropValue } from "./variant-utils.js";
@@ -111,7 +112,8 @@ export function handleInlineStyleValueFromProps(ctx: InlineStyleFromPropsContext
111112
);
112113
}
113114
const valueExprRaw = (() => {
114-
if (hasThemeAccessInArrowFn(e)) {
115+
const hasThemeAccess = hasThemeAccessInArrowFn(e);
116+
if (hasThemeAccess && !hasSimpleIdentifierParam(e)) {
115117
warnPropInlineStyle(
116118
decl,
117119
"Unsupported prop-based inline style props.theme access is not supported",
@@ -132,7 +134,10 @@ export function handleInlineStyleValueFromProps(ctx: InlineStyleFromPropsContext
132134
setBail();
133135
return null;
134136
}
135-
const baseExpr = inlineExpr;
137+
const baseExpr = hasThemeAccess ? rewritePropsThemeToThemeVar(inlineExpr) : inlineExpr;
138+
if (hasThemeAccess) {
139+
markDeclNeedsUseThemeHook(decl);
140+
}
136141
const { prefix, suffix } = extractStaticPartsForDecl(d);
137142
return prefix || suffix
138143
? buildTemplateWithStaticParts(j, baseExpr, prefix, suffix)
@@ -185,11 +190,8 @@ export function handleInlineStyleValueFromProps(ctx: InlineStyleFromPropsContext
185190
});
186191
return bailNow();
187192
}
188-
const propsUsed = collectPropsFromArrowFn(e);
189-
for (const propName of propsUsed) {
190-
ensureShouldForwardPropDrop(decl, propName);
191-
}
192-
if (hasThemeAccessInArrowFn(e)) {
193+
const hasThemeAccess = hasThemeAccessInArrowFn(e);
194+
if (hasThemeAccess && !hasSimpleIdentifierParam(e)) {
193195
warnPropInlineStyle(
194196
decl,
195197
"Unsupported prop-based inline style props.theme access is not supported",
@@ -198,7 +200,14 @@ export function handleInlineStyleValueFromProps(ctx: InlineStyleFromPropsContext
198200
);
199201
return bailNow();
200202
}
201-
const unwrapped = unwrapArrowFunctionToPropsExpr(j, e);
203+
const propsUsed = collectPropsFromArrowFn(e);
204+
for (const propName of propsUsed) {
205+
if (hasThemeAccess && propName === "theme") {
206+
continue;
207+
}
208+
ensureShouldForwardPropDrop(decl, propName);
209+
}
210+
const unwrapped = hasThemeAccess ? null : unwrapArrowFunctionToPropsExpr(j, e);
202211
const inlineExpr = unwrapped?.expr ?? inlineArrowFunctionBody(j, e);
203212
if (!inlineExpr) {
204213
warnPropInlineStyle(
@@ -210,7 +219,10 @@ export function handleInlineStyleValueFromProps(ctx: InlineStyleFromPropsContext
210219
return bailNow();
211220
}
212221
decl.needsWrapperComponent = true;
213-
const baseExpr = inlineExpr;
222+
const baseExpr = hasThemeAccess ? rewritePropsThemeToThemeVar(inlineExpr) : inlineExpr;
223+
if (hasThemeAccess) {
224+
markDeclNeedsUseThemeHook(decl);
225+
}
214226
// Build template literal when there's static prefix/suffix (e.g., `${...}ms`)
215227
const { prefix, suffix } = extractStaticPartsForDecl(d);
216228
const valueExpr =
@@ -348,3 +360,23 @@ export function handleInlineStyleValueFromProps(ctx: InlineStyleFromPropsContext
348360

349361
return true;
350362
}
363+
364+
// --- Non-exported helpers ---
365+
366+
function hasSimpleIdentifierParam(expr: { params?: Array<{ type?: string }> }): boolean {
367+
return expr.params?.length === 1 && expr.params[0]?.type === "Identifier";
368+
}
369+
370+
function markDeclNeedsUseThemeHook(decl: StyledDecl): void {
371+
if (!decl.needsUseThemeHook) {
372+
decl.needsUseThemeHook = [];
373+
}
374+
if (!decl.needsUseThemeHook.some((entry) => entry.themeProp === "__runtimeCall")) {
375+
decl.needsUseThemeHook.push({
376+
themeProp: "__runtimeCall",
377+
trueStyleKey: null,
378+
falseStyleKey: null,
379+
});
380+
}
381+
decl.needsWrapperComponent = true;
382+
}

src/internal/lower-rules/rule-interpolated-declaration.ts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,14 +1583,39 @@ export function handleInterpolatedDeclaration(args: InterpolatedDeclarationConte
15831583
break;
15841584
}
15851585
if (hasThemeAccessInArrowFn(e)) {
1586-
warnPropInlineStyle(
1587-
decl,
1588-
"Unsupported prop-based inline style props.theme access is not supported",
1589-
d.property,
1590-
loc,
1591-
);
1592-
bail = true;
1593-
break;
1586+
// StyleX style functions can't use runtime theme values.
1587+
// Redirect to inline styles with useTheme() hook instead.
1588+
const inlinedExpr = inlineArrowFunctionBody(j, e);
1589+
if (!inlinedExpr) {
1590+
warnPropInlineStyle(
1591+
decl,
1592+
"Unsupported prop-based inline style expression cannot be safely inlined",
1593+
d.property,
1594+
loc,
1595+
);
1596+
bail = true;
1597+
break;
1598+
}
1599+
const themeRewritten = rewritePropsThemeToThemeVar(inlinedExpr as ExpressionKind);
1600+
const { prefix, suffix } = extractStaticPartsForDecl(d);
1601+
const valueExpr =
1602+
prefix || suffix
1603+
? buildTemplateWithStaticParts(j, themeRewritten, prefix, suffix)
1604+
: themeRewritten;
1605+
markDeclNeedsUseThemeHook(decl);
1606+
for (const propName of res.props ?? []) {
1607+
if (propName === "theme") {
1608+
continue;
1609+
}
1610+
ensureShouldForwardPropDrop(decl, propName);
1611+
}
1612+
for (const out of cssDeclarationToStylexDeclarations(d)) {
1613+
if (!out.prop) {
1614+
continue;
1615+
}
1616+
inlineStyleProps.push({ prop: out.prop, expr: valueExpr });
1617+
}
1618+
continue;
15941619
}
15951620
const bodyExpr = getFunctionBodyExpr(e);
15961621
if (!bodyExpr) {
@@ -1944,7 +1969,8 @@ export function handleInterpolatedDeclaration(args: InterpolatedDeclarationConte
19441969
bail = true;
19451970
break;
19461971
}
1947-
if (hasThemeAccessInArrowFn(e)) {
1972+
const hasThemeAccess = hasThemeAccessInArrowFn(e);
1973+
if (hasThemeAccess && e.params?.[0]?.type !== "Identifier") {
19481974
warnPropInlineStyle(
19491975
decl,
19501976
"Unsupported prop-based inline style props.theme access is not supported",
@@ -1956,12 +1982,16 @@ export function handleInterpolatedDeclaration(args: InterpolatedDeclarationConte
19561982
}
19571983
const propsUsed = collectPropsFromArrowFn(e);
19581984
for (const propName of propsUsed) {
1985+
if (hasThemeAccess && propName === "theme") {
1986+
continue;
1987+
}
19591988
ensureShouldForwardPropDrop(decl, propName);
19601989
}
19611990
// Try to unwrap props access (props.$x → $x) for cleaner style functions.
19621991
// When only one transient prop is used, emit a single-param function
19631992
// (e.g., ($size) => ...) instead of (props) => ..., enabling consolidation.
1964-
const unwrapped = unwrapArrowFunctionToPropsExpr(j, e);
1993+
// Skip unwrapping when theme is accessed — theme refs need full inlining + rewriting.
1994+
const unwrapped = hasThemeAccess ? null : unwrapArrowFunctionToPropsExpr(j, e);
19651995
if (unwrapped && unwrapped.propsUsed.size === 1) {
19661996
const singleProp = [...unwrapped.propsUsed][0]!;
19671997
propsParam = j.identifier(singleProp);
@@ -1982,7 +2012,12 @@ export function handleInterpolatedDeclaration(args: InterpolatedDeclarationConte
19822012
bail = true;
19832013
break;
19842014
}
1985-
baseExpr = inlineExpr;
2015+
baseExpr = hasThemeAccess
2016+
? rewritePropsThemeToThemeVar(inlineExpr as ExpressionKind)
2017+
: inlineExpr;
2018+
if (hasThemeAccess) {
2019+
markDeclNeedsUseThemeHook(decl);
2020+
}
19862021
}
19872022
}
19882023
// Build template literal when there's static prefix/suffix (e.g., `${...}ms`)
@@ -2769,6 +2804,20 @@ function tryHandleMultiSlotTernary(ctx: DeclProcessingState, d: CssDeclarationIR
27692804
return true;
27702805
}
27712806

2807+
function markDeclNeedsUseThemeHook(decl: StyledDecl): void {
2808+
if (!decl.needsUseThemeHook) {
2809+
decl.needsUseThemeHook = [];
2810+
}
2811+
if (!decl.needsUseThemeHook.some((entry) => entry.themeProp === "__runtimeCall")) {
2812+
decl.needsUseThemeHook.push({
2813+
themeProp: "__runtimeCall",
2814+
trueStyleKey: null,
2815+
falseStyleKey: null,
2816+
});
2817+
}
2818+
decl.needsWrapperComponent = true;
2819+
}
2820+
27722821
/** CSS shorthand properties that cannot be represented as a single var() custom property. */
27732822
const UNSUPPORTED_CUSTOM_PROP_SHORTHANDS = new Set([
27742823
"border",

0 commit comments

Comments
 (0)