@mgcrea/react-native-tailwind 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +29 -2014
  2. package/dist/babel/config-loader.d.ts +3 -0
  3. package/dist/babel/config-loader.test.ts +2 -2
  4. package/dist/babel/config-loader.ts +37 -2
  5. package/dist/babel/index.cjs +2855 -2434
  6. package/dist/babel/plugin/componentScope.d.ts +26 -0
  7. package/dist/babel/plugin/componentScope.ts +87 -0
  8. package/dist/babel/plugin/state.d.ts +119 -0
  9. package/dist/babel/plugin/state.ts +177 -0
  10. package/dist/babel/plugin/visitors/className.d.ts +11 -0
  11. package/{src/babel/plugin.test.ts → dist/babel/plugin/visitors/className.test.ts} +74 -674
  12. package/dist/babel/plugin/visitors/className.ts +624 -0
  13. package/dist/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  14. package/dist/babel/plugin/visitors/imports.d.ts +11 -0
  15. package/dist/babel/plugin/visitors/imports.test.ts +88 -0
  16. package/dist/babel/plugin/visitors/imports.ts +101 -0
  17. package/dist/babel/plugin/visitors/program.d.ts +15 -0
  18. package/dist/babel/plugin/visitors/program.test.ts +325 -0
  19. package/dist/babel/plugin/visitors/program.ts +99 -0
  20. package/dist/babel/plugin/visitors/tw.d.ts +16 -0
  21. package/dist/babel/plugin/visitors/tw.test.ts +620 -0
  22. package/dist/babel/plugin/visitors/tw.ts +148 -0
  23. package/dist/babel/plugin.d.ts +3 -96
  24. package/dist/babel/plugin.test.ts +470 -0
  25. package/dist/babel/plugin.ts +28 -953
  26. package/dist/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  27. package/dist/babel/utils/componentSupport.test.ts +20 -7
  28. package/dist/babel/utils/componentSupport.ts +2 -0
  29. package/dist/babel/utils/modifierProcessing.ts +21 -0
  30. package/dist/babel/utils/platformModifierProcessing.ts +11 -0
  31. package/dist/babel/utils/styleInjection.d.ts +15 -0
  32. package/dist/babel/utils/styleInjection.ts +172 -17
  33. package/dist/babel/utils/twProcessing.ts +11 -0
  34. package/dist/babel/utils/windowDimensionsProcessing.d.ts +56 -0
  35. package/dist/babel/utils/windowDimensionsProcessing.ts +121 -0
  36. package/dist/components/TouchableOpacity.d.ts +35 -0
  37. package/dist/components/TouchableOpacity.js +1 -0
  38. package/dist/components/index.d.ts +3 -0
  39. package/dist/components/index.js +1 -0
  40. package/dist/config/markers.d.ts +5 -0
  41. package/dist/config/markers.js +1 -0
  42. package/dist/index.d.ts +2 -5
  43. package/dist/index.js +1 -1
  44. package/dist/parser/borders.d.ts +3 -1
  45. package/dist/parser/borders.js +1 -1
  46. package/dist/parser/borders.test.js +1 -1
  47. package/dist/parser/colors.js +1 -1
  48. package/dist/parser/colors.test.js +1 -1
  49. package/dist/parser/index.d.ts +1 -0
  50. package/dist/parser/index.js +1 -1
  51. package/dist/parser/layout.js +1 -1
  52. package/dist/parser/layout.test.js +1 -1
  53. package/dist/parser/sizing.js +1 -1
  54. package/dist/parser/typography.d.ts +2 -1
  55. package/dist/parser/typography.js +1 -1
  56. package/dist/parser/typography.test.js +1 -1
  57. package/dist/runtime.cjs +1 -1
  58. package/dist/runtime.cjs.map +4 -4
  59. package/dist/runtime.js +1 -1
  60. package/dist/runtime.js.map +4 -4
  61. package/package.json +1 -1
  62. package/src/babel/config-loader.test.ts +2 -2
  63. package/src/babel/config-loader.ts +37 -2
  64. package/src/babel/plugin/componentScope.ts +87 -0
  65. package/src/babel/plugin/state.ts +177 -0
  66. package/src/babel/plugin/visitors/className.test.ts +1312 -0
  67. package/src/babel/plugin/visitors/className.ts +624 -0
  68. package/src/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  69. package/src/babel/plugin/visitors/imports.test.ts +88 -0
  70. package/src/babel/plugin/visitors/imports.ts +101 -0
  71. package/src/babel/plugin/visitors/program.test.ts +325 -0
  72. package/src/babel/plugin/visitors/program.ts +99 -0
  73. package/src/babel/plugin/visitors/tw.test.ts +620 -0
  74. package/src/babel/plugin/visitors/tw.ts +148 -0
  75. package/src/babel/plugin.ts +28 -953
  76. package/src/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  77. package/src/babel/utils/componentSupport.test.ts +20 -7
  78. package/src/babel/utils/componentSupport.ts +2 -0
  79. package/src/babel/utils/modifierProcessing.ts +21 -0
  80. package/src/babel/utils/platformModifierProcessing.ts +11 -0
  81. package/src/babel/utils/styleInjection.ts +172 -17
  82. package/src/babel/utils/twProcessing.ts +11 -0
  83. package/src/babel/utils/windowDimensionsProcessing.ts +121 -0
  84. package/src/components/TouchableOpacity.tsx +71 -0
  85. package/src/components/index.ts +3 -0
  86. package/src/config/markers.ts +5 -0
  87. package/src/index.ts +4 -5
  88. package/src/parser/borders.test.ts +58 -0
  89. package/src/parser/borders.ts +18 -3
  90. package/src/parser/colors.test.ts +249 -0
  91. package/src/parser/colors.ts +38 -0
  92. package/src/parser/index.ts +4 -3
  93. package/src/parser/layout.test.ts +61 -0
  94. package/src/parser/layout.ts +55 -1
  95. package/src/parser/sizing.ts +11 -0
  96. package/src/parser/typography.test.ts +102 -0
  97. package/src/parser/typography.ts +61 -15
