@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.
- package/README.md +2 -1
- package/dist/babel/index.cjs +333 -195
- package/dist/babel/index.d.ts +4 -40
- package/dist/babel/index.test.ts +214 -1
- package/dist/babel/index.ts +4 -1169
- package/dist/babel/plugin.d.ts +42 -0
- package/{src/babel/index.test.ts → dist/babel/plugin.test.ts} +216 -2
- package/dist/babel/plugin.ts +491 -0
- package/dist/babel/utils/attributeMatchers.d.ts +23 -0
- package/dist/babel/utils/attributeMatchers.ts +71 -0
- package/dist/babel/utils/componentSupport.d.ts +18 -0
- package/dist/babel/utils/componentSupport.ts +68 -0
- package/dist/babel/utils/dynamicProcessing.d.ts +32 -0
- package/dist/babel/utils/dynamicProcessing.ts +223 -0
- package/dist/babel/utils/modifierProcessing.d.ts +26 -0
- package/dist/babel/utils/modifierProcessing.ts +118 -0
- package/dist/babel/utils/styleInjection.d.ts +15 -0
- package/dist/babel/utils/styleInjection.ts +80 -0
- package/dist/babel/utils/styleTransforms.d.ts +39 -0
- package/dist/babel/utils/styleTransforms.test.ts +349 -0
- package/dist/babel/utils/styleTransforms.ts +258 -0
- package/dist/babel/utils/twProcessing.d.ts +28 -0
- package/dist/babel/utils/twProcessing.ts +124 -0
- package/dist/components/TextInput.d.ts +171 -14
- package/dist/config/tailwind.d.ts +302 -0
- package/dist/config/tailwind.js +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -1
- package/dist/parser/colors.js +1 -1
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.js +1 -1
- package/dist/parser/modifiers.d.ts +2 -2
- package/dist/parser/modifiers.js +1 -1
- package/dist/parser/placeholder.d.ts +36 -0
- package/dist/parser/placeholder.js +1 -0
- package/dist/parser/placeholder.test.js +1 -0
- package/dist/parser/typography.d.ts +1 -0
- package/dist/parser/typography.js +1 -1
- package/dist/parser/typography.test.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +4 -4
- package/dist/runtime.d.ts +1 -14
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +4 -4
- package/dist/stubs/tw.d.ts +1 -14
- package/dist/types/core.d.ts +40 -0
- package/dist/types/core.js +0 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +1 -0
- package/dist/types/runtime.d.ts +15 -0
- package/dist/types/runtime.js +1 -0
- package/dist/types/util.d.ts +3 -0
- package/dist/types/util.js +0 -0
- package/package.json +1 -1
- package/src/babel/index.ts +4 -1169
- package/src/babel/plugin.test.ts +482 -0
- package/src/babel/plugin.ts +491 -0
- package/src/babel/utils/attributeMatchers.ts +71 -0
- package/src/babel/utils/componentSupport.ts +68 -0
- package/src/babel/utils/dynamicProcessing.ts +223 -0
- package/src/babel/utils/modifierProcessing.ts +118 -0
- package/src/babel/utils/styleInjection.ts +80 -0
- package/src/babel/utils/styleTransforms.test.ts +349 -0
- package/src/babel/utils/styleTransforms.ts +258 -0
- package/src/babel/utils/twProcessing.ts +124 -0
- package/src/components/TextInput.tsx +17 -14
- package/src/config/{palettes.ts → tailwind.ts} +2 -2
- package/src/index.ts +6 -3
- package/src/parser/colors.ts +2 -2
- package/src/parser/index.ts +1 -0
- package/src/parser/modifiers.ts +10 -4
- package/src/parser/placeholder.test.ts +105 -0
- package/src/parser/placeholder.ts +78 -0
- package/src/parser/typography.test.ts +11 -0
- package/src/parser/typography.ts +20 -2
- package/src/runtime.ts +1 -16
- package/src/stubs/tw.ts +1 -16
- package/src/{types.ts → types/core.ts} +0 -4
- package/src/types/index.ts +2 -0
- package/src/types/runtime.ts +17 -0
- 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;
|