@mgcrea/react-native-tailwind 0.2.0 → 0.4.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 +107 -6
- package/dist/babel/index.cjs +169 -27
- package/dist/babel/index.ts +318 -43
- package/package.json +1 -1
- package/src/babel/index.ts +318 -43
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
|
|
@@ -94,11 +95,11 @@ The Babel plugin transforms your code at compile time:
|
|
|
94
95
|
```tsx
|
|
95
96
|
import { StyleSheet } from "react-native";
|
|
96
97
|
|
|
97
|
-
<View style={
|
|
98
|
-
<ScrollView contentContainerStyle={
|
|
99
|
-
<FlatList columnWrapperStyle={
|
|
98
|
+
<View style={_twStyles._bg_blue_500_m_4_p_2_rounded_lg} />;
|
|
99
|
+
<ScrollView contentContainerStyle={_twStyles._gap_4_items_center} />;
|
|
100
|
+
<FlatList columnWrapperStyle={_twStyles._gap_4} ListHeaderComponentStyle={_twStyles._bg_gray_100_p_4} />;
|
|
100
101
|
|
|
101
|
-
const
|
|
102
|
+
const _twStyles = StyleSheet.create({
|
|
102
103
|
_bg_blue_500_m_4_p_2_rounded_lg: {
|
|
103
104
|
margin: 16,
|
|
104
105
|
padding: 8,
|
|
@@ -249,6 +250,106 @@ 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 ? _twStyles._bg_green_500_p_4 : _twStyles._bg_red_500_p_4}
|
|
283
|
+
>
|
|
284
|
+
<Text style={_twStyles._text_white}>{isActive ? "Active" : "Inactive"}</Text>
|
|
285
|
+
</Pressable>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Template Literal (Static + Dynamic):**
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
<Pressable className={`border-2 rounded-lg ${isActive ? "bg-blue-500" : "bg-gray-300"} p-4`}>
|
|
292
|
+
<Text className="text-white">Click Me</Text>
|
|
293
|
+
</Pressable>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Transforms to:**
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
<Pressable
|
|
300
|
+
style={[
|
|
301
|
+
_twStyles._border_2,
|
|
302
|
+
_twStyles._rounded_lg,
|
|
303
|
+
isActive ? _twStyles._bg_blue_500 : _twStyles._bg_gray_300,
|
|
304
|
+
_twStyles._p_4,
|
|
305
|
+
]}
|
|
306
|
+
>
|
|
307
|
+
<Text style={_twStyles._text_white}>Click Me</Text>
|
|
308
|
+
</Pressable>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Logical Expression:**
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
<View className={`p-4 bg-gray-100 ${isActive && "border-4 border-purple-500"}`}>
|
|
315
|
+
<Text>Content</Text>
|
|
316
|
+
</View>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Transforms to:**
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
<View style={[_twStyles._p_4, _twStyles._bg_gray_100, isActive && _twStyles._border_4_border_purple_500]}>
|
|
323
|
+
<Text>Content</Text>
|
|
324
|
+
</View>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Multiple Conditionals:**
|
|
328
|
+
|
|
329
|
+
```tsx
|
|
330
|
+
<View className={`${size === "lg" ? "p-8" : "p-4"} ${isActive ? "bg-blue-500" : "bg-gray-400"}`}>
|
|
331
|
+
<Text>Dynamic Size & Color</Text>
|
|
332
|
+
</View>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Key Benefits:**
|
|
336
|
+
|
|
337
|
+
- ✅ All string literals are parsed at compile-time
|
|
338
|
+
- ✅ Only conditional logic remains at runtime (no parser overhead)
|
|
339
|
+
- ✅ Full type-safety and validation for all class names
|
|
340
|
+
- ✅ Optimal performance with pre-compiled styles
|
|
341
|
+
|
|
342
|
+
**What Won't Work:**
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
// ❌ Runtime variables in class names
|
|
346
|
+
const spacing = 4;
|
|
347
|
+
<View className={`p-${spacing}`} /> // Can't parse "p-${spacing}" at compile time
|
|
348
|
+
|
|
349
|
+
// ✅ Use inline style for truly dynamic values:
|
|
350
|
+
<View className="border-2" style={{ padding: spacing * 4 }} />
|
|
351
|
+
```
|
|
352
|
+
|
|
252
353
|
### Combining with Inline Styles
|
|
253
354
|
|
|
254
355
|
You can use inline `style` prop alongside `className`:
|
|
@@ -262,7 +363,7 @@ You can use inline `style` prop alongside `className`:
|
|
|
262
363
|
The Babel plugin will merge them:
|
|
263
364
|
|
|
264
365
|
```tsx
|
|
265
|
-
<View style={[
|
|
366
|
+
<View style={[_twStyles._className_styles, { paddingTop: safeAreaInsets.top }]}>
|
|
266
367
|
<Text>Content</Text>
|
|
267
368
|
</View>
|
|
268
369
|
```
|
|
@@ -456,7 +557,7 @@ Access the parser and constants programmatically:
|
|
|
456
557
|
import { parseClassName, COLORS, SPACING_SCALE } from "@mgcrea/react-native-tailwind";
|
|
457
558
|
|
|
458
559
|
// Parse className strings
|
|
459
|
-
const
|
|
560
|
+
const _twStyles = parseClassName("m-4 p-2 bg-blue-500");
|
|
460
561
|
// Returns: { margin: 16, padding: 8, backgroundColor: '#3B82F6' }
|
|
461
562
|
|
|
462
563
|
// Access default scales
|
package/dist/babel/index.cjs
CHANGED
|
@@ -950,6 +950,7 @@ function extractCustomColors(filename) {
|
|
|
950
950
|
}
|
|
951
951
|
|
|
952
952
|
// src/babel/index.ts
|
|
953
|
+
var STYLES_IDENTIFIER = "_twStyles";
|
|
953
954
|
var SUPPORTED_CLASS_ATTRIBUTES = [
|
|
954
955
|
"className",
|
|
955
956
|
"contentContainerClassName",
|
|
@@ -972,6 +973,102 @@ function getTargetStyleProp(attributeName) {
|
|
|
972
973
|
}
|
|
973
974
|
return "style";
|
|
974
975
|
}
|
|
976
|
+
function processDynamicExpression(expression, state, t) {
|
|
977
|
+
if (t.isTemplateLiteral(expression)) {
|
|
978
|
+
return processTemplateLiteral(expression, state, t);
|
|
979
|
+
}
|
|
980
|
+
if (t.isConditionalExpression(expression)) {
|
|
981
|
+
return processConditionalExpression(expression, state, t);
|
|
982
|
+
}
|
|
983
|
+
if (t.isLogicalExpression(expression)) {
|
|
984
|
+
return processLogicalExpression(expression, state, t);
|
|
985
|
+
}
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
function processTemplateLiteral(node, state, t) {
|
|
989
|
+
const parts = [];
|
|
990
|
+
const staticParts = [];
|
|
991
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
992
|
+
const quasi = node.quasis[i];
|
|
993
|
+
const staticText = quasi.value.cooked?.trim();
|
|
994
|
+
if (staticText) {
|
|
995
|
+
const classes = staticText.split(/\s+/).filter(Boolean);
|
|
996
|
+
for (const cls of classes) {
|
|
997
|
+
const styleObject = parseClassName2(cls, state.customColors);
|
|
998
|
+
const styleKey = generateStyleKey2(cls);
|
|
999
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
1000
|
+
staticParts.push(cls);
|
|
1001
|
+
parts.push(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)));
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
if (i < node.expressions.length) {
|
|
1005
|
+
const expr = node.expressions[i];
|
|
1006
|
+
const result = processDynamicExpression(expr, state, t);
|
|
1007
|
+
if (result) {
|
|
1008
|
+
parts.push(result.expression);
|
|
1009
|
+
} else {
|
|
1010
|
+
parts.push(expr);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (parts.length === 0) {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
const expression = parts.length === 1 ? parts[0] : t.arrayExpression(parts);
|
|
1018
|
+
return {
|
|
1019
|
+
expression,
|
|
1020
|
+
staticParts: staticParts.length > 0 ? staticParts : void 0
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
function processConditionalExpression(node, state, t) {
|
|
1024
|
+
const consequent = processStringOrExpression(node.consequent, state, t);
|
|
1025
|
+
const alternate = processStringOrExpression(node.alternate, state, t);
|
|
1026
|
+
if (!consequent && !alternate) {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
const expression = t.conditionalExpression(
|
|
1030
|
+
node.test,
|
|
1031
|
+
consequent ?? t.nullLiteral(),
|
|
1032
|
+
alternate ?? t.nullLiteral()
|
|
1033
|
+
);
|
|
1034
|
+
return { expression };
|
|
1035
|
+
}
|
|
1036
|
+
function processLogicalExpression(node, state, t) {
|
|
1037
|
+
if (node.operator !== "&&") {
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
const right = processStringOrExpression(node.right, state, t);
|
|
1041
|
+
if (!right) {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
const expression = t.logicalExpression("&&", node.left, right);
|
|
1045
|
+
return { expression };
|
|
1046
|
+
}
|
|
1047
|
+
function processStringOrExpression(node, state, t) {
|
|
1048
|
+
if (t.isStringLiteral(node)) {
|
|
1049
|
+
const className = node.value.trim();
|
|
1050
|
+
if (!className) {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
const styleObject = parseClassName2(className, state.customColors);
|
|
1054
|
+
const styleKey = generateStyleKey2(className);
|
|
1055
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
1056
|
+
return t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey));
|
|
1057
|
+
}
|
|
1058
|
+
if (t.isConditionalExpression(node)) {
|
|
1059
|
+
const result = processConditionalExpression(node, state, t);
|
|
1060
|
+
return result?.expression ?? null;
|
|
1061
|
+
}
|
|
1062
|
+
if (t.isLogicalExpression(node)) {
|
|
1063
|
+
const result = processLogicalExpression(node, state, t);
|
|
1064
|
+
return result?.expression ?? null;
|
|
1065
|
+
}
|
|
1066
|
+
if (t.isTemplateLiteral(node)) {
|
|
1067
|
+
const result = processTemplateLiteral(node, state, t);
|
|
1068
|
+
return result?.expression ?? null;
|
|
1069
|
+
}
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
975
1072
|
function reactNativeTailwindBabelPlugin({
|
|
976
1073
|
types: t
|
|
977
1074
|
}) {
|
|
@@ -1021,34 +1118,61 @@ function reactNativeTailwindBabelPlugin({
|
|
|
1021
1118
|
return;
|
|
1022
1119
|
}
|
|
1023
1120
|
const value = node.value;
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1121
|
+
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
1122
|
+
if (t.isStringLiteral(value)) {
|
|
1123
|
+
const className = value.value.trim();
|
|
1124
|
+
if (!className) {
|
|
1125
|
+
path2.remove();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
state.hasClassNames = true;
|
|
1129
|
+
const styleObject = parseClassName2(className, state.customColors);
|
|
1130
|
+
const styleKey = generateStyleKey2(className);
|
|
1131
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
1132
|
+
const parent = path2.parent;
|
|
1133
|
+
const styleAttribute = parent.attributes.find(
|
|
1134
|
+
(attr) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp
|
|
1135
|
+
);
|
|
1136
|
+
if (styleAttribute) {
|
|
1137
|
+
mergeStyleAttribute(path2, styleAttribute, styleKey, t);
|
|
1138
|
+
} else {
|
|
1139
|
+
replaceWithStyleAttribute(path2, styleKey, targetStyleProp, t);
|
|
1031
1140
|
}
|
|
1032
1141
|
return;
|
|
1033
1142
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1143
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
1144
|
+
const expression = value.expression;
|
|
1145
|
+
if (t.isJSXEmptyExpression(expression)) {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
try {
|
|
1149
|
+
const result = processDynamicExpression(expression, state, t);
|
|
1150
|
+
if (result) {
|
|
1151
|
+
state.hasClassNames = true;
|
|
1152
|
+
const parent = path2.parent;
|
|
1153
|
+
const styleAttribute = parent.attributes.find(
|
|
1154
|
+
(attr) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp
|
|
1155
|
+
);
|
|
1156
|
+
if (styleAttribute) {
|
|
1157
|
+
mergeDynamicStyleAttribute(path2, styleAttribute, result, t);
|
|
1158
|
+
} else {
|
|
1159
|
+
replaceDynamicWithStyleAttribute(path2, result, targetStyleProp, t);
|
|
1160
|
+
}
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1165
|
+
console.warn(
|
|
1166
|
+
`[react-native-tailwind] Failed to process dynamic ${attributeName} at ${state.file.opts.filename ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1038
1170
|
}
|
|
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);
|
|
1171
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1172
|
+
const filename = state.file.opts.filename ?? "unknown";
|
|
1173
|
+
console.warn(
|
|
1174
|
+
`[react-native-tailwind] Dynamic ${attributeName} values are not fully supported at ${filename}. Use the ${targetStyleProp} prop for dynamic values.`
|
|
1175
|
+
);
|
|
1052
1176
|
}
|
|
1053
1177
|
}
|
|
1054
1178
|
}
|
|
@@ -1064,19 +1188,37 @@ function addStyleSheetImport(path2, t) {
|
|
|
1064
1188
|
function replaceWithStyleAttribute(classNamePath, styleKey, targetStyleProp, t) {
|
|
1065
1189
|
const styleAttribute = t.jsxAttribute(
|
|
1066
1190
|
t.jsxIdentifier(targetStyleProp),
|
|
1067
|
-
t.jsxExpressionContainer(t.memberExpression(t.identifier(
|
|
1191
|
+
t.jsxExpressionContainer(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)))
|
|
1068
1192
|
);
|
|
1069
1193
|
classNamePath.replaceWith(styleAttribute);
|
|
1070
1194
|
}
|
|
1071
1195
|
function mergeStyleAttribute(classNamePath, styleAttribute, styleKey, t) {
|
|
1072
1196
|
const existingStyle = styleAttribute.value.expression;
|
|
1073
1197
|
const styleArray = t.arrayExpression([
|
|
1074
|
-
t.memberExpression(t.identifier(
|
|
1198
|
+
t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)),
|
|
1075
1199
|
existingStyle
|
|
1076
1200
|
]);
|
|
1077
1201
|
styleAttribute.value = t.jsxExpressionContainer(styleArray);
|
|
1078
1202
|
classNamePath.remove();
|
|
1079
1203
|
}
|
|
1204
|
+
function replaceDynamicWithStyleAttribute(classNamePath, result, targetStyleProp, t) {
|
|
1205
|
+
const styleAttribute = t.jsxAttribute(
|
|
1206
|
+
t.jsxIdentifier(targetStyleProp),
|
|
1207
|
+
t.jsxExpressionContainer(result.expression)
|
|
1208
|
+
);
|
|
1209
|
+
classNamePath.replaceWith(styleAttribute);
|
|
1210
|
+
}
|
|
1211
|
+
function mergeDynamicStyleAttribute(classNamePath, styleAttribute, result, t) {
|
|
1212
|
+
const existingStyle = styleAttribute.value.expression;
|
|
1213
|
+
let styleArray;
|
|
1214
|
+
if (t.isArrayExpression(existingStyle)) {
|
|
1215
|
+
styleArray = t.arrayExpression([result.expression, ...existingStyle.elements]);
|
|
1216
|
+
} else {
|
|
1217
|
+
styleArray = t.arrayExpression([result.expression, existingStyle]);
|
|
1218
|
+
}
|
|
1219
|
+
styleAttribute.value = t.jsxExpressionContainer(styleArray);
|
|
1220
|
+
classNamePath.remove();
|
|
1221
|
+
}
|
|
1080
1222
|
function injectStyles(path2, styleRegistry, t) {
|
|
1081
1223
|
const styleProperties = [];
|
|
1082
1224
|
for (const [key, styleObject] of styleRegistry) {
|
|
@@ -1095,7 +1237,7 @@ function injectStyles(path2, styleRegistry, t) {
|
|
|
1095
1237
|
}
|
|
1096
1238
|
const styleSheet = t.variableDeclaration("const", [
|
|
1097
1239
|
t.variableDeclarator(
|
|
1098
|
-
t.identifier(
|
|
1240
|
+
t.identifier(STYLES_IDENTIFIER),
|
|
1099
1241
|
t.callExpression(t.memberExpression(t.identifier("StyleSheet"), t.identifier("create")), [
|
|
1100
1242
|
t.objectExpression(styleProperties)
|
|
1101
1243
|
])
|
package/dist/babel/index.ts
CHANGED
|
@@ -22,6 +22,9 @@ type PluginState = PluginPass & {
|
|
|
22
22
|
customColors: Record<string, string>;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
// Use a unique identifier to avoid conflicts with user's own styles
|
|
26
|
+
const STYLES_IDENTIFIER = "_twStyles";
|
|
27
|
+
|
|
25
28
|
/**
|
|
26
29
|
* Supported className-like attributes
|
|
27
30
|
*/
|
|
@@ -52,6 +55,193 @@ function getTargetStyleProp(attributeName: string): string {
|
|
|
52
55
|
return "style";
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Result of processing a dynamic expression
|
|
60
|
+
*/
|
|
61
|
+
type DynamicExpressionResult = {
|
|
62
|
+
// The transformed expression to use in the style prop
|
|
63
|
+
expression: any;
|
|
64
|
+
// Static parts that can be parsed at compile time (if any)
|
|
65
|
+
staticParts?: string[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Process a dynamic className expression
|
|
70
|
+
* Extracts static strings and transforms the expression to use pre-compiled styles
|
|
71
|
+
*/
|
|
72
|
+
function processDynamicExpression(
|
|
73
|
+
expression: any,
|
|
74
|
+
state: PluginState,
|
|
75
|
+
t: typeof BabelTypes,
|
|
76
|
+
): DynamicExpressionResult | null {
|
|
77
|
+
// Handle template literals: `m-4 ${condition ? "p-4" : "p-2"}`
|
|
78
|
+
if (t.isTemplateLiteral(expression)) {
|
|
79
|
+
return processTemplateLiteral(expression, state, t);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle conditional expressions: condition ? "m-4" : "p-2"
|
|
83
|
+
if (t.isConditionalExpression(expression)) {
|
|
84
|
+
return processConditionalExpression(expression, state, t);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle logical expressions: condition && "m-4"
|
|
88
|
+
if (t.isLogicalExpression(expression)) {
|
|
89
|
+
return processLogicalExpression(expression, state, t);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Unsupported expression type
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Process template literal: `static ${dynamic} more-static`
|
|
98
|
+
*/
|
|
99
|
+
function processTemplateLiteral(
|
|
100
|
+
node: any,
|
|
101
|
+
state: PluginState,
|
|
102
|
+
t: typeof BabelTypes,
|
|
103
|
+
): DynamicExpressionResult | null {
|
|
104
|
+
const parts: any[] = [];
|
|
105
|
+
const staticParts: string[] = [];
|
|
106
|
+
|
|
107
|
+
// Process quasis (static parts) and expressions (dynamic parts)
|
|
108
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
109
|
+
const quasi = node.quasis[i];
|
|
110
|
+
const staticText = quasi.value.cooked?.trim();
|
|
111
|
+
|
|
112
|
+
// Add static part if not empty
|
|
113
|
+
if (staticText) {
|
|
114
|
+
// Parse static classes and add to registry
|
|
115
|
+
const classes = staticText.split(/\s+/).filter(Boolean);
|
|
116
|
+
for (const cls of classes) {
|
|
117
|
+
const styleObject = parseClassName(cls, state.customColors);
|
|
118
|
+
const styleKey = generateStyleKey(cls);
|
|
119
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
120
|
+
staticParts.push(cls);
|
|
121
|
+
|
|
122
|
+
// Add to parts array
|
|
123
|
+
parts.push(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Add dynamic expression if exists
|
|
128
|
+
if (i < node.expressions.length) {
|
|
129
|
+
const expr = node.expressions[i];
|
|
130
|
+
|
|
131
|
+
// Recursively process nested dynamic expressions
|
|
132
|
+
const result = processDynamicExpression(expr, state, t);
|
|
133
|
+
if (result) {
|
|
134
|
+
parts.push(result.expression);
|
|
135
|
+
} else {
|
|
136
|
+
// For unsupported expressions, keep them as-is
|
|
137
|
+
// This won't work at runtime but maintains the structure
|
|
138
|
+
parts.push(expr);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (parts.length === 0) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// If single part, return it directly; otherwise return array
|
|
148
|
+
const expression = parts.length === 1 ? parts[0] : t.arrayExpression(parts);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
expression,
|
|
152
|
+
staticParts: staticParts.length > 0 ? staticParts : undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Process conditional expression: condition ? "class-a" : "class-b"
|
|
158
|
+
*/
|
|
159
|
+
function processConditionalExpression(
|
|
160
|
+
node: any,
|
|
161
|
+
state: PluginState,
|
|
162
|
+
t: typeof BabelTypes,
|
|
163
|
+
): DynamicExpressionResult | null {
|
|
164
|
+
const consequent = processStringOrExpression(node.consequent, state, t);
|
|
165
|
+
const alternate = processStringOrExpression(node.alternate, state, t);
|
|
166
|
+
|
|
167
|
+
if (!consequent && !alternate) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Build conditional: condition ? consequentStyle : alternateStyle
|
|
172
|
+
const expression = t.conditionalExpression(
|
|
173
|
+
node.test,
|
|
174
|
+
consequent ?? t.nullLiteral(),
|
|
175
|
+
alternate ?? t.nullLiteral(),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return { expression };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Process logical expression: condition && "class-a"
|
|
183
|
+
*/
|
|
184
|
+
function processLogicalExpression(
|
|
185
|
+
node: any,
|
|
186
|
+
state: PluginState,
|
|
187
|
+
t: typeof BabelTypes,
|
|
188
|
+
): DynamicExpressionResult | null {
|
|
189
|
+
// Only handle AND (&&) expressions
|
|
190
|
+
if (node.operator !== "&&") {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const right = processStringOrExpression(node.right, state, t);
|
|
195
|
+
|
|
196
|
+
if (!right) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Build logical: condition && style
|
|
201
|
+
const expression = t.logicalExpression("&&", node.left, right);
|
|
202
|
+
|
|
203
|
+
return { expression };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Process a node that might be a string literal or another expression
|
|
208
|
+
*/
|
|
209
|
+
function processStringOrExpression(node: any, state: PluginState, t: typeof BabelTypes): any {
|
|
210
|
+
// Handle string literals
|
|
211
|
+
if (t.isStringLiteral(node)) {
|
|
212
|
+
const className = node.value.trim();
|
|
213
|
+
if (!className) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Parse and register styles
|
|
218
|
+
const styleObject = parseClassName(className, state.customColors);
|
|
219
|
+
const styleKey = generateStyleKey(className);
|
|
220
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
221
|
+
|
|
222
|
+
return t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Handle nested expressions recursively
|
|
226
|
+
if (t.isConditionalExpression(node)) {
|
|
227
|
+
const result = processConditionalExpression(node, state, t);
|
|
228
|
+
return result?.expression ?? null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (t.isLogicalExpression(node)) {
|
|
232
|
+
const result = processLogicalExpression(node, state, t);
|
|
233
|
+
return result?.expression ?? null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (t.isTemplateLiteral(node)) {
|
|
237
|
+
const result = processTemplateLiteral(node, state, t);
|
|
238
|
+
return result?.expression ?? null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Unsupported - return null
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
55
245
|
export default function reactNativeTailwindBabelPlugin({
|
|
56
246
|
types: t,
|
|
57
247
|
}: {
|
|
@@ -121,54 +311,94 @@ export default function reactNativeTailwindBabelPlugin({
|
|
|
121
311
|
|
|
122
312
|
const value = node.value;
|
|
123
313
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
);
|
|
314
|
+
// Determine target style prop based on attribute name
|
|
315
|
+
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
316
|
+
|
|
317
|
+
// Handle static string literals (original behavior)
|
|
318
|
+
if (t.isStringLiteral(value)) {
|
|
319
|
+
const className = value.value.trim();
|
|
320
|
+
|
|
321
|
+
// Skip empty classNames
|
|
322
|
+
if (!className) {
|
|
323
|
+
path.remove();
|
|
324
|
+
return;
|
|
134
325
|
}
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
326
|
|
|
138
|
-
|
|
327
|
+
state.hasClassNames = true;
|
|
139
328
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
path.remove();
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
329
|
+
// Parse className to React Native styles
|
|
330
|
+
const styleObject = parseClassName(className, state.customColors);
|
|
145
331
|
|
|
146
|
-
|
|
332
|
+
// Generate unique style key
|
|
333
|
+
const styleKey = generateStyleKey(className);
|
|
147
334
|
|
|
148
|
-
|
|
149
|
-
|
|
335
|
+
// Store in registry
|
|
336
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
150
337
|
|
|
151
|
-
|
|
152
|
-
|
|
338
|
+
// Check if there's already a style prop on this element
|
|
339
|
+
const parent = path.parent as any;
|
|
340
|
+
const styleAttribute = parent.attributes.find(
|
|
341
|
+
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
342
|
+
);
|
|
153
343
|
|
|
154
|
-
|
|
155
|
-
|
|
344
|
+
if (styleAttribute) {
|
|
345
|
+
// Merge with existing style prop
|
|
346
|
+
mergeStyleAttribute(path, styleAttribute, styleKey, t);
|
|
347
|
+
} else {
|
|
348
|
+
// Replace className with style prop
|
|
349
|
+
replaceWithStyleAttribute(path, styleKey, targetStyleProp, t);
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
156
353
|
|
|
157
|
-
//
|
|
158
|
-
|
|
354
|
+
// Handle dynamic expressions (JSXExpressionContainer)
|
|
355
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
356
|
+
const expression = value.expression;
|
|
357
|
+
|
|
358
|
+
// Skip JSXEmptyExpression
|
|
359
|
+
if (t.isJSXEmptyExpression(expression)) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
// Process dynamic expression
|
|
365
|
+
const result = processDynamicExpression(expression, state, t);
|
|
366
|
+
|
|
367
|
+
if (result) {
|
|
368
|
+
state.hasClassNames = true;
|
|
369
|
+
|
|
370
|
+
// Check if there's already a style prop on this element
|
|
371
|
+
const parent = path.parent as any;
|
|
372
|
+
const styleAttribute = parent.attributes.find(
|
|
373
|
+
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (styleAttribute) {
|
|
377
|
+
// Merge with existing style prop
|
|
378
|
+
mergeDynamicStyleAttribute(path, styleAttribute, result, t);
|
|
379
|
+
} else {
|
|
380
|
+
// Replace className with style prop
|
|
381
|
+
replaceDynamicWithStyleAttribute(path, result, targetStyleProp, t);
|
|
382
|
+
}
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
} catch (error) {
|
|
386
|
+
// Fall through to warning
|
|
387
|
+
if (process.env.NODE_ENV !== "production") {
|
|
388
|
+
console.warn(
|
|
389
|
+
`[react-native-tailwind] Failed to process dynamic ${attributeName} at ${state.file.opts.filename ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
159
394
|
|
|
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);
|
|
395
|
+
// Unsupported dynamic className - warn in development
|
|
396
|
+
if (process.env.NODE_ENV !== "production") {
|
|
397
|
+
const filename = state.file.opts.filename ?? "unknown";
|
|
398
|
+
console.warn(
|
|
399
|
+
`[react-native-tailwind] Dynamic ${attributeName} values are not fully supported at ${filename}. ` +
|
|
400
|
+
`Use the ${targetStyleProp} prop for dynamic values.`,
|
|
401
|
+
);
|
|
172
402
|
}
|
|
173
403
|
},
|
|
174
404
|
},
|
|
@@ -199,7 +429,7 @@ function replaceWithStyleAttribute(
|
|
|
199
429
|
) {
|
|
200
430
|
const styleAttribute = t.jsxAttribute(
|
|
201
431
|
t.jsxIdentifier(targetStyleProp),
|
|
202
|
-
t.jsxExpressionContainer(t.memberExpression(t.identifier(
|
|
432
|
+
t.jsxExpressionContainer(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey))),
|
|
203
433
|
);
|
|
204
434
|
|
|
205
435
|
classNamePath.replaceWith(styleAttribute);
|
|
@@ -219,7 +449,7 @@ function mergeStyleAttribute(
|
|
|
219
449
|
// Create array with className styles first, then existing styles
|
|
220
450
|
// This allows existing styles to override className styles
|
|
221
451
|
const styleArray = t.arrayExpression([
|
|
222
|
-
t.memberExpression(t.identifier(
|
|
452
|
+
t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)),
|
|
223
453
|
existingStyle,
|
|
224
454
|
]);
|
|
225
455
|
|
|
@@ -229,6 +459,51 @@ function mergeStyleAttribute(
|
|
|
229
459
|
classNamePath.remove();
|
|
230
460
|
}
|
|
231
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Replace className with dynamic style attribute
|
|
464
|
+
*/
|
|
465
|
+
function replaceDynamicWithStyleAttribute(
|
|
466
|
+
classNamePath: NodePath,
|
|
467
|
+
result: DynamicExpressionResult,
|
|
468
|
+
targetStyleProp: string,
|
|
469
|
+
t: typeof BabelTypes,
|
|
470
|
+
) {
|
|
471
|
+
const styleAttribute = t.jsxAttribute(
|
|
472
|
+
t.jsxIdentifier(targetStyleProp),
|
|
473
|
+
t.jsxExpressionContainer(result.expression),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
classNamePath.replaceWith(styleAttribute);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Merge dynamic className styles with existing style prop
|
|
481
|
+
*/
|
|
482
|
+
function mergeDynamicStyleAttribute(
|
|
483
|
+
classNamePath: NodePath,
|
|
484
|
+
styleAttribute: any,
|
|
485
|
+
result: DynamicExpressionResult,
|
|
486
|
+
t: typeof BabelTypes,
|
|
487
|
+
) {
|
|
488
|
+
const existingStyle = styleAttribute.value.expression;
|
|
489
|
+
|
|
490
|
+
// Merge dynamic expression with existing styles
|
|
491
|
+
// If existing is already an array, append to it; otherwise create new array
|
|
492
|
+
let styleArray;
|
|
493
|
+
if (t.isArrayExpression(existingStyle)) {
|
|
494
|
+
// Prepend dynamic styles to existing array
|
|
495
|
+
styleArray = t.arrayExpression([result.expression, ...existingStyle.elements]);
|
|
496
|
+
} else {
|
|
497
|
+
// Create new array with dynamic styles first, then existing
|
|
498
|
+
styleArray = t.arrayExpression([result.expression, existingStyle]);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
styleAttribute.value = t.jsxExpressionContainer(styleArray);
|
|
502
|
+
|
|
503
|
+
// Remove the className attribute
|
|
504
|
+
classNamePath.remove();
|
|
505
|
+
}
|
|
506
|
+
|
|
232
507
|
/**
|
|
233
508
|
* Inject StyleSheet.create with all collected styles
|
|
234
509
|
*/
|
|
@@ -259,10 +534,10 @@ function injectStyles(
|
|
|
259
534
|
styleProperties.push(t.objectProperty(t.identifier(key), t.objectExpression(properties)));
|
|
260
535
|
}
|
|
261
536
|
|
|
262
|
-
// Create: const
|
|
537
|
+
// Create: const _tailwindStyles = StyleSheet.create({ ... })
|
|
263
538
|
const styleSheet = t.variableDeclaration("const", [
|
|
264
539
|
t.variableDeclarator(
|
|
265
|
-
t.identifier(
|
|
540
|
+
t.identifier(STYLES_IDENTIFIER),
|
|
266
541
|
t.callExpression(t.memberExpression(t.identifier("StyleSheet"), t.identifier("create")), [
|
|
267
542
|
t.objectExpression(styleProperties),
|
|
268
543
|
]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgcrea/react-native-tailwind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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
|
@@ -22,6 +22,9 @@ type PluginState = PluginPass & {
|
|
|
22
22
|
customColors: Record<string, string>;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
// Use a unique identifier to avoid conflicts with user's own styles
|
|
26
|
+
const STYLES_IDENTIFIER = "_twStyles";
|
|
27
|
+
|
|
25
28
|
/**
|
|
26
29
|
* Supported className-like attributes
|
|
27
30
|
*/
|
|
@@ -52,6 +55,193 @@ function getTargetStyleProp(attributeName: string): string {
|
|
|
52
55
|
return "style";
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Result of processing a dynamic expression
|
|
60
|
+
*/
|
|
61
|
+
type DynamicExpressionResult = {
|
|
62
|
+
// The transformed expression to use in the style prop
|
|
63
|
+
expression: any;
|
|
64
|
+
// Static parts that can be parsed at compile time (if any)
|
|
65
|
+
staticParts?: string[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Process a dynamic className expression
|
|
70
|
+
* Extracts static strings and transforms the expression to use pre-compiled styles
|
|
71
|
+
*/
|
|
72
|
+
function processDynamicExpression(
|
|
73
|
+
expression: any,
|
|
74
|
+
state: PluginState,
|
|
75
|
+
t: typeof BabelTypes,
|
|
76
|
+
): DynamicExpressionResult | null {
|
|
77
|
+
// Handle template literals: `m-4 ${condition ? "p-4" : "p-2"}`
|
|
78
|
+
if (t.isTemplateLiteral(expression)) {
|
|
79
|
+
return processTemplateLiteral(expression, state, t);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle conditional expressions: condition ? "m-4" : "p-2"
|
|
83
|
+
if (t.isConditionalExpression(expression)) {
|
|
84
|
+
return processConditionalExpression(expression, state, t);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle logical expressions: condition && "m-4"
|
|
88
|
+
if (t.isLogicalExpression(expression)) {
|
|
89
|
+
return processLogicalExpression(expression, state, t);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Unsupported expression type
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Process template literal: `static ${dynamic} more-static`
|
|
98
|
+
*/
|
|
99
|
+
function processTemplateLiteral(
|
|
100
|
+
node: any,
|
|
101
|
+
state: PluginState,
|
|
102
|
+
t: typeof BabelTypes,
|
|
103
|
+
): DynamicExpressionResult | null {
|
|
104
|
+
const parts: any[] = [];
|
|
105
|
+
const staticParts: string[] = [];
|
|
106
|
+
|
|
107
|
+
// Process quasis (static parts) and expressions (dynamic parts)
|
|
108
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
109
|
+
const quasi = node.quasis[i];
|
|
110
|
+
const staticText = quasi.value.cooked?.trim();
|
|
111
|
+
|
|
112
|
+
// Add static part if not empty
|
|
113
|
+
if (staticText) {
|
|
114
|
+
// Parse static classes and add to registry
|
|
115
|
+
const classes = staticText.split(/\s+/).filter(Boolean);
|
|
116
|
+
for (const cls of classes) {
|
|
117
|
+
const styleObject = parseClassName(cls, state.customColors);
|
|
118
|
+
const styleKey = generateStyleKey(cls);
|
|
119
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
120
|
+
staticParts.push(cls);
|
|
121
|
+
|
|
122
|
+
// Add to parts array
|
|
123
|
+
parts.push(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Add dynamic expression if exists
|
|
128
|
+
if (i < node.expressions.length) {
|
|
129
|
+
const expr = node.expressions[i];
|
|
130
|
+
|
|
131
|
+
// Recursively process nested dynamic expressions
|
|
132
|
+
const result = processDynamicExpression(expr, state, t);
|
|
133
|
+
if (result) {
|
|
134
|
+
parts.push(result.expression);
|
|
135
|
+
} else {
|
|
136
|
+
// For unsupported expressions, keep them as-is
|
|
137
|
+
// This won't work at runtime but maintains the structure
|
|
138
|
+
parts.push(expr);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (parts.length === 0) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// If single part, return it directly; otherwise return array
|
|
148
|
+
const expression = parts.length === 1 ? parts[0] : t.arrayExpression(parts);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
expression,
|
|
152
|
+
staticParts: staticParts.length > 0 ? staticParts : undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Process conditional expression: condition ? "class-a" : "class-b"
|
|
158
|
+
*/
|
|
159
|
+
function processConditionalExpression(
|
|
160
|
+
node: any,
|
|
161
|
+
state: PluginState,
|
|
162
|
+
t: typeof BabelTypes,
|
|
163
|
+
): DynamicExpressionResult | null {
|
|
164
|
+
const consequent = processStringOrExpression(node.consequent, state, t);
|
|
165
|
+
const alternate = processStringOrExpression(node.alternate, state, t);
|
|
166
|
+
|
|
167
|
+
if (!consequent && !alternate) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Build conditional: condition ? consequentStyle : alternateStyle
|
|
172
|
+
const expression = t.conditionalExpression(
|
|
173
|
+
node.test,
|
|
174
|
+
consequent ?? t.nullLiteral(),
|
|
175
|
+
alternate ?? t.nullLiteral(),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return { expression };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Process logical expression: condition && "class-a"
|
|
183
|
+
*/
|
|
184
|
+
function processLogicalExpression(
|
|
185
|
+
node: any,
|
|
186
|
+
state: PluginState,
|
|
187
|
+
t: typeof BabelTypes,
|
|
188
|
+
): DynamicExpressionResult | null {
|
|
189
|
+
// Only handle AND (&&) expressions
|
|
190
|
+
if (node.operator !== "&&") {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const right = processStringOrExpression(node.right, state, t);
|
|
195
|
+
|
|
196
|
+
if (!right) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Build logical: condition && style
|
|
201
|
+
const expression = t.logicalExpression("&&", node.left, right);
|
|
202
|
+
|
|
203
|
+
return { expression };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Process a node that might be a string literal or another expression
|
|
208
|
+
*/
|
|
209
|
+
function processStringOrExpression(node: any, state: PluginState, t: typeof BabelTypes): any {
|
|
210
|
+
// Handle string literals
|
|
211
|
+
if (t.isStringLiteral(node)) {
|
|
212
|
+
const className = node.value.trim();
|
|
213
|
+
if (!className) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Parse and register styles
|
|
218
|
+
const styleObject = parseClassName(className, state.customColors);
|
|
219
|
+
const styleKey = generateStyleKey(className);
|
|
220
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
221
|
+
|
|
222
|
+
return t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Handle nested expressions recursively
|
|
226
|
+
if (t.isConditionalExpression(node)) {
|
|
227
|
+
const result = processConditionalExpression(node, state, t);
|
|
228
|
+
return result?.expression ?? null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (t.isLogicalExpression(node)) {
|
|
232
|
+
const result = processLogicalExpression(node, state, t);
|
|
233
|
+
return result?.expression ?? null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (t.isTemplateLiteral(node)) {
|
|
237
|
+
const result = processTemplateLiteral(node, state, t);
|
|
238
|
+
return result?.expression ?? null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Unsupported - return null
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
55
245
|
export default function reactNativeTailwindBabelPlugin({
|
|
56
246
|
types: t,
|
|
57
247
|
}: {
|
|
@@ -121,54 +311,94 @@ export default function reactNativeTailwindBabelPlugin({
|
|
|
121
311
|
|
|
122
312
|
const value = node.value;
|
|
123
313
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
);
|
|
314
|
+
// Determine target style prop based on attribute name
|
|
315
|
+
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
316
|
+
|
|
317
|
+
// Handle static string literals (original behavior)
|
|
318
|
+
if (t.isStringLiteral(value)) {
|
|
319
|
+
const className = value.value.trim();
|
|
320
|
+
|
|
321
|
+
// Skip empty classNames
|
|
322
|
+
if (!className) {
|
|
323
|
+
path.remove();
|
|
324
|
+
return;
|
|
134
325
|
}
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
326
|
|
|
138
|
-
|
|
327
|
+
state.hasClassNames = true;
|
|
139
328
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
path.remove();
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
329
|
+
// Parse className to React Native styles
|
|
330
|
+
const styleObject = parseClassName(className, state.customColors);
|
|
145
331
|
|
|
146
|
-
|
|
332
|
+
// Generate unique style key
|
|
333
|
+
const styleKey = generateStyleKey(className);
|
|
147
334
|
|
|
148
|
-
|
|
149
|
-
|
|
335
|
+
// Store in registry
|
|
336
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
150
337
|
|
|
151
|
-
|
|
152
|
-
|
|
338
|
+
// Check if there's already a style prop on this element
|
|
339
|
+
const parent = path.parent as any;
|
|
340
|
+
const styleAttribute = parent.attributes.find(
|
|
341
|
+
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
342
|
+
);
|
|
153
343
|
|
|
154
|
-
|
|
155
|
-
|
|
344
|
+
if (styleAttribute) {
|
|
345
|
+
// Merge with existing style prop
|
|
346
|
+
mergeStyleAttribute(path, styleAttribute, styleKey, t);
|
|
347
|
+
} else {
|
|
348
|
+
// Replace className with style prop
|
|
349
|
+
replaceWithStyleAttribute(path, styleKey, targetStyleProp, t);
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
156
353
|
|
|
157
|
-
//
|
|
158
|
-
|
|
354
|
+
// Handle dynamic expressions (JSXExpressionContainer)
|
|
355
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
356
|
+
const expression = value.expression;
|
|
357
|
+
|
|
358
|
+
// Skip JSXEmptyExpression
|
|
359
|
+
if (t.isJSXEmptyExpression(expression)) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
// Process dynamic expression
|
|
365
|
+
const result = processDynamicExpression(expression, state, t);
|
|
366
|
+
|
|
367
|
+
if (result) {
|
|
368
|
+
state.hasClassNames = true;
|
|
369
|
+
|
|
370
|
+
// Check if there's already a style prop on this element
|
|
371
|
+
const parent = path.parent as any;
|
|
372
|
+
const styleAttribute = parent.attributes.find(
|
|
373
|
+
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (styleAttribute) {
|
|
377
|
+
// Merge with existing style prop
|
|
378
|
+
mergeDynamicStyleAttribute(path, styleAttribute, result, t);
|
|
379
|
+
} else {
|
|
380
|
+
// Replace className with style prop
|
|
381
|
+
replaceDynamicWithStyleAttribute(path, result, targetStyleProp, t);
|
|
382
|
+
}
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
} catch (error) {
|
|
386
|
+
// Fall through to warning
|
|
387
|
+
if (process.env.NODE_ENV !== "production") {
|
|
388
|
+
console.warn(
|
|
389
|
+
`[react-native-tailwind] Failed to process dynamic ${attributeName} at ${state.file.opts.filename ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
159
394
|
|
|
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);
|
|
395
|
+
// Unsupported dynamic className - warn in development
|
|
396
|
+
if (process.env.NODE_ENV !== "production") {
|
|
397
|
+
const filename = state.file.opts.filename ?? "unknown";
|
|
398
|
+
console.warn(
|
|
399
|
+
`[react-native-tailwind] Dynamic ${attributeName} values are not fully supported at ${filename}. ` +
|
|
400
|
+
`Use the ${targetStyleProp} prop for dynamic values.`,
|
|
401
|
+
);
|
|
172
402
|
}
|
|
173
403
|
},
|
|
174
404
|
},
|
|
@@ -199,7 +429,7 @@ function replaceWithStyleAttribute(
|
|
|
199
429
|
) {
|
|
200
430
|
const styleAttribute = t.jsxAttribute(
|
|
201
431
|
t.jsxIdentifier(targetStyleProp),
|
|
202
|
-
t.jsxExpressionContainer(t.memberExpression(t.identifier(
|
|
432
|
+
t.jsxExpressionContainer(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey))),
|
|
203
433
|
);
|
|
204
434
|
|
|
205
435
|
classNamePath.replaceWith(styleAttribute);
|
|
@@ -219,7 +449,7 @@ function mergeStyleAttribute(
|
|
|
219
449
|
// Create array with className styles first, then existing styles
|
|
220
450
|
// This allows existing styles to override className styles
|
|
221
451
|
const styleArray = t.arrayExpression([
|
|
222
|
-
t.memberExpression(t.identifier(
|
|
452
|
+
t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)),
|
|
223
453
|
existingStyle,
|
|
224
454
|
]);
|
|
225
455
|
|
|
@@ -229,6 +459,51 @@ function mergeStyleAttribute(
|
|
|
229
459
|
classNamePath.remove();
|
|
230
460
|
}
|
|
231
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Replace className with dynamic style attribute
|
|
464
|
+
*/
|
|
465
|
+
function replaceDynamicWithStyleAttribute(
|
|
466
|
+
classNamePath: NodePath,
|
|
467
|
+
result: DynamicExpressionResult,
|
|
468
|
+
targetStyleProp: string,
|
|
469
|
+
t: typeof BabelTypes,
|
|
470
|
+
) {
|
|
471
|
+
const styleAttribute = t.jsxAttribute(
|
|
472
|
+
t.jsxIdentifier(targetStyleProp),
|
|
473
|
+
t.jsxExpressionContainer(result.expression),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
classNamePath.replaceWith(styleAttribute);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Merge dynamic className styles with existing style prop
|
|
481
|
+
*/
|
|
482
|
+
function mergeDynamicStyleAttribute(
|
|
483
|
+
classNamePath: NodePath,
|
|
484
|
+
styleAttribute: any,
|
|
485
|
+
result: DynamicExpressionResult,
|
|
486
|
+
t: typeof BabelTypes,
|
|
487
|
+
) {
|
|
488
|
+
const existingStyle = styleAttribute.value.expression;
|
|
489
|
+
|
|
490
|
+
// Merge dynamic expression with existing styles
|
|
491
|
+
// If existing is already an array, append to it; otherwise create new array
|
|
492
|
+
let styleArray;
|
|
493
|
+
if (t.isArrayExpression(existingStyle)) {
|
|
494
|
+
// Prepend dynamic styles to existing array
|
|
495
|
+
styleArray = t.arrayExpression([result.expression, ...existingStyle.elements]);
|
|
496
|
+
} else {
|
|
497
|
+
// Create new array with dynamic styles first, then existing
|
|
498
|
+
styleArray = t.arrayExpression([result.expression, existingStyle]);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
styleAttribute.value = t.jsxExpressionContainer(styleArray);
|
|
502
|
+
|
|
503
|
+
// Remove the className attribute
|
|
504
|
+
classNamePath.remove();
|
|
505
|
+
}
|
|
506
|
+
|
|
232
507
|
/**
|
|
233
508
|
* Inject StyleSheet.create with all collected styles
|
|
234
509
|
*/
|
|
@@ -259,10 +534,10 @@ function injectStyles(
|
|
|
259
534
|
styleProperties.push(t.objectProperty(t.identifier(key), t.objectExpression(properties)));
|
|
260
535
|
}
|
|
261
536
|
|
|
262
|
-
// Create: const
|
|
537
|
+
// Create: const _tailwindStyles = StyleSheet.create({ ... })
|
|
263
538
|
const styleSheet = t.variableDeclaration("const", [
|
|
264
539
|
t.variableDeclarator(
|
|
265
|
-
t.identifier(
|
|
540
|
+
t.identifier(STYLES_IDENTIFIER),
|
|
266
541
|
t.callExpression(t.memberExpression(t.identifier("StyleSheet"), t.identifier("create")), [
|
|
267
542
|
t.objectExpression(styleProperties),
|
|
268
543
|
]),
|