@mgcrea/react-native-tailwind 0.13.0 → 0.14.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.
Files changed (53) hide show
  1. package/README.md +21 -22
  2. package/dist/babel/index.cjs +342 -17
  3. package/dist/babel/plugin/state.d.ts +4 -0
  4. package/dist/babel/plugin/state.ts +8 -0
  5. package/dist/babel/plugin/visitors/className.test.ts +313 -0
  6. package/dist/babel/plugin/visitors/className.ts +36 -8
  7. package/dist/babel/plugin/visitors/imports.ts +16 -1
  8. package/dist/babel/plugin/visitors/program.ts +19 -2
  9. package/dist/babel/plugin/visitors/tw.test.ts +151 -0
  10. package/dist/babel/utils/directionalModifierProcessing.d.ts +34 -0
  11. package/dist/babel/utils/directionalModifierProcessing.ts +99 -0
  12. package/dist/babel/utils/styleInjection.d.ts +16 -0
  13. package/dist/babel/utils/styleInjection.ts +138 -7
  14. package/dist/babel/utils/twProcessing.d.ts +2 -0
  15. package/dist/babel/utils/twProcessing.ts +92 -3
  16. package/dist/parser/borders.js +1 -1
  17. package/dist/parser/borders.test.js +1 -1
  18. package/dist/parser/index.d.ts +2 -2
  19. package/dist/parser/index.js +1 -1
  20. package/dist/parser/layout.js +1 -1
  21. package/dist/parser/layout.test.js +1 -1
  22. package/dist/parser/modifiers.d.ts +32 -2
  23. package/dist/parser/modifiers.js +1 -1
  24. package/dist/parser/modifiers.test.js +1 -1
  25. package/dist/parser/spacing.d.ts +1 -1
  26. package/dist/parser/spacing.js +1 -1
  27. package/dist/parser/spacing.test.js +1 -1
  28. package/dist/parser/typography.test.js +1 -1
  29. package/dist/runtime.cjs +1 -1
  30. package/dist/runtime.cjs.map +3 -3
  31. package/dist/runtime.js +1 -1
  32. package/dist/runtime.js.map +3 -3
  33. package/package.json +6 -6
  34. package/src/babel/plugin/state.ts +8 -0
  35. package/src/babel/plugin/visitors/className.test.ts +313 -0
  36. package/src/babel/plugin/visitors/className.ts +36 -8
  37. package/src/babel/plugin/visitors/imports.ts +16 -1
  38. package/src/babel/plugin/visitors/program.ts +19 -2
  39. package/src/babel/plugin/visitors/tw.test.ts +151 -0
  40. package/src/babel/utils/directionalModifierProcessing.ts +99 -0
  41. package/src/babel/utils/styleInjection.ts +138 -7
  42. package/src/babel/utils/twProcessing.ts +92 -3
  43. package/src/parser/borders.test.ts +104 -0
  44. package/src/parser/borders.ts +50 -7
  45. package/src/parser/index.ts +2 -0
  46. package/src/parser/layout.test.ts +74 -0
  47. package/src/parser/layout.ts +94 -0
  48. package/src/parser/modifiers.test.ts +206 -0
  49. package/src/parser/modifiers.ts +62 -3
  50. package/src/parser/spacing.test.ts +66 -0
  51. package/src/parser/spacing.ts +15 -5
  52. package/src/parser/typography.test.ts +8 -0
  53. package/src/parser/typography.ts +4 -0
