@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.
- package/README.md +152 -0
- package/dist/babel/config-loader.ts +2 -0
- package/dist/babel/index.cjs +205 -17
- package/dist/babel/plugin.d.ts +4 -1
- package/dist/babel/plugin.test.ts +327 -0
- package/dist/babel/plugin.ts +194 -14
- package/dist/babel/utils/platformModifierProcessing.d.ts +30 -0
- package/dist/babel/utils/platformModifierProcessing.ts +80 -0
- package/dist/babel/utils/styleInjection.d.ts +5 -1
- package/dist/babel/utils/styleInjection.ts +52 -7
- package/dist/babel/utils/styleTransforms.ts +1 -0
- package/dist/parser/aspectRatio.js +1 -1
- package/dist/parser/aspectRatio.test.js +1 -1
- package/dist/parser/index.d.ts +2 -2
- package/dist/parser/index.js +1 -1
- package/dist/parser/modifiers.d.ts +20 -2
- package/dist/parser/modifiers.js +1 -1
- package/dist/parser/spacing.d.ts +1 -1
- package/dist/parser/spacing.js +1 -1
- package/dist/parser/spacing.test.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +4 -4
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +4 -4
- package/dist/runtime.test.js +1 -1
- package/dist/stubs/tw.test.js +1 -0
- package/package.json +7 -7
- package/src/babel/config-loader.ts +2 -0
- package/src/babel/plugin.test.ts +327 -0
- package/src/babel/plugin.ts +194 -14
- package/src/babel/utils/platformModifierProcessing.ts +80 -0
- package/src/babel/utils/styleInjection.ts +52 -7
- package/src/babel/utils/styleTransforms.ts +1 -0
- package/src/parser/aspectRatio.test.ts +25 -2
- package/src/parser/aspectRatio.ts +4 -3
- package/src/parser/borders.ts +2 -0
- package/src/parser/colors.ts +2 -0
- package/src/parser/index.ts +9 -2
- package/src/parser/layout.ts +2 -0
- package/src/parser/modifiers.ts +38 -4
- package/src/parser/placeholder.ts +1 -0
- package/src/parser/sizing.ts +1 -0
- package/src/parser/spacing.test.ts +63 -0
- package/src/parser/spacing.ts +11 -6
- package/src/parser/transforms.ts +5 -0
- package/src/parser/typography.ts +2 -0
- package/src/runtime.test.ts +27 -0
- package/src/runtime.ts +2 -1
- package/src/stubs/tw.test.ts +27 -0
- package/dist/babel/index.test.ts +0 -481
- package/dist/config/palettes.d.ts +0 -302
- package/dist/config/palettes.js +0 -1
- package/dist/parser/__snapshots__/aspectRatio.test.js.snap +0 -9
- package/dist/parser/__snapshots__/borders.test.js.snap +0 -23
- package/dist/parser/__snapshots__/colors.test.js.snap +0 -251
- package/dist/parser/__snapshots__/shadows.test.js.snap +0 -76
- package/dist/parser/__snapshots__/sizing.test.js.snap +0 -61
- package/dist/parser/__snapshots__/spacing.test.js.snap +0 -40
- package/dist/parser/__snapshots__/transforms.test.js.snap +0 -58
- package/dist/parser/__snapshots__/typography.test.js.snap +0 -30
- package/dist/parser/aspectRatio.test.d.ts +0 -1
- package/dist/parser/borders.test.d.ts +0 -1
- package/dist/parser/colors.test.d.ts +0 -1
- package/dist/parser/layout.test.d.ts +0 -1
- package/dist/parser/modifiers.test.d.ts +0 -1
- package/dist/parser/shadows.test.d.ts +0 -1
- package/dist/parser/sizing.test.d.ts +0 -1
- package/dist/parser/spacing.test.d.ts +0 -1
- package/dist/parser/typography.test.d.ts +0 -1
- package/dist/types.d.ts +0 -42
- 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
|
+
});
|
package/dist/babel/plugin.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
329
|
+
// Separate modifiers by type
|
|
298
330
|
const placeholderModifiers = modifierClasses.filter((m) => m.modifier === "placeholder");
|
|
299
|
-
const
|
|
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
|
-
//
|
|
326
|
-
|
|
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;
|