@mgcrea/react-native-tailwind 0.8.1 → 0.9.1

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 (71) hide show
  1. package/README.md +152 -0
  2. package/dist/babel/config-loader.ts +2 -0
  3. package/dist/babel/index.cjs +205 -17
  4. package/dist/babel/plugin.d.ts +4 -1
  5. package/dist/babel/plugin.test.ts +327 -0
  6. package/dist/babel/plugin.ts +194 -14
  7. package/dist/babel/utils/platformModifierProcessing.d.ts +30 -0
  8. package/dist/babel/utils/platformModifierProcessing.ts +80 -0
  9. package/dist/babel/utils/styleInjection.d.ts +5 -1
  10. package/dist/babel/utils/styleInjection.ts +52 -7
  11. package/dist/babel/utils/styleTransforms.ts +1 -0
  12. package/dist/parser/aspectRatio.js +1 -1
  13. package/dist/parser/aspectRatio.test.js +1 -1
  14. package/dist/parser/index.d.ts +2 -2
  15. package/dist/parser/index.js +1 -1
  16. package/dist/parser/modifiers.d.ts +20 -2
  17. package/dist/parser/modifiers.js +1 -1
  18. package/dist/parser/spacing.d.ts +1 -1
  19. package/dist/parser/spacing.js +1 -1
  20. package/dist/parser/spacing.test.js +1 -1
  21. package/dist/runtime.cjs +1 -1
  22. package/dist/runtime.cjs.map +4 -4
  23. package/dist/runtime.js +1 -1
  24. package/dist/runtime.js.map +4 -4
  25. package/dist/runtime.test.js +1 -1
  26. package/dist/stubs/tw.test.js +1 -0
  27. package/package.json +7 -7
  28. package/src/babel/config-loader.ts +2 -0
  29. package/src/babel/plugin.test.ts +327 -0
  30. package/src/babel/plugin.ts +194 -14
  31. package/src/babel/utils/platformModifierProcessing.ts +80 -0
  32. package/src/babel/utils/styleInjection.ts +52 -7
  33. package/src/babel/utils/styleTransforms.ts +1 -0
  34. package/src/parser/aspectRatio.test.ts +25 -2
  35. package/src/parser/aspectRatio.ts +4 -3
  36. package/src/parser/borders.ts +2 -0
  37. package/src/parser/colors.ts +2 -0
  38. package/src/parser/index.ts +9 -2
  39. package/src/parser/layout.ts +2 -0
  40. package/src/parser/modifiers.ts +38 -4
  41. package/src/parser/placeholder.ts +1 -0
  42. package/src/parser/sizing.ts +1 -0
  43. package/src/parser/spacing.test.ts +63 -0
  44. package/src/parser/spacing.ts +11 -6
  45. package/src/parser/transforms.ts +5 -0
  46. package/src/parser/typography.ts +2 -0
  47. package/src/runtime.test.ts +27 -0
  48. package/src/runtime.ts +2 -1
  49. package/src/stubs/tw.test.ts +27 -0
  50. package/dist/babel/index.test.ts +0 -481
  51. package/dist/config/palettes.d.ts +0 -302
  52. package/dist/config/palettes.js +0 -1
  53. package/dist/parser/__snapshots__/aspectRatio.test.js.snap +0 -9
  54. package/dist/parser/__snapshots__/borders.test.js.snap +0 -23
  55. package/dist/parser/__snapshots__/colors.test.js.snap +0 -251
  56. package/dist/parser/__snapshots__/shadows.test.js.snap +0 -76
  57. package/dist/parser/__snapshots__/sizing.test.js.snap +0 -61
  58. package/dist/parser/__snapshots__/spacing.test.js.snap +0 -40
  59. package/dist/parser/__snapshots__/transforms.test.js.snap +0 -58
  60. package/dist/parser/__snapshots__/typography.test.js.snap +0 -30
  61. package/dist/parser/aspectRatio.test.d.ts +0 -1
  62. package/dist/parser/borders.test.d.ts +0 -1
  63. package/dist/parser/colors.test.d.ts +0 -1
  64. package/dist/parser/layout.test.d.ts +0 -1
  65. package/dist/parser/modifiers.test.d.ts +0 -1
  66. package/dist/parser/shadows.test.d.ts +0 -1
  67. package/dist/parser/sizing.test.d.ts +0 -1
  68. package/dist/parser/spacing.test.d.ts +0 -1
  69. package/dist/parser/typography.test.d.ts +0 -1
  70. package/dist/types.d.ts +0 -42
  71. package/dist/types.js +0 -1
