@mgcrea/react-native-tailwind 0.12.0 → 0.13.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 (97) hide show
  1. package/README.md +29 -2014
  2. package/dist/babel/config-loader.d.ts +3 -0
  3. package/dist/babel/config-loader.test.ts +2 -2
  4. package/dist/babel/config-loader.ts +37 -2
  5. package/dist/babel/index.cjs +2855 -2434
  6. package/dist/babel/plugin/componentScope.d.ts +26 -0
  7. package/dist/babel/plugin/componentScope.ts +87 -0
  8. package/dist/babel/plugin/state.d.ts +119 -0
  9. package/dist/babel/plugin/state.ts +177 -0
  10. package/dist/babel/plugin/visitors/className.d.ts +11 -0
  11. package/{src/babel/plugin.test.ts → dist/babel/plugin/visitors/className.test.ts} +74 -674
  12. package/dist/babel/plugin/visitors/className.ts +624 -0
  13. package/dist/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  14. package/dist/babel/plugin/visitors/imports.d.ts +11 -0
  15. package/dist/babel/plugin/visitors/imports.test.ts +88 -0
  16. package/dist/babel/plugin/visitors/imports.ts +101 -0
  17. package/dist/babel/plugin/visitors/program.d.ts +15 -0
  18. package/dist/babel/plugin/visitors/program.test.ts +325 -0
  19. package/dist/babel/plugin/visitors/program.ts +99 -0
  20. package/dist/babel/plugin/visitors/tw.d.ts +16 -0
  21. package/dist/babel/plugin/visitors/tw.test.ts +620 -0
  22. package/dist/babel/plugin/visitors/tw.ts +148 -0
  23. package/dist/babel/plugin.d.ts +3 -96
  24. package/dist/babel/plugin.test.ts +470 -0
  25. package/dist/babel/plugin.ts +28 -953
  26. package/dist/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  27. package/dist/babel/utils/componentSupport.test.ts +20 -7
  28. package/dist/babel/utils/componentSupport.ts +2 -0
  29. package/dist/babel/utils/modifierProcessing.ts +21 -0
  30. package/dist/babel/utils/platformModifierProcessing.ts +11 -0
  31. package/dist/babel/utils/styleInjection.d.ts +15 -0
  32. package/dist/babel/utils/styleInjection.ts +172 -17
  33. package/dist/babel/utils/twProcessing.ts +11 -0
  34. package/dist/babel/utils/windowDimensionsProcessing.d.ts +56 -0
  35. package/dist/babel/utils/windowDimensionsProcessing.ts +121 -0
  36. package/dist/components/TouchableOpacity.d.ts +35 -0
  37. package/dist/components/TouchableOpacity.js +1 -0
  38. package/dist/components/index.d.ts +3 -0
  39. package/dist/components/index.js +1 -0
  40. package/dist/config/markers.d.ts +5 -0
  41. package/dist/config/markers.js +1 -0
  42. package/dist/index.d.ts +2 -5
  43. package/dist/index.js +1 -1
  44. package/dist/parser/borders.d.ts +3 -1
  45. package/dist/parser/borders.js +1 -1
  46. package/dist/parser/borders.test.js +1 -1
  47. package/dist/parser/colors.js +1 -1
  48. package/dist/parser/colors.test.js +1 -1
  49. package/dist/parser/index.d.ts +1 -0
  50. package/dist/parser/index.js +1 -1
  51. package/dist/parser/layout.js +1 -1
  52. package/dist/parser/layout.test.js +1 -1
  53. package/dist/parser/sizing.js +1 -1
  54. package/dist/parser/typography.d.ts +2 -1
  55. package/dist/parser/typography.js +1 -1
  56. package/dist/parser/typography.test.js +1 -1
  57. package/dist/runtime.cjs +1 -1
  58. package/dist/runtime.cjs.map +4 -4
  59. package/dist/runtime.js +1 -1
  60. package/dist/runtime.js.map +4 -4
  61. package/package.json +1 -1
  62. package/src/babel/config-loader.test.ts +2 -2
  63. package/src/babel/config-loader.ts +37 -2
  64. package/src/babel/plugin/componentScope.ts +87 -0
  65. package/src/babel/plugin/state.ts +177 -0
  66. package/src/babel/plugin/visitors/className.test.ts +1312 -0
  67. package/src/babel/plugin/visitors/className.ts +624 -0
  68. package/src/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  69. package/src/babel/plugin/visitors/imports.test.ts +88 -0
  70. package/src/babel/plugin/visitors/imports.ts +101 -0
  71. package/src/babel/plugin/visitors/program.test.ts +325 -0
  72. package/src/babel/plugin/visitors/program.ts +99 -0
  73. package/src/babel/plugin/visitors/tw.test.ts +620 -0
  74. package/src/babel/plugin/visitors/tw.ts +148 -0
  75. package/src/babel/plugin.ts +28 -953
  76. package/src/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  77. package/src/babel/utils/componentSupport.test.ts +20 -7
  78. package/src/babel/utils/componentSupport.ts +2 -0
  79. package/src/babel/utils/modifierProcessing.ts +21 -0
  80. package/src/babel/utils/platformModifierProcessing.ts +11 -0
  81. package/src/babel/utils/styleInjection.ts +172 -17
  82. package/src/babel/utils/twProcessing.ts +11 -0
  83. package/src/babel/utils/windowDimensionsProcessing.ts +121 -0
  84. package/src/components/TouchableOpacity.tsx +71 -0
  85. package/src/components/index.ts +3 -0
  86. package/src/config/markers.ts +5 -0
  87. package/src/index.ts +4 -5
  88. package/src/parser/borders.test.ts +58 -0
  89. package/src/parser/borders.ts +18 -3
  90. package/src/parser/colors.test.ts +249 -0
  91. package/src/parser/colors.ts +38 -0
  92. package/src/parser/index.ts +4 -3
  93. package/src/parser/layout.test.ts +61 -0
  94. package/src/parser/layout.ts +55 -1
  95. package/src/parser/sizing.ts +11 -0
  96. package/src/parser/typography.test.ts +102 -0
  97. package/src/parser/typography.ts +61 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgcrea/react-native-tailwind",
