Skip to content

Commit d6964bf

Browse files
committed
Fix wrappers
1 parent d4e3466 commit d6964bf

13 files changed

Lines changed: 218 additions & 64 deletions

src/internal/emit-styles.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,9 +465,11 @@ export function emitStylesAndImports(args: {
465465
if (!d.needsWrapperComponent) {
466466
return false;
467467
}
468-
// Polymorphic intrinsic wrappers (styled.tag with as={} usage) pass style through directly
468+
// Polymorphic intrinsic wrappers only need the merger when they support external styling.
469469
if ((d as any).isPolymorphicIntrinsicWrapper) {
470-
return false;
470+
return (
471+
d.supportsExternalStyles || d.usedAsValue || (d as any).receivesClassNameOrStyleInJsx
472+
);
471473
}
472474
// Component must support external styling to need the merger
473475
return d.supportsExternalStyles || d.usedAsValue || (d as any).receivesClassNameOrStyleInJsx;

src/internal/emit-wrappers/emit-component.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,7 @@ export function emitComponentWrappers(ctx: any): { emitted: any[]; needsReactTyp
9494
const baseMaybeOmitted = omitted.length
9595
? `Omit<${baseProps}, ${omitted.join(" | ")}>`
9696
: baseProps;
97-
const typeText = joinIntersection(
98-
baseMaybeOmitted,
99-
"{ as?: C }",
100-
`Omit<React.ComponentPropsWithoutRef<C>, keyof ${baseProps} | "as">`,
101-
);
97+
const typeText = joinIntersection(baseMaybeOmitted, "{ as?: C }");
10298
emitNamedPropsType(
10399
d.localName,
104100
typeText,

src/internal/emit-wrappers/emit-intrinsic.ts

Lines changed: 179 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,84 @@ export function emitIntrinsicWrappers(ctx: any): { emitted: any[]; needsReactTyp
3838
const emitted: any[] = [];
3939
let needsReactTypeImport = false;
4040

41+
const mergeAsIntoPropsWithChildren = (typeText: string): string | null => {
42+
const prefix = "React.PropsWithChildren<";
43+
if (!typeText.trim().startsWith(prefix) || !typeText.trim().endsWith(">")) {
44+
return null;
45+
}
46+
const inner = typeText.trim().slice(prefix.length, -1).trim();
47+
if (inner === "{}") {
48+
return `${prefix}{ as?: React.ElementType }>`;
49+
}
50+
if (inner.startsWith("{") && inner.endsWith("}")) {
51+
let body = inner.slice(1, -1).trim();
52+
if (body.endsWith(";")) {
53+
body = body.slice(0, -1).trim();
54+
}
55+
const withAs = body.length > 0 ? `${body}; as?: React.ElementType` : "as?: React.ElementType";
56+
return `${prefix}{ ${withAs} }>`;
57+
}
58+
return null;
59+
};
60+
61+
const addAsPropToExistingType = (typeName: string): boolean => {
62+
if (!emitTypes) {
63+
return false;
64+
}
65+
let didUpdate = false;
66+
const interfaces = root.find(j.TSInterfaceDeclaration, {
67+
id: { type: "Identifier", name: typeName },
68+
} as any);
69+
interfaces.forEach((path: any) => {
70+
const iface = path.node;
71+
const members = iface.body?.body ?? [];
72+
const hasAs = members.some(
73+
(m: any) =>
74+
m.type === "TSPropertySignature" && m.key?.type === "Identifier" && m.key.name === "as",
75+
);
76+
if (hasAs) {
77+
didUpdate = true;
78+
return;
79+
}
80+
const parsed = j(`interface X { as?: React.ElementType }`).get().node.program.body[0] as any;
81+
const prop = parsed.body?.body?.[0];
82+
if (prop) {
83+
members.push(prop);
84+
didUpdate = true;
85+
}
86+
});
87+
if (didUpdate) {
88+
return true;
89+
}
90+
const typeAliases = root.find(j.TSTypeAliasDeclaration, {
91+
id: { type: "Identifier", name: typeName },
92+
} as any);
93+
typeAliases.forEach((path: any) => {
94+
const alias = path.node;
95+
const existing = alias.typeAnnotation;
96+
if (!existing) {
97+
return;
98+
}
99+
const existingStr = j(existing).toSource();
100+
if (existingStr.includes("as?:") || existingStr.includes("as :")) {
101+
didUpdate = true;
102+
return;
103+
}
104+
const parsed = j(`type X = { as?: React.ElementType };`).get().node.program.body[0] as any;
105+
const asType = parsed.typeAnnotation;
106+
if (!asType) {
107+
return;
108+
}
109+
if (existing.type === "TSIntersectionType") {
110+
existing.types = [...(existing.types ?? []), asType];
111+
} else {
112+
alias.typeAnnotation = j.tsIntersectionType([existing, asType]);
113+
}
114+
didUpdate = true;
115+
});
116+
return didUpdate;
117+
};
118+
41119
const inputWrapperDecls = wrapperDecls.filter(
42120
(d: any) =>
43121
d.base.kind === "intrinsic" && d.base.tagName === "input" && d.attrWrapper?.kind === "input",
@@ -291,6 +369,7 @@ export function emitIntrinsicWrappers(ctx: any): { emitted: any[]; needsReactTyp
291369
const tagName = d.base.tagName;
292370
const allowClassNameProp = shouldAllowClassNameProp(d);
293371
const allowStyleProp = shouldAllowStyleProp(d);
372+
const allowAsProp = !VOID_TAGS.has(tagName);
294373
const explicit = stringifyTsType(d.propsType);
295374

296375
// Check if the explicit props type is a simple (non-generic) type reference.
@@ -321,8 +400,10 @@ export function emitIntrinsicWrappers(ctx: any): { emitted: any[]; needsReactTyp
321400
}
322401
const baseMaybeOmitted =
323402
omitted.length > 0 ? `Omit<${base}, ${omitted.join(" | ")}>` : base;
324-
const extra = "{ as?: C }";
325-
return joinIntersection(baseMaybeOmitted, extra);
403+
if (!allowAsProp) {
404+
return baseMaybeOmitted;
405+
}
406+
return joinIntersection(baseMaybeOmitted, "{ as?: C }");
326407
})();
327408

328409
if (!isExplicitNonGenericType) {
@@ -359,11 +440,6 @@ export function emitIntrinsicWrappers(ctx: any): { emitted: any[]; needsReactTyp
359440
}
360441
}
361442

362-
const stylexPropsCall = j.callExpression(
363-
j.memberExpression(j.identifier("stylex"), j.identifier("props")),
364-
styleArgs,
365-
);
366-
367443
const isVoidTag = VOID_TAGS.has(tagName);
368444
const propsParamId = j.identifier("props");
369445
if (emitTypes) {
@@ -386,17 +462,23 @@ export function emitIntrinsicWrappers(ctx: any): { emitted: any[]; needsReactTyp
386462
const propsId = j.identifier("props");
387463
const childrenId = j.identifier("children");
388464
const restId = j.identifier("rest");
465+
const classNameId = j.identifier("className");
389466
const styleId = j.identifier("style");
390467

391468
const declStmt = j.variableDeclaration("const", [
392469
j.variableDeclarator(
393470
j.objectPattern([
394-
j.property.from({
395-
kind: "init",
396-
key: j.identifier("as"),
397-
value: j.assignmentPattern(j.identifier("Component"), j.literal(tagName)),
398-
shorthand: false,
399-
}),
471+
...(allowAsProp
472+
? [
473+
j.property.from({
474+
kind: "init",
475+
key: j.identifier("as"),
476+
value: j.assignmentPattern(j.identifier("Component"), j.literal(tagName)),
477+
shorthand: false,
478+
}),
479+
]
480+
: []),
481+
...(allowClassNameProp ? [patternProp("className", classNameId)] : []),
400482
...(isVoidTag ? [] : [patternProp("children", childrenId)]),
401483
...(allowStyleProp ? [patternProp("style", styleId)] : []),
402484
// Add variant props to destructuring
@@ -407,29 +489,62 @@ export function emitIntrinsicWrappers(ctx: any): { emitted: any[]; needsReactTyp
407489
),
408490
]);
409491

492+
const merging = emitStyleMerging({
493+
j,
494+
styleMerger,
495+
styleArgs,
496+
classNameId,
497+
styleId,
498+
allowClassNameProp,
499+
allowStyleProp,
500+
inlineStyleProps: [],
501+
});
502+
410503
const attrs: any[] = [
411504
j.jsxSpreadAttribute(restId),
412-
j.jsxSpreadAttribute(stylexPropsCall),
413-
...(allowStyleProp
414-
? [j.jsxAttribute(j.jsxIdentifier("style"), j.jsxExpressionContainer(styleId))]
415-
: []),
505+
j.jsxSpreadAttribute(merging.jsxSpreadExpr),
416506
];
417-
const openingEl = j.jsxOpeningElement(j.jsxIdentifier("Component"), attrs, isVoidTag);
507+
if (merging.classNameAttr) {
508+
attrs.push(
509+
j.jsxAttribute(
510+
j.jsxIdentifier("className"),
511+
j.jsxExpressionContainer(merging.classNameAttr),
512+
),
513+
);
514+
}
515+
if (merging.styleAttr) {
516+
attrs.push(
517+
j.jsxAttribute(j.jsxIdentifier("style"), j.jsxExpressionContainer(merging.styleAttr)),
518+
);
519+
}
520+
const openingEl = j.jsxOpeningElement(
521+
j.jsxIdentifier(allowAsProp ? "Component" : tagName),
522+
attrs,
523+
isVoidTag,
524+
);
418525
const jsx = isVoidTag
419526
? ({
420527
type: "JSXElement",
421528
openingElement: openingEl,
422529
closingElement: null,
423530
children: [],
424531
} as any)
425-
: j.jsxElement(openingEl, j.jsxClosingElement(j.jsxIdentifier("Component")), [
426-
j.jsxExpressionContainer(childrenId),
427-
]);
532+
: j.jsxElement(
533+
openingEl,
534+
j.jsxClosingElement(j.jsxIdentifier(allowAsProp ? "Component" : tagName)),
535+
[j.jsxExpressionContainer(childrenId)],
536+
);
537+
538+
const fnBodyStmts: any[] = [declStmt];
539+
if (merging.sxDecl) {
540+
fnBodyStmts.push(merging.sxDecl);
541+
}
542+
fnBodyStmts.push(j.returnStatement(jsx as any));
428543

429544
const fn = j.functionDeclaration(
430545
j.identifier(d.localName),
431546
[propsParamId],
432-
j.blockStatement([declStmt, j.returnStatement(jsx as any)]),
547+
j.blockStatement(fnBodyStmts),
433548
);
434549
// Move the generic parameters from the param to the function node (parser puts it on FunctionDeclaration).
435550
if ((propsParamId as any).typeParameters) {
@@ -1311,6 +1426,12 @@ export function emitIntrinsicWrappers(ctx: any): { emitted: any[]; needsReactTyp
13111426
const tagName = d.base.tagName;
13121427
const allowClassNameProp = shouldAllowClassNameProp(d);
13131428
const allowStyleProp = shouldAllowStyleProp(d);
1429+
const usedAttrsForType = getUsedAttrs(d.localName);
1430+
const allowAsProp =
1431+
!VOID_TAGS.has(tagName) &&
1432+
((d.supportsExternalStyles ?? false) ||
1433+
usedAttrsForType.has("as") ||
1434+
usedAttrsForType.has("forwardedAs"));
13141435
let inlineTypeText: string | undefined;
13151436
{
13161437
const explicit = stringifyTsType(d.propsType);
@@ -1323,7 +1444,6 @@ export function emitIntrinsicWrappers(ctx: any): { emitted: any[]; needsReactTyp
13231444
skipProps: explicitPropNames,
13241445
});
13251446

1326-
const usedAttrsForType = getUsedAttrs(d.localName);
13271447
const variantPropsForType = new Set(
13281448
Object.keys(d.variantStyleKeys ?? {}).flatMap((when: string) => {
13291449
return when.split("&&").flatMap((part: string) => {
@@ -1446,17 +1566,30 @@ export function emitIntrinsicWrappers(ctx: any): { emitted: any[]; needsReactTyp
14461566
}
14471567
return withChildren(explicit);
14481568
})();
1449-
const typeAliasEmitted = emitNamedPropsType(d.localName, typeText);
1569+
const asPropTypeText = allowAsProp ? "{ as?: React.ElementType }" : null;
1570+
const mergedPropsWithChildren = allowAsProp ? mergeAsIntoPropsWithChildren(typeText) : null;
1571+
const typeWithAs = mergedPropsWithChildren
1572+
? mergedPropsWithChildren
1573+
: asPropTypeText
1574+
? joinIntersection(typeText, asPropTypeText)
1575+
: typeText;
1576+
const typeAliasEmitted = emitNamedPropsType(d.localName, typeWithAs);
14501577
if (!typeAliasEmitted && explicit) {
14511578
const propsTypeName = propsTypeNameFor(d.localName);
14521579
const interfaceExtended = extendExistingInterface(propsTypeName, extendBaseTypeText);
14531580
if (!interfaceExtended) {
14541581
const typeAliasExtended = extendExistingTypeAlias(propsTypeName, extendBaseTypeText);
14551582
if (!typeAliasExtended) {
14561583
inlineTypeText = VOID_TAGS.has(tagName) ? explicit : withChildren(explicit);
1584+
if (asPropTypeText) {
1585+
inlineTypeText = joinIntersection(inlineTypeText, asPropTypeText);
1586+
}
14571587
}
14581588
}
14591589
}
1590+
if (!typeAliasEmitted && asPropTypeText) {
1591+
addAsPropToExistingType(propsTypeNameFor(d.localName));
1592+
}
14601593
needsReactTypeImport = true;
14611594
}
14621595
const styleArgs: any[] = [
@@ -1561,17 +1694,28 @@ export function emitIntrinsicWrappers(ctx: any): { emitted: any[]; needsReactTyp
15611694
return !destructureProps.includes(n);
15621695
}));
15631696

1564-
if (allowClassNameProp || allowStyleProp) {
1697+
if (allowAsProp || allowClassNameProp || allowStyleProp) {
15651698
const isVoidTag = VOID_TAGS.has(tagName);
15661699
const propsParamId = j.identifier("props");
15671700
annotatePropsParam(propsParamId, d.localName, inlineTypeText);
15681701
const propsId = j.identifier("props");
1702+
const componentId = j.identifier("Component");
15691703
const classNameId = j.identifier("className");
15701704
const childrenId = j.identifier("children");
15711705
const styleId = j.identifier("style");
15721706
const restId = shouldIncludeRest ? j.identifier("rest") : null;
15731707

15741708
const patternProps: any[] = [
1709+
...(allowAsProp
1710+
? [
1711+
j.property.from({
1712+
kind: "init",
1713+
key: j.identifier("as"),
1714+
value: j.assignmentPattern(componentId, j.literal(tagName)),
1715+
shorthand: false,
1716+
}),
1717+
]
1718+
: []),
15751719
...(allowClassNameProp ? [patternProp("className", classNameId)] : []),
15761720
...(isVoidTag ? [] : [patternProp("children", childrenId)]),
15771721
...(allowStyleProp ? [patternProp("style", styleId)] : []),
@@ -1614,7 +1758,11 @@ export function emitIntrinsicWrappers(ctx: any): { emitted: any[]; needsReactTyp
16141758
);
16151759
}
16161760

1617-
const openingEl = j.jsxOpeningElement(j.jsxIdentifier(tagName), openingAttrs, false);
1761+
const openingEl = j.jsxOpeningElement(
1762+
j.jsxIdentifier(allowAsProp ? "Component" : tagName),
1763+
openingAttrs,
1764+
false,
1765+
);
16181766

16191767
const jsx = isVoidTag
16201768
? ({
@@ -1623,9 +1771,11 @@ export function emitIntrinsicWrappers(ctx: any): { emitted: any[]; needsReactTyp
16231771
closingElement: null,
16241772
children: [],
16251773
} as any)
1626-
: j.jsxElement(openingEl, j.jsxClosingElement(j.jsxIdentifier(tagName)), [
1627-
j.jsxExpressionContainer(childrenId),
1628-
]);
1774+
: j.jsxElement(
1775+
openingEl,
1776+
j.jsxClosingElement(j.jsxIdentifier(allowAsProp ? "Component" : tagName)),
1777+
[j.jsxExpressionContainer(childrenId)],
1778+
);
16291779

16301780
const bodyStmts: any[] = [declStmt];
16311781
if (merging.sxDecl) {

test-cases/as-component-ref.output.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from "react";
22
import * as stylex from "@stylexjs/stylex";
3+
import { mergedSx } from "./lib/mergedSx";
34

45
// SpringValue simulates react-spring's animated value type
56
type SpringValue<T> = { get(): T };
@@ -28,7 +29,7 @@ type AnimatedTextProps<C extends React.ElementType = "span"> = Omit<
2829
function AnimatedText<C extends React.ElementType = "span">(props: AnimatedTextProps<C>) {
2930
const { as: Component = "span", children, style, ...rest } = props;
3031
return (
31-
<Component {...rest} {...stylex.props(styles.animatedText)} style={style}>
32+
<Component {...rest} {...mergedSx(styles.animatedText, undefined, style)}>
3233
{children}
3334
</Component>
3435
);

test-cases/as-prop.output.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ function Button<C extends React.ElementType = "button">(props: ButtonProps<C>) {
1919
type StyledTextProps<C extends React.ElementType = typeof Text> = Omit<
2020
React.ComponentProps<typeof Text>,
2121
"className" | "style"
22-
> & { as?: C } & Omit<
23-
React.ComponentPropsWithoutRef<C>,
24-
keyof React.ComponentProps<typeof Text> | "as"
25-
>;
22+
> & { as?: C };
2623

2724
function StyledText<C extends React.ElementType = typeof Text>(props: StyledTextProps<C>) {
2825
return <Text {...props} {...stylex.props(styles.text)} />;

0 commit comments

Comments
 (0)