@@ -3,245 +3,25 @@
3
3
  * Transforms className props to style props at compile time
4
4
  */
5
5
 
6
- import type { NodePath, PluginObj, PluginPass } from "@babel/core";
6
+ import type { PluginObj } from "@babel/core";
7
7
  import * as BabelTypes from "@babel/types";
8
- import type { ParsedModifier, StateModifierType } from "../parser/index.js";
9
- import {
10
- expandSchemeModifier,
11
- isColorSchemeModifier,
12
- isPlatformModifier,
13
- isSchemeModifier,
14
- isStateModifier,
15
- parseClassName,
16
- parsePlaceholderClasses,
17
- splitModifierClasses,
18
- } from "../parser/index.js";
19
- import type { StyleObject } from "../types/core.js";
20
- import { generateStyleKey } from "../utils/styleKey.js";
21
- import type { CustomTheme } from "./config-loader.js";
22
- import { extractCustomTheme } from "./config-loader.js";
8
+ import type { PluginOptions, PluginState } from "./plugin/state.js";
9
+ import { createInitialState } from "./plugin/state.js";
10
+ import { jsxAttributeVisitor } from "./plugin/visitors/className.js";
11
+ import { importDeclarationVisitor } from "./plugin/visitors/imports.js";
12
+ import { programEnter, programExit } from "./plugin/visitors/program.js";
13
+ import { callExpressionVisitor, taggedTemplateVisitor } from "./plugin/visitors/tw.js";
23
14
 
