@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.
Files changed (104) hide show
  1. package/README.md +45 -2031
  2. package/dist/babel/index.cjs +1726 -1094
  3. package/dist/babel/plugin/componentScope.d.ts +26 -0
  4. package/dist/babel/plugin/componentScope.ts +87 -0
  5. package/dist/babel/plugin/state.d.ts +123 -0
  6. package/dist/babel/plugin/state.ts +185 -0
  7. package/dist/babel/plugin/visitors/className.d.ts +11 -0
  8. package/{src/babel/plugin.test.ts → dist/babel/plugin/visitors/className.test.ts} +285 -572
  9. package/dist/babel/plugin/visitors/className.ts +652 -0
  10. package/dist/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  11. package/dist/babel/plugin/visitors/imports.d.ts +11 -0
  12. package/dist/babel/plugin/visitors/imports.test.ts +88 -0
  13. package/dist/babel/plugin/visitors/imports.ts +116 -0
  14. package/dist/babel/plugin/visitors/program.d.ts +15 -0
  15. package/dist/babel/plugin/visitors/program.test.ts +325 -0
  16. package/dist/babel/plugin/visitors/program.ts +116 -0
  17. package/dist/babel/plugin/visitors/tw.d.ts +16 -0
  18. package/dist/babel/plugin/visitors/tw.test.ts +771 -0
  19. package/dist/babel/plugin/visitors/tw.ts +148 -0
  20. package/dist/babel/plugin.d.ts +3 -96
  21. package/dist/babel/plugin.test.ts +470 -0
  22. package/dist/babel/plugin.ts +28 -963
  23. package/dist/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  24. package/dist/babel/utils/componentSupport.test.ts +20 -7
  25. package/dist/babel/utils/componentSupport.ts +2 -0
  26. package/dist/babel/utils/directionalModifierProcessing.d.ts +34 -0
  27. package/dist/babel/utils/directionalModifierProcessing.ts +99 -0
  28. package/dist/babel/utils/modifierProcessing.ts +21 -0
  29. package/dist/babel/utils/platformModifierProcessing.ts +11 -0
  30. package/dist/babel/utils/styleInjection.d.ts +31 -0
  31. package/dist/babel/utils/styleInjection.ts +253 -7
  32. package/dist/babel/utils/twProcessing.d.ts +2 -0
  33. package/dist/babel/utils/twProcessing.ts +103 -3
  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 +2 -2
  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/modifiers.d.ts +32 -2
  54. package/dist/parser/modifiers.js +1 -1
  55. package/dist/parser/modifiers.test.js +1 -1
  56. package/dist/parser/sizing.js +1 -1
  57. package/dist/parser/spacing.d.ts +1 -1
  58. package/dist/parser/spacing.js +1 -1
  59. package/dist/parser/spacing.test.js +1 -1
  60. package/dist/parser/typography.test.js +1 -1
  61. package/dist/runtime.cjs +1 -1
  62. package/dist/runtime.cjs.map +4 -4
  63. package/dist/runtime.js +1 -1
  64. package/dist/runtime.js.map +4 -4
  65. package/package.json +6 -6
  66. package/src/babel/plugin/componentScope.ts +87 -0
  67. package/src/babel/plugin/state.ts +185 -0
  68. package/src/babel/plugin/visitors/className.test.ts +1625 -0
  69. package/src/babel/plugin/visitors/className.ts +652 -0
  70. package/src/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  71. package/src/babel/plugin/visitors/imports.test.ts +88 -0
  72. package/src/babel/plugin/visitors/imports.ts +116 -0
  73. package/src/babel/plugin/visitors/program.test.ts +325 -0
  74. package/src/babel/plugin/visitors/program.ts +116 -0
  75. package/src/babel/plugin/visitors/tw.test.ts +771 -0
  76. package/src/babel/plugin/visitors/tw.ts +148 -0
  77. package/src/babel/plugin.ts +28 -963
  78. package/src/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  79. package/src/babel/utils/componentSupport.test.ts +20 -7
  80. package/src/babel/utils/componentSupport.ts +2 -0
  81. package/src/babel/utils/directionalModifierProcessing.ts +99 -0
  82. package/src/babel/utils/modifierProcessing.ts +21 -0
  83. package/src/babel/utils/platformModifierProcessing.ts +11 -0
  84. package/src/babel/utils/styleInjection.ts +253 -7
  85. package/src/babel/utils/twProcessing.ts +103 -3
  86. package/src/babel/utils/windowDimensionsProcessing.ts +121 -0
  87. package/src/components/TouchableOpacity.tsx +71 -0
  88. package/src/components/index.ts +3 -0
  89. package/src/config/markers.ts +5 -0
  90. package/src/index.ts +4 -5
  91. package/src/parser/borders.test.ts +162 -0
  92. package/src/parser/borders.ts +67 -9
  93. package/src/parser/colors.test.ts +249 -0
  94. package/src/parser/colors.ts +38 -0
  95. package/src/parser/index.ts +4 -2
  96. package/src/parser/layout.test.ts +74 -0
  97. package/src/parser/layout.ts +94 -0
  98. package/src/parser/modifiers.test.ts +206 -0
  99. package/src/parser/modifiers.ts +62 -3
  100. package/src/parser/sizing.ts +11 -0
  101. package/src/parser/spacing.test.ts +66 -0
  102. package/src/parser/spacing.ts +15 -5
  103. package/src/parser/typography.test.ts +8 -0
  104. package/src/parser/typography.ts +4 -0
