Skip to content

Commit c1a9bb5

Browse files
committed
Fixes for duplicated and removed exports
1 parent fe6c966 commit c1a9bb5

16 files changed

Lines changed: 495 additions & 29 deletions

src/__tests__/fixture-adapters.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ export const fixtureAdapter = defineAdapter({
3030
resolveValue(ctx) {
3131
if (ctx.kind === "theme") {
3232
// Test fixtures use a small ThemeProvider theme shape:
33-
// props.theme.color.labelBase -> themeVars.labelBase
34-
// props.theme.color[bg] -> themeVars[bg]
33+
// props.theme.colors.labelBase -> themeVars.labelBase
34+
// props.theme.colors[bg] -> themeVars[bg]
3535
//
3636
// `ctx.path` is the dot-path on the theme object (no bracket/index parts).
37-
if (ctx.path === "color") {
37+
if (ctx.path === "colors") {
3838
return {
3939
expr: "themeVars",
4040
imports: [

src/internal/emit-wrappers.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ function emitMinimalWrapper(args: {
151151
destructureProps: string[];
152152
displayName?: string;
153153
patternProp: (keyName: string, valueId?: any) => any;
154+
staticAttrs?: Record<string, any>;
154155
}): any[] {
155156
const {
156157
j,
@@ -162,6 +163,7 @@ function emitMinimalWrapper(args: {
162163
styleArgs,
163164
destructureProps,
164165
patternProp,
166+
staticAttrs = {},
165167
} = args;
166168
const isVoidTag = VOID_TAGS.has(tagName);
167169
const propsParamId = j.identifier("props");
@@ -207,12 +209,32 @@ function emitMinimalWrapper(args: {
207209
styleArgs,
208210
);
209211

210-
// Build JSX attributes: {...rest} {...stylex.props(...)} style={style}
211-
const jsxAttrs: any[] = [
212+
// Build JSX attributes: static attrs, {...rest} {...stylex.props(...)} style={style}
213+
const jsxAttrs: any[] = [];
214+
215+
// Add static attrs from .attrs() (e.g., type="range") first
216+
for (const [key, value] of Object.entries(staticAttrs)) {
217+
if (typeof value === "string") {
218+
jsxAttrs.push(j.jsxAttribute(j.jsxIdentifier(key), j.literal(value)));
219+
} else if (typeof value === "boolean") {
220+
jsxAttrs.push(
221+
j.jsxAttribute(
222+
j.jsxIdentifier(key),
223+
value ? null : j.jsxExpressionContainer(j.literal(false)),
224+
),
225+
);
226+
} else if (typeof value === "number") {
227+
jsxAttrs.push(
228+
j.jsxAttribute(j.jsxIdentifier(key), j.jsxExpressionContainer(j.literal(value))),
229+
);
230+
}
231+
}
232+
233+
jsxAttrs.push(
212234
j.jsxSpreadAttribute(restId),
213235
j.jsxSpreadAttribute(stylexPropsCall),
214236
j.jsxAttribute(j.jsxIdentifier("style"), j.jsxExpressionContainer(j.identifier("style"))),
215-
];
237+
);
216238

217239
const openingEl = j.jsxOpeningElement(j.jsxIdentifier(tagName), jsxAttrs, isVoidTag);
218240

@@ -1596,9 +1618,8 @@ export function emitWrappers(args: {
15961618
if (tagName === "button" && wrapperNames.has(d.localName)) {
15971619
return false;
15981620
}
1599-
if (tagName === "input" || tagName === "a") {
1600-
return false;
1601-
}
1621+
// Note: input/a tags without attrWrapper (e.g., simple .attrs() cases) are now
1622+
// handled here. The attrWrapper case is already excluded above at line 1591.
16021623
return true;
16031624
});
16041625

@@ -1783,6 +1804,7 @@ export function emitWrappers(args: {
17831804
styleArgs,
17841805
destructureProps,
17851806
patternProp,
1807+
...(d.attrsInfo?.staticAttrs ? { staticAttrs: d.attrsInfo.staticAttrs } : {}),
17861808
}),
17871809
d,
17881810
),

src/internal/lower-rules.ts

Lines changed: 173 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { API } from "jscodeshift";
22
import { resolveDynamicNode } from "./builtin-handlers.js";
3-
import { cssDeclarationToStylexDeclarations } from "./css-prop-mapping.js";
3+
import { cssDeclarationToStylexDeclarations, cssPropertyToStylexProp } from "./css-prop-mapping.js";
44
import { getMemberPathFromIdentifier, getNodeLocStart } from "./jscodeshift-utils.js";
55
import type { ImportSource } from "../adapter.js";
66
import {
@@ -674,6 +674,54 @@ export function lowerRules(args: {
674674

675675
// Now treat the interpolated portion as `borderColor`.
676676
const expr = decl.templateExpressions[slotId] as any;
677+
678+
// Helper to parse a border shorthand string and return expanded properties
679+
const parseBorderShorthand = (
680+
value: string,
681+
): { borderWidth?: string; borderStyle?: string; borderColor?: string } | null => {
682+
const tokens = value.trim().split(/\s+/);
683+
const borderStylesSet = new Set([
684+
"none",
685+
"solid",
686+
"dashed",
687+
"dotted",
688+
"double",
689+
"groove",
690+
"ridge",
691+
"inset",
692+
"outset",
693+
]);
694+
const looksLikeLengthLocal = (t: string) =>
695+
/^-?\d*\.?\d+(px|rem|em|vh|vw|vmin|vmax|%)?$/.test(t);
696+
697+
let bWidth: string | undefined;
698+
let bStyle: string | undefined;
699+
const colorParts: string[] = [];
700+
for (const token of tokens) {
701+
if (!bWidth && looksLikeLengthLocal(token)) {
702+
bWidth = token;
703+
} else if (!bStyle && borderStylesSet.has(token)) {
704+
bStyle = token;
705+
} else {
706+
colorParts.push(token);
707+
}
708+
}
709+
const bColor = colorParts.join(" ").trim();
710+
// If we found at least width or style, this is a border shorthand
711+
if (bWidth || bStyle) {
712+
return {
713+
...(bWidth ? { borderWidth: bWidth } : {}),
714+
...(bStyle ? { borderStyle: bStyle } : {}),
715+
...(bColor ? { borderColor: bColor } : {}),
716+
};
717+
}
718+
// Just a color value
719+
if (bColor) {
720+
return { borderColor: bColor };
721+
}
722+
return null;
723+
};
724+
677725
if (expr?.type === "ArrowFunctionExpression" && expr.body?.type === "ConditionalExpression") {
678726
const test = expr.body.test as any;
679727
const cons = expr.body.consequent as any;
@@ -684,14 +732,45 @@ export function lowerRules(args: {
684732
cons?.type === "StringLiteral" &&
685733
alt?.type === "StringLiteral"
686734
) {
687-
// Default to alternate; conditionally apply consequent.
688-
styleObj.borderColor = alt.value;
735+
const altParsed = parseBorderShorthand(alt.value);
736+
const consParsed = parseBorderShorthand(cons.value);
689737
const when = test.property.name;
690-
variantBuckets.set(when, {
691-
...variantBuckets.get(when),
692-
borderColor: cons.value,
693-
});
694-
variantStyleKeys[when] ??= `${decl.styleKey}${toSuffixFromProp(when)}`;
738+
const notWhen = `!${when}`;
739+
740+
// Check if either value is a full border shorthand (has width or style)
741+
const isFullShorthand =
742+
(altParsed && (altParsed.borderWidth || altParsed.borderStyle)) ||
743+
(consParsed && (consParsed.borderWidth || consParsed.borderStyle));
744+
745+
if (isFullShorthand) {
746+
// Both branches should become variants (neither goes to base style)
747+
if (altParsed) {
748+
variantBuckets.set(notWhen, {
749+
...variantBuckets.get(notWhen),
750+
...altParsed,
751+
});
752+
variantStyleKeys[notWhen] ??= `${decl.styleKey}${toSuffixFromProp(notWhen)}`;
753+
}
754+
if (consParsed) {
755+
variantBuckets.set(when, {
756+
...variantBuckets.get(when),
757+
...consParsed,
758+
});
759+
variantStyleKeys[when] ??= `${decl.styleKey}${toSuffixFromProp(when)}`;
760+
}
761+
} else {
762+
// Original behavior: default to alternate, conditionally apply consequent
763+
if (altParsed?.borderColor) {
764+
styleObj.borderColor = altParsed.borderColor;
765+
}
766+
if (consParsed?.borderColor) {
767+
variantBuckets.set(when, {
768+
...variantBuckets.get(when),
769+
borderColor: consParsed.borderColor,
770+
});
771+
variantStyleKeys[when] ??= `${decl.styleKey}${toSuffixFromProp(when)}`;
772+
}
773+
}
695774
return true;
696775
}
697776
}
@@ -702,6 +781,20 @@ export function lowerRules(args: {
702781
return true;
703782
}
704783

784+
// Handle arrow functions that are simple member expressions (like theme access):
785+
// border: 1px solid ${(props) => props.theme.colors.primary}
786+
// In this case, we modify the declaration's property to be "borderColor" so that
787+
// the generic dynamic handler (resolveDynamicNode) outputs borderColor instead of border.
788+
if (expr?.type === "ArrowFunctionExpression") {
789+
const body = expr.body as any;
790+
// Simple arrow function returning a member expression: (p) => p.theme.colors.X
791+
if (body?.type === "MemberExpression") {
792+
// Mutate the declaration's property so fallback handlers use borderColor
793+
d.property = "border-color";
794+
return false; // Let the generic handler resolve the theme value
795+
}
796+
}
797+
705798
// fallback to inline style via wrapper
706799
if (decl.shouldForwardProp) {
707800
inlineStyleProps.push({
@@ -1129,7 +1222,7 @@ export function lowerRules(args: {
11291222
}
11301223

11311224
// Handle computed theme object access keyed by a prop:
1132-
// background-color: ${(props) => props.theme.color[props.bg]}
1225+
// background-color: ${(props) => props.theme.colors[props.bg]}
11331226
//
11341227
// If the adapter can resolve `theme.color` as an object expression, we can emit a StyleX
11351228
// dynamic style function that indexes into that resolved object at runtime:
@@ -1382,7 +1475,10 @@ export function lowerRules(args: {
13821475
const neg = res.variants.find((v: any) => v.when.startsWith("!"));
13831476
const pos = res.variants.find((v: any) => !v.when.startsWith("!"));
13841477

1385-
const outs = cssDeclarationToStylexDeclarations(d);
1478+
const cssProp = (d.property ?? "").trim();
1479+
// Map CSS property to StyleX property (handle special cases like background → backgroundColor)
1480+
const stylexProp =
1481+
cssProp === "background" ? "backgroundColor" : cssPropertyToStylexProp(cssProp);
13861482

13871483
const parseResolved = (
13881484
expr: string,
@@ -1401,17 +1497,81 @@ export function lowerRules(args: {
14011497
return { exprAst, imports: imports ?? [] };
14021498
};
14031499

1500+
// Helper to expand border shorthand from a string literal like "2px solid blue"
1501+
const expandBorderShorthand = (
1502+
target: Record<string, unknown>,
1503+
exprAst: any,
1504+
): boolean => {
1505+
// Handle various AST wrapper structures
1506+
let node = exprAst;
1507+
// Unwrap ExpressionStatement if present
1508+
if (node?.type === "ExpressionStatement") {
1509+
node = node.expression;
1510+
}
1511+
// Only expand if it's a string literal
1512+
if (node?.type !== "StringLiteral" && node?.type !== "Literal") {
1513+
return false;
1514+
}
1515+
const value = node.value;
1516+
if (typeof value !== "string") {
1517+
return false;
1518+
}
1519+
const tokens = value.trim().split(/\s+/);
1520+
const BORDER_STYLES = new Set([
1521+
"none",
1522+
"solid",
1523+
"dashed",
1524+
"dotted",
1525+
"double",
1526+
"groove",
1527+
"ridge",
1528+
"inset",
1529+
"outset",
1530+
]);
1531+
const looksLikeLength = (t: string) =>
1532+
/^-?\d*\.?\d+(px|rem|em|vh|vw|vmin|vmax|ch|ex|lh|%)?$/.test(t);
1533+
1534+
let width: string | undefined;
1535+
let style: string | undefined;
1536+
const colorParts: string[] = [];
1537+
for (const token of tokens) {
1538+
if (!width && looksLikeLength(token)) {
1539+
width = token;
1540+
} else if (!style && BORDER_STYLES.has(token)) {
1541+
style = token;
1542+
} else {
1543+
colorParts.push(token);
1544+
}
1545+
}
1546+
const color = colorParts.join(" ").trim();
1547+
if (!width && !style && !color) {
1548+
return false;
1549+
}
1550+
if (width) {
1551+
target["borderWidth"] = j.literal(width);
1552+
}
1553+
if (style) {
1554+
target["borderStyle"] = j.literal(style);
1555+
}
1556+
if (color) {
1557+
target["borderColor"] = j.literal(color);
1558+
}
1559+
return true;
1560+
};
1561+
14041562
const applyParsed = (
14051563
target: Record<string, unknown>,
14061564
parsed: { exprAst: unknown; imports: any[] },
14071565
): void => {
14081566
for (const imp of parsed.imports) {
14091567
resolverImports.set(JSON.stringify(imp), imp);
14101568
}
1411-
for (let i = 0; i < outs.length; i++) {
1412-
const out = outs[i]!;
1413-
target[out.prop] = parsed.exprAst as any;
1569+
// Special handling for border shorthand with string literal values
1570+
if (cssProp === "border" && expandBorderShorthand(target, parsed.exprAst)) {
1571+
return;
14141572
}
1573+
// Default: use the property from cssDeclarationToStylexDeclarations
1574+
target[stylexProp] = parsed.exprAst as any;
14151575
};
14161576

14171577
// IMPORTANT: stage parsing first. If either branch fails to parse, skip this declaration entirely

src/transform.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,21 @@ export function transformWithWarnings(
838838
const canInline = decl.base.kind === "intrinsic" && hasInlinableAttrs;
839839
if (!canInline) {
840840
decl.needsWrapperComponent = true;
841+
} else {
842+
// Even if canInline is true, we need a wrapper if the component has no JSX usages.
843+
// Without usages, there's nothing to inline into and the export would be lost.
844+
const hasJsxUsages =
845+
root
846+
.find(j.JSXElement, {
847+
openingElement: { name: { type: "JSXIdentifier", name: decl.localName } },
848+
})
849+
.size() > 0 ||
850+
root
851+
.find(j.JSXOpeningElement, { name: { type: "JSXIdentifier", name: decl.localName } })
852+
.size() > 0;
853+
if (!hasJsxUsages) {
854+
decl.needsWrapperComponent = true;
855+
}
841856
}
842857
}
843858
}
@@ -874,9 +889,10 @@ export function transformWithWarnings(
874889
continue;
875890
}
876891

877-
// 3. If exported, ask adapter (default: false)
892+
// 3. If exported, ask adapter (default: true for exported components)
878893
if (!adapter.shouldSupportExternalStyles) {
879-
decl.supportsExternalStyles = false;
894+
// Default to true for exported components - they may be used with external className/style
895+
decl.supportsExternalStyles = true;
880896
continue;
881897
}
882898

@@ -955,6 +971,21 @@ export function transformWithWarnings(
955971
const canInline = decl.base.kind === "intrinsic" && hasInlinableAttrs;
956972
if (!canInline) {
957973
decl.needsWrapperComponent = true;
974+
} else {
975+
// Even if canInline is true, we need a wrapper if the component has no JSX usages.
976+
// Without usages, there's nothing to inline into and the export would be lost.
977+
const hasJsxUsages =
978+
root
979+
.find(j.JSXElement, {
980+
openingElement: { name: { type: "JSXIdentifier", name: decl.localName } },
981+
})
982+
.size() > 0 ||
983+
root
984+
.find(j.JSXOpeningElement, { name: { type: "JSXIdentifier", name: decl.localName } })
985+
.size() > 0;
986+
if (!hasJsxUsages) {
987+
decl.needsWrapperComponent = true;
988+
}
958989
}
959990
}
960991

test-cases/_unsupported.subgrid.input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const GroupHeaderRow = styled.div`
1111
${rowBase}
1212
position: sticky;
1313
z-index: 3; /* above regular rows */
14-
background: ${({ theme }: any) => theme.color.labelBase};
14+
background: ${({ theme }: any) => theme.colors.labelBase};
1515
`;
1616

1717
export const App = () => (

0 commit comments

Comments
 (0)