@mgcrea/react-native-tailwind 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/dist/babel/index.cjs +165 -24
- package/dist/babel/index.ts +311 -39
- package/package.json +1 -1
- package/src/babel/index.ts +311 -39
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ Compile-time Tailwind CSS for React Native with zero runtime overhead. Transform
|
|
|
22
22
|
- 🔧 **No dependencies** — Direct-to-React-Native style generation without tailwindcss package
|
|
23
23
|
- 🎨 **Custom colors** — Extend the default palette via `tailwind.config.*`
|
|
24
24
|
- 📐 **Arbitrary values** — Use custom sizes and borders: `w-[123px]`, `rounded-[20px]`
|
|
25
|
+
- 🔀 **Dynamic className** — Conditional styles with hybrid compile-time optimization
|
|
25
26
|
- 📜 **Special style props** — Support for `contentContainerClassName`, `columnWrapperClassName`, and more
|
|
26
27
|
|
|
27
28
|
## Installation
|
|
@@ -249,6 +250,116 @@ export function Card({ title, description, onPress }) {
|
|
|
249
250
|
}
|
|
250
251
|
```
|
|
251
252
|
|
|
253
|
+
### Dynamic className (Hybrid Optimization)
|
|
254
|
+
|
|
255
|
+
You can use dynamic expressions in `className` for conditional styling. The Babel plugin will parse all static strings at compile-time and preserve the conditional logic:
|
|
256
|
+
|
|
257
|
+
**Conditional Expression:**
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import { useState } from "react";
|
|
261
|
+
import { View, Text, Pressable } from "react-native";
|
|
262
|
+
|
|
263
|
+
export function ToggleButton() {
|
|
264
|
+
const [isActive, setIsActive] = useState(false);
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<Pressable
|
|
268
|
+
onPress={() => setIsActive(!isActive)}
|
|
269
|
+
className={isActive ? "bg-green-500 p-4" : "bg-red-500 p-4"}
|
|
270
|
+
>
|
|
271
|
+
<Text className="text-white">{isActive ? "Active" : "Inactive"}</Text>
|
|
272
|
+
</Pressable>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Transforms to:**
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
<Pressable
|
|
281
|
+
onPress={() => setIsActive(!isActive)}
|
|
282
|
+
style={isActive ? styles._bg_green_500_p_4 : styles._bg_red_500_p_4}
|
|
283
|
+
>
|
|
284
|
+
<Text style={styles._text_white}>{isActive ? "Active" : "Inactive"}</Text>
|
|
285
|
+
</Pressable>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Template Literal (Static + Dynamic):**
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
<Pressable
|
|
292
|
+
className={`border-2 rounded-lg ${isActive ? "bg-blue-500" : "bg-gray-300"} p-4`}
|
|
293
|
+
>
|
|
294
|
+
<Text className="text-white">Click Me</Text>
|
|
295
|
+
</Pressable>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Transforms to:**
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
<Pressable
|
|
302
|
+
style={[
|
|
303
|
+
styles._border_2,
|
|
304
|
+
styles._rounded_lg,
|
|
305
|
+
isActive ? styles._bg_blue_500 : styles._bg_gray_300,
|
|
306
|
+
styles._p_4
|
|
307
|
+
]}
|
|
308
|
+
>
|
|
309
|
+
<Text style={styles._text_white}>Click Me</Text>
|
|
310
|
+
</Pressable>
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Logical Expression:**
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
<View className={`p-4 bg-gray-100 ${isActive && "border-4 border-purple-500"}`}>
|
|
317
|
+
<Text>Content</Text>
|
|
318
|
+
</View>
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Transforms to:**
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
<View
|
|
325
|
+
style={[
|
|
326
|
+
styles._p_4,
|
|
327
|
+
styles._bg_gray_100,
|
|
328
|
+
isActive && styles._border_4_border_purple_500
|
|
329
|
+
]}
|
|
330
|
+
>
|
|
331
|
+
<Text>Content</Text>
|
|
332
|
+
</View>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Multiple Conditionals:**
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
<View
|
|
339
|
+
className={`${size === "lg" ? "p-8" : "p-4"} ${isActive ? "bg-blue-500" : "bg-gray-400"}`}
|
|
340
|
+
>
|
|
341
|
+
<Text>Dynamic Size & Color</Text>
|
|
342
|
+
</View>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Key Benefits:**
|
|
346
|
+
|
|
347
|
+
- ✅ All string literals are parsed at compile-time
|
|
348
|
+
- ✅ Only conditional logic remains at runtime (no parser overhead)
|
|
349
|
+
- ✅ Full type-safety and validation for all class names
|
|
350
|
+
- ✅ Optimal performance with pre-compiled styles
|
|
351
|
+
|
|
352
|
+
**What Won't Work:**
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
// ❌ Runtime variables in class names
|
|
356
|
+
const spacing = 4;
|
|
357
|
+
<View className={`p-${spacing}`} /> // Can't parse "p-${spacing}" at compile time
|
|
358
|
+
|
|
359
|
+
// ✅ Use inline style for truly dynamic values:
|
|
360
|
+
<View className="border-2" style={{ padding: spacing * 4 }} />
|
|
361
|
+
```
|
|
362
|
+
|
|
252
363
|
### Combining with Inline Styles
|
|
253
364
|
|
|
254
365
|
You can use inline `style` prop alongside `className`:
|
package/dist/babel/index.cjs
CHANGED
|
@@ -972,6 +972,102 @@ function getTargetStyleProp(attributeName) {
|
|
|
972
972
|
}
|
|
973
973
|
return "style";
|
|
974
974
|
}
|
|
975
|
+
function processDynamicExpression(expression, state, t) {
|
|
976
|
+
if (t.isTemplateLiteral(expression)) {
|
|
977
|
+
return processTemplateLiteral(expression, state, t);
|
|
978
|
+
}
|
|
979
|
+
if (t.isConditionalExpression(expression)) {
|
|
980
|
+
return processConditionalExpression(expression, state, t);
|
|
981
|
+
}
|
|
982
|
+
if (t.isLogicalExpression(expression)) {
|
|
983
|
+
return processLogicalExpression(expression, state, t);
|
|
984
|
+
}
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
function processTemplateLiteral(node, state, t) {
|
|
988
|
+
const parts = [];
|
|
989
|
+
const staticParts = [];
|
|
990
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
991
|
+
const quasi = node.quasis[i];
|
|
992
|
+
const staticText = quasi.value.cooked?.trim();
|
|
993
|
+
if (staticText) {
|
|
994
|
+
const classes = staticText.split(/\s+/).filter(Boolean);
|
|
995
|
+
for (const cls of classes) {
|
|
996
|
+
const styleObject = parseClassName2(cls, state.customColors);
|
|
997
|
+
const styleKey = generateStyleKey2(cls);
|
|
998
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
999
|
+
staticParts.push(cls);
|
|
1000
|
+
parts.push(t.memberExpression(t.identifier("styles"), t.identifier(styleKey)));
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (i < node.expressions.length) {
|
|
1004
|
+
const expr = node.expressions[i];
|
|
1005
|
+
const result = processDynamicExpression(expr, state, t);
|
|
1006
|
+
if (result) {
|
|
1007
|
+
parts.push(result.expression);
|
|
1008
|
+
} else {
|
|
1009
|
+
parts.push(expr);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (parts.length === 0) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
const expression = parts.length === 1 ? parts[0] : t.arrayExpression(parts);
|
|
1017
|
+
return {
|
|
1018
|
+
expression,
|
|
1019
|
+
staticParts: staticParts.length > 0 ? staticParts : void 0
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function processConditionalExpression(node, state, t) {
|
|
1023
|
+
const consequent = processStringOrExpression(node.consequent, state, t);
|
|
1024
|
+
const alternate = processStringOrExpression(node.alternate, state, t);
|
|
1025
|
+
if (!consequent && !alternate) {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
const expression = t.conditionalExpression(
|
|
1029
|
+
node.test,
|
|
1030
|
+
consequent ?? t.nullLiteral(),
|
|
1031
|
+
alternate ?? t.nullLiteral()
|
|
1032
|
+
);
|
|
1033
|
+
return { expression };
|
|
1034
|
+
}
|
|
1035
|
+
function processLogicalExpression(node, state, t) {
|
|
1036
|
+
if (node.operator !== "&&") {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
const right = processStringOrExpression(node.right, state, t);
|
|
1040
|
+
if (!right) {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
const expression = t.logicalExpression("&&", node.left, right);
|
|
1044
|
+
return { expression };
|
|
1045
|
+
}
|
|
1046
|
+
function processStringOrExpression(node, state, t) {
|
|
1047
|
+
if (t.isStringLiteral(node)) {
|
|
1048
|
+
const className = node.value.trim();
|
|
1049
|
+
if (!className) {
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
const styleObject = parseClassName2(className, state.customColors);
|
|
1053
|
+
const styleKey = generateStyleKey2(className);
|
|
1054
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
1055
|
+
return t.memberExpression(t.identifier("styles"), t.identifier(styleKey));
|
|
1056
|
+
}
|
|
1057
|
+
if (t.isConditionalExpression(node)) {
|
|
1058
|
+
const result = processConditionalExpression(node, state, t);
|
|
1059
|
+
return result?.expression ?? null;
|
|
1060
|
+
}
|
|
1061
|
+
if (t.isLogicalExpression(node)) {
|
|
1062
|
+
const result = processLogicalExpression(node, state, t);
|
|
1063
|
+
return result?.expression ?? null;
|
|
1064
|
+
}
|
|
1065
|
+
if (t.isTemplateLiteral(node)) {
|
|
1066
|
+
const result = processTemplateLiteral(node, state, t);
|
|
1067
|
+
return result?.expression ?? null;
|
|
1068
|
+
}
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
975
1071
|
function reactNativeTailwindBabelPlugin({
|
|
976
1072
|
types: t
|
|
977
1073
|
}) {
|
|
@@ -1021,34 +1117,61 @@ function reactNativeTailwindBabelPlugin({
|
|
|
1021
1117
|
return;
|
|
1022
1118
|
}
|
|
1023
1119
|
const value = node.value;
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1120
|
+
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
1121
|
+
if (t.isStringLiteral(value)) {
|
|
1122
|
+
const className = value.value.trim();
|
|
1123
|
+
if (!className) {
|
|
1124
|
+
path2.remove();
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
state.hasClassNames = true;
|
|
1128
|
+
const styleObject = parseClassName2(className, state.customColors);
|
|
1129
|
+
const styleKey = generateStyleKey2(className);
|
|
1130
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
1131
|
+
const parent = path2.parent;
|
|
1132
|
+
const styleAttribute = parent.attributes.find(
|
|
1133
|
+
(attr) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp
|
|
1134
|
+
);
|
|
1135
|
+
if (styleAttribute) {
|
|
1136
|
+
mergeStyleAttribute(path2, styleAttribute, styleKey, t);
|
|
1137
|
+
} else {
|
|
1138
|
+
replaceWithStyleAttribute(path2, styleKey, targetStyleProp, t);
|
|
1031
1139
|
}
|
|
1032
1140
|
return;
|
|
1033
1141
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1142
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
1143
|
+
const expression = value.expression;
|
|
1144
|
+
if (t.isJSXEmptyExpression(expression)) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
try {
|
|
1148
|
+
const result = processDynamicExpression(expression, state, t);
|
|
1149
|
+
if (result) {
|
|
1150
|
+
state.hasClassNames = true;
|
|
1151
|
+
const parent = path2.parent;
|
|
1152
|
+
const styleAttribute = parent.attributes.find(
|
|
1153
|
+
(attr) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp
|
|
1154
|
+
);
|
|
1155
|
+
if (styleAttribute) {
|
|
1156
|
+
mergeDynamicStyleAttribute(path2, styleAttribute, result, t);
|
|
1157
|
+
} else {
|
|
1158
|
+
replaceDynamicWithStyleAttribute(path2, result, targetStyleProp, t);
|
|
1159
|
+
}
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1164
|
+
console.warn(
|
|
1165
|
+
`[react-native-tailwind] Failed to process dynamic ${attributeName} at ${state.file.opts.filename ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1038
1169
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const parent = path2.parent;
|
|
1045
|
-
const styleAttribute = parent.attributes.find(
|
|
1046
|
-
(attr) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp
|
|
1047
|
-
);
|
|
1048
|
-
if (styleAttribute) {
|
|
1049
|
-
mergeStyleAttribute(path2, styleAttribute, styleKey, t);
|
|
1050
|
-
} else {
|
|
1051
|
-
replaceWithStyleAttribute(path2, styleKey, targetStyleProp, t);
|
|
1170
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1171
|
+
const filename = state.file.opts.filename ?? "unknown";
|
|
1172
|
+
console.warn(
|
|
1173
|
+
`[react-native-tailwind] Dynamic ${attributeName} values are not fully supported at ${filename}. Use the ${targetStyleProp} prop for dynamic values.`
|
|
1174
|
+
);
|
|
1052
1175
|
}
|
|
1053
1176
|
}
|
|
1054
1177
|
}
|
|
@@ -1077,6 +1200,24 @@ function mergeStyleAttribute(classNamePath, styleAttribute, styleKey, t) {
|
|
|
1077
1200
|
styleAttribute.value = t.jsxExpressionContainer(styleArray);
|
|
1078
1201
|
classNamePath.remove();
|
|
1079
1202
|
}
|
|
1203
|
+
function replaceDynamicWithStyleAttribute(classNamePath, result, targetStyleProp, t) {
|
|
1204
|
+
const styleAttribute = t.jsxAttribute(
|
|
1205
|
+
t.jsxIdentifier(targetStyleProp),
|
|
1206
|
+
t.jsxExpressionContainer(result.expression)
|
|
1207
|
+
);
|
|
1208
|
+
classNamePath.replaceWith(styleAttribute);
|
|
1209
|
+
}
|
|
1210
|
+
function mergeDynamicStyleAttribute(classNamePath, styleAttribute, result, t) {
|
|
1211
|
+
const existingStyle = styleAttribute.value.expression;
|
|
1212
|
+
let styleArray;
|
|
1213
|
+
if (t.isArrayExpression(existingStyle)) {
|
|
1214
|
+
styleArray = t.arrayExpression([result.expression, ...existingStyle.elements]);
|
|
1215
|
+
} else {
|
|
1216
|
+
styleArray = t.arrayExpression([result.expression, existingStyle]);
|
|
1217
|
+
}
|
|
1218
|
+
styleAttribute.value = t.jsxExpressionContainer(styleArray);
|
|
1219
|
+
classNamePath.remove();
|
|
1220
|
+
}
|
|
1080
1221
|
function injectStyles(path2, styleRegistry, t) {
|
|
1081
1222
|
const styleProperties = [];
|
|
1082
1223
|
for (const [key, styleObject] of styleRegistry) {
|
package/dist/babel/index.ts
CHANGED
|
@@ -52,6 +52,193 @@ function getTargetStyleProp(attributeName: string): string {
|
|
|
52
52
|
return "style";
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Result of processing a dynamic expression
|
|
57
|
+
*/
|
|
58
|
+
type DynamicExpressionResult = {
|
|
59
|
+
// The transformed expression to use in the style prop
|
|
60
|
+
expression: any;
|
|
61
|
+
// Static parts that can be parsed at compile time (if any)
|
|
62
|
+
staticParts?: string[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Process a dynamic className expression
|
|
67
|
+
* Extracts static strings and transforms the expression to use pre-compiled styles
|
|
68
|
+
*/
|
|
69
|
+
function processDynamicExpression(
|
|
70
|
+
expression: any,
|
|
71
|
+
state: PluginState,
|
|
72
|
+
t: typeof BabelTypes,
|
|
73
|
+
): DynamicExpressionResult | null {
|
|
74
|
+
// Handle template literals: `m-4 ${condition ? "p-4" : "p-2"}`
|
|
75
|
+
if (t.isTemplateLiteral(expression)) {
|
|
76
|
+
return processTemplateLiteral(expression, state, t);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Handle conditional expressions: condition ? "m-4" : "p-2"
|
|
80
|
+
if (t.isConditionalExpression(expression)) {
|
|
81
|
+
return processConditionalExpression(expression, state, t);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle logical expressions: condition && "m-4"
|
|
85
|
+
if (t.isLogicalExpression(expression)) {
|
|
86
|
+
return processLogicalExpression(expression, state, t);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Unsupported expression type
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Process template literal: `static ${dynamic} more-static`
|
|
95
|
+
*/
|
|
96
|
+
function processTemplateLiteral(
|
|
97
|
+
node: any,
|
|
98
|
+
state: PluginState,
|
|
99
|
+
t: typeof BabelTypes,
|
|
100
|
+
): DynamicExpressionResult | null {
|
|
101
|
+
const parts: any[] = [];
|
|
102
|
+
const staticParts: string[] = [];
|
|
103
|
+
|
|
104
|
+
// Process quasis (static parts) and expressions (dynamic parts)
|
|
105
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
106
|
+
const quasi = node.quasis[i];
|
|
107
|
+
const staticText = quasi.value.cooked?.trim();
|
|
108
|
+
|
|
109
|
+
// Add static part if not empty
|
|
110
|
+
if (staticText) {
|
|
111
|
+
// Parse static classes and add to registry
|
|
112
|
+
const classes = staticText.split(/\s+/).filter(Boolean);
|
|
113
|
+
for (const cls of classes) {
|
|
114
|
+
const styleObject = parseClassName(cls, state.customColors);
|
|
115
|
+
const styleKey = generateStyleKey(cls);
|
|
116
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
117
|
+
staticParts.push(cls);
|
|
118
|
+
|
|
119
|
+
// Add to parts array
|
|
120
|
+
parts.push(t.memberExpression(t.identifier("styles"), t.identifier(styleKey)));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add dynamic expression if exists
|
|
125
|
+
if (i < node.expressions.length) {
|
|
126
|
+
const expr = node.expressions[i];
|
|
127
|
+
|
|
128
|
+
// Recursively process nested dynamic expressions
|
|
129
|
+
const result = processDynamicExpression(expr, state, t);
|
|
130
|
+
if (result) {
|
|
131
|
+
parts.push(result.expression);
|
|
132
|
+
} else {
|
|
133
|
+
// For unsupported expressions, keep them as-is
|
|
134
|
+
// This won't work at runtime but maintains the structure
|
|
135
|
+
parts.push(expr);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (parts.length === 0) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// If single part, return it directly; otherwise return array
|
|
145
|
+
const expression = parts.length === 1 ? parts[0] : t.arrayExpression(parts);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
expression,
|
|
149
|
+
staticParts: staticParts.length > 0 ? staticParts : undefined,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Process conditional expression: condition ? "class-a" : "class-b"
|
|
155
|
+
*/
|
|
156
|
+
function processConditionalExpression(
|
|
157
|
+
node: any,
|
|
158
|
+
state: PluginState,
|
|
159
|
+
t: typeof BabelTypes,
|
|
160
|
+
): DynamicExpressionResult | null {
|
|
161
|
+
const consequent = processStringOrExpression(node.consequent, state, t);
|
|
162
|
+
const alternate = processStringOrExpression(node.alternate, state, t);
|
|
163
|
+
|
|
164
|
+
if (!consequent && !alternate) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Build conditional: condition ? consequentStyle : alternateStyle
|
|
169
|
+
const expression = t.conditionalExpression(
|
|
170
|
+
node.test,
|
|
171
|
+
consequent ?? t.nullLiteral(),
|
|
172
|
+
alternate ?? t.nullLiteral(),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return { expression };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Process logical expression: condition && "class-a"
|
|
180
|
+
*/
|
|
181
|
+
function processLogicalExpression(
|
|
182
|
+
node: any,
|
|
183
|
+
state: PluginState,
|
|
184
|
+
t: typeof BabelTypes,
|
|
185
|
+
): DynamicExpressionResult | null {
|
|
186
|
+
// Only handle AND (&&) expressions
|
|
187
|
+
if (node.operator !== "&&") {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const right = processStringOrExpression(node.right, state, t);
|
|
192
|
+
|
|
193
|
+
if (!right) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Build logical: condition && style
|
|
198
|
+
const expression = t.logicalExpression("&&", node.left, right);
|
|
199
|
+
|
|
200
|
+
return { expression };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Process a node that might be a string literal or another expression
|
|
205
|
+
*/
|
|
206
|
+
function processStringOrExpression(node: any, state: PluginState, t: typeof BabelTypes): any {
|
|
207
|
+
// Handle string literals
|
|
208
|
+
if (t.isStringLiteral(node)) {
|
|
209
|
+
const className = node.value.trim();
|
|
210
|
+
if (!className) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Parse and register styles
|
|
215
|
+
const styleObject = parseClassName(className, state.customColors);
|
|
216
|
+
const styleKey = generateStyleKey(className);
|
|
217
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
218
|
+
|
|
219
|
+
return t.memberExpression(t.identifier("styles"), t.identifier(styleKey));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Handle nested expressions recursively
|
|
223
|
+
if (t.isConditionalExpression(node)) {
|
|
224
|
+
const result = processConditionalExpression(node, state, t);
|
|
225
|
+
return result?.expression ?? null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (t.isLogicalExpression(node)) {
|
|
229
|
+
const result = processLogicalExpression(node, state, t);
|
|
230
|
+
return result?.expression ?? null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (t.isTemplateLiteral(node)) {
|
|
234
|
+
const result = processTemplateLiteral(node, state, t);
|
|
235
|
+
return result?.expression ?? null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Unsupported - return null
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
55
242
|
export default function reactNativeTailwindBabelPlugin({
|
|
56
243
|
types: t,
|
|
57
244
|
}: {
|
|
@@ -121,54 +308,94 @@ export default function reactNativeTailwindBabelPlugin({
|
|
|
121
308
|
|
|
122
309
|
const value = node.value;
|
|
123
310
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
);
|
|
311
|
+
// Determine target style prop based on attribute name
|
|
312
|
+
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
313
|
+
|
|
314
|
+
// Handle static string literals (original behavior)
|
|
315
|
+
if (t.isStringLiteral(value)) {
|
|
316
|
+
const className = value.value.trim();
|
|
317
|
+
|
|
318
|
+
// Skip empty classNames
|
|
319
|
+
if (!className) {
|
|
320
|
+
path.remove();
|
|
321
|
+
return;
|
|
134
322
|
}
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
323
|
|
|
138
|
-
|
|
324
|
+
state.hasClassNames = true;
|
|
139
325
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
path.remove();
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
326
|
+
// Parse className to React Native styles
|
|
327
|
+
const styleObject = parseClassName(className, state.customColors);
|
|
145
328
|
|
|
146
|
-
|
|
329
|
+
// Generate unique style key
|
|
330
|
+
const styleKey = generateStyleKey(className);
|
|
147
331
|
|
|
148
|
-
|
|
149
|
-
|
|
332
|
+
// Store in registry
|
|
333
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
150
334
|
|
|
151
|
-
|
|
152
|
-
|
|
335
|
+
// Check if there's already a style prop on this element
|
|
336
|
+
const parent = path.parent as any;
|
|
337
|
+
const styleAttribute = parent.attributes.find(
|
|
338
|
+
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
339
|
+
);
|
|
153
340
|
|
|
154
|
-
|
|
155
|
-
|
|
341
|
+
if (styleAttribute) {
|
|
342
|
+
// Merge with existing style prop
|
|
343
|
+
mergeStyleAttribute(path, styleAttribute, styleKey, t);
|
|
344
|
+
} else {
|
|
345
|
+
// Replace className with style prop
|
|
346
|
+
replaceWithStyleAttribute(path, styleKey, targetStyleProp, t);
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
156
350
|
|
|
157
|
-
//
|
|
158
|
-
|
|
351
|
+
// Handle dynamic expressions (JSXExpressionContainer)
|
|
352
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
353
|
+
const expression = value.expression;
|
|
354
|
+
|
|
355
|
+
// Skip JSXEmptyExpression
|
|
356
|
+
if (t.isJSXEmptyExpression(expression)) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
// Process dynamic expression
|
|
362
|
+
const result = processDynamicExpression(expression, state, t);
|
|
363
|
+
|
|
364
|
+
if (result) {
|
|
365
|
+
state.hasClassNames = true;
|
|
366
|
+
|
|
367
|
+
// Check if there's already a style prop on this element
|
|
368
|
+
const parent = path.parent as any;
|
|
369
|
+
const styleAttribute = parent.attributes.find(
|
|
370
|
+
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (styleAttribute) {
|
|
374
|
+
// Merge with existing style prop
|
|
375
|
+
mergeDynamicStyleAttribute(path, styleAttribute, result, t);
|
|
376
|
+
} else {
|
|
377
|
+
// Replace className with style prop
|
|
378
|
+
replaceDynamicWithStyleAttribute(path, result, targetStyleProp, t);
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
// Fall through to warning
|
|
384
|
+
if (process.env.NODE_ENV !== "production") {
|
|
385
|
+
console.warn(
|
|
386
|
+
`[react-native-tailwind] Failed to process dynamic ${attributeName} at ${state.file.opts.filename ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
159
391
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// Merge with existing style prop
|
|
168
|
-
mergeStyleAttribute(path, styleAttribute, styleKey, t);
|
|
169
|
-
} else {
|
|
170
|
-
// Replace className with style prop
|
|
171
|
-
replaceWithStyleAttribute(path, styleKey, targetStyleProp, t);
|
|
392
|
+
// Unsupported dynamic className - warn in development
|
|
393
|
+
if (process.env.NODE_ENV !== "production") {
|
|
394
|
+
const filename = state.file.opts.filename ?? "unknown";
|
|
395
|
+
console.warn(
|
|
396
|
+
`[react-native-tailwind] Dynamic ${attributeName} values are not fully supported at ${filename}. ` +
|
|
397
|
+
`Use the ${targetStyleProp} prop for dynamic values.`,
|
|
398
|
+
);
|
|
172
399
|
}
|
|
173
400
|
},
|
|
174
401
|
},
|
|
@@ -229,6 +456,51 @@ function mergeStyleAttribute(
|
|
|
229
456
|
classNamePath.remove();
|
|
230
457
|
}
|
|
231
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Replace className with dynamic style attribute
|
|
461
|
+
*/
|
|
462
|
+
function replaceDynamicWithStyleAttribute(
|
|
463
|
+
classNamePath: NodePath,
|
|
464
|
+
result: DynamicExpressionResult,
|
|
465
|
+
targetStyleProp: string,
|
|
466
|
+
t: typeof BabelTypes,
|
|
467
|
+
) {
|
|
468
|
+
const styleAttribute = t.jsxAttribute(
|
|
469
|
+
t.jsxIdentifier(targetStyleProp),
|
|
470
|
+
t.jsxExpressionContainer(result.expression),
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
classNamePath.replaceWith(styleAttribute);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Merge dynamic className styles with existing style prop
|
|
478
|
+
*/
|
|
479
|
+
function mergeDynamicStyleAttribute(
|
|
480
|
+
classNamePath: NodePath,
|
|
481
|
+
styleAttribute: any,
|
|
482
|
+
result: DynamicExpressionResult,
|
|
483
|
+
t: typeof BabelTypes,
|
|
484
|
+
) {
|
|
485
|
+
const existingStyle = styleAttribute.value.expression;
|
|
486
|
+
|
|
487
|
+
// Merge dynamic expression with existing styles
|
|
488
|
+
// If existing is already an array, append to it; otherwise create new array
|
|
489
|
+
let styleArray;
|
|
490
|
+
if (t.isArrayExpression(existingStyle)) {
|
|
491
|
+
// Prepend dynamic styles to existing array
|
|
492
|
+
styleArray = t.arrayExpression([result.expression, ...existingStyle.elements]);
|
|
493
|
+
} else {
|
|
494
|
+
// Create new array with dynamic styles first, then existing
|
|
495
|
+
styleArray = t.arrayExpression([result.expression, existingStyle]);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
styleAttribute.value = t.jsxExpressionContainer(styleArray);
|
|
499
|
+
|
|
500
|
+
// Remove the className attribute
|
|
501
|
+
classNamePath.remove();
|
|
502
|
+
}
|
|
503
|
+
|
|
232
504
|
/**
|
|
233
505
|
* Inject StyleSheet.create with all collected styles
|
|
234
506
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgcrea/react-native-tailwind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Compile-time Tailwind CSS for React Native with zero runtime overhead",
|
|
5
5
|
"author": "Olivier Louvignes <olivier@mgcrea.io> (https://github.com/mgcrea)",
|
|
6
6
|
"homepage": "https://github.com/mgcrea/react-native-tailwind#readme",
|
package/src/babel/index.ts
CHANGED
|
@@ -52,6 +52,193 @@ function getTargetStyleProp(attributeName: string): string {
|
|
|
52
52
|
return "style";
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Result of processing a dynamic expression
|
|
57
|
+
*/
|
|
58
|
+
type DynamicExpressionResult = {
|
|
59
|
+
// The transformed expression to use in the style prop
|
|
60
|
+
expression: any;
|
|
61
|
+
// Static parts that can be parsed at compile time (if any)
|
|
62
|
+
staticParts?: string[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Process a dynamic className expression
|
|
67
|
+
* Extracts static strings and transforms the expression to use pre-compiled styles
|
|
68
|
+
*/
|
|
69
|
+
function processDynamicExpression(
|
|
70
|
+
expression: any,
|
|
71
|
+
state: PluginState,
|
|
72
|
+
t: typeof BabelTypes,
|
|
73
|
+
): DynamicExpressionResult | null {
|
|
74
|
+
// Handle template literals: `m-4 ${condition ? "p-4" : "p-2"}`
|
|
75
|
+
if (t.isTemplateLiteral(expression)) {
|
|
76
|
+
return processTemplateLiteral(expression, state, t);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Handle conditional expressions: condition ? "m-4" : "p-2"
|
|
80
|
+
if (t.isConditionalExpression(expression)) {
|
|
81
|
+
return processConditionalExpression(expression, state, t);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle logical expressions: condition && "m-4"
|
|
85
|
+
if (t.isLogicalExpression(expression)) {
|
|
86
|
+
return processLogicalExpression(expression, state, t);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Unsupported expression type
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Process template literal: `static ${dynamic} more-static`
|
|
95
|
+
*/
|
|
96
|
+
function processTemplateLiteral(
|
|
97
|
+
node: any,
|
|
98
|
+
state: PluginState,
|
|
99
|
+
t: typeof BabelTypes,
|
|
100
|
+
): DynamicExpressionResult | null {
|
|
101
|
+
const parts: any[] = [];
|
|
102
|
+
const staticParts: string[] = [];
|
|
103
|
+
|
|
104
|
+
// Process quasis (static parts) and expressions (dynamic parts)
|
|
105
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
106
|
+
const quasi = node.quasis[i];
|
|
107
|
+
const staticText = quasi.value.cooked?.trim();
|
|
108
|
+
|
|
109
|
+
// Add static part if not empty
|
|
110
|
+
if (staticText) {
|
|
111
|
+
// Parse static classes and add to registry
|
|
112
|
+
const classes = staticText.split(/\s+/).filter(Boolean);
|
|
113
|
+
for (const cls of classes) {
|
|
114
|
+
const styleObject = parseClassName(cls, state.customColors);
|
|
115
|
+
const styleKey = generateStyleKey(cls);
|
|
116
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
117
|
+
staticParts.push(cls);
|
|
118
|
+
|
|
119
|
+
// Add to parts array
|
|
120
|
+
parts.push(t.memberExpression(t.identifier("styles"), t.identifier(styleKey)));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add dynamic expression if exists
|
|
125
|
+
if (i < node.expressions.length) {
|
|
126
|
+
const expr = node.expressions[i];
|
|
127
|
+
|
|
128
|
+
// Recursively process nested dynamic expressions
|
|
129
|
+
const result = processDynamicExpression(expr, state, t);
|
|
130
|
+
if (result) {
|
|
131
|
+
parts.push(result.expression);
|
|
132
|
+
} else {
|
|
133
|
+
// For unsupported expressions, keep them as-is
|
|
134
|
+
// This won't work at runtime but maintains the structure
|
|
135
|
+
parts.push(expr);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (parts.length === 0) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// If single part, return it directly; otherwise return array
|
|
145
|
+
const expression = parts.length === 1 ? parts[0] : t.arrayExpression(parts);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
expression,
|
|
149
|
+
staticParts: staticParts.length > 0 ? staticParts : undefined,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Process conditional expression: condition ? "class-a" : "class-b"
|
|
155
|
+
*/
|
|
156
|
+
function processConditionalExpression(
|
|
157
|
+
node: any,
|
|
158
|
+
state: PluginState,
|
|
159
|
+
t: typeof BabelTypes,
|
|
160
|
+
): DynamicExpressionResult | null {
|
|
161
|
+
const consequent = processStringOrExpression(node.consequent, state, t);
|
|
162
|
+
const alternate = processStringOrExpression(node.alternate, state, t);
|
|
163
|
+
|
|
164
|
+
if (!consequent && !alternate) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Build conditional: condition ? consequentStyle : alternateStyle
|
|
169
|
+
const expression = t.conditionalExpression(
|
|
170
|
+
node.test,
|
|
171
|
+
consequent ?? t.nullLiteral(),
|
|
172
|
+
alternate ?? t.nullLiteral(),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return { expression };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Process logical expression: condition && "class-a"
|
|
180
|
+
*/
|
|
181
|
+
function processLogicalExpression(
|
|
182
|
+
node: any,
|
|
183
|
+
state: PluginState,
|
|
184
|
+
t: typeof BabelTypes,
|
|
185
|
+
): DynamicExpressionResult | null {
|
|
186
|
+
// Only handle AND (&&) expressions
|
|
187
|
+
if (node.operator !== "&&") {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const right = processStringOrExpression(node.right, state, t);
|
|
192
|
+
|
|
193
|
+
if (!right) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Build logical: condition && style
|
|
198
|
+
const expression = t.logicalExpression("&&", node.left, right);
|
|
199
|
+
|
|
200
|
+
return { expression };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Process a node that might be a string literal or another expression
|
|
205
|
+
*/
|
|
206
|
+
function processStringOrExpression(node: any, state: PluginState, t: typeof BabelTypes): any {
|
|
207
|
+
// Handle string literals
|
|
208
|
+
if (t.isStringLiteral(node)) {
|
|
209
|
+
const className = node.value.trim();
|
|
210
|
+
if (!className) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Parse and register styles
|
|
215
|
+
const styleObject = parseClassName(className, state.customColors);
|
|
216
|
+
const styleKey = generateStyleKey(className);
|
|
217
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
218
|
+
|
|
219
|
+
return t.memberExpression(t.identifier("styles"), t.identifier(styleKey));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Handle nested expressions recursively
|
|
223
|
+
if (t.isConditionalExpression(node)) {
|
|
224
|
+
const result = processConditionalExpression(node, state, t);
|
|
225
|
+
return result?.expression ?? null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (t.isLogicalExpression(node)) {
|
|
229
|
+
const result = processLogicalExpression(node, state, t);
|
|
230
|
+
return result?.expression ?? null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (t.isTemplateLiteral(node)) {
|
|
234
|
+
const result = processTemplateLiteral(node, state, t);
|
|
235
|
+
return result?.expression ?? null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Unsupported - return null
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
55
242
|
export default function reactNativeTailwindBabelPlugin({
|
|
56
243
|
types: t,
|
|
57
244
|
}: {
|
|
@@ -121,54 +308,94 @@ export default function reactNativeTailwindBabelPlugin({
|
|
|
121
308
|
|
|
122
309
|
const value = node.value;
|
|
123
310
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
);
|
|
311
|
+
// Determine target style prop based on attribute name
|
|
312
|
+
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
313
|
+
|
|
314
|
+
// Handle static string literals (original behavior)
|
|
315
|
+
if (t.isStringLiteral(value)) {
|
|
316
|
+
const className = value.value.trim();
|
|
317
|
+
|
|
318
|
+
// Skip empty classNames
|
|
319
|
+
if (!className) {
|
|
320
|
+
path.remove();
|
|
321
|
+
return;
|
|
134
322
|
}
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
323
|
|
|
138
|
-
|
|
324
|
+
state.hasClassNames = true;
|
|
139
325
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
path.remove();
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
326
|
+
// Parse className to React Native styles
|
|
327
|
+
const styleObject = parseClassName(className, state.customColors);
|
|
145
328
|
|
|
146
|
-
|
|
329
|
+
// Generate unique style key
|
|
330
|
+
const styleKey = generateStyleKey(className);
|
|
147
331
|
|
|
148
|
-
|
|
149
|
-
|
|
332
|
+
// Store in registry
|
|
333
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
150
334
|
|
|
151
|
-
|
|
152
|
-
|
|
335
|
+
// Check if there's already a style prop on this element
|
|
336
|
+
const parent = path.parent as any;
|
|
337
|
+
const styleAttribute = parent.attributes.find(
|
|
338
|
+
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
339
|
+
);
|
|
153
340
|
|
|
154
|
-
|
|
155
|
-
|
|
341
|
+
if (styleAttribute) {
|
|
342
|
+
// Merge with existing style prop
|
|
343
|
+
mergeStyleAttribute(path, styleAttribute, styleKey, t);
|
|
344
|
+
} else {
|
|
345
|
+
// Replace className with style prop
|
|
346
|
+
replaceWithStyleAttribute(path, styleKey, targetStyleProp, t);
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
156
350
|
|
|
157
|
-
//
|
|
158
|
-
|
|
351
|
+
// Handle dynamic expressions (JSXExpressionContainer)
|
|
352
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
353
|
+
const expression = value.expression;
|
|
354
|
+
|
|
355
|
+
// Skip JSXEmptyExpression
|
|
356
|
+
if (t.isJSXEmptyExpression(expression)) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
// Process dynamic expression
|
|
362
|
+
const result = processDynamicExpression(expression, state, t);
|
|
363
|
+
|
|
364
|
+
if (result) {
|
|
365
|
+
state.hasClassNames = true;
|
|
366
|
+
|
|
367
|
+
// Check if there's already a style prop on this element
|
|
368
|
+
const parent = path.parent as any;
|
|
369
|
+
const styleAttribute = parent.attributes.find(
|
|
370
|
+
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (styleAttribute) {
|
|
374
|
+
// Merge with existing style prop
|
|
375
|
+
mergeDynamicStyleAttribute(path, styleAttribute, result, t);
|
|
376
|
+
} else {
|
|
377
|
+
// Replace className with style prop
|
|
378
|
+
replaceDynamicWithStyleAttribute(path, result, targetStyleProp, t);
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
// Fall through to warning
|
|
384
|
+
if (process.env.NODE_ENV !== "production") {
|
|
385
|
+
console.warn(
|
|
386
|
+
`[react-native-tailwind] Failed to process dynamic ${attributeName} at ${state.file.opts.filename ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
159
391
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// Merge with existing style prop
|
|
168
|
-
mergeStyleAttribute(path, styleAttribute, styleKey, t);
|
|
169
|
-
} else {
|
|
170
|
-
// Replace className with style prop
|
|
171
|
-
replaceWithStyleAttribute(path, styleKey, targetStyleProp, t);
|
|
392
|
+
// Unsupported dynamic className - warn in development
|
|
393
|
+
if (process.env.NODE_ENV !== "production") {
|
|
394
|
+
const filename = state.file.opts.filename ?? "unknown";
|
|
395
|
+
console.warn(
|
|
396
|
+
`[react-native-tailwind] Dynamic ${attributeName} values are not fully supported at ${filename}. ` +
|
|
397
|
+
`Use the ${targetStyleProp} prop for dynamic values.`,
|
|
398
|
+
);
|
|
172
399
|
}
|
|
173
400
|
},
|
|
174
401
|
},
|
|
@@ -229,6 +456,51 @@ function mergeStyleAttribute(
|
|
|
229
456
|
classNamePath.remove();
|
|
230
457
|
}
|
|
231
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Replace className with dynamic style attribute
|
|
461
|
+
*/
|
|
462
|
+
function replaceDynamicWithStyleAttribute(
|
|
463
|
+
classNamePath: NodePath,
|
|
464
|
+
result: DynamicExpressionResult,
|
|
465
|
+
targetStyleProp: string,
|
|
466
|
+
t: typeof BabelTypes,
|
|
467
|
+
) {
|
|
468
|
+
const styleAttribute = t.jsxAttribute(
|
|
469
|
+
t.jsxIdentifier(targetStyleProp),
|
|
470
|
+
t.jsxExpressionContainer(result.expression),
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
classNamePath.replaceWith(styleAttribute);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Merge dynamic className styles with existing style prop
|
|
478
|
+
*/
|
|
479
|
+
function mergeDynamicStyleAttribute(
|
|
480
|
+
classNamePath: NodePath,
|
|
481
|
+
styleAttribute: any,
|
|
482
|
+
result: DynamicExpressionResult,
|
|
483
|
+
t: typeof BabelTypes,
|
|
484
|
+
) {
|
|
485
|
+
const existingStyle = styleAttribute.value.expression;
|
|
486
|
+
|
|
487
|
+
// Merge dynamic expression with existing styles
|
|
488
|
+
// If existing is already an array, append to it; otherwise create new array
|
|
489
|
+
let styleArray;
|
|
490
|
+
if (t.isArrayExpression(existingStyle)) {
|
|
491
|
+
// Prepend dynamic styles to existing array
|
|
492
|
+
styleArray = t.arrayExpression([result.expression, ...existingStyle.elements]);
|
|
493
|
+
} else {
|
|
494
|
+
// Create new array with dynamic styles first, then existing
|
|
495
|
+
styleArray = t.arrayExpression([result.expression, existingStyle]);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
styleAttribute.value = t.jsxExpressionContainer(styleArray);
|
|
499
|
+
|
|
500
|
+
// Remove the className attribute
|
|
501
|
+
classNamePath.remove();
|
|
502
|
+
}
|
|
503
|
+
|
|
232
504
|
/**
|
|
233
505
|
* Inject StyleSheet.create with all collected styles
|
|
234
506
|
*/
|