@mgcrea/react-native-tailwind 0.9.1 → 0.10.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 (39) hide show
  1. package/README.md +356 -30
  2. package/dist/babel/config-loader.test.ts +152 -0
  3. package/dist/babel/index.cjs +547 -47
  4. package/dist/babel/plugin.d.ts +21 -0
  5. package/dist/babel/plugin.test.ts +331 -0
  6. package/dist/babel/plugin.ts +258 -28
  7. package/dist/babel/utils/colorSchemeModifierProcessing.d.ts +34 -0
  8. package/dist/babel/utils/colorSchemeModifierProcessing.ts +89 -0
  9. package/dist/babel/utils/dynamicProcessing.d.ts +33 -2
  10. package/dist/babel/utils/dynamicProcessing.ts +352 -33
  11. package/dist/babel/utils/styleInjection.d.ts +13 -0
  12. package/dist/babel/utils/styleInjection.ts +101 -0
  13. package/dist/babel/utils/styleTransforms.test.ts +56 -0
  14. package/dist/babel/utils/twProcessing.d.ts +2 -0
  15. package/dist/babel/utils/twProcessing.ts +22 -1
  16. package/dist/parser/index.d.ts +2 -2
  17. package/dist/parser/index.js +1 -1
  18. package/dist/parser/modifiers.d.ts +48 -2
  19. package/dist/parser/modifiers.js +1 -1
  20. package/dist/parser/modifiers.test.js +1 -1
  21. package/dist/runtime.cjs +1 -1
  22. package/dist/runtime.cjs.map +3 -3
  23. package/dist/runtime.js +1 -1
  24. package/dist/runtime.js.map +3 -3
  25. package/dist/types/config.d.ts +7 -0
  26. package/dist/types/config.js +0 -0
  27. package/package.json +3 -2
  28. package/src/babel/config-loader.test.ts +152 -0
  29. package/src/babel/plugin.test.ts +331 -0
  30. package/src/babel/plugin.ts +258 -28
  31. package/src/babel/utils/colorSchemeModifierProcessing.ts +89 -0
  32. package/src/babel/utils/dynamicProcessing.ts +352 -33
  33. package/src/babel/utils/styleInjection.ts +101 -0
  34. package/src/babel/utils/styleTransforms.test.ts +56 -0
  35. package/src/babel/utils/twProcessing.ts +22 -1
  36. package/src/parser/index.ts +12 -1
  37. package/src/parser/modifiers.test.ts +151 -1
  38. package/src/parser/modifiers.ts +139 -4
  39. package/src/types/config.ts +7 -0
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Configuration for the scheme: modifier
3
+ */
4
+ export type SchemeModifierConfig = {
5
+ darkSuffix?: string;
6
+ lightSuffix?: string;
7
+ };
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgcrea/react-native-tailwind",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Compile-time Tailwind CSS for React Native with zero runtime overhead",
5
5
  "author": "Olivier Louvignes <olivier@mgcrea.io> (https://github.com/mgcrea)",
6
6
  "homepage": "https://github.com/mgcrea/react-native-tailwind#readme",
@@ -90,7 +90,8 @@
90
90
  },
91
91
  "pnpm": {
92
92
  "onlyBuiltDependencies": [
93
- "esbuild"
93
+ "esbuild",
94
+ "unrs-resolver"
94
95
  ]
95
96
  }
96
97
  }
