@mgcrea/react-native-tailwind 0.6.1 → 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 (100) hide show
  1. package/README.md +406 -1
  2. package/dist/babel/config-loader.ts +1 -23
  3. package/dist/babel/index.cjs +516 -211
  4. package/dist/babel/index.d.ts +4 -15
  5. package/dist/babel/index.test.ts +481 -0
  6. package/dist/babel/index.ts +4 -861
  7. package/dist/babel/plugin.d.ts +42 -0
  8. package/dist/babel/plugin.test.ts +482 -0
  9. package/dist/babel/plugin.ts +491 -0
  10. package/dist/babel/utils/attributeMatchers.d.ts +23 -0
  11. package/dist/babel/utils/attributeMatchers.ts +71 -0
  12. package/dist/babel/utils/componentSupport.d.ts +18 -0
  13. package/dist/babel/utils/componentSupport.ts +68 -0
  14. package/dist/babel/utils/dynamicProcessing.d.ts +32 -0
  15. package/dist/babel/utils/dynamicProcessing.ts +223 -0
  16. package/dist/babel/utils/modifierProcessing.d.ts +26 -0
  17. package/dist/babel/utils/modifierProcessing.ts +118 -0
  18. package/dist/babel/utils/styleInjection.d.ts +15 -0
  19. package/dist/babel/utils/styleInjection.ts +80 -0
  20. package/dist/babel/utils/styleTransforms.d.ts +39 -0
  21. package/dist/babel/utils/styleTransforms.test.ts +349 -0
  22. package/dist/babel/utils/styleTransforms.ts +258 -0
  23. package/dist/babel/utils/twProcessing.d.ts +28 -0
  24. package/dist/babel/utils/twProcessing.ts +124 -0
  25. package/dist/components/TextInput.d.ts +171 -14
  26. package/dist/config/tailwind.d.ts +302 -0
  27. package/dist/config/tailwind.js +1 -0
  28. package/dist/index.d.ts +6 -2
  29. package/dist/index.js +1 -1
  30. package/dist/parser/__snapshots__/colors.test.js.snap +242 -90
  31. package/dist/parser/__snapshots__/transforms.test.js.snap +58 -0
  32. package/dist/parser/colors.js +1 -1
  33. package/dist/parser/index.d.ts +1 -0
  34. package/dist/parser/index.js +1 -1
  35. package/dist/parser/modifiers.d.ts +2 -2
  36. package/dist/parser/modifiers.js +1 -1
  37. package/dist/parser/placeholder.d.ts +36 -0
  38. package/dist/parser/placeholder.js +1 -0
  39. package/dist/parser/placeholder.test.js +1 -0
  40. package/dist/parser/typography.d.ts +1 -0
  41. package/dist/parser/typography.js +1 -1
  42. package/dist/parser/typography.test.js +1 -1
  43. package/dist/runtime.cjs +2 -0
  44. package/dist/runtime.cjs.map +7 -0
  45. package/dist/runtime.d.ts +126 -0
  46. package/dist/runtime.js +2 -0
  47. package/dist/runtime.js.map +7 -0
  48. package/dist/runtime.test.js +1 -0
  49. package/dist/stubs/tw.d.ts +47 -0
  50. package/dist/stubs/tw.js +1 -0
  51. package/dist/types/core.d.ts +40 -0
  52. package/dist/types/core.js +0 -0
  53. package/dist/types/index.d.ts +2 -0
  54. package/dist/types/index.js +1 -0
  55. package/dist/types/runtime.d.ts +15 -0
  56. package/dist/types/runtime.js +1 -0
  57. package/dist/types/util.d.ts +3 -0
  58. package/dist/types/util.js +0 -0
  59. package/dist/utils/flattenColors.d.ts +16 -0
  60. package/dist/utils/flattenColors.js +1 -0
  61. package/dist/utils/flattenColors.test.js +1 -0
  62. package/dist/utils/modifiers.d.ts +29 -0
  63. package/dist/utils/modifiers.js +1 -0
  64. package/dist/utils/modifiers.test.js +1 -0
  65. package/dist/utils/styleKey.test.js +1 -0
  66. package/package.json +15 -3
  67. package/src/babel/config-loader.ts +1 -23
  68. package/src/babel/index.ts +4 -861
  69. package/src/babel/plugin.test.ts +482 -0
  70. package/src/babel/plugin.ts +491 -0
  71. package/src/babel/utils/attributeMatchers.ts +71 -0
  72. package/src/babel/utils/componentSupport.ts +68 -0
  73. package/src/babel/utils/dynamicProcessing.ts +223 -0
  74. package/src/babel/utils/modifierProcessing.ts +118 -0
  75. package/src/babel/utils/styleInjection.ts +80 -0
  76. package/src/babel/utils/styleTransforms.test.ts +349 -0
  77. package/src/babel/utils/styleTransforms.ts +258 -0
  78. package/src/babel/utils/twProcessing.ts +124 -0
  79. package/src/components/TextInput.tsx +17 -14
  80. package/src/config/{palettes.ts → tailwind.ts} +2 -2
  81. package/src/index.ts +9 -1
  82. package/src/parser/colors.ts +9 -23
  83. package/src/parser/index.ts +1 -0
  84. package/src/parser/modifiers.ts +10 -4
  85. package/src/parser/placeholder.test.ts +105 -0
  86. package/src/parser/placeholder.ts +78 -0
  87. package/src/parser/typography.test.ts +11 -0
  88. package/src/parser/typography.ts +20 -2
  89. package/src/runtime.test.ts +325 -0
  90. package/src/runtime.ts +265 -0
  91. package/src/stubs/tw.ts +65 -0
  92. package/src/{types.ts → types/core.ts} +0 -4
  93. package/src/types/index.ts +2 -0
  94. package/src/types/runtime.ts +17 -0
  95. package/src/types/util.ts +1 -0
  96. package/src/utils/flattenColors.test.ts +361 -0
  97. package/src/utils/flattenColors.ts +32 -0
  98. package/src/utils/modifiers.test.ts +286 -0
  99. package/src/utils/modifiers.ts +63 -0
  100. package/src/utils/styleKey.test.ts +168 -0
