You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(adapter): auto-detect sx-aware wrapped components, with optional override hook (#394)
* feat(adapter): wrappedComponentInterface to emit sx prop when re-styling sx-aware components
Adds an optional adapter hook 'wrappedComponentInterface' that lets the
codemod know an imported component already accepts a StyleX 'sx' prop.
When wrapping such a component via styled(Component) and 'useSxProp' is
enabled, the codemod emits 'sx={styles.x}' instead of
'{...stylex.props(styles.x)}' and forwards className/style unchanged so
the wrapped component handles merging itself.
Co-authored-by: Kenneth Skovhus <skovhus@users.noreply.github.com>
* fix(adapter): compose caller sx and destructure when wrapped component is sx-aware
Addresses two P1 review comments on the wrappedComponentInterface feature:
1. Wrapper functions emitted for styled(SxAwareComponent) referenced an
undeclared 'sx' identifier when the wrapper accepted external sx
(allowSxProp). The wrapper now destructures 'sx' whenever the wrapped
component is sx-aware OR the wrapper exposes external sx.
2. The emitted 'sx={...}' on the wrapped component appeared after the
'{...props}' / '{...rest}' spread, silently overriding any caller-passed
sx. The simple wrapper path, the destructure wrapper path, and the
inlined-JSX rewrite path now all compose the caller's sx with the
internal styles via 'sx={[styles.x, sx]}' so consumer sx values
survive re-styling.
Test coverage extends test-cases/wrapper-sxAware to cover an exported
wrapper with external sx, an inlined call site receiving caller sx, and
the wrapper function receiving caller sx.
Also extracts a small isWrappedComponentSxAware helper so the lookup
logic isn't duplicated between the wrapper-emitter and the JSX-rewrite
step.
Co-authored-by: Kenneth Skovhus <skovhus@users.noreply.github.com>
* feat(adapter): auto-detect sx-aware wrapped components from prop type
The codemod now scans an imported component's definition file and walks
its declared prop type (intersections, type aliases, interfaces in the
same file) to find an `sx?:` member. When detected, `styled(Component)`
emits `<Component sx={styles.x} />` instead of
`<Component {...stylex.props(styles.x)} />` automatically — no adapter
configuration required.
The existing `wrappedComponentInterface` adapter hook is kept as an
explicit override for cases auto-detection cannot reach (typically
package imports where the source isn't on disk, or components whose sx
support is added by a HOC at runtime). It now wins over auto-detection
only when it returns a defined value; `undefined` falls through to the
auto-detector.
Adds unit tests for the detection helper covering:
- inline literal prop type
- generic component using `Type<C> = TextProps & Omit<…> & { sx?: … }`
(the canonical real-world pattern)
- interface-typed props
- negative case (no sx member)
- package imports (out of reach)
- all three adapter override outcomes
Extends the wrapper-sxAware fixture with a Text-style generic component
to lock in the type-alias intersection path end-to-end.
Co-authored-by: Kenneth Skovhus <skovhus@users.noreply.github.com>
* test: lock in Omit<…, "sx"> and value-position negatives for sx-aware detection
Adds two regression tests proving the auto-detector returns false when:
1. The component's prop type explicitly omits sx via
Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "sx">
2. "sx" appears as part of a string-literal value of an unrelated prop
(e.g. `variant?: "sx-like" | "other"`)
Both already pass with the current walker because:
- Type parameter lists of opaque utility types (Omit/Pick/etc) are not
descended into; only declared aliases/interfaces in the same file are
resolved.
- Detection only inspects literal/interface member *keys*, never value
positions, so substring matches in string literals can't trigger a
false positive.
Co-authored-by: Kenneth Skovhus <skovhus@users.noreply.github.com>
* test: comprehensive unit-test coverage for sx-aware auto-detection
Expands the wrapped-component-interface test suite to 43 cases organised
by intent, documenting both supported patterns and known limitations of
the static walker. Sections:
- Positive: prop type signatures
* function declarations / arrow / function expression
* required vs optional sx
* string-literal property keys
* any-typed sx values
* default exports
* non-exported declarations (the consumer's import is what makes it
'exported' for the codemod's purposes)
- Positive: type alias / interface resolution
* single alias hop, intersection (Text-style generic component),
nested chain, interface
* union types where one branch carries sx
* parenthesised types
* cyclic aliases (no infinite loop)
- Negative: sx absent or out of reach
* plain components, Omit<…, "sx">, Pick utility narrowing
* sx as a string literal value of an unrelated prop
* sx mentioned only in a comment
* components with no parameters / no parameter type annotation
* sx on a sibling component
* unknown local name
* package-style imports (no source path on disk)
- Negative: documented walker limitations
* React.FC<Props> generic on the variable annotation
* forwardRef HOC wrapper
* interface 'extends' clauses
* type imports across files
- File-system edge cases
* missing file, empty file, syntax error (no throw)
* extension probing for bare absolute paths
- Adapter override semantics
* true / false / undefined fallthrough
* package-import override path
* useSxProp disabled
* missing or empty importMap
- Caching
* repeat lookups stay consistent
* (file, componentName) keying — different components in same file
do not poison each other
Co-authored-by: Kenneth Skovhus <skovhus@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* Optional override for sx-aware wrapped components.
184
+
*
185
+
* When `useSxProp: true`, the codemod auto-detects whether an imported
186
+
* component accepts an `sx` prop by walking its declared prop type
187
+
* (intersections, type aliases, and interfaces in the same file). When
188
+
* `styled(Component)` wraps an sx-aware component, the codemod emits
189
+
* `<Component sx={styles.x} />` instead of `<Component {...stylex.props(styles.x)} />`
190
+
* and lets the wrapped component merge className/style itself.
191
+
*
192
+
* Use this hook to override auto-detection for cases it can't see — typically
193
+
* package imports (where the source isn't on disk) or components whose sx
194
+
* support is added by a HOC at runtime. Returning `undefined` falls through
195
+
* to auto-detection.
196
+
*/
197
+
wrappedComponentInterface(ctx) {
198
+
if (ctx.importSource.startsWith("@company/ui/")) {
199
+
return { acceptsSx: true };
200
+
}
201
+
returnundefined;
202
+
},
203
+
175
204
/**
176
205
* Optional: customize the runtime theme hook used when wrappers need theme booleans.
177
206
* Defaults to useTheme from styled-components.
@@ -203,6 +232,7 @@ Adapters are the main extension point, see full example above. They let you cont
203
232
- how helper calls are resolved (via `resolveCall({ ... })` returning `{ expr, imports }`, or `{ preserveRuntimeCall: true }` to keep only the original helper runtime call; `null`/`undefined` bails the file)
204
233
- which exported components should support external className/style extension and/or polymorphic `as` prop (`externalInterface`)
205
234
- how className/style merging is handled for components accepting external styling (`styleMerger`)
235
+
- which imported components already accept a StyleX `sx` prop (auto-detected from the imported component's prop type when `useSxProp: true`; can be overridden via `wrappedComponentInterface`). When detected, the codemod emits `sx={styles.x}` on the wrapped component instead of `{...stylex.props(styles.x)}`.
206
236
- which runtime theme hook import/call to use for emitted wrapper theme conditionals (`themeHook`)
207
237
- how `styled(ImportedComponent)` wrapping an external base component can be inlined into an intrinsic element with static StyleX styles (`resolveBaseComponent`)
0 commit comments