@@ -0,0 +1,152 @@
1
+ import * as fs from "fs";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { extractCustomColors, findTailwindConfig, loadTailwindConfig } from "./config-loader";
4
+
5
+ // Mock fs
6
+ vi.mock("fs");
7
+
8
+ describe("config-loader", () => {
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+ });
12
+
13
+ afterEach(() => {
14
+ vi.restoreAllMocks();
15
+ });
16
+
17
+ describe("findTailwindConfig", () => {
18
+ it("should find tailwind.config.mjs in current directory", () => {
19
+ const startDir = "/project/src";
20
+ const expectedPath = "/project/src/tailwind.config.mjs";
21
+
22
+ vi.spyOn(fs, "existsSync").mockImplementation((filepath) => {
23
+ return filepath === expectedPath;
24
+ });
25
+
26
+ const result = findTailwindConfig(startDir);
27
+ expect(result).toBe(expectedPath);
28
+ });
29
+
30
+ it("should find tailwind.config.js in parent directory", () => {
31
+ const startDir = "/project/src/components";
32
+ const expectedPath = "/project/tailwind.config.js";
33
+
34
+ vi.spyOn(fs, "existsSync").mockImplementation((filepath) => {
35
+ return filepath === expectedPath;
36
+ });
37
+
38
+ const result = findTailwindConfig(startDir);
39
+ expect(result).toBe(expectedPath);
40
+ });
41
+
42
+ it("should return null if no config found", () => {
43
+ const startDir = "/project/src";
44
+
45
+ vi.spyOn(fs, "existsSync").mockReturnValue(false);
46
+
47
+ const result = findTailwindConfig(startDir);
48
+ expect(result).toBeNull();
49
+ });
50
+
51
+ it("should prioritize config file extensions in correct order", () => {
52
+ const startDir = "/project";
53
+ const mjsPath = "/project/tailwind.config.mjs";
54
+ const jsPath = "/project/tailwind.config.js";
55
+
56
+ vi.spyOn(fs, "existsSync").mockImplementation((filepath) => {
57
+ // Both exist, but mjs should be found first
58
+ return filepath === mjsPath || filepath === jsPath;
59
+ });
60
+
61
+ const result = findTailwindConfig(startDir);
62
+ expect(result).toBe(mjsPath); // .mjs has priority
63
+ });
64
+ });
65
+
66
+ describe("loadTailwindConfig", () => {
67
+ it("should load config with default export", () => {
68
+ const configPath = "/project/tailwind.config.js";
69
+ const mockConfig = {
70
+ theme: {
71
+ extend: {
72
+ colors: { brand: "#123456" },
73
+ },
74
+ },
75
+ };
76
+
77
+ // Mock require.resolve and require
78
+ vi.spyOn(require, "resolve").mockReturnValue(configPath);
79
+ vi.doMock(configPath, () => ({ default: mockConfig }));
80
+
81
+ // We need to use dynamic import workaround for testing
82
+ const config = { default: mockConfig };
83
+ const result = "default" in config ? config.default : config;
84
+
85
+ expect(result).toEqual(mockConfig);
86
+ });
87
+
88
+ it("should load config with direct export", () => {
89
+ const mockConfig = {
90
+ theme: {
91
+ colors: { primary: "#ff0000" },
92
+ },
93
+ };
94
+
95
+ const config = mockConfig;
96
+ const result = "default" in config ? (config as { default: unknown }).default : config;
97
+
98
+ expect(result).toEqual(mockConfig);
99
+ });
100
+
101
+ it("should cache loaded configs", () => {
102
+ const configPath = "/project/tailwind.config.js";
103
+ const _mockConfig = { theme: {} };
104
+
105
+ vi.spyOn(require, "resolve").mockReturnValue(configPath);
106
+
107
+ // First load
108
+ const config1 = loadTailwindConfig(configPath);
109
+
110
+ // Second load - should hit cache
111
+ const config2 = loadTailwindConfig(configPath);
112
+
113
+ // Both should return same result (from cache)
114
+ expect(config1).toBe(config2);
115
+ });
116
+ });
117
+
118
+ describe("extractCustomColors", () => {
119
+ it("should return empty object when no config found", () => {
120
+ vi.spyOn(fs, "existsSync").mockReturnValue(false);
121
+
122
+ const result = extractCustomColors("/project/src/file.ts");
123
+ expect(result).toEqual({});
124
+ });
125
+
126
+ it("should return empty object when config has no theme", () => {
127
+ const configPath = "/project/tailwind.config.js";
128
+
129
+ vi.spyOn(fs, "existsSync").mockImplementation((filepath) => filepath === configPath);
130
+ vi.spyOn(require, "resolve").mockReturnValue(configPath);
131
+
132
+ // loadTailwindConfig will be called, but we've already tested it
133
+ // For integration, we'd need to mock the entire flow
134
+ const result = extractCustomColors("/project/src/file.ts");
135
+
136
+ // Without actual config loading, this returns empty
137
+ expect(result).toEqual({});
138
+ });
139
+
140
+ it("should extract colors from theme.extend.colors", () => {
141
+ // This would require complex mocking of the entire require flow
142
+ // Testing the logic: theme.extend.colors is preferred
143
+ const colors = { brand: { light: "#fff", dark: "#000" } };
144
+ const theme = {
145
+ extend: { colors },
146
+ };
147
+
148
+ // If we had the config, we'd flatten the colors
149
+ expect(theme.extend.colors).toEqual(colors);
150
+ });
151
+ });
152
+ });
@@ -722,6 +722,306 @@ describe("Babel plugin - platform modifier transformation", () => {
722
722
  });
