@mgcrea/react-native-tailwind 0.11.0 → 0.12.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.
@@ -7,7 +7,9 @@ import babelPlugin, { type PluginOptions } from "./plugin.js";
7
7
  * Helper to transform code with the Babel plugin
8
8
  */
9
9
  function transform(code: string, options?: PluginOptions, includeJsx = false) {
10
- const presets = includeJsx ? ["@babel/preset-react"] : [];
10
+ const presets = includeJsx
11
+ ? ["@babel/preset-react", ["@babel/preset-typescript", { isTSX: true, allExtensions: true }]]
12
+ : [];
11
13
 
12
14
  const result = transformSync(code, {
13
15
  presets,
@@ -332,6 +334,89 @@ describe("Babel plugin - className transformation (existing behavior)", () => {
332
334
  // Should not have className in output
333
335
  expect(output).not.toContain("className");
334
336
  });
337
+
338
+ it('should transform className={"..."} (string literal in expression container)', () => {
339
+ const input = `
340
+ import { View } from 'react-native';
341
+ export function Component() {
342
+ return <View className={"flex-row items-center justify-start"} />;
343
+ }
344
+ `;
345
+
346
+ const output = transform(input, undefined, true);
347
+
348
+ // Should have StyleSheet
349
+ expect(output).toContain("StyleSheet.create");
350
+ expect(output).toContain("_twStyles");
351
+
352
+ // Should replace className with style
353
+ expect(output).not.toContain("className");
354
+ expect(output).toContain("style:");
355
+
356
+ // Should have the expected style keys
357
+ expect(output).toContain("_flex_row_items_center_justify_start");
358
+ });
359
+
360
+ it('should transform className={"..."} with modifiers', () => {
361
+ const input = `
362
+ import { Pressable } from 'react-native';
363
+ export function Component() {
364
+ return <Pressable className={"bg-blue-500 active:bg-blue-700 p-4"} />;
365
+ }
366
+ `;
367
+
368
+ const output = transform(input, undefined, true);
369
+
370
+ // Should have StyleSheet with both base and active styles
371
+ expect(output).toContain("_bg_blue_500_p_4");
372
+ expect(output).toContain("_active_bg_blue_700");
373
+
374
+ // Should have style function for active modifier (Pressable uses 'pressed' parameter)
375
+ expect(output).toMatch(/(pressed|_state)/);
376
+
377
+ // Should not have className in output
378
+ expect(output).not.toContain("className");
379
+ });
380
+
381
+ it('should transform className={"..."} with platform modifiers', () => {
382
+ const input = `
383
+ import { View } from 'react-native';
384
+ export function Component() {
385
+ return <View className={"p-4 ios:p-6 android:p-8"} />;
386
+ }
387
+ `;
388
+
389
+ const output = transform(input, undefined, true);
390
+
391
+ // Should have Platform import
392
+ expect(output).toContain("Platform");
393
+ expect(output).toMatch(/from ['"]react-native['"]/); // Match both single and double quotes
394
+
395
+ // Should have Platform.select
396
+ expect(output).toContain("Platform.select");
397
+
398
+ // Should have platform-specific styles
399
+ expect(output).toContain("_ios_p_6");
400
+ expect(output).toContain("_android_p_8");
401
+
402
+ // Should not have className in output
403
+ expect(output).not.toContain("className");
404
+ });
405
+
406
+ it('should handle empty className={""}', () => {
407
+ const input = `
408
+ import { View } from 'react-native';
409
+ export function Component() {
410
+ return <View className={""} />;
411
+ }
412
+ `;
413
+
414
+ const output = transform(input, undefined, true);
415
+
416
+ // Should remove empty className attribute entirely
417
+ expect(output).not.toContain("className");
418
+ expect(output).not.toContain("style=");
419
+ });
335
420
  });
336
421
 
337
422
  describe("Babel plugin - placeholder: modifier transformation", () => {
@@ -1022,6 +1107,306 @@ describe("Babel plugin - color scheme modifier transformation", () => {
1022
1107
  });
1023
1108
  });
1024
1109
 
1110
+ describe("Babel plugin - custom color scheme hook import", () => {
1111
+ it("should use custom import source for color scheme hook", () => {
1112
+ const input = `
1113
+ import React from 'react';
1114
+ import { View } from 'react-native';
1115
+
1116
+ export function Component() {
1117
+ return <View className="dark:bg-gray-900" />;
1118
+ }
1119
+ `;
1120
+
1121
+ const output = transform(
1122
+ input,
1123
+ {
1124
+ colorScheme: {
1125
+ importFrom: "@/hooks/useColorScheme",
1126
+ importName: "useColorScheme",
1127
+ },
1128
+ },
1129
+ true,
1130
+ );
1131
+
1132
+ // Should import from custom source
1133
+ expect(output).toContain('from "@/hooks/useColorScheme"');
1134
+ expect(output).not.toContain('useColorScheme } from "react-native"');
1135
+
1136
+ // Should inject hook call
1137
+ expect(output).toContain("_twColorScheme = useColorScheme()");
1138
+
1139
+ // Should have conditional styling
1140
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
1141
+ });
1142
+
1143
+ it("should use custom hook name", () => {
1144
+ const input = `
1145
+ import React from 'react';
1146
+ import { View } from 'react-native';
1147
+
1148
+ export function Component() {
1149
+ return <View className="dark:bg-gray-900" />;
1150
+ }
1151
+ `;
1152
+
1153
+ const output = transform(
1154
+ input,
1155
+ {
1156
+ colorScheme: {
1157
+ importFrom: "@react-navigation/native",
1158
+ importName: "useTheme",
1159
+ },
1160
+ },
1161
+ true,
1162
+ );
1163
+
1164
+ // Should import useTheme from React Navigation
1165
+ expect(output).toContain('from "@react-navigation/native"');
1166
+ expect(output).toContain("useTheme");
1167
+
1168
+ // Should call useTheme hook
1169
+ expect(output).toContain("_twColorScheme = useTheme()");
1170
+
1171
+ // Should have conditional styling
1172
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
1173
+ });
1174
+
1175
+ it("should merge custom hook with existing import from same source", () => {
1176
+ const input = `
1177
+ import React from 'react';
1178
+ import { View, Text } from 'react-native';
1179
+ import { useNavigation } from '@react-navigation/native';
1180
+
1181
+ export function Component() {
1182
+ const navigation = useNavigation();
1183
+ return (
1184
+ <View className="dark:bg-gray-900">
1185
+ <Text onPress={() => navigation.navigate('Home')}>Go Home</Text>
1186
+ </View>
1187
+ );
1188
+ }
1189
+ `;
1190
+
1191
+ const output = transform(
1192
+ input,
1193
+ {
1194
+ colorScheme: {
1195
+ importFrom: "@react-navigation/native",
1196
+ importName: "useTheme",
1197
+ },
1198
+ },
1199
+ true,
1200
+ );
1201
+
1202
+ // Should merge with existing import (both useNavigation and useTheme in same import)
1203
+ expect(output).toMatch(
1204
+ /import\s+\{\s*useNavigation[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/,
1205
+ );
1206
+ expect(output).toContain("useNavigation()");
1207
+ expect(output).toContain("useTheme()");
1208
+
1209
+ // Should only have one import from that source
1210
+ const importCount = (output.match(/@react-navigation\/native/g) ?? []).length;
1211
+ expect(importCount).toBe(1);
1212
+ });
1213
+
1214
+ it("should not duplicate custom hook if already imported", () => {
1215
+ const input = `
1216
+ import React from 'react';
1217
+ import { View } from 'react-native';
1218
+ import { useColorScheme } from '@/hooks/useColorScheme';
1219
+
1220
+ export function Component() {
1221
+ return <View className="dark:bg-gray-900" />;
1222
+ }
1223
+ `;
1224
+
1225
+ const output = transform(
1226
+ input,
1227
+ {
1228
+ colorScheme: {
1229
+ importFrom: "@/hooks/useColorScheme",
1230
+ importName: "useColorScheme",
1231
+ },
1232
+ },
1233
+ true,
1234
+ );
1235
+
1236
+ // Should not add duplicate import
1237
+ const importMatches = output.match(/import.*useColorScheme.*from ['"]@\/hooks\/useColorScheme['"]/g);
1238
+ expect(importMatches).toHaveLength(1);
1239
+
1240
+ // Should still inject hook call
1241
+ expect(output).toContain("_twColorScheme = useColorScheme()");
1242
+ });
1243
+
1244
+ it("should use react-native by default when no custom config provided", () => {
1245
+ const input = `
1246
+ import React from 'react';
1247
+ import { View } from 'react-native';
1248
+
1249
+ export function Component() {
1250
+ return <View className="dark:bg-gray-900" />;
1251
+ }
1252
+ `;
1253
+
1254
+ const output = transform(input, undefined, true);
1255
+
1256
+ // Should use default react-native import (can be single or double quotes)
1257
+ expect(output).toMatch(/useColorScheme\s*}\s*from\s+['"]react-native['"]/);
1258
+ expect(output).not.toContain("@/hooks");
1259
+ expect(output).not.toContain("@react-navigation");
1260
+
1261
+ // Should inject hook call with default name
1262
+ expect(output).toContain("_twColorScheme = useColorScheme()");
1263
+ });
1264
+
1265
+ it("should create separate import when only type-only import exists", () => {
1266
+ const input = `
1267
+ import React from 'react';
1268
+ import { View } from 'react-native';
1269
+ import type { NavigationProp } from '@react-navigation/native';
1270
+
1271
+ export function Component() {
1272
+ return <View className="dark:bg-gray-900" />;
1273
+ }
1274
+ `;
1275
+
1276
+ const output = transform(
1277
+ input,
1278
+ {
1279
+ colorScheme: {
1280
+ importFrom: "@react-navigation/native",
1281
+ importName: "useTheme",
1282
+ },
1283
+ },
1284
+ true,
1285
+ );
1286
+
1287
+ // TypeScript preset strips type-only imports, but the important thing is:
1288
+ // 1. useTheme hook is imported (not skipped thinking it was already imported)
1289
+ // 2. Hook is correctly called in the component
1290
+ expect(output).toMatch(/import\s+\{\s*useTheme\s*\}\s+from\s+['"]@react-navigation\/native['"]/);
1291
+ expect(output).toContain("_twColorScheme = useTheme()");
1292
+ });
1293
+
1294
+ it("should use aliased identifier when hook is already imported with alias", () => {
1295
+ const input = `
1296
+ import React from 'react';
1297
+ import { View, Text } from 'react-native';
1298
+ import { useTheme as navTheme } from '@react-navigation/native';
1299
+
1300
+ export function Component() {
1301
+ const theme = navTheme();
1302
+ return (
1303
+ <View className="dark:bg-gray-900">
1304
+ <Text>{theme.dark ? 'Dark' : 'Light'}</Text>
1305
+ </View>
1306
+ );
1307
+ }
1308
+ `;
1309
+
1310
+ const output = transform(
1311
+ input,
1312
+ {
1313
+ colorScheme: {
1314
+ importFrom: "@react-navigation/native",
1315
+ importName: "useTheme",
1316
+ },
1317
+ },
1318
+ true,
1319
+ );
1320
+
1321
+ // Should not add duplicate import
1322
+ const importMatches = output.match(
1323
+ /import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g,
1324
+ );
1325
+ expect(importMatches).toHaveLength(1);
1326
+
1327
+ // Should still have the aliased import
1328
+ expect(output).toMatch(/useTheme\s+as\s+navTheme/);
1329
+
1330
+ // Should call the aliased name (navTheme), not the export name (useTheme)
1331
+ // Both the user's code and our injected hook should use navTheme
1332
+ expect(output).toContain("_twColorScheme = navTheme()");
1333
+ expect(output).not.toContain("_twColorScheme = useTheme()");
1334
+ });
1335
+
1336
+ it("should not treat type-only imports as having the hook", () => {
1337
+ const input = `
1338
+ import React from 'react';
1339
+ import { View } from 'react-native';
1340
+ import type { useColorScheme } from 'react-native';
1341
+
1342
+ export function Component() {
1343
+ return <View className="dark:bg-gray-900" />;
1344
+ }
1345
+ `;
1346
+
1347
+ const output = transform(input, undefined, true);
1348
+
1349
+ // Should add a VALUE import for useColorScheme (type import doesn't count)
1350
+ expect(output).toMatch(/import\s+\{[^}]*useColorScheme[^}]*\}\s+from\s+['"]react-native['"]/);
1351
+
1352
+ // Should inject the hook
1353
+ expect(output).toContain("_twColorScheme = useColorScheme()");
1354
+
1355
+ // Should have both type-only and value imports in output
1356
+ // (TypeScript preset keeps type imports for type checking)
1357
+ const colorSchemeMatches = output.match(/useColorScheme/g);
1358
+ expect(colorSchemeMatches).toBeTruthy();
1359
+ if (colorSchemeMatches) {
1360
+ expect(colorSchemeMatches.length).toBeGreaterThanOrEqual(2); // At least in import and hook call
1361
+ }
1362
+ });
1363
+
1364
+ it("should handle both type-only and aliased imports together", () => {
1365
+ const input = `
1366
+ import React from 'react';
1367
+ import { View, Text } from 'react-native';
1368
+ import type { Theme } from '@react-navigation/native';
1369
+ import { useTheme as getNavTheme } from '@react-navigation/native';
1370
+
1371
+ export function Component() {
1372
+ const theme = getNavTheme();
1373
+ return (
1374
+ <View className="dark:bg-gray-900">
1375
+ <Text>{theme.dark ? 'Dark Mode' : 'Light Mode'}</Text>
1376
+ </View>
1377
+ );
1378
+ }
1379
+ `;
1380
+
1381
+ const output = transform(
1382
+ input,
1383
+ {
1384
+ colorScheme: {
1385
+ importFrom: "@react-navigation/native",
1386
+ importName: "useTheme",
1387
+ },
1388
+ },
1389
+ true,
1390
+ );
1391
+
1392
+ // TypeScript preset strips type-only imports
1393
+ // The important thing is: should not add duplicate import, and should use aliased name
1394
+ expect(output).toMatch(
1395
+ /import\s+\{[^}]*useTheme\s+as\s+getNavTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/,
1396
+ );
1397
+
1398
+ // Should not add duplicate import - useTheme should only appear in the aliased import
1399
+ const useThemeImports = output.match(
1400
+ /import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g,
1401
+ );
1402
+ expect(useThemeImports).toHaveLength(1);
1403
+
1404
+ // Should call the aliased name for both user code and our injected hook
1405
+ expect(output).toContain("_twColorScheme = getNavTheme()");
1406
+ expect(output).not.toContain("_twColorScheme = useTheme()");
1407
+ });
1408
+ });
1409
+
1025
1410
  describe("Babel plugin - import injection", () => {
1026
1411
  it("should not add StyleSheet import to files without className usage", () => {
1027
1412
  const input = `
@@ -1138,3 +1523,390 @@ describe("Babel plugin - scheme: modifier", () => {
1138
1523
  expect(output).toContain("_twColorScheme === 'light'");
1139
1524
  });
1140
1525
  });
1526
+
1527
+ describe("Babel plugin - color scheme modifiers in tw/twStyle", () => {
1528
+ it("should transform tw with dark: modifier inside component", () => {
1529
+ const input = `
1530
+ import { tw } from '@mgcrea/react-native-tailwind';
1531
+
1532
+ function MyComponent() {
1533
+ const styles = tw\`bg-white dark:bg-gray-900\`;
1534
+ return null;
1535
+ }
1536
+ `;
1537
+
1538
+ const output = transform(input);
1539
+
1540
+ // Should inject useColorScheme hook
1541
+ expect(output).toContain("useColorScheme");
1542
+ expect(output).toContain("_twColorScheme");
1543
+
1544
+ // Should generate style array with conditionals
1545
+ expect(output).toContain("style: [");
1546
+ expect(output).toContain('_twColorScheme === "dark"');
1547
+ expect(output).toContain("_twStyles._dark_bg_gray_900");
1548
+ expect(output).toContain("_twStyles._bg_white");
1549
+
1550
+ // Should have StyleSheet.create
1551
+ expect(output).toContain("StyleSheet.create");
1552
+ });
1553
+
1554
+ it("should transform twStyle with light: modifier inside component", () => {
1555
+ const input = `
1556
+ import { twStyle } from '@mgcrea/react-native-tailwind';
1557
+
1558
+ export const MyComponent = () => {
1559
+ const buttonStyles = twStyle('text-gray-900 light:text-gray-100');
1560
+ return null;
1561
+ };
1562
+ `;
1563
+
1564
+ const output = transform(input);
1565
+
1566
+ // Should inject useColorScheme hook
1567
+ expect(output).toContain("useColorScheme");
1568
+ expect(output).toContain("_twColorScheme");
1569
+
1570
+ // Should generate style array with conditionals
1571
+ expect(output).toContain("style: [");
1572
+ expect(output).toContain('_twColorScheme === "light"');
1573
+ expect(output).toContain("_twStyles._light_text_gray_100");
1574
+ expect(output).toContain("_twStyles._text_gray_900");
1575
+ });
1576
+
1577
+ it("should transform tw with both dark: and light: modifiers", () => {
1578
+ const input = `
1579
+ import { tw } from '@mgcrea/react-native-tailwind';
1580
+
1581
+ function MyComponent() {
1582
+ const styles = tw\`bg-blue-500 dark:bg-blue-900 light:bg-blue-100\`;
1583
+ return null;
1584
+ }
1585
+ `;
1586
+
1587
+ const output = transform(input);
1588
+
1589
+ // Should have both conditionals
1590
+ expect(output).toContain('_twColorScheme === "dark"');
1591
+ expect(output).toContain('_twColorScheme === "light"');
1592
+ expect(output).toContain("_twStyles._dark_bg_blue_900");
1593
+ expect(output).toContain("_twStyles._light_bg_blue_100");
1594
+ expect(output).toContain("_twStyles._bg_blue_500");
1595
+ });
1596
+
1597
+ it("should combine color scheme modifiers with state modifiers", () => {
1598
+ const input = `
1599
+ import { tw } from '@mgcrea/react-native-tailwind';
1600
+
1601
+ function MyComponent() {
1602
+ const styles = tw\`bg-white dark:bg-gray-900 active:bg-blue-500\`;
1603
+ return null;
1604
+ }
1605
+ `;
1606
+
1607
+ const output = transform(input);
1608
+
1609
+ // Should have color scheme conditionals in style array
1610
+ expect(output).toContain("style: [");
1611
+ expect(output).toContain('_twColorScheme === "dark"');
1612
+
1613
+ // Should have activeStyle property (separate from color scheme)
1614
+ expect(output).toContain("activeStyle:");
1615
+ expect(output).toContain("_twStyles._active_bg_blue_500");
1616
+ });
1617
+
1618
+ it("should warn if tw with color scheme modifiers used outside component", () => {
1619
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1620
+
1621
+ const input = `
1622
+ import { tw } from '@mgcrea/react-native-tailwind';
1623
+
1624
+ const globalStyles = tw\`bg-white dark:bg-gray-900\`;
1625
+ `;
1626
+
1627
+ const output = transform(input);
1628
+
1629
+ // Should warn about usage outside component
1630
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
1631
+ expect.stringContaining("Color scheme modifiers (dark:, light:) in tw/twStyle calls"),
1632
+ );
1633
+
1634
+ // Should not inject hook (no component scope)
1635
+ expect(output).not.toContain("useColorScheme");
1636
+
1637
+ // Should still generate styles but without runtime conditionals
1638
+ expect(output).toContain("_twStyles");
1639
+
1640
+ consoleWarnSpy.mockRestore();
1641
+ });
1642
+
1643
+ it("should handle tw with only dark: modifier (no base class)", () => {
1644
+ const input = `
1645
+ import { tw } from '@mgcrea/react-native-tailwind';
1646
+
1647
+ function MyComponent() {
1648
+ const styles = tw\`dark:bg-gray-900\`;
1649
+ return null;
1650
+ }
1651
+ `;
1652
+
1653
+ const output = transform(input);
1654
+
1655
+ // Should still generate style array
1656
+ expect(output).toContain("style: [");
1657
+ expect(output).toContain('_twColorScheme === "dark"');
1658
+ expect(output).toContain("_twStyles._dark_bg_gray_900");
1659
+ });
1660
+
1661
+ it("should work with custom color scheme hook import", () => {
1662
+ const input = `
1663
+ import { tw } from '@mgcrea/react-native-tailwind';
1664
+ import { useTheme } from '@react-navigation/native';
1665
+
1666
+ function MyComponent() {
1667
+ const styles = tw\`bg-white dark:bg-gray-900\`;
1668
+ return null;
1669
+ }
1670
+ `;
1671
+
1672
+ const options: PluginOptions = {
1673
+ colorScheme: {
1674
+ importFrom: "@react-navigation/native",
1675
+ importName: "useTheme",
1676
+ },
1677
+ };
1678
+
1679
+ const output = transform(input, options);
1680
+
1681
+ // Should use existing import (not duplicate)
1682
+ const themeImportCount = (output.match(/useTheme/g) ?? []).length;
1683
+ // Should appear in import statement and hook call
1684
+ expect(themeImportCount).toBeGreaterThanOrEqual(2);
1685
+
1686
+ // Should call the custom hook
1687
+ expect(output).toContain("useTheme()");
1688
+ });
1689
+
1690
+ it("should generate both style array and darkStyle/lightStyle properties", () => {
1691
+ const input = `
1692
+ import { tw } from '@mgcrea/react-native-tailwind';
1693
+
1694
+ function MyComponent() {
1695
+ const styles = tw\`bg-white dark:bg-gray-900 light:bg-gray-50\`;
1696
+ return null;
1697
+ }
1698
+ `;
1699
+
1700
+ const output = transform(input);
1701
+
1702
+ // Should have runtime conditional in style array
1703
+ expect(output).toContain("style: [");
1704
+ expect(output).toContain('_twColorScheme === "dark"');
1705
+ expect(output).toContain('_twColorScheme === "light"');
1706
+
1707
+ // Should ALSO have darkStyle and lightStyle properties for manual access
1708
+ expect(output).toContain("darkStyle:");
1709
+ expect(output).toContain("lightStyle:");
1710
+ expect(output).toContain("_twStyles._dark_bg_gray_900");
1711
+ expect(output).toContain("_twStyles._light_bg_gray_50");
1712
+ });
1713
+
1714
+ it("should allow accessing raw color values from darkStyle/lightStyle", () => {
1715
+ const input = `
1716
+ import { tw } from '@mgcrea/react-native-tailwind';
1717
+
1718
+ function MyComponent() {
1719
+ const btnStyles = tw\`bg-blue-500 dark:bg-blue-900\`;
1720
+ // User can access raw hex for Reanimated
1721
+ const darkBgColor = btnStyles.darkStyle?.backgroundColor;
1722
+ return null;
1723
+ }
1724
+ `;
1725
+
1726
+ const output = transform(input);
1727
+
1728
+ // Should have darkStyle property available
1729
+ expect(output).toContain("darkStyle:");
1730
+ expect(output).toContain("_twStyles._dark_bg_blue_900");
1731
+
1732
+ // The actual usage line should be preserved (TypeScript/Babel doesn't remove it)
1733
+ expect(output).toContain("btnStyles.darkStyle");
1734
+ });
1735
+
1736
+ // Platform modifier tests for tw/twStyle
1737
+ it("should transform tw with ios: modifier", () => {
1738
+ const input = `
1739
+ import { tw } from '@mgcrea/react-native-tailwind';
1740
+
1741
+ function MyComponent() {
1742
+ const styles = tw\`bg-white ios:p-6\`;
1743
+ return null;
1744
+ }
1745
+ `;
1746
+
1747
+ const output = transform(input);
1748
+
1749
+ // Should add Platform import
1750
+ expect(output).toContain("Platform");
1751
+ expect(output).toContain('from "react-native"');
1752
+
1753
+ // Should generate style array with Platform.select()
1754
+ expect(output).toContain("style: [");
1755
+ expect(output).toContain("Platform.select");
1756
+ expect(output).toContain("ios:");
1757
+ expect(output).toContain("_twStyles._ios_p_6");
1758
+ expect(output).toContain("_twStyles._bg_white");
1759
+
1760
+ // Should have StyleSheet.create
1761
+ expect(output).toContain("StyleSheet.create");
1762
+ });
1763
+
1764
+ it("should transform twStyle with android: modifier", () => {
1765
+ const input = `
1766
+ import { twStyle } from '@mgcrea/react-native-tailwind';
1767
+
1768
+ export const MyComponent = () => {
1769
+ const buttonStyles = twStyle('bg-blue-500 android:p-8');
1770
+ return null;
1771
+ };
1772
+ `;
1773
+
1774
+ const output = transform(input);
1775
+
1776
+ // Should add Platform import
1777
+ expect(output).toContain("Platform");
1778
+
1779
+ // Should generate style array with Platform.select()
1780
+ expect(output).toContain("style: [");
1781
+ expect(output).toContain("Platform.select");
1782
+ expect(output).toContain("android:");
1783
+ expect(output).toContain("_twStyles._android_p_8");
1784
+ expect(output).toContain("_twStyles._bg_blue_500");
1785
+ });
1786
+
1787
+ it("should transform tw with multiple platform modifiers", () => {
1788
+ const input = `
1789
+ import { tw } from '@mgcrea/react-native-tailwind';
1790
+
1791
+ function MyComponent() {
1792
+ const styles = tw\`bg-white ios:p-6 android:p-8 web:p-4\`;
1793
+ return null;
1794
+ }
1795
+ `;
1796
+
1797
+ const output = transform(input);
1798
+
1799
+ // Should generate Platform.select() with all platforms
1800
+ expect(output).toContain("Platform.select");
1801
+ expect(output).toContain("ios:");
1802
+ expect(output).toContain("android:");
1803
+ expect(output).toContain("web:");
1804
+ expect(output).toContain("_twStyles._ios_p_6");
1805
+ expect(output).toContain("_twStyles._android_p_8");
1806
+ expect(output).toContain("_twStyles._web_p_4");
1807
+ });
1808
+
1809
+ it("should combine platform modifiers with color-scheme modifiers", () => {
1810
+ const input = `
1811
+ import { tw } from '@mgcrea/react-native-tailwind';
1812
+
1813
+ function MyComponent() {
1814
+ const styles = tw\`bg-white ios:p-6 dark:bg-gray-900\`;
1815
+ return null;
1816
+ }
1817
+ `;
1818
+
1819
+ const output = transform(input);
1820
+
1821
+ // Should have both Platform and useColorScheme
1822
+ expect(output).toContain("Platform");
1823
+ expect(output).toContain("useColorScheme");
1824
+ expect(output).toContain("_twColorScheme");
1825
+
1826
+ // Should have both conditionals in style array
1827
+ expect(output).toContain("Platform.select");
1828
+ expect(output).toContain('_twColorScheme === "dark"');
1829
+ });
1830
+
1831
+ it("should generate iosStyle/androidStyle/webStyle properties for manual access", () => {
1832
+ const input = `
1833
+ import { tw } from '@mgcrea/react-native-tailwind';
1834
+
1835
+ function MyComponent() {
1836
+ const styles = tw\`bg-white ios:p-6 android:p-8 web:p-4\`;
1837
+ return null;
1838
+ }
1839
+ `;
1840
+
1841
+ const output = transform(input);
1842
+
1843
+ // Should have separate platform style properties
1844
+ expect(output).toContain("iosStyle:");
1845
+ expect(output).toContain("_twStyles._ios_p_6");
1846
+ expect(output).toContain("androidStyle:");
1847
+ expect(output).toContain("_twStyles._android_p_8");
1848
+ expect(output).toContain("webStyle:");
1849
+ expect(output).toContain("_twStyles._web_p_4");
1850
+
1851
+ // Should also have runtime Platform.select() in style array
1852
+ expect(output).toContain("Platform.select");
1853
+ });
1854
+
1855
+ it("should work with only platform modifiers (no base class)", () => {
1856
+ const input = `
1857
+ import { tw } from '@mgcrea/react-native-tailwind';
1858
+
1859
+ function MyComponent() {
1860
+ const styles = tw\`ios:p-6 android:p-8\`;
1861
+ return null;
1862
+ }
1863
+ `;
1864
+
1865
+ const output = transform(input);
1866
+
1867
+ // Should generate Platform.select() even without base classes
1868
+ expect(output).toContain("Platform.select");
1869
+ expect(output).toContain("_twStyles._ios_p_6");
1870
+ expect(output).toContain("_twStyles._android_p_8");
1871
+ });
1872
+
1873
+ it("should allow accessing platform-specific styles manually", () => {
1874
+ const input = `
1875
+ import { tw } from '@mgcrea/react-native-tailwind';
1876
+
1877
+ function MyComponent() {
1878
+ const btnStyles = tw\`bg-blue-500 ios:p-6\`;
1879
+ const iosPadding = btnStyles.iosStyle;
1880
+ return null;
1881
+ }
1882
+ `;
1883
+
1884
+ const output = transform(input);
1885
+
1886
+ // Should have iosStyle property available
1887
+ expect(output).toContain("iosStyle:");
1888
+ expect(output).toContain("_twStyles._ios_p_6");
1889
+
1890
+ // The actual usage line should be preserved
1891
+ expect(output).toContain("btnStyles.iosStyle");
1892
+ });
1893
+
1894
+ it("should combine state modifiers with platform modifiers", () => {
1895
+ const input = `
1896
+ import { tw } from '@mgcrea/react-native-tailwind';
1897
+
1898
+ function MyComponent() {
1899
+ const styles = tw\`bg-white active:bg-blue-500 ios:p-6\`;
1900
+ return null;
1901
+ }
1902
+ `;
1903
+
1904
+ const output = transform(input);
1905
+
1906
+ // Should have both activeStyle and platform modifiers
1907
+ expect(output).toContain("activeStyle:");
1908
+ expect(output).toContain("_twStyles._active_bg_blue_500");
1909
+ expect(output).toContain("Platform.select");
1910
+ expect(output).toContain("_twStyles._ios_p_6");
1911
+ });
1912
+ });