@mgcrea/react-native-tailwind 0.9.1 → 0.11.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 +386 -43
- package/dist/babel/config-loader.d.ts +12 -3
- package/dist/babel/config-loader.test.ts +154 -0
- package/dist/babel/config-loader.ts +41 -9
- package/dist/babel/index.cjs +592 -69
- package/dist/babel/plugin.d.ts +23 -1
- package/dist/babel/plugin.test.ts +331 -0
- package/dist/babel/plugin.ts +268 -37
- package/dist/babel/utils/colorSchemeModifierProcessing.d.ts +34 -0
- package/dist/babel/utils/colorSchemeModifierProcessing.ts +89 -0
- package/dist/babel/utils/dynamicProcessing.d.ts +34 -3
- package/dist/babel/utils/dynamicProcessing.ts +358 -39
- package/dist/babel/utils/modifierProcessing.d.ts +3 -3
- package/dist/babel/utils/modifierProcessing.ts +5 -5
- package/dist/babel/utils/platformModifierProcessing.d.ts +3 -3
- package/dist/babel/utils/platformModifierProcessing.ts +4 -4
- package/dist/babel/utils/styleInjection.d.ts +13 -0
- package/dist/babel/utils/styleInjection.ts +101 -0
- package/dist/babel/utils/styleTransforms.test.ts +56 -0
- package/dist/babel/utils/twProcessing.d.ts +5 -3
- package/dist/babel/utils/twProcessing.ts +27 -6
- package/dist/parser/index.d.ts +13 -6
- package/dist/parser/index.js +1 -1
- package/dist/parser/modifiers.d.ts +48 -2
- package/dist/parser/modifiers.js +1 -1
- package/dist/parser/modifiers.test.js +1 -1
- package/dist/parser/typography.d.ts +3 -1
- package/dist/parser/typography.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +3 -3
- package/dist/runtime.d.ts +8 -1
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +3 -3
- package/dist/runtime.test.js +1 -1
- package/dist/types/config.d.ts +7 -0
- package/dist/types/config.js +0 -0
- package/package.json +3 -2
- package/src/babel/config-loader.test.ts +154 -0
- package/src/babel/config-loader.ts +41 -9
- package/src/babel/plugin.test.ts +331 -0
- package/src/babel/plugin.ts +268 -37
- package/src/babel/utils/colorSchemeModifierProcessing.ts +89 -0
- package/src/babel/utils/dynamicProcessing.ts +358 -39
- package/src/babel/utils/modifierProcessing.ts +5 -5
- package/src/babel/utils/platformModifierProcessing.ts +4 -4
- package/src/babel/utils/styleInjection.ts +101 -0
- package/src/babel/utils/styleTransforms.test.ts +56 -0
- package/src/babel/utils/twProcessing.ts +27 -6
- package/src/parser/index.ts +28 -9
- package/src/parser/modifiers.test.ts +151 -1
- package/src/parser/modifiers.ts +139 -4
- package/src/parser/typography.ts +14 -2
- package/src/runtime.test.ts +7 -7
- package/src/runtime.ts +37 -14
- package/src/types/config.ts +7 -0
package/dist/runtime.test.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var _vitest=require("vitest");var _runtime=require("./runtime");(0,_vitest.describe)("runtime",function(){(0,_vitest.beforeEach)(function(){(0,_runtime.clearCache)();(0,_runtime.setConfig)({});});(0,_vitest.describe)("tw template tag",function(){(0,_vitest.it)("should parse static classes",function(){var result=(0,_runtime.tw)`m-4 p-2 bg-blue-500`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,padding:8,backgroundColor:"#2b7fff"});(0,_vitest.expect)(result==null?void 0:result.activeStyle).toBeUndefined();(0,_vitest.expect)(result==null?void 0:result.disabledStyle).toBeUndefined();});(0,_vitest.it)("should handle interpolated values",function(){var isActive=true;var result=(0,_runtime.tw)`m-4 ${isActive&&"bg-blue-500"}`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,backgroundColor:"#2b7fff"});});(0,_vitest.it)("should handle conditional classes",function(){var isLarge=true;var result=(0,_runtime.tw)`p-4 ${isLarge?"text-xl":"text-sm"}`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({padding:16,fontSize:20});});(0,_vitest.it)("should handle falsy values",function(){var result=(0,_runtime.tw)`m-4 ${false} ${null} ${undefined} p-2`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,padding:8});});(0,_vitest.it)("should preserve zero values in template literals",function(){var result=(0,_runtime.tw)`opacity-${0} m-4`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({opacity:0,margin:16});});(0,_vitest.it)("should preserve empty string values in template literals",function(){var result=(0,_runtime.tw)`m-4 ${""}p-2`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,padding:8});});(0,_vitest.it)("should handle mixed falsy and truthy numeric values",function(){var spacing=0;var result=(0,_runtime.tw)`m-${spacing} p-4`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:0,padding:16});});(0,_vitest.it)("should return empty style object for empty className",function(){var result=(0,_runtime.tw)``;(0,_vitest.expect)(result).toEqual({style:{}});});(0,_vitest.it)("should normalize whitespace",function(){var result=(0,_runtime.tw)`m-4 p-2 bg-blue-500`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,padding:8,backgroundColor:"#2b7fff"});});});(0,_vitest.describe)("twStyle function",function(){(0,_vitest.it)("should parse className string",function(){var result=(0,_runtime.twStyle)("m-4 p-2 bg-blue-500");(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,padding:8,backgroundColor:"#2b7fff"});(0,_vitest.expect)(result==null?void 0:result.activeStyle).toBeUndefined();});(0,_vitest.it)("should return undefined for empty string",function(){var result=(0,_runtime.twStyle)("");(0,_vitest.expect)(result).toBeUndefined();});(0,_vitest.it)("should normalize whitespace",function(){var result=(0,_runtime.twStyle)("m-4 p-2 bg-blue-500");(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,padding:8,backgroundColor:"#2b7fff"});});});(0,_vitest.describe)("setConfig",function(){(0,_vitest.it)("should set custom colors",function(){(0,_runtime.setConfig)({theme:{extend:{colors:{primary:"#007AFF",secondary:"#5856D6"}}}});var
|
|
1
|
+
var _vitest=require("vitest");var _runtime=require("./runtime");(0,_vitest.describe)("runtime",function(){(0,_vitest.beforeEach)(function(){(0,_runtime.clearCache)();(0,_runtime.setConfig)({});});(0,_vitest.describe)("tw template tag",function(){(0,_vitest.it)("should parse static classes",function(){var result=(0,_runtime.tw)`m-4 p-2 bg-blue-500`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,padding:8,backgroundColor:"#2b7fff"});(0,_vitest.expect)(result==null?void 0:result.activeStyle).toBeUndefined();(0,_vitest.expect)(result==null?void 0:result.disabledStyle).toBeUndefined();});(0,_vitest.it)("should handle interpolated values",function(){var isActive=true;var result=(0,_runtime.tw)`m-4 ${isActive&&"bg-blue-500"}`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,backgroundColor:"#2b7fff"});});(0,_vitest.it)("should handle conditional classes",function(){var isLarge=true;var result=(0,_runtime.tw)`p-4 ${isLarge?"text-xl":"text-sm"}`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({padding:16,fontSize:20});});(0,_vitest.it)("should handle falsy values",function(){var result=(0,_runtime.tw)`m-4 ${false} ${null} ${undefined} p-2`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,padding:8});});(0,_vitest.it)("should preserve zero values in template literals",function(){var result=(0,_runtime.tw)`opacity-${0} m-4`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({opacity:0,margin:16});});(0,_vitest.it)("should preserve empty string values in template literals",function(){var result=(0,_runtime.tw)`m-4 ${""}p-2`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,padding:8});});(0,_vitest.it)("should handle mixed falsy and truthy numeric values",function(){var spacing=0;var result=(0,_runtime.tw)`m-${spacing} p-4`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:0,padding:16});});(0,_vitest.it)("should return empty style object for empty className",function(){var result=(0,_runtime.tw)``;(0,_vitest.expect)(result).toEqual({style:{}});});(0,_vitest.it)("should normalize whitespace",function(){var result=(0,_runtime.tw)`m-4 p-2 bg-blue-500`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,padding:8,backgroundColor:"#2b7fff"});});});(0,_vitest.describe)("twStyle function",function(){(0,_vitest.it)("should parse className string",function(){var result=(0,_runtime.twStyle)("m-4 p-2 bg-blue-500");(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,padding:8,backgroundColor:"#2b7fff"});(0,_vitest.expect)(result==null?void 0:result.activeStyle).toBeUndefined();});(0,_vitest.it)("should return undefined for empty string",function(){var result=(0,_runtime.twStyle)("");(0,_vitest.expect)(result).toBeUndefined();});(0,_vitest.it)("should normalize whitespace",function(){var result=(0,_runtime.twStyle)("m-4 p-2 bg-blue-500");(0,_vitest.expect)(result==null?void 0:result.style).toEqual({margin:16,padding:8,backgroundColor:"#2b7fff"});});});(0,_vitest.describe)("setConfig",function(){(0,_vitest.it)("should set custom colors",function(){(0,_runtime.setConfig)({theme:{extend:{colors:{primary:"#007AFF",secondary:"#5856D6"}}}});var theme=(0,_runtime.getCustomTheme)();(0,_vitest.expect)(theme.colors).toEqual({primary:"#007AFF",secondary:"#5856D6"});});(0,_vitest.it)("should flatten nested colors",function(){(0,_runtime.setConfig)({theme:{extend:{colors:{brand:{light:"#FF6B6B",dark:"#CC0000"}}}}});var theme=(0,_runtime.getCustomTheme)();(0,_vitest.expect)(theme.colors).toEqual({"brand-light":"#FF6B6B","brand-dark":"#CC0000"});});(0,_vitest.it)("should handle mixed flat and nested colors",function(){(0,_runtime.setConfig)({theme:{extend:{colors:{primary:"#007AFF",brand:{light:"#FF6B6B",dark:"#CC0000"}}}}});var theme=(0,_runtime.getCustomTheme)();(0,_vitest.expect)(theme.colors).toEqual({primary:"#007AFF","brand-light":"#FF6B6B","brand-dark":"#CC0000"});});(0,_vitest.it)("should clear cache when config changes",function(){var style=(0,_runtime.tw)`bg-blue-500`;(0,_vitest.expect)(style).toBeDefined();(0,_vitest.expect)((0,_runtime.getCacheStats)().size).toBe(1);(0,_runtime.setConfig)({theme:{extend:{colors:{primary:"#007AFF"}}}});(0,_vitest.expect)((0,_runtime.getCacheStats)().size).toBe(0);});(0,_vitest.it)("should use custom colors in parsing",function(){(0,_runtime.setConfig)({theme:{extend:{colors:{primary:"#007AFF"}}}});var result=(0,_runtime.tw)`bg-primary`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({backgroundColor:"#007AFF"});});});(0,_vitest.describe)("cache",function(){(0,_vitest.it)("should cache parsed styles",function(){var result1=(0,_runtime.tw)`m-4 p-2`;var result2=(0,_runtime.tw)`m-4 p-2`;(0,_vitest.expect)(result1).toBe(result2);});(0,_vitest.it)("should track cache stats",function(){var style1=(0,_runtime.tw)`m-4`;var style2=(0,_runtime.tw)`p-2`;var style3=(0,_runtime.tw)`bg-blue-500`;(0,_vitest.expect)(style1).toBeDefined();(0,_vitest.expect)(style2).toBeDefined();(0,_vitest.expect)(style3).toBeDefined();var stats=(0,_runtime.getCacheStats)();(0,_vitest.expect)(stats.size).toBe(3);(0,_vitest.expect)(stats.keys).toContain("m-4");(0,_vitest.expect)(stats.keys).toContain("p-2");(0,_vitest.expect)(stats.keys).toContain("bg-blue-500");});(0,_vitest.it)("should clear cache",function(){var style1=(0,_runtime.tw)`m-4`;var style2=(0,_runtime.tw)`p-2`;(0,_vitest.expect)(style1).toBeDefined();(0,_vitest.expect)(style2).toBeDefined();(0,_vitest.expect)((0,_runtime.getCacheStats)().size).toBe(2);(0,_runtime.clearCache)();(0,_vitest.expect)((0,_runtime.getCacheStats)().size).toBe(0);});});(0,_vitest.describe)("state modifiers",function(){(0,_vitest.it)("should return activeStyle when active: modifier is used",function(){var result=(0,_runtime.tw)`bg-blue-500 active:bg-blue-700`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({backgroundColor:"#2b7fff"});(0,_vitest.expect)(result==null?void 0:result.activeStyle).toEqual({backgroundColor:"#1447e6"});(0,_vitest.expect)(result==null?void 0:result.disabledStyle).toBeUndefined();});(0,_vitest.it)("should return disabledStyle when disabled: modifier is used",function(){var result=(0,_runtime.tw)`bg-blue-500 disabled:bg-gray-300`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({backgroundColor:"#2b7fff"});(0,_vitest.expect)(result==null?void 0:result.disabledStyle).toEqual({backgroundColor:"#d1d5dc"});(0,_vitest.expect)(result==null?void 0:result.activeStyle).toBeUndefined();});(0,_vitest.it)("should return both activeStyle and disabledStyle when both modifiers are used",function(){var result=(0,_runtime.tw)`bg-blue-500 active:bg-blue-700 disabled:bg-gray-300`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({backgroundColor:"#2b7fff"});(0,_vitest.expect)(result==null?void 0:result.activeStyle).toEqual({backgroundColor:"#1447e6"});(0,_vitest.expect)(result==null?void 0:result.disabledStyle).toEqual({backgroundColor:"#d1d5dc"});});(0,_vitest.it)("should merge base and active styles with multiple properties",function(){var result=(0,_runtime.tw)`p-4 m-2 bg-blue-500 active:bg-blue-700 active:p-6`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({padding:16,margin:8,backgroundColor:"#2b7fff"});(0,_vitest.expect)(result==null?void 0:result.activeStyle).toEqual({backgroundColor:"#1447e6",padding:24});});(0,_vitest.it)("should handle only modifier classes (no base)",function(){var result=(0,_runtime.tw)`active:bg-blue-700`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({});(0,_vitest.expect)(result==null?void 0:result.activeStyle).toEqual({backgroundColor:"#1447e6"});});(0,_vitest.it)("should work with twStyle function",function(){var result=(0,_runtime.twStyle)("bg-blue-500 active:bg-blue-700");(0,_vitest.expect)(result==null?void 0:result.style).toEqual({backgroundColor:"#2b7fff"});(0,_vitest.expect)(result==null?void 0:result.activeStyle).toEqual({backgroundColor:"#1447e6"});});(0,_vitest.it)("should provide raw hex values for animations",function(){var _result$activeStyle;var result=(0,_runtime.tw)`bg-blue-500 active:bg-blue-700`;(0,_vitest.expect)(result==null?void 0:result.style.backgroundColor).toBe("#2b7fff");(0,_vitest.expect)(result==null||(_result$activeStyle=result.activeStyle)==null?void 0:_result$activeStyle.backgroundColor).toBe("#1447e6");});(0,_vitest.it)("should return focusStyle when focus: modifier is used",function(){var result=(0,_runtime.tw)`bg-blue-500 focus:bg-blue-800`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({backgroundColor:"#2b7fff"});(0,_vitest.expect)(result==null?void 0:result.focusStyle).toEqual({backgroundColor:"#193cb8"});(0,_vitest.expect)(result==null?void 0:result.activeStyle).toBeUndefined();(0,_vitest.expect)(result==null?void 0:result.disabledStyle).toBeUndefined();});(0,_vitest.it)("should return all three modifier styles when all are used",function(){var result=(0,_runtime.tw)`bg-blue-500 active:bg-blue-700 focus:bg-blue-800 disabled:bg-gray-300`;(0,_vitest.expect)(result==null?void 0:result.style).toEqual({backgroundColor:"#2b7fff"});(0,_vitest.expect)(result==null?void 0:result.activeStyle).toEqual({backgroundColor:"#1447e6"});(0,_vitest.expect)(result==null?void 0:result.focusStyle).toEqual({backgroundColor:"#193cb8"});(0,_vitest.expect)(result==null?void 0:result.disabledStyle).toEqual({backgroundColor:"#d1d5dc"});});});});
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgcrea/react-native-tailwind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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,154 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { extractCustomTheme, 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("extractCustomTheme", () => {
|
|
119
|
+
it("should return empty theme when no config found", () => {
|
|
120
|
+
vi.spyOn(fs, "existsSync").mockReturnValue(false);
|
|
121
|
+
|
|
122
|
+
const result = extractCustomTheme("/project/src/file.ts");
|
|
123
|
+
expect(result).toEqual({ colors: {}, fontFamily: {} });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should return empty theme 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 = extractCustomTheme("/project/src/file.ts");
|
|
135
|
+
|
|
136
|
+
// Without actual config loading, this returns empty
|
|
137
|
+
expect(result).toEqual({ colors: {}, fontFamily: {} });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should extract colors and fontFamily from theme.extend", () => {
|
|
141
|
+
// This would require complex mocking of the entire require flow
|
|
142
|
+
// Testing the logic: theme.extend is preferred
|
|
143
|
+
const colors = { brand: { light: "#fff", dark: "#000" } };
|
|
144
|
+
const fontFamily = { sans: ['"SF Pro"'], custom: ['"Custom Font"'] };
|
|
145
|
+
const theme = {
|
|
146
|
+
extend: { colors, fontFamily },
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// If we had the config, we'd flatten the colors and convert fontFamily
|
|
150
|
+
expect(theme.extend.colors).toEqual(colors);
|
|
151
|
+
expect(theme.extend.fontFamily).toEqual(fontFamily);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -13,8 +13,10 @@ export type TailwindConfig = {
|
|
|
13
13
|
theme?: {
|
|
14
14
|
extend?: {
|
|
15
15
|
colors?: Record<string, string | Record<string, string>>;
|
|
16
|
+
fontFamily?: Record<string, string | string[]>;
|
|
16
17
|
};
|
|
17
18
|
colors?: Record<string, string | Record<string, string>>;
|
|
19
|
+
fontFamily?: Record<string, string | string[]>;
|
|
18
20
|
};
|
|
19
21
|
};
|
|
20
22
|
|
|
@@ -82,23 +84,31 @@ export function loadTailwindConfig(configPath: string): TailwindConfig | null {
|
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/**
|
|
85
|
-
*
|
|
86
|
-
* Prefers theme.extend.colors over theme.colors to avoid overriding defaults
|
|
87
|
+
* Custom theme configuration extracted from tailwind.config
|
|
87
88
|
*/
|
|
88
|
-
export
|
|
89
|
+
export type CustomTheme = {
|
|
90
|
+
colors: Record<string, string>;
|
|
91
|
+
fontFamily: Record<string, string>;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract all custom theme extensions from tailwind config
|
|
96
|
+
* Prefers theme.extend.* over theme.* to avoid overriding defaults
|
|
97
|
+
*/
|
|
98
|
+
export function extractCustomTheme(filename: string): CustomTheme {
|
|
89
99
|
const projectDir = path.dirname(filename);
|
|
90
100
|
const configPath = findTailwindConfig(projectDir);
|
|
91
101
|
|
|
92
102
|
if (!configPath) {
|
|
93
|
-
return {};
|
|
103
|
+
return { colors: {}, fontFamily: {} };
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
const config = loadTailwindConfig(configPath);
|
|
97
107
|
if (!config?.theme) {
|
|
98
|
-
return {};
|
|
108
|
+
return { colors: {}, fontFamily: {} };
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
//
|
|
111
|
+
// Extract colors
|
|
102
112
|
/* v8 ignore next 5 */
|
|
103
113
|
if (config.theme.colors && !config.theme.extend?.colors && process.env.NODE_ENV !== "production") {
|
|
104
114
|
console.warn(
|
|
@@ -106,9 +116,31 @@ export function extractCustomColors(filename: string): Record<string, string> {
|
|
|
106
116
|
"Use theme.extend.colors to add custom colors while keeping defaults.",
|
|
107
117
|
);
|
|
108
118
|
}
|
|
109
|
-
|
|
110
|
-
// Prefer theme.extend.colors
|
|
111
119
|
const colors = config.theme.extend?.colors ?? config.theme.colors ?? {};
|
|
112
120
|
|
|
113
|
-
|
|
121
|
+
// Extract fontFamily
|
|
122
|
+
/* v8 ignore next 5 */
|
|
123
|
+
if (config.theme.fontFamily && !config.theme.extend?.fontFamily && process.env.NODE_ENV !== "production") {
|
|
124
|
+
console.warn(
|
|
125
|
+
"[react-native-tailwind] Using theme.fontFamily will override all default font families. " +
|
|
126
|
+
"Use theme.extend.fontFamily to add custom fonts while keeping defaults.",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const fontFamily = config.theme.extend?.fontFamily ?? config.theme.fontFamily ?? {};
|
|
130
|
+
|
|
131
|
+
// Convert fontFamily values to strings (take first value if array)
|
|
132
|
+
const fontFamilyResult: Record<string, string> = {};
|
|
133
|
+
for (const [key, value] of Object.entries(fontFamily)) {
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
// Take first font in the array (React Native doesn't support font stacks)
|
|
136
|
+
fontFamilyResult[key] = value[0];
|
|
137
|
+
} else {
|
|
138
|
+
fontFamilyResult[key] = value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
colors: flattenColors(colors),
|
|
144
|
+
fontFamily: fontFamilyResult,
|
|
145
|
+
};
|
|
114
146
|
}
|
package/src/babel/plugin.test.ts
CHANGED
|
@@ -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
|
+
});
|