@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.
- package/README.md +21 -22
- package/dist/babel/index.cjs +342 -17
- package/dist/babel/plugin/state.d.ts +4 -0
- package/dist/babel/plugin/state.ts +8 -0
- package/dist/babel/plugin/visitors/className.test.ts +313 -0
- package/dist/babel/plugin/visitors/className.ts +36 -8
- package/dist/babel/plugin/visitors/imports.ts +16 -1
- package/dist/babel/plugin/visitors/program.ts +19 -2
- package/dist/babel/plugin/visitors/tw.test.ts +151 -0
- package/dist/babel/utils/directionalModifierProcessing.d.ts +34 -0
- package/dist/babel/utils/directionalModifierProcessing.ts +99 -0
- package/dist/babel/utils/styleInjection.d.ts +16 -0
- package/dist/babel/utils/styleInjection.ts +138 -7
- package/dist/babel/utils/twProcessing.d.ts +2 -0
- package/dist/babel/utils/twProcessing.ts +92 -3
- package/dist/parser/borders.js +1 -1
- package/dist/parser/borders.test.js +1 -1
- package/dist/parser/index.d.ts +2 -2
- package/dist/parser/index.js +1 -1
- package/dist/parser/layout.js +1 -1
- package/dist/parser/layout.test.js +1 -1
- package/dist/parser/modifiers.d.ts +32 -2
- package/dist/parser/modifiers.js +1 -1
- package/dist/parser/modifiers.test.js +1 -1
- package/dist/parser/spacing.d.ts +1 -1
- package/dist/parser/spacing.js +1 -1
- package/dist/parser/spacing.test.js +1 -1
- package/dist/parser/typography.test.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +3 -3
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +3 -3
- package/package.json +6 -6
- package/src/babel/plugin/state.ts +8 -0
- package/src/babel/plugin/visitors/className.test.ts +313 -0
- package/src/babel/plugin/visitors/className.ts +36 -8
- package/src/babel/plugin/visitors/imports.ts +16 -1
- package/src/babel/plugin/visitors/program.ts +19 -2
- package/src/babel/plugin/visitors/tw.test.ts +151 -0
- package/src/babel/utils/directionalModifierProcessing.ts +99 -0
- package/src/babel/utils/styleInjection.ts +138 -7
- package/src/babel/utils/twProcessing.ts +92 -3
- package/src/parser/borders.test.ts +104 -0
- package/src/parser/borders.ts +50 -7
- package/src/parser/index.ts +2 -0
- package/src/parser/layout.test.ts +74 -0
- package/src/parser/layout.ts +94 -0
- package/src/parser/modifiers.test.ts +206 -0
- package/src/parser/modifiers.ts +62 -3
- package/src/parser/spacing.test.ts +66 -0
- package/src/parser/spacing.ts +15 -5
- package/src/parser/typography.test.ts +8 -0
- 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,
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
+
});
|