24
- // Import utility functions
25
- import type { SchemeModifierConfig } from "../types/config.js";
26
- import {
27
- DEFAULT_CLASS_ATTRIBUTES,
28
- buildAttributeMatchers,
29
- getTargetStyleProp,
30
- isAttributeSupported,
31
- } from "./utils/attributeMatchers.js";
32
- import { processColorSchemeModifiers } from "./utils/colorSchemeModifierProcessing.js";
33
- import { getComponentModifierSupport, getStatePropertyForModifier } from "./utils/componentSupport.js";
34
- import { processDynamicExpression } from "./utils/dynamicProcessing.js";
35
- import { createStyleFunction, processStaticClassNameWithModifiers } from "./utils/modifierProcessing.js";
36
- import { processPlatformModifiers } from "./utils/platformModifierProcessing.js";
37
- import {
38
- addColorSchemeImport,
39
- addPlatformImport,
40
- addStyleSheetImport,
41
- injectColorSchemeHook,
42
- injectStylesAtTop,
43
- } from "./utils/styleInjection.js";
44
- import {
45
- addOrMergePlaceholderTextColorProp,
46
- findStyleAttribute,
47
- mergeDynamicStyleAttribute,
48
- mergeStyleAttribute,
49
- mergeStyleFunctionAttribute,
50
- replaceDynamicWithStyleAttribute,
51
- replaceWithStyleAttribute,
52
- replaceWithStyleFunctionAttribute,
53
- } from "./utils/styleTransforms.js";
54
- import { processTwCall, removeTwImports } from "./utils/twProcessing.js";
55
-
56
- /**
57
- * Plugin options
58
- */
59
- export type PluginOptions = {
60
- /**
61
- * List of JSX attribute names to transform (in addition to or instead of 'className')
62
- * Supports exact matches and glob patterns:
63
- * - Exact: 'className', 'containerClassName'
64
- * - Glob: '*ClassName' (matches any attribute ending in 'ClassName')
65
- *
66
- * @default ['className', 'contentContainerClassName', 'columnWrapperClassName', 'ListHeaderComponentClassName', 'ListFooterComponentClassName']
67
- */
68
- attributes?: string[];
69
-
70
- /**
71
- * Custom identifier name for the generated StyleSheet constant
72
- *
73
- * @default '_twStyles'
74
- */
75
- stylesIdentifier?: string;
76
-
77
- /**
78
- * Configuration for the scheme: modifier that expands to both dark: and light: modifiers
79
- *
80
- * @example
81
- * {
82
- * darkSuffix: '-dark', // scheme:bg-primary -> dark:bg-primary-dark
83
- * lightSuffix: '-light' // scheme:bg-primary -> light:bg-primary-light
84
- * }
85
- *
86
- * @default { darkSuffix: '-dark', lightSuffix: '-light' }
87
- */
88
- schemeModifier?: {
89
- darkSuffix?: string;
90
- lightSuffix?: string;
91
- };
92
-
93
- /**
94
- * Configuration for color scheme hook import (dark:/light: modifiers)
95
- *
96
- * Allows using custom color scheme hooks from theme providers instead of
97
- * React Native's built-in useColorScheme.
98
- *
99
- * @example
100
- * // Use custom hook from theme provider
101
- * {
102
- * importFrom: '@/hooks/useColorScheme',
103
- * importName: 'useColorScheme'
104
- * }
105
- *
106
- * @example
107
- * // Use React Navigation theme
108
- * {
109
- * importFrom: '@react-navigation/native',
110
- * importName: 'useTheme' // You'd wrap this to return ColorSchemeName
111
- * }
112
- *
113
- * @default { importFrom: 'react-native', importName: 'useColorScheme' }
114
- */
115
- colorScheme?: {
116
- /**
117
- * Module to import the color scheme hook from
118
- * @default 'react-native'
119
- */
120
- importFrom?: string;
121
-
122
- /**
123
- * Name of the hook to import
124
- * @default 'useColorScheme'
125
- */
126
- importName?: string;
127
- };
128
- };
129
-
130
- type PluginState = PluginPass & {
131
- styleRegistry: Map<string, StyleObject>;
132
- hasClassNames: boolean;
133
- hasStyleSheetImport: boolean;
134
- hasPlatformImport: boolean;
135
- needsPlatformImport: boolean;
136
- hasColorSchemeImport: boolean;
137
- needsColorSchemeImport: boolean;
138
- colorSchemeVariableName: string;
139
- colorSchemeImportSource: string; // Where to import the hook from (e.g., 'react-native')
140
- colorSchemeHookName: string; // Name of the hook to import (e.g., 'useColorScheme')
141
- colorSchemeLocalIdentifier?: string; // Local identifier if hook is already imported with an alias
142
- customTheme: CustomTheme;
143
- schemeModifierConfig: SchemeModifierConfig;
144
- supportedAttributes: Set<string>;
145
- attributePatterns: RegExp[];
146
- stylesIdentifier: string;
147
- // Track tw/twStyle imports from main package
148
- twImportNames: Set<string>; // e.g., ['tw', 'twStyle'] or ['tw as customTw']
149
- hasTwImport: boolean;
150
- // Track react-native import path for conditional StyleSheet/Platform injection
151
- reactNativeImportPath?: NodePath<BabelTypes.ImportDeclaration>;
152
- // Track function components that need colorScheme hook injection
153
- functionComponentsNeedingColorScheme: Set<NodePath<BabelTypes.Function>>;
154
- };
155
-
156
- // Default identifier for the generated StyleSheet constant
157
- const DEFAULT_STYLES_IDENTIFIER = "_twStyles";
158
-
159
- /**
160
- * Check if a function path represents a valid component scope for hook injection
161
- * Valid scopes:
162
- * - Top-level FunctionDeclaration
163
- * - FunctionExpression/ArrowFunctionExpression in top-level VariableDeclarator (with PascalCase name)
164
- * - NOT class methods, NOT nested functions, NOT inline callbacks
165
- *
166
- * @param functionPath - Path to the function to check
167
- * @param t - Babel types
168
- * @returns true if function is a valid component scope
169
- */
170
- function isComponentScope(functionPath: NodePath<BabelTypes.Function>, t: typeof BabelTypes): boolean {
171
- const node = functionPath.node;
172
- const parent = functionPath.parent;
173
- const parentPath = functionPath.parentPath;
174
-
175
- // Reject class methods (class components not supported for hooks)
176
- if (t.isClassMethod(parent)) {
177
- return false;
178
- }
179
-
180
- // Reject if inside a class body
181
- if (functionPath.findParent((p) => t.isClassBody(p.node))) {
182
- return false;
183
- }
184
-
185
- // Accept top-level FunctionDeclaration
186
- if (t.isFunctionDeclaration(node)) {
187
- // Check if it's at program level or in export
188
- if (t.isProgram(parent) || t.isExportNamedDeclaration(parent) || t.isExportDefaultDeclaration(parent)) {
189
- return true;
190
- }
191
- }
192
-
193
- // Accept FunctionExpression/ArrowFunctionExpression in VariableDeclarator
194
- if (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) {
195
- if (t.isVariableDeclarator(parent)) {
196
- // Check if it's at program level (via VariableDeclaration)
197
- const varDeclarationPath = parentPath?.parentPath;
198
- if (
199
- varDeclarationPath &&
200
- t.isVariableDeclaration(varDeclarationPath.node) &&
201
- (t.isProgram(varDeclarationPath.parent) || t.isExportNamedDeclaration(varDeclarationPath.parent))
202
- ) {
203
- // Check for PascalCase naming (component convention)
204
- if (t.isIdentifier(parent.id)) {
205
- const name = parent.id.name;
206
- return /^[A-Z]/.test(name); // Starts with uppercase
207
- }
208
- }
209
- }
210
- }
211
-
212
- return false;
213
- }
214
-
215
- /**
216
- * Find the nearest valid component scope for hook injection
217
- * Climbs the AST from the current path to find a component-level function
218
- *
219
- * @param path - Starting path (e.g., JSXAttribute)
220
- * @param t - Babel types
221
- * @returns NodePath to component function, or null if not found
222
- */
223
- function findComponentScope(path: NodePath, t: typeof BabelTypes): NodePath<BabelTypes.Function> | null {
224
- let current = path.getFunctionParent();
225
-
226
- while (current) {
227
- if (t.isFunction(current.node) && isComponentScope(current, t)) {
228
- return current;
229
- }
230
- // Climb to next parent function
231
- current = current.getFunctionParent();
232
- }
233
-
234
- return null;
235
- }
15
+ // Re-export PluginOptions for external use
16
+ export type { PluginOptions };
236
17
 
