@mgcrea/react-native-tailwind 0.8.1 → 0.9.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 +152 -0
- package/dist/babel/config-loader.ts +2 -0
- package/dist/babel/index.cjs +177 -4
- package/dist/babel/plugin.d.ts +2 -0
- package/dist/babel/plugin.test.ts +241 -0
- package/dist/babel/plugin.ts +187 -10
- package/dist/babel/utils/platformModifierProcessing.d.ts +30 -0
- package/dist/babel/utils/platformModifierProcessing.ts +80 -0
- package/dist/babel/utils/styleInjection.d.ts +4 -0
- package/dist/babel/utils/styleInjection.ts +28 -0
- package/dist/babel/utils/styleTransforms.ts +1 -0
- 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/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/stubs/tw.test.js +1 -0
- package/package.json +6 -5
- package/src/babel/config-loader.ts +2 -0
- package/src/babel/plugin.test.ts +241 -0
- package/src/babel/plugin.ts +187 -10
- package/src/babel/utils/platformModifierProcessing.ts +80 -0
- package/src/babel/utils/styleInjection.ts +28 -0
- package/src/babel/utils/styleTransforms.ts +1 -0
- package/src/parser/aspectRatio.ts +1 -0
- 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.ts +1 -0
- package/src/parser/transforms.ts +5 -0
- package/src/parser/typography.ts +2 -0
- package/src/stubs/tw.test.ts +27 -0
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
|
@@ -1730,13 +1730,15 @@ function parsePlaceholderClasses(classes, customColors) {
|
|
|
1730
1730
|
}
|
|
1731
1731
|
|
|
1732
1732
|
// src/parser/modifiers.ts
|
|
1733
|
-
var
|
|
1733
|
+
var STATE_MODIFIERS = [
|
|
1734
1734
|
"active",
|
|
1735
1735
|
"hover",
|
|
1736
1736
|
"focus",
|
|
1737
1737
|
"disabled",
|
|
1738
1738
|
"placeholder"
|
|
1739
1739
|
];
|
|
1740
|
+
var PLATFORM_MODIFIERS = ["ios", "android", "web"];
|
|
1741
|
+
var SUPPORTED_MODIFIERS = [...STATE_MODIFIERS, ...PLATFORM_MODIFIERS];
|
|
1740
1742
|
function parseModifier(cls) {
|
|
1741
1743
|
const colonIndex = cls.indexOf(":");
|
|
1742
1744
|
if (colonIndex === -1) {
|
|
@@ -1758,6 +1760,12 @@ function parseModifier(cls) {
|
|
|
1758
1760
|
baseClass
|
|
1759
1761
|
};
|
|
1760
1762
|
}
|
|
1763
|
+
function isStateModifier(modifier) {
|
|
1764
|
+
return STATE_MODIFIERS.includes(modifier);
|
|
1765
|
+
}
|
|
1766
|
+
function isPlatformModifier(modifier) {
|
|
1767
|
+
return PLATFORM_MODIFIERS.includes(modifier);
|
|
1768
|
+
}
|
|
1761
1769
|
function splitModifierClasses(className) {
|
|
1762
1770
|
const classes = className.trim().split(/\s+/).filter(Boolean);
|
|
1763
1771
|
const baseClasses = [];
|
|
@@ -2118,6 +2126,34 @@ function createStyleFunction(styleExpression, modifierTypes, t) {
|
|
|
2118
2126
|
return t.arrowFunctionExpression([param], styleExpression);
|
|
2119
2127
|
}
|
|
2120
2128
|
|
|
2129
|
+
// src/babel/utils/platformModifierProcessing.ts
|
|
2130
|
+
function processPlatformModifiers(platformModifiers, state, parseClassName2, generateStyleKey2, t) {
|
|
2131
|
+
state.needsPlatformImport = true;
|
|
2132
|
+
const modifiersByPlatform = /* @__PURE__ */ new Map();
|
|
2133
|
+
for (const mod of platformModifiers) {
|
|
2134
|
+
const platform = mod.modifier;
|
|
2135
|
+
if (!modifiersByPlatform.has(platform)) {
|
|
2136
|
+
modifiersByPlatform.set(platform, []);
|
|
2137
|
+
}
|
|
2138
|
+
const platformGroup = modifiersByPlatform.get(platform);
|
|
2139
|
+
if (platformGroup) {
|
|
2140
|
+
platformGroup.push(mod);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
const selectProperties = [];
|
|
2144
|
+
for (const [platform, modifiers] of modifiersByPlatform) {
|
|
2145
|
+
const classNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
2146
|
+
const styleObject = parseClassName2(classNames, state.customColors);
|
|
2147
|
+
const styleKey = generateStyleKey2(`${platform}_${classNames}`);
|
|
2148
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
2149
|
+
const styleReference = t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey));
|
|
2150
|
+
selectProperties.push(t.objectProperty(t.identifier(platform), styleReference));
|
|
2151
|
+
}
|
|
2152
|
+
return t.callExpression(t.memberExpression(t.identifier("Platform"), t.identifier("select")), [
|
|
2153
|
+
t.objectExpression(selectProperties)
|
|
2154
|
+
]);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2121
2157
|
// src/babel/utils/styleInjection.ts
|
|
2122
2158
|
function addStyleSheetImport(path2, t) {
|
|
2123
2159
|
const importDeclaration = t.importDeclaration(
|
|
@@ -2126,6 +2162,25 @@ function addStyleSheetImport(path2, t) {
|
|
|
2126
2162
|
);
|
|
2127
2163
|
path2.unshiftContainer("body", importDeclaration);
|
|
2128
2164
|
}
|
|
2165
|
+
function addPlatformImport(path2, t) {
|
|
2166
|
+
const body = path2.node.body;
|
|
2167
|
+
let reactNativeImport = null;
|
|
2168
|
+
for (const statement of body) {
|
|
2169
|
+
if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
|
|
2170
|
+
reactNativeImport = statement;
|
|
2171
|
+
break;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
if (reactNativeImport) {
|
|
2175
|
+
reactNativeImport.specifiers.push(t.importSpecifier(t.identifier("Platform"), t.identifier("Platform")));
|
|
2176
|
+
} else {
|
|
2177
|
+
const importDeclaration = t.importDeclaration(
|
|
2178
|
+
[t.importSpecifier(t.identifier("Platform"), t.identifier("Platform"))],
|
|
2179
|
+
t.stringLiteral("react-native")
|
|
2180
|
+
);
|
|
2181
|
+
path2.unshiftContainer("body", importDeclaration);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2129
2184
|
function injectStylesAtTop(path2, styleRegistry, stylesIdentifier, t) {
|
|
2130
2185
|
const styleProperties = [];
|
|
2131
2186
|
for (const [key, styleObject] of styleRegistry) {
|
|
@@ -2355,6 +2410,8 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2355
2410
|
state.styleRegistry = /* @__PURE__ */ new Map();
|
|
2356
2411
|
state.hasClassNames = false;
|
|
2357
2412
|
state.hasStyleSheetImport = false;
|
|
2413
|
+
state.hasPlatformImport = false;
|
|
2414
|
+
state.needsPlatformImport = false;
|
|
2358
2415
|
state.supportedAttributes = exactMatches;
|
|
2359
2416
|
state.attributePatterns = patterns;
|
|
2360
2417
|
state.stylesIdentifier = stylesIdentifier;
|
|
@@ -2372,10 +2429,13 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2372
2429
|
if (!state.hasStyleSheetImport) {
|
|
2373
2430
|
addStyleSheetImport(path2, t);
|
|
2374
2431
|
}
|
|
2432
|
+
if (state.needsPlatformImport && !state.hasPlatformImport) {
|
|
2433
|
+
addPlatformImport(path2, t);
|
|
2434
|
+
}
|
|
2375
2435
|
injectStylesAtTop(path2, state.styleRegistry, state.stylesIdentifier, t);
|
|
2376
2436
|
}
|
|
2377
2437
|
},
|
|
2378
|
-
// Check if StyleSheet
|
|
2438
|
+
// Check if StyleSheet/Platform are already imported and track tw/twStyle imports
|
|
2379
2439
|
ImportDeclaration(path2, state) {
|
|
2380
2440
|
const node = path2.node;
|
|
2381
2441
|
if (node.source.value === "react-native") {
|
|
@@ -2386,12 +2446,21 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2386
2446
|
}
|
|
2387
2447
|
return false;
|
|
2388
2448
|
});
|
|
2449
|
+
const hasPlatform = specifiers.some((spec) => {
|
|
2450
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
2451
|
+
return spec.imported.name === "Platform";
|
|
2452
|
+
}
|
|
2453
|
+
return false;
|
|
2454
|
+
});
|
|
2389
2455
|
if (hasStyleSheet) {
|
|
2390
2456
|
state.hasStyleSheetImport = true;
|
|
2391
2457
|
} else {
|
|
2392
2458
|
node.specifiers.push(t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet")));
|
|
2393
2459
|
state.hasStyleSheetImport = true;
|
|
2394
2460
|
}
|
|
2461
|
+
if (hasPlatform) {
|
|
2462
|
+
state.hasPlatformImport = true;
|
|
2463
|
+
}
|
|
2395
2464
|
}
|
|
2396
2465
|
if (node.source.value === "@mgcrea/react-native-tailwind") {
|
|
2397
2466
|
const specifiers = node.specifiers;
|
|
@@ -2494,7 +2563,10 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2494
2563
|
state.hasClassNames = true;
|
|
2495
2564
|
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
2496
2565
|
const placeholderModifiers = modifierClasses.filter((m) => m.modifier === "placeholder");
|
|
2497
|
-
const
|
|
2566
|
+
const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
|
|
2567
|
+
const stateModifiers = modifierClasses.filter(
|
|
2568
|
+
(m) => isStateModifier(m.modifier) && m.modifier !== "placeholder"
|
|
2569
|
+
);
|
|
2498
2570
|
if (placeholderModifiers.length > 0) {
|
|
2499
2571
|
const jsxOpeningElement = path2.parent;
|
|
2500
2572
|
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
@@ -2512,7 +2584,108 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2512
2584
|
}
|
|
2513
2585
|
}
|
|
2514
2586
|
}
|
|
2515
|
-
|
|
2587
|
+
const hasPlatformModifiers = platformModifiers.length > 0;
|
|
2588
|
+
const hasStateModifiers = stateModifiers.length > 0;
|
|
2589
|
+
const hasBaseClasses = baseClasses.length > 0;
|
|
2590
|
+
if (hasStateModifiers && hasPlatformModifiers) {
|
|
2591
|
+
const jsxOpeningElement = path2.parent;
|
|
2592
|
+
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
2593
|
+
if (componentSupport) {
|
|
2594
|
+
const styleArrayElements = [];
|
|
2595
|
+
if (hasBaseClasses) {
|
|
2596
|
+
const baseClassName = baseClasses.join(" ");
|
|
2597
|
+
const baseStyleObject = parseClassName(baseClassName, state.customColors);
|
|
2598
|
+
const baseStyleKey = generateStyleKey(baseClassName);
|
|
2599
|
+
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
2600
|
+
styleArrayElements.push(
|
|
2601
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey))
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
const platformSelectExpression = processPlatformModifiers(
|
|
2605
|
+
platformModifiers,
|
|
2606
|
+
state,
|
|
2607
|
+
parseClassName,
|
|
2608
|
+
generateStyleKey,
|
|
2609
|
+
t
|
|
2610
|
+
);
|
|
2611
|
+
styleArrayElements.push(platformSelectExpression);
|
|
2612
|
+
const modifiersByType = /* @__PURE__ */ new Map();
|
|
2613
|
+
for (const mod of stateModifiers) {
|
|
2614
|
+
const modType = mod.modifier;
|
|
2615
|
+
if (!modifiersByType.has(modType)) {
|
|
2616
|
+
modifiersByType.set(modType, []);
|
|
2617
|
+
}
|
|
2618
|
+
modifiersByType.get(modType)?.push(mod);
|
|
2619
|
+
}
|
|
2620
|
+
for (const [modifierType, modifiers] of modifiersByType) {
|
|
2621
|
+
if (!componentSupport.supportedModifiers.includes(modifierType)) {
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
2625
|
+
const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
|
|
2626
|
+
const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
|
|
2627
|
+
state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
|
|
2628
|
+
const stateProperty = getStatePropertyForModifier(modifierType);
|
|
2629
|
+
const conditionalExpression = t.logicalExpression(
|
|
2630
|
+
"&&",
|
|
2631
|
+
t.identifier(stateProperty),
|
|
2632
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey))
|
|
2633
|
+
);
|
|
2634
|
+
styleArrayElements.push(conditionalExpression);
|
|
2635
|
+
}
|
|
2636
|
+
const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier))).filter(
|
|
2637
|
+
(mod) => componentSupport.supportedModifiers.includes(mod)
|
|
2638
|
+
);
|
|
2639
|
+
const styleArrayExpression = t.arrayExpression(styleArrayElements);
|
|
2640
|
+
const styleFunctionExpression = createStyleFunction(styleArrayExpression, usedModifiers, t);
|
|
2641
|
+
const styleAttribute2 = findStyleAttribute(path2, targetStyleProp, t);
|
|
2642
|
+
if (styleAttribute2) {
|
|
2643
|
+
mergeStyleFunctionAttribute(path2, styleAttribute2, styleFunctionExpression, t);
|
|
2644
|
+
} else {
|
|
2645
|
+
replaceWithStyleFunctionAttribute(path2, styleFunctionExpression, targetStyleProp, t);
|
|
2646
|
+
}
|
|
2647
|
+
return;
|
|
2648
|
+
} else {
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
if (hasPlatformModifiers && !hasStateModifiers) {
|
|
2652
|
+
const styleExpressions = [];
|
|
2653
|
+
if (hasBaseClasses) {
|
|
2654
|
+
const baseClassName = baseClasses.join(" ");
|
|
2655
|
+
const baseStyleObject = parseClassName(baseClassName, state.customColors);
|
|
2656
|
+
const baseStyleKey = generateStyleKey(baseClassName);
|
|
2657
|
+
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
2658
|
+
styleExpressions.push(
|
|
2659
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey))
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
const platformSelectExpression = processPlatformModifiers(
|
|
2663
|
+
platformModifiers,
|
|
2664
|
+
state,
|
|
2665
|
+
parseClassName,
|
|
2666
|
+
generateStyleKey,
|
|
2667
|
+
t
|
|
2668
|
+
);
|
|
2669
|
+
styleExpressions.push(platformSelectExpression);
|
|
2670
|
+
const styleExpression = styleExpressions.length === 1 ? styleExpressions[0] : t.arrayExpression(styleExpressions);
|
|
2671
|
+
const styleAttribute2 = findStyleAttribute(path2, targetStyleProp, t);
|
|
2672
|
+
if (styleAttribute2) {
|
|
2673
|
+
const existingStyle = styleAttribute2.value;
|
|
2674
|
+
if (t.isJSXExpressionContainer(existingStyle) && !t.isJSXEmptyExpression(existingStyle.expression)) {
|
|
2675
|
+
const existing = existingStyle.expression;
|
|
2676
|
+
const mergedArray = t.isArrayExpression(existing) ? t.arrayExpression([styleExpression, ...existing.elements]) : t.arrayExpression([styleExpression, existing]);
|
|
2677
|
+
styleAttribute2.value = t.jsxExpressionContainer(mergedArray);
|
|
2678
|
+
} else {
|
|
2679
|
+
styleAttribute2.value = t.jsxExpressionContainer(styleExpression);
|
|
2680
|
+
}
|
|
2681
|
+
path2.remove();
|
|
2682
|
+
} else {
|
|
2683
|
+
path2.node.name = t.jsxIdentifier(targetStyleProp);
|
|
2684
|
+
path2.node.value = t.jsxExpressionContainer(styleExpression);
|
|
2685
|
+
}
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
if (hasStateModifiers) {
|
|
2516
2689
|
const jsxOpeningElement = path2.parent;
|
|
2517
2690
|
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
2518
2691
|
if (componentSupport) {
|
package/dist/babel/plugin.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ 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[];
|
|
@@ -480,3 +480,244 @@ describe("Babel plugin - placeholder: modifier transformation", () => {
|
|
|
480
480
|
consoleSpy.mockRestore();
|
|
481
481
|
});
|
|
482
482
|
});
|
|
483
|
+
|
|
484
|
+
describe("Babel plugin - platform modifier transformation", () => {
|
|
485
|
+
it("should transform platform modifiers to Platform.select()", () => {
|
|
486
|
+
const input = `
|
|
487
|
+
import React from 'react';
|
|
488
|
+
import { View } from 'react-native';
|
|
489
|
+
|
|
490
|
+
export function Component() {
|
|
491
|
+
return (
|
|
492
|
+
<View className="p-4 ios:p-6 android:p-8" />
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
`;
|
|
496
|
+
|
|
497
|
+
const output = transform(input, undefined, true);
|
|
498
|
+
|
|
499
|
+
// Should import Platform from react-native
|
|
500
|
+
expect(output).toContain("Platform");
|
|
501
|
+
expect(output).toMatch(/import.*Platform.*from ['"]react-native['"]/);
|
|
502
|
+
|
|
503
|
+
// Should generate Platform.select()
|
|
504
|
+
expect(output).toContain("Platform.select");
|
|
505
|
+
|
|
506
|
+
// Should have base padding style
|
|
507
|
+
expect(output).toContain("_p_4");
|
|
508
|
+
|
|
509
|
+
// Should have iOS and Android specific styles
|
|
510
|
+
expect(output).toContain("_ios_p_6");
|
|
511
|
+
expect(output).toContain("_android_p_8");
|
|
512
|
+
|
|
513
|
+
// Should have correct style values in StyleSheet.create
|
|
514
|
+
expect(output).toMatch(/padding:\s*16/); // p-4
|
|
515
|
+
expect(output).toMatch(/padding:\s*24/); // p-6 (ios)
|
|
516
|
+
expect(output).toMatch(/padding:\s*32/); // p-8 (android)
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("should support multiple platform modifiers on same element", () => {
|
|
520
|
+
const input = `
|
|
521
|
+
import React from 'react';
|
|
522
|
+
import { View } from 'react-native';
|
|
523
|
+
|
|
524
|
+
export function Component() {
|
|
525
|
+
return (
|
|
526
|
+
<View className="bg-white ios:bg-blue-50 android:bg-green-50 p-4 ios:p-6 android:p-8" />
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
`;
|
|
530
|
+
|
|
531
|
+
const output = transform(input, undefined, true);
|
|
532
|
+
|
|
533
|
+
// Should have Platform import
|
|
534
|
+
expect(output).toContain("Platform");
|
|
535
|
+
|
|
536
|
+
// Should have base styles (combined key)
|
|
537
|
+
expect(output).toContain("_bg_white_p_4");
|
|
538
|
+
|
|
539
|
+
// Should have iOS specific styles (combined key for multiple ios: modifiers)
|
|
540
|
+
expect(output).toContain("_ios_bg_blue_50_p_6");
|
|
541
|
+
|
|
542
|
+
// Should have Android specific styles (combined key for multiple android: modifiers)
|
|
543
|
+
expect(output).toContain("_android_bg_green_50_p_8");
|
|
544
|
+
|
|
545
|
+
// Should contain Platform.select with both platforms
|
|
546
|
+
expect(output).toMatch(/Platform\.select\s*\(\s*\{[\s\S]*ios:/);
|
|
547
|
+
expect(output).toMatch(/Platform\.select\s*\(\s*\{[\s\S]*android:/);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("should support web platform modifier", () => {
|
|
551
|
+
const input = `
|
|
552
|
+
import React from 'react';
|
|
553
|
+
import { View } from 'react-native';
|
|
554
|
+
|
|
555
|
+
export function Component() {
|
|
556
|
+
return (
|
|
557
|
+
<View className="p-4 web:p-2" />
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
`;
|
|
561
|
+
|
|
562
|
+
const output = transform(input, undefined, true);
|
|
563
|
+
|
|
564
|
+
// Should have Platform.select with web
|
|
565
|
+
expect(output).toContain("Platform.select");
|
|
566
|
+
expect(output).toContain("web:");
|
|
567
|
+
expect(output).toContain("_web_p_2");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("should work with platform modifiers on all components", () => {
|
|
571
|
+
const input = `
|
|
572
|
+
import React from 'react';
|
|
573
|
+
import { View, Text, ScrollView } from 'react-native';
|
|
574
|
+
|
|
575
|
+
export function Component() {
|
|
576
|
+
return (
|
|
577
|
+
<View className="ios:bg-blue-500 android:bg-green-500">
|
|
578
|
+
<Text className="ios:text-lg android:text-xl">Platform text</Text>
|
|
579
|
+
<ScrollView contentContainerClassName="ios:p-4 android:p-8" />
|
|
580
|
+
</View>
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
`;
|
|
584
|
+
|
|
585
|
+
const output = transform(input, undefined, true);
|
|
586
|
+
|
|
587
|
+
// Should work on View - check for Platform.select separately (not checking style= format)
|
|
588
|
+
expect(output).toContain("Platform.select");
|
|
589
|
+
|
|
590
|
+
// Should work on Text
|
|
591
|
+
expect(output).toContain("_ios_text_lg");
|
|
592
|
+
expect(output).toContain("_android_text_xl");
|
|
593
|
+
|
|
594
|
+
// Should work on ScrollView contentContainerStyle
|
|
595
|
+
expect(output).toContain("contentContainerStyle");
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("should combine platform modifiers with state modifiers", () => {
|
|
599
|
+
const input = `
|
|
600
|
+
import React from 'react';
|
|
601
|
+
import { Pressable, Text } from 'react-native';
|
|
602
|
+
|
|
603
|
+
export function Component() {
|
|
604
|
+
return (
|
|
605
|
+
<Pressable className="bg-blue-500 active:bg-blue-700 ios:shadow-md android:shadow-sm p-4">
|
|
606
|
+
<Text className="text-white">Button</Text>
|
|
607
|
+
</Pressable>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
`;
|
|
611
|
+
|
|
612
|
+
const output = transform(input, undefined, true);
|
|
613
|
+
|
|
614
|
+
// Should have Platform.select for platform modifiers
|
|
615
|
+
expect(output).toContain("Platform.select");
|
|
616
|
+
expect(output).toContain("_ios_shadow_md");
|
|
617
|
+
expect(output).toContain("_android_shadow_sm");
|
|
618
|
+
|
|
619
|
+
// Should have state modifier function for active
|
|
620
|
+
expect(output).toMatch(/\(\s*\{\s*pressed\s*\}\s*\)\s*=>/);
|
|
621
|
+
expect(output).toContain("pressed");
|
|
622
|
+
expect(output).toContain("_active_bg_blue_700");
|
|
623
|
+
|
|
624
|
+
// Should have base styles
|
|
625
|
+
expect(output).toContain("_bg_blue_500");
|
|
626
|
+
expect(output).toContain("_p_4");
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it("should handle platform-specific colors", () => {
|
|
630
|
+
const input = `
|
|
631
|
+
import React from 'react';
|
|
632
|
+
import { View, Text } from 'react-native';
|
|
633
|
+
|
|
634
|
+
export function Component() {
|
|
635
|
+
return (
|
|
636
|
+
<View className="bg-gray-100 ios:bg-blue-50 android:bg-green-50">
|
|
637
|
+
<Text className="text-gray-900 ios:text-blue-900 android:text-green-900">
|
|
638
|
+
Platform colors
|
|
639
|
+
</Text>
|
|
640
|
+
</View>
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
`;
|
|
644
|
+
|
|
645
|
+
const output = transform(input, undefined, true);
|
|
646
|
+
|
|
647
|
+
// Should have color values in StyleSheet
|
|
648
|
+
expect(output).toMatch(/#[0-9A-F]{6}/i); // Hex color format
|
|
649
|
+
|
|
650
|
+
// Should have platform-specific color classes
|
|
651
|
+
expect(output).toContain("_ios_text_blue_900");
|
|
652
|
+
expect(output).toContain("_android_text_green_900");
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("should only add Platform import once when needed", () => {
|
|
656
|
+
const input = `
|
|
657
|
+
import React from 'react';
|
|
658
|
+
import { View } from 'react-native';
|
|
659
|
+
|
|
660
|
+
export function Component() {
|
|
661
|
+
return (
|
|
662
|
+
<>
|
|
663
|
+
<View className="ios:p-4" />
|
|
664
|
+
<View className="android:p-8" />
|
|
665
|
+
<View className="ios:bg-blue-500" />
|
|
666
|
+
</>
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
`;
|
|
670
|
+
|
|
671
|
+
const output = transform(input, undefined, true);
|
|
672
|
+
|
|
673
|
+
// Should have Platform import
|
|
674
|
+
expect(output).toContain("Platform");
|
|
675
|
+
|
|
676
|
+
// Count how many times Platform is imported (should be once)
|
|
677
|
+
const platformImports = output.match(/import.*Platform.*from ['"]react-native['"]/g);
|
|
678
|
+
expect(platformImports).toHaveLength(1);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it("should merge with existing Platform import", () => {
|
|
682
|
+
const input = `
|
|
683
|
+
import React from 'react';
|
|
684
|
+
import { View, Platform } from 'react-native';
|
|
685
|
+
|
|
686
|
+
export function Component() {
|
|
687
|
+
return <View className="ios:p-4 android:p-8" />;
|
|
688
|
+
}
|
|
689
|
+
`;
|
|
690
|
+
|
|
691
|
+
const output = transform(input, undefined, true);
|
|
692
|
+
|
|
693
|
+
// Should still use Platform.select
|
|
694
|
+
expect(output).toContain("Platform.select");
|
|
695
|
+
|
|
696
|
+
// Should not duplicate Platform import - Platform appears in import and Platform.select calls
|
|
697
|
+
expect(output).toMatch(/Platform.*react-native/);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("should handle platform modifiers without base classes", () => {
|
|
701
|
+
const input = `
|
|
702
|
+
import React from 'react';
|
|
703
|
+
import { View } from 'react-native';
|
|
704
|
+
|
|
705
|
+
export function Component() {
|
|
706
|
+
return <View className="ios:p-6 android:p-8" />;
|
|
707
|
+
}
|
|
708
|
+
`;
|
|
709
|
+
|
|
710
|
+
const output = transform(input, undefined, true);
|
|
711
|
+
|
|
712
|
+
// Should only have Platform.select, no base style
|
|
713
|
+
expect(output).toContain("Platform.select");
|
|
714
|
+
expect(output).toContain("_ios_p_6");
|
|
715
|
+
expect(output).toContain("_android_p_8");
|
|
716
|
+
|
|
717
|
+
// Should not have generic padding without platform prefix
|
|
718
|
+
// Check that non-platform-prefixed style keys don't exist
|
|
719
|
+
expect(output).not.toMatch(/(?<!_ios|_android|_web)_p_4:/);
|
|
720
|
+
expect(output).not.toMatch(/(?<!_ios|_android|_web)_p_6:/);
|
|
721
|
+
expect(output).not.toMatch(/(?<!_ios|_android|_web)_p_8:/);
|
|
722
|
+
});
|
|
723
|
+
});
|