@mgcrea/react-native-tailwind 0.12.0 → 0.13.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 (97) hide show
  1. package/README.md +29 -2014
  2. package/dist/babel/config-loader.d.ts +3 -0
  3. package/dist/babel/config-loader.test.ts +2 -2
  4. package/dist/babel/config-loader.ts +37 -2
  5. package/dist/babel/index.cjs +2855 -2434
  6. package/dist/babel/plugin/componentScope.d.ts +26 -0
  7. package/dist/babel/plugin/componentScope.ts +87 -0
  8. package/dist/babel/plugin/state.d.ts +119 -0
  9. package/dist/babel/plugin/state.ts +177 -0
  10. package/dist/babel/plugin/visitors/className.d.ts +11 -0
  11. package/{src/babel/plugin.test.ts → dist/babel/plugin/visitors/className.test.ts} +74 -674
  12. package/dist/babel/plugin/visitors/className.ts +624 -0
  13. package/dist/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  14. package/dist/babel/plugin/visitors/imports.d.ts +11 -0
  15. package/dist/babel/plugin/visitors/imports.test.ts +88 -0
  16. package/dist/babel/plugin/visitors/imports.ts +101 -0
  17. package/dist/babel/plugin/visitors/program.d.ts +15 -0
  18. package/dist/babel/plugin/visitors/program.test.ts +325 -0
  19. package/dist/babel/plugin/visitors/program.ts +99 -0
  20. package/dist/babel/plugin/visitors/tw.d.ts +16 -0
  21. package/dist/babel/plugin/visitors/tw.test.ts +620 -0
  22. package/dist/babel/plugin/visitors/tw.ts +148 -0
  23. package/dist/babel/plugin.d.ts +3 -96
  24. package/dist/babel/plugin.test.ts +470 -0
  25. package/dist/babel/plugin.ts +28 -953
  26. package/dist/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  27. package/dist/babel/utils/componentSupport.test.ts +20 -7
  28. package/dist/babel/utils/componentSupport.ts +2 -0
  29. package/dist/babel/utils/modifierProcessing.ts +21 -0
  30. package/dist/babel/utils/platformModifierProcessing.ts +11 -0
  31. package/dist/babel/utils/styleInjection.d.ts +15 -0
  32. package/dist/babel/utils/styleInjection.ts +172 -17
  33. package/dist/babel/utils/twProcessing.ts +11 -0
  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 +1 -0
  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/sizing.js +1 -1
  54. package/dist/parser/typography.d.ts +2 -1
  55. package/dist/parser/typography.js +1 -1
  56. package/dist/parser/typography.test.js +1 -1
  57. package/dist/runtime.cjs +1 -1
  58. package/dist/runtime.cjs.map +4 -4
  59. package/dist/runtime.js +1 -1
  60. package/dist/runtime.js.map +4 -4
  61. package/package.json +1 -1
  62. package/src/babel/config-loader.test.ts +2 -2
  63. package/src/babel/config-loader.ts +37 -2
  64. package/src/babel/plugin/componentScope.ts +87 -0
  65. package/src/babel/plugin/state.ts +177 -0
  66. package/src/babel/plugin/visitors/className.test.ts +1312 -0
  67. package/src/babel/plugin/visitors/className.ts +624 -0
  68. package/src/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  69. package/src/babel/plugin/visitors/imports.test.ts +88 -0
  70. package/src/babel/plugin/visitors/imports.ts +101 -0
  71. package/src/babel/plugin/visitors/program.test.ts +325 -0
  72. package/src/babel/plugin/visitors/program.ts +99 -0
  73. package/src/babel/plugin/visitors/tw.test.ts +620 -0
  74. package/src/babel/plugin/visitors/tw.ts +148 -0
  75. package/src/babel/plugin.ts +28 -953
  76. package/src/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  77. package/src/babel/utils/componentSupport.test.ts +20 -7
  78. package/src/babel/utils/componentSupport.ts +2 -0
  79. package/src/babel/utils/modifierProcessing.ts +21 -0
  80. package/src/babel/utils/platformModifierProcessing.ts +11 -0
  81. package/src/babel/utils/styleInjection.ts +172 -17
  82. package/src/babel/utils/twProcessing.ts +11 -0
  83. package/src/babel/utils/windowDimensionsProcessing.ts +121 -0
  84. package/src/components/TouchableOpacity.tsx +71 -0
  85. package/src/components/index.ts +3 -0
  86. package/src/config/markers.ts +5 -0
  87. package/src/index.ts +4 -5
  88. package/src/parser/borders.test.ts +58 -0
  89. package/src/parser/borders.ts +18 -3
  90. package/src/parser/colors.test.ts +249 -0
  91. package/src/parser/colors.ts +38 -0
  92. package/src/parser/index.ts +4 -3
  93. package/src/parser/layout.test.ts +61 -0
  94. package/src/parser/layout.ts +55 -1
  95. package/src/parser/sizing.ts +11 -0
  96. package/src/parser/typography.test.ts +102 -0
  97. package/src/parser/typography.ts +61 -15
