@mgcrea/react-native-tailwind 0.10.0 → 0.11.1
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 +159 -13
- package/dist/babel/config-loader.d.ts +12 -3
- package/dist/babel/config-loader.test.ts +14 -12
- package/dist/babel/config-loader.ts +41 -9
- package/dist/babel/index.cjs +91 -54
- package/dist/babel/plugin.d.ts +39 -1
- package/dist/babel/plugin.test.ts +275 -1
- package/dist/babel/plugin.ts +84 -25
- package/dist/babel/utils/colorSchemeModifierProcessing.d.ts +3 -3
- package/dist/babel/utils/colorSchemeModifierProcessing.ts +4 -4
- package/dist/babel/utils/dynamicProcessing.d.ts +5 -5
- package/dist/babel/utils/dynamicProcessing.ts +11 -11
- 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 +5 -3
- package/dist/babel/utils/styleInjection.ts +38 -23
- package/dist/babel/utils/twProcessing.d.ts +3 -3
- package/dist/babel/utils/twProcessing.ts +6 -6
- package/dist/parser/index.d.ts +11 -4
- package/dist/parser/index.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/package.json +1 -1
- package/src/babel/config-loader.test.ts +14 -12
- package/src/babel/config-loader.ts +41 -9
- package/src/babel/plugin.test.ts +275 -1
- package/src/babel/plugin.ts +84 -25
- package/src/babel/utils/colorSchemeModifierProcessing.ts +4 -4
- package/src/babel/utils/dynamicProcessing.ts +11 -11
- package/src/babel/utils/modifierProcessing.ts +5 -5
- package/src/babel/utils/platformModifierProcessing.ts +4 -4
- package/src/babel/utils/styleInjection.ts +38 -23
- package/src/babel/utils/twProcessing.ts +6 -6
- package/src/parser/index.ts +16 -8
- package/src/parser/typography.ts +14 -2
- package/src/runtime.test.ts +7 -7
- package/src/runtime.ts +37 -14
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"});});});});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgcrea/react-native-tailwind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import { extractCustomTheme, findTailwindConfig, loadTailwindConfig } from "./config-loader";
|
|
4
4
|
|
|
5
5
|
// Mock fs
|
|
6
6
|
vi.mock("fs");
|
|
@@ -115,15 +115,15 @@ describe("config-loader", () => {
|
|
|
115
115
|
});
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
describe("
|
|
119
|
-
it("should return empty
|
|
118
|
+
describe("extractCustomTheme", () => {
|
|
119
|
+
it("should return empty theme when no config found", () => {
|
|
120
120
|
vi.spyOn(fs, "existsSync").mockReturnValue(false);
|
|
121
121
|
|
|
122
|
-
const result =
|
|
123
|
-
expect(result).toEqual({});
|
|
122
|
+
const result = extractCustomTheme("/project/src/file.ts");
|
|
123
|
+
expect(result).toEqual({ colors: {}, fontFamily: {} });
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
it("should return empty
|
|
126
|
+
it("should return empty theme when config has no theme", () => {
|
|
127
127
|
const configPath = "/project/tailwind.config.js";
|
|
128
128
|
|
|
129
129
|
vi.spyOn(fs, "existsSync").mockImplementation((filepath) => filepath === configPath);
|
|
@@ -131,22 +131,24 @@ describe("config-loader", () => {
|
|
|
131
131
|
|
|
132
132
|
// loadTailwindConfig will be called, but we've already tested it
|
|
133
133
|
// For integration, we'd need to mock the entire flow
|
|
134
|
-
const result =
|
|
134
|
+
const result = extractCustomTheme("/project/src/file.ts");
|
|
135
135
|
|
|
136
136
|
// Without actual config loading, this returns empty
|
|
137
|
-
expect(result).toEqual({});
|
|
137
|
+
expect(result).toEqual({ colors: {}, fontFamily: {} });
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
-
it("should extract colors from theme.extend
|
|
140
|
+
it("should extract colors and fontFamily from theme.extend", () => {
|
|
141
141
|
// This would require complex mocking of the entire require flow
|
|
142
|
-
// Testing the logic: theme.extend
|
|
142
|
+
// Testing the logic: theme.extend is preferred
|
|
143
143
|
const colors = { brand: { light: "#fff", dark: "#000" } };
|
|
144
|
+
const fontFamily = { sans: ['"SF Pro"'], custom: ['"Custom Font"'] };
|
|
144
145
|
const theme = {
|
|
145
|
-
extend: { colors },
|
|
146
|
+
extend: { colors, fontFamily },
|
|
146
147
|
};
|
|
147
148
|
|
|
148
|
-
// If we had the config, we'd flatten the colors
|
|
149
|
+
// If we had the config, we'd flatten the colors and convert fontFamily
|
|
149
150
|
expect(theme.extend.colors).toEqual(colors);
|
|
151
|
+
expect(theme.extend.fontFamily).toEqual(fontFamily);
|
|
150
152
|
});
|
|
151
153
|
});
|
|
152
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
|
@@ -7,7 +7,9 @@ import babelPlugin, { type PluginOptions } from "./plugin.js";
|
|
|
7
7
|
* Helper to transform code with the Babel plugin
|
|
8
8
|
*/
|
|
9
9
|
function transform(code: string, options?: PluginOptions, includeJsx = false) {
|
|
10
|
-
const presets = includeJsx
|
|
10
|
+
const presets = includeJsx
|
|
11
|
+
? ["@babel/preset-react", ["@babel/preset-typescript", { isTSX: true, allExtensions: true }]]
|
|
12
|
+
: [];
|
|
11
13
|
|
|
12
14
|
const result = transformSync(code, {
|
|
13
15
|
presets,
|
|
@@ -1022,6 +1024,278 @@ describe("Babel plugin - color scheme modifier transformation", () => {
|
|
|
1022
1024
|
});
|
|
1023
1025
|
});
|
|
1024
1026
|
|
|
1027
|
+
describe("Babel plugin - custom color scheme hook import", () => {
|
|
1028
|
+
it("should use custom import source for color scheme hook", () => {
|
|
1029
|
+
const input = `
|
|
1030
|
+
import React from 'react';
|
|
1031
|
+
import { View } from 'react-native';
|
|
1032
|
+
|
|
1033
|
+
export function Component() {
|
|
1034
|
+
return <View className="dark:bg-gray-900" />;
|
|
1035
|
+
}
|
|
1036
|
+
`;
|
|
1037
|
+
|
|
1038
|
+
const output = transform(
|
|
1039
|
+
input,
|
|
1040
|
+
{
|
|
1041
|
+
colorScheme: {
|
|
1042
|
+
importFrom: "@/hooks/useColorScheme",
|
|
1043
|
+
importName: "useColorScheme",
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
true,
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
// Should import from custom source
|
|
1050
|
+
expect(output).toContain('from "@/hooks/useColorScheme"');
|
|
1051
|
+
expect(output).not.toContain('useColorScheme } from "react-native"');
|
|
1052
|
+
|
|
1053
|
+
// Should inject hook call
|
|
1054
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1055
|
+
|
|
1056
|
+
// Should have conditional styling
|
|
1057
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it("should use custom hook name", () => {
|
|
1061
|
+
const input = `
|
|
1062
|
+
import React from 'react';
|
|
1063
|
+
import { View } from 'react-native';
|
|
1064
|
+
|
|
1065
|
+
export function Component() {
|
|
1066
|
+
return <View className="dark:bg-gray-900" />;
|
|
1067
|
+
}
|
|
1068
|
+
`;
|
|
1069
|
+
|
|
1070
|
+
const output = transform(
|
|
1071
|
+
input,
|
|
1072
|
+
{
|
|
1073
|
+
colorScheme: {
|
|
1074
|
+
importFrom: "@react-navigation/native",
|
|
1075
|
+
importName: "useTheme",
|
|
1076
|
+
},
|
|
1077
|
+
},
|
|
1078
|
+
true,
|
|
1079
|
+
);
|
|
1080
|
+
|
|
1081
|
+
// Should import useTheme from React Navigation
|
|
1082
|
+
expect(output).toContain('from "@react-navigation/native"');
|
|
1083
|
+
expect(output).toContain("useTheme");
|
|
1084
|
+
|
|
1085
|
+
// Should call useTheme hook
|
|
1086
|
+
expect(output).toContain("_twColorScheme = useTheme()");
|
|
1087
|
+
|
|
1088
|
+
// Should have conditional styling
|
|
1089
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it("should merge custom hook with existing import from same source", () => {
|
|
1093
|
+
const input = `
|
|
1094
|
+
import React from 'react';
|
|
1095
|
+
import { View, Text } from 'react-native';
|
|
1096
|
+
import { useNavigation } from '@react-navigation/native';
|
|
1097
|
+
|
|
1098
|
+
export function Component() {
|
|
1099
|
+
const navigation = useNavigation();
|
|
1100
|
+
return (
|
|
1101
|
+
<View className="dark:bg-gray-900">
|
|
1102
|
+
<Text onPress={() => navigation.navigate('Home')}>Go Home</Text>
|
|
1103
|
+
</View>
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
`;
|
|
1107
|
+
|
|
1108
|
+
const output = transform(
|
|
1109
|
+
input,
|
|
1110
|
+
{
|
|
1111
|
+
colorScheme: {
|
|
1112
|
+
importFrom: "@react-navigation/native",
|
|
1113
|
+
importName: "useTheme",
|
|
1114
|
+
},
|
|
1115
|
+
},
|
|
1116
|
+
true,
|
|
1117
|
+
);
|
|
1118
|
+
|
|
1119
|
+
// Should merge with existing import (both useNavigation and useTheme in same import)
|
|
1120
|
+
expect(output).toMatch(
|
|
1121
|
+
/import\s+\{\s*useNavigation[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/,
|
|
1122
|
+
);
|
|
1123
|
+
expect(output).toContain("useNavigation()");
|
|
1124
|
+
expect(output).toContain("useTheme()");
|
|
1125
|
+
|
|
1126
|
+
// Should only have one import from that source
|
|
1127
|
+
const importCount = (output.match(/@react-navigation\/native/g) ?? []).length;
|
|
1128
|
+
expect(importCount).toBe(1);
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
it("should not duplicate custom hook if already imported", () => {
|
|
1132
|
+
const input = `
|
|
1133
|
+
import React from 'react';
|
|
1134
|
+
import { View } from 'react-native';
|
|
1135
|
+
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
1136
|
+
|
|
1137
|
+
export function Component() {
|
|
1138
|
+
return <View className="dark:bg-gray-900" />;
|
|
1139
|
+
}
|
|
1140
|
+
`;
|
|
1141
|
+
|
|
1142
|
+
const output = transform(
|
|
1143
|
+
input,
|
|
1144
|
+
{
|
|
1145
|
+
colorScheme: {
|
|
1146
|
+
importFrom: "@/hooks/useColorScheme",
|
|
1147
|
+
importName: "useColorScheme",
|
|
1148
|
+
},
|
|
1149
|
+
},
|
|
1150
|
+
true,
|
|
1151
|
+
);
|
|
1152
|
+
|
|
1153
|
+
// Should not add duplicate import
|
|
1154
|
+
const importMatches = output.match(/import.*useColorScheme.*from ['"]@\/hooks\/useColorScheme['"]/g);
|
|
1155
|
+
expect(importMatches).toHaveLength(1);
|
|
1156
|
+
|
|
1157
|
+
// Should still inject hook call
|
|
1158
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
it("should use react-native by default when no custom config provided", () => {
|
|
1162
|
+
const input = `
|
|
1163
|
+
import React from 'react';
|
|
1164
|
+
import { View } from 'react-native';
|
|
1165
|
+
|
|
1166
|
+
export function Component() {
|
|
1167
|
+
return <View className="dark:bg-gray-900" />;
|
|
1168
|
+
}
|
|
1169
|
+
`;
|
|
1170
|
+
|
|
1171
|
+
const output = transform(input, undefined, true);
|
|
1172
|
+
|
|
1173
|
+
// Should use default react-native import (can be single or double quotes)
|
|
1174
|
+
expect(output).toMatch(/useColorScheme\s*}\s*from\s+['"]react-native['"]/);
|
|
1175
|
+
expect(output).not.toContain("@/hooks");
|
|
1176
|
+
expect(output).not.toContain("@react-navigation");
|
|
1177
|
+
|
|
1178
|
+
// Should inject hook call with default name
|
|
1179
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
it("should create separate import when only type-only import exists", () => {
|
|
1183
|
+
const input = `
|
|
1184
|
+
import React from 'react';
|
|
1185
|
+
import { View } from 'react-native';
|
|
1186
|
+
import type { NavigationProp } from '@react-navigation/native';
|
|
1187
|
+
|
|
1188
|
+
export function Component() {
|
|
1189
|
+
return <View className="dark:bg-gray-900" />;
|
|
1190
|
+
}
|
|
1191
|
+
`;
|
|
1192
|
+
|
|
1193
|
+
const output = transform(
|
|
1194
|
+
input,
|
|
1195
|
+
{
|
|
1196
|
+
colorScheme: {
|
|
1197
|
+
importFrom: "@react-navigation/native",
|
|
1198
|
+
importName: "useTheme",
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
true,
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
// TypeScript preset strips type-only imports, but the important thing is:
|
|
1205
|
+
// 1. useTheme hook is imported (not skipped thinking it was already imported)
|
|
1206
|
+
// 2. Hook is correctly called in the component
|
|
1207
|
+
expect(output).toMatch(/import\s+\{\s*useTheme\s*\}\s+from\s+['"]@react-navigation\/native['"]/);
|
|
1208
|
+
expect(output).toContain("_twColorScheme = useTheme()");
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
it("should use aliased identifier when hook is already imported with alias", () => {
|
|
1212
|
+
const input = `
|
|
1213
|
+
import React from 'react';
|
|
1214
|
+
import { View, Text } from 'react-native';
|
|
1215
|
+
import { useTheme as navTheme } from '@react-navigation/native';
|
|
1216
|
+
|
|
1217
|
+
export function Component() {
|
|
1218
|
+
const theme = navTheme();
|
|
1219
|
+
return (
|
|
1220
|
+
<View className="dark:bg-gray-900">
|
|
1221
|
+
<Text>{theme.dark ? 'Dark' : 'Light'}</Text>
|
|
1222
|
+
</View>
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
`;
|
|
1226
|
+
|
|
1227
|
+
const output = transform(
|
|
1228
|
+
input,
|
|
1229
|
+
{
|
|
1230
|
+
colorScheme: {
|
|
1231
|
+
importFrom: "@react-navigation/native",
|
|
1232
|
+
importName: "useTheme",
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
true,
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
// Should not add duplicate import
|
|
1239
|
+
const importMatches = output.match(
|
|
1240
|
+
/import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g,
|
|
1241
|
+
);
|
|
1242
|
+
expect(importMatches).toHaveLength(1);
|
|
1243
|
+
|
|
1244
|
+
// Should still have the aliased import
|
|
1245
|
+
expect(output).toMatch(/useTheme\s+as\s+navTheme/);
|
|
1246
|
+
|
|
1247
|
+
// Should call the aliased name (navTheme), not the export name (useTheme)
|
|
1248
|
+
// Both the user's code and our injected hook should use navTheme
|
|
1249
|
+
expect(output).toContain("_twColorScheme = navTheme()");
|
|
1250
|
+
expect(output).not.toContain("_twColorScheme = useTheme()");
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
it("should handle both type-only and aliased imports together", () => {
|
|
1254
|
+
const input = `
|
|
1255
|
+
import React from 'react';
|
|
1256
|
+
import { View, Text } from 'react-native';
|
|
1257
|
+
import type { Theme } from '@react-navigation/native';
|
|
1258
|
+
import { useTheme as getNavTheme } from '@react-navigation/native';
|
|
1259
|
+
|
|
1260
|
+
export function Component() {
|
|
1261
|
+
const theme = getNavTheme();
|
|
1262
|
+
return (
|
|
1263
|
+
<View className="dark:bg-gray-900">
|
|
1264
|
+
<Text>{theme.dark ? 'Dark Mode' : 'Light Mode'}</Text>
|
|
1265
|
+
</View>
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
`;
|
|
1269
|
+
|
|
1270
|
+
const output = transform(
|
|
1271
|
+
input,
|
|
1272
|
+
{
|
|
1273
|
+
colorScheme: {
|
|
1274
|
+
importFrom: "@react-navigation/native",
|
|
1275
|
+
importName: "useTheme",
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
true,
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
// TypeScript preset strips type-only imports
|
|
1282
|
+
// The important thing is: should not add duplicate import, and should use aliased name
|
|
1283
|
+
expect(output).toMatch(
|
|
1284
|
+
/import\s+\{[^}]*useTheme\s+as\s+getNavTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/,
|
|
1285
|
+
);
|
|
1286
|
+
|
|
1287
|
+
// Should not add duplicate import - useTheme should only appear in the aliased import
|
|
1288
|
+
const useThemeImports = output.match(
|
|
1289
|
+
/import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g,
|
|
1290
|
+
);
|
|
1291
|
+
expect(useThemeImports).toHaveLength(1);
|
|
1292
|
+
|
|
1293
|
+
// Should call the aliased name for both user code and our injected hook
|
|
1294
|
+
expect(output).toContain("_twColorScheme = getNavTheme()");
|
|
1295
|
+
expect(output).not.toContain("_twColorScheme = useTheme()");
|
|
1296
|
+
});
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1025
1299
|
describe("Babel plugin - import injection", () => {
|
|
1026
1300
|
it("should not add StyleSheet import to files without className usage", () => {
|
|
1027
1301
|
const input = `
|