723
723
  });
724
724
 
725
+ describe("Babel plugin - color scheme modifier transformation", () => {
726
+ it("should transform dark: modifier to conditional expression", () => {
727
+ const input = `
728
+ import React from 'react';
729
+ import { View } from 'react-native';
730
+
731
+ export function Component() {
732
+ return (
733
+ <View className="bg-white dark:bg-gray-900" />
734
+ );
735
+ }
736
+ `;
737
+
738
+ const output = transform(input, undefined, true);
739
+
740
+ // Should import useColorScheme
741
+ expect(output).toContain("useColorScheme");
742
+ expect(output).toMatch(/import.*useColorScheme.*from ['"]react-native['"]/);
743
+
744
+ // Should inject colorScheme hook in component
745
+ expect(output).toContain("_twColorScheme");
746
+ expect(output).toContain("useColorScheme()");
747
+
748
+ // Should have base bg-white style
749
+ expect(output).toContain("_bg_white");
750
+
751
+ // Should have dark:bg-gray-900 style
752
+ expect(output).toContain("_dark_bg_gray_900");
753
+
754
+ // Should generate conditional: _twColorScheme === 'dark' && ...
755
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
756
+ });
757
+
758
+ it("should support both dark: and light: modifiers", () => {
759
+ const input = `
760
+ import React from 'react';
761
+ import { View } from 'react-native';
762
+
763
+ export function Component() {
764
+ return (
765
+ <View className="bg-gray-100 dark:bg-gray-900 light:bg-white" />
766
+ );
767
+ }
768
+ `;
769
+
770
+ const output = transform(input, undefined, true);
771
+
772
+ // Should have all three styles
773
+ expect(output).toContain("_bg_gray_100");
774
+ expect(output).toContain("_dark_bg_gray_900");
775
+ expect(output).toContain("_light_bg_white");
776
+
777
+ // Should have both conditionals
778
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
779
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]light['"]/);
780
+ });
781
+
782
+ it("should inject hook once for multiple elements with color scheme modifiers", () => {
783
+ const input = `
784
+ import React from 'react';
785
+ import { View, Text } from 'react-native';
786
+
787
+ export function Component() {
788
+ return (
789
+ <>
790
+ <View className="dark:bg-gray-900" />
791
+ <Text className="dark:text-white" />
792
+ <View className="light:bg-white" />
793
+ </>
794
+ );
795
+ }
796
+ `;
797
+
798
+ const output = transform(input, undefined, true);
799
+
800
+ // Count occurrences of useColorScheme() call - should be exactly 1
801
+ const hookCallMatches = output.match(/=\s*useColorScheme\(\)/g);
802
+ expect(hookCallMatches).toHaveLength(1);
803
+
804
+ // Should have color scheme variable
805
+ expect(output).toContain("_twColorScheme");
806
+ });
807
+
808
+ it("should work with color scheme and platform modifiers together", () => {
809
+ const input = `
810
+ import React from 'react';
811
+ import { View } from 'react-native';
812
+
813
+ export function Component() {
814
+ return (
815
+ <View className="p-4 ios:p-6 dark:bg-gray-900" />
816
+ );
817
+ }
818
+ `;
819
+
820
+ const output = transform(input, undefined, true);
821
+
822
+ // Should have Platform import
823
+ expect(output).toContain("Platform");
824
+
825
+ // Should have useColorScheme import
826
+ expect(output).toContain("useColorScheme");
827
+
828
+ // Should have Platform.select for ios:
829
+ expect(output).toContain("Platform.select");
830
+ expect(output).toContain("_ios_p_6");
831
+
832
+ // Should have color scheme conditional for dark:
833
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
834
+ expect(output).toContain("_dark_bg_gray_900");
835
+ });
836
+
837
+ it("should only add useColorScheme import once when needed", () => {
838
+ const input = `
839
+ import React from 'react';
840
+ import { View } from 'react-native';
841
+
842
+ export function Component() {
843
+ return (
844
+ <>
845
+ <View className="dark:bg-black" />
846
+ <View className="light:bg-white" />
847
+ </>
848
+ );
849
+ }
850
+ `;
851
+
852
+ const output = transform(input, undefined, true);
853
+
854
+ // Count useColorScheme imports
855
+ const importMatches = output.match(/import.*useColorScheme.*from ['"]react-native['"]/g);
856
+ expect(importMatches).toHaveLength(1);
857
+ });
858
+
859
+ it("should merge with existing useColorScheme import", () => {
860
+ const input = `
861
+ import React from 'react';
862
+ import { View, useColorScheme } from 'react-native';
863
+
864
+ export function Component() {
865
+ return <View className="dark:bg-gray-900" />;
866
+ }
867
+ `;
868
+
869
+ const output = transform(input, undefined, true);
870
+
871
+ // Should still use useColorScheme
872
+ expect(output).toContain("useColorScheme");
873
+
874
+ // Should inject hook call
875
+ expect(output).toContain("_twColorScheme");
876
+ expect(output).toContain("useColorScheme()");
877
+ });
878
+
879
+ it("should work with concise arrow functions", () => {
880
+ const input = `
881
+ import React from 'react';
882
+ import { View } from 'react-native';
883
+
884
+ const Component = () => <View className="dark:bg-gray-900" />;
885
+ `;
886
+
887
+ const output = transform(input, undefined, true);
888
+
889
+ // Should inject useColorScheme import
890
+ expect(output).toContain("useColorScheme");
891
+
892
+ // Should convert concise arrow to block statement and inject hook
893
+ expect(output).toContain("_twColorScheme");
894
+ expect(output).toContain("useColorScheme()");
895
+ expect(output).toContain("return");
896
+
897
+ // Should have the style
898
+ expect(output).toContain("_dark_bg_gray_900");
899
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
900
+ });
901
+
902
+ it("should inject hook at component level when dark: used in nested callback", () => {
903
+ const input = `
904
+ import React from 'react';
905
+ import { View } from 'react-native';
906
+
907
+ export function Component() {
908
+ const items = [1, 2, 3];
909
+ return (
910
+ <View>
911
+ {items.map(item => (
912
+ <View key={item} className="dark:bg-gray-900" />
913
+ ))}
914
+ </View>
915
+ );
916
+ }
917
+ `;
918
+
919
+ const output = transform(input, undefined, true);
920
+
921
+ // Should inject hook at Component level (not in map callback)
922
+ expect(output).toContain("_twColorScheme");
923
+ expect(output).toContain("useColorScheme()");
924
+
925
+ // Hook should be injected in Component function, not in map callback
926
+ // Count occurrences - should be exactly 1 at Component level
927
+ const hookCallMatches = output.match(/=\s*useColorScheme\(\)/g);
928
+ expect(hookCallMatches).toHaveLength(1);
929
+
930
+ // Should still generate conditional expression
931
+ expect(output).toContain("_dark_bg_gray_900");
932
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
933
+ });
934
+
935
+ it("should handle dynamic expressions with dark:/light: modifiers", () => {
936
+ const input = `
937
+ import React from 'react';
938
+ import { View } from 'react-native';
939
+
940
+ export function Component({ isActive }) {
941
+ return (
942
+ <View className={\`p-4 \${isActive ? "dark:bg-blue-500" : "dark:bg-gray-900"}\`} />
943
+ );
944
+ }
945
+ `;
946
+
947
+ const output = transform(input, undefined, true);
948
+
949
+ // Should inject useColorScheme
950
+ expect(output).toContain("useColorScheme");
951
+ expect(output).toContain("_twColorScheme");
952
+
953
+ // Should have both dark styles
954
+ expect(output).toContain("_dark_bg_blue_500");
955
+ expect(output).toContain("_dark_bg_gray_900");
956
+
957
+ // Should have conditional expressions for color scheme
958
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
959
+ });
960
+
961
+ it("should handle dynamic expressions with platform modifiers", () => {
962
+ const input = `
963
+ import React from 'react';
964
+ import { View } from 'react-native';
965
+
966
+ export function Component({ isLarge }) {
967
+ return (
968
+ <View className={\`p-4 \${isLarge ? "ios:p-8" : "ios:p-6"}\`} />
969
+ );
970
+ }
971
+ `;
972
+
973
+ const output = transform(input, undefined, true);
974
+
975
+ // Should inject Platform import
976
+ expect(output).toContain("Platform");
977
+
978
+ // Should have both ios styles
979
+ expect(output).toContain("_ios_p_8");
980
+ expect(output).toContain("_ios_p_6");
981
+
982
+ // Should have Platform.select
983
+ expect(output).toContain("Platform.select");
984
+ });
985
+
986
+ it("should skip color scheme modifiers when used outside component scope", () => {
987
+ // Suppress console.warn for this test
988
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
989
+
990
+ const input = `
991
+ import React from 'react';
992
+ import { View } from 'react-native';
993
+
994
+ // Class component - no function component scope
995
+ class MyComponent extends React.Component {
996
+ render() {
997
+ return <View className="p-4 dark:bg-gray-900" />;
998
+ }
999
+ }
1000
+ `;
1001
+
1002
+ const output = transform(input, undefined, true);
1003
+
1004
+ // Should warn about invalid context
1005
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
1006
+ expect.stringContaining("dark:/light: modifiers require a function component scope"),
1007
+ );
1008
+
1009
+ // Should NOT inject useColorScheme import (no valid component scope)
1010
+ expect(output).not.toContain("useColorScheme");
1011
+
1012
+ // Should NOT have _twColorScheme variable reference (would cause ReferenceError)
1013
+ expect(output).not.toContain("_twColorScheme");
1014
+
1015
+ // Should NOT have dark: style conditional (skipped due to no component scope)
1016
+ expect(output).not.toContain("_dark_bg_gray_900");
1017
+
1018
+ // Should still transform base classes (p-4)
1019
+ expect(output).toContain("_p_4");
1020
+
1021
+ consoleWarnSpy.mockRestore();
1022
+ });
1023
+ });
1024
+
725
1025
  describe("Babel plugin - import injection", () => {
726
1026
  it("should not add StyleSheet import to files without className usage", () => {
727
1027
  const input = `
@@ -807,3 +1107,34 @@ describe("Babel plugin - import injection", () => {
807
1107
  expect(output).toContain("StyleSheet");
808
1108
  });
809
1109
  });
1110
+
1111
+ describe("Babel plugin - scheme: modifier", () => {
1112
+ it.skip("should expand scheme: modifier into dark: and light: modifiers", () => {
1113
+ // Note: This test requires tailwind.config.js with custom colors defined
1114
+ // The scheme: modifier expands to dark: and light: modifiers which require
1115
+ // the color variants to exist in customColors (e.g., systemGray-dark, systemGray-light)
1116
+ //
1117
+ // Integration test should be done in a real project with tailwind.config.js
1118
+ const input = `
1119
+ import { View } from 'react-native';
1120
+
1121
+ function MyComponent() {
1122
+ return <View className="scheme:text-systemGray" />;
1123
+ }
1124
+ `;
1125
+
1126
+ const output = transform(input, undefined, true);
1127
+
1128
+ // Should generate both dark and light variants
1129
+ expect(output).toContain("_dark_text_systemGray_dark");
1130
+ expect(output).toContain("_light_text_systemGray_light");
1131
+
1132
+ // Should inject useColorScheme hook
1133
+ expect(output).toContain("useColorScheme");
1134
+ expect(output).toContain("_twColorScheme");
1135
+
1136
+ // Should have conditional expressions
1137
+ expect(output).toContain("_twColorScheme === 'dark'");
1138
+ expect(output).toContain("_twColorScheme === 'light'");
1139
+ });
1140
+ });