@@ -103,6 +103,10 @@ export type PluginState = PluginPass & {
103
103
  needsWindowDimensionsImport: boolean;
104
104
  windowDimensionsVariableName: string;
105
105
  windowDimensionsLocalIdentifier?: string; // Local identifier if hook is already imported with an alias
106
+ hasI18nManagerImport: boolean;
107
+ needsI18nManagerImport: boolean;
108
+ i18nManagerVariableName: string; // Variable name for the RTL state (e.g., '_twIsRTL')
109
+ i18nManagerLocalIdentifier?: string; // Local identifier if I18nManager is already imported with an alias
106
110
  customTheme: CustomTheme;
107
111
  schemeModifierConfig: SchemeModifierConfig;
108
112
  supportedAttributes: Set<string>;
@@ -163,6 +167,10 @@ export function createInitialState(
163
167
  needsWindowDimensionsImport: false,
164
168
  windowDimensionsVariableName: "_twDimensions",
165
169
  windowDimensionsLocalIdentifier: undefined,
170
+ hasI18nManagerImport: false,
171
+ needsI18nManagerImport: false,
172
+ i18nManagerVariableName: "_twIsRTL",
173
+ i18nManagerLocalIdentifier: undefined,
166
174
  customTheme,
167
175
  schemeModifierConfig,
168
176
  supportedAttributes: exactMatches,
@@ -76,6 +76,35 @@ describe("className visitor - basic transformation", () => {
76
76
  expect(output).toMatch(/_state\s*=>/);
77
77
  });
78
78
 
79
+ it("should preserve 'use client' directive when injecting StyleSheet.create", () => {
80
+ const input = `
81
+ 'use client';
82
+ import { View } from 'react-native';
83
+ export function Component() {
84
+ return <View className="m-4 p-2" />;
85
+ }
86
+ `;
87
+
88
+ const output = transform(input, undefined, true);
89
+
90
+ // 'use client' should be the first statement
91
+ const lines = output.split("\n").filter((l: string) => l.trim());
92
+ const useClientIndex = lines.findIndex(
93
+ (l: string) => l.includes("'use client'") || l.includes('"use client"'),
94
+ );
95
+ expect(useClientIndex).toBe(0);
96
+
97
+ // StyleSheet.create should be in the output
98
+ expect(output).toContain("StyleSheet.create");
99
+ expect(output).toContain("_twStyles");
100
+
101
+ // Imports should come after 'use client', before StyleSheet.create
102
+ const importIndex = lines.findIndex((l: string) => l.includes("import"));
103
+ const styleSheetIndex = lines.findIndex((l: string) => l.includes("StyleSheet.create"));
104
+ expect(importIndex).toBeGreaterThan(useClientIndex);
105
+ expect(styleSheetIndex).toBeGreaterThan(importIndex);
106
+ });
107
+
79
108
  it("should merge dynamic className with function-based style prop", () => {
80
109
  const input = `
81
110
  import { TextInput } from 'react-native';
@@ -1310,3 +1339,287 @@ describe("className visitor - directional border colors", () => {
1310
1339
  expect(output).toMatch(/isError\s*\?\s*_twStyles\._border_t_red_500/);
1311
1340
  });
1312
1341
  });
1342
+
1343
+ describe("className visitor - directional modifiers (RTL/LTR)", () => {
1344
+ it("should transform rtl: modifier and inject I18nManager import", () => {
1345
+ const input = `
1346
+ import { View } from 'react-native';
1347
+ export function Component() {
1348
+ return <View className="rtl:mr-4" />;
1349
+ }
1350
+ `;
1351
+
1352
+ const output = transform(input, undefined, true);
1353
+
1354
+ // Should import I18nManager
1355
+ expect(output).toContain("I18nManager");
1356
+
1357
+ // Should declare _twIsRTL variable
1358
+ expect(output).toContain("_twIsRTL");
1359
+ expect(output).toContain("I18nManager.isRTL");
1360
+
1361
+ // Should have StyleSheet with rtl style
1362
+ expect(output).toContain("StyleSheet.create");
1363
+ expect(output).toContain("_rtl_mr_4");
1364
+
1365
+ // Should have conditional for RTL
1366
+ expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/);
1367
+ });
1368
+
1369
+ it("should transform ltr: modifier with negated conditional", () => {
1370
+ const input = `
1371
+ import { View } from 'react-native';
1372
+ export function Component() {
1373
+ return <View className="ltr:ml-4" />;
1374
+ }
1375
+ `;
1376
+
1377
+ const output = transform(input, undefined, true);
1378
+
1379
+ // Should import I18nManager
1380
+ expect(output).toContain("I18nManager");
1381
+
1382
+ // Should have StyleSheet with ltr style
1383
+ expect(output).toContain("_ltr_ml_4");
1384
+
1385
+ // Should have negated conditional for LTR (!_twIsRTL)
1386
+ expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/);
1387
+ });
1388
+
1389
+ it("should combine rtl: and ltr: modifiers", () => {
1390
+ const input = `
1391
+ import { View } from 'react-native';
1392
+ export function Component() {
1393
+ return <View className="rtl:mr-4 ltr:ml-4" />;
1394
+ }
1395
+ `;
1396
+
1397
+ const output = transform(input, undefined, true);
1398
+
1399
+ // Should have both styles
1400
+ expect(output).toContain("_rtl_mr_4");
1401
+ expect(output).toContain("_ltr_ml_4");
1402
+
1403
+ // Should have both conditionals
1404
+ expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/);
1405
+ expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/);
1406
+ });
1407
+
1408
+ it("should combine directional modifiers with base classes", () => {
1409
+ const input = `
1410
+ import { View } from 'react-native';
1411
+ export function Component() {
1412
+ return <View className="p-4 bg-white rtl:pr-8 ltr:pl-8" />;
1413
+ }
1414
+ `;
1415
+
1416
+ const output = transform(input, undefined, true);
1417
+
1418
+ // Should have base style
1419
+ expect(output).toContain("_bg_white_p_4");
1420
+
1421
+ // Should have directional styles
1422
+ expect(output).toContain("_rtl_pr_8");
1423
+ expect(output).toContain("_ltr_pl_8");
1424
+
1425
+ // Should generate an array with base and conditional styles
1426
+ expect(output).toMatch(/style:\s*\[/);
1427
+ });
1428
+
1429
+ it("should combine directional modifiers with platform modifiers", () => {
1430
+ const input = `
1431
+ import { View } from 'react-native';
1432
+ export function Component() {
1433
+ return <View className="p-4 ios:p-6 rtl:mr-4" />;
1434
+ }
1435
+ `;
1436
+
1437
+ const output = transform(input, undefined, true);
1438
+
1439
+ // Should have Platform import
1440
+ expect(output).toContain("Platform");
1441
+
1442
+ // Should have I18nManager import
1443
+ expect(output).toContain("I18nManager");
1444
+
1445
+ // Should have all styles
1446
+ expect(output).toContain("_p_4");
1447
+ expect(output).toContain("_ios_p_6");
1448
+ expect(output).toContain("_rtl_mr_4");
1449
+
1450
+ // Should have Platform.select
1451
+ expect(output).toContain("Platform.select");
1452
+
1453
+ // Should have RTL conditional
1454
+ expect(output).toMatch(/_twIsRTL\s*&&/);
1455
+ });
1456
+
1457
+ it("should not add I18nManager import if already present", () => {
1458
+ const input = `
1459
+ import { View, I18nManager } from 'react-native';
1460
+ export function Component() {
1461
+ return <View className="rtl:mr-4" />;
1462
+ }
1463
+ `;
1464
+
1465
+ const output = transform(input, undefined, true);
1466
+
1467
+ // Should have only one I18nManager import (merged, not duplicated)
1468
+ const i18nMatches = output.match(/I18nManager/g);
1469
+ // Should have I18nManager in: import, variable declaration, and style usage
1470
+ expect(i18nMatches).toBeTruthy();
1471
+ // Should not have duplicate imports
1472
+ expect(output).not.toMatch(/import\s*\{[^}]*I18nManager[^}]*I18nManager[^}]*\}/);
1473
+ });
1474
+
1475
+ it("should work with directional logical properties", () => {
1476
+ const input = `
1477
+ import { View } from 'react-native';
1478
+ export function Component() {
1479
+ return <View className="rtl:ms-4 ltr:me-4" />;
1480
+ }
1481
+ `;
1482
+
1483
+ const output = transform(input, undefined, true);
1484
+
1485
+ // Should have logical property styles
1486
+ expect(output).toContain("_rtl_ms_4");
1487
+ expect(output).toContain("_ltr_me_4");
1488
+
1489
+ // Should contain marginStart and marginEnd in the StyleSheet
1490
+ expect(output).toContain("marginStart");
1491
+ expect(output).toContain("marginEnd");
1492
+ });
1493
+
1494
+ it("should combine directional modifiers with color scheme modifiers", () => {
1495
+ const input = `
1496
+ import { View } from 'react-native';
1497
+ export function Component() {
1498
+ return <View className="bg-white dark:bg-gray-900 rtl:pr-4" />;
1499
+ }
1500
+ `;
1501
+
1502
+ const output = transform(input, undefined, true);
1503
+
1504
+ // Should have useColorScheme
1505
+ expect(output).toContain("useColorScheme");
1506
+
1507
+ // Should have I18nManager
1508
+ expect(output).toContain("I18nManager");
1509
+
1510
+ // Should have all styles
1511
+ expect(output).toContain("_bg_white");
1512
+ expect(output).toContain("_dark_bg_gray_900");
1513
+ expect(output).toContain("_rtl_pr_4");
1514
+
1515
+ // Should have both conditionals
1516
+ expect(output).toMatch(/_twColorScheme\s*===\s*["']dark["']/);
1517
+ expect(output).toMatch(/_twIsRTL\s*&&/);
1518
+ });
1519
+
1520
+ it("should handle aliased I18nManager import", () => {
1521
+ const input = `
1522
+ import { View, I18nManager as RTL } from 'react-native';
1523
+ export function Component() {
1524
+ // Use RTL somewhere so TypeScript doesn't strip the unused import
1525
+ const isRtl = RTL.isRTL;
1526
+ return <View className="rtl:mr-4" />;
1527
+ }
1528
+ `;
1529
+
1530
+ const output = transform(input, undefined, true);
1531
+
1532
+ // Should use the aliased identifier RTL.isRTL instead of I18nManager.isRTL
1533
+ expect(output).toContain("RTL.isRTL");
1534
+ // Should preserve the aliased import
1535
+ expect(output).toContain("I18nManager as RTL");
1536
+ // Should not add a separate I18nManager import without alias
1537
+ expect(output).not.toMatch(/I18nManager,|,\s*I18nManager\s*[,}]/);
1538
+ });
1539
+
1540
+ it("should preserve 'use client' directive when injecting I18nManager variable", () => {
1541
+ const input = `
1542
+ 'use client';
1543
+ import { View } from 'react-native';
1544
+ export function Component() {
1545
+ return <View className="rtl:mr-4" />;
1546
+ }
1547
+ `;
1548
+
1549
+ const output = transform(input, undefined, true);
1550
+
1551
+ // 'use client' should be the first statement
1552
+ const lines = output.split("\n").filter((l: string) => l.trim());
1553
+ const useClientIndex = lines.findIndex(
1554
+ (l: string) => l.includes("'use client'") || l.includes('"use client"'),
1555
+ );
1556
+ expect(useClientIndex).toBe(0);
1557
+
1558
+ // I18nManager variable should come after imports, not before 'use client'
1559
+ expect(output).toContain("_twIsRTL");
1560
+ expect(output).toContain("I18nManager.isRTL");
1561
+ });
1562
+
1563
+ it("should preserve 'use strict' directive when injecting I18nManager variable", () => {
1564
+ const input = `
1565
+ 'use strict';
1566
+ import { View } from 'react-native';
1567
+ export function Component() {
1568
+ return <View className="rtl:mr-4" />;
1569
+ }
1570
+ `;
1571
+
1572
+ const output = transform(input, undefined, true);
1573
+
1574
+ // 'use strict' should be preserved at the top
1575
+ const lines = output.split("\n").filter((l: string) => l.trim());
1576
+ const useStrictIndex = lines.findIndex(
1577
+ (l: string) => l.includes("'use strict'") || l.includes('"use strict"'),
1578
+ );
1579
+ expect(useStrictIndex).toBe(0);
1580
+
1581
+ // I18nManager variable should work correctly
1582
+ expect(output).toContain("_twIsRTL");
1583
+ expect(output).toContain("I18nManager.isRTL");
1584
+ });
1585
+
1586
+ it("should expand text-start to directional modifiers", () => {
1587
+ const input = `
1588
+ import { Text } from 'react-native';
1589
+ export function Component() {
1590
+ return <Text className="text-start" />;
1591
+ }
1592
+ `;
1593
+
1594
+ const output = transform(input, undefined, true);
1595
+
1596
+ // Should have I18nManager import (text-start expands to ltr:/rtl: modifiers)
1597
+ expect(output).toContain("I18nManager");
1598
+
1599
+ // Should have both ltr and rtl styles
1600
+ expect(output).toContain("_ltr_text_left");
1601
+ expect(output).toContain("_rtl_text_right");
1602
+
1603
+ // Should have conditionals for both
1604
+ expect(output).toMatch(/_twIsRTL\s*&&/);
1605
+ expect(output).toMatch(/!\s*_twIsRTL\s*&&/);
1606
+ });
1607
+
1608
+ it("should expand text-end to directional modifiers", () => {
1609
+ const input = `
1610
+ import { Text } from 'react-native';
1611
+ export function Component() {
1612
+ return <Text className="text-end" />;
1613
+ }
1614
+ `;
1615
+
1616
+ const output = transform(input, undefined, true);
1617
+
1618
+ // Should have I18nManager import
1619
+ expect(output).toContain("I18nManager");
1620
+
1621
+ // text-end expands to ltr:text-right rtl:text-left
1622
+ expect(output).toContain("_ltr_text_right");
1623
+ expect(output).toContain("_rtl_text_left");
1624
+ });
1625
+ });
@@ -8,6 +8,7 @@ import type { ParsedModifier, StateModifierType } from "../../../parser/index.js
8
8
  import {
9
9
  expandSchemeModifier,
10
10
  isColorSchemeModifier,
11
+ isDirectionalModifier,
11
12
  isPlatformModifier,
12
13
  isSchemeModifier,
13
14
  isStateModifier,
@@ -19,6 +20,7 @@ import { generateStyleKey } from "../../../utils/styleKey.js";
19
20
  import { getTargetStyleProp, isAttributeSupported } from "../../utils/attributeMatchers.js";
20
21
  import { processColorSchemeModifiers } from "../../utils/colorSchemeModifierProcessing.js";
21
22
  import { getComponentModifierSupport, getStatePropertyForModifier } from "../../utils/componentSupport.js";
23
+ import { processDirectionalModifiers } from "../../utils/directionalModifierProcessing.js";
22
24
  import { processDynamicExpression } from "../../utils/dynamicProcessing.js";
23
25
  import { createStyleFunction, processStaticClassNameWithModifiers } from "../../utils/modifierProcessing.js";
24
26
  import { processPlatformModifiers } from "../../utils/platformModifierProcessing.js";
@@ -107,6 +109,7 @@ export function jsxAttributeVisitor(
107
109
  const placeholderModifiers = modifierClasses.filter((m) => m.modifier === "placeholder");
108
110
  const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
109
111
  const colorSchemeModifiers = modifierClasses.filter((m) => isColorSchemeModifier(m.modifier));
112
+ const directionalModifiers = modifierClasses.filter((m) => isDirectionalModifier(m.modifier));
110
113
  const stateModifiers = modifierClasses.filter(
111
114
  (m) => isStateModifier(m.modifier) && m.modifier !== "placeholder",
112
115
  );
@@ -138,6 +141,7 @@ export function jsxAttributeVisitor(
138
141
  // Handle combination of modifiers
139
142
  const hasPlatformModifiers = platformModifiers.length > 0;
140
143
  const hasColorSchemeModifiers = colorSchemeModifiers.length > 0;
144
+ const hasDirectionalModifiers = directionalModifiers.length > 0;
141
145
  const hasStateModifiers = stateModifiers.length > 0;
142
146
  const hasBaseClasses = baseClasses.length > 0;
143
147
 
@@ -160,14 +164,14 @@ export function jsxAttributeVisitor(
160
164
  }
161
165
 
162
166
  // If we have multiple modifier types, combine them in an array expression
163
- // For state modifiers, wrap in arrow function; for color scheme, they're just conditionals
164
- if (hasStateModifiers && (hasPlatformModifiers || hasColorSchemeModifiers)) {
167
+ // For state modifiers, wrap in arrow function; for color scheme and directional, they're just conditionals
168
+ if (hasStateModifiers && (hasPlatformModifiers || hasColorSchemeModifiers || hasDirectionalModifiers)) {
165
169
  // Get the JSX opening element for component support checking
166
170
  const jsxOpeningElement = path.parent;
167
171
  const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
168
172
 
169
173
  if (componentSupport) {
170
- // Build style array: [baseStyle, Platform.select(...), colorSchemeConditionals, stateConditionals]
174
+ // Build style array: [baseStyle, Platform.select(...), colorSchemeConditionals, directionalConditionals, stateConditionals]
171
175
  const styleArrayElements: BabelTypes.Expression[] = [];
172
176
 
173
177
  // Add base classes
@@ -179,7 +183,7 @@ export function jsxAttributeVisitor(
179
183
  if (hasRuntimeDimensions(baseStyleObject)) {
180
184
  throw path.buildCodeFrameError(
181
185
  `w-screen and h-screen cannot be combined with modifiers. ` +
182
- `Found: "${baseClassName}" with state, platform, or color scheme modifiers. ` +
186
+ `Found: "${baseClassName}" with state, platform, color scheme, or directional modifiers. ` +
183
187
  `Use w-screen/h-screen without modifiers instead.`,
184
188
  );
185
189
  }
@@ -215,6 +219,18 @@ export function jsxAttributeVisitor(
215
219
  styleArrayElements.push(...colorSchemeConditionals);
216
220
  }
217
221
 
222
+ // Add directional modifiers as conditionals
223
+ if (hasDirectionalModifiers) {
224
+ const directionalConditionals = processDirectionalModifiers(
225
+ directionalModifiers,
226
+ state,
227
+ parseClassName,
228
+ generateStyleKey,
229
+ t,
230
+ );
231
+ styleArrayElements.push(...directionalConditionals);
232
+ }
233
+
218
234
  // Add state modifiers as conditionals
219
235
  // Group by modifier type
220
236
  const modifiersByType = new Map<StateModifierType, ParsedModifier[]>();
@@ -267,9 +283,9 @@ export function jsxAttributeVisitor(
267
283
  }
268
284
  }
269
285
 
270
- // Handle platform and/or color scheme modifiers (no state modifiers)
271
- if ((hasPlatformModifiers || hasColorSchemeModifiers) && !hasStateModifiers) {
272
- // Build style array/expression: [baseStyle, Platform.select(...), colorSchemeConditionals]
286
+ // Handle platform, color scheme, and/or directional modifiers (no state modifiers)
287
+ if ((hasPlatformModifiers || hasColorSchemeModifiers || hasDirectionalModifiers) && !hasStateModifiers) {
288
+ // Build style array/expression: [baseStyle, Platform.select(...), colorSchemeConditionals, directionalConditionals]
273
289
  const styleExpressions: BabelTypes.Expression[] = [];
274
290
 
275
291
  // Add base classes
@@ -281,7 +297,7 @@ export function jsxAttributeVisitor(
281
297
  if (hasRuntimeDimensions(baseStyleObject)) {
282
298
  throw path.buildCodeFrameError(
283
299
  `w-screen and h-screen cannot be combined with modifiers. ` +
284
- `Found: "${baseClassName}" with platform or color scheme modifiers. ` +
300
+ `Found: "${baseClassName}" with platform, color scheme, or directional modifiers. ` +
285
301
  `Use w-screen/h-screen without modifiers instead.`,
286
302
  );
287
303
  }
@@ -317,6 +333,18 @@ export function jsxAttributeVisitor(
317
333
  styleExpressions.push(...colorSchemeConditionals);
318
334
  }
319
335
 
336
+ // Add directional modifiers as conditionals
337
+ if (hasDirectionalModifiers) {
338
+ const directionalConditionals = processDirectionalModifiers(
339
+ directionalModifiers,
340
+ state,
341
+ parseClassName,
342
+ generateStyleKey,
343
+ t,
344
+ );
345
+ styleExpressions.push(...directionalConditionals);
346
+ }
347
+
320
348
  // Generate style attribute
321
349
  const styleExpression =
322
350
  styleExpressions.length === 1 ? styleExpressions[0] : t.arrayExpression(styleExpressions);
@@ -17,7 +17,7 @@ export function importDeclarationVisitor(
17
17
  ): void {
18
18
  const node = path.node;
19
19
 
20
- // Track react-native StyleSheet and Platform imports
20
+ // Track react-native StyleSheet, Platform, and I18nManager imports
21
21
  if (node.source.value === "react-native") {
22
22
  const specifiers = node.specifiers;
23
23
 
@@ -35,6 +35,21 @@ export function importDeclarationVisitor(
35
35
  return false;
36
36
  });
37
37
 
38
+ // Check for I18nManager import (only value imports, not type-only)
39
+ if (node.importKind !== "type") {
40
+ for (const spec of specifiers) {
41
+ if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
42
+ if (spec.imported.name === "I18nManager") {
43
+ state.hasI18nManagerImport = true;
44
+ // Track the local identifier (handles aliased imports)
45
+ // e.g., import { I18nManager as RTL } → local name is 'RTL'
46
+ state.i18nManagerLocalIdentifier = spec.local.name;
47
+ break;
48
+ }
49
+ }
50
+ }
51
+ }
52
+
38
53
  // Check for useWindowDimensions import (only value imports, not type-only)
39
54
  if (node.importKind !== "type") {
40
55
  for (const spec of specifiers) {
@@ -6,10 +6,12 @@ import type { NodePath } from "@babel/core";
6
6
  import type * as BabelTypes from "@babel/types";
7
7
  import {
8
8
  addColorSchemeImport,
9
+ addI18nManagerImport,
9
10
  addPlatformImport,
10
11
  addStyleSheetImport,
11
12
  addWindowDimensionsImport,
12
13
  injectColorSchemeHook,
14
+ injectI18nManagerVariable,
13
15
  injectStylesAtTop,
14
16
  injectWindowDimensionsHook,
15
17
  } from "../../utils/styleInjection.js";
@@ -39,8 +41,13 @@ export function programExit(
39
41
  removeTwImports(path, t);
40
42
  }
41
43
 
42
- // If no classNames were found and no hooks needed, skip processing
43
- if (!state.hasClassNames && !state.needsWindowDimensionsImport && !state.needsColorSchemeImport) {
44
+ // If no classNames were found and no hooks/imports needed, skip processing
45
+ if (
46
+ !state.hasClassNames &&
47
+ !state.needsWindowDimensionsImport &&
48
+ !state.needsColorSchemeImport &&
49
+ !state.needsI18nManagerImport
50
+ ) {
44
51
  return;
45
52
  }
46
53
 
@@ -54,6 +61,16 @@ export function programExit(
54
61
  addPlatformImport(path, t);
55
62
  }
56
63
 
64
+ // Add I18nManager import if directional modifiers were used and not already present
65
+ if (state.needsI18nManagerImport && !state.hasI18nManagerImport) {
66
+ addI18nManagerImport(path, t);
67
+ }
68
+
69
+ // Inject I18nManager.isRTL variable at module level (not a hook, so no component scope needed)
70
+ if (state.needsI18nManagerImport) {
71
+ injectI18nManagerVariable(path, state.i18nManagerVariableName, state.i18nManagerLocalIdentifier, t);
72
+ }
73
+
57
74
  // Add color scheme hook import if color scheme modifiers were used and not already present
58
75
  if (state.needsColorSchemeImport && !state.hasColorSchemeImport) {
59
76
  addColorSchemeImport(path, state.colorSchemeImportSource, state.colorSchemeHookName, t);
@@ -618,3 +618,154 @@ describe("tw/twStyle - integration with className", () => {
618
618
  expect(output).toContain("_m_4_p_2");
619
619
  });
620
620
  });
621
+
622
+ describe("tw visitor - directional modifiers (RTL/LTR)", () => {
623
+ it("should transform rtl: modifier in tw template", () => {
624
+ const input = `
625
+ import { tw } from '@mgcrea/react-native-tailwind';
626
+
627
+ function MyComponent() {
628
+ const styles = tw\`p-4 rtl:mr-4\`;
629
+ return null;
630
+ }
631
+ `;
632
+
633
+ const output = transform(input);
634
+
635
+ // Should import I18nManager
636
+ expect(output).toContain("I18nManager");
637
+
638
+ // Should declare _twIsRTL variable
639
+ expect(output).toContain("_twIsRTL");
640
+ expect(output).toContain("I18nManager.isRTL");
641
+
642
+ // Should have style array with conditional
643
+ expect(output).toContain("style:");
644
+ expect(output).toContain("_twStyles._p_4");
645
+ expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/);
646
+
647
+ // Should have rtlStyle property
648
+ expect(output).toContain("rtlStyle:");
649
+ expect(output).toContain("_twStyles._rtl_mr_4");
650
+ });
651
+
652
+ it("should transform ltr: modifier with negated conditional", () => {
653
+ const input = `
654
+ import { tw } from '@mgcrea/react-native-tailwind';
655
+
656
+ function MyComponent() {
657
+ const styles = tw\`p-4 ltr:ml-4\`;
658
+ return null;
659
+ }
660
+ `;
661
+
662
+ const output = transform(input);
663
+
664
+ // Should import I18nManager
665
+ expect(output).toContain("I18nManager");
666
+
667
+ // Should have negated conditional for LTR (!_twIsRTL)
668
+ expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/);
669
+
670
+ // Should have ltrStyle property
671
+ expect(output).toContain("ltrStyle:");
672
+ });
673
+
674
+ it("should combine rtl: and ltr: modifiers", () => {
675
+ const input = `
676
+ import { tw } from '@mgcrea/react-native-tailwind';
677
+
678
+ function MyComponent() {
679
+ const styles = tw\`rtl:mr-4 ltr:ml-4\`;
680
+ return null;
681
+ }
682
+ `;
683
+
684
+ const output = transform(input);
685
+
686
+ // Should have both conditionals
687
+ expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/);
688
+ expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/);
689
+
690
+ // Should have both style properties
691
+ expect(output).toContain("rtlStyle:");
692
+ expect(output).toContain("ltrStyle:");
693
+ });
694
+
695
+ it("should combine directional modifiers with platform modifiers", () => {
696
+ const input = `
697
+ import { tw } from '@mgcrea/react-native-tailwind';
698
+
699
+ function MyComponent() {
700
+ const styles = tw\`p-4 ios:p-6 rtl:mr-4\`;
701
+ return null;
702
+ }
703
+ `;
704
+
705
+ const output = transform(input);
706
+
707
+ // Should have Platform import
708
+ expect(output).toContain("Platform");
709
+
710
+ // Should have I18nManager import
711
+ expect(output).toContain("I18nManager");
712
+
713
+ // Should have both modifiers in style array
714
+ expect(output).toContain("Platform.select");
715
+ expect(output).toMatch(/_twIsRTL\s*&&/);
716
+
717
+ // Should have iosStyle and rtlStyle properties
718
+ expect(output).toContain("iosStyle:");
719
+ expect(output).toContain("rtlStyle:");
720
+ });
721
+
722
+ it("should combine directional modifiers with state modifiers", () => {
723
+ const input = `
724
+ import { tw } from '@mgcrea/react-native-tailwind';
725
+
726
+ function MyComponent() {
727
+ const styles = tw\`bg-white active:bg-blue-500 rtl:pr-4\`;
728
+ return null;
729
+ }
730
+ `;
731
+
732
+ const output = transform(input);
733
+
734
+ // Should have I18nManager import
735
+ expect(output).toContain("I18nManager");
736
+
737
+ // Should have directional conditional
738
+ expect(output).toMatch(/_twIsRTL\s*&&/);
739
+
740
+ // Should have activeStyle property
741
+ expect(output).toContain("activeStyle:");
742
+ expect(output).toContain("_twStyles._active_bg_blue_500");
743
+
744
+ // Should have rtlStyle property
745
+ expect(output).toContain("rtlStyle:");
746
+ });
747
+
748
+ it("should work with twStyle function for RTL modifiers", () => {
749
+ const input = `
750
+ import { twStyle } from '@mgcrea/react-native-tailwind';
751
+
752
+ function MyComponent() {
753
+ const styles = twStyle('p-4 rtl:mr-4 ltr:ml-4');
754
+ return null;
755
+ }
756
+ `;
757
+
758
+ const output = transform(input);
759
+
760
+ // Should import I18nManager
761
+ expect(output).toContain("I18nManager");
762
+
763
+ // Should have both conditionals
764
+ expect(output).toMatch(/_twIsRTL\s*&&/);
765
+ expect(output).toMatch(/!\s*_twIsRTL\s*&&/);
766
+
767
+ // Should have both style properties
768
+ expect(output).toContain("rtlStyle:");
769
+ expect(output).toContain("ltrStyle:");
770
+ });
771
+ });