@@ -0,0 +1,624 @@
1
+ /**
2
+ * JSXAttribute visitor - handles className attribute transformations
3
+ */
4
+
5
+ import type { NodePath } from "@babel/core";
6
+ import type * as BabelTypes from "@babel/types";
7
+ import type { ParsedModifier, StateModifierType } from "../../../parser/index.js";
8
+ import {
9
+ expandSchemeModifier,
10
+ isColorSchemeModifier,
11
+ isPlatformModifier,
12
+ isSchemeModifier,
13
+ isStateModifier,
14
+ parseClassName,
15
+ parsePlaceholderClasses,
16
+ splitModifierClasses,
17
+ } from "../../../parser/index.js";
18
+ import { generateStyleKey } from "../../../utils/styleKey.js";
19
+ import { getTargetStyleProp, isAttributeSupported } from "../../utils/attributeMatchers.js";
20
+ import { processColorSchemeModifiers } from "../../utils/colorSchemeModifierProcessing.js";
21
+ import { getComponentModifierSupport, getStatePropertyForModifier } from "../../utils/componentSupport.js";
22
+ import { processDynamicExpression } from "../../utils/dynamicProcessing.js";
23
+ import { createStyleFunction, processStaticClassNameWithModifiers } from "../../utils/modifierProcessing.js";
24
+ import { processPlatformModifiers } from "../../utils/platformModifierProcessing.js";
25
+ import {
26
+ addOrMergePlaceholderTextColorProp,
27
+ findStyleAttribute,
28
+ mergeDynamicStyleAttribute,
29
+ mergeStyleAttribute,
30
+ mergeStyleFunctionAttribute,
31
+ replaceDynamicWithStyleAttribute,
32
+ replaceWithStyleAttribute,
33
+ replaceWithStyleFunctionAttribute,
34
+ } from "../../utils/styleTransforms.js";
35
+ import {
36
+ createRuntimeDimensionObject,
37
+ hasRuntimeDimensions,
38
+ splitStaticAndRuntimeStyles,
39
+ } from "../../utils/windowDimensionsProcessing.js";
40
+ import { findComponentScope } from "../componentScope.js";
41
+ import type { PluginState } from "../state.js";
42
+
43
+ /**
44
+ * JSXAttribute visitor
45
+ * Handles all className attribute transformations (static, dynamic, modifiers)
46
+ */
47
+ export function jsxAttributeVisitor(
48
+ path: NodePath<BabelTypes.JSXAttribute>,
49
+ state: PluginState,
50
+ t: typeof BabelTypes,
51
+ ): void {
52
+ const node = path.node;
53
+
54
+ // Ensure we have a JSXIdentifier name (not JSXNamespacedName)
55
+ if (!t.isJSXIdentifier(node.name)) {
56
+ return;
57
+ }
58
+
59
+ const attributeName = node.name.name;
60
+
61
+ // Only process configured className-like attributes
62
+ if (!isAttributeSupported(attributeName, state.supportedAttributes, state.attributePatterns)) {
63
+ return;
64
+ }
65
+
66
+ const value = node.value;
67
+
68
+ // Determine target style prop based on attribute name
69
+ const targetStyleProp = getTargetStyleProp(attributeName);
70
+
71
+ /**
72
+ * Process static className string (handles both direct StringLiteral and StringLiteral in JSXExpressionContainer)
73
+ */
74
+ const processStaticClassName = (className: string): boolean => {
75
+ const trimmedClassName = className.trim();
76
+
77
+ // Skip empty classNames
78
+ if (!trimmedClassName) {
79
+ path.remove();
80
+ return true;
81
+ }
82
+
83
+ state.hasClassNames = true;
84
+
85
+ // Check if className contains modifiers (active:, hover:, focus:, placeholder:, ios:, android:, web:, dark:, light:, scheme:)
86
+ const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(trimmedClassName);
87
+
88
+ // Expand scheme: modifiers into dark: and light: modifiers
89
+ const modifierClasses: ParsedModifier[] = [];
90
+ for (const modifier of rawModifierClasses) {
91
+ if (isSchemeModifier(modifier.modifier)) {
92
+ // Expand scheme: into dark: and light:
93
+ const expanded = expandSchemeModifier(
94
+ modifier,
95
+ state.customTheme.colors ?? {},
96
+ state.schemeModifierConfig.darkSuffix,
97
+ state.schemeModifierConfig.lightSuffix,
98
+ );
99
+ modifierClasses.push(...expanded);
100
+ } else {
101
+ // Keep other modifiers as-is
102
+ modifierClasses.push(modifier);
103
+ }
104
+ }
105
+
106
+ // Separate modifiers by type
107
+ const placeholderModifiers = modifierClasses.filter((m) => m.modifier === "placeholder");
108
+ const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
109
+ const colorSchemeModifiers = modifierClasses.filter((m) => isColorSchemeModifier(m.modifier));
110
+ const stateModifiers = modifierClasses.filter(
111
+ (m) => isStateModifier(m.modifier) && m.modifier !== "placeholder",
112
+ );
113
+
114
+ // Handle placeholder modifiers first (they generate placeholderTextColor prop, not style)
115
+ if (placeholderModifiers.length > 0) {
116
+ // Check if this is a TextInput component (placeholder only works on TextInput)
117
+ const jsxOpeningElement = path.parent as BabelTypes.JSXOpeningElement;
118
+ const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
119
+
120
+ if (componentSupport?.supportedModifiers.includes("placeholder")) {
121
+ const placeholderClasses = placeholderModifiers.map((m) => m.baseClass).join(" ");
122
+ const placeholderColor = parsePlaceholderClasses(placeholderClasses, state.customTheme.colors);
123
+
124
+ if (placeholderColor) {
125
+ // Add or merge placeholderTextColor prop
126
+ addOrMergePlaceholderTextColorProp(jsxOpeningElement, placeholderColor, t);
127
+ }
128
+ } else {
129
+ // Warn if placeholder modifier used on non-TextInput element
130
+ if (process.env.NODE_ENV !== "production") {
131
+ console.warn(
132
+ `[react-native-tailwind] placeholder: modifier can only be used on TextInput component at ${state.file.opts.filename ?? "unknown"}`,
133
+ );
134
+ }
135
+ }
136
+ }
137
+
138
+ // Handle combination of modifiers
139
+ const hasPlatformModifiers = platformModifiers.length > 0;
140
+ const hasColorSchemeModifiers = colorSchemeModifiers.length > 0;
141
+ const hasStateModifiers = stateModifiers.length > 0;
142
+ const hasBaseClasses = baseClasses.length > 0;
143
+
144
+ // If we have color scheme modifiers, we need to track the parent function component
145
+ let componentScope: NodePath<BabelTypes.Function> | null = null;
146
+ if (hasColorSchemeModifiers) {
147
+ componentScope = findComponentScope(path, t);
148
+ if (componentScope) {
149
+ state.functionComponentsNeedingColorScheme.add(componentScope);
150
+ } else {
151
+ // Warn if color scheme modifiers used in invalid context (class component, nested function)
152
+ if (process.env.NODE_ENV !== "production") {
153
+ console.warn(
154
+ `[react-native-tailwind] dark:/light: modifiers require a function component scope. ` +
155
+ `Found in non-component context at ${state.file.opts.filename ?? "unknown"}. ` +
156
+ `These modifiers are not supported in class components or nested callbacks.`,
157
+ );
158
+ }
159
+ }
160
+ }
161
+
162
+ // If we have multiple modifier types, combine them in an array expression
163
+ // For state modifiers, wrap in arrow function; for color scheme, they're just conditionals
164
+ if (hasStateModifiers && (hasPlatformModifiers || hasColorSchemeModifiers)) {
165
+ // Get the JSX opening element for component support checking
166
+ const jsxOpeningElement = path.parent;
167
+ const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
168
+
169
+ if (componentSupport) {
170
+ // Build style array: [baseStyle, Platform.select(...), colorSchemeConditionals, stateConditionals]
171
+ const styleArrayElements: BabelTypes.Expression[] = [];
172
+
173
+ // Add base classes
174
+ if (hasBaseClasses) {
175
+ const baseClassName = baseClasses.join(" ");
176
+ const baseStyleObject = parseClassName(baseClassName, state.customTheme);
177
+
178
+ // Check for runtime dimensions (w-screen, h-screen) in base classes
179
+ if (hasRuntimeDimensions(baseStyleObject)) {
180
+ throw path.buildCodeFrameError(
181
+ `w-screen and h-screen cannot be combined with modifiers. ` +
182
+ `Found: "${baseClassName}" with state, platform, or color scheme modifiers. ` +
183
+ `Use w-screen/h-screen without modifiers instead.`,
184
+ );
185
+ }
186
+
187
+ const baseStyleKey = generateStyleKey(baseClassName);
188
+ state.styleRegistry.set(baseStyleKey, baseStyleObject);
189
+ styleArrayElements.push(
190
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
191
+ );
192
+ }
193
+
194
+ // Add platform modifiers as Platform.select()
195
+ if (hasPlatformModifiers) {
196
+ const platformSelectExpression = processPlatformModifiers(
197
+ platformModifiers,
198
+ state,
199
+ parseClassName,
200
+ generateStyleKey,
201
+ t,
202
+ );
203
+ styleArrayElements.push(platformSelectExpression);
204
+ }
205
+
206
+ // Add color scheme modifiers as conditionals (only if component scope exists)
207
+ if (hasColorSchemeModifiers && componentScope) {
208
+ const colorSchemeConditionals = processColorSchemeModifiers(
209
+ colorSchemeModifiers,
210
+ state,
211
+ parseClassName,
212
+ generateStyleKey,
213
+ t,
214
+ );
215
+ styleArrayElements.push(...colorSchemeConditionals);
216
+ }
217
+
218
+ // Add state modifiers as conditionals
219
+ // Group by modifier type
220
+ const modifiersByType = new Map<StateModifierType, ParsedModifier[]>();
221
+ for (const mod of stateModifiers) {
222
+ const modType = mod.modifier as StateModifierType;
223
+ if (!modifiersByType.has(modType)) {
224
+ modifiersByType.set(modType, []);
225
+ }
226
+ modifiersByType.get(modType)?.push(mod);
227
+ }
228
+
229
+ // Build conditionals for each state modifier type
230
+ for (const [modifierType, modifiers] of modifiersByType) {
231
+ if (!componentSupport.supportedModifiers.includes(modifierType)) {
232
+ continue; // Skip unsupported modifiers
233
+ }
234
+
235
+ const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
236
+ const modifierStyleObject = parseClassName(modifierClassNames, state.customTheme);
237
+ const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
238
+ state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
239
+
240
+ const stateProperty = getStatePropertyForModifier(modifierType);
241
+ const conditionalExpression = t.logicalExpression(
242
+ "&&",
243
+ t.identifier(stateProperty),
244
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey)),
245
+ );
246
+
247
+ styleArrayElements.push(conditionalExpression);
248
+ }
249
+
250
+ // Wrap in arrow function for state support
251
+ const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier))).filter((mod) =>
252
+ componentSupport.supportedModifiers.includes(mod),
253
+ );
254
+ const styleArrayExpression = t.arrayExpression(styleArrayElements);
255
+ const styleFunctionExpression = createStyleFunction(styleArrayExpression, usedModifiers, t);
256
+
257
+ const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
258
+ if (styleAttribute) {
259
+ mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
260
+ } else {
261
+ replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
262
+ }
263
+ return true;
264
+ } else {
265
+ // Component doesn't support state modifiers, but we can still use platform modifiers
266
+ // Fall through to platform-only handling
267
+ }
268
+ }
269
+
270
+ // Handle platform and/or color scheme modifiers (no state modifiers)
271
+ if ((hasPlatformModifiers || hasColorSchemeModifiers) && !hasStateModifiers) {
272
+ // Build style array/expression: [baseStyle, Platform.select(...), colorSchemeConditionals]
273
+ const styleExpressions: BabelTypes.Expression[] = [];
274
+
275
+ // Add base classes
276
+ if (hasBaseClasses) {
277
+ const baseClassName = baseClasses.join(" ");
278
+ const baseStyleObject = parseClassName(baseClassName, state.customTheme);
279
+
280
+ // Check for runtime dimensions (w-screen, h-screen) in base classes
281
+ if (hasRuntimeDimensions(baseStyleObject)) {
282
+ throw path.buildCodeFrameError(
283
+ `w-screen and h-screen cannot be combined with modifiers. ` +
284
+ `Found: "${baseClassName}" with platform or color scheme modifiers. ` +
285
+ `Use w-screen/h-screen without modifiers instead.`,
286
+ );
287
+ }
288
+
289
+ const baseStyleKey = generateStyleKey(baseClassName);
290
+ state.styleRegistry.set(baseStyleKey, baseStyleObject);
291
+ styleExpressions.push(
292
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
293
+ );
294
+ }
295
+
296
+ // Add platform modifiers as Platform.select()
297
+ if (hasPlatformModifiers) {
298
+ const platformSelectExpression = processPlatformModifiers(
299
+ platformModifiers,
300
+ state,
301
+ parseClassName,
302
+ generateStyleKey,
303
+ t,
304
+ );
305
+ styleExpressions.push(platformSelectExpression);
306
+ }
307
+
308
+ // Add color scheme modifiers as conditionals (only if we have a valid component scope)
309
+ if (hasColorSchemeModifiers && componentScope) {
310
+ const colorSchemeConditionals = processColorSchemeModifiers(
311
+ colorSchemeModifiers,
312
+ state,
313
+ parseClassName,
314
+ generateStyleKey,
315
+ t,
316
+ );
317
+ styleExpressions.push(...colorSchemeConditionals);
318
+ }
319
+
320
+ // Generate style attribute
321
+ const styleExpression =
322
+ styleExpressions.length === 1 ? styleExpressions[0] : t.arrayExpression(styleExpressions);
323
+
324
+ const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
325
+ if (styleAttribute) {
326
+ // Merge with existing style attribute
327
+ const existingStyle = styleAttribute.value;
328
+ if (t.isJSXExpressionContainer(existingStyle) && !t.isJSXEmptyExpression(existingStyle.expression)) {
329
+ const existing = existingStyle.expression;
330
+ // Merge as array: [ourStyles, existingStyles]
331
+ const mergedArray = t.isArrayExpression(existing)
332
+ ? t.arrayExpression([styleExpression, ...existing.elements])
333
+ : t.arrayExpression([styleExpression, existing]);
334
+ styleAttribute.value = t.jsxExpressionContainer(mergedArray);
335
+ } else {
336
+ styleAttribute.value = t.jsxExpressionContainer(styleExpression);
337
+ }
338
+ path.remove();
339
+ } else {
340
+ // Replace className with style prop containing our expression
341
+ path.node.name = t.jsxIdentifier(targetStyleProp);
342
+ path.node.value = t.jsxExpressionContainer(styleExpression);
343
+ }
344
+ return true;
345
+ }
346
+
347
+ // If there are state modifiers (and no platform modifiers), check if this component supports them
348
+ if (hasStateModifiers) {
349
+ // Get the JSX opening element (the direct parent of the attribute)
350
+ const jsxOpeningElement = path.parent;
351
+ const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
352
+
353
+ if (componentSupport) {
354
+ // Get modifier types used in className
355
+ const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier)));
356
+
357
+ // Check if all modifiers are supported by this component
358
+ const unsupportedModifiers = usedModifiers.filter(
359
+ (mod) => !componentSupport.supportedModifiers.includes(mod),
360
+ );
361
+
362
+ if (unsupportedModifiers.length > 0) {
363
+ // Warn about unsupported modifiers
364
+ if (process.env.NODE_ENV !== "production") {
365
+ console.warn(
366
+ `[react-native-tailwind] Modifiers (${unsupportedModifiers.map((m) => `${m}:`).join(", ")}) are not supported on ${componentSupport.component} component at ${state.file.opts.filename ?? "unknown"}. ` +
367
+ `Supported modifiers: ${componentSupport.supportedModifiers.join(", ")}`,
368
+ );
369
+ }
370
+ // Filter out unsupported modifiers
371
+ const supportedModifierClasses = stateModifiers.filter((m) =>
372
+ componentSupport.supportedModifiers.includes(m.modifier),
373
+ );
374
+
375
+ // If no supported modifiers remain, fall through to normal processing
376
+ if (supportedModifierClasses.length === 0) {
377
+ // Continue to normal processing
378
+ } else {
379
+ // Process only supported modifiers
380
+ const filteredClassName =
381
+ baseClasses.join(" ") +
382
+ " " +
383
+ supportedModifierClasses.map((m) => `${m.modifier}:${m.baseClass}`).join(" ");
384
+ const styleExpression = processStaticClassNameWithModifiers(
385
+ filteredClassName.trim(),
386
+ state,
387
+ parseClassName,
388
+ generateStyleKey,
389
+ splitModifierClasses,
390
+ t,
391
+ );
392
+ const modifierTypes = Array.from(new Set(supportedModifierClasses.map((m) => m.modifier)));
393
+ const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
394
+
395
+ const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
396
+
397
+ if (styleAttribute) {
398
+ mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
399
+ } else {
400
+ replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
401
+ }
402
+ return true;
403
+ }
404
+ } else {
405
+ // All modifiers are supported - process normally
406
+ const styleExpression = processStaticClassNameWithModifiers(
407
+ trimmedClassName,
408
+ state,
409
+ parseClassName,
410
+ generateStyleKey,
411
+ splitModifierClasses,
412
+ t,
413
+ );
414
+ const modifierTypes = usedModifiers;
415
+ const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
416
+
417
+ const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
418
+
419
+ if (styleAttribute) {
420
+ mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
421
+ } else {
422
+ replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
423
+ }
424
+ return true;
425
+ }
426
+ } else {
427
+ // Component doesn't support any modifiers
428
+ if (process.env.NODE_ENV !== "production") {
429
+ const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier)));
430
+ console.warn(
431
+ `[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"}`,
432
+ );
433
+ }
434
+ // Fall through to normal processing (ignore modifiers)
435
+ }
436
+ }
437
+
438
+ // Normal processing without modifiers
439
+ // Use baseClasses only (placeholder modifiers already handled separately)
440
+ const classNameForStyle = baseClasses.join(" ");
441
+ if (!classNameForStyle) {
442
+ // No base classes, only had placeholder modifiers - just remove className
443
+ path.remove();
444
+ return true;
445
+ }
446
+
447
+ const styleObject = parseClassName(classNameForStyle, state.customTheme);
448
+
449
+ // Check if the style object contains runtime dimension markers (w-screen, h-screen)
450
+ if (hasRuntimeDimensions(styleObject)) {
451
+ // Split into static and runtime parts
452
+ const { static: staticStyles, runtime: runtimeStyles } = splitStaticAndRuntimeStyles(styleObject);
453
+
454
+ // Track component scope for hook injection
455
+ const componentScope = findComponentScope(path, t);
456
+ if (componentScope) {
457
+ state.hasClassNames = true; // Mark that we have classNames to process
458
+ state.functionComponentsNeedingWindowDimensions.add(componentScope);
459
+ state.needsWindowDimensionsImport = true;
460
+
461
+ // Build style array: [staticStyles, { width: _twDimensions.width }]
462
+ const styleExpressions: BabelTypes.Expression[] = [];
463
+
464
+ // Add static styles if any
465
+ if (Object.keys(staticStyles).length > 0) {
466
+ const styleKey = generateStyleKey(classNameForStyle);
467
+ state.styleRegistry.set(styleKey, staticStyles);
468
+ styleExpressions.push(
469
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey)),
470
+ );
471
+ }
472
+
473
+ // Add runtime dimension object
474
+ const runtimeDimensionObject = createRuntimeDimensionObject(runtimeStyles, state, t);
475
+ styleExpressions.push(runtimeDimensionObject);
476
+
477
+ // Create style array or single expression
478
+ const styleExpression =
479
+ styleExpressions.length === 1 ? styleExpressions[0] : t.arrayExpression(styleExpressions);
480
+
481
+ // Check if there's already a style prop on this element
482
+ const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
483
+
484
+ if (styleAttribute) {
485
+ // Merge with existing style attribute
486
+ const existingStyle = styleAttribute.value;
487
+ if (t.isJSXExpressionContainer(existingStyle) && !t.isJSXEmptyExpression(existingStyle.expression)) {
488
+ const existing = existingStyle.expression;
489
+
490
+ // Check if existing style is a function (e.g., Pressable's style prop)
491
+ if (t.isArrowFunctionExpression(existing) || t.isFunctionExpression(existing)) {
492
+ // Existing style is a function - create wrapper that calls it and merges results
493
+ // (_state) => [styleExpression, existingStyleFn(_state)]
494
+ const paramIdentifier = t.identifier("_state");
495
+ const functionCall = t.callExpression(existing, [paramIdentifier]);
496
+
497
+ const mergedArray = t.arrayExpression([styleExpression, functionCall]);
498
+
499
+ const wrappedFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
500
+ styleAttribute.value = t.jsxExpressionContainer(wrappedFunction);
501
+ } else {
502
+ // Merge as array: [ourStyles, existingStyles]
503
+ const mergedArray = t.isArrayExpression(existing)
504
+ ? t.arrayExpression([styleExpression, ...existing.elements])
505
+ : t.arrayExpression([styleExpression, existing]);
506
+ styleAttribute.value = t.jsxExpressionContainer(mergedArray);
507
+ }
508
+ } else {
509
+ styleAttribute.value = t.jsxExpressionContainer(styleExpression);
510
+ }
511
+ path.remove();
512
+ } else {
513
+ // Replace className with style prop containing runtime expression
514
+ path.node.name = t.jsxIdentifier(targetStyleProp);
515
+ path.node.value = t.jsxExpressionContainer(styleExpression);
516
+ }
517
+ return true;
518
+ } else {
519
+ // Warn if w-screen/h-screen used in invalid context
520
+ if (process.env.NODE_ENV !== "production") {
521
+ console.warn(
522
+ `[react-native-tailwind] w-screen/h-screen classes require a function component scope. ` +
523
+ `Found in non-component context at ${state.file.opts.filename ?? "unknown"}. ` +
524
+ `These classes are not supported in class components or nested callbacks.`,
525
+ );
526
+ }
527
+ // Fall through to normal processing (will generate static styles, which won't work correctly)
528
+ }
529
+ }
530
+
531
+ const styleKey = generateStyleKey(classNameForStyle);
532
+ state.styleRegistry.set(styleKey, styleObject);
533
+
534
+ // Check if there's already a style prop on this element
535
+ const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
536
+
537
+ if (styleAttribute) {
538
+ // Merge with existing style prop
539
+ mergeStyleAttribute(path, styleAttribute, styleKey, state.stylesIdentifier, t);
540
+ } else {
541
+ // Replace className with style prop
542
+ replaceWithStyleAttribute(path, styleKey, targetStyleProp, state.stylesIdentifier, t);
543
+ }
544
+ return true;
545
+ };
546
+
547
+ // Handle static string literals
548
+ if (t.isStringLiteral(value)) {
549
+ if (processStaticClassName(value.value)) {
550
+ return;
551
+ }
552
+ }
553
+
554
+ // Handle dynamic expressions (JSXExpressionContainer)
555
+ if (t.isJSXExpressionContainer(value)) {
556
+ const expression = value.expression;
557
+
558
+ // Skip JSXEmptyExpression
559
+ if (t.isJSXEmptyExpression(expression)) {
560
+ return;
561
+ }
562
+
563
+ // Fast path: Support string literals wrapped in JSXExpressionContainer: className={"flex-row"}
564
+ if (t.isStringLiteral(expression)) {
565
+ if (processStaticClassName(expression.value)) {
566
+ return;
567
+ }
568
+ }
569
+
570
+ try {
571
+ // Find component scope for color scheme modifiers
572
+ const componentScope = findComponentScope(path, t);
573
+
574
+ // Process dynamic expression with modifier support
575
+ const result = processDynamicExpression(
576
+ expression,
577
+ state,
578
+ parseClassName,
579
+ generateStyleKey,
580
+ splitModifierClasses,
581
+ processPlatformModifiers,
582
+ processColorSchemeModifiers,
583
+ componentScope,
584
+ isPlatformModifier as (modifier: unknown) => boolean,
585
+ isColorSchemeModifier as (modifier: unknown) => boolean,
586
+ isSchemeModifier as (modifier: unknown) => boolean,
587
+ expandSchemeModifier,
588
+ t,
589
+ );
590
+
591
+ if (result) {
592
+ state.hasClassNames = true;
593
+
594
+ // Check if there's already a style prop on this element
595
+ const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
596
+
597
+ if (styleAttribute) {
598
+ // Merge with existing style prop
599
+ mergeDynamicStyleAttribute(path, styleAttribute, result, t);
600
+ } else {
601
+ // Replace className with style prop
602
+ replaceDynamicWithStyleAttribute(path, result, targetStyleProp, t);
603
+ }
604
+ return;
605
+ }
606
+ } catch (error) {
607
+ // Fall through to warning
608
+ if (process.env.NODE_ENV !== "production") {
609
+ console.warn(
610
+ `[react-native-tailwind] Failed to process dynamic ${attributeName} at ${state.file.opts.filename ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`,
611
+ );
612
+ }
613
+ }
614
+ }
615
+
616
+ // Unsupported dynamic className - warn in development
617
+ if (process.env.NODE_ENV !== "production") {
618
+ const filename = state.file.opts.filename ?? "unknown";
619
+ console.warn(
620
+ `[react-native-tailwind] Dynamic ${attributeName} values are not fully supported at ${filename}. ` +
621
+ `Use the ${targetStyleProp} prop for dynamic values.`,
622
+ );
623
+ }
624
+ }