@@ -480,3 +480,330 @@ describe("Babel plugin - placeholder: modifier transformation", () => {
480
480
  consoleSpy.mockRestore();
481
481
  });
482
482
  });
483
+
484
+ describe("Babel plugin - platform modifier transformation", () => {
485
+ it("should transform platform modifiers to Platform.select()", () => {
486
+ const input = `
487
+ import React from 'react';
488
+ import { View } from 'react-native';
489
+
490
+ export function Component() {
491
+ return (
492
+ <View className="p-4 ios:p-6 android:p-8" />
493
+ );
494
+ }
495
+ `;
496
+
497
+ const output = transform(input, undefined, true);
498
+
499
+ // Should import Platform from react-native
500
+ expect(output).toContain("Platform");
501
+ expect(output).toMatch(/import.*Platform.*from ['"]react-native['"]/);
502
+
503
+ // Should generate Platform.select()
504
+ expect(output).toContain("Platform.select");
505
+
506
+ // Should have base padding style
507
+ expect(output).toContain("_p_4");
508
+
509
+ // Should have iOS and Android specific styles
510
+ expect(output).toContain("_ios_p_6");
511
+ expect(output).toContain("_android_p_8");
512
+
513
+ // Should have correct style values in StyleSheet.create
514
+ expect(output).toMatch(/padding:\s*16/); // p-4
515
+ expect(output).toMatch(/padding:\s*24/); // p-6 (ios)
516
+ expect(output).toMatch(/padding:\s*32/); // p-8 (android)
517
+ });
518
+
519
+ it("should support multiple platform modifiers on same element", () => {
520
+ const input = `
521
+ import React from 'react';
522
+ import { View } from 'react-native';
523
+
524
+ export function Component() {
525
+ return (
526
+ <View className="bg-white ios:bg-blue-50 android:bg-green-50 p-4 ios:p-6 android:p-8" />
527
+ );
528
+ }
529
+ `;
530
+
531
+ const output = transform(input, undefined, true);
532
+
533
+ // Should have Platform import
534
+ expect(output).toContain("Platform");
535
+
536
+ // Should have base styles (combined key)
537
+ expect(output).toContain("_bg_white_p_4");
538
+
539
+ // Should have iOS specific styles (combined key for multiple ios: modifiers)
540
+ expect(output).toContain("_ios_bg_blue_50_p_6");
541
+
542
+ // Should have Android specific styles (combined key for multiple android: modifiers)
543
+ expect(output).toContain("_android_bg_green_50_p_8");
544
+
545
+ // Should contain Platform.select with both platforms
546
+ expect(output).toMatch(/Platform\.select\s*\(\s*\{[\s\S]*ios:/);
547
+ expect(output).toMatch(/Platform\.select\s*\(\s*\{[\s\S]*android:/);
548
+ });
549
+
550
+ it("should support web platform modifier", () => {
551
+ const input = `
552
+ import React from 'react';
553
+ import { View } from 'react-native';
554
+
555
+ export function Component() {
556
+ return (
557
+ <View className="p-4 web:p-2" />
558
+ );
559
+ }
560
+ `;
561
+
562
+ const output = transform(input, undefined, true);
563
+
564
+ // Should have Platform.select with web
565
+ expect(output).toContain("Platform.select");
566
+ expect(output).toContain("web:");
567
+ expect(output).toContain("_web_p_2");
568
+ });
569
+
570
+ it("should work with platform modifiers on all components", () => {
571
+ const input = `
572
+ import React from 'react';
573
+ import { View, Text, ScrollView } from 'react-native';
574
+
575
+ export function Component() {
576
+ return (
577
+ <View className="ios:bg-blue-500 android:bg-green-500">
578
+ <Text className="ios:text-lg android:text-xl">Platform text</Text>
579
+ <ScrollView contentContainerClassName="ios:p-4 android:p-8" />
580
+ </View>
581
+ );
582
+ }
583
+ `;
584
+
585
+ const output = transform(input, undefined, true);
586
+
587
+ // Should work on View - check for Platform.select separately (not checking style= format)
588
+ expect(output).toContain("Platform.select");
589
+
590
+ // Should work on Text
591
+ expect(output).toContain("_ios_text_lg");
592
+ expect(output).toContain("_android_text_xl");
593
+
594
+ // Should work on ScrollView contentContainerStyle
595
+ expect(output).toContain("contentContainerStyle");
596
+ });
597
+
598
+ it("should combine platform modifiers with state modifiers", () => {
599
+ const input = `
600
+ import React from 'react';
601
+ import { Pressable, Text } from 'react-native';
602
+
603
+ export function Component() {
604
+ return (
605
+ <Pressable className="bg-blue-500 active:bg-blue-700 ios:shadow-md android:shadow-sm p-4">
606
+ <Text className="text-white">Button</Text>
607
+ </Pressable>
608
+ );
609
+ }
610
+ `;
611
+
612
+ const output = transform(input, undefined, true);
613
+
614
+ // Should have Platform.select for platform modifiers
615
+ expect(output).toContain("Platform.select");
616
+ expect(output).toContain("_ios_shadow_md");
617
+ expect(output).toContain("_android_shadow_sm");
618
+
619
+ // Should have state modifier function for active
620
+ expect(output).toMatch(/\(\s*\{\s*pressed\s*\}\s*\)\s*=>/);
621
+ expect(output).toContain("pressed");
622
+ expect(output).toContain("_active_bg_blue_700");
623
+
624
+ // Should have base styles
625
+ expect(output).toContain("_bg_blue_500");
626
+ expect(output).toContain("_p_4");
627
+ });
628
+
629
+ it("should handle platform-specific colors", () => {
630
+ const input = `
631
+ import React from 'react';
632
+ import { View, Text } from 'react-native';
633
+
634
+ export function Component() {
635
+ return (
636
+ <View className="bg-gray-100 ios:bg-blue-50 android:bg-green-50">
637
+ <Text className="text-gray-900 ios:text-blue-900 android:text-green-900">
638
+ Platform colors
639
+ </Text>
640
+ </View>
641
+ );
642
+ }
643
+ `;
644
+
645
+ const output = transform(input, undefined, true);
646
+
647
+ // Should have color values in StyleSheet
648
+ expect(output).toMatch(/#[0-9A-F]{6}/i); // Hex color format
649
+
650
+ // Should have platform-specific color classes
651
+ expect(output).toContain("_ios_text_blue_900");
652
+ expect(output).toContain("_android_text_green_900");
653
+ });
654
+
655
+ it("should only add Platform import once when needed", () => {
656
+ const input = `
657
+ import React from 'react';
658
+ import { View } from 'react-native';
659
+
660
+ export function Component() {
661
+ return (
662
+ <>
663
+ <View className="ios:p-4" />
664
+ <View className="android:p-8" />
665
+ <View className="ios:bg-blue-500" />
666
+ </>
667
+ );
668
+ }
669
+ `;
670
+
671
+ const output = transform(input, undefined, true);
672
+
673
+ // Should have Platform import
674
+ expect(output).toContain("Platform");
675
+
676
+ // Count how many times Platform is imported (should be once)
677
+ const platformImports = output.match(/import.*Platform.*from ['"]react-native['"]/g);
678
+ expect(platformImports).toHaveLength(1);
679
+ });
680
+
681
+ it("should merge with existing Platform import", () => {
682
+ const input = `
683
+ import React from 'react';
684
+ import { View, Platform } from 'react-native';
685
+
686
+ export function Component() {
687
+ return <View className="ios:p-4 android:p-8" />;
688
+ }
689
+ `;
690
+
691
+ const output = transform(input, undefined, true);
692
+
693
+ // Should still use Platform.select
694
+ expect(output).toContain("Platform.select");
695
+
696
+ // Should not duplicate Platform import - Platform appears in import and Platform.select calls
697
+ expect(output).toMatch(/Platform.*react-native/);
698
+ });
699
+
700
+ it("should handle platform modifiers without base classes", () => {
701
+ const input = `
702
+ import React from 'react';
703
+ import { View } from 'react-native';
704
+
705
+ export function Component() {
706
+ return <View className="ios:p-6 android:p-8" />;
707
+ }
708
+ `;
709
+
710
+ const output = transform(input, undefined, true);
711
+
712
+ // Should only have Platform.select, no base style
713
+ expect(output).toContain("Platform.select");
714
+ expect(output).toContain("_ios_p_6");
715
+ expect(output).toContain("_android_p_8");
716
+
717
+ // Should not have generic padding without platform prefix
718
+ // Check that non-platform-prefixed style keys don't exist
719
+ expect(output).not.toMatch(/(?<!_ios|_android|_web)_p_4:/);
720
+ expect(output).not.toMatch(/(?<!_ios|_android|_web)_p_6:/);
721
+ expect(output).not.toMatch(/(?<!_ios|_android|_web)_p_8:/);
722
+ });
723
+ });
724
+
725
+ describe("Babel plugin - import injection", () => {
726
+ it("should not add StyleSheet import to files without className usage", () => {
727
+ const input = `
728
+ import { View, Text } from 'react-native';
729
+
730
+ function MyComponent() {
731
+ return <View><Text>Hello</Text></View>;
732
+ }
733
+ `;
734
+
735
+ const output = transform(input, undefined, true);
736
+
737
+ // Should not mutate the import by adding StyleSheet
738
+ // Count occurrences of "StyleSheet" in output
739
+ const styleSheetCount = (output.match(/StyleSheet/g) ?? []).length;
740
+ expect(styleSheetCount).toBe(0);
741
+
742
+ // Should not have _twStyles definition
743
+ expect(output).not.toContain("_twStyles");
744
+ expect(output).not.toContain("StyleSheet.create");
745
+
746
+ // Original imports should remain unchanged
747
+ expect(output).toContain("View");
748
+ expect(output).toContain("Text");
749
+ });
750
+
751
+ it("should add StyleSheet import only when className is used", () => {
752
+ const input = `
753
+ import { View } from 'react-native';
754
+
755
+ function MyComponent() {
756
+ return <View className="m-4 p-2" />;
757
+ }
758
+ `;
759
+
760
+ const output = transform(input, undefined, true);
761
+
762
+ // Should have StyleSheet import (both single and double quotes)
763
+ expect(output).toMatch(/import.*StyleSheet.*from ['"]react-native['"]|require\(['"]react-native['"]\)/);
764
+
765
+ // Should have _twStyles definition
766
+ expect(output).toContain("_twStyles");
767
+ expect(output).toContain("StyleSheet.create");
768
+ });
769
+
770
+ it("should add Platform import only when platform modifiers are used", () => {
771
+ const input = `
772
+ import { View } from 'react-native';
773
+
774
+ function MyComponent() {
775
+ return <View className="ios:m-4 android:m-2" />;
776
+ }
777
+ `;
778
+
779
+ const output = transform(input, undefined, true);
780
+
781
+ // Should have Platform import
782
+ expect(output).toContain("Platform");
783
+
784
+ // Should have StyleSheet import too
785
+ expect(output).toContain("StyleSheet");
786
+
787
+ // Should use Platform.select
788
+ expect(output).toContain("Platform.select");
789
+ });
790
+
791
+ it("should not add Platform import without platform modifiers", () => {
792
+ const input = `
793
+ import { View } from 'react-native';
794
+
795
+ function MyComponent() {
796
+ return <View className="m-4 p-2" />;
797
+ }
798
+ `;
799
+
800
+ const output = transform(input, undefined, true);
801
+
802
+ // Should not have Platform import
803
+ const platformCount = (output.match(/Platform/g) ?? []).length;
804
+ expect(platformCount).toBe(0);
805
+
806
+ // Should still have StyleSheet
807
+ expect(output).toContain("StyleSheet");
808
+ });
809
+ });
@@ -5,7 +5,14 @@
5
5
 
6
6
  import type { NodePath, PluginObj, PluginPass } from "@babel/core";
7
7
  import * as BabelTypes from "@babel/types";
8
- import { parseClassName, parsePlaceholderClasses, splitModifierClasses } from "../parser/index.js";
8
+ import type { ParsedModifier, StateModifierType } from "../parser/index.js";
9
+ import {
10
+ isPlatformModifier,
11
+ isStateModifier,
12
+ parseClassName,
13
+ parsePlaceholderClasses,
14
+ splitModifierClasses,
15
+ } from "../parser/index.js";
9
16
  import type { StyleObject } from "../types/core.js";
10
17
  import { generateStyleKey } from "../utils/styleKey.js";
11
18
  import { extractCustomColors } from "./config-loader.js";
@@ -17,10 +24,11 @@ import {
17
24
  getTargetStyleProp,
18
25
  isAttributeSupported,
19
26
  } from "./utils/attributeMatchers.js";
20
- import { getComponentModifierSupport } from "./utils/componentSupport.js";
27
+ import { getComponentModifierSupport, getStatePropertyForModifier } from "./utils/componentSupport.js";
21
28
  import { processDynamicExpression } from "./utils/dynamicProcessing.js";
22
29
  import { createStyleFunction, processStaticClassNameWithModifiers } from "./utils/modifierProcessing.js";
23
- import { addStyleSheetImport, injectStylesAtTop } from "./utils/styleInjection.js";
30
+ import { processPlatformModifiers } from "./utils/platformModifierProcessing.js";
31
+ import { addPlatformImport, addStyleSheetImport, injectStylesAtTop } from "./utils/styleInjection.js";
24
32
  import {
25
33
  addOrMergePlaceholderTextColorProp,
26
34
  findStyleAttribute,
@@ -59,6 +67,8 @@ type PluginState = PluginPass & {
59
67
  styleRegistry: Map<string, StyleObject>;
60
68
  hasClassNames: boolean;
61
69
  hasStyleSheetImport: boolean;
70
+ hasPlatformImport: boolean;
71
+ needsPlatformImport: boolean;
62
72
  customColors: Record<string, string>;
63
73
  supportedAttributes: Set<string>;
64
74
  attributePatterns: RegExp[];
@@ -66,6 +76,8 @@ type PluginState = PluginPass & {
66
76
  // Track tw/twStyle imports from main package
67
77
  twImportNames: Set<string>; // e.g., ['tw', 'twStyle'] or ['tw as customTw']
68
78
  hasTwImport: boolean;
79
+ // Track react-native import path for conditional StyleSheet/Platform injection
80
+ reactNativeImportPath?: NodePath<BabelTypes.ImportDeclaration>;
69
81
  };
70
82
 
71
83
  // Default identifier for the generated StyleSheet constant
@@ -90,6 +102,8 @@ export default function reactNativeTailwindBabelPlugin(
90
102
  state.styleRegistry = new Map();
91
103
  state.hasClassNames = false;
92
104
  state.hasStyleSheetImport = false;
105
+ state.hasPlatformImport = false;
106
+ state.needsPlatformImport = false;
93
107
  state.supportedAttributes = exactMatches;
94
108
  state.attributePatterns = patterns;
95
109
  state.stylesIdentifier = stylesIdentifier;
@@ -116,19 +130,25 @@ export default function reactNativeTailwindBabelPlugin(
116
130
  addStyleSheetImport(path, t);
117
131
  }
118
132
 
133
+ // Add Platform import if platform modifiers were used and not already present
134
+ if (state.needsPlatformImport && !state.hasPlatformImport) {
135
+ addPlatformImport(path, t);
136
+ }
137
+
119
138
  // Generate and inject StyleSheet.create at the beginning of the file (after imports)
120
139
  // This ensures _twStyles is defined before any code that references it
121
140
  injectStylesAtTop(path, state.styleRegistry, state.stylesIdentifier, t);
122
141
  },
123
142
  },
124
143
 
125
- // Check if StyleSheet is already imported and track tw/twStyle imports
144
+ // Check if StyleSheet/Platform are already imported and track tw/twStyle imports
126
145
  ImportDeclaration(path, state) {
127
146
  const node = path.node;
128
147
 
129
- // Track react-native StyleSheet import
148
+ // Track react-native StyleSheet and Platform imports
130
149
  if (node.source.value === "react-native") {
131
150
  const specifiers = node.specifiers;
151
+
132
152
  const hasStyleSheet = specifiers.some((spec) => {
133
153
  if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
134
154
  return spec.imported.name === "StyleSheet";
@@ -136,13 +156,25 @@ export default function reactNativeTailwindBabelPlugin(
136
156
  return false;
137
157
  });
138
158
 
159
+ const hasPlatform = specifiers.some((spec) => {
160
+ if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
161
+ return spec.imported.name === "Platform";
162
+ }
163
+ return false;
164
+ });
165
+
166
+ // Only track if imports exist - don't mutate yet
167
+ // Actual import injection happens in Program.exit only if needed
139
168
  if (hasStyleSheet) {
140
169
  state.hasStyleSheetImport = true;
141
- } else {
142
- // Add StyleSheet to existing import
143
- node.specifiers.push(t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet")));
144
- state.hasStyleSheetImport = true;
145
170
  }
171
+
172
+ if (hasPlatform) {
173
+ state.hasPlatformImport = true;
174
+ }
175
+
176
+ // Store reference to the react-native import for later modification if needed
177
+ state.reactNativeImportPath = path;
146
178
  }
147
179
 
148
180
  // Track tw/twStyle imports from main package (for compile-time transformation)
@@ -291,12 +323,15 @@ export default function reactNativeTailwindBabelPlugin(
291
323
 
292
324
  state.hasClassNames = true;
293
325
 
294
- // Check if className contains modifiers (active:, hover:, focus:, placeholder:)
326
+ // Check if className contains modifiers (active:, hover:, focus:, placeholder:, ios:, android:, web:)
295
327
  const { baseClasses, modifierClasses } = splitModifierClasses(className);
296
328
 
297
- // Separate placeholder modifiers from state modifiers
329
+ // Separate modifiers by type
298
330
  const placeholderModifiers = modifierClasses.filter((m) => m.modifier === "placeholder");
299
- const stateModifiers = modifierClasses.filter((m) => m.modifier !== "placeholder");
331
+ const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
332
+ const stateModifiers = modifierClasses.filter(
333
+ (m) => isStateModifier(m.modifier) && m.modifier !== "placeholder",
334
+ );
300
335
 
301
336
  // Handle placeholder modifiers first (they generate placeholderTextColor prop, not style)
302
337
  if (placeholderModifiers.length > 0) {
@@ -322,8 +357,153 @@ export default function reactNativeTailwindBabelPlugin(
322
357
  }
323
358
  }
324
359
 
325
- // If there are state modifiers, check if this component supports them
326
- if (stateModifiers.length > 0) {
360
+ // Handle combination of modifiers
361
+ const hasPlatformModifiers = platformModifiers.length > 0;
362
+ const hasStateModifiers = stateModifiers.length > 0;
363
+ const hasBaseClasses = baseClasses.length > 0;
364
+
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) {
368
+ // Get the JSX opening element for component support checking
369
+ const jsxOpeningElement = path.parent;
370
+ const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
371
+
372
+ if (componentSupport) {
373
+ // Build style array: [baseStyle, Platform.select(...), stateConditionals]
374
+ const styleArrayElements: BabelTypes.Expression[] = [];
375
+
376
+ // Add base classes
377
+ if (hasBaseClasses) {
378
+ const baseClassName = baseClasses.join(" ");
379
+ const baseStyleObject = parseClassName(baseClassName, state.customColors);
380
+ const baseStyleKey = generateStyleKey(baseClassName);
381
+ state.styleRegistry.set(baseStyleKey, baseStyleObject);
382
+ styleArrayElements.push(
383
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
384
+ );
385
+ }
386
+
387
+ // 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);
396
+
397
+ // Add state modifiers as conditionals
398
+ // Group by modifier type
399
+ const modifiersByType = new Map<StateModifierType, ParsedModifier[]>();
400
+ for (const mod of stateModifiers) {
401
+ const modType = mod.modifier as StateModifierType;
402
+ if (!modifiersByType.has(modType)) {
403
+ modifiersByType.set(modType, []);
404
+ }
405
+ modifiersByType.get(modType)?.push(mod);
406
+ }
407
+
408
+ // Build conditionals for each state modifier type
409
+ for (const [modifierType, modifiers] of modifiersByType) {
410
+ if (!componentSupport.supportedModifiers.includes(modifierType)) {
411
+ continue; // Skip unsupported modifiers
412
+ }
413
+
414
+ const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
415
+ const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
416
+ const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
417
+ state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
418
+
419
+ const stateProperty = getStatePropertyForModifier(modifierType);
420
+ const conditionalExpression = t.logicalExpression(
421
+ "&&",
422
+ t.identifier(stateProperty),
423
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey)),
424
+ );
425
+
426
+ styleArrayElements.push(conditionalExpression);
427
+ }
428
+
429
+ // Wrap in arrow function for state support
430
+ const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier))).filter((mod) =>
431
+ componentSupport.supportedModifiers.includes(mod),
432
+ );
433
+ const styleArrayExpression = t.arrayExpression(styleArrayElements);
434
+ const styleFunctionExpression = createStyleFunction(styleArrayExpression, usedModifiers, t);
435
+
436
+ const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
437
+ if (styleAttribute) {
438
+ mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
439
+ } else {
440
+ replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
441
+ }
442
+ return;
443
+ } else {
444
+ // Component doesn't support state modifiers, but we can still use platform modifiers
445
+ // Fall through to platform-only handling
446
+ }
447
+ }
448
+
449
+ // Handle platform-only modifiers (no state modifiers)
450
+ if (hasPlatformModifiers && !hasStateModifiers) {
451
+ // Build style array/expression: [baseStyle, Platform.select(...)]
452
+ const styleExpressions: BabelTypes.Expression[] = [];
453
+
454
+ // Add base classes
455
+ if (hasBaseClasses) {
456
+ const baseClassName = baseClasses.join(" ");
457
+ const baseStyleObject = parseClassName(baseClassName, state.customColors);
458
+ const baseStyleKey = generateStyleKey(baseClassName);
459
+ state.styleRegistry.set(baseStyleKey, baseStyleObject);
460
+ styleExpressions.push(
461
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
462
+ );
463
+ }
464
+
465
+ // 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);
474
+
475
+ // Generate style attribute
476
+ const styleExpression =
477
+ styleExpressions.length === 1 ? styleExpressions[0] : t.arrayExpression(styleExpressions);
478
+
479
+ const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
480
+ if (styleAttribute) {
481
+ // Merge with existing style attribute
482
+ const existingStyle = styleAttribute.value;
483
+ if (
484
+ t.isJSXExpressionContainer(existingStyle) &&
485
+ !t.isJSXEmptyExpression(existingStyle.expression)
486
+ ) {
487
+ const existing = existingStyle.expression;
488
+ // Merge as array: [ourStyles, existingStyles]
489
+ const mergedArray = t.isArrayExpression(existing)
490
+ ? t.arrayExpression([styleExpression, ...existing.elements])
491
+ : t.arrayExpression([styleExpression, existing]);
492
+ styleAttribute.value = t.jsxExpressionContainer(mergedArray);
493
+ } else {
494
+ styleAttribute.value = t.jsxExpressionContainer(styleExpression);
495
+ }
496
+ path.remove();
497
+ } else {
498
+ // Replace className with style prop containing our expression
499
+ path.node.name = t.jsxIdentifier(targetStyleProp);
500
+ path.node.value = t.jsxExpressionContainer(styleExpression);
501
+ }
502
+ return;
503
+ }
504
+
505
+ // If there are state modifiers (and no platform modifiers), check if this component supports them
506
+ if (hasStateModifiers) {
327
507
  // Get the JSX opening element (the direct parent of the attribute)
328
508
  const jsxOpeningElement = path.parent;
329
509
  const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Utility functions for processing platform modifiers (ios:, android:, web:)
3
+ */
4
+ import type * as BabelTypes from "@babel/types";
5
+ import type { ParsedModifier } from "../../parser/index.js";
6
+ import type { StyleObject } from "../../types/core.js";
7
+ /**
8
+ * Plugin state interface (subset needed for platform modifier processing)
9
+ */
10
+ export interface PlatformModifierProcessingState {
11
+ styleRegistry: Map<string, StyleObject>;
12
+ customColors: Record<string, string>;
13
+ stylesIdentifier: string;
14
+ needsPlatformImport: boolean;
15
+ }
16
+ /**
17
+ * Process platform modifiers and generate Platform.select() expression
18
+ *
19
+ * @param platformModifiers - Array of parsed platform modifiers
20
+ * @param state - Plugin state
21
+ * @param parseClassName - Function to parse class names into style objects
22
+ * @param generateStyleKey - Function to generate unique style keys
23
+ * @param t - Babel types
24
+ * @returns AST node for Platform.select() call
25
+ *
26
+ * @example
27
+ * Input: [{ modifier: "ios", baseClass: "shadow-lg" }, { modifier: "android", baseClass: "elevation-4" }]
28
+ * Output: Platform.select({ ios: styles._ios_shadow_lg, android: styles._android_elevation_4 })
29
+ */
30
+ export declare function processPlatformModifiers(platformModifiers: ParsedModifier[], state: PlatformModifierProcessingState, parseClassName: (className: string, customColors: Record<string, string>) => StyleObject, generateStyleKey: (className: string) => string, t: typeof BabelTypes): BabelTypes.Expression;