@mgcrea/react-native-tailwind 0.12.1 → 0.14.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 +45 -2031
- package/dist/babel/index.cjs +1726 -1094
- package/dist/babel/plugin/componentScope.d.ts +26 -0
- package/dist/babel/plugin/componentScope.ts +87 -0
- package/dist/babel/plugin/state.d.ts +123 -0
- package/dist/babel/plugin/state.ts +185 -0
- package/dist/babel/plugin/visitors/className.d.ts +11 -0
- package/{src/babel/plugin.test.ts → dist/babel/plugin/visitors/className.test.ts} +285 -572
- package/dist/babel/plugin/visitors/className.ts +652 -0
- package/dist/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
- package/dist/babel/plugin/visitors/imports.d.ts +11 -0
- package/dist/babel/plugin/visitors/imports.test.ts +88 -0
- package/dist/babel/plugin/visitors/imports.ts +116 -0
- package/dist/babel/plugin/visitors/program.d.ts +15 -0
- package/dist/babel/plugin/visitors/program.test.ts +325 -0
- package/dist/babel/plugin/visitors/program.ts +116 -0
- package/dist/babel/plugin/visitors/tw.d.ts +16 -0
- package/dist/babel/plugin/visitors/tw.test.ts +771 -0
- package/dist/babel/plugin/visitors/tw.ts +148 -0
- package/dist/babel/plugin.d.ts +3 -96
- package/dist/babel/plugin.test.ts +470 -0
- package/dist/babel/plugin.ts +28 -963
- package/dist/babel/utils/colorSchemeModifierProcessing.ts +11 -0
- package/dist/babel/utils/componentSupport.test.ts +20 -7
- package/dist/babel/utils/componentSupport.ts +2 -0
- package/dist/babel/utils/directionalModifierProcessing.d.ts +34 -0
- package/dist/babel/utils/directionalModifierProcessing.ts +99 -0
- package/dist/babel/utils/modifierProcessing.ts +21 -0
- package/dist/babel/utils/platformModifierProcessing.ts +11 -0
- package/dist/babel/utils/styleInjection.d.ts +31 -0
- package/dist/babel/utils/styleInjection.ts +253 -7
- package/dist/babel/utils/twProcessing.d.ts +2 -0
- package/dist/babel/utils/twProcessing.ts +103 -3
- package/dist/babel/utils/windowDimensionsProcessing.d.ts +56 -0
- package/dist/babel/utils/windowDimensionsProcessing.ts +121 -0
- package/dist/components/TouchableOpacity.d.ts +35 -0
- package/dist/components/TouchableOpacity.js +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +1 -0
- package/dist/config/markers.d.ts +5 -0
- package/dist/config/markers.js +1 -0
- package/dist/index.d.ts +2 -5
- package/dist/index.js +1 -1
- package/dist/parser/borders.d.ts +3 -1
- package/dist/parser/borders.js +1 -1
- package/dist/parser/borders.test.js +1 -1
- package/dist/parser/colors.js +1 -1
- package/dist/parser/colors.test.js +1 -1
- package/dist/parser/index.d.ts +2 -2
- package/dist/parser/index.js +1 -1
- package/dist/parser/layout.js +1 -1
- package/dist/parser/layout.test.js +1 -1
- package/dist/parser/modifiers.d.ts +32 -2
- package/dist/parser/modifiers.js +1 -1
- package/dist/parser/modifiers.test.js +1 -1
- package/dist/parser/sizing.js +1 -1
- package/dist/parser/spacing.d.ts +1 -1
- package/dist/parser/spacing.js +1 -1
- package/dist/parser/spacing.test.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.js +1 -1
- package/dist/runtime.js.map +4 -4
- package/package.json +6 -6
- package/src/babel/plugin/componentScope.ts +87 -0
- package/src/babel/plugin/state.ts +185 -0
- package/src/babel/plugin/visitors/className.test.ts +1625 -0
- package/src/babel/plugin/visitors/className.ts +652 -0
- package/src/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
- package/src/babel/plugin/visitors/imports.test.ts +88 -0
- package/src/babel/plugin/visitors/imports.ts +116 -0
- package/src/babel/plugin/visitors/program.test.ts +325 -0
- package/src/babel/plugin/visitors/program.ts +116 -0
- package/src/babel/plugin/visitors/tw.test.ts +771 -0
- package/src/babel/plugin/visitors/tw.ts +148 -0
- package/src/babel/plugin.ts +28 -963
- package/src/babel/utils/colorSchemeModifierProcessing.ts +11 -0
- package/src/babel/utils/componentSupport.test.ts +20 -7
- package/src/babel/utils/componentSupport.ts +2 -0
- package/src/babel/utils/directionalModifierProcessing.ts +99 -0
- package/src/babel/utils/modifierProcessing.ts +21 -0
- package/src/babel/utils/platformModifierProcessing.ts +11 -0
- package/src/babel/utils/styleInjection.ts +253 -7
- package/src/babel/utils/twProcessing.ts +103 -3
- package/src/babel/utils/windowDimensionsProcessing.ts +121 -0
- package/src/components/TouchableOpacity.tsx +71 -0
- package/src/components/index.ts +3 -0
- package/src/config/markers.ts +5 -0
- package/src/index.ts +4 -5
- package/src/parser/borders.test.ts +162 -0
- package/src/parser/borders.ts +67 -9
- package/src/parser/colors.test.ts +249 -0
- package/src/parser/colors.ts +38 -0
- package/src/parser/index.ts +4 -2
- package/src/parser/layout.test.ts +74 -0
- package/src/parser/layout.ts +94 -0
- package/src/parser/modifiers.test.ts +206 -0
- package/src/parser/modifiers.ts +62 -3
- package/src/parser/sizing.ts +11 -0
- package/src/parser/spacing.test.ts +66 -0
- package/src/parser/spacing.ts +15 -5
- package/src/parser/typography.test.ts +8 -0
- package/src/parser/typography.ts +4 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { transform } from "../../../../test/helpers/babelTransform.js";
|
|
4
|
+
|
|
5
|
+
describe("className visitor - window dimensions (w-screen/h-screen)", () => {
|
|
6
|
+
it("should inject useWindowDimensions hook for w-screen in function component", () => {
|
|
7
|
+
const input = `
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { View } from 'react-native';
|
|
10
|
+
|
|
11
|
+
export function MyComponent() {
|
|
12
|
+
return <View className="w-screen bg-white" />;
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const output = transform(input, undefined, true);
|
|
17
|
+
|
|
18
|
+
// Should import useWindowDimensions
|
|
19
|
+
expect(output).toContain("useWindowDimensions");
|
|
20
|
+
expect(output).toMatch(/import.*useWindowDimensions.*from ['"]react-native['"]/);
|
|
21
|
+
|
|
22
|
+
// Should inject hook call
|
|
23
|
+
expect(output).toContain("_twDimensions");
|
|
24
|
+
expect(output).toContain("useWindowDimensions()");
|
|
25
|
+
|
|
26
|
+
// Should generate inline style with width
|
|
27
|
+
expect(output).toMatch(/width:\s*_twDimensions\.width/);
|
|
28
|
+
|
|
29
|
+
// Should have StyleSheet for static styles
|
|
30
|
+
expect(output).toContain("_twStyles");
|
|
31
|
+
expect(output).toContain("backgroundColor:");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should inject useWindowDimensions hook for h-screen in function component", () => {
|
|
35
|
+
const input = `
|
|
36
|
+
import React from 'react';
|
|
37
|
+
import { View } from 'react-native';
|
|
38
|
+
|
|
39
|
+
export function MyComponent() {
|
|
40
|
+
return <View className="h-screen bg-blue-500" />;
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
const output = transform(input, undefined, true);
|
|
45
|
+
|
|
46
|
+
// Should import useWindowDimensions
|
|
47
|
+
expect(output).toContain("useWindowDimensions");
|
|
48
|
+
|
|
49
|
+
// Should inject hook call
|
|
50
|
+
expect(output).toContain("_twDimensions");
|
|
51
|
+
expect(output).toContain("useWindowDimensions()");
|
|
52
|
+
|
|
53
|
+
// Should generate inline style with height
|
|
54
|
+
expect(output).toMatch(/height:\s*_twDimensions\.height/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should handle both w-screen and h-screen together", () => {
|
|
58
|
+
const input = `
|
|
59
|
+
import React from 'react';
|
|
60
|
+
import { View } from 'react-native';
|
|
61
|
+
|
|
62
|
+
function FullScreenView() {
|
|
63
|
+
return <View className="w-screen h-screen bg-gray-100" />;
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const output = transform(input, undefined, true);
|
|
68
|
+
|
|
69
|
+
// Should inject hook
|
|
70
|
+
expect(output).toContain("useWindowDimensions()");
|
|
71
|
+
|
|
72
|
+
// Should generate inline style with both dimensions
|
|
73
|
+
expect(output).toMatch(/width:\s*_twDimensions\.width/);
|
|
74
|
+
expect(output).toMatch(/height:\s*_twDimensions\.height/);
|
|
75
|
+
|
|
76
|
+
// Should have StyleSheet for static styles
|
|
77
|
+
expect(output).toContain("backgroundColor:");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should handle w-screen/h-screen with arrow function component", () => {
|
|
81
|
+
const input = `
|
|
82
|
+
import React from 'react';
|
|
83
|
+
import { View } from 'react-native';
|
|
84
|
+
|
|
85
|
+
const MyComponent = () => {
|
|
86
|
+
return <View className="w-screen" />;
|
|
87
|
+
};
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
const output = transform(input, undefined, true);
|
|
91
|
+
|
|
92
|
+
// Should inject hook
|
|
93
|
+
expect(output).toContain("useWindowDimensions()");
|
|
94
|
+
expect(output).toMatch(/width:\s*_twDimensions\.width/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should handle concise arrow function and inject hook", () => {
|
|
98
|
+
const input = `
|
|
99
|
+
import React from 'react';
|
|
100
|
+
import { View } from 'react-native';
|
|
101
|
+
|
|
102
|
+
const MyComponent = () => <View className="w-screen" />;
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
const output = transform(input, undefined, true);
|
|
106
|
+
|
|
107
|
+
// Should convert concise arrow to block statement and inject hook
|
|
108
|
+
expect(output).toContain("useWindowDimensions()");
|
|
109
|
+
expect(output).toContain("return");
|
|
110
|
+
expect(output).toMatch(/width:\s*_twDimensions\.width/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should merge useWindowDimensions with existing react-native import", () => {
|
|
114
|
+
const input = `
|
|
115
|
+
import React from 'react';
|
|
116
|
+
import { View, Text } from 'react-native';
|
|
117
|
+
|
|
118
|
+
function MyComponent() {
|
|
119
|
+
return <View className="w-screen" />;
|
|
120
|
+
}
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
const output = transform(input, undefined, true);
|
|
124
|
+
|
|
125
|
+
// Should merge useWindowDimensions into existing import (not create separate import)
|
|
126
|
+
// The key is that useWindowDimensions should be imported
|
|
127
|
+
expect(output).toContain("useWindowDimensions");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should warn when w-screen/h-screen used outside function component", () => {
|
|
131
|
+
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
132
|
+
|
|
133
|
+
const input = `
|
|
134
|
+
import React from 'react';
|
|
135
|
+
import { View } from 'react-native';
|
|
136
|
+
|
|
137
|
+
class MyComponent extends React.Component {
|
|
138
|
+
render() {
|
|
139
|
+
return <View className="w-screen" />;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
transform(input, undefined, true);
|
|
145
|
+
|
|
146
|
+
// Should warn about usage in class component
|
|
147
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
148
|
+
expect.stringContaining("w-screen/h-screen classes require a function component scope"),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
consoleWarnSpy.mockRestore();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should combine w-screen with other static classes", () => {
|
|
155
|
+
const input = `
|
|
156
|
+
import React from 'react';
|
|
157
|
+
import { View } from 'react-native';
|
|
158
|
+
|
|
159
|
+
function MyComponent() {
|
|
160
|
+
return <View className="w-screen p-4 bg-white rounded-lg" />;
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
const output = transform(input, undefined, true);
|
|
165
|
+
|
|
166
|
+
// Should have static styles in StyleSheet
|
|
167
|
+
expect(output).toContain("padding:");
|
|
168
|
+
expect(output).toContain("backgroundColor:");
|
|
169
|
+
expect(output).toContain("borderRadius:");
|
|
170
|
+
|
|
171
|
+
// Should have runtime dimension
|
|
172
|
+
expect(output).toMatch(/width:\s*_twDimensions\.width/);
|
|
173
|
+
|
|
174
|
+
// Should combine as array
|
|
175
|
+
expect(output).toContain("_twStyles");
|
|
176
|
+
expect(output).toContain("_twDimensions");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should inject hook only once even with multiple w-screen/h-screen uses", () => {
|
|
180
|
+
const input = `
|
|
181
|
+
import React from 'react';
|
|
182
|
+
import { View } from 'react-native';
|
|
183
|
+
|
|
184
|
+
function MyComponent() {
|
|
185
|
+
return (
|
|
186
|
+
<>
|
|
187
|
+
<View className="w-screen" />
|
|
188
|
+
<View className="h-screen" />
|
|
189
|
+
<View className="w-screen h-screen" />
|
|
190
|
+
</>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
`;
|
|
194
|
+
|
|
195
|
+
const output = transform(input, undefined, true);
|
|
196
|
+
|
|
197
|
+
// Count occurrences of hook injection (should be exactly 1)
|
|
198
|
+
const hookMatches = output.match(/_twDimensions\s*=\s*useWindowDimensions\(\)/g) ?? [];
|
|
199
|
+
expect(hookMatches.length).toBe(1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should handle aliased useWindowDimensions import", () => {
|
|
203
|
+
const input = `
|
|
204
|
+
import React from 'react';
|
|
205
|
+
import { View, useWindowDimensions as useDims } from 'react-native';
|
|
206
|
+
|
|
207
|
+
function MyComponent() {
|
|
208
|
+
const dims = useDims();
|
|
209
|
+
return <View className="w-screen" />;
|
|
210
|
+
}
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
const output = transform(input, undefined, true);
|
|
214
|
+
|
|
215
|
+
// Should use the aliased name
|
|
216
|
+
expect(output).toContain("useDims()");
|
|
217
|
+
// Should still generate _twDimensions variable for our use
|
|
218
|
+
expect(output).toContain("_twDimensions");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should error when w-screen is combined with dark: modifier", () => {
|
|
222
|
+
const input = `
|
|
223
|
+
import { View } from 'react-native';
|
|
224
|
+
export function MyComponent() {
|
|
225
|
+
return <View className="dark:w-screen bg-white" />;
|
|
226
|
+
}
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
expect(() => transform(input, undefined, true)).toThrow(
|
|
230
|
+
/w-screen and h-screen cannot be combined with color scheme modifiers/,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should error when h-screen is combined with light: modifier", () => {
|
|
235
|
+
const input = `
|
|
236
|
+
import { View } from 'react-native';
|
|
237
|
+
export function MyComponent() {
|
|
238
|
+
return <View className="light:h-screen p-4" />;
|
|
239
|
+
}
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
expect(() => transform(input, undefined, true)).toThrow(
|
|
243
|
+
/w-screen and h-screen cannot be combined with color scheme modifiers/,
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should error when w-screen is combined with active: modifier", () => {
|
|
248
|
+
const input = `
|
|
249
|
+
import { Pressable } from 'react-native';
|
|
250
|
+
export function MyComponent() {
|
|
251
|
+
return <Pressable className="active:w-screen bg-blue-500" />;
|
|
252
|
+
}
|
|
253
|
+
`;
|
|
254
|
+
|
|
255
|
+
expect(() => transform(input, undefined, true)).toThrow(
|
|
256
|
+
/w-screen and h-screen cannot be combined with state modifiers/,
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("should error when h-screen is combined with hover: modifier", () => {
|
|
261
|
+
const input = `
|
|
262
|
+
import { Pressable } from 'react-native';
|
|
263
|
+
export function MyComponent() {
|
|
264
|
+
return <Pressable className="hover:h-screen p-4" />;
|
|
265
|
+
}
|
|
266
|
+
`;
|
|
267
|
+
|
|
268
|
+
expect(() => transform(input, undefined, true)).toThrow(
|
|
269
|
+
/w-screen and h-screen cannot be combined with state modifiers/,
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("should error when w-screen is combined with ios: modifier", () => {
|
|
274
|
+
const input = `
|
|
275
|
+
import { View } from 'react-native';
|
|
276
|
+
export function MyComponent() {
|
|
277
|
+
return <View className="ios:w-screen bg-white" />;
|
|
278
|
+
}
|
|
279
|
+
`;
|
|
280
|
+
|
|
281
|
+
expect(() => transform(input, undefined, true)).toThrow(
|
|
282
|
+
/w-screen and h-screen cannot be combined with.*platform modifiers/,
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("should error when h-screen is combined with android: modifier", () => {
|
|
287
|
+
const input = `
|
|
288
|
+
import { View } from 'react-native';
|
|
289
|
+
export function MyComponent() {
|
|
290
|
+
return <View className="android:h-screen p-4" />;
|
|
291
|
+
}
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
expect(() => transform(input, undefined, true)).toThrow(
|
|
295
|
+
/w-screen and h-screen cannot be combined with.*platform modifiers/,
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("should error when w-screen is used in tw`` call", () => {
|
|
300
|
+
const input = `
|
|
301
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
302
|
+
export function MyComponent() {
|
|
303
|
+
const styles = tw\`w-screen bg-white\`;
|
|
304
|
+
return <View style={styles.style} />;
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
|
|
308
|
+
expect(() => transform(input, undefined, true)).toThrow(
|
|
309
|
+
/w-screen and h-screen are not supported in tw.*or twStyle/,
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("should error when h-screen is used in twStyle() call", () => {
|
|
314
|
+
const input = `
|
|
315
|
+
import { twStyle } from '@mgcrea/react-native-tailwind';
|
|
316
|
+
export function MyComponent() {
|
|
317
|
+
const styles = twStyle('h-screen p-4');
|
|
318
|
+
return <View style={styles.style} />;
|
|
319
|
+
}
|
|
320
|
+
`;
|
|
321
|
+
|
|
322
|
+
expect(() => transform(input, undefined, true)).toThrow(
|
|
323
|
+
/w-screen and h-screen are not supported in tw.*or twStyle/,
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should properly merge w-screen with Pressable style function", () => {
|
|
328
|
+
const input = `
|
|
329
|
+
import { Pressable } from 'react-native';
|
|
330
|
+
export function MyComponent() {
|
|
331
|
+
return (
|
|
332
|
+
<Pressable
|
|
333
|
+
className="w-screen bg-white"
|
|
334
|
+
style={({ pressed }) => [pressed && { opacity: 0.5 }]}
|
|
335
|
+
/>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
`;
|
|
339
|
+
|
|
340
|
+
const output = transform(input, undefined, true);
|
|
341
|
+
|
|
342
|
+
// Should inject useWindowDimensions hook
|
|
343
|
+
expect(output).toContain("useWindowDimensions");
|
|
344
|
+
expect(output).toContain("_twDimensions");
|
|
345
|
+
|
|
346
|
+
// Should wrap the style function properly
|
|
347
|
+
expect(output).toContain("_state");
|
|
348
|
+
// Should contain the runtime dimension access
|
|
349
|
+
expect(output).toContain("_twDimensions.width");
|
|
350
|
+
// Should call the existing function
|
|
351
|
+
expect(output).toContain("pressed");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should properly merge h-screen with arrow function style", () => {
|
|
355
|
+
const input = `
|
|
356
|
+
import { Pressable } from 'react-native';
|
|
357
|
+
export function MyComponent() {
|
|
358
|
+
return (
|
|
359
|
+
<Pressable
|
|
360
|
+
className="h-screen p-4"
|
|
361
|
+
style={(state) => state.pressed ? { opacity: 0.8 } : null}
|
|
362
|
+
/>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
`;
|
|
366
|
+
|
|
367
|
+
const output = transform(input, undefined, true);
|
|
368
|
+
|
|
369
|
+
// Should inject useWindowDimensions hook
|
|
370
|
+
expect(output).toContain("useWindowDimensions");
|
|
371
|
+
expect(output).toContain("_twDimensions");
|
|
372
|
+
|
|
373
|
+
// Should wrap the style function
|
|
374
|
+
expect(output).toContain("_state");
|
|
375
|
+
// Should contain the runtime dimension access
|
|
376
|
+
expect(output).toContain("_twDimensions.height");
|
|
377
|
+
// Should call the original function
|
|
378
|
+
expect(output).toContain("state.pressed");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("should error when w-screen is in base classes with dark: modifier", () => {
|
|
382
|
+
const input = `
|
|
383
|
+
import { View } from 'react-native';
|
|
384
|
+
export function MyComponent() {
|
|
385
|
+
return <View className="w-screen dark:bg-black" />;
|
|
386
|
+
}
|
|
387
|
+
`;
|
|
388
|
+
|
|
389
|
+
expect(() => transform(input, undefined, true)).toThrow(
|
|
390
|
+
/w-screen and h-screen cannot be combined with modifiers/,
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("should error when w-screen is in base classes with ios: modifier", () => {
|
|
395
|
+
const input = `
|
|
396
|
+
import { View } from 'react-native';
|
|
397
|
+
export function MyComponent() {
|
|
398
|
+
return <View className="w-screen ios:p-4" />;
|
|
399
|
+
}
|
|
400
|
+
`;
|
|
401
|
+
|
|
402
|
+
expect(() => transform(input, undefined, true)).toThrow(
|
|
403
|
+
/w-screen and h-screen cannot be combined with modifiers/,
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportDeclaration visitor - tracks existing imports
|
|
3
|
+
*/
|
|
4
|
+
import type { NodePath } from "@babel/core";
|
|
5
|
+
import type * as BabelTypes from "@babel/types";
|
|
6
|
+
import type { PluginState } from "../state.js";
|
|
7
|
+
/**
|
|
8
|
+
* ImportDeclaration visitor
|
|
9
|
+
* Tracks existing imports from react-native and the main package
|
|
10
|
+
*/
|
|
11
|
+
export declare function importDeclarationVisitor(path: NodePath<BabelTypes.ImportDeclaration>, state: PluginState, t: typeof BabelTypes): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { transform } from "../../../../test/helpers/babelTransform.js";
|
|
3
|
+
|
|
4
|
+
describe("imports visitor - import tracking and injection", () => {
|
|
5
|
+
it("should not add StyleSheet import to files without className usage", () => {
|
|
6
|
+
const input = `
|
|
7
|
+
import { View, Text } from 'react-native';
|
|
8
|
+
|
|
9
|
+
function MyComponent() {
|
|
10
|
+
return <View><Text>Hello</Text></View>;
|
|
11
|
+
}
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const output = transform(input, undefined, true);
|
|
15
|
+
|
|
16
|
+
// Should not mutate the import by adding StyleSheet
|
|
17
|
+
// Count occurrences of "StyleSheet" in output
|
|
18
|
+
const styleSheetCount = (output.match(/StyleSheet/g) ?? []).length;
|
|
19
|
+
expect(styleSheetCount).toBe(0);
|
|
20
|
+
|
|
21
|
+
// Should not have _twStyles definition
|
|
22
|
+
expect(output).not.toContain("_twStyles");
|
|
23
|
+
expect(output).not.toContain("StyleSheet.create");
|
|
24
|
+
|
|
25
|
+
// Original imports should remain unchanged
|
|
26
|
+
expect(output).toContain("View");
|
|
27
|
+
expect(output).toContain("Text");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should add StyleSheet import only when className is used", () => {
|
|
31
|
+
const input = `
|
|
32
|
+
import { View } from 'react-native';
|
|
33
|
+
|
|
34
|
+
function MyComponent() {
|
|
35
|
+
return <View className="m-4 p-2" />;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const output = transform(input, undefined, true);
|
|
40
|
+
|
|
41
|
+
// Should have StyleSheet import (both single and double quotes)
|
|
42
|
+
expect(output).toMatch(/import.*StyleSheet.*from ['"]react-native['"]|require\(['"]react-native['"]\)/);
|
|
43
|
+
|
|
44
|
+
// Should have _twStyles definition
|
|
45
|
+
expect(output).toContain("_twStyles");
|
|
46
|
+
expect(output).toContain("StyleSheet.create");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should add Platform import only when platform modifiers are used", () => {
|
|
50
|
+
const input = `
|
|
51
|
+
import { View } from 'react-native';
|
|
52
|
+
|
|
53
|
+
function MyComponent() {
|
|
54
|
+
return <View className="ios:m-4 android:m-2" />;
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const output = transform(input, undefined, true);
|
|
59
|
+
|
|
60
|
+
// Should have Platform import
|
|
61
|
+
expect(output).toContain("Platform");
|
|
62
|
+
|
|
63
|
+
// Should have StyleSheet import too
|
|
64
|
+
expect(output).toContain("StyleSheet");
|
|
65
|
+
|
|
66
|
+
// Should use Platform.select
|
|
67
|
+
expect(output).toContain("Platform.select");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should not add Platform import without platform modifiers", () => {
|
|
71
|
+
const input = `
|
|
72
|
+
import { View } from 'react-native';
|
|
73
|
+
|
|
74
|
+
function MyComponent() {
|
|
75
|
+
return <View className="m-4 p-2" />;
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const output = transform(input, undefined, true);
|
|
80
|
+
|
|
81
|
+
// Should not have Platform import
|
|
82
|
+
const platformCount = (output.match(/Platform/g) ?? []).length;
|
|
83
|
+
expect(platformCount).toBe(0);
|
|
84
|
+
|
|
85
|
+
// Should still have StyleSheet
|
|
86
|
+
expect(output).toContain("StyleSheet");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportDeclaration visitor - tracks existing imports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NodePath } from "@babel/core";
|
|
6
|
+
import type * as BabelTypes from "@babel/types";
|
|
7
|
+
import type { PluginState } from "../state.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ImportDeclaration visitor
|
|
11
|
+
* Tracks existing imports from react-native and the main package
|
|
12
|
+
*/
|
|
13
|
+
export function importDeclarationVisitor(
|
|
14
|
+
path: NodePath<BabelTypes.ImportDeclaration>,
|
|
15
|
+
state: PluginState,
|
|
16
|
+
t: typeof BabelTypes,
|
|
17
|
+
): void {
|
|
18
|
+
const node = path.node;
|
|
19
|
+
|
|
20
|
+
// Track react-native StyleSheet, Platform, and I18nManager imports
|
|
21
|
+
if (node.source.value === "react-native") {
|
|
22
|
+
const specifiers = node.specifiers;
|
|
23
|
+
|
|
24
|
+
const hasStyleSheet = specifiers.some((spec) => {
|
|
25
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
26
|
+
return spec.imported.name === "StyleSheet";
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const hasPlatform = specifiers.some((spec) => {
|
|
32
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
33
|
+
return spec.imported.name === "Platform";
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Check for I18nManager import (only value imports, not type-only)
|
|
39
|
+
if (node.importKind !== "type") {
|
|
40
|
+
for (const spec of specifiers) {
|
|
41
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
42
|
+
if (spec.imported.name === "I18nManager") {
|
|
43
|
+
state.hasI18nManagerImport = true;
|
|
44
|
+
// Track the local identifier (handles aliased imports)
|
|
45
|
+
// e.g., import { I18nManager as RTL } → local name is 'RTL'
|
|
46
|
+
state.i18nManagerLocalIdentifier = spec.local.name;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check for useWindowDimensions import (only value imports, not type-only)
|
|
54
|
+
if (node.importKind !== "type") {
|
|
55
|
+
for (const spec of specifiers) {
|
|
56
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
57
|
+
if (spec.imported.name === "useWindowDimensions") {
|
|
58
|
+
state.hasWindowDimensionsImport = true;
|
|
59
|
+
// Track the local identifier (handles aliased imports)
|
|
60
|
+
// e.g., import { useWindowDimensions as useDims } → local name is 'useDims'
|
|
61
|
+
state.windowDimensionsLocalIdentifier = spec.local.name;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Only track if imports exist - don't mutate yet
|
|
69
|
+
// Actual import injection happens in Program.exit only if needed
|
|
70
|
+
if (hasStyleSheet) {
|
|
71
|
+
state.hasStyleSheetImport = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (hasPlatform) {
|
|
75
|
+
state.hasPlatformImport = true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Store reference to the react-native import for later modification if needed
|
|
79
|
+
state.reactNativeImportPath = path;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Track color scheme hook import from the configured source
|
|
83
|
+
// (default: react-native, but can be custom like @/hooks/useColorScheme)
|
|
84
|
+
// Only track value imports (not type-only imports which get erased)
|
|
85
|
+
if (node.source.value === state.colorSchemeImportSource && node.importKind !== "type") {
|
|
86
|
+
const specifiers = node.specifiers;
|
|
87
|
+
|
|
88
|
+
for (const spec of specifiers) {
|
|
89
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
90
|
+
if (spec.imported.name === state.colorSchemeHookName) {
|
|
91
|
+
state.hasColorSchemeImport = true;
|
|
92
|
+
// Track the local identifier (handles aliased imports)
|
|
93
|
+
// e.g., import { useTheme as navTheme } → local name is 'navTheme'
|
|
94
|
+
state.colorSchemeLocalIdentifier = spec.local.name;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Track tw/twStyle imports from main package (for compile-time transformation)
|
|
102
|
+
if (node.source.value === "@mgcrea/react-native-tailwind") {
|
|
103
|
+
const specifiers = node.specifiers;
|
|
104
|
+
specifiers.forEach((spec) => {
|
|
105
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
106
|
+
const importedName = spec.imported.name;
|
|
107
|
+
if (importedName === "tw" || importedName === "twStyle") {
|
|
108
|
+
// Track the local name (could be renamed: import { tw as customTw })
|
|
109
|
+
const localName = spec.local.name;
|
|
110
|
+
state.twImportNames.add(localName);
|
|
111
|
+
// Don't set hasTwImport yet - only set it when we successfully transform a call
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Program visitor - entry and exit points for file processing
|
|
3
|
+
*/
|
|
4
|
+
import type { NodePath } from "@babel/core";
|
|
5
|
+
import type * as BabelTypes from "@babel/types";
|
|
6
|
+
import type { PluginState } from "../state.js";
|
|
7
|
+
/**
|
|
8
|
+
* Program enter visitor - initialize state for each file
|
|
9
|
+
*/
|
|
10
|
+
export declare function programEnter(_path: NodePath<BabelTypes.Program>, _state: PluginState): void;
|
|
11
|
+
/**
|
|
12
|
+
* Program exit visitor - finalize transformations
|
|
13
|
+
* Injects imports, hooks, and StyleSheet.create
|
|
14
|
+
*/
|
|
15
|
+
export declare function programExit(path: NodePath<BabelTypes.Program>, state: PluginState, t: typeof BabelTypes): void;
|