@mgcrea/react-native-tailwind 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +2 -1
  2. package/dist/babel/index.cjs +333 -195
  3. package/dist/babel/index.d.ts +4 -40
  4. package/dist/babel/index.test.ts +214 -1
  5. package/dist/babel/index.ts +4 -1169
  6. package/dist/babel/plugin.d.ts +42 -0
  7. package/{src/babel/index.test.ts → dist/babel/plugin.test.ts} +216 -2
  8. package/dist/babel/plugin.ts +491 -0
  9. package/dist/babel/utils/attributeMatchers.d.ts +23 -0
  10. package/dist/babel/utils/attributeMatchers.ts +71 -0
  11. package/dist/babel/utils/componentSupport.d.ts +18 -0
  12. package/dist/babel/utils/componentSupport.ts +68 -0
  13. package/dist/babel/utils/dynamicProcessing.d.ts +32 -0
  14. package/dist/babel/utils/dynamicProcessing.ts +223 -0
  15. package/dist/babel/utils/modifierProcessing.d.ts +26 -0
  16. package/dist/babel/utils/modifierProcessing.ts +118 -0
  17. package/dist/babel/utils/styleInjection.d.ts +15 -0
  18. package/dist/babel/utils/styleInjection.ts +80 -0
  19. package/dist/babel/utils/styleTransforms.d.ts +39 -0
  20. package/dist/babel/utils/styleTransforms.test.ts +349 -0
  21. package/dist/babel/utils/styleTransforms.ts +258 -0
  22. package/dist/babel/utils/twProcessing.d.ts +28 -0
  23. package/dist/babel/utils/twProcessing.ts +124 -0
  24. package/dist/components/TextInput.d.ts +171 -14
  25. package/dist/config/tailwind.d.ts +302 -0
  26. package/dist/config/tailwind.js +1 -0
  27. package/dist/index.d.ts +5 -4
  28. package/dist/index.js +1 -1
  29. package/dist/parser/colors.js +1 -1
  30. package/dist/parser/index.d.ts +1 -0
  31. package/dist/parser/index.js +1 -1
  32. package/dist/parser/modifiers.d.ts +2 -2
  33. package/dist/parser/modifiers.js +1 -1
  34. package/dist/parser/placeholder.d.ts +36 -0
  35. package/dist/parser/placeholder.js +1 -0
  36. package/dist/parser/placeholder.test.js +1 -0
  37. package/dist/parser/typography.d.ts +1 -0
  38. package/dist/parser/typography.js +1 -1
  39. package/dist/parser/typography.test.js +1 -1
  40. package/dist/runtime.cjs +1 -1
  41. package/dist/runtime.cjs.map +4 -4
  42. package/dist/runtime.d.ts +1 -14
  43. package/dist/runtime.js +1 -1
  44. package/dist/runtime.js.map +4 -4
  45. package/dist/stubs/tw.d.ts +1 -14
  46. package/dist/types/core.d.ts +40 -0
  47. package/dist/types/core.js +0 -0
  48. package/dist/types/index.d.ts +2 -0
  49. package/dist/types/index.js +1 -0
  50. package/dist/types/runtime.d.ts +15 -0
  51. package/dist/types/runtime.js +1 -0
  52. package/dist/types/util.d.ts +3 -0
  53. package/dist/types/util.js +0 -0
  54. package/package.json +1 -1
  55. package/src/babel/index.ts +4 -1169
  56. package/src/babel/plugin.test.ts +482 -0
  57. package/src/babel/plugin.ts +491 -0
  58. package/src/babel/utils/attributeMatchers.ts +71 -0
  59. package/src/babel/utils/componentSupport.ts +68 -0
  60. package/src/babel/utils/dynamicProcessing.ts +223 -0
  61. package/src/babel/utils/modifierProcessing.ts +118 -0
  62. package/src/babel/utils/styleInjection.ts +80 -0
  63. package/src/babel/utils/styleTransforms.test.ts +349 -0
  64. package/src/babel/utils/styleTransforms.ts +258 -0
  65. package/src/babel/utils/twProcessing.ts +124 -0
  66. package/src/components/TextInput.tsx +17 -14
  67. package/src/config/{palettes.ts → tailwind.ts} +2 -2
  68. package/src/index.ts +6 -3
  69. package/src/parser/colors.ts +2 -2
  70. package/src/parser/index.ts +1 -0
  71. package/src/parser/modifiers.ts +10 -4
  72. package/src/parser/placeholder.test.ts +105 -0
  73. package/src/parser/placeholder.ts +78 -0
  74. package/src/parser/typography.test.ts +11 -0
  75. package/src/parser/typography.ts +20 -2
  76. package/src/runtime.ts +1 -16
  77. package/src/stubs/tw.ts +1 -16
  78. package/src/{types.ts → types/core.ts} +0 -4
  79. package/src/types/index.ts +2 -0
  80. package/src/types/runtime.ts +17 -0
  81. package/src/types/util.ts +1 -0
