@mgcrea/react-native-tailwind 0.13.0 → 0.15.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 (73) hide show
  1. package/README.md +33 -30
  2. package/dist/babel/config-loader.d.ts +10 -0
  3. package/dist/babel/config-loader.test.ts +75 -21
  4. package/dist/babel/config-loader.ts +100 -2
  5. package/dist/babel/index.cjs +439 -46
  6. package/dist/babel/plugin/state.d.ts +4 -0
  7. package/dist/babel/plugin/state.ts +8 -0
  8. package/dist/babel/plugin/visitors/className.test.ts +313 -0
  9. package/dist/babel/plugin/visitors/className.ts +36 -8
  10. package/dist/babel/plugin/visitors/imports.ts +16 -1
  11. package/dist/babel/plugin/visitors/program.ts +19 -2
  12. package/dist/babel/plugin/visitors/tw.test.ts +151 -0
  13. package/dist/babel/utils/directionalModifierProcessing.d.ts +34 -0
  14. package/dist/babel/utils/directionalModifierProcessing.ts +99 -0
  15. package/dist/babel/utils/styleInjection.d.ts +16 -0
  16. package/dist/babel/utils/styleInjection.ts +138 -7
  17. package/dist/babel/utils/twProcessing.d.ts +2 -0
  18. package/dist/babel/utils/twProcessing.ts +92 -3
  19. package/dist/parser/borders.js +1 -1
  20. package/dist/parser/borders.test.js +1 -1
  21. package/dist/parser/index.d.ts +3 -2
  22. package/dist/parser/index.js +1 -1
  23. package/dist/parser/layout.d.ts +3 -1
  24. package/dist/parser/layout.js +1 -1
  25. package/dist/parser/layout.test.js +1 -1
  26. package/dist/parser/modifiers.d.ts +32 -2
  27. package/dist/parser/modifiers.js +1 -1
  28. package/dist/parser/modifiers.test.js +1 -1
  29. package/dist/parser/sizing.d.ts +3 -1
  30. package/dist/parser/sizing.js +1 -1
  31. package/dist/parser/sizing.test.js +1 -1
  32. package/dist/parser/spacing.d.ts +4 -2
  33. package/dist/parser/spacing.js +1 -1
  34. package/dist/parser/spacing.test.js +1 -1
  35. package/dist/parser/transforms.d.ts +3 -1
  36. package/dist/parser/transforms.js +1 -1
  37. package/dist/parser/transforms.test.js +1 -1
  38. package/dist/parser/typography.test.js +1 -1
  39. package/dist/runtime.cjs +1 -1
  40. package/dist/runtime.cjs.map +3 -3
  41. package/dist/runtime.d.ts +2 -0
  42. package/dist/runtime.js +1 -1
  43. package/dist/runtime.js.map +3 -3
  44. package/dist/runtime.test.js +1 -1
  45. package/package.json +6 -6
  46. package/src/babel/config-loader.test.ts +75 -21
  47. package/src/babel/config-loader.ts +100 -2
  48. package/src/babel/plugin/state.ts +8 -0
  49. package/src/babel/plugin/visitors/className.test.ts +313 -0
  50. package/src/babel/plugin/visitors/className.ts +36 -8
  51. package/src/babel/plugin/visitors/imports.ts +16 -1
  52. package/src/babel/plugin/visitors/program.ts +19 -2
  53. package/src/babel/plugin/visitors/tw.test.ts +151 -0
  54. package/src/babel/utils/directionalModifierProcessing.ts +99 -0
  55. package/src/babel/utils/styleInjection.ts +138 -7
  56. package/src/babel/utils/twProcessing.ts +92 -3
  57. package/src/parser/borders.test.ts +104 -0
  58. package/src/parser/borders.ts +50 -7
  59. package/src/parser/index.ts +8 -5
  60. package/src/parser/layout.test.ts +168 -0
  61. package/src/parser/layout.ts +107 -8
  62. package/src/parser/modifiers.test.ts +206 -0
  63. package/src/parser/modifiers.ts +62 -3
  64. package/src/parser/sizing.test.ts +56 -0
  65. package/src/parser/sizing.ts +20 -15
  66. package/src/parser/spacing.test.ts +123 -0
  67. package/src/parser/spacing.ts +30 -15
  68. package/src/parser/transforms.test.ts +57 -0
  69. package/src/parser/transforms.ts +7 -3
  70. package/src/parser/typography.test.ts +8 -0
  71. package/src/parser/typography.ts +4 -0
  72. package/src/runtime.test.ts +149 -0
  73. package/src/runtime.ts +53 -1