3
- "version": "0.12.0",
3
+ "version": "0.13.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",
@@ -120,7 +120,7 @@ describe("config-loader", () => {
120
120
  vi.spyOn(fs, "existsSync").mockReturnValue(false);
121
121
 
122
122
  const result = extractCustomTheme("/project/src/file.ts");
123
- expect(result).toEqual({ colors: {}, fontFamily: {} });
123
+ expect(result).toEqual({ colors: {}, fontFamily: {}, fontSize: {} });
124
124
  });
125
125
 
126
126
  it("should return empty theme when config has no theme", () => {
@@ -134,7 +134,7 @@ describe("config-loader", () => {
134
134
  const result = extractCustomTheme("/project/src/file.ts");
135
135
 
136
136
  // Without actual config loading, this returns empty
137
- expect(result).toEqual({ colors: {}, fontFamily: {} });
137
+ expect(result).toEqual({ colors: {}, fontFamily: {}, fontSize: {} });
138
138
  });
139
139
 
140
140
  it("should extract colors and fontFamily from theme.extend", () => {
@@ -14,9 +14,11 @@ export type TailwindConfig = {
14
14
  extend?: {
15
15
  colors?: Record<string, string | Record<string, string>>;
16
16
  fontFamily?: Record<string, string | string[]>;
17
+ fontSize?: Record<string, string | number>;
17
18
  };
18
19
  colors?: Record<string, string | Record<string, string>>;
19
20
  fontFamily?: Record<string, string | string[]>;
21
+ fontSize?: Record<string, string | number>;
20
22
  };
21
23
  };
22
24
 
@@ -89,6 +91,7 @@ export function loadTailwindConfig(configPath: string): TailwindConfig | null {
89
91
  export type CustomTheme = {
90
92
  colors: Record<string, string>;
91
93
  fontFamily: Record<string, string>;
94
+ fontSize: Record<string, number>;
92
95
  };
93
96
 
94
97
  /**
@@ -100,12 +103,12 @@ export function extractCustomTheme(filename: string): CustomTheme {
100
103
  const configPath = findTailwindConfig(projectDir);
101
104
 
102
105
  if (!configPath) {
103
- return { colors: {}, fontFamily: {} };
106
+ return { colors: {}, fontFamily: {}, fontSize: {} };
104
107
  }
105
108
 
106
109
  const config = loadTailwindConfig(configPath);
107
110
  if (!config?.theme) {
108
- return { colors: {}, fontFamily: {} };
111
+ return { colors: {}, fontFamily: {}, fontSize: {} };
109
112
  }
110
113
 
111
114
  // Extract colors
@@ -139,8 +142,40 @@ export function extractCustomTheme(filename: string): CustomTheme {
139
142
  }
140
143
  }
141
144
 
145
+ // Extract fontSize
146
+ /* v8 ignore next 5 */
147
+ if (config.theme.fontSize && !config.theme.extend?.fontSize && process.env.NODE_ENV !== "production") {
148
+ console.warn(
149
+ "[react-native-tailwind] Using theme.fontSize will override all default font sizes. " +
150
+ "Use theme.extend.fontSize to add custom font sizes while keeping defaults.",
151
+ );
152
+ }
153
+ const fontSize = config.theme.extend?.fontSize ?? config.theme.fontSize ?? {};
154
+
155
+ // Convert fontSize values to numbers (handle string or number values)
156
+ const fontSizeResult: Record<string, number> = {};
157
+ for (const [key, value] of Object.entries(fontSize)) {
158
+ if (typeof value === "number") {
159
+ fontSizeResult[key] = value;
160
+ } else if (typeof value === "string") {
161
+ // Parse string values like "18px" or "18" to number
162
+ const parsed = parseFloat(value.replace(/px$/, ""));
163
+ if (!isNaN(parsed)) {
164
+ fontSizeResult[key] = parsed;
165
+ } else {
166
+ /* v8 ignore next 5 */
167
+ if (process.env.NODE_ENV !== "production") {
168
+ console.warn(
169
+ `[react-native-tailwind] Invalid fontSize value for "${key}": ${value}. Expected number or string like "18px".`,
170
+ );
171
+ }
172
+ }
173
+ }
174
+ }
175
+
142
176
  return {
143
177
  colors: flattenColors(colors),
144
178
  fontFamily: fontFamilyResult,
179
+ fontSize: fontSizeResult,
145
180
  };
146
181
  }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Component scope detection helpers for hook injection
3
+ */
4
+
5
+ import type { NodePath } from "@babel/core";
6
+ import type * as BabelTypes from "@babel/types";
7
+
8
+ /**
9
+ * Check if a function path represents a valid component scope for hook injection
10
+ * Valid scopes:
11
+ * - Top-level FunctionDeclaration
12
+ * - FunctionExpression/ArrowFunctionExpression in top-level VariableDeclarator (with PascalCase name)
13
+ * - NOT class methods, NOT nested functions, NOT inline callbacks
14
+ *
15
+ * @param functionPath - Path to the function to check
16
+ * @param t - Babel types
17
+ * @returns true if function is a valid component scope
18
+ */
19
+ export function isComponentScope(functionPath: NodePath<BabelTypes.Function>, t: typeof BabelTypes): boolean {
20
+ const node = functionPath.node;
21
+ const parent = functionPath.parent;
22
+ const parentPath = functionPath.parentPath;
23
+
24
+ // Reject class methods (class components not supported for hooks)
25
+ if (t.isClassMethod(parent)) {
26
+ return false;
27
+ }
28
+
29
+ // Reject if inside a class body
30
+ if (functionPath.findParent((p) => t.isClassBody(p.node))) {
31
+ return false;
32
+ }
33
+
34
+ // Accept top-level FunctionDeclaration
35
+ if (t.isFunctionDeclaration(node)) {
36
+ // Check if it's at program level or in export
37
+ if (t.isProgram(parent) || t.isExportNamedDeclaration(parent) || t.isExportDefaultDeclaration(parent)) {
38
+ return true;
39
+ }
40
+ }
41
+
42
+ // Accept FunctionExpression/ArrowFunctionExpression in VariableDeclarator
43
+ if (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) {
44
+ if (t.isVariableDeclarator(parent)) {
45
+ // Check if it's at program level (via VariableDeclaration)
46
+ const varDeclarationPath = parentPath?.parentPath;
47
+ if (
48
+ varDeclarationPath &&
49
+ t.isVariableDeclaration(varDeclarationPath.node) &&
50
+ (t.isProgram(varDeclarationPath.parent) || t.isExportNamedDeclaration(varDeclarationPath.parent))
51
+ ) {
52
+ // Check for PascalCase naming (component convention)
53
+ if (t.isIdentifier(parent.id)) {
54
+ const name = parent.id.name;
55
+ return /^[A-Z]/.test(name); // Starts with uppercase
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ return false;
62
+ }
63
+
64
+ /**
65
+ * Find the nearest valid component scope for hook injection
66
+ * Climbs the AST from the current path to find a component-level function
67
+ *
68
+ * @param path - Starting path (e.g., JSXAttribute)
69
+ * @param t - Babel types
70
+ * @returns NodePath to component function, or null if not found
71
+ */
72
+ export function findComponentScope(
73
+ path: NodePath,
74
+ t: typeof BabelTypes,
75
+ ): NodePath<BabelTypes.Function> | null {
76
+ let current = path.getFunctionParent();
77
+
78
+ while (current) {
79
+ if (t.isFunction(current.node) && isComponentScope(current, t)) {
80
+ return current;
81
+ }
82
+ // Climb to next parent function
83
+ current = current.getFunctionParent();
84
+ }
85
+
86
+ return null;
87
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Plugin state and options types
3
+ */
4
+
5
+ import type { NodePath, PluginPass } from "@babel/core";
6
+ import type * as BabelTypes from "@babel/types";
7
+ import type { SchemeModifierConfig } from "../../types/config.js";
8
+ import type { StyleObject } from "../../types/core.js";
9
+ import type { CustomTheme } from "../config-loader.js";
10
+ import { extractCustomTheme } from "../config-loader.js";
11
+ import { DEFAULT_CLASS_ATTRIBUTES, buildAttributeMatchers } from "../utils/attributeMatchers.js";
12
+
13
+ /**
14
+ * Plugin options
15
+ */
16
+ export type PluginOptions = {
17
+ /**
18
+ * List of JSX attribute names to transform (in addition to or instead of 'className')
19
+ * Supports exact matches and glob patterns:
20
+ * - Exact: 'className', 'containerClassName'
21
+ * - Glob: '*ClassName' (matches any attribute ending in 'ClassName')
22
+ *
23
+ * @default ['className', 'contentContainerClassName', 'columnWrapperClassName', 'ListHeaderComponentClassName', 'ListFooterComponentClassName']
24
+ */
25
+ attributes?: string[];
26
+
27
+ /**
28
+ * Custom identifier name for the generated StyleSheet constant
29
+ *
30
+ * @default '_twStyles'
31
+ */
32
+ stylesIdentifier?: string;
33
+
34
+ /**
35
+ * Configuration for the scheme: modifier that expands to both dark: and light: modifiers
36
+ *
37
+ * @example
38
+ * {
39
+ * darkSuffix: '-dark', // scheme:bg-primary -> dark:bg-primary-dark
40
+ * lightSuffix: '-light' // scheme:bg-primary -> light:bg-primary-light
41
+ * }
42
+ *
43
+ * @default { darkSuffix: '-dark', lightSuffix: '-light' }
44
+ */
45
+ schemeModifier?: {
46
+ darkSuffix?: string;
47
+ lightSuffix?: string;
48
+ };
49
+
50
+ /**
51
+ * Configuration for color scheme hook import (dark:/light: modifiers)
52
+ *
53
+ * Allows using custom color scheme hooks from theme providers instead of
54
+ * React Native's built-in useColorScheme.
55
+ *
56
+ * @example
57
+ * // Use custom hook from theme provider
58
+ * {
59
+ * importFrom: '@/hooks/useColorScheme',
60
+ * importName: 'useColorScheme'
61
+ * }
62
+ *
63
+ * @example
64
+ * // Use React Navigation theme
65
+ * {
66
+ * importFrom: '@react-navigation/native',
67
+ * importName: 'useTheme' // You'd wrap this to return ColorSchemeName
68
+ * }
69
+ *
70
+ * @default { importFrom: 'react-native', importName: 'useColorScheme' }
71
+ */
72
+ colorScheme?: {
73
+ /**
74
+ * Module to import the color scheme hook from
75
+ * @default 'react-native'
76
+ */
77
+ importFrom?: string;
78
+
79
+ /**
80
+ * Name of the hook to import
81
+ * @default 'useColorScheme'
82
+ */
83
+ importName?: string;
84
+ };
85
+ };
86
+
87
+ /**
88
+ * Plugin state - passed through all visitors
89
+ */
90
+ export type PluginState = PluginPass & {
91
+ styleRegistry: Map<string, StyleObject>;
92
+ hasClassNames: boolean;
93
+ hasStyleSheetImport: boolean;
94
+ hasPlatformImport: boolean;
95
+ needsPlatformImport: boolean;
96
+ hasColorSchemeImport: boolean;
97
+ needsColorSchemeImport: boolean;
98
+ colorSchemeVariableName: string;
99
+ colorSchemeImportSource: string; // Where to import the hook from (e.g., 'react-native')
100
+ colorSchemeHookName: string; // Name of the hook to import (e.g., 'useColorScheme')
101
+ colorSchemeLocalIdentifier?: string; // Local identifier if hook is already imported with an alias
102
+ hasWindowDimensionsImport: boolean;
103
+ needsWindowDimensionsImport: boolean;
104
+ windowDimensionsVariableName: string;
105
+ windowDimensionsLocalIdentifier?: string; // Local identifier if hook is already imported with an alias
106
+ customTheme: CustomTheme;
107
+ schemeModifierConfig: SchemeModifierConfig;
108
+ supportedAttributes: Set<string>;
109
+ attributePatterns: RegExp[];
110
+ stylesIdentifier: string;
111
+ // Track tw/twStyle imports from main package
112
+ twImportNames: Set<string>; // e.g., ['tw', 'twStyle'] or ['tw as customTw']
113
+ hasTwImport: boolean;
114
+ // Track react-native import path for conditional StyleSheet/Platform injection
115
+ reactNativeImportPath?: NodePath<BabelTypes.ImportDeclaration>;
116
+ // Track function components that need colorScheme hook injection
117
+ functionComponentsNeedingColorScheme: Set<NodePath<BabelTypes.Function>>;
118
+ // Track function components that need windowDimensions hook injection
119
+ functionComponentsNeedingWindowDimensions: Set<NodePath<BabelTypes.Function>>;
120
+ };
121
+
122
+ // Default identifier for the generated StyleSheet constant
123
+ export const DEFAULT_STYLES_IDENTIFIER = "_twStyles";
124
+
125
+ /**
126
+ * Create initial plugin state for a file
127
+ *
128
+ * @param options - Plugin options from babel config
129
+ * @param filename - Current file being processed
130
+ * @param colorSchemeImportSource - Where to import the color scheme hook from
131
+ * @param colorSchemeHookName - Name of the color scheme hook to import
132
+ * @param schemeModifierConfig - Configuration for scheme: modifier expansion
133
+ * @returns Initial plugin state
134
+ */
135
+ export function createInitialState(
136
+ options: PluginOptions | undefined,
137
+ filename: string,
138
+ colorSchemeImportSource: string,
139
+ colorSchemeHookName: string,
140
+ schemeModifierConfig: SchemeModifierConfig,
141
+ ): Partial<PluginState> {
142
+ // Build attribute matchers from options
143
+ const attributes = options?.attributes ?? [...DEFAULT_CLASS_ATTRIBUTES];
144
+ const { exactMatches, patterns } = buildAttributeMatchers(attributes);
145
+ const stylesIdentifier = options?.stylesIdentifier ?? DEFAULT_STYLES_IDENTIFIER;
146
+
147
+ // Load custom theme from tailwind.config.*
148
+ const customTheme = extractCustomTheme(filename);
149
+
150
+ return {
151
+ styleRegistry: new Map(),
152
+ hasClassNames: false,
153
+ hasStyleSheetImport: false,
154
+ hasPlatformImport: false,
155
+ needsPlatformImport: false,
156
+ hasColorSchemeImport: false,
157
+ needsColorSchemeImport: false,
158
+ colorSchemeVariableName: "_twColorScheme",
159
+ colorSchemeImportSource,
160
+ colorSchemeHookName,
161
+ colorSchemeLocalIdentifier: undefined,
162
+ hasWindowDimensionsImport: false,
163
+ needsWindowDimensionsImport: false,
164
+ windowDimensionsVariableName: "_twDimensions",
165
+ windowDimensionsLocalIdentifier: undefined,
166
+ customTheme,
167
+ schemeModifierConfig,
168
+ supportedAttributes: exactMatches,
169
+ attributePatterns: patterns,
170
+ stylesIdentifier,
171
+ twImportNames: new Set(),
172
+ hasTwImport: false,
173
+ reactNativeImportPath: undefined,
174
+ functionComponentsNeedingColorScheme: new Set(),
175
+ functionComponentsNeedingWindowDimensions: new Set(),
176
+ };
177
+ }