@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
package/README.md CHANGED
@@ -23,6 +23,9 @@
23
23
  <a href="https://github.com/mgcrea/react-native-tailwind/actions/workflows/main.yaml">
24
24
  <img src="https://img.shields.io/github/actions/workflow/status/mgcrea/react-native-tailwind/main.yaml?style=for-the-badge&branch=main" alt="build status" />
25
25
  </a>
26
+ <a href="https://codecov.io/gh/mgcrea/react-native-tailwind">
27
+ <img src="https://img.shields.io/codecov/c/github/mgcrea/react-native-tailwind?style=for-the-badge" alt="coverage" />
28
+ </a>
26
29
  <a href="https://depfu.com/github/mgcrea/react-native-tailwind">
27
30
  <img src="https://img.shields.io/depfu/dependencies/github/mgcrea/react-native-tailwind?style=for-the-badge" alt="dependencies status" />
28
31
  </a>
@@ -46,6 +49,8 @@ Compile-time Tailwind CSS for React Native with zero runtime overhead. Transform
46
49
  - 🏃 **Runtime option** — Optional `tw` template tag for fully dynamic styling (~25KB)
47
50
  - 🎯 **State modifiers** — `active:`, `hover:`, `focus:`, and `disabled:` modifiers for interactive components
48
51
  - 📱 **Platform modifiers** — `ios:`, `android:`, and `web:` modifiers for platform-specific styling
52
+ - 🌓 **Color scheme modifiers** — `dark:` and `light:` modifiers for automatic theme adaptation
53
+ - 🎨 **Scheme modifier** — `scheme:` convenience modifier that expands to both `dark:` and `light:` variants
49
54
  - 📜 **Special style props** — Support for `contentContainerClassName`, `columnWrapperClassName`, and more
50
55
  - 🎛️ **Custom attributes** — Configure which props to transform with exact matching or glob patterns
51
56
 
@@ -78,28 +83,6 @@ module.exports = {
78
83
  };