package/README.md CHANGED
@@ -27,7 +27,7 @@
27
27
  <img src="https://img.shields.io/codecov/c/github/mgcrea/react-native-tailwind?style=for-the-badge" alt="coverage" />
28
28
  </a>
29
29
  <a href="https://depfu.com/github/mgcrea/react-native-tailwind">
30
- <img src="https://img.shields.io/depfu/dependencies/github/mgcrea/react-native-tailwind?style=for-the-badge" alt="dependencies status" />
30
+ <img src="https://img.shields.io/badge/dependencies-none-brightgreen?style=for-the-badge" alt="dependencies status" />
31
31
  </a>
32
32
  </p>
33
33
 
@@ -37,22 +37,21 @@ Compile-time Tailwind CSS for React Native with zero runtime overhead. Transform
37
37
 
38
38
  ## Features
39
39
 
40
- - **Zero runtime overhead** All transformations happen at compile time
41
- - 🔧 **No dependencies** Direct-to-React-Native style generation without tailwindcss package
42
- - 🎯 **Babel-only setup** No Metro configuration required
43
- - 📝 **TypeScript-first** Full type safety and autocomplete support
44
- - 🚀 **Optimized performance** Compiles down to `StyleSheet.create` for optimal performance
45
- - 📦 **Small bundle size** Only includes actual styles used in your app
46
- - 🎨 **Custom colors** Extend the default palette via `tailwind.config.*`
47
- - 📐 **Arbitrary values** Use custom sizes and borders: `w-[123px]`, `rounded-[20px]`
48
- - 🔀 **Dynamic className** Conditional styles with hybrid compile-time optimization
49
- - 🏃 **Runtime option** Optional `tw` template tag for fully dynamic styling (~25KB)
50
- - 🎯 **State modifiers** `active:`, `hover:`, `focus:`, and `disabled:` modifiers for interactive components
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
54
- - 📜 **Special style props** Support for `contentContainerClassName`, `columnWrapperClassName`, and more
55
- - 🎛️ **Custom attributes** — Configure which props to transform with exact matching or glob patterns
40
+ - **⚡ Zero Runtime Overhead** - All transformations happen at compile time
41
+ - **🔧 No Dependencies** - Direct-to-React-Native style generation without tailwindcss package
42
+ - **🎯 Babel-only Setup** - No Metro configuration required
43
+ - **📝 TypeScript-first** - Full type safety and autocomplete support
44
+ - **🚀 Optimized Performance** - Compiles down to StyleSheet.create for optimal performance
45
+ - **🔀 Dynamic className** - Conditional styles support with compile-time optimization
46
+ - **📦 Small Bundle Size** - Only includes actual styles used in your app
47
+ - **🎯 State Modifiers** - `active:`, `hover:`, `focus:`, and `disabled:` modifiers for interactive components
48
+ - **📱 Platform Modifiers** - `ios:`, `android:`, and `web:` modifiers for platform-specific styling
49
+ - **🌓 Color Scheme Modifiers** - `dark:` and `light:` and `scheme:` modifiers for automatic theme adaptation
50
+ - **🎨 Custom Colors** - Extend the default palette via tailwind.config.\*
51
+ - **📐 Arbitrary Values** - Use custom sizes and borders: `w-[123px]`, `rounded-[20px]`
52
+ - **📜 Special Style Props** - Support for `contentContainerClassName`, `columnWrapperClassName`, and more
53
+
54
+ 📊 **[How It Compares](https://mgcrea.github.io/react-native-tailwind/getting-started/how-it-compares/)** - See how this library stacks up against other React Native styling solutions.
56
55
 
57
56
  ## Demo
58
57
 
@@ -113,7 +112,7 @@ The Babel plugin transforms your code at compile time:
113
112
  **Input** (what you write):
114
113
 
115
114
  ```tsx
116
- <View className="m-4 p-2 bg-blue-500 rounded-lg" />
115
+ <View className={`rounded-lg p-4 ${isSelected ? "bg-blue-500 border border-blue-700" : "bg-gray-200"}`} />
117
116
  ```
118
117
 
119
118
  **Output** (what Babel generates):
@@ -121,15 +120,19 @@ The Babel plugin transforms your code at compile time:
121
120
  ```tsx
122
121
  import { StyleSheet } from "react-native";
123
122
 
124
- <View style={_twStyles._bg_blue_500_m_4_p_2_rounded_lg} />;
123
+ <View
124
+ style={[
125
+ _twStyles._rounded_lg,
126
+ _twStyles._p_4,
127
+ isSelected ? _twStyles._bg_blue_500_border_border_blue_700 : _twStyles._bg_gray_200,
128
+ ]}
129
+ />;
125
130
 
126
131
  const _twStyles = StyleSheet.create({
127
- _bg_blue_500_m_4_p_2_rounded_lg: {
128
- margin: 16,
129
- padding: 8,
130
- backgroundColor: "#3B82F6",
131
- borderRadius: 8,
132
- },
132
+ _rounded_lg: { borderRadius: 8 },
133
+ _p_4: { padding: 16 },
134
+ _bg_blue_500_border_border_blue_700: { backgroundColor: "#3B82F6", borderWidth: 1, borderColor: "#1D4ED8" },
135
+ _bg_gray_200: { backgroundColor: "#E5E7EB" },
133
136
  });
134
137
  ```
135
138
 
@@ -200,9 +203,7 @@ import { View, Text } from "react-native";
200
203
  export function PlatformCard() {
201
204
  return (
202
205
  <View className="p-4 ios:p-6 android:p-8 bg-white rounded-lg">
203
- <Text className="text-base ios:text-blue-600 android:text-green-600">
204
- Platform-specific styles
205
- </Text>
206
+ <Text className="text-base ios:text-blue-600 android:text-green-600">Platform-specific styles</Text>
206
207
  </View>
207
208
  );
208
209
  }
@@ -212,12 +213,14 @@ export function PlatformCard() {
212
213
 
213
214
  Contributions are welcome! Please read our [Contributing Guide](https://mgcrea.github.io/react-native-tailwind/advanced/contributing/) for details.
214
215
 
216
+ ## Credits
217
+
218
+ - [Tailwind CSS](https://tailwindcss.com/) - The utility-first CSS framework that revolutionized the way we style applications. If you enjoy this library, consider supporting them by purchasing [Tailwind Plus](https://tailwindcss.com/plus).
219
+
215
220
  ## Authors
216
221
 
217
222
  - [Olivier Louvignes](https://github.com/mgcrea) - [@mgcrea](https://twitter.com/mgcrea)
218
223
 
219
- ## License
220
-
221
224
  ```text
222
225
  MIT License
223
226
 
@@ -8,12 +8,21 @@ export type TailwindConfig = {
8
8
  colors?: Record<string, string | Record<string, string>>;
9
9
  fontFamily?: Record<string, string | string[]>;
10
10
  fontSize?: Record<string, string | number>;
11
+ spacing?: Record<string, string | number>;
12
+ [key: string]: unknown;
11
13
  };
12
14
  colors?: Record<string, string | Record<string, string>>;
13
15
  fontFamily?: Record<string, string | string[]>;
14
16
  fontSize?: Record<string, string | number>;
17
+ spacing?: Record<string, string | number>;
18
+ [key: string]: unknown;
15
19
  };
16
20
  };
21
+ /**
22
+ * Check for unsupported theme extensions and warn the user
23
+ * @internal Exported for testing
24
+ */
25
+ export declare function warnUnsupportedThemeKeys(config: TailwindConfig, configPath: string): void;
17
26
  /**
18
27
  * Find tailwind.config.* file by traversing up from startDir
19
28
  */
@@ -29,6 +38,7 @@ export type CustomTheme = {
29
38
  colors: Record<string, string>;
30
39
  fontFamily: Record<string, string>;
31
40
  fontSize: Record<string, number>;
41
+ spacing: Record<string, number>;
32
42
  };
33
43
  /**
34
44
  * Extract all custom theme extensions from tailwind config
@@ -1,6 +1,11 @@
1
1
  import * as fs from "fs";
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
- import { extractCustomTheme, findTailwindConfig, loadTailwindConfig } from "./config-loader";
3
+ import {
4
+ extractCustomTheme,
5
+ findTailwindConfig,
6
+ loadTailwindConfig,
7
+ warnUnsupportedThemeKeys,
8
+ } from "./config-loader";
4
9
 
5
10
  // Mock fs
6
11
  vi.mock("fs");
@@ -120,35 +125,84 @@ describe("config-loader", () => {
120
125
  vi.spyOn(fs, "existsSync").mockReturnValue(false);
121
126
 
122
127
  const result = extractCustomTheme("/project/src/file.ts");
123
- expect(result).toEqual({ colors: {}, fontFamily: {}, fontSize: {} });
128
+ expect(result).toEqual({ colors: {}, fontFamily: {}, fontSize: {}, spacing: {} });
124
129
  });
130
+ });
125
131
 
126
- it("should return empty theme when config has no theme", () => {
127
- const configPath = "/project/tailwind.config.js";
132
+ describe("warnUnsupportedThemeKeys", () => {
133
+ it("should warn about unsupported theme keys", () => {
134
+ const configPath = "/project/unsupported/tailwind.config.js";
135
+ const mockConfig = {
136
+ theme: {
137
+ extend: {
138
+ colors: { brand: "#123456" },
139
+ spacing: { "72": "18rem" }, // Supported (now!)
140
+ borderRadius: { xl: "1rem" }, // Unsupported
141
+ lineHeight: { tight: "1.25" }, // Unsupported
142
+ },
143
+ screens: { tablet: "640px" }, // Unsupported
144
+ },
145
+ };
128
146
 
129
- vi.spyOn(fs, "existsSync").mockImplementation((filepath) => filepath === configPath);
130
- vi.spyOn(require, "resolve").mockReturnValue(configPath);
147
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
131
148
 
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");
149
+ warnUnsupportedThemeKeys(mockConfig, configPath);
135
150
 
136
- // Without actual config loading, this returns empty
137
- expect(result).toEqual({ colors: {}, fontFamily: {}, fontSize: {} });
151
+ expect(consoleSpy).toHaveBeenCalledWith(
152
+ expect.stringContaining("Unsupported theme configuration detected"),
153
+ );
154
+ // spacing is now supported, so should NOT warn about it
155
+ expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining("theme.extend.spacing"));
156
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("theme.extend.borderRadius"));
157
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("theme.extend.lineHeight"));
158
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("theme.screens"));
159
+ expect(consoleSpy).toHaveBeenCalledWith(
160
+ expect.stringContaining("https://github.com/mgcrea/react-native-tailwind/issues/new"),
161
+ );
162
+
163
+ consoleSpy.mockRestore();
138
164
  });
139
165
 
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 },
166
+ it("should not warn for supported theme keys only", () => {
167
+ const configPath = "/project/supported/tailwind.config.js";
168
+ const mockConfig = {
169
+ theme: {
170
+ extend: {
171
+ colors: { brand: "#123456" },
172
+ fontFamily: { custom: "CustomFont" },
173
+ fontSize: { huge: "48px" },
174
+ spacing: { "72": "18rem" },
175
+ },
176
+ },
177
+ };
178
+
179
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
180
+
181
+ warnUnsupportedThemeKeys(mockConfig, configPath);
182
+
183
+ expect(consoleSpy).not.toHaveBeenCalled();
184
+
185
+ consoleSpy.mockRestore();
186
+ });
187
+
188
+ it("should only warn once per config path", () => {
189
+ const configPath = "/project/once/tailwind.config.js";
190
+ const mockConfig = {
191
+ theme: {
192
+ extend: {
193
+ borderRadius: { xl: "1rem" }, // Unsupported
194
+ },
195
+ },
147
196
  };
148
197
 
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);
198
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
199
+
200
+ warnUnsupportedThemeKeys(mockConfig, configPath);
201
+ warnUnsupportedThemeKeys(mockConfig, configPath);
202
+
203
+ expect(consoleSpy).toHaveBeenCalledTimes(1);
204
+
205
+ consoleSpy.mockRestore();
152
206
  });
153
207
  });
154
208
  });
@@ -15,13 +15,68 @@ export type TailwindConfig = {
15
15
  colors?: Record<string, string | Record<string, string>>;
16
16
  fontFamily?: Record<string, string | string[]>;
17
17
  fontSize?: Record<string, string | number>;
18
+ spacing?: Record<string, string | number>;
19
+ [key: string]: unknown;
18
20
  };
19
21
  colors?: Record<string, string | Record<string, string>>;
20
22
  fontFamily?: Record<string, string | string[]>;
21
23
  fontSize?: Record<string, string | number>;
24
+ spacing?: Record<string, string | number>;
25
+ [key: string]: unknown;
22
26
  };
23
27
  };
24
28
 
29
+ /**
30
+ * Theme keys currently supported by react-native-tailwind
31
+ */
32
+ const SUPPORTED_THEME_KEYS = new Set(["colors", "fontFamily", "fontSize", "spacing", "extend"]);
33
+
34
+ /**
35
+ * Cache for warned config paths to avoid duplicate warnings
36
+ */
37
+ const warnedConfigPaths = new Set<string>();
38
+
39
+ /**
40
+ * Check for unsupported theme extensions and warn the user
41
+ * @internal Exported for testing
42
+ */
43
+ export function warnUnsupportedThemeKeys(config: TailwindConfig, configPath: string): void {
44
+ if (process.env.NODE_ENV === "production" || warnedConfigPaths.has(configPath)) {
45
+ return;
46
+ }
47
+
48
+ const unsupportedKeys: string[] = [];
49
+
50
+ // Check theme.extend keys
51
+ if (config.theme?.extend && typeof config.theme.extend === "object") {
52
+ for (const key of Object.keys(config.theme.extend)) {
53
+ if (!SUPPORTED_THEME_KEYS.has(key)) {
54
+ unsupportedKeys.push(`theme.extend.${key}`);
55
+ }
56
+ }
57
+ }
58
+
59
+ // Check direct theme keys (excluding 'extend')
60
+ if (config.theme && typeof config.theme === "object") {
61
+ for (const key of Object.keys(config.theme)) {
62
+ if (key !== "extend" && !SUPPORTED_THEME_KEYS.has(key)) {
63
+ unsupportedKeys.push(`theme.${key}`);
64
+ }
65
+ }
66
+ }
67
+
68
+ if (unsupportedKeys.length > 0) {
69
+ warnedConfigPaths.add(configPath);
70
+ console.warn(
71
+ `[react-native-tailwind] Unsupported theme configuration detected:\n` +
72
+ ` ${unsupportedKeys.join(", ")}\n\n` +
73
+ ` Currently supported: colors, fontFamily, fontSize, spacing\n\n` +
74
+ ` These extensions will be ignored. If you need support for these features,\n` +
75
+ ` please open an issue: https://github.com/mgcrea/react-native-tailwind/issues/new`,
76
+ );
77
+ }
78
+ }
79
+
25
80
  // Cache configs per path to avoid repeated file I/O
26
81
  const configCache = new Map<string, TailwindConfig | null>();
27
82
 
@@ -92,6 +147,7 @@ export type CustomTheme = {
92
147
  colors: Record<string, string>;
93
148
  fontFamily: Record<string, string>;
94
149
  fontSize: Record<string, number>;
150
+ spacing: Record<string, number>;
95
151
  };
96
152
 
97
153
  /**
@@ -103,14 +159,17 @@ export function extractCustomTheme(filename: string): CustomTheme {
103
159
  const configPath = findTailwindConfig(projectDir);
104
160
 
105
161
  if (!configPath) {
106
- return { colors: {}, fontFamily: {}, fontSize: {} };
162
+ return { colors: {}, fontFamily: {}, fontSize: {}, spacing: {} };
107
163
  }
108
164
 
109
165
  const config = loadTailwindConfig(configPath);
110
166
  if (!config?.theme) {
111
- return { colors: {}, fontFamily: {}, fontSize: {} };
167
+ return { colors: {}, fontFamily: {}, fontSize: {}, spacing: {} };
112
168
  }
113
169
 
170
+ // Warn about unsupported theme keys
171
+ warnUnsupportedThemeKeys(config, configPath);
172
+
114
173
  // Extract colors
115
174
  /* v8 ignore next 5 */
116
175
  if (config.theme.colors && !config.theme.extend?.colors && process.env.NODE_ENV !== "production") {
@@ -173,9 +232,48 @@ export function extractCustomTheme(filename: string): CustomTheme {
173
232
  }
174
233
  }
175
234
 
235
+ // Extract spacing
236
+ /* v8 ignore next 5 */
237
+ if (config.theme.spacing && !config.theme.extend?.spacing && process.env.NODE_ENV !== "production") {
238
+ console.warn(
239
+ "[react-native-tailwind] Using theme.spacing will override all default spacing. " +
240
+ "Use theme.extend.spacing to add custom spacing while keeping defaults.",
241
+ );
242
+ }
243
+ const spacing = config.theme.extend?.spacing ?? config.theme.spacing ?? {};
244
+
245
+ // Convert spacing values to numbers (handle rem, px, or number values)
246
+ const spacingResult: Record<string, number> = {};
247
+ for (const [key, value] of Object.entries(spacing)) {
248
+ if (typeof value === "number") {
249
+ spacingResult[key] = value;
250
+ } else if (typeof value === "string") {
251
+ // Parse string values: "18rem" -> 288, "16px" -> 16, "16" -> 16
252
+ let parsed: number;
253
+ if (value.endsWith("rem")) {
254
+ // Convert rem to px (1rem = 16px)
255
+ parsed = parseFloat(value.replace(/rem$/, "")) * 16;
256
+ } else {
257
+ // Parse px or unitless values
258
+ parsed = parseFloat(value.replace(/px$/, ""));
259
+ }
260
+ if (!isNaN(parsed)) {
261
+ spacingResult[key] = parsed;
262
+ } else {
263
+ /* v8 ignore next 5 */
264
+ if (process.env.NODE_ENV !== "production") {
265
+ console.warn(
266
+ `[react-native-tailwind] Invalid spacing value for "${key}": ${value}. Expected number or string like "16px" or "1rem".`,
267
+ );
268
+ }
269
+ }
270
+ }
271
+ }
272
+
176
273
  return {
177
274
  colors: flattenColors(colors),
178
275
  fontFamily: fontFamilyResult,
179
276
  fontSize: fontSizeResult,
277
+ spacing: spacingResult,
180
278
  };
181
279
  }