@mgcrea/react-native-tailwind 0.11.1 → 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.
- package/dist/babel/index.cjs +189 -20
- package/dist/babel/plugin.test.ts +498 -0
- package/dist/babel/plugin.ts +55 -16
- package/dist/babel/utils/twProcessing.d.ts +8 -1
- package/dist/babel/utils/twProcessing.ts +212 -4
- 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/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +2 -2
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +2 -2
- package/dist/runtime.test.js +1 -1
- package/dist/types/runtime.d.ts +8 -1
- package/package.json +1 -1
- package/src/babel/plugin.test.ts +498 -0
- package/src/babel/plugin.ts +55 -16
- package/src/babel/utils/twProcessing.ts +212 -4
- package/src/parser/spacing.test.ts +62 -0
- package/src/parser/spacing.ts +7 -7
- package/src/runtime.test.ts +4 -1
- package/src/types/runtime.ts +8 -1
|
@@ -334,6 +334,89 @@ describe("Babel plugin - className transformation (existing behavior)", () => {
|
|
|
334
334
|
// Should not have className in output
|
|
335
335
|
expect(output).not.toContain("className");
|
|
336
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
|
+
});
|
|
337
420
|
});
|
|
338
421
|
|
|
339
422
|
describe("Babel plugin - placeholder: modifier transformation", () => {
|
|
@@ -1250,6 +1333,34 @@ describe("Babel plugin - custom color scheme hook import", () => {
|
|
|
1250
1333
|
expect(output).not.toContain("_twColorScheme = useTheme()");
|
|
1251
1334
|
});
|
|
1252
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
|
+
|
|
1253
1364
|
it("should handle both type-only and aliased imports together", () => {
|
|
1254
1365
|
const input = `
|
|
1255
1366
|
import React from 'react';
|
|
@@ -1412,3 +1523,390 @@ describe("Babel plugin - scheme: modifier", () => {
|
|
|
1412
1523
|
expect(output).toContain("_twColorScheme === 'light'");
|
|
1413
1524
|
});
|
|
1414
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
|
+
});
|
package/dist/babel/plugin.ts
CHANGED
|
@@ -276,6 +276,10 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
276
276
|
state.twImportNames = new Set();
|
|
277
277
|
state.hasTwImport = false;
|
|
278
278
|
state.functionComponentsNeedingColorScheme = new Set();
|
|
279
|
+
state.hasColorSchemeImport = false;
|
|
280
|
+
state.colorSchemeLocalIdentifier = undefined;
|
|
281
|
+
state.needsPlatformImport = false;
|
|
282
|
+
state.hasPlatformImport = false;
|
|
279
283
|
|
|
280
284
|
// Load custom theme from tailwind.config.*
|
|
281
285
|
state.customTheme = extractCustomTheme(state.file.opts.filename ?? "");
|
|
@@ -367,7 +371,8 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
367
371
|
|
|
368
372
|
// Track color scheme hook import from the configured source
|
|
369
373
|
// (default: react-native, but can be custom like @/hooks/useColorScheme)
|
|
370
|
-
|
|
374
|
+
// Only track value imports (not type-only imports which get erased)
|
|
375
|
+
if (node.source.value === state.colorSchemeImportSource && node.importKind !== "type") {
|
|
371
376
|
const specifiers = node.specifiers;
|
|
372
377
|
|
|
373
378
|
for (const spec of specifiers) {
|
|
@@ -444,7 +449,16 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
444
449
|
state.hasClassNames = true;
|
|
445
450
|
|
|
446
451
|
// Process the className with modifiers
|
|
447
|
-
processTwCall(
|
|
452
|
+
processTwCall(
|
|
453
|
+
className,
|
|
454
|
+
path,
|
|
455
|
+
state,
|
|
456
|
+
parseClassName,
|
|
457
|
+
generateStyleKey,
|
|
458
|
+
splitModifierClasses,
|
|
459
|
+
findComponentScope,
|
|
460
|
+
t,
|
|
461
|
+
);
|
|
448
462
|
},
|
|
449
463
|
|
|
450
464
|
// Handle twStyle('...') call expressions
|
|
@@ -494,7 +508,16 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
494
508
|
state.hasClassNames = true;
|
|
495
509
|
|
|
496
510
|
// Process the className with modifiers
|
|
497
|
-
processTwCall(
|
|
511
|
+
processTwCall(
|
|
512
|
+
className,
|
|
513
|
+
path,
|
|
514
|
+
state,
|
|
515
|
+
parseClassName,
|
|
516
|
+
generateStyleKey,
|
|
517
|
+
splitModifierClasses,
|
|
518
|
+
findComponentScope,
|
|
519
|
+
t,
|
|
520
|
+
);
|
|
498
521
|
},
|
|
499
522
|
|
|
500
523
|
JSXAttribute(path, state) {
|
|
@@ -517,20 +540,22 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
517
540
|
// Determine target style prop based on attribute name
|
|
518
541
|
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
519
542
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
543
|
+
/**
|
|
544
|
+
* Process static className string (handles both direct StringLiteral and StringLiteral in JSXExpressionContainer)
|
|
545
|
+
*/
|
|
546
|
+
const processStaticClassName = (className: string): boolean => {
|
|
547
|
+
const trimmedClassName = className.trim();
|
|
523
548
|
|
|
524
549
|
// Skip empty classNames
|
|
525
|
-
if (!
|
|
550
|
+
if (!trimmedClassName) {
|
|
526
551
|
path.remove();
|
|
527
|
-
return;
|
|
552
|
+
return true;
|
|
528
553
|
}
|
|
529
554
|
|
|
530
555
|
state.hasClassNames = true;
|
|
531
556
|
|
|
532
557
|
// Check if className contains modifiers (active:, hover:, focus:, placeholder:, ios:, android:, web:, dark:, light:, scheme:)
|
|
533
|
-
const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(
|
|
558
|
+
const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(trimmedClassName);
|
|
534
559
|
|
|
535
560
|
// Expand scheme: modifiers into dark: and light: modifiers
|
|
536
561
|
const modifierClasses: ParsedModifier[] = [];
|
|
@@ -697,7 +722,7 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
697
722
|
} else {
|
|
698
723
|
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
699
724
|
}
|
|
700
|
-
return;
|
|
725
|
+
return true;
|
|
701
726
|
} else {
|
|
702
727
|
// Component doesn't support state modifiers, but we can still use platform modifiers
|
|
703
728
|
// Fall through to platform-only handling
|
|
@@ -771,7 +796,7 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
771
796
|
path.node.name = t.jsxIdentifier(targetStyleProp);
|
|
772
797
|
path.node.value = t.jsxExpressionContainer(styleExpression);
|
|
773
798
|
}
|
|
774
|
-
return;
|
|
799
|
+
return true;
|
|
775
800
|
}
|
|
776
801
|
|
|
777
802
|
// If there are state modifiers (and no platform modifiers), check if this component supports them
|
|
@@ -829,12 +854,12 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
829
854
|
} else {
|
|
830
855
|
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
831
856
|
}
|
|
832
|
-
return;
|
|
857
|
+
return true;
|
|
833
858
|
}
|
|
834
859
|
} else {
|
|
835
860
|
// All modifiers are supported - process normally
|
|
836
861
|
const styleExpression = processStaticClassNameWithModifiers(
|
|
837
|
-
|
|
862
|
+
trimmedClassName,
|
|
838
863
|
state,
|
|
839
864
|
parseClassName,
|
|
840
865
|
generateStyleKey,
|
|
@@ -851,7 +876,7 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
851
876
|
} else {
|
|
852
877
|
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
853
878
|
}
|
|
854
|
-
return;
|
|
879
|
+
return true;
|
|
855
880
|
}
|
|
856
881
|
} else {
|
|
857
882
|
// Component doesn't support any modifiers
|
|
@@ -871,7 +896,7 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
871
896
|
if (!classNameForStyle) {
|
|
872
897
|
// No base classes, only had placeholder modifiers - just remove className
|
|
873
898
|
path.remove();
|
|
874
|
-
return;
|
|
899
|
+
return true;
|
|
875
900
|
}
|
|
876
901
|
|
|
877
902
|
const styleObject = parseClassName(classNameForStyle, state.customTheme);
|
|
@@ -888,7 +913,14 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
888
913
|
// Replace className with style prop
|
|
889
914
|
replaceWithStyleAttribute(path, styleKey, targetStyleProp, state.stylesIdentifier, t);
|
|
890
915
|
}
|
|
891
|
-
return;
|
|
916
|
+
return true;
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// Handle static string literals
|
|
920
|
+
if (t.isStringLiteral(value)) {
|
|
921
|
+
if (processStaticClassName(value.value)) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
892
924
|
}
|
|
893
925
|
|
|
894
926
|
// Handle dynamic expressions (JSXExpressionContainer)
|
|
@@ -900,6 +932,13 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
900
932
|
return;
|
|
901
933
|
}
|
|
902
934
|
|
|
935
|
+
// Fast path: Support string literals wrapped in JSXExpressionContainer: className={"flex-row"}
|
|
936
|
+
if (t.isStringLiteral(expression)) {
|
|
937
|
+
if (processStaticClassName(expression.value)) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
903
942
|
try {
|
|
904
943
|
// Find component scope for color scheme modifiers
|
|
905
944
|
const componentScope = findComponentScope(path, t);
|