79
84
  ```
80
85
 
81
- **Advanced:** You can customize which attributes are transformed and the generated styles identifier:
82
-
83
- ```javascript
84
- module.exports = {
85
- presets: ["module:@react-native/babel-preset"],
86
- plugins: [
87
- [
88
- "@mgcrea/react-native-tailwind/babel",
89
- {
90
- // Specify which attributes to transform
91
- // Default: ['className', 'contentContainerClassName', 'columnWrapperClassName', 'ListHeaderComponentClassName', 'ListFooterComponentClassName']
92
- attributes: ["className", "buttonClassName", "containerClassName"],
93
-
94
- // Custom identifier for the generated StyleSheet constant
95
- // Default: '_twStyles'
96
- stylesIdentifier: "styles",
97
- },
98
- ],
99
- ],
100
- };
101
- ```
102
-
103
86
  ### 2. Enable TypeScript Support (TypeScript)
104
87
 
105
88
  Create a type declaration file in your project to enable `className` prop autocomplete:
@@ -722,9 +705,7 @@ import { View, Text } from "react-native";
722
705
  export function PlatformCard() {
723
706
  return (
724
707
  <View className="p-4 ios:p-6 android:p-8 bg-white rounded-lg">
725
- <Text className="text-base ios:text-blue-600 android:text-green-600">
726
- Platform-specific styles
727
- </Text>
708
+ <Text className="text-base ios:text-blue-600 android:text-green-600">Platform-specific styles</Text>
728
709
  </View>
729
710
  );
730
711
  }
@@ -844,15 +825,16 @@ import { Pressable } from "@mgcrea/react-native-tailwind";
844
825
 
845
826
  **Supported Platforms:**
846
827
 
847
- | Modifier | Platform | Description |
848
- | -------- | -------------- | ----------------------------- |
849
- | `ios:` | iOS | Styles specific to iOS |
850
- | `android:` | Android | Styles specific to Android |
851
- | `web:` | React Native Web | Styles for web platform |
828
+ | Modifier | Platform | Description |
829
+ | ---------- | ---------------- | -------------------------- |
830
+ | `ios:` | iOS | Styles specific to iOS |
831
+ | `android:` | Android | Styles specific to Android |
832
+ | `web:` | React Native Web | Styles for web platform |
852
833
 
853
834
  **How it works:**
854
835
 
855
836
  The Babel plugin:
837
+
856
838
  1. Detects platform modifiers during compilation
857
839
  2. Parses all platform-specific classes at compile-time
858
840
  3. Generates `Platform.select()` expressions with references to pre-compiled styles
@@ -861,6 +843,350 @@ The Babel plugin:
861
843
 
862
844
  This approach provides the best of both worlds: compile-time optimization for all styles, with minimal runtime platform detection only for the conditional selection logic.
863
845
 
846
+ ### Color Scheme Modifiers
847
+
848
+ Apply color scheme-specific styles using `dark:` and `light:` modifiers that automatically react to the device's appearance settings. These work on **all components in functional components** and compile to conditional expressions that use React Native's `useColorScheme()` hook.
849
+
850
+ **Basic Example:**
851
+
852
+ ```tsx
853
+ import { View, Text } from "react-native";
854
+
855
+ export function ThemeCard() {
856
+ return (
857
+ <View className="bg-white dark:bg-gray-900 p-4 rounded-lg">
858
+ <Text className="text-gray-900 dark:text-white">Adapts to device theme</Text>
859
+ </View>
860
+ );
861
+ }
862
+ ```
863
+
864
+ **Transforms to:**
865
+
866
+ ```tsx
867
+ import { useColorScheme, StyleSheet } from "react-native";
868
+
869
+ export function ThemeCard() {
870
+ const _twColorScheme = useColorScheme();
871
+
872
+ return (
873
+ <View
874
+ style={[
875
+ _twStyles._bg_white_p_4_rounded_lg,
876
+ _twColorScheme === "dark" && _twStyles._dark_bg_gray_900,
877
+ ]}
878
+ >
879
+ <Text style={[_twStyles._text_gray_900, _twColorScheme === "dark" && _twStyles._dark_text_white]}>
880
+ Adapts to device theme
881
+ </Text>
882
+ </View>
883
+ );
884
+ }
885
+
886
+ // Generated styles:
887
+ const _twStyles = StyleSheet.create({
888
+ _bg_white_p_4_rounded_lg: {
889
+ backgroundColor: "#FFFFFF",
890
+ padding: 16,
891
+ borderRadius: 8,
892
+ },
893
+ _dark_bg_gray_900: { backgroundColor: "#111827" },
894
+ _text_gray_900: { color: "#111827" },
895
+ _dark_text_white: { color: "#FFFFFF" },
896
+ });
897
+ ```
898
+
899
+ **Common Use Cases:**
900
+
901
+ **Dark mode support:**
902
+
903
+ ```tsx
904
+ // Automatically switches between light and dark themes
905
+ <View className="bg-white dark:bg-gray-900">
906
+ <Text className="text-gray-900 dark:text-white">Theme-aware text</Text>
907
+ </View>
908
+ ```
909
+
910
+ **Both light and dark overrides:**
911
+
912
+ ```tsx
913
+ // Specify both light and dark mode styles explicitly
914
+ <View className="bg-gray-100 light:bg-white dark:bg-gray-900">
915
+ <Text className="text-gray-600 light:text-gray-900 dark:text-gray-100">Custom light & dark styles</Text>
916
+ </View>
917
+ ```
918
+
919
+ **Mixed color scheme and platform modifiers:**
920
+
921
+ ```tsx
922
+ // Combine color scheme with platform-specific styles
923
+ <View className="p-4 ios:p-6 dark:bg-gray-900 android:rounded-xl">
924
+ <Text className="text-base dark:text-white ios:text-blue-600">Platform + theme aware</Text>
925
+ </View>
926
+ ```
927
+
928
+ **Theme-aware cards:**
929
+
930
+ ```tsx
931
+ // Card that looks great in both light and dark mode
932
+ <View className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 p-4 rounded-lg">
933
+ <Text className="text-lg font-bold text-gray-900 dark:text-white mb-2">Card Title</Text>
934
+ <Text className="text-gray-600 dark:text-gray-300">Card description text</Text>
935
+ </View>
936
+ ```
937
+
938
+ **Key Features:**
939
+
940
+ - ✅ **Reactive** — Automatically updates when user changes system appearance
941
+ - ✅ **Zero runtime parsing** — All styles compiled at build time
942
+ - ✅ **Auto-injected hook** — `useColorScheme()` automatically added to components
943
+ - ✅ **Works with all modifiers** — Combine with platform and state modifiers
944
+ - ✅ **Type-safe** — Full TypeScript autocomplete
945
+ - ✅ **Optimized** — Minimal runtime overhead (just conditional checks)
946
+
947
+ **Supported Modifiers:**
948
+
949
+ | Modifier | Color Scheme | Description |
950
+ | -------- | ------------ | ------------------------------ |
951
+ | `dark:` | Dark mode | Styles when dark mode is active |
952
+ | `light:` | Light mode | Styles when light mode is active |
953
+
954
+ **How it works:**
955
+
956
+ The Babel plugin:
957
+
958
+ 1. Detects color scheme modifiers during compilation
959
+ 2. Finds the parent function component
960
+ 3. Auto-injects `const _twColorScheme = useColorScheme();` at the top of the component
961
+ 4. Parses all color scheme-specific classes at compile-time
962
+ 5. Generates conditional expressions: `_twColorScheme === 'dark' && styles._dark_bg_gray_900`
963
+ 6. Auto-imports `useColorScheme` from `react-native` when needed
964
+ 7. Reuses the same hook variable for multiple elements in the same component
965
+
966
+ **Requirements:**
967
+
968
+ - ⚠️ **Functional components only** — Color scheme modifiers require hooks (React Native's `useColorScheme()`)
969
+ - ✅ Works with function declarations: `function Component() { ... }`
970
+ - ✅ Works with arrow functions: `const Component = () => { ... }`
971
+ - ✅ Works with concise arrow functions: `const Component = () => <View className="dark:..." />`
972
+ - ✅ Works in nested callbacks: Hook injected at component level, not in callbacks
973
+ - ❌ **Not supported in class components** — Will show a warning
974
+ - ⚠️ **React Native 0.62+** — Requires the `useColorScheme` API
975
+ - ✅ **Dynamic expressions supported** — Works in template literals and conditional expressions:
976
+
977
+ ```tsx
978
+ <View className={`p-4 ${isActive ? "dark:bg-blue-500" : "dark:bg-gray-900"}`} />
979
+ ```
980
+
981
+ **Performance:**
982
+
983
+ - **Compile-time**: All styles parsed and registered during build
984
+ - **Runtime**: One `useColorScheme()` hook call per component + minimal conditional checks
985
+ - **Bundle size**: Only includes styles actually used in your code
986
+
987
+ #### Scheme Modifier (Convenience)
988
+
989
+ The `scheme:` modifier is a convenience feature that automatically expands to both `dark:` and `light:` modifiers for color classes. This is useful when you have custom colors with separate dark and light variants.
990
+
991
+ **Basic Usage:**
992
+
993
+ ```tsx
994
+ import { View, Text } from "react-native";
995
+
996
+ export function ThemedCard() {
997
+ return (
998
+ <View className="scheme:bg-systemGray p-4 rounded-lg">
999
+ <Text className="scheme:text-systemLabel">Adaptive system colors</Text>
1000
+ </View>
1001
+ );
1002
+ }
1003
+ ```
1004
+
1005
+ **Transforms to:**
1006
+
1007
+ ```tsx
1008
+ // Automatically expands to both dark: and light: modifiers
1009
+ <View className="dark:bg-systemGray-dark light:bg-systemGray-light p-4 rounded-lg">
1010
+ <Text className="dark:text-systemLabel-dark light:text-systemLabel-light">
1011
+ Adaptive system colors
1012
+ </Text>
1013
+ </View>
1014
+ ```
1015
+
1016
+ **Requirements:**
1017
+
1018
+ To use the `scheme:` modifier, you must define both color variants in your `tailwind.config.*`:
1019
+
1020
+ ```javascript
1021
+ // tailwind.config.mjs
1022
+ export default {
1023
+ theme: {
1024
+ extend: {
1025
+ colors: {
1026
+ // Option 1: Nested structure
1027
+ systemGray: {
1028
+ light: "#8e8e93",
1029
+ dark: "#8e8e93",
1030
+ },
1031
+ systemLabel: {
1032
+ light: "#000000",
1033
+ dark: "#ffffff",
1034
+ },
1035
+
1036
+ // Option 2: Flat structure with suffixes
1037
+ "primary-light": "#bfdbfe",
1038
+ "primary-dark": "#1e40af",
1039
+ },
1040
+ },
1041
+ },
1042
+ };
1043
+ ```
1044
+
1045
+ **Configuring Suffixes:**
1046
+
1047
+ By default, the plugin looks for `-dark` and `-light` suffixes. You can customize these in your Babel configuration:
1048
+
1049
+ ```javascript
1050
+ // babel.config.js
1051
+ module.exports = {
1052
+ plugins: [
1053
+ [
1054
+ "@mgcrea/react-native-tailwind/babel",
1055
+ {
1056
+ schemeModifier: {
1057
+ darkSuffix: "-dark", // default
1058
+ lightSuffix: "-light", // default
1059
+ },
1060
+ },
1061
+ ],
1062
+ ],
1063
+ };
1064
+ ```
1065
+
1066
+ **Custom Suffixes Example:**
1067
+
1068
+ ```javascript
1069
+ // babel.config.js with custom suffixes
1070
+ module.exports = {
1071
+ plugins: [
1072
+ [
1073
+ "@mgcrea/react-native-tailwind/babel",
1074
+ {
1075
+ schemeModifier: {
1076
+ darkSuffix: "Dark",
1077
+ lightSuffix: "Light",
1078
+ },
1079
+ },
1080
+ ],
1081
+ ],
1082
+ };
1083
+
1084
+ // tailwind.config.mjs
1085
+ export default {
1086
+ theme: {
1087
+ extend: {
1088
+ colors: {
1089
+ systemGrayDark: "#8e8e93",
1090
+ systemGrayLight: "#8e8e93",
1091
+ },
1092
+ },
1093
+ },
1094
+ };
1095
+
1096
+ // Usage (same as before)
1097
+ <View className="scheme:bg-systemGray" />
1098
+ ```
1099
+
1100
+ **Validation:**
1101
+
1102
+ The plugin validates that both color variants exist at compile time:
1103
+
1104
+ ```tsx
1105
+ // ✅ Works - both variants exist
1106
+ <View className="scheme:bg-systemGray" />
1107
+ // Expands to: dark:bg-systemGray-dark light:bg-systemGray-light
1108
+
1109
+ // ⚠️ Warning (development only) - missing light variant
1110
+ <View className="scheme:bg-incomplete" />
1111
+ // Only has: incomplete-dark
1112
+ // Warning: "Missing: incomplete-light. This modifier will be ignored."
1113
+
1114
+ // ⚠️ Warning - non-color class
1115
+ <View className="scheme:p-4" />
1116
+ // Warning: "scheme: modifier only supports color classes (text-*, bg-*, border-*)"
1117
+ ```
1118
+
1119
+ **Supported Color Classes:**
1120
+
1121
+ The `scheme:` modifier only works with color utilities:
1122
+
1123
+ - ✅ `scheme:text-{color}` — Text colors
1124
+ - ✅ `scheme:bg-{color}` — Background colors
1125
+ - ✅ `scheme:border-{color}` — Border colors
1126
+ - ❌ Other utilities — Ignored with development warning
1127
+
1128
+ **Use Cases:**
1129
+
1130
+ **Semantic color names:**
1131
+
1132
+ ```tsx
1133
+ // Define semantic system colors that adapt to appearance
1134
+ <View className="scheme:bg-systemBackground p-4">
1135
+ <Text className="scheme:text-systemLabel">System-native appearance</Text>
1136
+ <View className="scheme:border-systemSeparator border-t mt-2 pt-2">
1137
+ <Text className="scheme:text-systemSecondaryLabel">Secondary text</Text>
1138
+ </View>
1139
+ </View>
1140
+ ```
1141
+
1142
+ **Brand colors with theme variants:**
1143
+
1144
+ ```tsx
1145
+ // Brand colors that look good in both themes
1146
+ <View className="scheme:bg-brand p-6 rounded-xl">
1147
+ <Text className="scheme:text-brandContrast text-xl font-bold">
1148
+ Branded Card
1149
+ </Text>
1150
+ <Text className="scheme:text-brandSubtle mt-2">
1151
+ Automatically adapts to user's theme preference
1152
+ </Text>
1153
+ </View>
1154
+ ```
1155
+
1156
+ **Mixed with other modifiers:**
1157
+
1158
+ ```tsx
1159
+ import { Pressable } from "@mgcrea/react-native-tailwind";
1160
+
1161
+ // Combine with state and platform modifiers
1162
+ <Pressable className="scheme:bg-interactive active:opacity-80 ios:p-6 android:p-4 rounded-lg">
1163
+ <Text className="scheme:text-interactiveText font-semibold">
1164
+ Press Me
1165
+ </Text>
1166
+ </Pressable>
1167
+ ```
1168
+
1169
+ **Key Features:**
1170
+
1171
+ - ✅ **DRY principle** — Define color pairs once, use everywhere with `scheme:`
1172
+ - ✅ **Compile-time expansion** — Expands to `dark:` and `light:` during build
1173
+ - ✅ **Type-safe** — Full TypeScript autocomplete
1174
+ - ✅ **Validates at compile-time** — Ensures both variants exist
1175
+ - ✅ **Zero runtime overhead** — Same performance as writing `dark:` and `light:` manually
1176
+ - ✅ **Configurable suffixes** — Adapt to your naming convention
1177
+
1178
+ **How it works:**
1179
+
1180
+ The Babel plugin:
1181
+
1182
+ 1. Detects `scheme:` modifiers during compilation
1183
+ 2. Validates that the class is a color utility (`text-*`, `bg-*`, `border-*`)
1184
+ 3. Checks that both color variants exist in your custom colors (e.g., `systemGray-dark` and `systemGray-light`)
1185
+ 4. Expands to both `dark:` and `light:` modifiers before further processing
1186
+ 5. Processes the expanded modifiers using the standard color scheme logic (injects `useColorScheme()` hook)
1187
+
1188
+ This means `scheme:bg-systemGray` is functionally identical to writing `dark:bg-systemGray-dark light:bg-systemGray-light`, but more concise and maintainable.
1189
+
864
1190
  ### ScrollView Content Container
865
1191
 
866
1192
  Use `contentContainerClassName` to style the ScrollView's content container:
@@ -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
+ });