@mgcrea/react-native-tailwind 0.7.0 → 0.8.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 (81) hide show
  1. package/README.md +2 -1
  2. package/dist/babel/index.cjs +333 -195
  3. package/dist/babel/index.d.ts +4 -40
  4. package/dist/babel/index.test.ts +214 -1
  5. package/dist/babel/index.ts +4 -1169
  6. package/dist/babel/plugin.d.ts +42 -0
  7. package/{src/babel/index.test.ts → dist/babel/plugin.test.ts} +216 -2
  8. package/dist/babel/plugin.ts +491 -0
  9. package/dist/babel/utils/attributeMatchers.d.ts +23 -0
  10. package/dist/babel/utils/attributeMatchers.ts +71 -0
  11. package/dist/babel/utils/componentSupport.d.ts +18 -0
  12. package/dist/babel/utils/componentSupport.ts +68 -0
  13. package/dist/babel/utils/dynamicProcessing.d.ts +32 -0
  14. package/dist/babel/utils/dynamicProcessing.ts +223 -0
  15. package/dist/babel/utils/modifierProcessing.d.ts +26 -0
  16. package/dist/babel/utils/modifierProcessing.ts +118 -0
  17. package/dist/babel/utils/styleInjection.d.ts +15 -0
  18. package/dist/babel/utils/styleInjection.ts +80 -0
  19. package/dist/babel/utils/styleTransforms.d.ts +39 -0
  20. package/dist/babel/utils/styleTransforms.test.ts +349 -0
  21. package/dist/babel/utils/styleTransforms.ts +258 -0
  22. package/dist/babel/utils/twProcessing.d.ts +28 -0
  23. package/dist/babel/utils/twProcessing.ts +124 -0
  24. package/dist/components/TextInput.d.ts +171 -14
  25. package/dist/config/tailwind.d.ts +302 -0
  26. package/dist/config/tailwind.js +1 -0
  27. package/dist/index.d.ts +5 -4
  28. package/dist/index.js +1 -1
  29. package/dist/parser/colors.js +1 -1
  30. package/dist/parser/index.d.ts +1 -0
  31. package/dist/parser/index.js +1 -1
  32. package/dist/parser/modifiers.d.ts +2 -2
  33. package/dist/parser/modifiers.js +1 -1
  34. package/dist/parser/placeholder.d.ts +36 -0
  35. package/dist/parser/placeholder.js +1 -0
  36. package/dist/parser/placeholder.test.js +1 -0
  37. package/dist/parser/typography.d.ts +1 -0
  38. package/dist/parser/typography.js +1 -1
  39. package/dist/parser/typography.test.js +1 -1
  40. package/dist/runtime.cjs +1 -1
  41. package/dist/runtime.cjs.map +4 -4
  42. package/dist/runtime.d.ts +1 -14
  43. package/dist/runtime.js +1 -1
  44. package/dist/runtime.js.map +4 -4
  45. package/dist/stubs/tw.d.ts +1 -14
  46. package/dist/types/core.d.ts +40 -0
  47. package/dist/types/core.js +0 -0
  48. package/dist/types/index.d.ts +2 -0
  49. package/dist/types/index.js +1 -0
  50. package/dist/types/runtime.d.ts +15 -0
  51. package/dist/types/runtime.js +1 -0
  52. package/dist/types/util.d.ts +3 -0
  53. package/dist/types/util.js +0 -0
  54. package/package.json +1 -1
  55. package/src/babel/index.ts +4 -1169
  56. package/src/babel/plugin.test.ts +482 -0
  57. package/src/babel/plugin.ts +491 -0
  58. package/src/babel/utils/attributeMatchers.ts +71 -0
  59. package/src/babel/utils/componentSupport.ts +68 -0
  60. package/src/babel/utils/dynamicProcessing.ts +223 -0
  61. package/src/babel/utils/modifierProcessing.ts +118 -0
  62. package/src/babel/utils/styleInjection.ts +80 -0
  63. package/src/babel/utils/styleTransforms.test.ts +349 -0
  64. package/src/babel/utils/styleTransforms.ts +258 -0
  65. package/src/babel/utils/twProcessing.ts +124 -0
  66. package/src/components/TextInput.tsx +17 -14
  67. package/src/config/{palettes.ts → tailwind.ts} +2 -2
  68. package/src/index.ts +6 -3
  69. package/src/parser/colors.ts +2 -2
  70. package/src/parser/index.ts +1 -0
  71. package/src/parser/modifiers.ts +10 -4
  72. package/src/parser/placeholder.test.ts +105 -0
  73. package/src/parser/placeholder.ts +78 -0
  74. package/src/parser/typography.test.ts +11 -0
  75. package/src/parser/typography.ts +20 -2
  76. package/src/runtime.ts +1 -16
  77. package/src/stubs/tw.ts +1 -16
  78. package/src/{types.ts → types/core.ts} +0 -4
  79. package/src/types/index.ts +2 -0
  80. package/src/types/runtime.ts +17 -0
  81. package/src/types/util.ts +1 -0