@@ -0,0 +1,771 @@
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
+ import type { PluginOptions } from "../state.js";
5
+
6
+ describe("tw visitor - template tag transformation", () => {
7
+ it("should transform simple tw template literal", () => {
8
+ const input = `
9
+ import { tw } from '@mgcrea/react-native-tailwind';
10
+ const styles = tw\`bg-blue-500 m-4\`;
11
+ `;
12
+
13
+ const output = transform(input);
14
+
15
+ // Should have StyleSheet import (either ESM or CommonJS)
16
+ expect(output).toMatch(/import.*StyleSheet.*from "react-native"|require\("react-native"\)/);
17
+ expect(output).toContain("StyleSheet");
18
+
19
+ // Should have _twStyles definition
20
+ expect(output).toContain("_twStyles");
21
+ expect(output).toContain("StyleSheet.create");
22
+
23
+ // Should transform tw call to object with style property
24
+ expect(output).toContain("style:");
25
+ expect(output).toContain("_twStyles._bg_blue_500_m_4");
26
+
27
+ // Should remove tw import
28
+ expect(output).not.toContain("from '@mgcrea/react-native-tailwind'");
29
+ });
30
+
31
+ it("should transform tw with state modifiers", () => {
32
+ const input = `
33
+ import { tw } from '@mgcrea/react-native-tailwind';
34
+ const styles = tw\`bg-blue-500 active:bg-blue-700 disabled:bg-gray-300\`;
35
+ `;
36
+
37
+ const output = transform(input);
38
+
39
+ // Should have base style
40
+ expect(output).toContain("style:");
41
+ expect(output).toContain("_bg_blue_500");
42
+
43
+ // Should have activeStyle
44
+ expect(output).toContain("activeStyle:");
45
+ expect(output).toContain("_active_bg_blue_700");
46
+
47
+ // Should have disabledStyle
48
+ expect(output).toContain("disabledStyle:");
49
+ expect(output).toContain("_disabled_bg_gray_300");
50
+
51
+ // Should create StyleSheet with all styles
52
+ expect(output).toContain("backgroundColor:");
53
+ });
54
+
55
+ it("should inject StyleSheet.create after imports", () => {
56
+ const input = `
57
+ import { tw } from '@mgcrea/react-native-tailwind';
58
+ import { View } from 'react-native';
59
+
60
+ const styles = tw\`m-4\`;
61
+ `;
62
+
63
+ const output = transform(input);
64
+
65
+ // Find the position of imports and StyleSheet.create
66
+ const viewImportPos = output.indexOf('require("react-native")');
67
+ const styleSheetCreatePos = output.indexOf("_twStyles");
68
+
69
+ // StyleSheet.create should come after imports
70
+ expect(styleSheetCreatePos).toBeGreaterThan(viewImportPos);
71
+ });
72
+
73
+ it("should handle tw in object literals", () => {
74
+ const input = `
75
+ import { tw } from '@mgcrea/react-native-tailwind';
76
+
77
+ const sizeVariants = {
78
+ sm: {
79
+ container: tw\`h-9 px-3\`,
80
+ text: tw\`text-sm\`,
81
+ },
82
+ };
83
+ `;
84
+
85
+ const output = transform(input);
86
+
87
+ // Should define _twStyles before object literal
88
+ const twStylesPos = output.indexOf("_twStyles");
89
+ const sizeVariantsPos = output.indexOf("sizeVariants");
90
+
91
+ expect(twStylesPos).toBeGreaterThan(0);
92
+ expect(twStylesPos).toBeLessThan(sizeVariantsPos);
93
+
94
+ // Should have both styles
95
+ expect(output).toContain("_h_9_px_3");
96
+ expect(output).toContain("_text_sm");
97
+ });
98
+
99
+ it("should handle empty tw template literal", () => {
100
+ const input = `
101
+ import { tw } from '@mgcrea/react-native-tailwind';
102
+ const styles = tw\`\`;
103
+ `;
104
+
105
+ const output = transform(input);
106
+
107
+ // Should replace with empty style object
108
+ expect(output).toContain("style:");
109
+ expect(output).toContain("{}");
110
+ });
111
+
112
+ it("should preserve other imports from the same package", () => {
113
+ const input = `
114
+ import { tw, TwStyle, COLORS } from '@mgcrea/react-native-tailwind';
115
+ const styles = tw\`m-4\`;
116
+ `;
117
+
118
+ const output = transform(input);
119
+
120
+ // Should remove tw but keep other imports
121
+ expect(output).not.toContain('"tw"');
122
+ expect(output).toContain("TwStyle");
123
+ expect(output).toContain("COLORS");
124
+ });
125
+
126
+ it("should handle renamed tw import", () => {
127
+ const input = `
128
+ import { tw as customTw } from '@mgcrea/react-native-tailwind';
129
+ const styles = customTw\`m-4 p-2\`;
130
+ `;
131
+
132
+ const output = transform(input);
133
+
134
+ // Should still transform the renamed import
135
+ expect(output).toContain("_twStyles");
136
+ expect(output).toContain("_m_4_p_2");
137
+ expect(output).not.toContain("customTw");
138
+ });
139
+
140
+ it("should handle multiple tw calls", () => {
141
+ const input = `
142
+ import { tw } from '@mgcrea/react-native-tailwind';
143
+ const style1 = tw\`bg-red-500\`;
144
+ const style2 = tw\`bg-blue-500\`;
145
+ const style3 = tw\`bg-green-500\`;
146
+ `;
147
+
148
+ const output = transform(input);
149
+
150
+ // Should have all three styles in StyleSheet
151
+ expect(output).toContain("_bg_red_500");
152
+ expect(output).toContain("_bg_blue_500");
153
+ expect(output).toContain("_bg_green_500");
154
+
155
+ // Should have StyleSheet.create with all styles
156
+ expect(output).toContain("StyleSheet.create");
157
+ });
158
+
159
+ it("should use custom stylesIdentifier option", () => {
160
+ const input = `
161
+ import { tw } from '@mgcrea/react-native-tailwind';
162
+ const styles = tw\`m-4\`;
163
+ `;
164
+
165
+ const output = transform(input, { stylesIdentifier: "myStyles" });
166
+
167
+ // Should use custom identifier
168
+ expect(output).toContain("myStyles");
169
+ expect(output).toContain("myStyles._m_4");
170
+ expect(output).not.toContain("_twStyles");
171
+ });
172
+ });
173
+
174
+ describe("twStyle visitor - function transformation", () => {
175
+ it("should transform twStyle function call", () => {
176
+ const input = `
177
+ import { twStyle } from '@mgcrea/react-native-tailwind';
178
+ const styles = twStyle('bg-blue-500 m-4');
179
+ `;
180
+
181
+ const output = transform(input);
182
+
183
+ // Should transform to object with style property
184
+ expect(output).toContain("style:");
185
+ expect(output).toContain("_twStyles._bg_blue_500_m_4");
186
+ });
187
+
188
+ it("should transform twStyle with modifiers", () => {
189
+ const input = `
190
+ import { twStyle } from '@mgcrea/react-native-tailwind';
191
+ const styles = twStyle('bg-blue-500 active:bg-blue-700');
192
+ `;
193
+
194
+ const output = transform(input);
195
+
196
+ expect(output).toContain("style:");
197
+ expect(output).toContain("activeStyle:");
198
+ });
199
+
200
+ it("should handle empty twStyle call", () => {
201
+ const input = `
202
+ import { twStyle } from '@mgcrea/react-native-tailwind';
203
+ const styles = twStyle('');
204
+ `;
205
+
206
+ const output = transform(input);
207
+
208
+ // Should replace with undefined
209
+ expect(output).toContain("undefined");
210
+ });
211
+ });
212
+
213
+ describe("tw/twStyle - color scheme modifiers", () => {
214
+ it("should transform tw with dark: modifier inside component", () => {
215
+ const input = `
216
+ import { tw } from '@mgcrea/react-native-tailwind';
217
+
218
+ function MyComponent() {
219
+ const styles = tw\`bg-white dark:bg-gray-900\`;
220
+ return null;
221
+ }
222
+ `;
223
+
224
+ const output = transform(input);
225
+
226
+ // Should inject useColorScheme hook
227
+ expect(output).toContain("useColorScheme");
228
+ expect(output).toContain("_twColorScheme");
229
+
230
+ // Should generate style array with conditionals
231
+ expect(output).toContain("style: [");
232
+ expect(output).toContain('_twColorScheme === "dark"');
233
+ expect(output).toContain("_twStyles._dark_bg_gray_900");
234
+ expect(output).toContain("_twStyles._bg_white");
235
+
236
+ // Should have StyleSheet.create
237
+ expect(output).toContain("StyleSheet.create");
238
+ });
239
+
240
+ it("should transform twStyle with light: modifier inside component", () => {
241
+ const input = `
242
+ import { twStyle } from '@mgcrea/react-native-tailwind';
243
+
244
+ export const MyComponent = () => {
245
+ const buttonStyles = twStyle('text-gray-900 light:text-gray-100');
246
+ return null;
247
+ };
248
+ `;
249
+
250
+ const output = transform(input);
251
+
252
+ // Should inject useColorScheme hook
253
+ expect(output).toContain("useColorScheme");
254
+ expect(output).toContain("_twColorScheme");
255
+
256
+ // Should generate style array with conditionals
257
+ expect(output).toContain("style: [");
258
+ expect(output).toContain('_twColorScheme === "light"');
259
+ expect(output).toContain("_twStyles._light_text_gray_100");
260
+ expect(output).toContain("_twStyles._text_gray_900");
261
+ });
262
+
263
+ it("should transform tw with both dark: and light: modifiers", () => {
264
+ const input = `
265
+ import { tw } from '@mgcrea/react-native-tailwind';
266
+
267
+ function MyComponent() {
268
+ const styles = tw\`bg-blue-500 dark:bg-blue-900 light:bg-blue-100\`;
269
+ return null;
270
+ }
271
+ `;
272
+
273
+ const output = transform(input);
274
+
275
+ // Should have both conditionals
276
+ expect(output).toContain('_twColorScheme === "dark"');
277
+ expect(output).toContain('_twColorScheme === "light"');
278
+ expect(output).toContain("_twStyles._dark_bg_blue_900");
279
+ expect(output).toContain("_twStyles._light_bg_blue_100");
280
+ expect(output).toContain("_twStyles._bg_blue_500");
281
+ });
282
+
283
+ it("should combine color scheme modifiers with state modifiers", () => {
284
+ const input = `
285
+ import { tw } from '@mgcrea/react-native-tailwind';
286
+
287
+ function MyComponent() {
288
+ const styles = tw\`bg-white dark:bg-gray-900 active:bg-blue-500\`;
289
+ return null;
290
+ }
291
+ `;
292
+
293
+ const output = transform(input);
294
+
295
+ // Should have color scheme conditionals in style array
296
+ expect(output).toContain("style: [");
297
+ expect(output).toContain('_twColorScheme === "dark"');
298
+
299
+ // Should have activeStyle property (separate from color scheme)
300
+ expect(output).toContain("activeStyle:");
301
+ expect(output).toContain("_twStyles._active_bg_blue_500");
302
+ });
303
+
304
+ it("should warn if tw with color scheme modifiers used outside component", () => {
305
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
306
+
307
+ const input = `
308
+ import { tw } from '@mgcrea/react-native-tailwind';
309
+
310
+ const globalStyles = tw\`bg-white dark:bg-gray-900\`;
311
+ `;
312
+
313
+ const output = transform(input);
314
+
315
+ // Should warn about usage outside component
316
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
317
+ expect.stringContaining("Color scheme modifiers (dark:, light:) in tw/twStyle calls"),
318
+ );
319
+
320
+ // Should not inject hook (no component scope)
321
+ expect(output).not.toContain("useColorScheme");
322
+
323
+ // Should still generate styles but without runtime conditionals
324
+ expect(output).toContain("_twStyles");
325
+
326
+ consoleWarnSpy.mockRestore();
327
+ });
328
+
329
+ it("should handle tw with only dark: modifier (no base class)", () => {
330
+ const input = `
331
+ import { tw } from '@mgcrea/react-native-tailwind';
332
+
333
+ function MyComponent() {
334
+ const styles = tw\`dark:bg-gray-900\`;
335
+ return null;
336
+ }
337
+ `;
338
+
339
+ const output = transform(input);
340
+
341
+ // Should still generate style array
342
+ expect(output).toContain("style: [");
343
+ expect(output).toContain('_twColorScheme === "dark"');
344
+ expect(output).toContain("_twStyles._dark_bg_gray_900");
345
+ });
346
+
347
+ it("should work with custom color scheme hook import", () => {
348
+ const input = `
349
+ import { tw } from '@mgcrea/react-native-tailwind';
350
+ import { useTheme } from '@react-navigation/native';
351
+
352
+ function MyComponent() {
353
+ const styles = tw\`bg-white dark:bg-gray-900\`;
354
+ return null;
355
+ }
356
+ `;
357
+
358
+ const options: PluginOptions = {
359
+ colorScheme: {
360
+ importFrom: "@react-navigation/native",
361
+ importName: "useTheme",
362
+ },
363
+ };
364
+
365
+ const output = transform(input, options);
366
+
367
+ // Should use existing import (not duplicate)
368
+ const themeImportCount = (output.match(/useTheme/g) ?? []).length;
369
+ // Should appear in import statement and hook call
370
+ expect(themeImportCount).toBeGreaterThanOrEqual(2);
371
+
372
+ // Should call the custom hook
373
+ expect(output).toContain("useTheme()");
374
+ });
375
+
376
+ it("should generate both style array and darkStyle/lightStyle properties", () => {
377
+ const input = `
378
+ import { tw } from '@mgcrea/react-native-tailwind';
379
+
380
+ function MyComponent() {
381
+ const styles = tw\`bg-white dark:bg-gray-900 light:bg-gray-50\`;
382
+ return null;
383
+ }
384
+ `;
385
+
386
+ const output = transform(input);
387
+
388
+ // Should have runtime conditional in style array
389
+ expect(output).toContain("style: [");
390
+ expect(output).toContain('_twColorScheme === "dark"');
391
+ expect(output).toContain('_twColorScheme === "light"');
392
+
393
+ // Should ALSO have darkStyle and lightStyle properties for manual access
394
+ expect(output).toContain("darkStyle:");
395
+ expect(output).toContain("lightStyle:");
396
+ expect(output).toContain("_twStyles._dark_bg_gray_900");
397
+ expect(output).toContain("_twStyles._light_bg_gray_50");
398
+ });
399
+
400
+ it("should allow accessing raw color values from darkStyle/lightStyle", () => {
401
+ const input = `
402
+ import { tw } from '@mgcrea/react-native-tailwind';
403
+
404
+ function MyComponent() {
405
+ const btnStyles = tw\`bg-blue-500 dark:bg-blue-900\`;
406
+ // User can access raw hex for Reanimated
407
+ const darkBgColor = btnStyles.darkStyle?.backgroundColor;
408
+ return null;
409
+ }
410
+ `;
411
+
412
+ const output = transform(input);
413
+
414
+ // Should have darkStyle property available
415
+ expect(output).toContain("darkStyle:");
416
+ expect(output).toContain("_twStyles._dark_bg_blue_900");
417
+
418
+ // The actual usage line should be preserved (TypeScript/Babel doesn't remove it)
419
+ expect(output).toContain("btnStyles.darkStyle");
420
+ });
421
+ });
422
+
423
+ describe("tw/twStyle - platform modifiers", () => {
424
+ it("should transform tw with ios: modifier", () => {
425
+ const input = `
426
+ import { tw } from '@mgcrea/react-native-tailwind';
427
+
428
+ function MyComponent() {
429
+ const styles = tw\`bg-white ios:p-6\`;
430
+ return null;
431
+ }
432
+ `;
433
+
434
+ const output = transform(input);
435
+
436
+ // Should add Platform import
437
+ expect(output).toContain("Platform");
438
+ expect(output).toContain('from "react-native"');
439
+
440
+ // Should generate style array with Platform.select()
441
+ expect(output).toContain("style: [");
442
+ expect(output).toContain("Platform.select");
443
+ expect(output).toContain("ios:");
444
+ expect(output).toContain("_twStyles._ios_p_6");
445
+ expect(output).toContain("_twStyles._bg_white");
446
+
447
+ // Should have StyleSheet.create
448
+ expect(output).toContain("StyleSheet.create");
449
+ });
450
+
451
+ it("should transform twStyle with android: modifier", () => {
452
+ const input = `
453
+ import { twStyle } from '@mgcrea/react-native-tailwind';
454
+
455
+ export const MyComponent = () => {
456
+ const buttonStyles = twStyle('bg-blue-500 android:p-8');
457
+ return null;
458
+ };
459
+ `;
460
+
461
+ const output = transform(input);
462
+
463
+ // Should add Platform import
464
+ expect(output).toContain("Platform");
465
+
466
+ // Should generate style array with Platform.select()
467
+ expect(output).toContain("style: [");
468
+ expect(output).toContain("Platform.select");
469
+ expect(output).toContain("android:");
470
+ expect(output).toContain("_twStyles._android_p_8");
471
+ expect(output).toContain("_twStyles._bg_blue_500");
472
+ });
473
+
474
+ it("should transform tw with multiple platform modifiers", () => {
475
+ const input = `
476
+ import { tw } from '@mgcrea/react-native-tailwind';
477
+
478
+ function MyComponent() {
479
+ const styles = tw\`bg-white ios:p-6 android:p-8 web:p-4\`;
480
+ return null;
481
+ }
482
+ `;
483
+
484
+ const output = transform(input);
485
+
486
+ // Should generate Platform.select() with all platforms
487
+ expect(output).toContain("Platform.select");
488
+ expect(output).toContain("ios:");
489
+ expect(output).toContain("android:");
490
+ expect(output).toContain("web:");
491
+ expect(output).toContain("_twStyles._ios_p_6");
492
+ expect(output).toContain("_twStyles._android_p_8");
493
+ expect(output).toContain("_twStyles._web_p_4");
494
+ });
495
+
496
+ it("should combine platform modifiers with color-scheme modifiers", () => {
497
+ const input = `
498
+ import { tw } from '@mgcrea/react-native-tailwind';
499
+
500
+ function MyComponent() {
501
+ const styles = tw\`bg-white ios:p-6 dark:bg-gray-900\`;
502
+ return null;
503
+ }
504
+ `;
505
+
506
+ const output = transform(input);
507
+
508
+ // Should have both Platform and useColorScheme
509
+ expect(output).toContain("Platform");
510
+ expect(output).toContain("useColorScheme");
511
+ expect(output).toContain("_twColorScheme");
512
+
513
+ // Should have both conditionals in style array
514
+ expect(output).toContain("Platform.select");
515
+ expect(output).toContain('_twColorScheme === "dark"');
516
+ });
517
+
518
+ it("should generate iosStyle/androidStyle/webStyle properties for manual access", () => {
519
+ const input = `
520
+ import { tw } from '@mgcrea/react-native-tailwind';
521
+
522
+ function MyComponent() {
523
+ const styles = tw\`bg-white ios:p-6 android:p-8 web:p-4\`;
524
+ return null;
525
+ }
526
+ `;
527
+
528
+ const output = transform(input);
529
+
530
+ // Should have separate platform style properties
531
+ expect(output).toContain("iosStyle:");
532
+ expect(output).toContain("_twStyles._ios_p_6");
533
+ expect(output).toContain("androidStyle:");
534
+ expect(output).toContain("_twStyles._android_p_8");
535
+ expect(output).toContain("webStyle:");
536
+ expect(output).toContain("_twStyles._web_p_4");
537
+
538
+ // Should also have runtime Platform.select() in style array
539
+ expect(output).toContain("Platform.select");
540
+ });
541
+
542
+ it("should work with only platform modifiers (no base class)", () => {
543
+ const input = `
544
+ import { tw } from '@mgcrea/react-native-tailwind';
545
+
546
+ function MyComponent() {
547
+ const styles = tw\`ios:p-6 android:p-8\`;
548
+ return null;
549
+ }
550
+ `;
551
+
552
+ const output = transform(input);
553
+
554
+ // Should generate Platform.select() even without base classes
555
+ expect(output).toContain("Platform.select");
556
+ expect(output).toContain("_twStyles._ios_p_6");
557
+ expect(output).toContain("_twStyles._android_p_8");
558
+ });
559
+
560
+ it("should allow accessing platform-specific styles manually", () => {
561
+ const input = `
562
+ import { tw } from '@mgcrea/react-native-tailwind';
563
+
564
+ function MyComponent() {
565
+ const btnStyles = tw\`bg-blue-500 ios:p-6\`;
566
+ const iosPadding = btnStyles.iosStyle;
567
+ return null;
568
+ }
569
+ `;
570
+
571
+ const output = transform(input);
572
+
573
+ // Should have iosStyle property available
574
+ expect(output).toContain("iosStyle:");
575
+ expect(output).toContain("_twStyles._ios_p_6");
576
+
577
+ // The actual usage line should be preserved
578
+ expect(output).toContain("btnStyles.iosStyle");
579
+ });
580
+
581
+ it("should combine state modifiers with platform modifiers", () => {
582
+ const input = `
583
+ import { tw } from '@mgcrea/react-native-tailwind';
584
+
585
+ function MyComponent() {
586
+ const styles = tw\`bg-white active:bg-blue-500 ios:p-6\`;
587
+ return null;
588
+ }
589
+ `;
590
+
591
+ const output = transform(input);
592
+
593
+ // Should have both activeStyle and platform modifiers
594
+ expect(output).toContain("activeStyle:");
595
+ expect(output).toContain("_twStyles._active_bg_blue_500");
596
+ expect(output).toContain("Platform.select");
597
+ expect(output).toContain("_twStyles._ios_p_6");
598
+ });
599
+ });
600
+
601
+ describe("tw/twStyle - integration with className", () => {
602
+ it("should work with both tw and className in same file", () => {
603
+ const input = `
604
+ import { tw } from '@mgcrea/react-native-tailwind';
605
+ import { View } from 'react-native';
606
+
607
+ const styles = tw\`bg-red-500\`;
608
+
609
+ export function Component() {
610
+ return <View className="m-4 p-2" />;
611
+ }
612
+ `;
613
+
614
+ const output = transform(input, undefined, true); // Enable JSX
615
+
616
+ // Should have both styles in StyleSheet
617
+ expect(output).toContain("_bg_red_500");
618
+ expect(output).toContain("_m_4_p_2");
619
+ });
620
+ });
621
+
622
+ describe("tw visitor - directional modifiers (RTL/LTR)", () => {
623
+ it("should transform rtl: modifier in tw template", () => {
624
+ const input = `
625
+ import { tw } from '@mgcrea/react-native-tailwind';
626
+
627
+ function MyComponent() {
628
+ const styles = tw\`p-4 rtl:mr-4\`;
629
+ return null;
630
+ }
631
+ `;
632
+
633
+ const output = transform(input);
634
+
635
+ // Should import I18nManager
636
+ expect(output).toContain("I18nManager");
637
+
638
+ // Should declare _twIsRTL variable
639
+ expect(output).toContain("_twIsRTL");
640
+ expect(output).toContain("I18nManager.isRTL");
641
+
642
+ // Should have style array with conditional
643
+ expect(output).toContain("style:");
644
+ expect(output).toContain("_twStyles._p_4");
645
+ expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/);
646
+
647
+ // Should have rtlStyle property
648
+ expect(output).toContain("rtlStyle:");
649
+ expect(output).toContain("_twStyles._rtl_mr_4");
650
+ });
651
+
652
+ it("should transform ltr: modifier with negated conditional", () => {
653
+ const input = `
654
+ import { tw } from '@mgcrea/react-native-tailwind';
655
+
656
+ function MyComponent() {
657
+ const styles = tw\`p-4 ltr:ml-4\`;
658
+ return null;
659
+ }
660
+ `;
661
+
662
+ const output = transform(input);
663
+
664
+ // Should import I18nManager
665
+ expect(output).toContain("I18nManager");
666
+
667
+ // Should have negated conditional for LTR (!_twIsRTL)
668
+ expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/);
669
+
670
+ // Should have ltrStyle property
671
+ expect(output).toContain("ltrStyle:");
672
+ });
673
+
674
+ it("should combine rtl: and ltr: modifiers", () => {
675
+ const input = `
676
+ import { tw } from '@mgcrea/react-native-tailwind';
677
+
678
+ function MyComponent() {
679
+ const styles = tw\`rtl:mr-4 ltr:ml-4\`;
680
+ return null;
681
+ }
682
+ `;
683
+
684
+ const output = transform(input);
685
+
686
+ // Should have both conditionals
687
+ expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/);
688
+ expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/);
689
+
690
+ // Should have both style properties
691
+ expect(output).toContain("rtlStyle:");
692
+ expect(output).toContain("ltrStyle:");
693
+ });
694
+
695
+ it("should combine directional modifiers with platform modifiers", () => {
696
+ const input = `
697
+ import { tw } from '@mgcrea/react-native-tailwind';
698
+
699
+ function MyComponent() {
700
+ const styles = tw\`p-4 ios:p-6 rtl:mr-4\`;
701
+ return null;
702
+ }
703
+ `;
704
+
705
+ const output = transform(input);
706
+
707
+ // Should have Platform import
708
+ expect(output).toContain("Platform");
709
+
710
+ // Should have I18nManager import
711
+ expect(output).toContain("I18nManager");
712
+
713
+ // Should have both modifiers in style array
714
+ expect(output).toContain("Platform.select");
715
+ expect(output).toMatch(/_twIsRTL\s*&&/);
716
+
717
+ // Should have iosStyle and rtlStyle properties
718
+ expect(output).toContain("iosStyle:");
719
+ expect(output).toContain("rtlStyle:");
720
+ });
721
+
722
+ it("should combine directional modifiers with state modifiers", () => {
723
+ const input = `
724
+ import { tw } from '@mgcrea/react-native-tailwind';
725
+
726
+ function MyComponent() {
727
+ const styles = tw\`bg-white active:bg-blue-500 rtl:pr-4\`;
728
+ return null;
729
+ }
730
+ `;
731
+
732
+ const output = transform(input);
733
+
734
+ // Should have I18nManager import
735
+ expect(output).toContain("I18nManager");
736
+
737
+ // Should have directional conditional
738
+ expect(output).toMatch(/_twIsRTL\s*&&/);
739
+
740
+ // Should have activeStyle property
741
+ expect(output).toContain("activeStyle:");
742
+ expect(output).toContain("_twStyles._active_bg_blue_500");
743
+
744
+ // Should have rtlStyle property
745
+ expect(output).toContain("rtlStyle:");
746
+ });
747
+
748
+ it("should work with twStyle function for RTL modifiers", () => {
749
+ const input = `
750
+ import { twStyle } from '@mgcrea/react-native-tailwind';
751
+
752
+ function MyComponent() {
753
+ const styles = twStyle('p-4 rtl:mr-4 ltr:ml-4');
754
+ return null;
755
+ }
756
+ `;
757
+
758
+ const output = transform(input);
759
+
760
+ // Should import I18nManager
761
+ expect(output).toContain("I18nManager");
762
+
763
+ // Should have both conditionals
764
+ expect(output).toMatch(/_twIsRTL\s*&&/);
765
+ expect(output).toMatch(/!\s*_twIsRTL\s*&&/);
766
+
767
+ // Should have both style properties
768
+ expect(output).toContain("rtlStyle:");
769
+ expect(output).toContain("ltrStyle:");
770
+ });
771
+ });