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