@@ -0,0 +1,349 @@
1
+ import { transformSync } from "@babel/core";
2
+ import { describe, expect, it } from "vitest";
3
+ import babelPlugin from "../plugin.js";
4
+
5
+ /**
6
+ * Helper to transform code with the Babel plugin
7
+ */
8
+ function transform(code: string) {
9
+ const result = transformSync(code, {
10
+ presets: ["@babel/preset-react"],
11
+ plugins: [babelPlugin],
12
+ filename: "test.tsx",
13
+ configFile: false,
14
+ babelrc: false,
15
+ });
16
+
17
+ return result?.code ?? "";
18
+ }
19
+
20
+ describe("Style merging - mergeStyleAttribute", () => {
21
+ it("should merge className with identifier style prop (object variable)", () => {
22
+ const input = `
23
+ import { TouchableOpacity } from 'react-native';
24
+ export function Component() {
25
+ const style = { marginHorizontal: 24 };
26
+ return (
27
+ <TouchableOpacity
28
+ className="m-4"
29
+ style={style}
30
+ activeOpacity={0.8}
31
+ />
32
+ );
33
+ }
34
+ `;
35
+
36
+ const output = transform(input);
37
+
38
+ // Should have StyleSheet with className styles
39
+ expect(output).toContain("StyleSheet.create");
40
+ expect(output).toContain("_m_4");
41
+
42
+ // Should create a simple array merge, NOT a function wrapper
43
+ expect(output).toContain("[_twStyles._m_4, style]");
44
+
45
+ // Should NOT have typeof check or _state parameter
46
+ expect(output).not.toContain("typeof");
47
+ expect(output).not.toContain("_state");
48
+
49
+ // Should not have className in output
50
+ expect(output).not.toContain("className");
51
+ });
52
+
53
+ it("should merge className with member expression style prop", () => {
54
+ const input = `
55
+ import { View } from 'react-native';
56
+ export function Component(props) {
57
+ return (
58
+ <View
59
+ className="p-2 bg-blue-500"
60
+ style={props.style}
61
+ />
62
+ );
63
+ }
64
+ `;
65
+
66
+ const output = transform(input);
67
+
68
+ // Should have StyleSheet with className styles
69
+ expect(output).toContain("StyleSheet.create");
70
+ expect(output).toContain("_bg_blue_500_p_2");
71
+
72
+ // Should create a simple array merge, NOT a function wrapper
73
+ expect(output).toContain("[_twStyles._bg_blue_500_p_2, props.style]");
74
+
75
+ // Should NOT have typeof check or _state parameter
76
+ expect(output).not.toContain("typeof");
77
+ expect(output).not.toContain("_state");
78
+
79
+ // Should not have className in output
80
+ expect(output).not.toContain("className");
81
+ });
82
+
83
+ it("should merge className with array style prop", () => {
84
+ const input = `
85
+ import { View } from 'react-native';
86
+ export function Component() {
87
+ return (
88
+ <View
89
+ className="m-4"
90
+ style={[baseStyle, conditionalStyle]}
91
+ />
92
+ );
93
+ }
94
+ `;
95
+
96
+ const output = transform(input);
97
+
98
+ // Should have StyleSheet with className styles
99
+ expect(output).toContain("StyleSheet.create");
100
+ expect(output).toContain("_m_4");
101
+
102
+ // Should create array merge without function wrapper
103
+ expect(output).toContain("_twStyles._m_4");
104
+
105
+ // Should NOT have typeof check or _state parameter
106
+ expect(output).not.toContain("typeof");
107
+ expect(output).not.toContain("_state");
108
+
109
+ // Should not have className in output
110
+ expect(output).not.toContain("className");
111
+ });
112
+
113
+ it("should merge className with inline object style prop", () => {
114
+ const input = `
115
+ import { View } from 'react-native';
116
+ export function Component() {
117
+ return (
118
+ <View
119
+ className="m-4 p-2"
120
+ style={{ backgroundColor: 'red' }}
121
+ />
122
+ );
123
+ }
124
+ `;
125
+
126
+ const output = transform(input);
127
+
128
+ // Should have StyleSheet with className styles
129
+ expect(output).toContain("StyleSheet.create");
130
+ expect(output).toContain("_m_4_p_2");
131
+
132
+ // Should create array merge
133
+ expect(output).toContain("_twStyles._m_4_p_2");
134
+ expect(output).toContain("backgroundColor:");
135
+
136
+ // Should not have className in output
137
+ expect(output).not.toContain("className");
138
+ });
139
+
140
+ it("should merge className with inline function style prop", () => {
141
+ const input = `
142
+ import { TextInput } from 'react-native';
143
+ export function Component() {
144
+ return (
145
+ <TextInput
146
+ className="border border-gray-300"
147
+ style={({ focused, disabled }) => [
148
+ baseStyles,
149
+ focused && focusedStyles,
150
+ ]}
151
+ />
152
+ );
153
+ }
154
+ `;
155
+
156
+ const output = transform(input);
157
+
158
+ // Should have StyleSheet with className styles
159
+ expect(output).toContain("StyleSheet.create");
160
+ expect(output).toContain("_border_border_gray_300");
161
+
162
+ // Should create a wrapper function that merges both
163
+ expect(output).toContain("_state");
164
+ expect(output).toContain("_twStyles._border_border_gray_300");
165
+
166
+ // Should have a function that accepts state and returns an array
167
+ expect(output).toMatch(/_state\s*=>/);
168
+
169
+ // Should not have className in output
170
+ expect(output).not.toContain("className");
171
+ });
172
+ });
173
+
174
+ describe("Style merging - mergeDynamicStyleAttribute", () => {
175
+ it("should merge dynamic className with identifier style prop", () => {
176
+ const input = `
177
+ import { View } from 'react-native';
178
+ export function Component({ isActive }) {
179
+ const customStyle = { opacity: 0.8 };
180
+ return (
181
+ <View
182
+ className={\`p-2 \${isActive ? 'bg-blue-500' : 'bg-gray-300'}\`}
183
+ style={customStyle}
184
+ />
185
+ );
186
+ }
187
+ `;
188
+
189
+ const output = transform(input);
190
+
191
+ // Should have StyleSheet with both className styles
192
+ expect(output).toContain("StyleSheet.create");
193
+ expect(output).toContain("_p_2");
194
+ expect(output).toContain("_bg_blue_500");
195
+ expect(output).toContain("_bg_gray_300");
196
+
197
+ // Should create array merge without function wrapper
198
+ expect(output).toContain("customStyle");
199
+
200
+ // Should NOT have typeof check or _state parameter for the style merge
201
+ expect(output).not.toContain("typeof");
202
+ expect(output).not.toContain("_state");
203
+
204
+ // Should not have className in output
205
+ expect(output).not.toContain("className");
206
+ });
207
+
208
+ it("should merge dynamic className with inline function style prop", () => {
209
+ const input = `
210
+ import { TextInput } from 'react-native';
211
+ export function Component({ isError }) {
212
+ return (
213
+ <TextInput
214
+ className={\`border \${isError ? 'border-red-500' : 'border-gray-300'}\`}
215
+ style={({ focused }) => [
216
+ baseStyles,
217
+ focused && focusedStyles,
218
+ ]}
219
+ />
220
+ );
221
+ }
222
+ `;
223
+
224
+ const output = transform(input);
225
+
226
+ // Should have StyleSheet with both className styles
227
+ expect(output).toContain("StyleSheet.create");
228
+ expect(output).toContain("_border");
229
+ expect(output).toContain("_border_red_500");
230
+ expect(output).toContain("_border_gray_300");
231
+
232
+ // Should create a wrapper function that merges dynamic styles with function result
233
+ expect(output).toContain("_state");
234
+
235
+ // Should not have className in output
236
+ expect(output).not.toContain("className");
237
+ });
238
+
239
+ it("should merge dynamic className with member expression style prop", () => {
240
+ const input = `
241
+ import { View } from 'react-native';
242
+ export function Component({ variant, ...props }) {
243
+ return (
244
+ <View
245
+ className={\`m-2 \${variant === 'primary' ? 'bg-blue-500' : 'bg-gray-300'}\`}
246
+ style={props.style}
247
+ />
248
+ );
249
+ }
250
+ `;
251
+
252
+ const output = transform(input);
253
+
254
+ // Should have StyleSheet with both className styles
255
+ expect(output).toContain("StyleSheet.create");
256
+ expect(output).toContain("_m_2");
257
+ expect(output).toContain("_bg_blue_500");
258
+ expect(output).toContain("_bg_gray_300");
259
+
260
+ // Should create array merge without function wrapper
261
+ expect(output).toContain("props.style");
262
+
263
+ // Should NOT have typeof check or _state parameter for the style merge
264
+ expect(output).not.toContain("typeof");
265
+ expect(output).not.toContain("_state");
266
+
267
+ // Should not have className in output
268
+ expect(output).not.toContain("className");
269
+ });
270
+ });
271
+
272
+ describe("Style merging - edge cases", () => {
273
+ it("should handle className without existing style prop", () => {
274
+ const input = `
275
+ import { View } from 'react-native';
276
+ export function Component() {
277
+ return <View className="m-4 p-2" />;
278
+ }
279
+ `;
280
+
281
+ const output = transform(input);
282
+
283
+ // Should have StyleSheet with className styles
284
+ expect(output).toContain("StyleSheet.create");
285
+ expect(output).toContain("_m_4_p_2");
286
+
287
+ // Should have style prop with reference to stylesheet
288
+ expect(output).toContain("style: _twStyles._m_4_p_2");
289
+
290
+ // Should not have className in output
291
+ expect(output).not.toContain("className");
292
+ });
293
+
294
+ it("should handle empty className", () => {
295
+ const input = `
296
+ import { View } from 'react-native';
297
+ export function Component() {
298
+ return <View className="" style={myStyle} />;
299
+ }
300
+ `;
301
+
302
+ const output = transform(input);
303
+
304
+ // Should not create StyleSheet
305
+ expect(output).not.toContain("StyleSheet.create");
306
+
307
+ // Should preserve original style prop
308
+ expect(output).toContain("style: myStyle");
309
+
310
+ // Should not have className in output
311
+ expect(output).not.toContain("className");
312
+ });
313
+
314
+ it("should handle multiple components with different merge scenarios", () => {
315
+ const input = `
316
+ import { View, TouchableOpacity } from 'react-native';
317
+ export function Component() {
318
+ const style1 = { opacity: 0.5 };
319
+ return (
320
+ <>
321
+ <View className="m-4" style={style1} />
322
+ <TouchableOpacity className="p-2" />
323
+ <View className="bg-red-500" style={[baseStyle]} />
324
+ </>
325
+ );
326
+ }
327
+ `;
328
+
329
+ const output = transform(input);
330
+
331
+ // Should have StyleSheet with all className styles
332
+ expect(output).toContain("StyleSheet.create");
333
+ expect(output).toContain("_m_4");
334
+ expect(output).toContain("_p_2");
335
+ expect(output).toContain("_bg_red_500");
336
+
337
+ // Should handle identifier merge
338
+ expect(output).toContain("[_twStyles._m_4, style1]");
339
+
340
+ // Should handle no merge
341
+ expect(output).toContain("_twStyles._p_2");
342
+
343
+ // Should handle array merge
344
+ expect(output).toContain("_twStyles._bg_red_500");
345
+
346
+ // Should not have className in output
347
+ expect(output).not.toContain("className");
348
+ });
349
+ });
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Utility functions for transforming and merging style attributes
3
+ */
4
+
5
+ import type { NodePath } from "@babel/core";
6
+ import type * as BabelTypes from "@babel/types";
7
+ import type { DynamicExpressionResult } from "./dynamicProcessing.js";
8
+
9
+ /**
10
+ * Helper to extract expression from JSX attribute value
11
+ * Returns null if not a valid expression container or if empty
12
+ */
13
+ function getStyleExpression(
14
+ styleAttribute: BabelTypes.JSXAttribute,
15
+ t: typeof BabelTypes,
16
+ ): BabelTypes.Expression | null {
17
+ const value = styleAttribute.value;
18
+ if (!t.isJSXExpressionContainer(value)) return null;
19
+ const expression = value.expression;
20
+ if (t.isJSXEmptyExpression(expression)) return null;
21
+ return expression;
22
+ }
23
+
24
+ /**
25
+ * Helper to find existing style attribute on parent JSX element
26
+ */
27
+ export function findStyleAttribute(
28
+ path: NodePath,
29
+ targetStyleProp: string,
30
+ t: typeof BabelTypes,
31
+ ): BabelTypes.JSXAttribute | undefined {
32
+ const parent = path.parent as BabelTypes.JSXOpeningElement;
33
+ return parent.attributes.find(
34
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === targetStyleProp,
35
+ ) as BabelTypes.JSXAttribute | undefined;
36
+ }
37
+
38
+ /**
39
+ * Replace className with style attribute
40
+ */
41
+ export function replaceWithStyleAttribute(
42
+ classNamePath: NodePath,
43
+ styleKey: string,
44
+ targetStyleProp: string,
45
+ stylesIdentifier: string,
46
+ t: typeof BabelTypes,
47
+ ): void {
48
+ const styleAttribute = t.jsxAttribute(
49
+ t.jsxIdentifier(targetStyleProp),
50
+ t.jsxExpressionContainer(t.memberExpression(t.identifier(stylesIdentifier), t.identifier(styleKey))),
51
+ );
52
+
53
+ classNamePath.replaceWith(styleAttribute);
54
+ }
55
+
56
+ /**
57
+ * Merge className styles with existing style prop
58
+ */
59
+ export function mergeStyleAttribute(
60
+ classNamePath: NodePath,
61
+ styleAttribute: BabelTypes.JSXAttribute,
62
+ styleKey: string,
63
+ stylesIdentifier: string,
64
+ t: typeof BabelTypes,
65
+ ): void {
66
+ const existingStyle = getStyleExpression(styleAttribute, t);
67
+ if (!existingStyle) return;
68
+
69
+ // Check if existing style is definitely a function expression (inline)
70
+ if (t.isArrowFunctionExpression(existingStyle) || t.isFunctionExpression(existingStyle)) {
71
+ // Existing style is a function - create wrapper that calls it and merges results
72
+ // (_state) => [styles._key, existingStyleFn(_state)]
73
+ const paramIdentifier = t.identifier("_state");
74
+ const functionCall = t.callExpression(existingStyle, [paramIdentifier]);
75
+
76
+ const mergedArray = t.arrayExpression([
77
+ t.memberExpression(t.identifier(stylesIdentifier), t.identifier(styleKey)),
78
+ functionCall,
79
+ ]);
80
+ const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
81
+
82
+ styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
83
+ } else {
84
+ // For identifiers, member expressions, and other static values:
85
+ // Always use simple array merge - don't generate function wrappers
86
+ // React Native components (except Pressable) don't accept style functions,
87
+ // and even for Pressable, if the user passes a variable, we should trust
88
+ // their type and merge it as-is rather than wrapping in a runtime check.
89
+ //
90
+ // This fixes the issue where `style={myStyle}` would be incorrectly wrapped
91
+ // in a function when used with className.
92
+ const styleArray = t.arrayExpression([
93
+ t.memberExpression(t.identifier(stylesIdentifier), t.identifier(styleKey)),
94
+ existingStyle,
95
+ ]);
96
+
97
+ styleAttribute.value = t.jsxExpressionContainer(styleArray);
98
+ }
99
+
100
+ // Remove the className attribute
101
+ classNamePath.remove();
102
+ }
103
+
104
+ /**
105
+ * Replace className with dynamic style attribute
106
+ */
107
+ export function replaceDynamicWithStyleAttribute(
108
+ classNamePath: NodePath,
109
+ result: DynamicExpressionResult,
110
+ targetStyleProp: string,
111
+ t: typeof BabelTypes,
112
+ ): void {
113
+ const styleAttribute = t.jsxAttribute(
114
+ t.jsxIdentifier(targetStyleProp),
115
+ t.jsxExpressionContainer(result.expression),
116
+ );
117
+
118
+ classNamePath.replaceWith(styleAttribute);
119
+ }
120
+
121
+ /**
122
+ * Merge dynamic className styles with existing style prop
123
+ */
124
+ export function mergeDynamicStyleAttribute(
125
+ classNamePath: NodePath,
126
+ styleAttribute: BabelTypes.JSXAttribute,
127
+ result: DynamicExpressionResult,
128
+ t: typeof BabelTypes,
129
+ ): void {
130
+ const existingStyle = getStyleExpression(styleAttribute, t);
131
+ if (!existingStyle) return;
132
+
133
+ // Check if existing style is definitely a function expression (inline)
134
+ if (t.isArrowFunctionExpression(existingStyle) || t.isFunctionExpression(existingStyle)) {
135
+ // Existing style is a function - create wrapper that calls it and merges results
136
+ // (_state) => [dynamicStyles, existingStyleFn(_state)]
137
+ const paramIdentifier = t.identifier("_state");
138
+ const functionCall = t.callExpression(existingStyle, [paramIdentifier]);
139
+
140
+ const mergedArray = t.arrayExpression([result.expression, functionCall]);
141
+ const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
142
+
143
+ styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
144
+ } else {
145
+ // For identifiers, member expressions, and other values:
146
+ // Always use simple array merge - don't generate function wrappers
147
+ // This matches the behavior of mergeStyleAttribute for consistency
148
+ let styleArray;
149
+ if (t.isArrayExpression(existingStyle)) {
150
+ // Prepend dynamic styles to existing array
151
+ styleArray = t.arrayExpression([result.expression, ...existingStyle.elements]);
152
+ } else {
153
+ // Create new array with dynamic styles first, then existing
154
+ styleArray = t.arrayExpression([result.expression, existingStyle]);
155
+ }
156
+
157
+ styleAttribute.value = t.jsxExpressionContainer(styleArray);
158
+ }
159
+
160
+ // Remove the className attribute
161
+ classNamePath.remove();
162
+ }
163
+
164
+ /**
165
+ * Replace className with style function attribute (for Pressable with modifiers)
166
+ */
167
+ export function replaceWithStyleFunctionAttribute(
168
+ classNamePath: NodePath,
169
+ styleFunctionExpression: BabelTypes.Expression,
170
+ targetStyleProp: string,
171
+ t: typeof BabelTypes,
172
+ ): void {
173
+ const styleAttribute = t.jsxAttribute(
174
+ t.jsxIdentifier(targetStyleProp),
175
+ t.jsxExpressionContainer(styleFunctionExpression),
176
+ );
177
+
178
+ classNamePath.replaceWith(styleAttribute);
179
+ }
180
+
181
+ /**
182
+ * Merge className style function with existing style prop (for Pressable with modifiers)
183
+ */
184
+ export function mergeStyleFunctionAttribute(
185
+ classNamePath: NodePath,
186
+ styleAttribute: BabelTypes.JSXAttribute,
187
+ styleFunctionExpression: BabelTypes.Expression,
188
+ t: typeof BabelTypes,
189
+ ): void {
190
+ const existingStyle = getStyleExpression(styleAttribute, t);
191
+ if (!existingStyle) return;
192
+
193
+ // Create a wrapper function that merges both styles
194
+ // ({ pressed }) => [styleFunctionResult, existingStyle]
195
+ // We need to call the style function and merge results
196
+
197
+ // If existing is already a function, we need to handle it specially
198
+ if (t.isArrowFunctionExpression(existingStyle) || t.isFunctionExpression(existingStyle)) {
199
+ // Both are functions - create wrapper that calls both
200
+ // (_state) => [newStyleFn(_state), existingStyleFn(_state)]
201
+ // Create an identifier for the parameter to pass to the function calls
202
+ const paramIdentifier = t.identifier("_state");
203
+
204
+ const newFunctionCall = t.callExpression(styleFunctionExpression, [paramIdentifier]);
205
+ const existingFunctionCall = t.callExpression(existingStyle, [paramIdentifier]);
206
+
207
+ const mergedArray = t.arrayExpression([newFunctionCall, existingFunctionCall]);
208
+ const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
209
+
210
+ styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
211
+ } else {
212
+ // Existing is static - create function that returns array
213
+ // (_state) => [styleFunctionResult, existingStyle]
214
+ // Create an identifier for the parameter to pass to the function call
215
+ const paramIdentifier = t.identifier("_state");
216
+
217
+ const functionCall = t.callExpression(styleFunctionExpression, [paramIdentifier]);
218
+ const mergedArray = t.arrayExpression([functionCall, existingStyle]);
219
+ const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
220
+
221
+ styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
222
+ }
223
+
224
+ // Remove the className attribute
225
+ classNamePath.remove();
226
+ }
227
+
228
+ /**
229
+ * Add or merge placeholderTextColor prop on a JSX element
230
+ * Handles merging with existing placeholderTextColor if present
231
+ */
232
+ export function addOrMergePlaceholderTextColorProp(
233
+ jsxOpeningElement: BabelTypes.JSXOpeningElement,
234
+ color: string,
235
+ t: typeof BabelTypes,
236
+ ): void {
237
+ // Check if element already has placeholderTextColor prop
238
+ const existingProp = jsxOpeningElement.attributes.find(
239
+ (attr) => t.isJSXAttribute(attr) && attr.name.name === "placeholderTextColor",
240
+ );
241
+
242
+ if (existingProp) {
243
+ // If explicit prop exists, don't override it (explicit props take precedence)
244
+ // This matches the behavior of style prop precedence
245
+ if (process.env.NODE_ENV !== "production") {
246
+ console.warn(
247
+ `[react-native-tailwind] placeholderTextColor prop will be overridden by className placeholder: modifier. ` +
248
+ `Remove the explicit prop or the placeholder: modifier to avoid confusion.`,
249
+ );
250
+ }
251
+ // Override the existing prop value
252
+ (existingProp as BabelTypes.JSXAttribute).value = t.stringLiteral(color);
253
+ } else {
254
+ // Add new placeholderTextColor prop
255
+ const newProp = t.jsxAttribute(t.jsxIdentifier("placeholderTextColor"), t.stringLiteral(color));
256
+ jsxOpeningElement.attributes.push(newProp);
257
+ }
258
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Utility functions for processing tw`...` and twStyle() calls
3
+ */
4
+ import type { NodePath } from "@babel/core";
5
+ import type * as BabelTypes from "@babel/types";
6
+ import type { ParsedModifier } from "../../parser/index.js";
7
+ import type { StyleObject } from "../../types/core.js";
8
+ /**
9
+ * Plugin state interface (subset needed for tw processing)
10
+ */
11
+ export interface TwProcessingState {
12
+ styleRegistry: Map<string, StyleObject>;
13
+ customColors: Record<string, string>;
14
+ stylesIdentifier: string;
15
+ }
16
+ /**
17
+ * Process tw`...` or twStyle('...') call and replace with TwStyle object
18
+ * Generates: { style: styles._base, activeStyle: styles._active, ... }
19
+ */
20
+ export declare function processTwCall(className: string, path: NodePath, state: TwProcessingState, parseClassName: (className: string, customColors: Record<string, string>) => StyleObject, generateStyleKey: (className: string) => string, splitModifierClasses: (className: string) => {
21
+ baseClasses: string[];
22
+ modifierClasses: ParsedModifier[];
23
+ }, t: typeof BabelTypes): void;
24
+ /**
25
+ * Remove tw/twStyle imports from @mgcrea/react-native-tailwind
26
+ * This is called after all tw calls have been transformed
27
+ */
28
+ export declare function removeTwImports(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void;