@mgcrea/react-native-tailwind 0.8.1 → 0.9.1
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 +152 -0
- package/dist/babel/config-loader.ts +2 -0
- package/dist/babel/index.cjs +205 -17
- package/dist/babel/plugin.d.ts +4 -1
- package/dist/babel/plugin.test.ts +327 -0
- package/dist/babel/plugin.ts +194 -14
- package/dist/babel/utils/platformModifierProcessing.d.ts +30 -0
- package/dist/babel/utils/platformModifierProcessing.ts +80 -0
- package/dist/babel/utils/styleInjection.d.ts +5 -1
- package/dist/babel/utils/styleInjection.ts +52 -7
- package/dist/babel/utils/styleTransforms.ts +1 -0
- package/dist/parser/aspectRatio.js +1 -1
- package/dist/parser/aspectRatio.test.js +1 -1
- package/dist/parser/index.d.ts +2 -2
- package/dist/parser/index.js +1 -1
- package/dist/parser/modifiers.d.ts +20 -2
- package/dist/parser/modifiers.js +1 -1
- package/dist/parser/spacing.d.ts +1 -1
- package/dist/parser/spacing.js +1 -1
- package/dist/parser/spacing.test.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +4 -4
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +4 -4
- package/dist/runtime.test.js +1 -1
- package/dist/stubs/tw.test.js +1 -0
- package/package.json +7 -7
- package/src/babel/config-loader.ts +2 -0
- package/src/babel/plugin.test.ts +327 -0
- package/src/babel/plugin.ts +194 -14
- package/src/babel/utils/platformModifierProcessing.ts +80 -0
- package/src/babel/utils/styleInjection.ts +52 -7
- package/src/babel/utils/styleTransforms.ts +1 -0
- package/src/parser/aspectRatio.test.ts +25 -2
- package/src/parser/aspectRatio.ts +4 -3
- package/src/parser/borders.ts +2 -0
- package/src/parser/colors.ts +2 -0
- package/src/parser/index.ts +9 -2
- package/src/parser/layout.ts +2 -0
- package/src/parser/modifiers.ts +38 -4
- package/src/parser/placeholder.ts +1 -0
- package/src/parser/sizing.ts +1 -0
- package/src/parser/spacing.test.ts +63 -0
- package/src/parser/spacing.ts +11 -6
- package/src/parser/transforms.ts +5 -0
- package/src/parser/typography.ts +2 -0
- package/src/runtime.test.ts +27 -0
- package/src/runtime.ts +2 -1
- package/src/stubs/tw.test.ts +27 -0
- package/dist/babel/index.test.ts +0 -481
- package/dist/config/palettes.d.ts +0 -302
- package/dist/config/palettes.js +0 -1
- package/dist/parser/__snapshots__/aspectRatio.test.js.snap +0 -9
- package/dist/parser/__snapshots__/borders.test.js.snap +0 -23
- package/dist/parser/__snapshots__/colors.test.js.snap +0 -251
- package/dist/parser/__snapshots__/shadows.test.js.snap +0 -76
- package/dist/parser/__snapshots__/sizing.test.js.snap +0 -61
- package/dist/parser/__snapshots__/spacing.test.js.snap +0 -40
- package/dist/parser/__snapshots__/transforms.test.js.snap +0 -58
- package/dist/parser/__snapshots__/typography.test.js.snap +0 -30
- package/dist/parser/aspectRatio.test.d.ts +0 -1
- package/dist/parser/borders.test.d.ts +0 -1
- package/dist/parser/colors.test.d.ts +0 -1
- package/dist/parser/layout.test.d.ts +0 -1
- package/dist/parser/modifiers.test.d.ts +0 -1
- package/dist/parser/shadows.test.d.ts +0 -1
- package/dist/parser/sizing.test.d.ts +0 -1
- package/dist/parser/spacing.test.d.ts +0 -1
- package/dist/parser/typography.test.d.ts +0 -1
- package/dist/types.d.ts +0 -42
- package/dist/types.js +0 -1
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@ Compile-time Tailwind CSS for React Native with zero runtime overhead. Transform
|
|
|
45
45
|
- 🔀 **Dynamic className** — Conditional styles with hybrid compile-time optimization
|
|
46
46
|
- 🏃 **Runtime option** — Optional `tw` template tag for fully dynamic styling (~25KB)
|
|
47
47
|
- 🎯 **State modifiers** — `active:`, `hover:`, `focus:`, and `disabled:` modifiers for interactive components
|
|
48
|
+
- 📱 **Platform modifiers** — `ios:`, `android:`, and `web:` modifiers for platform-specific styling
|
|
48
49
|
- 📜 **Special style props** — Support for `contentContainerClassName`, `columnWrapperClassName`, and more
|
|
49
50
|
- 🎛️ **Custom attributes** — Configure which props to transform with exact matching or glob patterns
|
|
50
51
|
|
|
@@ -709,6 +710,157 @@ The enhanced `TextInput` also provides a convenient `disabled` prop that overrid
|
|
|
709
710
|
- ✅ **Type-safe** — Full TypeScript autocomplete for all modifiers
|
|
710
711
|
- ✅ **Works with custom colors** — `focus:border-primary`, `active:bg-secondary`, `disabled:bg-gray-200`, etc.
|
|
711
712
|
|
|
713
|
+
### Platform Modifiers
|
|
714
|
+
|
|
715
|
+
Apply platform-specific styles using `ios:`, `android:`, and `web:` modifiers. These work on **all components** (not just enhanced ones) and compile to `Platform.select()` calls with zero runtime parsing overhead.
|
|
716
|
+
|
|
717
|
+
**Basic Example:**
|
|
718
|
+
|
|
719
|
+
```tsx
|
|
720
|
+
import { View, Text } from "react-native";
|
|
721
|
+
|
|
722
|
+
export function PlatformCard() {
|
|
723
|
+
return (
|
|
724
|
+
<View className="p-4 ios:p-6 android:p-8 bg-white rounded-lg">
|
|
725
|
+
<Text className="text-base ios:text-blue-600 android:text-green-600">
|
|
726
|
+
Platform-specific styles
|
|
727
|
+
</Text>
|
|
728
|
+
</View>
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
**Transforms to:**
|
|
734
|
+
|
|
735
|
+
```tsx
|
|
736
|
+
import { Platform, StyleSheet } from "react-native";
|
|
737
|
+
|
|
738
|
+
<View
|
|
739
|
+
style={[
|
|
740
|
+
_twStyles._bg_white_p_4_rounded_lg,
|
|
741
|
+
Platform.select({
|
|
742
|
+
ios: _twStyles._ios_p_6,
|
|
743
|
+
android: _twStyles._android_p_8,
|
|
744
|
+
}),
|
|
745
|
+
]}
|
|
746
|
+
>
|
|
747
|
+
<Text
|
|
748
|
+
style={[
|
|
749
|
+
_twStyles._text_base,
|
|
750
|
+
Platform.select({
|
|
751
|
+
ios: _twStyles._ios_text_blue_600,
|
|
752
|
+
android: _twStyles._android_text_green_600,
|
|
753
|
+
}),
|
|
754
|
+
]}
|
|
755
|
+
>
|
|
756
|
+
Platform-specific styles
|
|
757
|
+
</Text>
|
|
758
|
+
</View>;
|
|
759
|
+
|
|
760
|
+
// Generated styles:
|
|
761
|
+
const _twStyles = StyleSheet.create({
|
|
762
|
+
_bg_white_p_4_rounded_lg: {
|
|
763
|
+
backgroundColor: "#FFFFFF",
|
|
764
|
+
padding: 16,
|
|
765
|
+
borderRadius: 8,
|
|
766
|
+
},
|
|
767
|
+
_ios_p_6: { padding: 24 },
|
|
768
|
+
_android_p_8: { padding: 32 },
|
|
769
|
+
_text_base: { fontSize: 16 },
|
|
770
|
+
_ios_text_blue_600: { color: "#2563EB" },
|
|
771
|
+
_android_text_green_600: { color: "#059669" },
|
|
772
|
+
});
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
**Common Use Cases:**
|
|
776
|
+
|
|
777
|
+
**Platform-specific colors:**
|
|
778
|
+
|
|
779
|
+
```tsx
|
|
780
|
+
// Different colors per platform for brand consistency
|
|
781
|
+
<View className="bg-blue-500 ios:bg-blue-600 android:bg-green-600">
|
|
782
|
+
<Text className="text-white">Platform-specific background</Text>
|
|
783
|
+
</View>
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
**Platform-specific spacing:**
|
|
787
|
+
|
|
788
|
+
```tsx
|
|
789
|
+
// More padding on Android due to larger default touch targets
|
|
790
|
+
<View className="p-4 ios:p-6 android:p-8">
|
|
791
|
+
<Text>Platform-specific padding</Text>
|
|
792
|
+
</View>
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
**Combined with base styles:**
|
|
796
|
+
|
|
797
|
+
```tsx
|
|
798
|
+
// Base styles + platform-specific overrides
|
|
799
|
+
<View className="border-2 border-gray-300 ios:border-blue-500 android:border-green-500 rounded-lg p-4">
|
|
800
|
+
<Text className="text-gray-800 ios:text-blue-800 android:text-green-800">
|
|
801
|
+
Base styles with platform overrides
|
|
802
|
+
</Text>
|
|
803
|
+
</View>
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
**Multiple platform modifiers:**
|
|
807
|
+
|
|
808
|
+
```tsx
|
|
809
|
+
// Combine multiple platform-specific styles
|
|
810
|
+
<View className="bg-gray-100 ios:bg-blue-50 android:bg-green-50 p-4 ios:p-6 android:p-8 rounded-lg">
|
|
811
|
+
<Text>Multiple platform styles</Text>
|
|
812
|
+
</View>
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
**Web platform support:**
|
|
816
|
+
|
|
817
|
+
```tsx
|
|
818
|
+
// Different styles for React Native Web
|
|
819
|
+
<View className="p-4 ios:p-6 android:p-8 web:p-2">
|
|
820
|
+
<Text className="text-base web:text-lg">Cross-platform styling</Text>
|
|
821
|
+
</View>
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
**Mixing with state modifiers:**
|
|
825
|
+
|
|
826
|
+
```tsx
|
|
827
|
+
import { Pressable } from "@mgcrea/react-native-tailwind";
|
|
828
|
+
|
|
829
|
+
// Platform modifiers work alongside state modifiers
|
|
830
|
+
<Pressable className="bg-blue-500 active:bg-blue-700 ios:border-2 android:border-0 p-4 rounded-lg">
|
|
831
|
+
<Text className="text-white">Button with platform + state modifiers</Text>
|
|
832
|
+
</Pressable>;
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
**Key Features:**
|
|
836
|
+
|
|
837
|
+
- ✅ **Works on all components** — No need for enhanced components (unlike state modifiers)
|
|
838
|
+
- ✅ **Zero runtime overhead** — All parsing happens at compile-time
|
|
839
|
+
- ✅ **Native Platform API** — Uses React Native's `Platform.select()` under the hood
|
|
840
|
+
- ✅ **Type-safe** — Full TypeScript autocomplete for platform modifiers
|
|
841
|
+
- ✅ **Optimized** — Styles deduplicated via `StyleSheet.create`
|
|
842
|
+
- ✅ **Works with custom colors** — `ios:bg-primary`, `android:bg-secondary`, etc.
|
|
843
|
+
- ✅ **Minimal runtime cost** — Only one `Platform.select()` call per element with platform modifiers
|
|
844
|
+
|
|
845
|
+
**Supported Platforms:**
|
|
846
|
+
|
|
847
|
+
| Modifier | Platform | Description |
|
|
848
|
+
| -------- | -------------- | ----------------------------- |
|
|
849
|
+
| `ios:` | iOS | Styles specific to iOS |
|
|
850
|
+
| `android:` | Android | Styles specific to Android |
|
|
851
|
+
| `web:` | React Native Web | Styles for web platform |
|
|
852
|
+
|
|
853
|
+
**How it works:**
|
|
854
|
+
|
|
855
|
+
The Babel plugin:
|
|
856
|
+
1. Detects platform modifiers during compilation
|
|
857
|
+
2. Parses all platform-specific classes at compile-time
|
|
858
|
+
3. Generates `Platform.select()` expressions with references to pre-compiled styles
|
|
859
|
+
4. Auto-imports `Platform` from `react-native` when needed
|
|
860
|
+
5. Merges platform styles with base classes and other modifiers in style arrays
|
|
861
|
+
|
|
862
|
+
This approach provides the best of both worlds: compile-time optimization for all styles, with minimal runtime platform detection only for the conditional selection logic.
|
|
863
|
+
|
|
712
864
|
### ScrollView Content Container
|
|
713
865
|
|
|
714
866
|
Use `contentContainerClassName` to style the ScrollView's content container:
|
|
@@ -72,6 +72,7 @@ export function loadTailwindConfig(configPath: string): TailwindConfig | null {
|
|
|
72
72
|
configCache.set(configPath, resolved);
|
|
73
73
|
return resolved;
|
|
74
74
|
} catch (error) {
|
|
75
|
+
/* v8 ignore next 3 */
|
|
75
76
|
if (process.env.NODE_ENV !== "production") {
|
|
76
77
|
console.warn(`[react-native-tailwind] Failed to load config from ${configPath}:`, error);
|
|
77
78
|
}
|
|
@@ -98,6 +99,7 @@ export function extractCustomColors(filename: string): Record<string, string> {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
// Warn if using theme.colors instead of theme.extend.colors
|
|
102
|
+
/* v8 ignore next 5 */
|
|
101
103
|
if (config.theme.colors && !config.theme.extend?.colors && process.env.NODE_ENV !== "production") {
|
|
102
104
|
console.warn(
|
|
103
105
|
"[react-native-tailwind] Using theme.colors will override all default colors. " +
|
package/dist/babel/index.cjs
CHANGED
|
@@ -65,7 +65,7 @@ function parseAspectRatio(cls) {
|
|
|
65
65
|
if (cls in ASPECT_RATIO_PRESETS) {
|
|
66
66
|
const aspectRatio2 = ASPECT_RATIO_PRESETS[cls];
|
|
67
67
|
if (aspectRatio2 === void 0) {
|
|
68
|
-
return {};
|
|
68
|
+
return { aspectRatio: void 0 };
|
|
69
69
|
}
|
|
70
70
|
return { aspectRatio: aspectRatio2 };
|
|
71
71
|
}
|
|
@@ -1209,16 +1209,19 @@ function parseArbitrarySpacing(value) {
|
|
|
1209
1209
|
return null;
|
|
1210
1210
|
}
|
|
1211
1211
|
function parseSpacing(cls) {
|
|
1212
|
-
const marginMatch = cls.match(/^m([xytrbls]?)-(.+)$/);
|
|
1212
|
+
const marginMatch = cls.match(/^(-?)m([xytrbls]?)-(.+)$/);
|
|
1213
1213
|
if (marginMatch) {
|
|
1214
|
-
const [, dir, valueStr] = marginMatch;
|
|
1214
|
+
const [, negativePrefix, dir, valueStr] = marginMatch;
|
|
1215
|
+
const isNegative = negativePrefix === "-";
|
|
1215
1216
|
const arbitraryValue = parseArbitrarySpacing(valueStr);
|
|
1216
1217
|
if (arbitraryValue !== null) {
|
|
1217
|
-
|
|
1218
|
+
const finalValue = isNegative ? -arbitraryValue : arbitraryValue;
|
|
1219
|
+
return getMarginStyle(dir, finalValue);
|
|
1218
1220
|
}
|
|
1219
1221
|
const scaleValue = SPACING_SCALE[valueStr];
|
|
1220
1222
|
if (scaleValue !== void 0) {
|
|
1221
|
-
|
|
1223
|
+
const finalValue = isNegative ? -scaleValue : scaleValue;
|
|
1224
|
+
return getMarginStyle(dir, finalValue);
|
|
1222
1225
|
}
|
|
1223
1226
|
}
|
|
1224
1227
|
const paddingMatch = cls.match(/^p([xytrbls]?)-(.+)$/);
|
|
@@ -1730,13 +1733,15 @@ function parsePlaceholderClasses(classes, customColors) {
|
|
|
1730
1733
|
}
|
|
1731
1734
|
|
|
1732
1735
|
// src/parser/modifiers.ts
|
|
1733
|
-
var
|
|
1736
|
+
var STATE_MODIFIERS = [
|
|
1734
1737
|
"active",
|
|
1735
1738
|
"hover",
|
|
1736
1739
|
"focus",
|
|
1737
1740
|
"disabled",
|
|
1738
1741
|
"placeholder"
|
|
1739
1742
|
];
|
|
1743
|
+
var PLATFORM_MODIFIERS = ["ios", "android", "web"];
|
|
1744
|
+
var SUPPORTED_MODIFIERS = [...STATE_MODIFIERS, ...PLATFORM_MODIFIERS];
|
|
1740
1745
|
function parseModifier(cls) {
|
|
1741
1746
|
const colonIndex = cls.indexOf(":");
|
|
1742
1747
|
if (colonIndex === -1) {
|
|
@@ -1758,6 +1763,12 @@ function parseModifier(cls) {
|
|
|
1758
1763
|
baseClass
|
|
1759
1764
|
};
|
|
1760
1765
|
}
|
|
1766
|
+
function isStateModifier(modifier) {
|
|
1767
|
+
return STATE_MODIFIERS.includes(modifier);
|
|
1768
|
+
}
|
|
1769
|
+
function isPlatformModifier(modifier) {
|
|
1770
|
+
return PLATFORM_MODIFIERS.includes(modifier);
|
|
1771
|
+
}
|
|
1761
1772
|
function splitModifierClasses(className) {
|
|
1762
1773
|
const classes = className.trim().split(/\s+/).filter(Boolean);
|
|
1763
1774
|
const baseClasses = [];
|
|
@@ -2118,13 +2129,74 @@ function createStyleFunction(styleExpression, modifierTypes, t) {
|
|
|
2118
2129
|
return t.arrowFunctionExpression([param], styleExpression);
|
|
2119
2130
|
}
|
|
2120
2131
|
|
|
2132
|
+
// src/babel/utils/platformModifierProcessing.ts
|
|
2133
|
+
function processPlatformModifiers(platformModifiers, state, parseClassName2, generateStyleKey2, t) {
|
|
2134
|
+
state.needsPlatformImport = true;
|
|
2135
|
+
const modifiersByPlatform = /* @__PURE__ */ new Map();
|
|
2136
|
+
for (const mod of platformModifiers) {
|
|
2137
|
+
const platform = mod.modifier;
|
|
2138
|
+
if (!modifiersByPlatform.has(platform)) {
|
|
2139
|
+
modifiersByPlatform.set(platform, []);
|
|
2140
|
+
}
|
|
2141
|
+
const platformGroup = modifiersByPlatform.get(platform);
|
|
2142
|
+
if (platformGroup) {
|
|
2143
|
+
platformGroup.push(mod);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
const selectProperties = [];
|
|
2147
|
+
for (const [platform, modifiers] of modifiersByPlatform) {
|
|
2148
|
+
const classNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
2149
|
+
const styleObject = parseClassName2(classNames, state.customColors);
|
|
2150
|
+
const styleKey = generateStyleKey2(`${platform}_${classNames}`);
|
|
2151
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
2152
|
+
const styleReference = t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey));
|
|
2153
|
+
selectProperties.push(t.objectProperty(t.identifier(platform), styleReference));
|
|
2154
|
+
}
|
|
2155
|
+
return t.callExpression(t.memberExpression(t.identifier("Platform"), t.identifier("select")), [
|
|
2156
|
+
t.objectExpression(selectProperties)
|
|
2157
|
+
]);
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2121
2160
|
// src/babel/utils/styleInjection.ts
|
|
2122
2161
|
function addStyleSheetImport(path2, t) {
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2162
|
+
const body = path2.node.body;
|
|
2163
|
+
let reactNativeImport = null;
|
|
2164
|
+
for (const statement of body) {
|
|
2165
|
+
if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
|
|
2166
|
+
reactNativeImport = statement;
|
|
2167
|
+
break;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
if (reactNativeImport) {
|
|
2171
|
+
reactNativeImport.specifiers.push(
|
|
2172
|
+
t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet"))
|
|
2173
|
+
);
|
|
2174
|
+
} else {
|
|
2175
|
+
const importDeclaration = t.importDeclaration(
|
|
2176
|
+
[t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet"))],
|
|
2177
|
+
t.stringLiteral("react-native")
|
|
2178
|
+
);
|
|
2179
|
+
path2.unshiftContainer("body", importDeclaration);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
function addPlatformImport(path2, t) {
|
|
2183
|
+
const body = path2.node.body;
|
|
2184
|
+
let reactNativeImport = null;
|
|
2185
|
+
for (const statement of body) {
|
|
2186
|
+
if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
|
|
2187
|
+
reactNativeImport = statement;
|
|
2188
|
+
break;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
if (reactNativeImport) {
|
|
2192
|
+
reactNativeImport.specifiers.push(t.importSpecifier(t.identifier("Platform"), t.identifier("Platform")));
|
|
2193
|
+
} else {
|
|
2194
|
+
const importDeclaration = t.importDeclaration(
|
|
2195
|
+
[t.importSpecifier(t.identifier("Platform"), t.identifier("Platform"))],
|
|
2196
|
+
t.stringLiteral("react-native")
|
|
2197
|
+
);
|
|
2198
|
+
path2.unshiftContainer("body", importDeclaration);
|
|
2199
|
+
}
|
|
2128
2200
|
}
|
|
2129
2201
|
function injectStylesAtTop(path2, styleRegistry, stylesIdentifier, t) {
|
|
2130
2202
|
const styleProperties = [];
|
|
@@ -2355,6 +2427,8 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2355
2427
|
state.styleRegistry = /* @__PURE__ */ new Map();
|
|
2356
2428
|
state.hasClassNames = false;
|
|
2357
2429
|
state.hasStyleSheetImport = false;
|
|
2430
|
+
state.hasPlatformImport = false;
|
|
2431
|
+
state.needsPlatformImport = false;
|
|
2358
2432
|
state.supportedAttributes = exactMatches;
|
|
2359
2433
|
state.attributePatterns = patterns;
|
|
2360
2434
|
state.stylesIdentifier = stylesIdentifier;
|
|
@@ -2372,10 +2446,13 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2372
2446
|
if (!state.hasStyleSheetImport) {
|
|
2373
2447
|
addStyleSheetImport(path2, t);
|
|
2374
2448
|
}
|
|
2449
|
+
if (state.needsPlatformImport && !state.hasPlatformImport) {
|
|
2450
|
+
addPlatformImport(path2, t);
|
|
2451
|
+
}
|
|
2375
2452
|
injectStylesAtTop(path2, state.styleRegistry, state.stylesIdentifier, t);
|
|
2376
2453
|
}
|
|
2377
2454
|
},
|
|
2378
|
-
// Check if StyleSheet
|
|
2455
|
+
// Check if StyleSheet/Platform are already imported and track tw/twStyle imports
|
|
2379
2456
|
ImportDeclaration(path2, state) {
|
|
2380
2457
|
const node = path2.node;
|
|
2381
2458
|
if (node.source.value === "react-native") {
|
|
@@ -2386,12 +2463,19 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2386
2463
|
}
|
|
2387
2464
|
return false;
|
|
2388
2465
|
});
|
|
2466
|
+
const hasPlatform = specifiers.some((spec) => {
|
|
2467
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
2468
|
+
return spec.imported.name === "Platform";
|
|
2469
|
+
}
|
|
2470
|
+
return false;
|
|
2471
|
+
});
|
|
2389
2472
|
if (hasStyleSheet) {
|
|
2390
2473
|
state.hasStyleSheetImport = true;
|
|
2391
|
-
} else {
|
|
2392
|
-
node.specifiers.push(t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet")));
|
|
2393
|
-
state.hasStyleSheetImport = true;
|
|
2394
2474
|
}
|
|
2475
|
+
if (hasPlatform) {
|
|
2476
|
+
state.hasPlatformImport = true;
|
|
2477
|
+
}
|
|
2478
|
+
state.reactNativeImportPath = path2;
|
|
2395
2479
|
}
|
|
2396
2480
|
if (node.source.value === "@mgcrea/react-native-tailwind") {
|
|
2397
2481
|
const specifiers = node.specifiers;
|
|
@@ -2494,7 +2578,10 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2494
2578
|
state.hasClassNames = true;
|
|
2495
2579
|
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
2496
2580
|
const placeholderModifiers = modifierClasses.filter((m) => m.modifier === "placeholder");
|
|
2497
|
-
const
|
|
2581
|
+
const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
|
|
2582
|
+
const stateModifiers = modifierClasses.filter(
|
|
2583
|
+
(m) => isStateModifier(m.modifier) && m.modifier !== "placeholder"
|
|
2584
|
+
);
|
|
2498
2585
|
if (placeholderModifiers.length > 0) {
|
|
2499
2586
|
const jsxOpeningElement = path2.parent;
|
|
2500
2587
|
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
@@ -2512,7 +2599,108 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2512
2599
|
}
|
|
2513
2600
|
}
|
|
2514
2601
|
}
|
|
2515
|
-
|
|
2602
|
+
const hasPlatformModifiers = platformModifiers.length > 0;
|
|
2603
|
+
const hasStateModifiers = stateModifiers.length > 0;
|
|
2604
|
+
const hasBaseClasses = baseClasses.length > 0;
|
|
2605
|
+
if (hasStateModifiers && hasPlatformModifiers) {
|
|
2606
|
+
const jsxOpeningElement = path2.parent;
|
|
2607
|
+
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
2608
|
+
if (componentSupport) {
|
|
2609
|
+
const styleArrayElements = [];
|
|
2610
|
+
if (hasBaseClasses) {
|
|
2611
|
+
const baseClassName = baseClasses.join(" ");
|
|
2612
|
+
const baseStyleObject = parseClassName(baseClassName, state.customColors);
|
|
2613
|
+
const baseStyleKey = generateStyleKey(baseClassName);
|
|
2614
|
+
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
2615
|
+
styleArrayElements.push(
|
|
2616
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey))
|
|
2617
|
+
);
|
|
2618
|
+
}
|
|
2619
|
+
const platformSelectExpression = processPlatformModifiers(
|
|
2620
|
+
platformModifiers,
|
|
2621
|
+
state,
|
|
2622
|
+
parseClassName,
|
|
2623
|
+
generateStyleKey,
|
|
2624
|
+
t
|
|
2625
|
+
);
|
|
2626
|
+
styleArrayElements.push(platformSelectExpression);
|
|
2627
|
+
const modifiersByType = /* @__PURE__ */ new Map();
|
|
2628
|
+
for (const mod of stateModifiers) {
|
|
2629
|
+
const modType = mod.modifier;
|
|
2630
|
+
if (!modifiersByType.has(modType)) {
|
|
2631
|
+
modifiersByType.set(modType, []);
|
|
2632
|
+
}
|
|
2633
|
+
modifiersByType.get(modType)?.push(mod);
|
|
2634
|
+
}
|
|
2635
|
+
for (const [modifierType, modifiers] of modifiersByType) {
|
|
2636
|
+
if (!componentSupport.supportedModifiers.includes(modifierType)) {
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
2640
|
+
const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
|
|
2641
|
+
const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
|
|
2642
|
+
state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
|
|
2643
|
+
const stateProperty = getStatePropertyForModifier(modifierType);
|
|
2644
|
+
const conditionalExpression = t.logicalExpression(
|
|
2645
|
+
"&&",
|
|
2646
|
+
t.identifier(stateProperty),
|
|
2647
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey))
|
|
2648
|
+
);
|
|
2649
|
+
styleArrayElements.push(conditionalExpression);
|
|
2650
|
+
}
|
|
2651
|
+
const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier))).filter(
|
|
2652
|
+
(mod) => componentSupport.supportedModifiers.includes(mod)
|
|
2653
|
+
);
|
|
2654
|
+
const styleArrayExpression = t.arrayExpression(styleArrayElements);
|
|
2655
|
+
const styleFunctionExpression = createStyleFunction(styleArrayExpression, usedModifiers, t);
|
|
2656
|
+
const styleAttribute2 = findStyleAttribute(path2, targetStyleProp, t);
|
|
2657
|
+
if (styleAttribute2) {
|
|
2658
|
+
mergeStyleFunctionAttribute(path2, styleAttribute2, styleFunctionExpression, t);
|
|
2659
|
+
} else {
|
|
2660
|
+
replaceWithStyleFunctionAttribute(path2, styleFunctionExpression, targetStyleProp, t);
|
|
2661
|
+
}
|
|
2662
|
+
return;
|
|
2663
|
+
} else {
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
if (hasPlatformModifiers && !hasStateModifiers) {
|
|
2667
|
+
const styleExpressions = [];
|
|
2668
|
+
if (hasBaseClasses) {
|
|
2669
|
+
const baseClassName = baseClasses.join(" ");
|
|
2670
|
+
const baseStyleObject = parseClassName(baseClassName, state.customColors);
|
|
2671
|
+
const baseStyleKey = generateStyleKey(baseClassName);
|
|
2672
|
+
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
2673
|
+
styleExpressions.push(
|
|
2674
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey))
|
|
2675
|
+
);
|
|
2676
|
+
}
|
|
2677
|
+
const platformSelectExpression = processPlatformModifiers(
|
|
2678
|
+
platformModifiers,
|
|
2679
|
+
state,
|
|
2680
|
+
parseClassName,
|
|
2681
|
+
generateStyleKey,
|
|
2682
|
+
t
|
|
2683
|
+
);
|
|
2684
|
+
styleExpressions.push(platformSelectExpression);
|
|
2685
|
+
const styleExpression = styleExpressions.length === 1 ? styleExpressions[0] : t.arrayExpression(styleExpressions);
|
|
2686
|
+
const styleAttribute2 = findStyleAttribute(path2, targetStyleProp, t);
|
|
2687
|
+
if (styleAttribute2) {
|
|
2688
|
+
const existingStyle = styleAttribute2.value;
|
|
2689
|
+
if (t.isJSXExpressionContainer(existingStyle) && !t.isJSXEmptyExpression(existingStyle.expression)) {
|
|
2690
|
+
const existing = existingStyle.expression;
|
|
2691
|
+
const mergedArray = t.isArrayExpression(existing) ? t.arrayExpression([styleExpression, ...existing.elements]) : t.arrayExpression([styleExpression, existing]);
|
|
2692
|
+
styleAttribute2.value = t.jsxExpressionContainer(mergedArray);
|
|
2693
|
+
} else {
|
|
2694
|
+
styleAttribute2.value = t.jsxExpressionContainer(styleExpression);
|
|
2695
|
+
}
|
|
2696
|
+
path2.remove();
|
|
2697
|
+
} else {
|
|
2698
|
+
path2.node.name = t.jsxIdentifier(targetStyleProp);
|
|
2699
|
+
path2.node.value = t.jsxExpressionContainer(styleExpression);
|
|
2700
|
+
}
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
if (hasStateModifiers) {
|
|
2516
2704
|
const jsxOpeningElement = path2.parent;
|
|
2517
2705
|
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
2518
2706
|
if (componentSupport) {
|
package/dist/babel/plugin.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Babel plugin for react-native-tailwind
|
|
3
3
|
* Transforms className props to style props at compile time
|
|
4
4
|
*/
|
|
5
|
-
import type { PluginObj, PluginPass } from "@babel/core";
|
|
5
|
+
import type { NodePath, PluginObj, PluginPass } from "@babel/core";
|
|
6
6
|
import * as BabelTypes from "@babel/types";
|
|
7
7
|
import type { StyleObject } from "../types/core.js";
|
|
8
8
|
/**
|
|
@@ -29,12 +29,15 @@ type PluginState = PluginPass & {
|
|
|
29
29
|
styleRegistry: Map<string, StyleObject>;
|
|
30
30
|
hasClassNames: boolean;
|
|
31
31
|
hasStyleSheetImport: boolean;
|
|
32
|
+
hasPlatformImport: boolean;
|
|
33
|
+
needsPlatformImport: boolean;
|
|
32
34
|
customColors: Record<string, string>;
|
|
33
35
|
supportedAttributes: Set<string>;
|
|
34
36
|
attributePatterns: RegExp[];
|
|
35
37
|
stylesIdentifier: string;
|
|
36
38
|
twImportNames: Set<string>;
|
|
37
39
|
hasTwImport: boolean;
|
|
40
|
+
reactNativeImportPath?: NodePath<BabelTypes.ImportDeclaration>;
|
|
38
41
|
};
|
|
39
42
|
export default function reactNativeTailwindBabelPlugin({ types: t }: {
|
|
40
43
|
types: typeof BabelTypes;
|