@@ -1,864 +1,7 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
- /* eslint-disable @typescript-eslint/no-unsafe-call */
3
- /* eslint-disable @typescript-eslint/no-unsafe-member-access */
4
- /* eslint-disable @typescript-eslint/no-explicit-any */
5
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
6
-
7
- /**
8
- * Babel plugin for react-native-tailwind
9
- * Transforms className props to style props at compile time
10
- */
11
-
12
- import type { NodePath, PluginObj, PluginPass } from "@babel/core";
13
- import * as BabelTypes from "@babel/types";
14
- import { StyleObject } from "src/types.js";
15
- import type { ModifierType, ParsedModifier } from "../parser/index.js";
16
- import { parseClassName as parseClassNameFn, splitModifierClasses } from "../parser/index.js";
17
- import { generateStyleKey as generateStyleKeyFn } from "../utils/styleKey.js";
18
- import { extractCustomColors } from "./config-loader.js";
19
-
20
- type PluginState = PluginPass & {
21
- styleRegistry: Map<string, StyleObject>;
22
- hasClassNames: boolean;
23
- hasStyleSheetImport: boolean;
24
- customColors: Record<string, string>;
25
- };
26
-
27
- // Use a unique identifier to avoid conflicts with user's own styles
28
- const STYLES_IDENTIFIER = "_twStyles";
29
-
30
- /**
31
- * Supported className-like attributes
32
- */
33
- const SUPPORTED_CLASS_ATTRIBUTES = [
34
- "className",
35
- "containerClassName",
36
- "contentContainerClassName",
37
- "columnWrapperClassName",
38
- "ListHeaderComponentClassName",
39
- "ListFooterComponentClassName",
40
- ] as const;
41
-
42
- /**
43
- * Get the target style prop name based on the className attribute
44
- */
45
- function getTargetStyleProp(attributeName: string): string {
46
- if (attributeName === "containerClassName") {
47
- return "containerStyle";
48
- }
49
- if (attributeName === "contentContainerClassName") {
50
- return "contentContainerStyle";
51
- }
52
- if (attributeName === "columnWrapperClassName") {
53
- return "columnWrapperStyle";
54
- }
55
- if (attributeName === "ListHeaderComponentClassName") {
56
- return "ListHeaderComponentStyle";
57
- }
58
- if (attributeName === "ListFooterComponentClassName") {
59
- return "ListFooterComponentStyle";
60
- }
61
- return "style";
62
- }
63
-
64
- /**
65
- * Check if a JSX element supports modifiers and determine which modifiers are supported
66
- * Returns an object with component info and supported modifiers
67
- */
68
- function getComponentModifierSupport(
69
- jsxElement: any,
70
- t: typeof BabelTypes,
71
- ): { component: string; supportedModifiers: ModifierType[] } | null {
72
- if (!t.isJSXOpeningElement(jsxElement)) {
73
- return null;
74
- }
75
-
76
- const name = jsxElement.name;
77
- let componentName: string | null = null;
78
-
79
- // Handle simple identifier: <Pressable>
80
- if (t.isJSXIdentifier(name)) {
81
- componentName = name.name;
82
- }
83
-
84
- // Handle member expression: <ReactNative.Pressable>
85
- if (t.isJSXMemberExpression(name)) {
86
- const property = name.property;
87
- if (t.isJSXIdentifier(property)) {
88
- componentName = property.name;
89
- }
90
- }
91
-
92
- if (!componentName) {
93
- return null;
94
- }
95
-
96
- // Map components to their supported modifiers
97
- switch (componentName) {
98
- case "Pressable":
99
- return { component: "Pressable", supportedModifiers: ["active", "hover", "focus", "disabled"] };
100
- case "TextInput":
101
- return { component: "TextInput", supportedModifiers: ["focus", "disabled"] };
102
- default:
103
- return null;
104
- }
105
- }
106
-
107
1
  /**
108
- * Result of processing a dynamic expression
2
+ * Main entry point for the Babel plugin
3
+ * Re-exports the plugin from plugin.ts
109
4
  */
