Skip to content

Commit c533518

Browse files
skovhusclaude
andauthored
feat: reorder style fns, merge static+dynamic, and use named params (#330)
* feat: reorder style fns, merge static+dynamic, and use named params - Style function entries in stylex.create() now appear immediately after their corresponding static style key for better readability - When a static base style is never used without its dynamic counterpart, they are merged into a single style function - Style functions with a single parameter now use a named props object pattern: `(props: {size: number}) => ({...})` instead of positional args - Extract shared wrapCallArgForPropsObject helper to eliminate duplication across three emitter call sites Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: skip non-computed MemberExpression properties in identifier rewrite replaceIdentifierInAst now skips the `property` key of non-computed MemberExpressions, preventing incorrect rewrites like `Math.min` → `Math.props.min` when the param name matches a member property name. Addresses review comment on #330. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: bail on base/fn merge when properties overlap When mergeBaseIntoSingleStyleFn encounters static properties that also exist in the dynamic function body, it now bails instead of silently dropping the static values. This avoids any semantic ambiguity about override priority between static and dynamic values for the same CSS property. Addresses review comment on #330. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: only use named params for merged-base style functions Derived style function keys like `boxBoxShadow` already encode the parameter name, so positional args are clearer: `styles.boxBoxShadow(shadow)`. Named params are now only applied when the function key IS the base style key (merged base), where the key alone doesn't communicate what the parameter means: `styles.box({ size: 16 })`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ea07ffd commit c533518

22 files changed

Lines changed: 733 additions & 86 deletions

src/__tests__/fixture-adapters.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const fixtureAdapter = defineAdapter({
5656
"htmlProp-input",
5757
"transientProp-notForwarded",
5858
"inlineBase-booleanVariantKey",
59+
"inlineBase-singletonBooleanWithTemplateExpr",
5960
"inlineBase-stringVariantExported",
6061
].some((filePath) => ctx.filePath.includes(filePath))
6162
) {

src/__tests__/transform.test.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4359,9 +4359,14 @@ export const Header = styled(Flex)<{ isCompact?: boolean }>\`
43594359
43604360
export function App() {
43614361
return (
4362-
<Header justify="center" gap={12} isCompact>
4363-
Header
4364-
</Header>
4362+
<>
4363+
<Header justify="center" gap={12} isCompact>
4364+
Header
4365+
</Header>
4366+
<Header justify="flex-start" gap={8}>
4367+
Header 2
4368+
</Header>
4369+
</>
43654370
);
43664371
}
43674372
`;
@@ -4397,9 +4402,14 @@ export const Header = styled(Flex).withConfig({
43974402
43984403
export function App() {
43994404
return (
4400-
<Header justify="center" gap={12} isCompact>
4401-
Header
4402-
</Header>
4405+
<>
4406+
<Header justify="center" gap={12} isCompact>
4407+
Header
4408+
</Header>
4409+
<Header justify="flex-start" gap={8}>
4410+
Header 2
4411+
</Header>
4412+
</>
44034413
);
44044414
}
44054415
`;
@@ -4535,7 +4545,9 @@ export function App() {
45354545

45364546
expect(result.code).not.toBeNull();
45374547
const code = result.code ?? "";
4538-
expect(code).toContain("containerGapVariants");
4548+
// Single-key consumed prop emits as conditional style, not a variant object
4549+
expect(code).toContain("styles.containerGap8");
4550+
// Template expression also produces a conditional style for gap
45394551
expect(code).toContain("styles.containerGap");
45404552
});
45414553

src/internal/emit-wrappers/emit-intrinsic-should-forward-prop.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ import {
2525
appendThemeBooleanStyleArgs,
2626
buildUseThemeDeclaration,
2727
} from "./emit-intrinsic-simple.js";
28-
import { mergeOrderedEntries, styleRef, type OrderedStyleEntry } from "./style-expr-builders.js";
28+
import {
29+
mergeOrderedEntries,
30+
styleRef,
31+
wrapCallArgForPropsObject,
32+
type OrderedStyleEntry,
33+
} from "./style-expr-builders.js";
2934
import type { JSCodeshift, Identifier } from "jscodeshift";
3035

3136
/**
@@ -514,7 +519,8 @@ export function emitShouldForwardPropWrappers(ctx: EmitIntrinsicContext): void {
514519
: p.jsxProp === "__props"
515520
? j.identifier("props")
516521
: j.identifier(p.jsxProp);
517-
const callArg = p.callArg ?? propExpr;
522+
const rawCallArg = p.callArg ?? propExpr;
523+
const callArg = wrapCallArgForPropsObject(j, rawCallArg, p.propsObjectKey);
518524
const call = j.callExpression(styleRef(j, stylesIdentifier, p.fnKey), [callArg]);
519525
let expr: ExpressionKind;
520526
if (p.conditionWhen) {

src/internal/emit-wrappers/style-expr-builders.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,24 @@ export function styleRef(j: JSCodeshift, stylesIdentifier: string, key: string):
6161
return j.memberExpression(j.identifier(stylesIdentifier), j.identifier(key)) as ExpressionKind;
6262
}
6363

64+
/**
65+
* When a style function uses a `props` object parameter, wraps the raw call
66+
* argument in `{ [propsObjectKey]: rawArg }`. Returns `rawArg` unchanged when
67+
* `propsObjectKey` is not set.
68+
*/
69+
export function wrapCallArgForPropsObject(
70+
j: JSCodeshift,
71+
rawArg: ExpressionKind,
72+
propsObjectKey: string | undefined,
73+
): ExpressionKind {
74+
if (!propsObjectKey) {
75+
return rawArg;
76+
}
77+
return j.objectExpression([
78+
j.property("init", j.identifier(propsObjectKey), rawArg),
79+
]) as unknown as ExpressionKind;
80+
}
81+
6482
// ---------------------------------------------------------------------------
6583
// Extra style key splitting
6684
// ---------------------------------------------------------------------------
@@ -541,7 +559,8 @@ export function buildStyleFnExpressions(
541559

542560
for (const p of styleFnPairs) {
543561
const propExpr = p.jsxProp === "__props" ? propsId : propExprBuilder(p.jsxProp);
544-
const callArg = p.callArg ?? propExpr;
562+
const rawCallArg = p.callArg ?? propExpr;
563+
const callArg = wrapCallArgForPropsObject(j, rawCallArg, p.propsObjectKey);
545564
const call = j.callExpression(styleRef(j, stylesIdentifier, p.fnKey), [callArg]);
546565

547566
// Track call arg identifier for destructuring if needed

src/internal/lower-rules/decl-types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@ export type StyleFnFromPropsEntry = {
2222
callArg?: ExpressionKind;
2323
/** Source order index for CSS cascade ordering against variant entries. */
2424
sourceOrder?: number;
25+
/**
26+
* When set, the style function uses a `props` object parameter and the call
27+
* site must wrap the argument in `{ [propsObjectKey]: callArg }`.
28+
*/
29+
propsObjectKey?: string;
2530
};

0 commit comments

Comments
 (0)