@mgcrea/react-native-tailwind 0.9.1 → 0.11.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 (55) hide show
  1. package/README.md +386 -43
  2. package/dist/babel/config-loader.d.ts +12 -3
  3. package/dist/babel/config-loader.test.ts +154 -0
  4. package/dist/babel/config-loader.ts +41 -9
  5. package/dist/babel/index.cjs +592 -69
  6. package/dist/babel/plugin.d.ts +23 -1
  7. package/dist/babel/plugin.test.ts +331 -0
  8. package/dist/babel/plugin.ts +268 -37
  9. package/dist/babel/utils/colorSchemeModifierProcessing.d.ts +34 -0
  10. package/dist/babel/utils/colorSchemeModifierProcessing.ts +89 -0
  11. package/dist/babel/utils/dynamicProcessing.d.ts +34 -3
  12. package/dist/babel/utils/dynamicProcessing.ts +358 -39
  13. package/dist/babel/utils/modifierProcessing.d.ts +3 -3
  14. package/dist/babel/utils/modifierProcessing.ts +5 -5
  15. package/dist/babel/utils/platformModifierProcessing.d.ts +3 -3
  16. package/dist/babel/utils/platformModifierProcessing.ts +4 -4
  17. package/dist/babel/utils/styleInjection.d.ts +13 -0
  18. package/dist/babel/utils/styleInjection.ts +101 -0
  19. package/dist/babel/utils/styleTransforms.test.ts +56 -0
  20. package/dist/babel/utils/twProcessing.d.ts +5 -3
  21. package/dist/babel/utils/twProcessing.ts +27 -6
  22. package/dist/parser/index.d.ts +13 -6
  23. package/dist/parser/index.js +1 -1
  24. package/dist/parser/modifiers.d.ts +48 -2
  25. package/dist/parser/modifiers.js +1 -1
  26. package/dist/parser/modifiers.test.js +1 -1
  27. package/dist/parser/typography.d.ts +3 -1
  28. package/dist/parser/typography.js +1 -1
  29. package/dist/runtime.cjs +1 -1
  30. package/dist/runtime.cjs.map +3 -3
  31. package/dist/runtime.d.ts +8 -1
  32. package/dist/runtime.js +1 -1
  33. package/dist/runtime.js.map +3 -3
  34. package/dist/runtime.test.js +1 -1
  35. package/dist/types/config.d.ts +7 -0
  36. package/dist/types/config.js +0 -0
  37. package/package.json +3 -2
  38. package/src/babel/config-loader.test.ts +154 -0
  39. package/src/babel/config-loader.ts +41 -9
  40. package/src/babel/plugin.test.ts +331 -0
  41. package/src/babel/plugin.ts +268 -37
  42. package/src/babel/utils/colorSchemeModifierProcessing.ts +89 -0
  43. package/src/babel/utils/dynamicProcessing.ts +358 -39
  44. package/src/babel/utils/modifierProcessing.ts +5 -5
  45. package/src/babel/utils/platformModifierProcessing.ts +4 -4
  46. package/src/babel/utils/styleInjection.ts +101 -0
  47. package/src/babel/utils/styleTransforms.test.ts +56 -0
  48. package/src/babel/utils/twProcessing.ts +27 -6
  49. package/src/parser/index.ts +28 -9
  50. package/src/parser/modifiers.test.ts +151 -1
  51. package/src/parser/modifiers.ts +139 -4
  52. package/src/parser/typography.ts +14 -2
  53. package/src/runtime.test.ts +7 -7
  54. package/src/runtime.ts +37 -14
  55. package/src/types/config.ts +7 -0
@@ -7,7 +7,10 @@ import type { NodePath, PluginObj, PluginPass } from "@babel/core";
7
7
  import * as BabelTypes from "@babel/types";
8
8
  import type { ParsedModifier, StateModifierType } from "../parser/index.js";
