@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.
Files changed (81) hide show
  1. package/README.md +2 -1
  2. package/dist/babel/index.cjs +333 -195
  3. package/dist/babel/index.d.ts +4 -40
  4. package/dist/babel/index.test.ts +214 -1
  5. package/dist/babel/index.ts +4 -1169
  6. package/dist/babel/plugin.d.ts +42 -0
  7. package/{src/babel/index.test.ts → dist/babel/plugin.test.ts} +216 -2
  8. package/dist/babel/plugin.ts +491 -0
  9. package/dist/babel/utils/attributeMatchers.d.ts +23 -0
  10. package/dist/babel/utils/attributeMatchers.ts +71 -0
  11. package/dist/babel/utils/componentSupport.d.ts +18 -0
  12. package/dist/babel/utils/componentSupport.ts +68 -0
  13. package/dist/babel/utils/dynamicProcessing.d.ts +32 -0
  14. package/dist/babel/utils/dynamicProcessing.ts +223 -0
  15. package/dist/babel/utils/modifierProcessing.d.ts +26 -0
  16. package/dist/babel/utils/modifierProcessing.ts +118 -0
  17. package/dist/babel/utils/styleInjection.d.ts +15 -0
  18. package/dist/babel/utils/styleInjection.ts +80 -0
  19. package/dist/babel/utils/styleTransforms.d.ts +39 -0
  20. package/dist/babel/utils/styleTransforms.test.ts +349 -0
  21. package/dist/babel/utils/styleTransforms.ts +258 -0
  22. package/dist/babel/utils/twProcessing.d.ts +28 -0
  23. package/dist/babel/utils/twProcessing.ts +124 -0
  24. package/dist/components/TextInput.d.ts +171 -14
  25. package/dist/config/tailwind.d.ts +302 -0
  26. package/dist/config/tailwind.js +1 -0
  27. package/dist/index.d.ts +5 -4
  28. package/dist/index.js +1 -1
  29. package/dist/parser/colors.js +1 -1
  30. package/dist/parser/index.d.ts +1 -0
  31. package/dist/parser/index.js +1 -1
  32. package/dist/parser/modifiers.d.ts +2 -2
  33. package/dist/parser/modifiers.js +1 -1
  34. package/dist/parser/placeholder.d.ts +36 -0
  35. package/dist/parser/placeholder.js +1 -0
  36. package/dist/parser/placeholder.test.js +1 -0
  37. package/dist/parser/typography.d.ts +1 -0
  38. package/dist/parser/typography.js +1 -1
  39. package/dist/parser/typography.test.js +1 -1
  40. package/dist/runtime.cjs +1 -1
  41. package/dist/runtime.cjs.map +4 -4
  42. package/dist/runtime.d.ts +1 -14
  43. package/dist/runtime.js +1 -1
  44. package/dist/runtime.js.map +4 -4
  45. package/dist/stubs/tw.d.ts +1 -14
  46. package/dist/types/core.d.ts +40 -0
  47. package/dist/types/core.js +0 -0
  48. package/dist/types/index.d.ts +2 -0
  49. package/dist/types/index.js +1 -0
  50. package/dist/types/runtime.d.ts +15 -0
  51. package/dist/types/runtime.js +1 -0
  52. package/dist/types/util.d.ts +3 -0
  53. package/dist/types/util.js +0 -0
  54. package/package.json +1 -1
  55. package/src/babel/index.ts +4 -1169
  56. package/src/babel/plugin.test.ts +482 -0
  57. package/src/babel/plugin.ts +491 -0
  58. package/src/babel/utils/attributeMatchers.ts +71 -0
  59. package/src/babel/utils/componentSupport.ts +68 -0
  60. package/src/babel/utils/dynamicProcessing.ts +223 -0
  61. package/src/babel/utils/modifierProcessing.ts +118 -0
  62. package/src/babel/utils/styleInjection.ts +80 -0
  63. package/src/babel/utils/styleTransforms.test.ts +349 -0
  64. package/src/babel/utils/styleTransforms.ts +258 -0
  65. package/src/babel/utils/twProcessing.ts +124 -0
  66. package/src/components/TextInput.tsx +17 -14
  67. package/src/config/{palettes.ts → tailwind.ts} +2 -2
  68. package/src/index.ts +6 -3
  69. package/src/parser/colors.ts +2 -2
  70. package/src/parser/index.ts +1 -0
  71. package/src/parser/modifiers.ts +10 -4
  72. package/src/parser/placeholder.test.ts +105 -0
  73. package/src/parser/placeholder.ts +78 -0
  74. package/src/parser/typography.test.ts +11 -0
  75. package/src/parser/typography.ts +20 -2
  76. package/src/runtime.ts +1 -16
  77. package/src/stubs/tw.ts +1 -16
  78. package/src/{types.ts → types/core.ts} +0 -4
  79. package/src/types/index.ts +2 -0
  80. package/src/types/runtime.ts +17 -0
  81. package/src/types/util.ts +1 -0
@@ -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
- * Process conditional expression: condition ? "class-a" : "class-b"
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
- function generateStyleKey(className: string): string {
1171
- return generateStyleKeyFn(className);
1172
- }
6
+ export { default } from "./plugin.js";
7
+ export type { PluginOptions } from "./plugin.js";