@@ -0,0 +1,482 @@
1
+ /* eslint-disable @typescript-eslint/no-empty-function */
2
+ import { transformSync } from "@babel/core";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import babelPlugin, { type PluginOptions } from "./plugin.js";
5
+
6
+ /**
7
+ * Helper to transform code with the Babel plugin
8
+ */
9
+ function transform(code: string, options?: PluginOptions, includeJsx = false) {
10
+ const presets = includeJsx ? ["@babel/preset-react"] : [];
11
+
12
+ const result = transformSync(code, {
13
+ presets,
14
+ plugins: [[babelPlugin, options]],
15
+ filename: "test.tsx",
16
+ configFile: false,
17
+ babelrc: false,
18
+ });
19
+
20
+ return result?.code ?? "";
21
+ }
22
+
23
+ describe("Babel plugin - tw template tag transformation", () => {
24
+ it("should transform simple tw template literal", () => {
25
+ const input = `
26
+ import { tw } from '@mgcrea/react-native-tailwind';
27
+ const styles = tw\`bg-blue-500 m-4\`;
28
+ `;
29
+
30
+ const output = transform(input);
31
+
32
+ // Should have StyleSheet import (either ESM or CommonJS)
33
+ expect(output).toMatch(/import.*StyleSheet.*from "react-native"|require\("react-native"\)/);
34
+ expect(output).toContain("StyleSheet");
35
+
36
+ // Should have _twStyles definition
37
+ expect(output).toContain("_twStyles");
38
+ expect(output).toContain("StyleSheet.create");
39
+
40
+ // Should transform tw call to object with style property
41
+ expect(output).toContain("style:");
42
+ expect(output).toContain("_twStyles._bg_blue_500_m_4");
43
+
44
+ // Should remove tw import
45
+ expect(output).not.toContain("from '@mgcrea/react-native-tailwind'");
46
+ });
47
+
48
+ it("should transform tw with state modifiers", () => {
49
+ const input = `
50
+ import { tw } from '@mgcrea/react-native-tailwind';
51
+ const styles = tw\`bg-blue-500 active:bg-blue-700 disabled:bg-gray-300\`;
52
+ `;
53
+
54
+ const output = transform(input);
55
+
56
+ // Should have base style
57
+ expect(output).toContain("style:");
58
+ expect(output).toContain("_bg_blue_500");
59
+
60
+ // Should have activeStyle
61
+ expect(output).toContain("activeStyle:");
62
+ expect(output).toContain("_active_bg_blue_700");
63
+
64
+ // Should have disabledStyle
65
+ expect(output).toContain("disabledStyle:");
66
+ expect(output).toContain("_disabled_bg_gray_300");
67
+
68
+ // Should create StyleSheet with all styles
69
+ expect(output).toContain("backgroundColor:");
70
+ });
71
+
72
+ it("should inject StyleSheet.create after imports", () => {
73
+ const input = `
74
+ import { tw } from '@mgcrea/react-native-tailwind';
75
+ import { View } from 'react-native';
76
+
77
+ const styles = tw\`m-4\`;
78
+ `;
79
+
80
+ const output = transform(input);
81
+
82
+ // Find the position of imports and StyleSheet.create
83
+ const viewImportPos = output.indexOf('require("react-native")');
84
+ const styleSheetCreatePos = output.indexOf("_twStyles");
85
+
86
+ // StyleSheet.create should come after imports
87
+ expect(styleSheetCreatePos).toBeGreaterThan(viewImportPos);
88
+ });
89
+
90
+ it("should handle tw in object literals", () => {
91
+ const input = `
92
+ import { tw } from '@mgcrea/react-native-tailwind';
93
+
94
+ const sizeVariants = {
95
+ sm: {
96
+ container: tw\`h-9 px-3\`,
97
+ text: tw\`text-sm\`,
98
+ },
99
+ };
100
+ `;
101
+
102
+ const output = transform(input);
103
+
104
+ // Should define _twStyles before object literal
105
+ const twStylesPos = output.indexOf("_twStyles");
106
+ const sizeVariantsPos = output.indexOf("sizeVariants");
107
+
108
+ expect(twStylesPos).toBeGreaterThan(0);
109
+ expect(twStylesPos).toBeLessThan(sizeVariantsPos);
110
+
111
+ // Should have both styles
112
+ expect(output).toContain("_h_9_px_3");
113
+ expect(output).toContain("_text_sm");
114
+ });
115
+
116
+ it("should handle empty tw template literal", () => {
117
+ const input = `
118
+ import { tw } from '@mgcrea/react-native-tailwind';
119
+ const styles = tw\`\`;
120
+ `;
121
+
122
+ const output = transform(input);
123
+
124
+ // Should replace with empty style object
125
+ expect(output).toContain("style:");
126
+ expect(output).toContain("{}");
127
+ });
128
+
129
+ it("should preserve other imports from the same package", () => {
130
+ const input = `
131
+ import { tw, TwStyle, COLORS } from '@mgcrea/react-native-tailwind';
132
+ const styles = tw\`m-4\`;
133
+ `;
134
+
135
+ const output = transform(input);
136
+
137
+ // Should remove tw but keep other imports
138
+ expect(output).not.toContain('"tw"');
139
+ expect(output).toContain("TwStyle");
140
+ expect(output).toContain("COLORS");
141
+ });
142
+
143
+ it("should handle renamed tw import", () => {
144
+ const input = `
145
+ import { tw as customTw } from '@mgcrea/react-native-tailwind';
146
+ const styles = customTw\`m-4 p-2\`;
147
+ `;
148
+
149
+ const output = transform(input);
150
+
151
+ // Should still transform the renamed import
152
+ expect(output).toContain("_twStyles");
153
+ expect(output).toContain("_m_4_p_2");
154
+ expect(output).not.toContain("customTw");
155
+ });
156
+
157
+ it("should handle multiple tw calls", () => {
158
+ const input = `
159
+ import { tw } from '@mgcrea/react-native-tailwind';
160
+ const style1 = tw\`bg-red-500\`;
161
+ const style2 = tw\`bg-blue-500\`;
162
+ const style3 = tw\`bg-green-500\`;
163
+ `;
164
+
165
+ const output = transform(input);
166
+
167
+ // Should have all three styles in StyleSheet
168
+ expect(output).toContain("_bg_red_500");
169
+ expect(output).toContain("_bg_blue_500");
170
+ expect(output).toContain("_bg_green_500");
171
+
172
+ // Should have StyleSheet.create with all styles
173
+ expect(output).toContain("StyleSheet.create");
174
+ });
175
+
176
+ it("should use custom stylesIdentifier option", () => {
177
+ const input = `
178
+ import { tw } from '@mgcrea/react-native-tailwind';
179
+ const styles = tw\`m-4\`;
180
+ `;
181
+
182
+ const output = transform(input, { stylesIdentifier: "myStyles" });
183
+
184
+ // Should use custom identifier
185
+ expect(output).toContain("myStyles");
186
+ expect(output).toContain("myStyles._m_4");
187
+ expect(output).not.toContain("_twStyles");
188
+ });
189
+ });
190
+
191
+ describe("Babel plugin - twStyle function transformation", () => {
192
+ it("should transform twStyle function call", () => {
193
+ const input = `
194
+ import { twStyle } from '@mgcrea/react-native-tailwind';
195
+ const styles = twStyle('bg-blue-500 m-4');
196
+ `;
197
+
198
+ const output = transform(input);
199
+
200
+ // Should transform to object with style property
201
+ expect(output).toContain("style:");
202
+ expect(output).toContain("_twStyles._bg_blue_500_m_4");
203
+ });
204
+
205
+ it("should transform twStyle with modifiers", () => {
206
+ const input = `
207
+ import { twStyle } from '@mgcrea/react-native-tailwind';
208
+ const styles = twStyle('bg-blue-500 active:bg-blue-700');
209
+ `;
210
+
211
+ const output = transform(input);
212
+
213
+ expect(output).toContain("style:");
214
+ expect(output).toContain("activeStyle:");
215
+ });
216
+
217
+ it("should handle empty twStyle call", () => {
218
+ const input = `
219
+ import { twStyle } from '@mgcrea/react-native-tailwind';
220
+ const styles = twStyle('');
221
+ `;
222
+
223
+ const output = transform(input);
224
+
225
+ // Should replace with undefined
226
+ expect(output).toContain("undefined");
227
+ });
228
+ });
229
+
230
+ // Note: JSX tests require @babel/preset-react
231
+ describe("Babel plugin - className transformation (existing behavior)", () => {
232
+ it("should still transform className props", () => {
233
+ const input = `
234
+ import { View } from 'react-native';
235
+ export function Component() {
236
+ return <View className="m-4 p-2 bg-blue-500" />;
237
+ }
238
+ `;
239
+
240
+ const output = transform(input, undefined, true); // Enable JSX
241
+
242
+ // Should have StyleSheet
243
+ expect(output).toContain("StyleSheet.create");
244
+ expect(output).toContain("_twStyles");
245
+
246
+ // Should replace className with style
247
+ expect(output).not.toContain("className");
248
+ expect(output).toContain("style:");
249
+ });
250
+
251
+ it("should work with both tw and className in same file", () => {
252
+ const input = `
253
+ import { tw } from '@mgcrea/react-native-tailwind';
254
+ import { View } from 'react-native';
255
+
256
+ const styles = tw\`bg-red-500\`;
257
+
258
+ export function Component() {
259
+ return <View className="m-4 p-2" />;
260
+ }
261
+ `;
262
+
263
+ const output = transform(input, undefined, true); // Enable JSX
264
+
265
+ // Should have both styles in StyleSheet
266
+ expect(output).toContain("_bg_red_500");
267
+ expect(output).toContain("_m_4_p_2");
268
+ });
269
+
270
+ it("should merge className with function-based style prop", () => {
271
+ const input = `
272
+ import { TextInput } from 'react-native';
273
+ export function Component() {
274
+ return (
275
+ <TextInput
276
+ className="border border-gray-300 bg-gray-100"
277
+ style={({ focused, disabled }) => [
278
+ baseStyles,
279
+ focused && focusedStyles,
280
+ ]}
281
+ />
282
+ );
283
+ }
284
+ `;
285
+
286
+ const output = transform(input, undefined, true); // Enable JSX
287
+
288
+ // Should have StyleSheet with className styles
289
+ expect(output).toContain("StyleSheet.create");
290
+ // Style keys are sorted alphabetically: bg-gray-100 comes before border
291
+ expect(output).toContain("_bg_gray_100_border_border_gray_300");
292
+
293
+ // Should create a wrapper function that merges both
294
+ // The wrapper should call the original function and merge results
295
+ expect(output).toContain("_state");
296
+ expect(output).toContain("_twStyles._bg_gray_100_border_border_gray_300");
297
+
298
+ // Should not have className in output
299
+ expect(output).not.toContain("className");
300
+
301
+ // Should have a function that accepts state and returns an array
302
+ expect(output).toMatch(/_state\s*=>/);
303
+ });
304
+
305
+ it("should merge dynamic className with function-based style prop", () => {
306
+ const input = `
307
+ import { TextInput } from 'react-native';
308
+ export function Component({ isError }) {
309
+ return (
310
+ <TextInput
311
+ className={\`border \${isError ? 'border-red-500' : 'border-gray-300'}\`}
312
+ style={({ focused }) => [
313
+ baseStyles,
314
+ focused && focusedStyles,
315
+ ]}
316
+ />
317
+ );
318
+ }
319
+ `;
320
+
321
+ const output = transform(input, undefined, true); // Enable JSX
322
+
323
+ // Should have StyleSheet with both className styles
324
+ expect(output).toContain("StyleSheet.create");
325
+ expect(output).toContain("_border");
326
+ expect(output).toContain("_border_red_500");
327
+ expect(output).toContain("_border_gray_300");
328
+
329
+ // Should create a wrapper function that merges dynamic styles with function result
330
+ expect(output).toContain("_state");
331
+
332
+ // Should not have className in output
333
+ expect(output).not.toContain("className");
334
+ });
335
+ });
336
+
337
+ describe("Babel plugin - placeholder: modifier transformation", () => {
338
+ it("should transform placeholder:text-{color} to placeholderTextColor prop", () => {
339
+ const input = `
340
+ import { TextInput } from 'react-native';
341
+ export function Component() {
342
+ return (
343
+ <TextInput
344
+ className="border-2 placeholder:text-gray-400"
345
+ placeholder="Email"
346
+ />
347
+ );
348
+ }
349
+ `;
350
+
351
+ const output = transform(input, undefined, true);
352
+
353
+ // Should have placeholderTextColor prop with correct hex value (from custom palette)
354
+ expect(output).toContain('placeholderTextColor: "#99a1af"');
355
+
356
+ // Should still have style for border-2
357
+ expect(output).toContain("StyleSheet.create");
358
+ expect(output).toContain("_border_2");
359
+
360
+ // Should not have className in output
361
+ expect(output).not.toContain("className");
362
+ });
363
+
364
+ it("should support placeholder colors with opacity", () => {
365
+ const input = `
366
+ import { TextInput } from 'react-native';
367
+ export function Component() {
368
+ return <TextInput className="placeholder:text-red-500/50" />;
369
+ }
370
+ `;
371
+
372
+ const output = transform(input, undefined, true);
373
+
374
+ // Should have 8-digit hex with alpha channel (custom palette red-500, uppercased)
375
+ expect(output).toContain('placeholderTextColor: "#FB2C3680"');
376
+ });
377
+
378
+ it("should support arbitrary placeholder colors", () => {
379
+ const input = `
380
+ import { TextInput } from 'react-native';
381
+ export function Component() {
382
+ return <TextInput className="placeholder:text-[#ff0000]" />;
383
+ }
384
+ `;
385
+
386
+ const output = transform(input, undefined, true);
387
+
388
+ expect(output).toContain('placeholderTextColor: "#ff0000"');
389
+ });
390
+
391
+ it("should combine placeholder: with other modifiers", () => {
392
+ const input = `
393
+ import { TextInput } from 'react-native';
394
+ export function Component() {
395
+ return (
396
+ <TextInput
397
+ className="border-2 focus:border-blue-500 placeholder:text-gray-400"
398
+ placeholder="Email"
399
+ />
400
+ );
401
+ }
402
+ `;
403
+
404
+ const output = transform(input, undefined, true);
405
+
406
+ // Should have placeholderTextColor prop (custom palette gray-400)
407
+ expect(output).toContain('placeholderTextColor: "#99a1af"');
408
+
409
+ // Should have focus: modifier handling (style function)
410
+ expect(output).toContain("focused");
411
+ expect(output).toMatch(/style[\s\S]*=>/); // Style function
412
+
413
+ // Should not have className
414
+ expect(output).not.toContain("className");
415
+ });
416
+
417
+ it("should handle multiple placeholder: classes (last wins)", () => {
418
+ const input = `
419
+ import { TextInput } from 'react-native';
420
+ export function Component() {
421
+ return (
422
+ <TextInput className="placeholder:text-red-500 placeholder:text-blue-500" />
423
+ );
424
+ }
425
+ `;
426
+
427
+ const output = transform(input, undefined, true);
428
+
429
+ // Blue should win (last color, custom palette blue-500)
430
+ expect(output).toContain('placeholderTextColor: "#2b7fff"');
431
+ });
432
+
433
+ it("should ignore non-text utilities in placeholder: modifier", () => {
434
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
435
+
436
+ const input = `
437
+ import { TextInput } from 'react-native';
438
+ export function Component() {
439
+ return (
440
+ <TextInput className="placeholder:font-bold placeholder:text-gray-400" />
441
+ );
442
+ }
443
+ `;
444
+
445
+ const output = transform(input, undefined, true);
446
+
447
+ // Should still have the valid text color (custom palette gray-400)
448
+ expect(output).toContain('placeholderTextColor: "#99a1af"');
449
+
450
+ // Should not have font-bold anywhere
451
+ expect(output).not.toContain("fontWeight");
452
+
453
+ consoleSpy.mockRestore();
454
+ });
455
+
456
+ it.skip("should work with custom colors", () => {
457
+ // Note: This test would require setting up a tailwind.config file
458
+ // For now, we'll skip custom color testing in Babel tests
459
+ // Custom colors are tested in the parser tests
460
+ });
461
+
462
+ it("should not transform placeholder: on non-TextInput elements", () => {
463
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
464
+
465
+ const input = `
466
+ import { View } from 'react-native';
467
+ export function Component() {
468
+ return <View className="placeholder:text-gray-400" />;
469
+ }
470
+ `;
471
+
472
+ const output = transform(input, undefined, true);
473
+
474
+ // Should not have placeholderTextColor prop (View doesn't support it)
475
+ expect(output).not.toContain("placeholderTextColor");
476
+
477
+ // Should warn about unsupported modifier
478
+ // (The warning happens because View doesn't support any modifiers)
479
+
480
+ consoleSpy.mockRestore();
481
+ });
482
+ });