@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
package/dist/babel/index.ts
CHANGED
|
@@ -1,1172 +1,7 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
3
|
-
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
4
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
5
|
-
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Babel plugin for react-native-tailwind
|
|
9
|
-
* Transforms className props to style props at compile time
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { NodePath, PluginObj, PluginPass } from "@babel/core";
|
|
13
|
-
import * as BabelTypes from "@babel/types";
|
|
14
|
-
import { StyleObject } from "src/types.js";
|
|
15
|
-
import type { ModifierType, ParsedModifier } from "../parser/index.js";
|
|
16
|
-
import { parseClassName as parseClassNameFn, splitModifierClasses } from "../parser/index.js";
|
|
17
|
-
import { generateStyleKey as generateStyleKeyFn } from "../utils/styleKey.js";
|
|
18
|
-
import { extractCustomColors } from "./config-loader.js";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Plugin options
|
|
22
|
-
*/
|
|
23
|
-
export type PluginOptions = {
|
|
24
|
-
/**
|
|
25
|
-
* List of JSX attribute names to transform (in addition to or instead of 'className')
|
|
26
|
-
* Supports exact matches and glob patterns:
|
|
27
|
-
* - Exact: 'className', 'containerClassName'
|
|
28
|
-
* - Glob: '*ClassName' (matches any attribute ending in 'ClassName')
|
|
29
|
-
*
|
|
30
|
-
* @default ['className', 'contentContainerClassName', 'columnWrapperClassName', 'ListHeaderComponentClassName', 'ListFooterComponentClassName']
|
|
31
|
-
*/
|
|
32
|
-
attributes?: string[];
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Custom identifier name for the generated StyleSheet constant
|
|
36
|
-
*
|
|
37
|
-
* @default '_twStyles'
|
|
38
|
-
*/
|
|
39
|
-
stylesIdentifier?: string;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
type PluginState = PluginPass & {
|
|
43
|
-
styleRegistry: Map<string, StyleObject>;
|
|
44
|
-
hasClassNames: boolean;
|
|
45
|
-
hasStyleSheetImport: boolean;
|
|
46
|
-
customColors: Record<string, string>;
|
|
47
|
-
supportedAttributes: Set<string>;
|
|
48
|
-
attributePatterns: RegExp[];
|
|
49
|
-
stylesIdentifier: string;
|
|
50
|
-
// Track tw/twStyle imports from main package
|
|
51
|
-
twImportNames: Set<string>; // e.g., ['tw', 'twStyle'] or ['tw as customTw']
|
|
52
|
-
hasTwImport: boolean;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// Default identifier for the generated StyleSheet constant
|
|
56
|
-
const DEFAULT_STYLES_IDENTIFIER = "_twStyles";
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Default className-like attributes (used when no custom attributes are provided)
|
|
60
|
-
*/
|
|
61
|
-
const DEFAULT_CLASS_ATTRIBUTES = [
|
|
62
|
-
"className",
|
|
63
|
-
"contentContainerClassName",
|
|
64
|
-
"columnWrapperClassName",
|
|
65
|
-
"ListHeaderComponentClassName",
|
|
66
|
-
"ListFooterComponentClassName",
|
|
67
|
-
] as const;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Build attribute matching structures from plugin options
|
|
71
|
-
* Separates exact matches from pattern-based matches
|
|
72
|
-
*/
|
|
73
|
-
function buildAttributeMatchers(attributes: string[]): {
|
|
74
|
-
exactMatches: Set<string>;
|
|
75
|
-
patterns: RegExp[];
|
|
76
|
-
} {
|
|
77
|
-
const exactMatches = new Set<string>();
|
|
78
|
-
const patterns: RegExp[] = [];
|
|
79
|
-
|
|
80
|
-
for (const attr of attributes) {
|
|
81
|
-
if (attr.includes("*")) {
|
|
82
|
-
// Convert glob pattern to regex
|
|
83
|
-
// *ClassName -> /^.*ClassName$/
|
|
84
|
-
// container* -> /^container.*$/
|
|
85
|
-
const regexPattern = "^" + attr.replace(/\*/g, ".*") + "$";
|
|
86
|
-
patterns.push(new RegExp(regexPattern));
|
|
87
|
-
} else {
|
|
88
|
-
// Exact match
|
|
89
|
-
exactMatches.add(attr);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { exactMatches, patterns };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Check if an attribute name matches the configured attributes
|
|
98
|
-
*/
|
|
99
|
-
function isAttributeSupported(attributeName: string, exactMatches: Set<string>, patterns: RegExp[]): boolean {
|
|
100
|
-
// Check exact matches first (faster)
|
|
101
|
-
if (exactMatches.has(attributeName)) {
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Check pattern matches
|
|
106
|
-
for (const pattern of patterns) {
|
|
107
|
-
if (pattern.test(attributeName)) {
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Get the target style prop name based on the className attribute
|
|
117
|
-
*/
|
|
118
|
-
function getTargetStyleProp(attributeName: string): string {
|
|
119
|
-
return attributeName.endsWith("ClassName") ? attributeName.replace("ClassName", "Style") : "style";
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Check if a JSX element supports modifiers and determine which modifiers are supported
|
|
124
|
-
* Returns an object with component info and supported modifiers
|
|
125
|
-
*/
|
|
126
|
-
function getComponentModifierSupport(
|
|
127
|
-
jsxElement: any,
|
|
128
|
-
t: typeof BabelTypes,
|
|
129
|
-
): { component: string; supportedModifiers: ModifierType[] } | null {
|
|
130
|
-
if (!t.isJSXOpeningElement(jsxElement)) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const name = jsxElement.name;
|
|
135
|
-
let componentName: string | null = null;
|
|
136
|
-
|
|
137
|
-
// Handle simple identifier: <Pressable>
|
|
138
|
-
if (t.isJSXIdentifier(name)) {
|
|
139
|
-
componentName = name.name;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Handle member expression: <ReactNative.Pressable>
|
|
143
|
-
if (t.isJSXMemberExpression(name)) {
|
|
144
|
-
const property = name.property;
|
|
145
|
-
if (t.isJSXIdentifier(property)) {
|
|
146
|
-
componentName = property.name;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (!componentName) {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Map components to their supported modifiers
|
|
155
|
-
switch (componentName) {
|
|
156
|
-
case "Pressable":
|
|
157
|
-
return { component: "Pressable", supportedModifiers: ["active", "hover", "focus", "disabled"] };
|
|
158
|
-
case "TextInput":
|
|
159
|
-
return { component: "TextInput", supportedModifiers: ["focus", "disabled"] };
|
|
160
|
-
default:
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Result of processing a dynamic expression
|
|
167
|
-
*/
|
|
168
|
-
type DynamicExpressionResult = {
|
|
169
|
-
// The transformed expression to use in the style prop
|
|
170
|
-
expression: any;
|
|
171
|
-
// Static parts that can be parsed at compile time (if any)
|
|
172
|
-
staticParts?: string[];
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Process a dynamic className expression
|
|
177
|
-
* Extracts static strings and transforms the expression to use pre-compiled styles
|
|
178
|
-
*/
|
|
179
|
-
function processDynamicExpression(
|
|
180
|
-
expression: any,
|
|
181
|
-
state: PluginState,
|
|
182
|
-
t: typeof BabelTypes,
|
|
183
|
-
): DynamicExpressionResult | null {
|
|
184
|
-
// Handle template literals: `m-4 ${condition ? "p-4" : "p-2"}`
|
|
185
|
-
if (t.isTemplateLiteral(expression)) {
|
|
186
|
-
return processTemplateLiteral(expression, state, t);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Handle conditional expressions: condition ? "m-4" : "p-2"
|
|
190
|
-
if (t.isConditionalExpression(expression)) {
|
|
191
|
-
return processConditionalExpression(expression, state, t);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Handle logical expressions: condition && "m-4"
|
|
195
|
-
if (t.isLogicalExpression(expression)) {
|
|
196
|
-
return processLogicalExpression(expression, state, t);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Unsupported expression type
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Process template literal: `static ${dynamic} more-static`
|
|
205
|
-
*/
|
|
206
|
-
function processTemplateLiteral(
|
|
207
|
-
node: any,
|
|
208
|
-
state: PluginState,
|
|
209
|
-
t: typeof BabelTypes,
|
|
210
|
-
): DynamicExpressionResult | null {
|
|
211
|
-
const parts: any[] = [];
|
|
212
|
-
const staticParts: string[] = [];
|
|
213
|
-
|
|
214
|
-
// Process quasis (static parts) and expressions (dynamic parts)
|
|
215
|
-
for (let i = 0; i < node.quasis.length; i++) {
|
|
216
|
-
const quasi = node.quasis[i];
|
|
217
|
-
const staticText = quasi.value.cooked?.trim();
|
|
218
|
-
|
|
219
|
-
// Add static part if not empty
|
|
220
|
-
if (staticText) {
|
|
221
|
-
// Parse static classes and add to registry
|
|
222
|
-
const classes = staticText.split(/\s+/).filter(Boolean);
|
|
223
|
-
for (const cls of classes) {
|
|
224
|
-
const styleObject = parseClassName(cls, state.customColors);
|
|
225
|
-
const styleKey = generateStyleKey(cls);
|
|
226
|
-
state.styleRegistry.set(styleKey, styleObject);
|
|
227
|
-
staticParts.push(cls);
|
|
228
|
-
|
|
229
|
-
// Add to parts array
|
|
230
|
-
parts.push(t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey)));
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Add dynamic expression if exists
|
|
235
|
-
if (i < node.expressions.length) {
|
|
236
|
-
const expr = node.expressions[i];
|
|
237
|
-
|
|
238
|
-
// Recursively process nested dynamic expressions
|
|
239
|
-
const result = processDynamicExpression(expr, state, t);
|
|
240
|
-
if (result) {
|
|
241
|
-
parts.push(result.expression);
|
|
242
|
-
} else {
|
|
243
|
-
// For unsupported expressions, keep them as-is
|
|
244
|
-
// This won't work at runtime but maintains the structure
|
|
245
|
-
parts.push(expr);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (parts.length === 0) {
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// If single part, return it directly; otherwise return array
|
|
255
|
-
const expression = parts.length === 1 ? parts[0] : t.arrayExpression(parts);
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
expression,
|
|
259
|
-
staticParts: staticParts.length > 0 ? staticParts : undefined,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
1
|
/**
|
|
264
|
-
*
|
|
2
|
+
* Main entry point for the Babel plugin
|
|
3
|
+
* Re-exports the plugin from plugin.ts
|
|
265
4
|
*/
|
|
266
|
-
function processConditionalExpression(
|
|
267
|
-
node: any,
|
|
268
|
-
state: PluginState,
|
|
269
|
-
t: typeof BabelTypes,
|
|
270
|
-
): DynamicExpressionResult | null {
|
|
271
|
-
const consequent = processStringOrExpression(node.consequent, state, t);
|
|
272
|
-
const alternate = processStringOrExpression(node.alternate, state, t);
|
|
273
|
-
|
|
274
|
-
if (!consequent && !alternate) {
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Build conditional: condition ? consequentStyle : alternateStyle
|
|
279
|
-
const expression = t.conditionalExpression(
|
|
280
|
-
node.test,
|
|
281
|
-
consequent ?? t.nullLiteral(),
|
|
282
|
-
alternate ?? t.nullLiteral(),
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
return { expression };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Process logical expression: condition && "class-a"
|
|
290
|
-
*/
|
|
291
|
-
function processLogicalExpression(
|
|
292
|
-
node: any,
|
|
293
|
-
state: PluginState,
|
|
294
|
-
t: typeof BabelTypes,
|
|
295
|
-
): DynamicExpressionResult | null {
|
|
296
|
-
// Only handle AND (&&) expressions
|
|
297
|
-
if (node.operator !== "&&") {
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const right = processStringOrExpression(node.right, state, t);
|
|
302
|
-
|
|
303
|
-
if (!right) {
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Build logical: condition && style
|
|
308
|
-
const expression = t.logicalExpression("&&", node.left, right);
|
|
309
|
-
|
|
310
|
-
return { expression };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Process a node that might be a string literal or another expression
|
|
315
|
-
*/
|
|
316
|
-
function processStringOrExpression(node: any, state: PluginState, t: typeof BabelTypes): any {
|
|
317
|
-
// Handle string literals
|
|
318
|
-
if (t.isStringLiteral(node)) {
|
|
319
|
-
const className = node.value.trim();
|
|
320
|
-
if (!className) {
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Parse and register styles
|
|
325
|
-
const styleObject = parseClassName(className, state.customColors);
|
|
326
|
-
const styleKey = generateStyleKey(className);
|
|
327
|
-
state.styleRegistry.set(styleKey, styleObject);
|
|
328
|
-
|
|
329
|
-
return t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Handle nested expressions recursively
|
|
333
|
-
if (t.isConditionalExpression(node)) {
|
|
334
|
-
const result = processConditionalExpression(node, state, t);
|
|
335
|
-
return result?.expression ?? null;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (t.isLogicalExpression(node)) {
|
|
339
|
-
const result = processLogicalExpression(node, state, t);
|
|
340
|
-
return result?.expression ?? null;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (t.isTemplateLiteral(node)) {
|
|
344
|
-
const result = processTemplateLiteral(node, state, t);
|
|
345
|
-
return result?.expression ?? null;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Unsupported - return null
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Process a static className string that contains modifiers
|
|
354
|
-
* Returns a style function expression for Pressable components
|
|
355
|
-
*/
|
|
356
|
-
function processStaticClassNameWithModifiers(
|
|
357
|
-
className: string,
|
|
358
|
-
state: PluginState,
|
|
359
|
-
t: typeof BabelTypes,
|
|
360
|
-
): any {
|
|
361
|
-
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
362
|
-
|
|
363
|
-
// Parse and register base classes
|
|
364
|
-
let baseStyleExpression: any = null;
|
|
365
|
-
if (baseClasses.length > 0) {
|
|
366
|
-
const baseClassName = baseClasses.join(" ");
|
|
367
|
-
const baseStyleObject = parseClassName(baseClassName, state.customColors);
|
|
368
|
-
const baseStyleKey = generateStyleKey(baseClassName);
|
|
369
|
-
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
370
|
-
baseStyleExpression = t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey));
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Parse and register modifier classes
|
|
374
|
-
// Group by modifier type for better organization
|
|
375
|
-
const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
|
|
376
|
-
for (const mod of modifierClasses) {
|
|
377
|
-
if (!modifiersByType.has(mod.modifier)) {
|
|
378
|
-
modifiersByType.set(mod.modifier, []);
|
|
379
|
-
}
|
|
380
|
-
const modGroup = modifiersByType.get(mod.modifier);
|
|
381
|
-
if (modGroup) {
|
|
382
|
-
modGroup.push(mod);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Build style function: ({ pressed }) => [baseStyle, pressed && modifierStyle]
|
|
387
|
-
const styleArrayElements: any[] = [];
|
|
388
|
-
|
|
389
|
-
// Add base style first
|
|
390
|
-
if (baseStyleExpression) {
|
|
391
|
-
styleArrayElements.push(baseStyleExpression);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Add conditional styles for each modifier type
|
|
395
|
-
for (const [modifierType, modifiers] of modifiersByType) {
|
|
396
|
-
// Parse all modifier classes together
|
|
397
|
-
const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
398
|
-
const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
|
|
399
|
-
const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
|
|
400
|
-
state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
|
|
401
|
-
|
|
402
|
-
// Create conditional: pressed && styles._active_bg_blue_700
|
|
403
|
-
const stateProperty = getStatePropertyForModifier(modifierType);
|
|
404
|
-
const conditionalExpression = t.logicalExpression(
|
|
405
|
-
"&&",
|
|
406
|
-
t.identifier(stateProperty),
|
|
407
|
-
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey)),
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
styleArrayElements.push(conditionalExpression);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// If only base style, return it directly; otherwise return array
|
|
414
|
-
if (styleArrayElements.length === 1) {
|
|
415
|
-
return styleArrayElements[0];
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return t.arrayExpression(styleArrayElements);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Get the state property name for a modifier type
|
|
423
|
-
* Maps modifier types to component state parameter properties
|
|
424
|
-
*/
|
|
425
|
-
function getStatePropertyForModifier(modifier: ModifierType): string {
|
|
426
|
-
switch (modifier) {
|
|
427
|
-
case "active":
|
|
428
|
-
return "pressed";
|
|
429
|
-
case "hover":
|
|
430
|
-
return "hovered";
|
|
431
|
-
case "focus":
|
|
432
|
-
return "focused";
|
|
433
|
-
case "disabled":
|
|
434
|
-
return "disabled";
|
|
435
|
-
default:
|
|
436
|
-
return "pressed"; // fallback
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Create a style function for Pressable: ({ pressed }) => styleExpression
|
|
442
|
-
*/
|
|
443
|
-
function createStyleFunction(styleExpression: any, modifierTypes: ModifierType[], t: typeof BabelTypes): any {
|
|
444
|
-
// Build parameter object: { pressed, hovered, focused }
|
|
445
|
-
const paramProperties: any[] = [];
|
|
446
|
-
const usedStateProps = new Set<string>();
|
|
447
|
-
|
|
448
|
-
for (const modifierType of modifierTypes) {
|
|
449
|
-
const stateProperty = getStatePropertyForModifier(modifierType);
|
|
450
|
-
if (!usedStateProps.has(stateProperty)) {
|
|
451
|
-
usedStateProps.add(stateProperty);
|
|
452
|
-
paramProperties.push(
|
|
453
|
-
t.objectProperty(t.identifier(stateProperty), t.identifier(stateProperty), false, true),
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const param = t.objectPattern(paramProperties);
|
|
459
|
-
|
|
460
|
-
// Create arrow function: ({ pressed }) => styleExpression
|
|
461
|
-
return t.arrowFunctionExpression([param], styleExpression);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Process tw`...` or twStyle('...') call and replace with TwStyle object
|
|
466
|
-
* Generates: { style: styles._base, activeStyle: styles._active, ... }
|
|
467
|
-
*/
|
|
468
|
-
function processTwCall(className: string, path: NodePath, state: PluginState, t: typeof BabelTypes): void {
|
|
469
|
-
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
470
|
-
|
|
471
|
-
// Build TwStyle object properties
|
|
472
|
-
const objectProperties: any[] = [];
|
|
473
|
-
|
|
474
|
-
// Parse and add base styles
|
|
475
|
-
if (baseClasses.length > 0) {
|
|
476
|
-
const baseClassName = baseClasses.join(" ");
|
|
477
|
-
const baseStyleObject = parseClassName(baseClassName, state.customColors);
|
|
478
|
-
const baseStyleKey = generateStyleKey(baseClassName);
|
|
479
|
-
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
480
|
-
|
|
481
|
-
objectProperties.push(
|
|
482
|
-
t.objectProperty(
|
|
483
|
-
t.identifier("style"),
|
|
484
|
-
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
|
|
485
|
-
),
|
|
486
|
-
);
|
|
487
|
-
} else {
|
|
488
|
-
// No base classes - add empty style object
|
|
489
|
-
objectProperties.push(t.objectProperty(t.identifier("style"), t.objectExpression([])));
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Group modifiers by type
|
|
493
|
-
const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
|
|
494
|
-
for (const mod of modifierClasses) {
|
|
495
|
-
if (!modifiersByType.has(mod.modifier)) {
|
|
496
|
-
modifiersByType.set(mod.modifier, []);
|
|
497
|
-
}
|
|
498
|
-
const modGroup = modifiersByType.get(mod.modifier);
|
|
499
|
-
if (modGroup) {
|
|
500
|
-
modGroup.push(mod);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Add modifier styles
|
|
505
|
-
for (const [modifierType, modifiers] of modifiersByType) {
|
|
506
|
-
const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
507
|
-
const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
|
|
508
|
-
const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
|
|
509
|
-
state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
|
|
510
|
-
|
|
511
|
-
// Map modifier type to property name: active -> activeStyle
|
|
512
|
-
const propertyName = `${modifierType}Style`;
|
|
513
|
-
|
|
514
|
-
objectProperties.push(
|
|
515
|
-
t.objectProperty(
|
|
516
|
-
t.identifier(propertyName),
|
|
517
|
-
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey)),
|
|
518
|
-
),
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Replace the tw`...` or twStyle('...') with the object
|
|
523
|
-
const twStyleObject = t.objectExpression(objectProperties);
|
|
524
|
-
path.replaceWith(twStyleObject);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
export default function reactNativeTailwindBabelPlugin(
|
|
528
|
-
{ types: t }: { types: typeof BabelTypes },
|
|
529
|
-
options?: PluginOptions,
|
|
530
|
-
): PluginObj<PluginState> {
|
|
531
|
-
// Build attribute matchers from options
|
|
532
|
-
const attributes = options?.attributes ?? [...DEFAULT_CLASS_ATTRIBUTES];
|
|
533
|
-
const { exactMatches, patterns } = buildAttributeMatchers(attributes);
|
|
534
|
-
const stylesIdentifier = options?.stylesIdentifier ?? DEFAULT_STYLES_IDENTIFIER;
|
|
535
|
-
|
|
536
|
-
return {
|
|
537
|
-
name: "react-native-tailwind",
|
|
538
|
-
|
|
539
|
-
visitor: {
|
|
540
|
-
Program: {
|
|
541
|
-
enter(_path: NodePath, state: PluginState) {
|
|
542
|
-
// Initialize state for this file
|
|
543
|
-
state.styleRegistry = new Map();
|
|
544
|
-
state.hasClassNames = false;
|
|
545
|
-
state.hasStyleSheetImport = false;
|
|
546
|
-
state.supportedAttributes = exactMatches;
|
|
547
|
-
state.attributePatterns = patterns;
|
|
548
|
-
state.stylesIdentifier = stylesIdentifier;
|
|
549
|
-
state.twImportNames = new Set();
|
|
550
|
-
state.hasTwImport = false;
|
|
551
|
-
|
|
552
|
-
// Load custom colors from tailwind.config.*
|
|
553
|
-
state.customColors = extractCustomColors(state.file.opts.filename ?? "");
|
|
554
|
-
},
|
|
555
|
-
|
|
556
|
-
exit(path: NodePath, state: PluginState) {
|
|
557
|
-
// Remove tw/twStyle imports if they were used (and transformed)
|
|
558
|
-
if (state.hasTwImport) {
|
|
559
|
-
removeTwImports(path, t);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// If no classNames were found, skip StyleSheet generation
|
|
563
|
-
if (!state.hasClassNames || state.styleRegistry.size === 0) {
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Add StyleSheet import if not already present
|
|
568
|
-
if (!state.hasStyleSheetImport) {
|
|
569
|
-
addStyleSheetImport(path, t);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Generate and inject StyleSheet.create at the beginning of the file (after imports)
|
|
573
|
-
// This ensures _twStyles is defined before any code that references it
|
|
574
|
-
injectStylesAtTop(path, state.styleRegistry, state.stylesIdentifier, t);
|
|
575
|
-
},
|
|
576
|
-
},
|
|
577
|
-
|
|
578
|
-
// Check if StyleSheet is already imported and track tw/twStyle imports
|
|
579
|
-
ImportDeclaration(path: NodePath, state: PluginState) {
|
|
580
|
-
const node = path.node as any;
|
|
581
|
-
|
|
582
|
-
// Track react-native StyleSheet import
|
|
583
|
-
if (node.source.value === "react-native") {
|
|
584
|
-
const specifiers = node.specifiers;
|
|
585
|
-
const hasStyleSheet = specifiers.some((spec: any) => {
|
|
586
|
-
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
587
|
-
return spec.imported.name === "StyleSheet";
|
|
588
|
-
}
|
|
589
|
-
return false;
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
if (hasStyleSheet) {
|
|
593
|
-
state.hasStyleSheetImport = true;
|
|
594
|
-
} else {
|
|
595
|
-
// Add StyleSheet to existing import
|
|
596
|
-
node.specifiers.push(t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet")));
|
|
597
|
-
state.hasStyleSheetImport = true;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Track tw/twStyle imports from main package (for compile-time transformation)
|
|
602
|
-
if (node.source.value === "@mgcrea/react-native-tailwind") {
|
|
603
|
-
const specifiers = node.specifiers;
|
|
604
|
-
specifiers.forEach((spec: any) => {
|
|
605
|
-
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
606
|
-
const importedName = spec.imported.name;
|
|
607
|
-
if (importedName === "tw" || importedName === "twStyle") {
|
|
608
|
-
// Track the local name (could be renamed: import { tw as customTw })
|
|
609
|
-
const localName = spec.local.name;
|
|
610
|
-
state.twImportNames.add(localName);
|
|
611
|
-
state.hasTwImport = true;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
},
|
|
617
|
-
|
|
618
|
-
// Handle tw`...` tagged template expressions
|
|
619
|
-
TaggedTemplateExpression(path: NodePath, state: PluginState) {
|
|
620
|
-
const node = path.node as any;
|
|
621
|
-
|
|
622
|
-
// Check if the tag is a tracked tw import
|
|
623
|
-
if (!t.isIdentifier(node.tag)) {
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
const tagName = node.tag.name;
|
|
628
|
-
if (!state.twImportNames.has(tagName)) {
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Extract static className from template literal
|
|
633
|
-
const quasi = node.quasi;
|
|
634
|
-
if (!t.isTemplateLiteral(quasi)) {
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Only support static strings (no interpolations)
|
|
639
|
-
if (quasi.expressions.length > 0) {
|
|
640
|
-
if (process.env.NODE_ENV !== "production") {
|
|
641
|
-
console.warn(
|
|
642
|
-
`[react-native-tailwind] Dynamic tw\`...\` with interpolations is not supported at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
643
|
-
`Use style prop for dynamic values.`,
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Get the static className string
|
|
650
|
-
const className = quasi.quasis[0]?.value.cooked?.trim() ?? "";
|
|
651
|
-
if (!className) {
|
|
652
|
-
// Replace with empty object
|
|
653
|
-
path.replaceWith(
|
|
654
|
-
t.objectExpression([t.objectProperty(t.identifier("style"), t.objectExpression([]))]),
|
|
655
|
-
);
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
state.hasClassNames = true;
|
|
660
|
-
|
|
661
|
-
// Process the className with modifiers
|
|
662
|
-
processTwCall(className, path, state, t);
|
|
663
|
-
},
|
|
664
|
-
|
|
665
|
-
// Handle twStyle('...') call expressions
|
|
666
|
-
CallExpression(path: NodePath, state: PluginState) {
|
|
667
|
-
const node = path.node as any;
|
|
668
|
-
|
|
669
|
-
// Check if the callee is a tracked twStyle import
|
|
670
|
-
if (!t.isIdentifier(node.callee)) {
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const calleeName = node.callee.name;
|
|
675
|
-
if (!state.twImportNames.has(calleeName)) {
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Must have exactly one argument
|
|
680
|
-
if (node.arguments.length !== 1) {
|
|
681
|
-
if (process.env.NODE_ENV !== "production") {
|
|
682
|
-
console.warn(
|
|
683
|
-
`[react-native-tailwind] twStyle() expects exactly one argument at ${state.file.opts.filename ?? "unknown"}`,
|
|
684
|
-
);
|
|
685
|
-
}
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
const arg = node.arguments[0];
|
|
690
|
-
|
|
691
|
-
// Only support static string literals
|
|
692
|
-
if (!t.isStringLiteral(arg)) {
|
|
693
|
-
if (process.env.NODE_ENV !== "production") {
|
|
694
|
-
console.warn(
|
|
695
|
-
`[react-native-tailwind] twStyle() only supports static string literals at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
696
|
-
`Use style prop for dynamic values.`,
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const className = arg.value.trim();
|
|
703
|
-
if (!className) {
|
|
704
|
-
// Replace with undefined
|
|
705
|
-
path.replaceWith(t.identifier("undefined"));
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
state.hasClassNames = true;
|
|
710
|
-
|
|
711
|
-
// Process the className with modifiers
|
|
712
|
-
processTwCall(className, path, state, t);
|
|
713
|
-
},
|
|
714
|
-
|
|
715
|
-
JSXAttribute(path: NodePath, state: PluginState) {
|
|
716
|
-
const node = path.node as any;
|
|
717
|
-
const attributeName = node.name.name;
|
|
718
|
-
|
|
719
|
-
// Only process configured className-like attributes
|
|
720
|
-
if (!isAttributeSupported(attributeName, state.supportedAttributes, state.attributePatterns)) {
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const value = node.value;
|
|
725
|
-
|
|
726
|
-
// Determine target style prop based on attribute name
|
|
727
|
-
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
728
|
-
|
|
729
|
-
// Handle static string literals
|
|
730
|
-
if (t.isStringLiteral(value)) {
|
|
731
|
-
const className = value.value.trim();
|
|
732
|
-
|
|
733
|
-
// Skip empty classNames
|
|
734
|
-
if (!className) {
|
|
735
|
-
path.remove();
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
state.hasClassNames = true;
|
|
740
|
-
|
|
741
|
-
// Check if className contains modifiers (active:, hover:, focus:)
|
|
742
|
-
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
743
|
-
|
|
744
|
-
// If there are modifiers, check if this component supports them
|
|
745
|
-
if (modifierClasses.length > 0) {
|
|
746
|
-
// Get the JSX opening element (the direct parent of the attribute)
|
|
747
|
-
const jsxOpeningElement = path.parent;
|
|
748
|
-
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
749
|
-
|
|
750
|
-
if (componentSupport) {
|
|
751
|
-
// Get modifier types used in className
|
|
752
|
-
const usedModifiers = Array.from(new Set(modifierClasses.map((m) => m.modifier)));
|
|
753
|
-
|
|
754
|
-
// Check if all modifiers are supported by this component
|
|
755
|
-
const unsupportedModifiers = usedModifiers.filter(
|
|
756
|
-
(mod) => !componentSupport.supportedModifiers.includes(mod),
|
|
757
|
-
);
|
|
758
|
-
|
|
759
|
-
if (unsupportedModifiers.length > 0) {
|
|
760
|
-
// Warn about unsupported modifiers
|
|
761
|
-
if (process.env.NODE_ENV !== "production") {
|
|
762
|
-
console.warn(
|
|
763
|
-
`[react-native-tailwind] Modifiers (${unsupportedModifiers.map((m) => `${m}:`).join(", ")}) are not supported on ${componentSupport.component} component at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
764
|
-
`Supported modifiers: ${componentSupport.supportedModifiers.join(", ")}`,
|
|
765
|
-
);
|
|
766
|
-
}
|
|
767
|
-
// Filter out unsupported modifiers
|
|
768
|
-
const supportedModifierClasses = modifierClasses.filter((m) =>
|
|
769
|
-
componentSupport.supportedModifiers.includes(m.modifier),
|
|
770
|
-
);
|
|
771
|
-
|
|
772
|
-
// If no supported modifiers remain, fall through to normal processing
|
|
773
|
-
if (supportedModifierClasses.length === 0) {
|
|
774
|
-
// Continue to normal processing
|
|
775
|
-
} else {
|
|
776
|
-
// Process only supported modifiers
|
|
777
|
-
const filteredClassName =
|
|
778
|
-
baseClasses.join(" ") +
|
|
779
|
-
" " +
|
|
780
|
-
supportedModifierClasses.map((m) => `${m.modifier}:${m.baseClass}`).join(" ");
|
|
781
|
-
const styleExpression = processStaticClassNameWithModifiers(
|
|
782
|
-
filteredClassName.trim(),
|
|
783
|
-
state,
|
|
784
|
-
t,
|
|
785
|
-
);
|
|
786
|
-
const modifierTypes = Array.from(new Set(supportedModifierClasses.map((m) => m.modifier)));
|
|
787
|
-
const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
|
|
788
|
-
|
|
789
|
-
const parent = path.parent as any;
|
|
790
|
-
const styleAttribute = parent.attributes.find(
|
|
791
|
-
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
792
|
-
);
|
|
793
|
-
|
|
794
|
-
if (styleAttribute) {
|
|
795
|
-
mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
|
|
796
|
-
} else {
|
|
797
|
-
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
798
|
-
}
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
} else {
|
|
802
|
-
// All modifiers are supported - process normally
|
|
803
|
-
const styleExpression = processStaticClassNameWithModifiers(className, state, t);
|
|
804
|
-
const modifierTypes = usedModifiers;
|
|
805
|
-
const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
|
|
806
|
-
|
|
807
|
-
const parent = path.parent as any;
|
|
808
|
-
const styleAttribute = parent.attributes.find(
|
|
809
|
-
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
810
|
-
);
|
|
811
|
-
|
|
812
|
-
if (styleAttribute) {
|
|
813
|
-
mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
|
|
814
|
-
} else {
|
|
815
|
-
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
816
|
-
}
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
} else {
|
|
820
|
-
// Component doesn't support any modifiers
|
|
821
|
-
if (process.env.NODE_ENV !== "production") {
|
|
822
|
-
const usedModifiers = Array.from(new Set(modifierClasses.map((m) => m.modifier)));
|
|
823
|
-
console.warn(
|
|
824
|
-
`[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"}`,
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
// Fall through to normal processing (ignore modifiers)
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Normal processing without modifiers
|
|
832
|
-
const styleObject = parseClassName(className, state.customColors);
|
|
833
|
-
const styleKey = generateStyleKey(className);
|
|
834
|
-
state.styleRegistry.set(styleKey, styleObject);
|
|
835
|
-
|
|
836
|
-
// Check if there's already a style prop on this element
|
|
837
|
-
const parent = path.parent as any;
|
|
838
|
-
const styleAttribute = parent.attributes.find(
|
|
839
|
-
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
840
|
-
);
|
|
841
|
-
|
|
842
|
-
if (styleAttribute) {
|
|
843
|
-
// Merge with existing style prop
|
|
844
|
-
mergeStyleAttribute(path, styleAttribute, styleKey, state.stylesIdentifier, t);
|
|
845
|
-
} else {
|
|
846
|
-
// Replace className with style prop
|
|
847
|
-
replaceWithStyleAttribute(path, styleKey, targetStyleProp, state.stylesIdentifier, t);
|
|
848
|
-
}
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// Handle dynamic expressions (JSXExpressionContainer)
|
|
853
|
-
if (t.isJSXExpressionContainer(value)) {
|
|
854
|
-
const expression = value.expression;
|
|
855
|
-
|
|
856
|
-
// Skip JSXEmptyExpression
|
|
857
|
-
if (t.isJSXEmptyExpression(expression)) {
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
try {
|
|
862
|
-
// Process dynamic expression
|
|
863
|
-
const result = processDynamicExpression(expression, state, t);
|
|
864
|
-
|
|
865
|
-
if (result) {
|
|
866
|
-
state.hasClassNames = true;
|
|
867
|
-
|
|
868
|
-
// Check if there's already a style prop on this element
|
|
869
|
-
const parent = path.parent as any;
|
|
870
|
-
const styleAttribute = parent.attributes.find(
|
|
871
|
-
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
872
|
-
);
|
|
873
|
-
|
|
874
|
-
if (styleAttribute) {
|
|
875
|
-
// Merge with existing style prop
|
|
876
|
-
mergeDynamicStyleAttribute(path, styleAttribute, result, t);
|
|
877
|
-
} else {
|
|
878
|
-
// Replace className with style prop
|
|
879
|
-
replaceDynamicWithStyleAttribute(path, result, targetStyleProp, t);
|
|
880
|
-
}
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
} catch (error) {
|
|
884
|
-
// Fall through to warning
|
|
885
|
-
if (process.env.NODE_ENV !== "production") {
|
|
886
|
-
console.warn(
|
|
887
|
-
`[react-native-tailwind] Failed to process dynamic ${attributeName} at ${state.file.opts.filename ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
888
|
-
);
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// Unsupported dynamic className - warn in development
|
|
894
|
-
if (process.env.NODE_ENV !== "production") {
|
|
895
|
-
const filename = state.file.opts.filename ?? "unknown";
|
|
896
|
-
console.warn(
|
|
897
|
-
`[react-native-tailwind] Dynamic ${attributeName} values are not fully supported at ${filename}. ` +
|
|
898
|
-
`Use the ${targetStyleProp} prop for dynamic values.`,
|
|
899
|
-
);
|
|
900
|
-
}
|
|
901
|
-
},
|
|
902
|
-
},
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
/**
|
|
907
|
-
* Add StyleSheet import to the file
|
|
908
|
-
*/
|
|
909
|
-
function addStyleSheetImport(path: NodePath, t: typeof BabelTypes) {
|
|
910
|
-
const importDeclaration = t.importDeclaration(
|
|
911
|
-
[t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet"))],
|
|
912
|
-
t.stringLiteral("react-native"),
|
|
913
|
-
);
|
|
914
|
-
|
|
915
|
-
// Add import at the top of the file
|
|
916
|
-
(path as any).unshiftContainer("body", importDeclaration);
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* Remove tw/twStyle imports from @mgcrea/react-native-tailwind
|
|
921
|
-
* This is called after all tw calls have been transformed
|
|
922
|
-
*/
|
|
923
|
-
function removeTwImports(path: NodePath, t: typeof BabelTypes) {
|
|
924
|
-
// Traverse the program to find and remove tw/twStyle imports
|
|
925
|
-
path.traverse({
|
|
926
|
-
ImportDeclaration(importPath: NodePath) {
|
|
927
|
-
const node = importPath.node as any;
|
|
928
|
-
|
|
929
|
-
// Only process imports from main package
|
|
930
|
-
if (node.source.value !== "@mgcrea/react-native-tailwind") {
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// Filter out tw/twStyle specifiers
|
|
935
|
-
const remainingSpecifiers = node.specifiers.filter((spec: any) => {
|
|
936
|
-
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
937
|
-
const importedName = spec.imported.name;
|
|
938
|
-
return importedName !== "tw" && importedName !== "twStyle";
|
|
939
|
-
}
|
|
940
|
-
return true;
|
|
941
|
-
});
|
|
942
|
-
|
|
943
|
-
if (remainingSpecifiers.length === 0) {
|
|
944
|
-
// Remove entire import if no specifiers remain
|
|
945
|
-
importPath.remove();
|
|
946
|
-
} else if (remainingSpecifiers.length < node.specifiers.length) {
|
|
947
|
-
// Update import with remaining specifiers
|
|
948
|
-
node.specifiers = remainingSpecifiers;
|
|
949
|
-
}
|
|
950
|
-
},
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
/**
|
|
955
|
-
* Replace className with style attribute
|
|
956
|
-
*/
|
|
957
|
-
function replaceWithStyleAttribute(
|
|
958
|
-
classNamePath: NodePath,
|
|
959
|
-
styleKey: string,
|
|
960
|
-
targetStyleProp: string,
|
|
961
|
-
stylesIdentifier: string,
|
|
962
|
-
t: typeof BabelTypes,
|
|
963
|
-
) {
|
|
964
|
-
const styleAttribute = t.jsxAttribute(
|
|
965
|
-
t.jsxIdentifier(targetStyleProp),
|
|
966
|
-
t.jsxExpressionContainer(t.memberExpression(t.identifier(stylesIdentifier), t.identifier(styleKey))),
|
|
967
|
-
);
|
|
968
|
-
|
|
969
|
-
classNamePath.replaceWith(styleAttribute);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
/**
|
|
973
|
-
* Merge className styles with existing style prop
|
|
974
|
-
*/
|
|
975
|
-
function mergeStyleAttribute(
|
|
976
|
-
classNamePath: NodePath,
|
|
977
|
-
styleAttribute: any,
|
|
978
|
-
styleKey: string,
|
|
979
|
-
stylesIdentifier: string,
|
|
980
|
-
t: typeof BabelTypes,
|
|
981
|
-
) {
|
|
982
|
-
const existingStyle = styleAttribute.value.expression;
|
|
983
|
-
|
|
984
|
-
// Create array with className styles first, then existing styles
|
|
985
|
-
// This allows existing styles to override className styles
|
|
986
|
-
const styleArray = t.arrayExpression([
|
|
987
|
-
t.memberExpression(t.identifier(stylesIdentifier), t.identifier(styleKey)),
|
|
988
|
-
existingStyle,
|
|
989
|
-
]);
|
|
990
|
-
|
|
991
|
-
styleAttribute.value = t.jsxExpressionContainer(styleArray);
|
|
992
|
-
|
|
993
|
-
// Remove the className attribute
|
|
994
|
-
classNamePath.remove();
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
/**
|
|
998
|
-
* Replace className with dynamic style attribute
|
|
999
|
-
*/
|
|
1000
|
-
function replaceDynamicWithStyleAttribute(
|
|
1001
|
-
classNamePath: NodePath,
|
|
1002
|
-
result: DynamicExpressionResult,
|
|
1003
|
-
targetStyleProp: string,
|
|
1004
|
-
t: typeof BabelTypes,
|
|
1005
|
-
) {
|
|
1006
|
-
const styleAttribute = t.jsxAttribute(
|
|
1007
|
-
t.jsxIdentifier(targetStyleProp),
|
|
1008
|
-
t.jsxExpressionContainer(result.expression),
|
|
1009
|
-
);
|
|
1010
|
-
|
|
1011
|
-
classNamePath.replaceWith(styleAttribute);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
/**
|
|
1015
|
-
* Merge dynamic className styles with existing style prop
|
|
1016
|
-
*/
|
|
1017
|
-
function mergeDynamicStyleAttribute(
|
|
1018
|
-
classNamePath: NodePath,
|
|
1019
|
-
styleAttribute: any,
|
|
1020
|
-
result: DynamicExpressionResult,
|
|
1021
|
-
t: typeof BabelTypes,
|
|
1022
|
-
) {
|
|
1023
|
-
const existingStyle = styleAttribute.value.expression;
|
|
1024
|
-
|
|
1025
|
-
// Merge dynamic expression with existing styles
|
|
1026
|
-
// If existing is already an array, append to it; otherwise create new array
|
|
1027
|
-
let styleArray;
|
|
1028
|
-
if (t.isArrayExpression(existingStyle)) {
|
|
1029
|
-
// Prepend dynamic styles to existing array
|
|
1030
|
-
styleArray = t.arrayExpression([result.expression, ...existingStyle.elements]);
|
|
1031
|
-
} else {
|
|
1032
|
-
// Create new array with dynamic styles first, then existing
|
|
1033
|
-
styleArray = t.arrayExpression([result.expression, existingStyle]);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
styleAttribute.value = t.jsxExpressionContainer(styleArray);
|
|
1037
|
-
|
|
1038
|
-
// Remove the className attribute
|
|
1039
|
-
classNamePath.remove();
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/**
|
|
1043
|
-
* Replace className with style function attribute (for Pressable with modifiers)
|
|
1044
|
-
*/
|
|
1045
|
-
function replaceWithStyleFunctionAttribute(
|
|
1046
|
-
classNamePath: NodePath,
|
|
1047
|
-
styleFunctionExpression: any,
|
|
1048
|
-
targetStyleProp: string,
|
|
1049
|
-
t: typeof BabelTypes,
|
|
1050
|
-
) {
|
|
1051
|
-
const styleAttribute = t.jsxAttribute(
|
|
1052
|
-
t.jsxIdentifier(targetStyleProp),
|
|
1053
|
-
t.jsxExpressionContainer(styleFunctionExpression),
|
|
1054
|
-
);
|
|
1055
|
-
|
|
1056
|
-
classNamePath.replaceWith(styleAttribute);
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
/**
|
|
1060
|
-
* Merge className style function with existing style prop (for Pressable with modifiers)
|
|
1061
|
-
*/
|
|
1062
|
-
function mergeStyleFunctionAttribute(
|
|
1063
|
-
classNamePath: NodePath,
|
|
1064
|
-
styleAttribute: any,
|
|
1065
|
-
styleFunctionExpression: any,
|
|
1066
|
-
t: typeof BabelTypes,
|
|
1067
|
-
) {
|
|
1068
|
-
const existingStyle = styleAttribute.value.expression;
|
|
1069
|
-
|
|
1070
|
-
// Create a wrapper function that merges both styles
|
|
1071
|
-
// ({ pressed }) => [styleFunctionResult, existingStyle]
|
|
1072
|
-
// We need to call the style function and merge results
|
|
1073
|
-
|
|
1074
|
-
// If existing is already a function, we need to handle it specially
|
|
1075
|
-
if (t.isArrowFunctionExpression(existingStyle) || t.isFunctionExpression(existingStyle)) {
|
|
1076
|
-
// Both are functions - create wrapper that calls both
|
|
1077
|
-
// (_state) => [newStyleFn(_state), existingStyleFn(_state)]
|
|
1078
|
-
// Create an identifier for the parameter to pass to the function calls
|
|
1079
|
-
const paramIdentifier = t.identifier("_state");
|
|
1080
|
-
|
|
1081
|
-
const newFunctionCall = t.callExpression(styleFunctionExpression, [paramIdentifier]);
|
|
1082
|
-
const existingFunctionCall = t.callExpression(existingStyle, [paramIdentifier]);
|
|
1083
|
-
|
|
1084
|
-
const mergedArray = t.arrayExpression([newFunctionCall, existingFunctionCall]);
|
|
1085
|
-
const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
|
|
1086
|
-
|
|
1087
|
-
styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
|
|
1088
|
-
} else {
|
|
1089
|
-
// Existing is static - create function that returns array
|
|
1090
|
-
// (_state) => [styleFunctionResult, existingStyle]
|
|
1091
|
-
// Create an identifier for the parameter to pass to the function call
|
|
1092
|
-
const paramIdentifier = t.identifier("_state");
|
|
1093
|
-
|
|
1094
|
-
const functionCall = t.callExpression(styleFunctionExpression, [paramIdentifier]);
|
|
1095
|
-
const mergedArray = t.arrayExpression([functionCall, existingStyle]);
|
|
1096
|
-
const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
|
|
1097
|
-
|
|
1098
|
-
styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// Remove the className attribute
|
|
1102
|
-
classNamePath.remove();
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
/**
|
|
1106
|
-
* Inject StyleSheet.create with all collected styles at the top of the file
|
|
1107
|
-
* This ensures the styles object is defined before any code that references it
|
|
1108
|
-
*/
|
|
1109
|
-
function injectStylesAtTop(
|
|
1110
|
-
path: NodePath,
|
|
1111
|
-
styleRegistry: Map<string, StyleObject>,
|
|
1112
|
-
stylesIdentifier: string,
|
|
1113
|
-
t: typeof BabelTypes,
|
|
1114
|
-
) {
|
|
1115
|
-
// Build style object properties
|
|
1116
|
-
const styleProperties: any[] = [];
|
|
1117
|
-
|
|
1118
|
-
for (const [key, styleObject] of styleRegistry) {
|
|
1119
|
-
const properties = Object.entries(styleObject).map(([styleProp, styleValue]) => {
|
|
1120
|
-
let valueNode;
|
|
1121
|
-
|
|
1122
|
-
if (typeof styleValue === "number") {
|
|
1123
|
-
valueNode = t.numericLiteral(styleValue);
|
|
1124
|
-
} else if (typeof styleValue === "string") {
|
|
1125
|
-
valueNode = t.stringLiteral(styleValue);
|
|
1126
|
-
} else {
|
|
1127
|
-
// Fallback for other types
|
|
1128
|
-
valueNode = t.valueToNode(styleValue);
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
return t.objectProperty(t.identifier(styleProp), valueNode);
|
|
1132
|
-
});
|
|
1133
|
-
|
|
1134
|
-
styleProperties.push(t.objectProperty(t.identifier(key), t.objectExpression(properties)));
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// Create: const _twStyles = StyleSheet.create({ ... })
|
|
1138
|
-
const styleSheet = t.variableDeclaration("const", [
|
|
1139
|
-
t.variableDeclarator(
|
|
1140
|
-
t.identifier(stylesIdentifier),
|
|
1141
|
-
t.callExpression(t.memberExpression(t.identifier("StyleSheet"), t.identifier("create")), [
|
|
1142
|
-
t.objectExpression(styleProperties),
|
|
1143
|
-
]),
|
|
1144
|
-
),
|
|
1145
|
-
]);
|
|
1146
|
-
|
|
1147
|
-
// Find the index to insert after all imports
|
|
1148
|
-
const body = (path as any).node.body;
|
|
1149
|
-
let insertIndex = 0;
|
|
1150
|
-
|
|
1151
|
-
// Find the last import statement
|
|
1152
|
-
for (let i = 0; i < body.length; i++) {
|
|
1153
|
-
if (t.isImportDeclaration(body[i])) {
|
|
1154
|
-
insertIndex = i + 1;
|
|
1155
|
-
} else {
|
|
1156
|
-
// Stop at the first non-import statement
|
|
1157
|
-
break;
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// Insert StyleSheet.create after imports
|
|
1162
|
-
body.splice(insertIndex, 0, styleSheet);
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
// Helper functions that use the imported parser
|
|
1166
|
-
function parseClassName(className: string, customColors: Record<string, string>): StyleObject {
|
|
1167
|
-
return parseClassNameFn(className, customColors);
|
|
1168
|
-
}
|
|
1169
5
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
}
|
|
6
|
+
export { default } from "./plugin.js";
|
|
7
|
+
export type { PluginOptions } from "./plugin.js";
|