@mgcrea/react-native-tailwind 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/babel/index.cjs +333 -195
- package/dist/babel/index.d.ts +4 -40
- package/dist/babel/index.test.ts +214 -1
- package/dist/babel/index.ts +4 -1169
- package/dist/babel/plugin.d.ts +42 -0
- package/{src/babel/index.test.ts → dist/babel/plugin.test.ts} +216 -2
- package/dist/babel/plugin.ts +491 -0
- package/dist/babel/utils/attributeMatchers.d.ts +23 -0
- package/dist/babel/utils/attributeMatchers.ts +71 -0
- package/dist/babel/utils/componentSupport.d.ts +18 -0
- package/dist/babel/utils/componentSupport.ts +68 -0
- package/dist/babel/utils/dynamicProcessing.d.ts +32 -0
- package/dist/babel/utils/dynamicProcessing.ts +223 -0
- package/dist/babel/utils/modifierProcessing.d.ts +26 -0
- package/dist/babel/utils/modifierProcessing.ts +118 -0
- package/dist/babel/utils/styleInjection.d.ts +15 -0
- package/dist/babel/utils/styleInjection.ts +80 -0
- package/dist/babel/utils/styleTransforms.d.ts +39 -0
- package/dist/babel/utils/styleTransforms.test.ts +349 -0
- package/dist/babel/utils/styleTransforms.ts +258 -0
- package/dist/babel/utils/twProcessing.d.ts +28 -0
- package/dist/babel/utils/twProcessing.ts +124 -0
- package/dist/components/TextInput.d.ts +171 -14
- package/dist/config/tailwind.d.ts +302 -0
- package/dist/config/tailwind.js +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -1
- package/dist/parser/colors.js +1 -1
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.js +1 -1
- package/dist/parser/modifiers.d.ts +2 -2
- package/dist/parser/modifiers.js +1 -1
- package/dist/parser/placeholder.d.ts +36 -0
- package/dist/parser/placeholder.js +1 -0
- package/dist/parser/placeholder.test.js +1 -0
- package/dist/parser/typography.d.ts +1 -0
- package/dist/parser/typography.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.d.ts +1 -14
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +4 -4
- package/dist/stubs/tw.d.ts +1 -14
- package/dist/types/core.d.ts +40 -0
- package/dist/types/core.js +0 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +1 -0
- package/dist/types/runtime.d.ts +15 -0
- package/dist/types/runtime.js +1 -0
- package/dist/types/util.d.ts +3 -0
- package/dist/types/util.js +0 -0
- package/package.json +1 -1
- package/src/babel/index.ts +4 -1169
- package/src/babel/plugin.test.ts +482 -0
- package/src/babel/plugin.ts +491 -0
- package/src/babel/utils/attributeMatchers.ts +71 -0
- package/src/babel/utils/componentSupport.ts +68 -0
- package/src/babel/utils/dynamicProcessing.ts +223 -0
- package/src/babel/utils/modifierProcessing.ts +118 -0
- package/src/babel/utils/styleInjection.ts +80 -0
- package/src/babel/utils/styleTransforms.test.ts +349 -0
- package/src/babel/utils/styleTransforms.ts +258 -0
- package/src/babel/utils/twProcessing.ts +124 -0
- package/src/components/TextInput.tsx +17 -14
- package/src/config/{palettes.ts → tailwind.ts} +2 -2
- package/src/index.ts +6 -3
- package/src/parser/colors.ts +2 -2
- package/src/parser/index.ts +1 -0
- package/src/parser/modifiers.ts +10 -4
- package/src/parser/placeholder.test.ts +105 -0
- package/src/parser/placeholder.ts +78 -0
- package/src/parser/typography.test.ts +11 -0
- package/src/parser/typography.ts +20 -2
- package/src/runtime.ts +1 -16
- package/src/stubs/tw.ts +1 -16
- package/src/{types.ts → types/core.ts} +0 -4
- package/src/types/index.ts +2 -0
- package/src/types/runtime.ts +17 -0
- package/src/types/util.ts +1 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Babel plugin for react-native-tailwind
|
|
3
|
+
* Transforms className props to style props at compile time
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { NodePath, PluginObj, PluginPass } from "@babel/core";
|
|
7
|
+
import * as BabelTypes from "@babel/types";
|
|
8
|
+
import { parseClassName, parsePlaceholderClasses, splitModifierClasses } from "../parser/index.js";
|
|
9
|
+
import type { StyleObject } from "../types/core.js";
|
|
10
|
+
import { generateStyleKey } from "../utils/styleKey.js";
|
|
11
|
+
import { extractCustomColors } from "./config-loader.js";
|
|
12
|
+
|
|
13
|
+
// Import utility functions
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_CLASS_ATTRIBUTES,
|
|
16
|
+
buildAttributeMatchers,
|
|
17
|
+
getTargetStyleProp,
|
|
18
|
+
isAttributeSupported,
|
|
19
|
+
} from "./utils/attributeMatchers.js";
|
|
20
|
+
import { getComponentModifierSupport } from "./utils/componentSupport.js";
|
|
21
|
+
import { processDynamicExpression } from "./utils/dynamicProcessing.js";
|
|
22
|
+
import { createStyleFunction, processStaticClassNameWithModifiers } from "./utils/modifierProcessing.js";
|
|
23
|
+
import { addStyleSheetImport, injectStylesAtTop } from "./utils/styleInjection.js";
|
|
24
|
+
import {
|
|
25
|
+
addOrMergePlaceholderTextColorProp,
|
|
26
|
+
findStyleAttribute,
|
|
27
|
+
mergeDynamicStyleAttribute,
|
|
28
|
+
mergeStyleAttribute,
|
|
29
|
+
mergeStyleFunctionAttribute,
|
|
30
|
+
replaceDynamicWithStyleAttribute,
|
|
31
|
+
replaceWithStyleAttribute,
|
|
32
|
+
replaceWithStyleFunctionAttribute,
|
|
33
|
+
} from "./utils/styleTransforms.js";
|
|
34
|
+
import { processTwCall, removeTwImports } from "./utils/twProcessing.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Plugin options
|
|
38
|
+
*/
|
|
39
|
+
export type PluginOptions = {
|
|
40
|
+
/**
|
|
41
|
+
* List of JSX attribute names to transform (in addition to or instead of 'className')
|
|
42
|
+
* Supports exact matches and glob patterns:
|
|
43
|
+
* - Exact: 'className', 'containerClassName'
|
|
44
|
+
* - Glob: '*ClassName' (matches any attribute ending in 'ClassName')
|
|
45
|
+
*
|
|
46
|
+
* @default ['className', 'contentContainerClassName', 'columnWrapperClassName', 'ListHeaderComponentClassName', 'ListFooterComponentClassName']
|
|
47
|
+
*/
|
|
48
|
+
attributes?: string[];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Custom identifier name for the generated StyleSheet constant
|
|
52
|
+
*
|
|
53
|
+
* @default '_twStyles'
|
|
54
|
+
*/
|
|
55
|
+
stylesIdentifier?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type PluginState = PluginPass & {
|
|
59
|
+
styleRegistry: Map<string, StyleObject>;
|
|
60
|
+
hasClassNames: boolean;
|
|
61
|
+
hasStyleSheetImport: boolean;
|
|
62
|
+
customColors: Record<string, string>;
|
|
63
|
+
supportedAttributes: Set<string>;
|
|
64
|
+
attributePatterns: RegExp[];
|
|
65
|
+
stylesIdentifier: string;
|
|
66
|
+
// Track tw/twStyle imports from main package
|
|
67
|
+
twImportNames: Set<string>; // e.g., ['tw', 'twStyle'] or ['tw as customTw']
|
|
68
|
+
hasTwImport: boolean;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Default identifier for the generated StyleSheet constant
|
|
72
|
+
const DEFAULT_STYLES_IDENTIFIER = "_twStyles";
|
|
73
|
+
|
|
74
|
+
export default function reactNativeTailwindBabelPlugin(
|
|
75
|
+
{ types: t }: { types: typeof BabelTypes },
|
|
76
|
+
options?: PluginOptions,
|
|
77
|
+
): PluginObj<PluginState> {
|
|
78
|
+
// Build attribute matchers from options
|
|
79
|
+
const attributes = options?.attributes ?? [...DEFAULT_CLASS_ATTRIBUTES];
|
|
80
|
+
const { exactMatches, patterns } = buildAttributeMatchers(attributes);
|
|
81
|
+
const stylesIdentifier = options?.stylesIdentifier ?? DEFAULT_STYLES_IDENTIFIER;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
name: "react-native-tailwind",
|
|
85
|
+
|
|
86
|
+
visitor: {
|
|
87
|
+
Program: {
|
|
88
|
+
enter(_path: NodePath, state: PluginState) {
|
|
89
|
+
// Initialize state for this file
|
|
90
|
+
state.styleRegistry = new Map();
|
|
91
|
+
state.hasClassNames = false;
|
|
92
|
+
state.hasStyleSheetImport = false;
|
|
93
|
+
state.supportedAttributes = exactMatches;
|
|
94
|
+
state.attributePatterns = patterns;
|
|
95
|
+
state.stylesIdentifier = stylesIdentifier;
|
|
96
|
+
state.twImportNames = new Set();
|
|
97
|
+
state.hasTwImport = false;
|
|
98
|
+
|
|
99
|
+
// Load custom colors from tailwind.config.*
|
|
100
|
+
state.customColors = extractCustomColors(state.file.opts.filename ?? "");
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
exit(path, state) {
|
|
104
|
+
// Remove tw/twStyle imports if they were used (and transformed)
|
|
105
|
+
if (state.hasTwImport) {
|
|
106
|
+
removeTwImports(path, t);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// If no classNames were found, skip StyleSheet generation
|
|
110
|
+
if (!state.hasClassNames || state.styleRegistry.size === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Add StyleSheet import if not already present
|
|
115
|
+
if (!state.hasStyleSheetImport) {
|
|
116
|
+
addStyleSheetImport(path, t);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Generate and inject StyleSheet.create at the beginning of the file (after imports)
|
|
120
|
+
// This ensures _twStyles is defined before any code that references it
|
|
121
|
+
injectStylesAtTop(path, state.styleRegistry, state.stylesIdentifier, t);
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// Check if StyleSheet is already imported and track tw/twStyle imports
|
|
126
|
+
ImportDeclaration(path, state) {
|
|
127
|
+
const node = path.node;
|
|
128
|
+
|
|
129
|
+
// Track react-native StyleSheet import
|
|
130
|
+
if (node.source.value === "react-native") {
|
|
131
|
+
const specifiers = node.specifiers;
|
|
132
|
+
const hasStyleSheet = specifiers.some((spec) => {
|
|
133
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
134
|
+
return spec.imported.name === "StyleSheet";
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (hasStyleSheet) {
|
|
140
|
+
state.hasStyleSheetImport = true;
|
|
141
|
+
} else {
|
|
142
|
+
// Add StyleSheet to existing import
|
|
143
|
+
node.specifiers.push(t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet")));
|
|
144
|
+
state.hasStyleSheetImport = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Track tw/twStyle imports from main package (for compile-time transformation)
|
|
149
|
+
if (node.source.value === "@mgcrea/react-native-tailwind") {
|
|
150
|
+
const specifiers = node.specifiers;
|
|
151
|
+
specifiers.forEach((spec) => {
|
|
152
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
153
|
+
const importedName = spec.imported.name;
|
|
154
|
+
if (importedName === "tw" || importedName === "twStyle") {
|
|
155
|
+
// Track the local name (could be renamed: import { tw as customTw })
|
|
156
|
+
const localName = spec.local.name;
|
|
157
|
+
state.twImportNames.add(localName);
|
|
158
|
+
state.hasTwImport = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// Handle tw`...` tagged template expressions
|
|
166
|
+
TaggedTemplateExpression(path, state) {
|
|
167
|
+
const node = path.node;
|
|
168
|
+
|
|
169
|
+
// Check if the tag is a tracked tw import
|
|
170
|
+
if (!t.isIdentifier(node.tag)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const tagName = node.tag.name;
|
|
175
|
+
if (!state.twImportNames.has(tagName)) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Extract static className from template literal
|
|
180
|
+
const quasi = node.quasi;
|
|
181
|
+
if (!t.isTemplateLiteral(quasi)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Only support static strings (no interpolations)
|
|
186
|
+
if (quasi.expressions.length > 0) {
|
|
187
|
+
if (process.env.NODE_ENV !== "production") {
|
|
188
|
+
console.warn(
|
|
189
|
+
`[react-native-tailwind] Dynamic tw\`...\` with interpolations is not supported at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
190
|
+
`Use style prop for dynamic values.`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get the static className string
|
|
197
|
+
const className = quasi.quasis[0]?.value.cooked?.trim() ?? "";
|
|
198
|
+
if (!className) {
|
|
199
|
+
// Replace with empty object
|
|
200
|
+
path.replaceWith(
|
|
201
|
+
t.objectExpression([t.objectProperty(t.identifier("style"), t.objectExpression([]))]),
|
|
202
|
+
);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
state.hasClassNames = true;
|
|
207
|
+
|
|
208
|
+
// Process the className with modifiers
|
|
209
|
+
processTwCall(className, path, state, parseClassName, generateStyleKey, splitModifierClasses, t);
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// Handle twStyle('...') call expressions
|
|
213
|
+
CallExpression(path, state) {
|
|
214
|
+
const node = path.node;
|
|
215
|
+
|
|
216
|
+
// Check if the callee is a tracked twStyle import
|
|
217
|
+
if (!t.isIdentifier(node.callee)) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const calleeName = node.callee.name;
|
|
222
|
+
if (!state.twImportNames.has(calleeName)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Must have exactly one argument
|
|
227
|
+
if (node.arguments.length !== 1) {
|
|
228
|
+
if (process.env.NODE_ENV !== "production") {
|
|
229
|
+
console.warn(
|
|
230
|
+
`[react-native-tailwind] twStyle() expects exactly one argument at ${state.file.opts.filename ?? "unknown"}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const arg = node.arguments[0];
|
|
237
|
+
|
|
238
|
+
// Only support static string literals
|
|
239
|
+
if (!t.isStringLiteral(arg)) {
|
|
240
|
+
if (process.env.NODE_ENV !== "production") {
|
|
241
|
+
console.warn(
|
|
242
|
+
`[react-native-tailwind] twStyle() only supports static string literals at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
243
|
+
`Use style prop for dynamic values.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const className = arg.value.trim();
|
|
250
|
+
if (!className) {
|
|
251
|
+
// Replace with undefined
|
|
252
|
+
path.replaceWith(t.identifier("undefined"));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
state.hasClassNames = true;
|
|
257
|
+
|
|
258
|
+
// Process the className with modifiers
|
|
259
|
+
processTwCall(className, path, state, parseClassName, generateStyleKey, splitModifierClasses, t);
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
JSXAttribute(path, state) {
|
|
263
|
+
const node = path.node;
|
|
264
|
+
|
|
265
|
+
// Ensure we have a JSXIdentifier name (not JSXNamespacedName)
|
|
266
|
+
if (!t.isJSXIdentifier(node.name)) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const attributeName = node.name.name;
|
|
271
|
+
|
|
272
|
+
// Only process configured className-like attributes
|
|
273
|
+
if (!isAttributeSupported(attributeName, state.supportedAttributes, state.attributePatterns)) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const value = node.value;
|
|
278
|
+
|
|
279
|
+
// Determine target style prop based on attribute name
|
|
280
|
+
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
281
|
+
|
|
282
|
+
// Handle static string literals
|
|
283
|
+
if (t.isStringLiteral(value)) {
|
|
284
|
+
const className = value.value.trim();
|
|
285
|
+
|
|
286
|
+
// Skip empty classNames
|
|
287
|
+
if (!className) {
|
|
288
|
+
path.remove();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
state.hasClassNames = true;
|
|
293
|
+
|
|
294
|
+
// Check if className contains modifiers (active:, hover:, focus:, placeholder:)
|
|
295
|
+
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
296
|
+
|
|
297
|
+
// Separate placeholder modifiers from state modifiers
|
|
298
|
+
const placeholderModifiers = modifierClasses.filter((m) => m.modifier === "placeholder");
|
|
299
|
+
const stateModifiers = modifierClasses.filter((m) => m.modifier !== "placeholder");
|
|
300
|
+
|
|
301
|
+
// Handle placeholder modifiers first (they generate placeholderTextColor prop, not style)
|
|
302
|
+
if (placeholderModifiers.length > 0) {
|
|
303
|
+
// Check if this is a TextInput component (placeholder only works on TextInput)
|
|
304
|
+
const jsxOpeningElement = path.parent as BabelTypes.JSXOpeningElement;
|
|
305
|
+
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
306
|
+
|
|
307
|
+
if (componentSupport?.supportedModifiers.includes("placeholder")) {
|
|
308
|
+
const placeholderClasses = placeholderModifiers.map((m) => m.baseClass).join(" ");
|
|
309
|
+
const placeholderColor = parsePlaceholderClasses(placeholderClasses, state.customColors);
|
|
310
|
+
|
|
311
|
+
if (placeholderColor) {
|
|
312
|
+
// Add or merge placeholderTextColor prop
|
|
313
|
+
addOrMergePlaceholderTextColorProp(jsxOpeningElement, placeholderColor, t);
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
// Warn if placeholder modifier used on non-TextInput element
|
|
317
|
+
if (process.env.NODE_ENV !== "production") {
|
|
318
|
+
console.warn(
|
|
319
|
+
`[react-native-tailwind] placeholder: modifier can only be used on TextInput component at ${state.file.opts.filename ?? "unknown"}`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// If there are state modifiers, check if this component supports them
|
|
326
|
+
if (stateModifiers.length > 0) {
|
|
327
|
+
// Get the JSX opening element (the direct parent of the attribute)
|
|
328
|
+
const jsxOpeningElement = path.parent;
|
|
329
|
+
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
330
|
+
|
|
331
|
+
if (componentSupport) {
|
|
332
|
+
// Get modifier types used in className
|
|
333
|
+
const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier)));
|
|
334
|
+
|
|
335
|
+
// Check if all modifiers are supported by this component
|
|
336
|
+
const unsupportedModifiers = usedModifiers.filter(
|
|
337
|
+
(mod) => !componentSupport.supportedModifiers.includes(mod),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
if (unsupportedModifiers.length > 0) {
|
|
341
|
+
// Warn about unsupported modifiers
|
|
342
|
+
if (process.env.NODE_ENV !== "production") {
|
|
343
|
+
console.warn(
|
|
344
|
+
`[react-native-tailwind] Modifiers (${unsupportedModifiers.map((m) => `${m}:`).join(", ")}) are not supported on ${componentSupport.component} component at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
345
|
+
`Supported modifiers: ${componentSupport.supportedModifiers.join(", ")}`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
// Filter out unsupported modifiers
|
|
349
|
+
const supportedModifierClasses = stateModifiers.filter((m) =>
|
|
350
|
+
componentSupport.supportedModifiers.includes(m.modifier),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// If no supported modifiers remain, fall through to normal processing
|
|
354
|
+
if (supportedModifierClasses.length === 0) {
|
|
355
|
+
// Continue to normal processing
|
|
356
|
+
} else {
|
|
357
|
+
// Process only supported modifiers
|
|
358
|
+
const filteredClassName =
|
|
359
|
+
baseClasses.join(" ") +
|
|
360
|
+
" " +
|
|
361
|
+
supportedModifierClasses.map((m) => `${m.modifier}:${m.baseClass}`).join(" ");
|
|
362
|
+
const styleExpression = processStaticClassNameWithModifiers(
|
|
363
|
+
filteredClassName.trim(),
|
|
364
|
+
state,
|
|
365
|
+
parseClassName,
|
|
366
|
+
generateStyleKey,
|
|
367
|
+
splitModifierClasses,
|
|
368
|
+
t,
|
|
369
|
+
);
|
|
370
|
+
const modifierTypes = Array.from(new Set(supportedModifierClasses.map((m) => m.modifier)));
|
|
371
|
+
const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
|
|
372
|
+
|
|
373
|
+
const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
|
|
374
|
+
|
|
375
|
+
if (styleAttribute) {
|
|
376
|
+
mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
|
|
377
|
+
} else {
|
|
378
|
+
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
// All modifiers are supported - process normally
|
|
384
|
+
const styleExpression = processStaticClassNameWithModifiers(
|
|
385
|
+
className,
|
|
386
|
+
state,
|
|
387
|
+
parseClassName,
|
|
388
|
+
generateStyleKey,
|
|
389
|
+
splitModifierClasses,
|
|
390
|
+
t,
|
|
391
|
+
);
|
|
392
|
+
const modifierTypes = usedModifiers;
|
|
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;
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
// Component doesn't support any modifiers
|
|
406
|
+
if (process.env.NODE_ENV !== "production") {
|
|
407
|
+
const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier)));
|
|
408
|
+
console.warn(
|
|
409
|
+
`[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"}`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
// Fall through to normal processing (ignore modifiers)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Normal processing without modifiers
|
|
417
|
+
// Use baseClasses only (placeholder modifiers already handled separately)
|
|
418
|
+
const classNameForStyle = baseClasses.join(" ");
|
|
419
|
+
if (!classNameForStyle) {
|
|
420
|
+
// No base classes, only had placeholder modifiers - just remove className
|
|
421
|
+
path.remove();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const styleObject = parseClassName(classNameForStyle, state.customColors);
|
|
426
|
+
const styleKey = generateStyleKey(classNameForStyle);
|
|
427
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
428
|
+
|
|
429
|
+
// Check if there's already a style prop on this element
|
|
430
|
+
const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
|
|
431
|
+
|
|
432
|
+
if (styleAttribute) {
|
|
433
|
+
// Merge with existing style prop
|
|
434
|
+
mergeStyleAttribute(path, styleAttribute, styleKey, state.stylesIdentifier, t);
|
|
435
|
+
} else {
|
|
436
|
+
// Replace className with style prop
|
|
437
|
+
replaceWithStyleAttribute(path, styleKey, targetStyleProp, state.stylesIdentifier, t);
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Handle dynamic expressions (JSXExpressionContainer)
|
|
443
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
444
|
+
const expression = value.expression;
|
|
445
|
+
|
|
446
|
+
// Skip JSXEmptyExpression
|
|
447
|
+
if (t.isJSXEmptyExpression(expression)) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
// Process dynamic expression
|
|
453
|
+
const result = processDynamicExpression(expression, state, parseClassName, generateStyleKey, t);
|
|
454
|
+
|
|
455
|
+
if (result) {
|
|
456
|
+
state.hasClassNames = true;
|
|
457
|
+
|
|
458
|
+
// Check if there's already a style prop on this element
|
|
459
|
+
const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
|
|
460
|
+
|
|
461
|
+
if (styleAttribute) {
|
|
462
|
+
// Merge with existing style prop
|
|
463
|
+
mergeDynamicStyleAttribute(path, styleAttribute, result, t);
|
|
464
|
+
} else {
|
|
465
|
+
// Replace className with style prop
|
|
466
|
+
replaceDynamicWithStyleAttribute(path, result, targetStyleProp, t);
|
|
467
|
+
}
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
} catch (error) {
|
|
471
|
+
// Fall through to warning
|
|
472
|
+
if (process.env.NODE_ENV !== "production") {
|
|
473
|
+
console.warn(
|
|
474
|
+
`[react-native-tailwind] Failed to process dynamic ${attributeName} at ${state.file.opts.filename ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Unsupported dynamic className - warn in development
|
|
481
|
+
if (process.env.NODE_ENV !== "production") {
|
|
482
|
+
const filename = state.file.opts.filename ?? "unknown";
|
|
483
|
+
console.warn(
|
|
484
|
+
`[react-native-tailwind] Dynamic ${attributeName} values are not fully supported at ${filename}. ` +
|
|
485
|
+
`Use the ${targetStyleProp} prop for dynamic values.`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for matching and handling JSX attribute names
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Default className-like attributes (used when no custom attributes are provided)
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_CLASS_ATTRIBUTES: readonly ["className", "contentContainerClassName", "columnWrapperClassName", "ListHeaderComponentClassName", "ListFooterComponentClassName"];
|
|
8
|
+
/**
|
|
9
|
+
* Build attribute matching structures from plugin options
|
|
10
|
+
* Separates exact matches from pattern-based matches
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildAttributeMatchers(attributes: string[]): {
|
|
13
|
+
exactMatches: Set<string>;
|
|
14
|
+
patterns: RegExp[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Check if an attribute name matches the configured attributes
|
|
18
|
+
*/
|
|
19
|
+
export declare function isAttributeSupported(attributeName: string, exactMatches: Set<string>, patterns: RegExp[]): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Get the target style prop name based on the className attribute
|
|
22
|
+
*/
|
|
23
|
+
export declare function getTargetStyleProp(attributeName: string): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for matching and handling JSX attribute names
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default className-like attributes (used when no custom attributes are provided)
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_CLASS_ATTRIBUTES = [
|
|
9
|
+
"className",
|
|
10
|
+
"contentContainerClassName",
|
|
11
|
+
"columnWrapperClassName",
|
|
12
|
+
"ListHeaderComponentClassName",
|
|
13
|
+
"ListFooterComponentClassName",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build attribute matching structures from plugin options
|
|
18
|
+
* Separates exact matches from pattern-based matches
|
|
19
|
+
*/
|
|
20
|
+
export function buildAttributeMatchers(attributes: string[]): {
|
|
21
|
+
exactMatches: Set<string>;
|
|
22
|
+
patterns: RegExp[];
|
|
23
|
+
} {
|
|
24
|
+
const exactMatches = new Set<string>();
|
|
25
|
+
const patterns: RegExp[] = [];
|
|
26
|
+
|
|
27
|
+
for (const attr of attributes) {
|
|
28
|
+
if (attr.includes("*")) {
|
|
29
|
+
// Convert glob pattern to regex
|
|
30
|
+
// *ClassName -> /^.*ClassName$/
|
|
31
|
+
// container* -> /^container.*$/
|
|
32
|
+
const regexPattern = "^" + attr.replace(/\*/g, ".*") + "$";
|
|
33
|
+
patterns.push(new RegExp(regexPattern));
|
|
34
|
+
} else {
|
|
35
|
+
// Exact match
|
|
36
|
+
exactMatches.add(attr);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { exactMatches, patterns };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if an attribute name matches the configured attributes
|
|
45
|
+
*/
|
|
46
|
+
export function isAttributeSupported(
|
|
47
|
+
attributeName: string,
|
|
48
|
+
exactMatches: Set<string>,
|
|
49
|
+
patterns: RegExp[],
|
|
50
|
+
): boolean {
|
|
51
|
+
// Check exact matches first (faster)
|
|
52
|
+
if (exactMatches.has(attributeName)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check pattern matches
|
|
57
|
+
for (const pattern of patterns) {
|
|
58
|
+
if (pattern.test(attributeName)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the target style prop name based on the className attribute
|
|
68
|
+
*/
|
|
69
|
+
export function getTargetStyleProp(attributeName: string): string {
|
|
70
|
+
return attributeName.endsWith("ClassName") ? attributeName.replace("ClassName", "Style") : "style";
|
|
71
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for determining component modifier support
|
|
3
|
+
*/
|
|
4
|
+
import type * as BabelTypes from "@babel/types";
|
|
5
|
+
import type { ModifierType } from "../../parser/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* Check if a JSX element supports modifiers and determine which modifiers are supported
|
|
8
|
+
* Returns an object with component info and supported modifiers
|
|
9
|
+
*/
|
|
10
|
+
export declare function getComponentModifierSupport(jsxElement: BabelTypes.Node, t: typeof BabelTypes): {
|
|
11
|
+
component: string;
|
|
12
|
+
supportedModifiers: ModifierType[];
|
|
13
|
+
} | null;
|
|
14
|
+
/**
|
|
15
|
+
* Get the state property name for a modifier type
|
|
16
|
+
* Maps modifier types to component state parameter properties
|
|
17
|
+
*/
|
|
18
|
+
export declare function getStatePropertyForModifier(modifier: ModifierType): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for determining component modifier support
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type * as BabelTypes from "@babel/types";
|
|
6
|
+
import type { ModifierType } from "../../parser/index.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a JSX element supports modifiers and determine which modifiers are supported
|
|
10
|
+
* Returns an object with component info and supported modifiers
|
|
11
|
+
*/
|
|
12
|
+
export function getComponentModifierSupport(
|
|
13
|
+
jsxElement: BabelTypes.Node,
|
|
14
|
+
t: typeof BabelTypes,
|
|
15
|
+
): { component: string; supportedModifiers: ModifierType[] } | null {
|
|
16
|
+
if (!t.isJSXOpeningElement(jsxElement)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const name = jsxElement.name;
|
|
21
|
+
let componentName: string | null = null;
|
|
22
|
+
|
|
23
|
+
// Handle simple identifier: <Pressable>
|
|
24
|
+
if (t.isJSXIdentifier(name)) {
|
|
25
|
+
componentName = name.name;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Handle member expression: <ReactNative.Pressable>
|
|
29
|
+
if (t.isJSXMemberExpression(name)) {
|
|
30
|
+
const property = name.property;
|
|
31
|
+
if (t.isJSXIdentifier(property)) {
|
|
32
|
+
componentName = property.name;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!componentName) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Map components to their supported modifiers
|
|
41
|
+
switch (componentName) {
|
|
42
|
+
case "Pressable":
|
|
43
|
+
return { component: "Pressable", supportedModifiers: ["active", "hover", "focus", "disabled"] };
|
|
44
|
+
case "TextInput":
|
|
45
|
+
return { component: "TextInput", supportedModifiers: ["focus", "disabled", "placeholder"] };
|
|
46
|
+
default:
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the state property name for a modifier type
|
|
53
|
+
* Maps modifier types to component state parameter properties
|
|
54
|
+
*/
|
|
55
|
+
export function getStatePropertyForModifier(modifier: ModifierType): string {
|
|
56
|
+
switch (modifier) {
|
|
57
|
+
case "active":
|
|
58
|
+
return "pressed";
|
|
59
|
+
case "hover":
|
|
60
|
+
return "hovered";
|
|
61
|
+
case "focus":
|
|
62
|
+
return "focused";
|
|
63
|
+
case "disabled":
|
|
64
|
+
return "disabled";
|
|
65
|
+
default:
|
|
66
|
+
return "pressed"; // fallback
|
|
67
|
+
}
|
|
68
|
+
}
|