237
18
  export default function reactNativeTailwindBabelPlugin(
238
19
  { types: t }: { types: typeof BabelTypes },
239
20
  options?: PluginOptions,
240
21
  ): PluginObj<PluginState> {
241
- // Build attribute matchers from options
242
- const attributes = options?.attributes ?? [...DEFAULT_CLASS_ATTRIBUTES];
243
- const { exactMatches, patterns } = buildAttributeMatchers(attributes);
244
- const stylesIdentifier = options?.stylesIdentifier ?? DEFAULT_STYLES_IDENTIFIER;
22
+ // Color scheme hook configuration from plugin options
23
+ const colorSchemeImportSource = options?.colorScheme?.importFrom ?? "react-native";
24
+ const colorSchemeHookName = options?.colorScheme?.importName ?? "useColorScheme";
245
25
 
246
26
  // Scheme modifier configuration from plugin options
247
27
  const schemeModifierConfig = {
@@ -249,750 +29,45 @@ export default function reactNativeTailwindBabelPlugin(
249
29
  lightSuffix: options?.schemeModifier?.lightSuffix ?? "-light",
250
30
  };
251
31
 
252
- // Color scheme hook configuration from plugin options
253
- const colorSchemeImportSource = options?.colorScheme?.importFrom ?? "react-native";
254
- const colorSchemeHookName = options?.colorScheme?.importName ?? "useColorScheme";
255
-
256
32
  return {
257
33
  name: "react-native-tailwind",
258
34
 
259
35
  visitor: {
260
36
  Program: {
261
- enter(_path: NodePath, state: PluginState) {
37
+ enter(path, state) {
262
38
  // Initialize state for this file
263
- state.styleRegistry = new Map();
264
- state.hasClassNames = false;
265
- state.hasStyleSheetImport = false;
266
- state.hasPlatformImport = false;
267
- state.needsPlatformImport = false;
268
- state.hasColorSchemeImport = false;
269
- state.needsColorSchemeImport = false;
270
- state.colorSchemeVariableName = "_twColorScheme";
271
- state.colorSchemeImportSource = colorSchemeImportSource;
272
- state.colorSchemeHookName = colorSchemeHookName;
273
- state.supportedAttributes = exactMatches;
274
- state.attributePatterns = patterns;
275
- state.stylesIdentifier = stylesIdentifier;
276
- state.twImportNames = new Set();
277
- state.hasTwImport = false;
278
- state.functionComponentsNeedingColorScheme = new Set();
279
- state.hasColorSchemeImport = false;
280
- state.colorSchemeLocalIdentifier = undefined;
281
- state.needsPlatformImport = false;
282
- state.hasPlatformImport = false;
283
-
284
- // Load custom theme from tailwind.config.*
285
- state.customTheme = extractCustomTheme(state.file.opts.filename ?? "");
39
+ const initialState = createInitialState(
40
+ options,
41
+ state.file.opts.filename ?? "",
42
+ colorSchemeImportSource,
43
+ colorSchemeHookName,
44
+ schemeModifierConfig,
45
+ );
46
+ Object.assign(state, initialState);
286
47
 
287
- // Use scheme modifier config from plugin options
288
- state.schemeModifierConfig = schemeModifierConfig;
48
+ // Call programEnter (currently a no-op, but kept for consistency)
49
+ programEnter(path, state);
289
50
  },
290
51
 
291
52
  exit(path, state) {
292
- // Remove tw/twStyle imports if they were used (and transformed)
293
- if (state.hasTwImport) {
294
- removeTwImports(path, t);
295
- }
296
-
297
- // If no classNames were found, skip StyleSheet generation
298
- if (!state.hasClassNames || state.styleRegistry.size === 0) {
299
- return;
300
- }
301
-
302
- // Add StyleSheet import if not already present
303
- if (!state.hasStyleSheetImport) {
304
- addStyleSheetImport(path, t);
305
- }
306
-
307
- // Add Platform import if platform modifiers were used and not already present
308
- if (state.needsPlatformImport && !state.hasPlatformImport) {
309
- addPlatformImport(path, t);
310
- }
311
-
312
- // Add color scheme hook import if color scheme modifiers were used and not already present
313
- if (state.needsColorSchemeImport && !state.hasColorSchemeImport) {
314
- addColorSchemeImport(path, state.colorSchemeImportSource, state.colorSchemeHookName, t);
315
- }
316
-
317
- // Inject color scheme hook in function components that need it
318
- if (state.needsColorSchemeImport) {
319
- for (const functionPath of state.functionComponentsNeedingColorScheme) {
320
- injectColorSchemeHook(
321
- functionPath,
322
- state.colorSchemeVariableName,
323
- state.colorSchemeHookName,
324
- state.colorSchemeLocalIdentifier,
325
- t,
326
- );
327
- }
328
- }
329
-
330
- // Generate and inject StyleSheet.create at the beginning of the file (after imports)
331
- // This ensures _twStyles is defined before any code that references it
332
- injectStylesAtTop(path, state.styleRegistry, state.stylesIdentifier, t);
53
+ programExit(path, state, t);
333
54
  },
334
55
  },
335
56
 
336
- // Check if StyleSheet/Platform are already imported and track tw/twStyle imports
337
57
  ImportDeclaration(path, state) {
338
- const node = path.node;
339
-
340
- // Track react-native StyleSheet and Platform imports
341
- if (node.source.value === "react-native") {
342
- const specifiers = node.specifiers;
343
-
344
- const hasStyleSheet = specifiers.some((spec) => {
345
- if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
346
- return spec.imported.name === "StyleSheet";
347
- }
348
- return false;
349
- });
350
-
351
- const hasPlatform = specifiers.some((spec) => {
352
- if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
353
- return spec.imported.name === "Platform";
354
- }
355
- return false;
356
- });
357
-
358
- // Only track if imports exist - don't mutate yet
359
- // Actual import injection happens in Program.exit only if needed
360
- if (hasStyleSheet) {
361
- state.hasStyleSheetImport = true;
362
- }
363
-
364
- if (hasPlatform) {
365
- state.hasPlatformImport = true;
366
- }
367
-
368
- // Store reference to the react-native import for later modification if needed
369
- state.reactNativeImportPath = path;
370
- }
371
-
372
- // Track color scheme hook import from the configured source
373
- // (default: react-native, but can be custom like @/hooks/useColorScheme)
374
- // Only track value imports (not type-only imports which get erased)
375
- if (node.source.value === state.colorSchemeImportSource && node.importKind !== "type") {
376
- const specifiers = node.specifiers;
377
-
378
- for (const spec of specifiers) {
379
- if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
380
- if (spec.imported.name === state.colorSchemeHookName) {
381
- state.hasColorSchemeImport = true;
382
- // Track the local identifier (handles aliased imports)
383
- // e.g., import { useTheme as navTheme } → local name is 'navTheme'
384
- state.colorSchemeLocalIdentifier = spec.local.name;
385
- break;
386
- }
387
- }
388
- }
389
- }
390
-
391
- // Track tw/twStyle imports from main package (for compile-time transformation)
392
- if (node.source.value === "@mgcrea/react-native-tailwind") {
393
- const specifiers = node.specifiers;
394
- specifiers.forEach((spec) => {
395
- if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
396
- const importedName = spec.imported.name;
397
- if (importedName === "tw" || importedName === "twStyle") {
398
- // Track the local name (could be renamed: import { tw as customTw })
399
- const localName = spec.local.name;
400
- state.twImportNames.add(localName);
401
- state.hasTwImport = true;
402
- }
403
- }
404
- });
405
- }
58
+ importDeclarationVisitor(path, state, t);
406
59
  },
407
60
 
408
- // Handle tw`...` tagged template expressions
409
61
  TaggedTemplateExpression(path, state) {
410
- const node = path.node;
411
-
412
- // Check if the tag is a tracked tw import
413
- if (!t.isIdentifier(node.tag)) {
414
- return;
415
- }
416
-
417
- const tagName = node.tag.name;
418
- if (!state.twImportNames.has(tagName)) {
419
- return;
420
- }
421
-
422
- // Extract static className from template literal
423
- const quasi = node.quasi;
424
- if (!t.isTemplateLiteral(quasi)) {
425
- return;
426
- }
427
-
428
- // Only support static strings (no interpolations)
429
- if (quasi.expressions.length > 0) {
430
- if (process.env.NODE_ENV !== "production") {
431
- console.warn(
432
- `[react-native-tailwind] Dynamic tw\`...\` with interpolations is not supported at ${state.file.opts.filename ?? "unknown"}. ` +
433
- `Use style prop for dynamic values.`,
434
- );
435
- }
436
- return;
437
- }
438
-
439
- // Get the static className string
440
- const className = quasi.quasis[0]?.value.cooked?.trim() ?? "";
441
- if (!className) {
442
- // Replace with empty object
443
- path.replaceWith(
444
- t.objectExpression([t.objectProperty(t.identifier("style"), t.objectExpression([]))]),
445
- );
446
- return;
447
- }
448
-
449
- state.hasClassNames = true;
450
-
451
- // Process the className with modifiers
452
- processTwCall(
453
- className,
454
- path,
455
- state,
456
- parseClassName,
457
- generateStyleKey,
458
- splitModifierClasses,
459
- findComponentScope,
460
- t,
461
- );
62
+ taggedTemplateVisitor(path, state, t);
462
63
  },
463
64
 
464
- // Handle twStyle('...') call expressions
465
65
  CallExpression(path, state) {
466
- const node = path.node;
467
-
468
- // Check if the callee is a tracked twStyle import
469
- if (!t.isIdentifier(node.callee)) {
470
- return;
471
- }
472
-
473
- const calleeName = node.callee.name;
474
- if (!state.twImportNames.has(calleeName)) {
475
- return;
476
- }
477
-
478
- // Must have exactly one argument
479
- if (node.arguments.length !== 1) {
480
- if (process.env.NODE_ENV !== "production") {
481
- console.warn(
482
- `[react-native-tailwind] twStyle() expects exactly one argument at ${state.file.opts.filename ?? "unknown"}`,
483
- );
484
- }
485
- return;
486
- }
487
-
488
- const arg = node.arguments[0];
489
-
490
- // Only support static string literals
491
- if (!t.isStringLiteral(arg)) {
492
- if (process.env.NODE_ENV !== "production") {
493
- console.warn(
494
- `[react-native-tailwind] twStyle() only supports static string literals at ${state.file.opts.filename ?? "unknown"}. ` +
495
- `Use style prop for dynamic values.`,
496
- );
497
- }
498
- return;
499
- }
500
-
501
- const className = arg.value.trim();
502
- if (!className) {
503
- // Replace with undefined
504
- path.replaceWith(t.identifier("undefined"));
505
- return;
506
- }
507
-
508
- state.hasClassNames = true;
509
-
510
- // Process the className with modifiers
511
- processTwCall(
512
- className,
513
- path,
514
- state,
515
- parseClassName,
516
- generateStyleKey,
517
- splitModifierClasses,
518
- findComponentScope,
519
- t,
520
- );
66
+ callExpressionVisitor(path, state, t);
521
67
  },
522
68
 
523
69
  JSXAttribute(path, state) {
524
- const node = path.node;
525
-
526
- // Ensure we have a JSXIdentifier name (not JSXNamespacedName)
527
- if (!t.isJSXIdentifier(node.name)) {
528
- return;
529
- }
530
-
531
- const attributeName = node.name.name;
532
-
533
- // Only process configured className-like attributes
534
- if (!isAttributeSupported(attributeName, state.supportedAttributes, state.attributePatterns)) {
535
- return;
536
- }
537
-
538
- const value = node.value;
539
-
540
- // Determine target style prop based on attribute name
541
- const targetStyleProp = getTargetStyleProp(attributeName);
542
-
543
- /**
544
- * Process static className string (handles both direct StringLiteral and StringLiteral in JSXExpressionContainer)
545
- */
546
- const processStaticClassName = (className: string): boolean => {
547
- const trimmedClassName = className.trim();
548
-
549
- // Skip empty classNames
550
- if (!trimmedClassName) {
551
- path.remove();
552
- return true;
553
- }
554
-
555
- state.hasClassNames = true;
556
-
557
- // Check if className contains modifiers (active:, hover:, focus:, placeholder:, ios:, android:, web:, dark:, light:, scheme:)
558
- const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(trimmedClassName);
559
-
560
- // Expand scheme: modifiers into dark: and light: modifiers
561
- const modifierClasses: ParsedModifier[] = [];
562
- for (const modifier of rawModifierClasses) {
563
- if (isSchemeModifier(modifier.modifier)) {
564
- // Expand scheme: into dark: and light:
565
- const expanded = expandSchemeModifier(
566
- modifier,
567
- state.customTheme.colors ?? {},
568
- state.schemeModifierConfig.darkSuffix,
569
- state.schemeModifierConfig.lightSuffix,
570
- );
571
- modifierClasses.push(...expanded);
572
- } else {
573
- // Keep other modifiers as-is
574
- modifierClasses.push(modifier);
575
- }
576
- }
577
-
578
- // Separate modifiers by type
579
- const placeholderModifiers = modifierClasses.filter((m) => m.modifier === "placeholder");
580
- const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
581
- const colorSchemeModifiers = modifierClasses.filter((m) => isColorSchemeModifier(m.modifier));
582
- const stateModifiers = modifierClasses.filter(
583
- (m) => isStateModifier(m.modifier) && m.modifier !== "placeholder",
584
- );
585
-
586
- // Handle placeholder modifiers first (they generate placeholderTextColor prop, not style)
587
- if (placeholderModifiers.length > 0) {
588
- // Check if this is a TextInput component (placeholder only works on TextInput)
589
- const jsxOpeningElement = path.parent as BabelTypes.JSXOpeningElement;
590
- const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
591
-
592
- if (componentSupport?.supportedModifiers.includes("placeholder")) {
593
- const placeholderClasses = placeholderModifiers.map((m) => m.baseClass).join(" ");
594
- const placeholderColor = parsePlaceholderClasses(placeholderClasses, state.customTheme.colors);
595
-
596
- if (placeholderColor) {
597
- // Add or merge placeholderTextColor prop
598
- addOrMergePlaceholderTextColorProp(jsxOpeningElement, placeholderColor, t);
599
- }
600
- } else {
601
- // Warn if placeholder modifier used on non-TextInput element
602
- if (process.env.NODE_ENV !== "production") {
603
- console.warn(
604
- `[react-native-tailwind] placeholder: modifier can only be used on TextInput component at ${state.file.opts.filename ?? "unknown"}`,
605
- );
606
- }
607
- }
608
- }
609
-
610
- // Handle combination of modifiers
611
- const hasPlatformModifiers = platformModifiers.length > 0;
612
- const hasColorSchemeModifiers = colorSchemeModifiers.length > 0;
613
- const hasStateModifiers = stateModifiers.length > 0;
614
- const hasBaseClasses = baseClasses.length > 0;
615
-
616
- // If we have color scheme modifiers, we need to track the parent function component
617
- let componentScope: NodePath<BabelTypes.Function> | null = null;
618
- if (hasColorSchemeModifiers) {
619
- componentScope = findComponentScope(path, t);
620
- if (componentScope) {
621
- state.functionComponentsNeedingColorScheme.add(componentScope);
622
- } else {
623
- // Warn if color scheme modifiers used in invalid context (class component, nested function)
624
- if (process.env.NODE_ENV !== "production") {
625
- console.warn(
626
- `[react-native-tailwind] dark:/light: modifiers require a function component scope. ` +
627
- `Found in non-component context at ${state.file.opts.filename ?? "unknown"}. ` +
628
- `These modifiers are not supported in class components or nested callbacks.`,
629
- );
630
- }
631
- }
632
- }
633
-
634
- // If we have multiple modifier types, combine them in an array expression
635
- // For state modifiers, wrap in arrow function; for color scheme, they're just conditionals
636
- if (hasStateModifiers && (hasPlatformModifiers || hasColorSchemeModifiers)) {
637
- // Get the JSX opening element for component support checking
638
- const jsxOpeningElement = path.parent;
639
- const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
640
-
641
- if (componentSupport) {
642
- // Build style array: [baseStyle, Platform.select(...), colorSchemeConditionals, stateConditionals]
643
- const styleArrayElements: BabelTypes.Expression[] = [];
644
-
645
- // Add base classes
646
- if (hasBaseClasses) {
647
- const baseClassName = baseClasses.join(" ");
648
- const baseStyleObject = parseClassName(baseClassName, state.customTheme);
649
- const baseStyleKey = generateStyleKey(baseClassName);
650
- state.styleRegistry.set(baseStyleKey, baseStyleObject);
651
- styleArrayElements.push(
652
- t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
653
- );
654
- }
655
-
656
- // Add platform modifiers as Platform.select()
657
- if (hasPlatformModifiers) {
658
- const platformSelectExpression = processPlatformModifiers(
659
- platformModifiers,
660
- state,
661
- parseClassName,
662
- generateStyleKey,
663
- t,
664
- );
665
- styleArrayElements.push(platformSelectExpression);
666
- }
667
-
668
- // Add color scheme modifiers as conditionals (only if component scope exists)
669
- if (hasColorSchemeModifiers && componentScope) {
670
- const colorSchemeConditionals = processColorSchemeModifiers(
671
- colorSchemeModifiers,
672
- state,
673
- parseClassName,
674
- generateStyleKey,
675
- t,
676
- );
677
- styleArrayElements.push(...colorSchemeConditionals);
678
- }
679
-
680
- // Add state modifiers as conditionals
681
- // Group by modifier type
682
- const modifiersByType = new Map<StateModifierType, ParsedModifier[]>();
683
- for (const mod of stateModifiers) {
684
- const modType = mod.modifier as StateModifierType;
685
- if (!modifiersByType.has(modType)) {
686
- modifiersByType.set(modType, []);
687
- }
688
- modifiersByType.get(modType)?.push(mod);
689
- }
690
-
691
- // Build conditionals for each state modifier type
692
- for (const [modifierType, modifiers] of modifiersByType) {
693
- if (!componentSupport.supportedModifiers.includes(modifierType)) {
694
- continue; // Skip unsupported modifiers
695
- }
696
-
697
- const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
698
- const modifierStyleObject = parseClassName(modifierClassNames, state.customTheme);
699
- const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
700
- state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
701
-
702
- const stateProperty = getStatePropertyForModifier(modifierType);
703
- const conditionalExpression = t.logicalExpression(
704
- "&&",
705
- t.identifier(stateProperty),
706
- t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey)),
707
- );
708
-
709
- styleArrayElements.push(conditionalExpression);
710
- }
711
-
712
- // Wrap in arrow function for state support
713
- const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier))).filter((mod) =>
714
- componentSupport.supportedModifiers.includes(mod),
715
- );
716
- const styleArrayExpression = t.arrayExpression(styleArrayElements);
717
- const styleFunctionExpression = createStyleFunction(styleArrayExpression, usedModifiers, t);
718
-
719
- const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
720
- if (styleAttribute) {
721
- mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
722
- } else {
723
- replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
724
- }
725
- return true;
726
- } else {
727
- // Component doesn't support state modifiers, but we can still use platform modifiers
728
- // Fall through to platform-only handling
729
- }
730
- }
731
-
732
- // Handle platform and/or color scheme modifiers (no state modifiers)
733
- if ((hasPlatformModifiers || hasColorSchemeModifiers) && !hasStateModifiers) {
734
- // Build style array/expression: [baseStyle, Platform.select(...), colorSchemeConditionals]
735
- const styleExpressions: BabelTypes.Expression[] = [];
736
-
737
- // Add base classes
738
- if (hasBaseClasses) {
739
- const baseClassName = baseClasses.join(" ");
740
- const baseStyleObject = parseClassName(baseClassName, state.customTheme);
741
- const baseStyleKey = generateStyleKey(baseClassName);
742
- state.styleRegistry.set(baseStyleKey, baseStyleObject);
743
- styleExpressions.push(
744
- t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
745
- );
746
- }
747
-
748
- // Add platform modifiers as Platform.select()
749
- if (hasPlatformModifiers) {
750
- const platformSelectExpression = processPlatformModifiers(
751
- platformModifiers,
752
- state,
753
- parseClassName,
754
- generateStyleKey,
755
- t,
756
- );
757
- styleExpressions.push(platformSelectExpression);
758
- }
759
-
760
- // Add color scheme modifiers as conditionals (only if we have a valid component scope)
761
- if (hasColorSchemeModifiers && componentScope) {
762
- const colorSchemeConditionals = processColorSchemeModifiers(
763
- colorSchemeModifiers,
764
- state,
765
- parseClassName,
766
- generateStyleKey,
767
- t,
768
- );
769
- styleExpressions.push(...colorSchemeConditionals);
770
- }
771
-
772
- // Generate style attribute
773
- const styleExpression =
774
- styleExpressions.length === 1 ? styleExpressions[0] : t.arrayExpression(styleExpressions);
775
-
776
- const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
777
- if (styleAttribute) {
778
- // Merge with existing style attribute
779
- const existingStyle = styleAttribute.value;
780
- if (
781
- t.isJSXExpressionContainer(existingStyle) &&
782
- !t.isJSXEmptyExpression(existingStyle.expression)
783
- ) {
784
- const existing = existingStyle.expression;
785
- // Merge as array: [ourStyles, existingStyles]
786
- const mergedArray = t.isArrayExpression(existing)
787
- ? t.arrayExpression([styleExpression, ...existing.elements])
788
- : t.arrayExpression([styleExpression, existing]);
789
- styleAttribute.value = t.jsxExpressionContainer(mergedArray);
790
- } else {
791
- styleAttribute.value = t.jsxExpressionContainer(styleExpression);
792
- }
793
- path.remove();
794
- } else {
795
- // Replace className with style prop containing our expression
796
- path.node.name = t.jsxIdentifier(targetStyleProp);
797
- path.node.value = t.jsxExpressionContainer(styleExpression);
798
- }
799
- return true;
800
- }
801
-
802
- // If there are state modifiers (and no platform modifiers), check if this component supports them
803
- if (hasStateModifiers) {
804
- // Get the JSX opening element (the direct parent of the attribute)
805
- const jsxOpeningElement = path.parent;
806
- const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
807
-
808
- if (componentSupport) {
809
- // Get modifier types used in className
810
- const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier)));
811
-
812
- // Check if all modifiers are supported by this component
813
- const unsupportedModifiers = usedModifiers.filter(
814
- (mod) => !componentSupport.supportedModifiers.includes(mod),
815
- );
816
-
817
- if (unsupportedModifiers.length > 0) {
818
- // Warn about unsupported modifiers
819
- if (process.env.NODE_ENV !== "production") {
820
- console.warn(
821
- `[react-native-tailwind] Modifiers (${unsupportedModifiers.map((m) => `${m}:`).join(", ")}) are not supported on ${componentSupport.component} component at ${state.file.opts.filename ?? "unknown"}. ` +
822
- `Supported modifiers: ${componentSupport.supportedModifiers.join(", ")}`,
823
- );
824
- }
825
- // Filter out unsupported modifiers
826
- const supportedModifierClasses = stateModifiers.filter((m) =>
827
- componentSupport.supportedModifiers.includes(m.modifier),
828
- );
829
-
830
- // If no supported modifiers remain, fall through to normal processing
831
- if (supportedModifierClasses.length === 0) {
832
- // Continue to normal processing
833
- } else {
834
- // Process only supported modifiers
835
- const filteredClassName =
836
- baseClasses.join(" ") +
837
- " " +
838
- supportedModifierClasses.map((m) => `${m.modifier}:${m.baseClass}`).join(" ");
839
- const styleExpression = processStaticClassNameWithModifiers(
840
- filteredClassName.trim(),
841
- state,
842
- parseClassName,
843
- generateStyleKey,
844
- splitModifierClasses,
845
- t,
846
- );
847
- const modifierTypes = Array.from(new Set(supportedModifierClasses.map((m) => m.modifier)));
848
- const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
849
-
850
- const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
851
-
852
- if (styleAttribute) {
853
- mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
854
- } else {
855
- replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
856
- }
857
- return true;
858
- }
859
- } else {
860
- // All modifiers are supported - process normally
861
- const styleExpression = processStaticClassNameWithModifiers(
862
- trimmedClassName,
863
- state,
864
- parseClassName,
865
- generateStyleKey,
866
- splitModifierClasses,
867
- t,
868
- );
869
- const modifierTypes = usedModifiers;
870
- const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
871
-
872
- const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
873
-
874
- if (styleAttribute) {
875
- mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
876
- } else {
877
- replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
878
- }
879
- return true;
880
- }
881
- } else {
882
- // Component doesn't support any modifiers
883
- if (process.env.NODE_ENV !== "production") {
884
- const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier)));
885
- console.warn(
886
- `[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"}`,
887
- );
888
- }
889
- // Fall through to normal processing (ignore modifiers)
890
- }
891
- }
892
-
893
- // Normal processing without modifiers
894
- // Use baseClasses only (placeholder modifiers already handled separately)
895
- const classNameForStyle = baseClasses.join(" ");
896
- if (!classNameForStyle) {
897
- // No base classes, only had placeholder modifiers - just remove className
898
- path.remove();
899
- return true;
900
- }
901
-
902
- const styleObject = parseClassName(classNameForStyle, state.customTheme);
903
- const styleKey = generateStyleKey(classNameForStyle);
904
- state.styleRegistry.set(styleKey, styleObject);
905
-
906
- // Check if there's already a style prop on this element
907
- const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
908
-
909
- if (styleAttribute) {
910
- // Merge with existing style prop
911
- mergeStyleAttribute(path, styleAttribute, styleKey, state.stylesIdentifier, t);
912
- } else {
913
- // Replace className with style prop
914
- replaceWithStyleAttribute(path, styleKey, targetStyleProp, state.stylesIdentifier, t);
915
- }
916
- return true;
917
- };
918
-
919
- // Handle static string literals
920
- if (t.isStringLiteral(value)) {
921
- if (processStaticClassName(value.value)) {
922
- return;
923
- }
924
- }
925
-
926
- // Handle dynamic expressions (JSXExpressionContainer)
927
- if (t.isJSXExpressionContainer(value)) {
928
- const expression = value.expression;
929
-
930
- // Skip JSXEmptyExpression
931
- if (t.isJSXEmptyExpression(expression)) {
932
- return;
933
- }
934
-
935
- // Fast path: Support string literals wrapped in JSXExpressionContainer: className={"flex-row"}
936
- if (t.isStringLiteral(expression)) {
937
- if (processStaticClassName(expression.value)) {
938
- return;
939
- }
940
- }
941
-
942
- try {
943
- // Find component scope for color scheme modifiers
944
- const componentScope = findComponentScope(path, t);
945
-
946
- // Process dynamic expression with modifier support
947
- const result = processDynamicExpression(
948
- expression,
949
- state,
950
- parseClassName,
951
- generateStyleKey,
952
- splitModifierClasses,
953
- processPlatformModifiers,
954
- processColorSchemeModifiers,
955
- componentScope,
956
- isPlatformModifier as (modifier: unknown) => boolean,
957
- isColorSchemeModifier as (modifier: unknown) => boolean,
958
- isSchemeModifier as (modifier: unknown) => boolean,
959
- expandSchemeModifier,
960
- t,
961
- );
962
-
963
- if (result) {
964
- state.hasClassNames = true;
965
-
966
- // Check if there's already a style prop on this element
967
- const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
968
-
969
- if (styleAttribute) {
970
- // Merge with existing style prop
971
- mergeDynamicStyleAttribute(path, styleAttribute, result, t);
972
- } else {
973
- // Replace className with style prop
974
- replaceDynamicWithStyleAttribute(path, result, targetStyleProp, t);
975
- }
976
- return;
977
- }
978
- } catch (error) {
979
- // Fall through to warning
980
- if (process.env.NODE_ENV !== "production") {
981
- console.warn(
982
- `[react-native-tailwind] Failed to process dynamic ${attributeName} at ${state.file.opts.filename ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`,
983
- );
984
- }
985
- }
986
- }
987
-
988
- // Unsupported dynamic className - warn in development
989
- if (process.env.NODE_ENV !== "production") {
990
- const filename = state.file.opts.filename ?? "unknown";
991
- console.warn(
992
- `[react-native-tailwind] Dynamic ${attributeName} values are not fully supported at ${filename}. ` +
993
- `Use the ${targetStyleProp} prop for dynamic values.`,
994
- );
995
- }
70
+ jsxAttributeVisitor(path, state, t);
996
71
  },
997
72
  },
998
73
  };