9
9
  import {
10
+ expandSchemeModifier,
11
+ isColorSchemeModifier,
10
12
  isPlatformModifier,
13
+ isSchemeModifier,
11
14
  isStateModifier,
12
15
  parseClassName,
13
16
  parsePlaceholderClasses,
@@ -15,20 +18,29 @@ import {
15
18
  } from "../parser/index.js";
16
19
  import type { StyleObject } from "../types/core.js";
17
20
  import { generateStyleKey } from "../utils/styleKey.js";
18
- import { extractCustomColors } from "./config-loader.js";
21
+ import type { CustomTheme } from "./config-loader.js";
22
+ import { extractCustomTheme } from "./config-loader.js";
19
23
 
20
24
  // Import utility functions
25
+ import type { SchemeModifierConfig } from "../types/config.js";
21
26
  import {
22
27
  DEFAULT_CLASS_ATTRIBUTES,
23
28
  buildAttributeMatchers,
24
29
  getTargetStyleProp,
25
30
  isAttributeSupported,
26
31
  } from "./utils/attributeMatchers.js";
32
+ import { processColorSchemeModifiers } from "./utils/colorSchemeModifierProcessing.js";
27
33
  import { getComponentModifierSupport, getStatePropertyForModifier } from "./utils/componentSupport.js";
28
34
  import { processDynamicExpression } from "./utils/dynamicProcessing.js";
29
35
  import { createStyleFunction, processStaticClassNameWithModifiers } from "./utils/modifierProcessing.js";
30
36
  import { processPlatformModifiers } from "./utils/platformModifierProcessing.js";
31
- import { addPlatformImport, addStyleSheetImport, injectStylesAtTop } from "./utils/styleInjection.js";
37
+ import {
38
+ addColorSchemeImport,
39
+ addPlatformImport,
40
+ addStyleSheetImport,
41
+ injectColorSchemeHook,
42
+ injectStylesAtTop,
43
+ } from "./utils/styleInjection.js";
32
44
  import {
33
45
  addOrMergePlaceholderTextColorProp,
34
46
  findStyleAttribute,
@@ -61,6 +73,22 @@ export type PluginOptions = {
61
73
  * @default '_twStyles'
62
74
  */
63
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
+ };
64
92
  };
65
93
 
66
94
  type PluginState = PluginPass & {
@@ -69,7 +97,11 @@ type PluginState = PluginPass & {
69
97
  hasStyleSheetImport: boolean;
70
98
  hasPlatformImport: boolean;
71
99
  needsPlatformImport: boolean;
72
- customColors: Record<string, string>;
100
+ hasColorSchemeImport: boolean;
101
+ needsColorSchemeImport: boolean;
102
+ colorSchemeVariableName: string;
103
+ customTheme: CustomTheme;
104
+ schemeModifierConfig: SchemeModifierConfig;
73
105
  supportedAttributes: Set<string>;
74
106
  attributePatterns: RegExp[];
75
107
  stylesIdentifier: string;
@@ -78,11 +110,91 @@ type PluginState = PluginPass & {
78
110
  hasTwImport: boolean;
79
111
  // Track react-native import path for conditional StyleSheet/Platform injection
80
112
  reactNativeImportPath?: NodePath<BabelTypes.ImportDeclaration>;
113
+ // Track function components that need colorScheme hook injection
114
+ functionComponentsNeedingColorScheme: Set<NodePath<BabelTypes.Function>>;
81
115
  };
82
116
 
83
117
  // Default identifier for the generated StyleSheet constant
84
118
  const DEFAULT_STYLES_IDENTIFIER = "_twStyles";
85
119
 
120
+ /**
121
+ * Check if a function path represents a valid component scope for hook injection
122
+ * Valid scopes:
123
+ * - Top-level FunctionDeclaration
124
+ * - FunctionExpression/ArrowFunctionExpression in top-level VariableDeclarator (with PascalCase name)
125
+ * - NOT class methods, NOT nested functions, NOT inline callbacks
126
+ *
127
+ * @param functionPath - Path to the function to check
128
+ * @param t - Babel types
129
+ * @returns true if function is a valid component scope
130
+ */
131
+ function isComponentScope(functionPath: NodePath<BabelTypes.Function>, t: typeof BabelTypes): boolean {
132
+ const node = functionPath.node;
133
+ const parent = functionPath.parent;
134
+ const parentPath = functionPath.parentPath;
135
+
136
+ // Reject class methods (class components not supported for hooks)
137
+ if (t.isClassMethod(parent)) {
138
+ return false;
139
+ }
140
+
141
+ // Reject if inside a class body
142
+ if (functionPath.findParent((p) => t.isClassBody(p.node))) {
143
+ return false;
144
+ }
145
+
146
+ // Accept top-level FunctionDeclaration
147
+ if (t.isFunctionDeclaration(node)) {
148
+ // Check if it's at program level or in export
149
+ if (t.isProgram(parent) || t.isExportNamedDeclaration(parent) || t.isExportDefaultDeclaration(parent)) {
150
+ return true;
151
+ }
152
+ }
153
+
154
+ // Accept FunctionExpression/ArrowFunctionExpression in VariableDeclarator
155
+ if (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) {
156
+ if (t.isVariableDeclarator(parent)) {
157
+ // Check if it's at program level (via VariableDeclaration)
158
+ const varDeclarationPath = parentPath?.parentPath;
159
+ if (
160
+ varDeclarationPath &&
161
+ t.isVariableDeclaration(varDeclarationPath.node) &&
162
+ (t.isProgram(varDeclarationPath.parent) || t.isExportNamedDeclaration(varDeclarationPath.parent))
163
+ ) {
164
+ // Check for PascalCase naming (component convention)
165
+ if (t.isIdentifier(parent.id)) {
166
+ const name = parent.id.name;
167
+ return /^[A-Z]/.test(name); // Starts with uppercase
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ return false;
174
+ }
175
+
176
+ /**
177
+ * Find the nearest valid component scope for hook injection
178
+ * Climbs the AST from the current path to find a component-level function
179
+ *
180
+ * @param path - Starting path (e.g., JSXAttribute)
181
+ * @param t - Babel types
182
+ * @returns NodePath to component function, or null if not found
183
+ */
184
+ function findComponentScope(path: NodePath, t: typeof BabelTypes): NodePath<BabelTypes.Function> | null {
185
+ let current = path.getFunctionParent();
186
+
187
+ while (current) {
188
+ if (t.isFunction(current.node) && isComponentScope(current, t)) {
189
+ return current;
190
+ }
191
+ // Climb to next parent function
192
+ current = current.getFunctionParent();
193
+ }
194
+
195
+ return null;
196
+ }
197
+
86
198
  export default function reactNativeTailwindBabelPlugin(
87
199
  { types: t }: { types: typeof BabelTypes },
88
200
  options?: PluginOptions,
@@ -92,6 +204,12 @@ export default function reactNativeTailwindBabelPlugin(
92
204
  const { exactMatches, patterns } = buildAttributeMatchers(attributes);
93
205
  const stylesIdentifier = options?.stylesIdentifier ?? DEFAULT_STYLES_IDENTIFIER;
94
206
 
207
+ // Scheme modifier configuration from plugin options
208
+ const schemeModifierConfig = {
209
+ darkSuffix: options?.schemeModifier?.darkSuffix ?? "-dark",
210
+ lightSuffix: options?.schemeModifier?.lightSuffix ?? "-light",
211
+ };
212
+
95
213
  return {
96
214
  name: "react-native-tailwind",
97
215
 
@@ -104,14 +222,21 @@ export default function reactNativeTailwindBabelPlugin(
104
222
  state.hasStyleSheetImport = false;
105
223
  state.hasPlatformImport = false;
106
224
  state.needsPlatformImport = false;
225
+ state.hasColorSchemeImport = false;
226
+ state.needsColorSchemeImport = false;
227
+ state.colorSchemeVariableName = "_twColorScheme";
107
228
  state.supportedAttributes = exactMatches;
108
229
  state.attributePatterns = patterns;
109
230
  state.stylesIdentifier = stylesIdentifier;
110
231
  state.twImportNames = new Set();
111
232
  state.hasTwImport = false;
233
+ state.functionComponentsNeedingColorScheme = new Set();
234
+
235
+ // Load custom theme from tailwind.config.*
236
+ state.customTheme = extractCustomTheme(state.file.opts.filename ?? "");
112
237
 
113
- // Load custom colors from tailwind.config.*
114
- state.customColors = extractCustomColors(state.file.opts.filename ?? "");
238
+ // Use scheme modifier config from plugin options
239
+ state.schemeModifierConfig = schemeModifierConfig;
115
240
  },
116
241
 
117
242
  exit(path, state) {
@@ -135,6 +260,18 @@ export default function reactNativeTailwindBabelPlugin(
135
260
  addPlatformImport(path, t);
136
261
  }
137
262
 
263
+ // Add useColorScheme import if color scheme modifiers were used and not already present
264
+ if (state.needsColorSchemeImport && !state.hasColorSchemeImport) {
265
+ addColorSchemeImport(path, t);
266
+ }
267
+
268
+ // Inject useColorScheme hook in function components that need it
269
+ if (state.needsColorSchemeImport) {
270
+ for (const functionPath of state.functionComponentsNeedingColorScheme) {
271
+ injectColorSchemeHook(functionPath, state.colorSchemeVariableName, t);
272
+ }
273
+ }
274
+
138
275
  // Generate and inject StyleSheet.create at the beginning of the file (after imports)
139
276
  // This ensures _twStyles is defined before any code that references it
140
277
  injectStylesAtTop(path, state.styleRegistry, state.stylesIdentifier, t);
@@ -163,6 +300,13 @@ export default function reactNativeTailwindBabelPlugin(
163
300
  return false;
164
301
  });
165
302
 
303
+ const hasUseColorScheme = specifiers.some((spec) => {
304
+ if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
305
+ return spec.imported.name === "useColorScheme";
306
+ }
307
+ return false;
308
+ });
309
+
166
310
  // Only track if imports exist - don't mutate yet
167
311
  // Actual import injection happens in Program.exit only if needed
168
312
  if (hasStyleSheet) {
@@ -173,6 +317,10 @@ export default function reactNativeTailwindBabelPlugin(
173
317
  state.hasPlatformImport = true;
174
318
  }
175
319
 
320
+ if (hasUseColorScheme) {
321
+ state.hasColorSchemeImport = true;
322
+ }
323
+
176
324
  // Store reference to the react-native import for later modification if needed
177
325
  state.reactNativeImportPath = path;
178
326
  }
@@ -323,12 +471,31 @@ export default function reactNativeTailwindBabelPlugin(
323
471
 
324
472
  state.hasClassNames = true;
325
473
 
326
- // Check if className contains modifiers (active:, hover:, focus:, placeholder:, ios:, android:, web:)
327
- const { baseClasses, modifierClasses } = splitModifierClasses(className);
474
+ // Check if className contains modifiers (active:, hover:, focus:, placeholder:, ios:, android:, web:, dark:, light:, scheme:)
475
+ const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(className);
476
+
477
+ // Expand scheme: modifiers into dark: and light: modifiers
478
+ const modifierClasses: ParsedModifier[] = [];
479
+ for (const modifier of rawModifierClasses) {
480
+ if (isSchemeModifier(modifier.modifier)) {
481
+ // Expand scheme: into dark: and light:
482
+ const expanded = expandSchemeModifier(
483
+ modifier,
484
+ state.customTheme.colors ?? {},
485
+ state.schemeModifierConfig.darkSuffix,
486
+ state.schemeModifierConfig.lightSuffix,
487
+ );
488
+ modifierClasses.push(...expanded);
489
+ } else {
490
+ // Keep other modifiers as-is
491
+ modifierClasses.push(modifier);
492
+ }
493
+ }
328
494
 
329
495
  // Separate modifiers by type
330
496
  const placeholderModifiers = modifierClasses.filter((m) => m.modifier === "placeholder");
331
497
  const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
498
+ const colorSchemeModifiers = modifierClasses.filter((m) => isColorSchemeModifier(m.modifier));
332
499
  const stateModifiers = modifierClasses.filter(
333
500
  (m) => isStateModifier(m.modifier) && m.modifier !== "placeholder",
334
501
  );
@@ -341,7 +508,7 @@ export default function reactNativeTailwindBabelPlugin(
341
508
 
342
509
  if (componentSupport?.supportedModifiers.includes("placeholder")) {
343
510
  const placeholderClasses = placeholderModifiers.map((m) => m.baseClass).join(" ");
344
- const placeholderColor = parsePlaceholderClasses(placeholderClasses, state.customColors);
511
+ const placeholderColor = parsePlaceholderClasses(placeholderClasses, state.customTheme.colors);
345
512
 
346
513
  if (placeholderColor) {
347
514
  // Add or merge placeholderTextColor prop
@@ -359,24 +526,43 @@ export default function reactNativeTailwindBabelPlugin(
359
526
 
360
527
  // Handle combination of modifiers
361
528
  const hasPlatformModifiers = platformModifiers.length > 0;
529
+ const hasColorSchemeModifiers = colorSchemeModifiers.length > 0;
362
530
  const hasStateModifiers = stateModifiers.length > 0;
363
531
  const hasBaseClasses = baseClasses.length > 0;
364
532
 
365
- // If we have both state and platform modifiers, or platform modifiers with complex state,
366
- // we need to combine them in an array expression wrapped in an arrow function
367
- if (hasStateModifiers && hasPlatformModifiers) {
533
+ // If we have color scheme modifiers, we need to track the parent function component
534
+ let componentScope: NodePath<BabelTypes.Function> | null = null;
535
+ if (hasColorSchemeModifiers) {
536
+ componentScope = findComponentScope(path, t);
537
+ if (componentScope) {
538
+ state.functionComponentsNeedingColorScheme.add(componentScope);
539
+ } else {
540
+ // Warn if color scheme modifiers used in invalid context (class component, nested function)
541
+ if (process.env.NODE_ENV !== "production") {
542
+ console.warn(
543
+ `[react-native-tailwind] dark:/light: modifiers require a function component scope. ` +
544
+ `Found in non-component context at ${state.file.opts.filename ?? "unknown"}. ` +
545
+ `These modifiers are not supported in class components or nested callbacks.`,
546
+ );
547
+ }
548
+ }
549
+ }
550
+
551
+ // If we have multiple modifier types, combine them in an array expression
552
+ // For state modifiers, wrap in arrow function; for color scheme, they're just conditionals
553
+ if (hasStateModifiers && (hasPlatformModifiers || hasColorSchemeModifiers)) {
368
554
  // Get the JSX opening element for component support checking
369
555
  const jsxOpeningElement = path.parent;
370
556
  const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
371
557
 
372
558
  if (componentSupport) {
373
- // Build style array: [baseStyle, Platform.select(...), stateConditionals]
559
+ // Build style array: [baseStyle, Platform.select(...), colorSchemeConditionals, stateConditionals]
374
560
  const styleArrayElements: BabelTypes.Expression[] = [];
375
561
 
376
562
  // Add base classes
377
563
  if (hasBaseClasses) {
378
564
  const baseClassName = baseClasses.join(" ");
379
- const baseStyleObject = parseClassName(baseClassName, state.customColors);
565
+ const baseStyleObject = parseClassName(baseClassName, state.customTheme);
380
566
  const baseStyleKey = generateStyleKey(baseClassName);
381
567
  state.styleRegistry.set(baseStyleKey, baseStyleObject);
382
568
  styleArrayElements.push(
@@ -385,14 +571,28 @@ export default function reactNativeTailwindBabelPlugin(
385
571
  }
386
572
 
387
573
  // Add platform modifiers as Platform.select()
388
- const platformSelectExpression = processPlatformModifiers(
389
- platformModifiers,
390
- state,
391
- parseClassName,
392
- generateStyleKey,
393
- t,
394
- );
395
- styleArrayElements.push(platformSelectExpression);
574
+ if (hasPlatformModifiers) {
575
+ const platformSelectExpression = processPlatformModifiers(
576
+ platformModifiers,
577
+ state,
578
+ parseClassName,
579
+ generateStyleKey,
580
+ t,
581
+ );
582
+ styleArrayElements.push(platformSelectExpression);
583
+ }
584
+
585
+ // Add color scheme modifiers as conditionals (only if component scope exists)
586
+ if (hasColorSchemeModifiers && componentScope) {
587
+ const colorSchemeConditionals = processColorSchemeModifiers(
588
+ colorSchemeModifiers,
589
+ state,
590
+ parseClassName,
591
+ generateStyleKey,
592
+ t,
593
+ );
594
+ styleArrayElements.push(...colorSchemeConditionals);
595
+ }
396
596
 
397
597
  // Add state modifiers as conditionals
398
598
  // Group by modifier type
@@ -412,7 +612,7 @@ export default function reactNativeTailwindBabelPlugin(
412
612
  }
413
613
 
414
614
  const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
415
- const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
615
+ const modifierStyleObject = parseClassName(modifierClassNames, state.customTheme);
416
616
  const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
417
617
  state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
418
618
 
@@ -446,15 +646,15 @@ export default function reactNativeTailwindBabelPlugin(
446
646
  }
447
647
  }
448
648
 
449
- // Handle platform-only modifiers (no state modifiers)
450
- if (hasPlatformModifiers && !hasStateModifiers) {
451
- // Build style array/expression: [baseStyle, Platform.select(...)]
649
+ // Handle platform and/or color scheme modifiers (no state modifiers)
650
+ if ((hasPlatformModifiers || hasColorSchemeModifiers) && !hasStateModifiers) {
651
+ // Build style array/expression: [baseStyle, Platform.select(...), colorSchemeConditionals]
452
652
  const styleExpressions: BabelTypes.Expression[] = [];
453
653
 
454
654
  // Add base classes
455
655
  if (hasBaseClasses) {
456
656
  const baseClassName = baseClasses.join(" ");
457
- const baseStyleObject = parseClassName(baseClassName, state.customColors);
657
+ const baseStyleObject = parseClassName(baseClassName, state.customTheme);
458
658
  const baseStyleKey = generateStyleKey(baseClassName);
459
659
  state.styleRegistry.set(baseStyleKey, baseStyleObject);
460
660
  styleExpressions.push(
@@ -463,14 +663,28 @@ export default function reactNativeTailwindBabelPlugin(
463
663
  }
464
664
 
465
665
  // Add platform modifiers as Platform.select()
466
- const platformSelectExpression = processPlatformModifiers(
467
- platformModifiers,
468
- state,
469
- parseClassName,
470
- generateStyleKey,
471
- t,
472
- );
473
- styleExpressions.push(platformSelectExpression);
666
+ if (hasPlatformModifiers) {
667
+ const platformSelectExpression = processPlatformModifiers(
668
+ platformModifiers,
669
+ state,
670
+ parseClassName,
671
+ generateStyleKey,
672
+ t,
673
+ );
674
+ styleExpressions.push(platformSelectExpression);
675
+ }
676
+
677
+ // Add color scheme modifiers as conditionals (only if we have a valid component scope)
678
+ if (hasColorSchemeModifiers && componentScope) {
679
+ const colorSchemeConditionals = processColorSchemeModifiers(
680
+ colorSchemeModifiers,
681
+ state,
682
+ parseClassName,
683
+ generateStyleKey,
684
+ t,
685
+ );
686
+ styleExpressions.push(...colorSchemeConditionals);
687
+ }
474
688
 
475
689
  // Generate style attribute
476
690
  const styleExpression =
@@ -602,7 +816,7 @@ export default function reactNativeTailwindBabelPlugin(
602
816
  return;
603
817
  }
604
818
 
605
- const styleObject = parseClassName(classNameForStyle, state.customColors);
819
+ const styleObject = parseClassName(classNameForStyle, state.customTheme);
606
820
  const styleKey = generateStyleKey(classNameForStyle);
607
821
  state.styleRegistry.set(styleKey, styleObject);
608
822
 
@@ -629,8 +843,25 @@ export default function reactNativeTailwindBabelPlugin(
629
843
  }
630
844
 
631
845
  try {
632
- // Process dynamic expression
633
- const result = processDynamicExpression(expression, state, parseClassName, generateStyleKey, t);
846
+ // Find component scope for color scheme modifiers
847
+ const componentScope = findComponentScope(path, t);
848
+
849
+ // Process dynamic expression with modifier support
850
+ const result = processDynamicExpression(
851
+ expression,
852
+ state,
853
+ parseClassName,
854
+ generateStyleKey,
855
+ splitModifierClasses,
856
+ processPlatformModifiers,
857
+ processColorSchemeModifiers,
858
+ componentScope,
859
+ isPlatformModifier as (modifier: unknown) => boolean,
860
+ isColorSchemeModifier as (modifier: unknown) => boolean,
861
+ isSchemeModifier as (modifier: unknown) => boolean,
862
+ expandSchemeModifier,
863
+ t,
864
+ );
634
865
 
635
866
  if (result) {
636
867
  state.hasClassNames = true;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Utility functions for processing color scheme modifiers (dark:, light:)
3
+ */
4
+
5
+ import type * as BabelTypes from "@babel/types";
6
+ import type { ColorSchemeModifierType, CustomTheme, ParsedModifier } from "../../parser/index.js";
7
+ import type { StyleObject } from "../../types/core.js";
8
+
9
+ /**
10
+ * Plugin state interface (subset needed for color scheme modifier processing)
11
+ */
12
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
13
+ export interface ColorSchemeModifierProcessingState {
14
+ styleRegistry: Map<string, StyleObject>;
15
+ customTheme: CustomTheme;
16
+ stylesIdentifier: string;
17
+ needsColorSchemeImport: boolean;
18
+ colorSchemeVariableName: string;
19
+ }
20
+
21
+ /**
22
+ * Process color scheme modifiers and generate conditional style expressions
23
+ *
24
+ * @param colorSchemeModifiers - Array of parsed color scheme modifiers
25
+ * @param state - Plugin state
26
+ * @param parseClassName - Function to parse class names into style objects
27
+ * @param generateStyleKey - Function to generate unique style keys
28
+ * @param t - Babel types
29
+ * @returns Array of AST nodes for conditional expressions
30
+ *
31
+ * @example
32
+ * Input: [{ modifier: "dark", baseClass: "bg-gray-900" }, { modifier: "light", baseClass: "bg-white" }]
33
+ * Output: [
34
+ * _twColorScheme === 'dark' && styles._dark_bg_gray_900,
35
+ * _twColorScheme === 'light' && styles._light_bg_white
36
+ * ]
37
+ */
38
+ export function processColorSchemeModifiers(
39
+ colorSchemeModifiers: ParsedModifier[],
40
+ state: ColorSchemeModifierProcessingState,
41
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
42
+ generateStyleKey: (className: string) => string,
43
+ t: typeof BabelTypes,
44
+ ): BabelTypes.Expression[] {
45
+ // Mark that we need useColorScheme import and hook injection
46
+ state.needsColorSchemeImport = true;
47
+
48
+ // Group modifiers by color scheme (dark, light)
49
+ const modifiersByScheme = new Map<ColorSchemeModifierType, ParsedModifier[]>();
50
+
51
+ for (const mod of colorSchemeModifiers) {
52
+ const scheme = mod.modifier as ColorSchemeModifierType;
53
+ if (!modifiersByScheme.has(scheme)) {
54
+ modifiersByScheme.set(scheme, []);
55
+ }
56
+ const schemeGroup = modifiersByScheme.get(scheme);
57
+ if (schemeGroup) {
58
+ schemeGroup.push(mod);
59
+ }
60
+ }
61
+
62
+ // Build conditional expressions for each color scheme
63
+ const conditionalExpressions: BabelTypes.Expression[] = [];
64
+
65
+ for (const [scheme, modifiers] of modifiersByScheme) {
66
+ // Parse all classes for this color scheme together
67
+ const classNames = modifiers.map((m) => m.baseClass).join(" ");
68
+ const styleObject = parseClassName(classNames, state.customTheme);
69
+ const styleKey = generateStyleKey(`${scheme}_${classNames}`);
70
+
71
+ // Register style in the registry
72
+ state.styleRegistry.set(styleKey, styleObject);
73
+
74
+ // Create conditional: _twColorScheme === 'dark' && styles._dark_bg_gray_900
75
+ const colorSchemeCheck = t.binaryExpression(
76
+ "===",
77
+ t.identifier(state.colorSchemeVariableName),
78
+ t.stringLiteral(scheme),
79
+ );
80
+
81
+ const styleReference = t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey));
82
+
83
+ const conditionalExpression = t.logicalExpression("&&", colorSchemeCheck, styleReference);
84
+
85
+ conditionalExpressions.push(conditionalExpression);
86
+ }
87
+
88
+ return conditionalExpressions;
89
+ }