@@ -76,6 +76,35 @@ describe("className visitor - basic transformation", () => {
7676 expect ( output ) . toMatch ( / _ s t a t e \s * = > / ) ;
7777 } ) ;
7878
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+
79108 it ( "should merge dynamic className with function-based style prop" , ( ) => {
80109 const input = `
81110 import { TextInput } from 'react-native';
@@ -1310,3 +1339,287 @@ describe("className visitor - directional border colors", () => {
13101339 expect ( output ) . toMatch ( / i s E r r o r \s * \? \s * _ t w S t y l e s \. _ b o r d e r _ t _ r e d _ 5 0 0 / ) ;
13111340 } ) ;
13121341} ) ;
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 ( / _ t w I s R T L \s * & & \s * _ t w S t y l e s \. _ r t l _ m r _ 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 * _ t w I s R T L \s * & & \s * _ t w S t y l e s \. _ l t r _ m l _ 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 ( / _ t w I s R T L \s * & & \s * _ t w S t y l e s \. _ r t l _ m r _ 4 / ) ;
1405+ expect ( output ) . toMatch ( / ! \s * _ t w I s R T L \s * & & \s * _ t w S t y l e s \. _ l t r _ m l _ 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 ( / s t y l e : \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 ( / _ t w I s R T L \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 ( / I 1 8 n M a n a g e r / 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 ( / i m p o r t \s * \{ [ ^ } ] * I 1 8 n M a n a g e r [ ^ } ] * I 1 8 n M a n a g e r [ ^ } ] * \} / ) ;
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 ( / _ t w C o l o r S c h e m e \s * = = = \s * [ " ' ] d a r k [ " ' ] / ) ;
1517+ expect ( output ) . toMatch ( / _ t w I s R T L \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 ( / I 1 8 n M a n a g e r , | , \s * I 1 8 n M a n a g e r \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 ( / _ t w I s R T L \s * & & / ) ;
1605+ expect ( output ) . toMatch ( / ! \s * _ t w I s R T L \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+ } ) ;
0 commit comments