110
- type DynamicExpressionResult = {
111
- // The transformed expression to use in the style prop
112
- expression: any;
113
- // Static parts that can be parsed at compile time (if any)
114
- staticParts?: string[];
115
- };
116
-
117
- /**
118
- * Process a dynamic className expression
119
- * Extracts static strings and transforms the expression to use pre-compiled styles
120
- */
121
- function processDynamicExpression(
122
- expression: any,
123
- state: PluginState,
124
- t: typeof BabelTypes,
125
- ): DynamicExpressionResult | null {
126
- // Handle template literals: `m-4 ${condition ? "p-4" : "p-2"}`
127
- if (t.isTemplateLiteral(expression)) {
128
- return processTemplateLiteral(expression, state, t);
129
- }
130
-
131
- // Handle conditional expressions: condition ? "m-4" : "p-2"
132
- if (t.isConditionalExpression(expression)) {
133
- return processConditionalExpression(expression, state, t);
134
- }
135
-
136
- // Handle logical expressions: condition && "m-4"
137
- if (t.isLogicalExpression(expression)) {
138
- return processLogicalExpression(expression, state, t);
139
- }
140
-
141
- // Unsupported expression type
142
- return null;
143
- }
144
-
145
- /**
146
- * Process template literal: `static ${dynamic} more-static`
147
- */
148
- function processTemplateLiteral(
149
- node: any,
150
- state: PluginState,
151
- t: typeof BabelTypes,
152
- ): DynamicExpressionResult | null {
153
- const parts: any[] = [];
154
- const staticParts: string[] = [];
155
-
156
- // Process quasis (static parts) and expressions (dynamic parts)
157
- for (let i = 0; i < node.quasis.length; i++) {
158
- const quasi = node.quasis[i];
159
- const staticText = quasi.value.cooked?.trim();
160
-
161
- // Add static part if not empty
162
- if (staticText) {
163
- // Parse static classes and add to registry
164
- const classes = staticText.split(/\s+/).filter(Boolean);
165
- for (const cls of classes) {
166
- const styleObject = parseClassName(cls, state.customColors);
167
- const styleKey = generateStyleKey(cls);
168
- state.styleRegistry.set(styleKey, styleObject);
169
- staticParts.push(cls);
170
-
171
- // Add to parts array
172
- parts.push(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)));
173
- }
174
- }
175
-
176
- // Add dynamic expression if exists
177
- if (i < node.expressions.length) {
178
- const expr = node.expressions[i];
179
-
180
- // Recursively process nested dynamic expressions
181
- const result = processDynamicExpression(expr, state, t);
182
- if (result) {
183
- parts.push(result.expression);
184
- } else {
185
- // For unsupported expressions, keep them as-is
186
- // This won't work at runtime but maintains the structure
187
- parts.push(expr);
188
- }
189
- }
190
- }
191
-
192
- if (parts.length === 0) {
193
- return null;
194
- }
195
-
196
- // If single part, return it directly; otherwise return array
197
- const expression = parts.length === 1 ? parts[0] : t.arrayExpression(parts);
198
-
199
- return {
200
- expression,
201
- staticParts: staticParts.length > 0 ? staticParts : undefined,
202
- };
203
- }
204
-
205
- /**
206
- * Process conditional expression: condition ? "class-a" : "class-b"
207
- */
208
- function processConditionalExpression(
209
- node: any,
210
- state: PluginState,
211
- t: typeof BabelTypes,
212
- ): DynamicExpressionResult | null {
213
- const consequent = processStringOrExpression(node.consequent, state, t);
214
- const alternate = processStringOrExpression(node.alternate, state, t);
215
-
216
- if (!consequent && !alternate) {
217
- return null;
218
- }
219
-
220
- // Build conditional: condition ? consequentStyle : alternateStyle
221
- const expression = t.conditionalExpression(
222
- node.test,
223
- consequent ?? t.nullLiteral(),
224
- alternate ?? t.nullLiteral(),
225
- );
226
-
227
- return { expression };
228
- }
229
-
230
- /**
231
- * Process logical expression: condition && "class-a"
232
- */
233
- function processLogicalExpression(
234
- node: any,
235
- state: PluginState,
236
- t: typeof BabelTypes,
237
- ): DynamicExpressionResult | null {
238
- // Only handle AND (&&) expressions
239
- if (node.operator !== "&&") {
240
- return null;
241
- }
242
-
243
- const right = processStringOrExpression(node.right, state, t);
244
-
245
- if (!right) {
246
- return null;
247
- }
248
-
249
- // Build logical: condition && style
250
- const expression = t.logicalExpression("&&", node.left, right);
251
-
252
- return { expression };
253
- }
254
-
255
- /**
256
- * Process a node that might be a string literal or another expression
257
- */
258
- function processStringOrExpression(node: any, state: PluginState, t: typeof BabelTypes): any {
259
- // Handle string literals
260
- if (t.isStringLiteral(node)) {
261
- const className = node.value.trim();
262
- if (!className) {
263
- return null;
264
- }
265
-
266
- // Parse and register styles
267
- const styleObject = parseClassName(className, state.customColors);
268
- const styleKey = generateStyleKey(className);
269
- state.styleRegistry.set(styleKey, styleObject);
270
-
271
- return t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey));
272
- }
273
-
274
- // Handle nested expressions recursively
275
- if (t.isConditionalExpression(node)) {
276
- const result = processConditionalExpression(node, state, t);
277
- return result?.expression ?? null;
278
- }
279
-
280
- if (t.isLogicalExpression(node)) {
281
- const result = processLogicalExpression(node, state, t);
282
- return result?.expression ?? null;
283
- }
284
-
285
- if (t.isTemplateLiteral(node)) {
286
- const result = processTemplateLiteral(node, state, t);
287
- return result?.expression ?? null;
288
- }
289
-
290
- // Unsupported - return null
291
- return null;
292
- }
293
-
294
- /**
295
- * Process a static className string that contains modifiers
296
- * Returns a style function expression for Pressable components
297
- */
298
- function processStaticClassNameWithModifiers(
299
- className: string,
300
- state: PluginState,
301
- t: typeof BabelTypes,
302
- ): any {
303
- const { baseClasses, modifierClasses } = splitModifierClasses(className);
304
-
305
- // Parse and register base classes
306
- let baseStyleExpression: any = null;
307
- if (baseClasses.length > 0) {
308
- const baseClassName = baseClasses.join(" ");
309
- const baseStyleObject = parseClassName(baseClassName, state.customColors);
310
- const baseStyleKey = generateStyleKey(baseClassName);
311
- state.styleRegistry.set(baseStyleKey, baseStyleObject);
312
- baseStyleExpression = t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(baseStyleKey));
313
- }
314
-
315
- // Parse and register modifier classes
316
- // Group by modifier type for better organization
317
- const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
318
- for (const mod of modifierClasses) {
319
- if (!modifiersByType.has(mod.modifier)) {
320
- modifiersByType.set(mod.modifier, []);
321
- }
322
- const modGroup = modifiersByType.get(mod.modifier);
323
- if (modGroup) {
324
- modGroup.push(mod);
325
- }
326
- }
327
-
328
- // Build style function: ({ pressed }) => [baseStyle, pressed && modifierStyle]
329
- const styleArrayElements: any[] = [];
330
-
331
- // Add base style first
332
- if (baseStyleExpression) {
333
- styleArrayElements.push(baseStyleExpression);
334
- }
335
-
336
- // Add conditional styles for each modifier type
337
- for (const [modifierType, modifiers] of modifiersByType) {
338
- // Parse all modifier classes together
339
- const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
340
- const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
341
- const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
342
- state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
343
-
344
- // Create conditional: pressed && styles._active_bg_blue_700
345
- const stateProperty = getStatePropertyForModifier(modifierType);
346
- const conditionalExpression = t.logicalExpression(
347
- "&&",
348
- t.identifier(stateProperty),
349
- t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(modifierStyleKey)),
350
- );
351
-
352
- styleArrayElements.push(conditionalExpression);
353
- }
354
-
355
- // If only base style, return it directly; otherwise return array
356
- if (styleArrayElements.length === 1) {
357
- return styleArrayElements[0];
358
- }
359
-
360
- return t.arrayExpression(styleArrayElements);
361
- }
362
-
363
- /**
364
- * Get the state property name for a modifier type
365
- * Maps modifier types to component state parameter properties
366
- */
367
- function getStatePropertyForModifier(modifier: ModifierType): string {
368
- switch (modifier) {
369
- case "active":
370
- return "pressed";
371
- case "hover":
372
- return "hovered";
373
- case "focus":
374
- return "focused";
375
- case "disabled":
376
- return "disabled";
377
- default:
378
- return "pressed"; // fallback
379
- }
380
- }
381
-
382
- /**
383
- * Create a style function for Pressable: ({ pressed }) => styleExpression
384
- */
385
- function createStyleFunction(styleExpression: any, modifierTypes: ModifierType[], t: typeof BabelTypes): any {
386
- // Build parameter object: { pressed, hovered, focused }
387
- const paramProperties: any[] = [];
388
- const usedStateProps = new Set<string>();
389
-
390
- for (const modifierType of modifierTypes) {
391
- const stateProperty = getStatePropertyForModifier(modifierType);
392
- if (!usedStateProps.has(stateProperty)) {
393
- usedStateProps.add(stateProperty);
394
- paramProperties.push(
395
- t.objectProperty(t.identifier(stateProperty), t.identifier(stateProperty), false, true),
396
- );
397
- }
398
- }
399
-
400
- const param = t.objectPattern(paramProperties);
401
-
402
- // Create arrow function: ({ pressed }) => styleExpression
403
- return t.arrowFunctionExpression([param], styleExpression);
404
- }
405
-
406
- export default function reactNativeTailwindBabelPlugin({
407
- types: t,
408
- }: {
409
- types: typeof BabelTypes;
410
- }): PluginObj<PluginState> {
411
- return {
412
- name: "react-native-tailwind",
413
-
414
- visitor: {
415
- Program: {
416
- enter(_path: NodePath, state: PluginState) {
417
- // Initialize state for this file
418
- state.styleRegistry = new Map();
419
- state.hasClassNames = false;
420
- state.hasStyleSheetImport = false;
421
-
422
- // Load custom colors from tailwind.config.*
423
- state.customColors = extractCustomColors(state.file.opts.filename ?? "");
424
- },
425
-
426
- exit(path: NodePath, state: PluginState) {
427
- // If no classNames were found, skip StyleSheet generation
428
- if (!state.hasClassNames || state.styleRegistry.size === 0) {
429
- return;
430
- }
431
-
432
- // Add StyleSheet import if not already present
433
- if (!state.hasStyleSheetImport) {
434
- addStyleSheetImport(path, t);
435
- }
436
-
437
- // Generate and inject StyleSheet.create at the end of the file
438
- injectStyles(path, state.styleRegistry, t);
439
- },
440
- },
441
-
442
- // Check if StyleSheet is already imported
443
- ImportDeclaration(path: NodePath, state: PluginState) {
444
- const node = path.node as any;
445
- if (node.source.value === "react-native") {
446
- const specifiers = node.specifiers;
447
- const hasStyleSheet = specifiers.some((spec: any) => {
448
- if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
449
- return spec.imported.name === "StyleSheet";
450
- }
451
- return false;
452
- });
453
-
454
- if (hasStyleSheet) {
455
- state.hasStyleSheetImport = true;
456
- } else {
457
- // Add StyleSheet to existing import
458
- node.specifiers.push(t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet")));
459
- state.hasStyleSheetImport = true;
460
- }
461
- }
462
- },
463
-
464
- JSXAttribute(path: NodePath, state: PluginState) {
465
- const node = path.node as any;
466
- const attributeName = node.name.name;
467
-
468
- // Only process className-like attributes
469
- if (!SUPPORTED_CLASS_ATTRIBUTES.includes(attributeName)) {
470
- return;
471
- }
472
-
473
- const value = node.value;
474
-
475
- // Determine target style prop based on attribute name
476
- const targetStyleProp = getTargetStyleProp(attributeName);
477
-
478
- // Handle static string literals
479
- if (t.isStringLiteral(value)) {
480
- const className = value.value.trim();
481
-
482
- // Skip empty classNames
483
- if (!className) {
484
- path.remove();
485
- return;
486
- }
487
-
488
- state.hasClassNames = true;
489
-
490
- // Check if className contains modifiers (active:, hover:, focus:)
491
- const { baseClasses, modifierClasses } = splitModifierClasses(className);
492
-
493
- // If there are modifiers, check if this component supports them
494
- if (modifierClasses.length > 0) {
495
- // Get the JSX opening element (the direct parent of the attribute)
496
- const jsxOpeningElement = path.parent;
497
- const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
498
-
499
- if (componentSupport) {
500
- // Get modifier types used in className
501
- const usedModifiers = Array.from(new Set(modifierClasses.map((m) => m.modifier)));
502
-
503
- // Check if all modifiers are supported by this component
504
- const unsupportedModifiers = usedModifiers.filter(
505
- (mod) => !componentSupport.supportedModifiers.includes(mod),
506
- );
507
-
508
- if (unsupportedModifiers.length > 0) {
509
- // Warn about unsupported modifiers
510
- if (process.env.NODE_ENV !== "production") {
511
- console.warn(
512
- `[react-native-tailwind] Modifiers (${unsupportedModifiers.map((m) => `${m}:`).join(", ")}) are not supported on ${componentSupport.component} component at ${state.file.opts.filename ?? "unknown"}. ` +
513
- `Supported modifiers: ${componentSupport.supportedModifiers.join(", ")}`,
514
- );
515
- }
516
- // Filter out unsupported modifiers
517
- const supportedModifierClasses = modifierClasses.filter((m) =>
518
- componentSupport.supportedModifiers.includes(m.modifier),
519
- );
520
-
521
- // If no supported modifiers remain, fall through to normal processing
522
- if (supportedModifierClasses.length === 0) {
523
- // Continue to normal processing
524
- } else {
525
- // Process only supported modifiers
526
- const filteredClassName =
527
- baseClasses.join(" ") +
528
- " " +
529
- supportedModifierClasses.map((m) => `${m.modifier}:${m.baseClass}`).join(" ");
530
- const styleExpression = processStaticClassNameWithModifiers(
531
- filteredClassName.trim(),
532
- state,
533
- t,
534
- );
535
- const modifierTypes = Array.from(new Set(supportedModifierClasses.map((m) => m.modifier)));
536
- const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
537
-
538
- const parent = path.parent as any;
539
- const styleAttribute = parent.attributes.find(
540
- (attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
541
- );
542
-
543
- if (styleAttribute) {
544
- mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
545
- } else {
546
- replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
547
- }
548
- return;
549
- }
550
- } else {
551
- // All modifiers are supported - process normally
552
- const styleExpression = processStaticClassNameWithModifiers(className, state, t);
553
- const modifierTypes = usedModifiers;
554
- const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
555
-
556
- const parent = path.parent as any;
557
- const styleAttribute = parent.attributes.find(
558
- (attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
559
- );
560
-
561
- if (styleAttribute) {
562
- mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
563
- } else {
564
- replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
565
- }
566
- return;
567
- }
568
- } else {
569
- // Component doesn't support any modifiers
570
- if (process.env.NODE_ENV !== "production") {
571
- const usedModifiers = Array.from(new Set(modifierClasses.map((m) => m.modifier)));
572
- console.warn(
573
- `[react-native-tailwind] Modifiers (${usedModifiers.map((m) => `${m}:`).join(", ")}) can only be used on compatible components (Pressable, TextInput). Found on unsupported element at ${state.file.opts.filename ?? "unknown"}`,
574
- );
575
- }
576
- // Fall through to normal processing (ignore modifiers)
577
- }
578
- }
579
-
580
- // Normal processing without modifiers
581
- const styleObject = parseClassName(className, state.customColors);
582
- const styleKey = generateStyleKey(className);
583
- state.styleRegistry.set(styleKey, styleObject);
584
-
585
- // Check if there's already a style prop on this element
586
- const parent = path.parent as any;
587
- const styleAttribute = parent.attributes.find(
588
- (attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
589
- );
590
-
591
- if (styleAttribute) {
592
- // Merge with existing style prop
593
- mergeStyleAttribute(path, styleAttribute, styleKey, t);
594
- } else {
595
- // Replace className with style prop
596
- replaceWithStyleAttribute(path, styleKey, targetStyleProp, t);
597
- }
598
- return;
599
- }
600
-
601
- // Handle dynamic expressions (JSXExpressionContainer)
602
- if (t.isJSXExpressionContainer(value)) {
603
- const expression = value.expression;
604
-
605
- // Skip JSXEmptyExpression
606
- if (t.isJSXEmptyExpression(expression)) {
607
- return;
608
- }
609
-
610
- try {
611
- // Process dynamic expression
612
- const result = processDynamicExpression(expression, state, t);
613
-
614
- if (result) {
615
- state.hasClassNames = true;
616
-
617
- // Check if there's already a style prop on this element
618
- const parent = path.parent as any;
619
- const styleAttribute = parent.attributes.find(
620
- (attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
621
- );
622
-
623
- if (styleAttribute) {
624
- // Merge with existing style prop
625
- mergeDynamicStyleAttribute(path, styleAttribute, result, t);
626
- } else {
627
- // Replace className with style prop
628
- replaceDynamicWithStyleAttribute(path, result, targetStyleProp, t);
629
- }
630
- return;
631
- }
632
- } catch (error) {
633
- // Fall through to warning
634
- if (process.env.NODE_ENV !== "production") {
635
- console.warn(
636
- `[react-native-tailwind] Failed to process dynamic ${attributeName} at ${state.file.opts.filename ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`,
637
- );
638
- }
639
- }
640
- }
641
-
642
- // Unsupported dynamic className - warn in development
643
- if (process.env.NODE_ENV !== "production") {
644
- const filename = state.file.opts.filename ?? "unknown";
645
- console.warn(
646
- `[react-native-tailwind] Dynamic ${attributeName} values are not fully supported at ${filename}. ` +
647
- `Use the ${targetStyleProp} prop for dynamic values.`,
648
- );
649
- }
650
- },
651
- },
652
- };
653
- }
654
-
655
- /**
656
- * Add StyleSheet import to the file
657
- */
658
- function addStyleSheetImport(path: NodePath, t: typeof BabelTypes) {
659
- const importDeclaration = t.importDeclaration(
660
- [t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet"))],
661
- t.stringLiteral("react-native"),
662
- );
663
-
664
- // Add import at the top of the file
665
- (path as any).unshiftContainer("body", importDeclaration);
666
- }
667
-
668
- /**
669
- * Replace className with style attribute
670
- */
671
- function replaceWithStyleAttribute(
672
- classNamePath: NodePath,
673
- styleKey: string,
674
- targetStyleProp: string,
675
- t: typeof BabelTypes,
676
- ) {
677
- const styleAttribute = t.jsxAttribute(
678
- t.jsxIdentifier(targetStyleProp),
679
- t.jsxExpressionContainer(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey))),
680
- );
681
-
682
- classNamePath.replaceWith(styleAttribute);
683
- }
684
-
685
- /**
686
- * Merge className styles with existing style prop
687
- */
688
- function mergeStyleAttribute(
689
- classNamePath: NodePath,
690
- styleAttribute: any,
691
- styleKey: string,
692
- t: typeof BabelTypes,
693
- ) {
694
- const existingStyle = styleAttribute.value.expression;
695
-
696
- // Create array with className styles first, then existing styles
697
- // This allows existing styles to override className styles
698
- const styleArray = t.arrayExpression([
699
- t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)),
700
- existingStyle,
701
- ]);
702
-
703
- styleAttribute.value = t.jsxExpressionContainer(styleArray);
704
-
705
- // Remove the className attribute
706
- classNamePath.remove();
707
- }
708
-
709
- /**
710
- * Replace className with dynamic style attribute
711
- */
712
- function replaceDynamicWithStyleAttribute(
713
- classNamePath: NodePath,
714
- result: DynamicExpressionResult,
715
- targetStyleProp: string,
716
- t: typeof BabelTypes,
717
- ) {
718
- const styleAttribute = t.jsxAttribute(
719
- t.jsxIdentifier(targetStyleProp),
720
- t.jsxExpressionContainer(result.expression),
721
- );
722
-
723
- classNamePath.replaceWith(styleAttribute);
724
- }
725
-
726
- /**
727
- * Merge dynamic className styles with existing style prop
728
- */
729
- function mergeDynamicStyleAttribute(
730
- classNamePath: NodePath,
731
- styleAttribute: any,
732
- result: DynamicExpressionResult,
733
- t: typeof BabelTypes,
734
- ) {
735
- const existingStyle = styleAttribute.value.expression;
736
-
737
- // Merge dynamic expression with existing styles
738
- // If existing is already an array, append to it; otherwise create new array
739
- let styleArray;
740
- if (t.isArrayExpression(existingStyle)) {
741
- // Prepend dynamic styles to existing array
742
- styleArray = t.arrayExpression([result.expression, ...existingStyle.elements]);
743
- } else {
744
- // Create new array with dynamic styles first, then existing
745
- styleArray = t.arrayExpression([result.expression, existingStyle]);
746
- }
747
-
748
- styleAttribute.value = t.jsxExpressionContainer(styleArray);
749
-
750
- // Remove the className attribute
751
- classNamePath.remove();
752
- }
753
-
754
- /**
755
- * Replace className with style function attribute (for Pressable with modifiers)
756
- */
757
- function replaceWithStyleFunctionAttribute(
758
- classNamePath: NodePath,
759
- styleFunctionExpression: any,
760
- targetStyleProp: string,
761
- t: typeof BabelTypes,
762
- ) {
763
- const styleAttribute = t.jsxAttribute(
764
- t.jsxIdentifier(targetStyleProp),
765
- t.jsxExpressionContainer(styleFunctionExpression),
766
- );
767
-
768
- classNamePath.replaceWith(styleAttribute);
769
- }
770
-
771
- /**
772
- * Merge className style function with existing style prop (for Pressable with modifiers)
773
- */
774
- function mergeStyleFunctionAttribute(
775
- classNamePath: NodePath,
776
- styleAttribute: any,
777
- styleFunctionExpression: any,
778
- t: typeof BabelTypes,
779
- ) {
780
- const existingStyle = styleAttribute.value.expression;
781
-
782
- // Create a wrapper function that merges both styles
783
- // ({ pressed }) => [styleFunctionResult, existingStyle]
784
- // We need to call the style function and merge results
785
-
786
- // If existing is already a function, we need to handle it specially
787
- if (t.isArrowFunctionExpression(existingStyle) || t.isFunctionExpression(existingStyle)) {
788
- // Both are functions - create wrapper that calls both
789
- // (_state) => [newStyleFn(_state), existingStyleFn(_state)]
790
- // Create an identifier for the parameter to pass to the function calls
791
- const paramIdentifier = t.identifier("_state");
792
-
793
- const newFunctionCall = t.callExpression(styleFunctionExpression, [paramIdentifier]);
794
- const existingFunctionCall = t.callExpression(existingStyle, [paramIdentifier]);
795
-
796
- const mergedArray = t.arrayExpression([newFunctionCall, existingFunctionCall]);
797
- const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
798
-
799
- styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
800
- } else {
801
- // Existing is static - create function that returns array
802
- // (_state) => [styleFunctionResult, existingStyle]
803
- // Create an identifier for the parameter to pass to the function call
804
- const paramIdentifier = t.identifier("_state");
805
-
806
- const functionCall = t.callExpression(styleFunctionExpression, [paramIdentifier]);
807
- const mergedArray = t.arrayExpression([functionCall, existingStyle]);
808
- const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
809
-
810
- styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
811
- }
812
-
813
- // Remove the className attribute
814
- classNamePath.remove();
815
- }
816
-
817
- /**
818
- * Inject StyleSheet.create with all collected styles
819
- */
820
- function injectStyles(path: NodePath, styleRegistry: Map<string, StyleObject>, t: typeof BabelTypes) {
821
- // Build style object properties
822
- const styleProperties: any[] = [];
823
-
824
- for (const [key, styleObject] of styleRegistry) {
825
- const properties = Object.entries(styleObject).map(([styleProp, styleValue]) => {
826
- let valueNode;
827
-
828
- if (typeof styleValue === "number") {
829
- valueNode = t.numericLiteral(styleValue);
830
- } else if (typeof styleValue === "string") {
831
- valueNode = t.stringLiteral(styleValue);
832
- } else {
833
- // Fallback for other types
834
- valueNode = t.valueToNode(styleValue);
835
- }
836
-
837
- return t.objectProperty(t.identifier(styleProp), valueNode);
838
- });
839
-
840
- styleProperties.push(t.objectProperty(t.identifier(key), t.objectExpression(properties)));
841
- }
842
-
843
- // Create: const _tailwindStyles = StyleSheet.create({ ... })
844
- const styleSheet = t.variableDeclaration("const", [
845
- t.variableDeclarator(
846
- t.identifier(STYLES_IDENTIFIER),
847
- t.callExpression(t.memberExpression(t.identifier("StyleSheet"), t.identifier("create")), [
848
- t.objectExpression(styleProperties),
849
- ]),
850
- ),
851
- ]);
852
-
853
- // Add StyleSheet.create at the end of the file
854
- (path as any).pushContainer("body", styleSheet);
855
- }
856
-
857
- // Helper functions that use the imported parser
858
- function parseClassName(className: string, customColors: Record<string, string>): StyleObject {
859
- return parseClassNameFn(className, customColors);
860
- }
861
5
 
862
- function generateStyleKey(className: string): string {
863
- return generateStyleKeyFn(className);
864
- }
6
+ export { default } from "./plugin.js";
7
+ export type { PluginOptions } from "./plugin.js";