@mgcrea/react-native-tailwind 0.11.1 → 0.12.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/dist/babel/config-loader.d.ts +3 -0
- package/dist/babel/config-loader.test.ts +2 -2
- package/dist/babel/config-loader.ts +37 -2
- package/dist/babel/index.cjs +325 -42
- package/dist/babel/plugin.test.ts +498 -0
- package/dist/babel/plugin.ts +66 -17
- package/dist/babel/utils/styleInjection.ts +57 -17
- package/dist/babel/utils/twProcessing.d.ts +8 -1
- package/dist/babel/utils/twProcessing.ts +212 -4
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.js +1 -1
- package/dist/parser/layout.js +1 -1
- package/dist/parser/layout.test.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/parser/typography.d.ts +2 -1
- package/dist/parser/typography.js +1 -1
- package/dist/parser/typography.test.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +3 -3
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +3 -3
- package/dist/runtime.test.js +1 -1
- package/dist/types/runtime.d.ts +8 -1
- package/package.json +1 -1
- package/src/babel/config-loader.test.ts +2 -2
- package/src/babel/config-loader.ts +37 -2
- package/src/babel/plugin.test.ts +498 -0
- package/src/babel/plugin.ts +66 -17
- package/src/babel/utils/styleInjection.ts +57 -17
- package/src/babel/utils/twProcessing.ts +212 -4
- package/src/parser/index.ts +2 -1
- package/src/parser/layout.test.ts +61 -0
- package/src/parser/layout.ts +55 -1
- package/src/parser/spacing.test.ts +62 -0
- package/src/parser/spacing.ts +7 -7
- package/src/parser/typography.test.ts +102 -0
- package/src/parser/typography.ts +61 -15
- package/src/runtime.test.ts +4 -1
- package/src/types/runtime.ts +8 -1
package/src/babel/plugin.ts
CHANGED
|
@@ -276,6 +276,10 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
276
276
|
state.twImportNames = new Set();
|
|
277
277
|
state.hasTwImport = false;
|
|
278
278
|
state.functionComponentsNeedingColorScheme = new Set();
|
|
279
|
+
state.hasColorSchemeImport = false;
|
|
280
|
+
state.colorSchemeLocalIdentifier = undefined;
|
|
281
|
+
state.needsPlatformImport = false;
|
|
282
|
+
state.hasPlatformImport = false;
|
|
279
283
|
|
|
280
284
|
// Load custom theme from tailwind.config.*
|
|
281
285
|
state.customTheme = extractCustomTheme(state.file.opts.filename ?? "");
|
|
@@ -367,7 +371,8 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
367
371
|
|
|
368
372
|
// Track color scheme hook import from the configured source
|
|
369
373
|
// (default: react-native, but can be custom like @/hooks/useColorScheme)
|
|
370
|
-
|
|
374
|
+
// Only track value imports (not type-only imports which get erased)
|
|
375
|
+
if (node.source.value === state.colorSchemeImportSource && node.importKind !== "type") {
|
|
371
376
|
const specifiers = node.specifiers;
|
|
372
377
|
|
|
373
378
|
for (const spec of specifiers) {
|
|
@@ -393,7 +398,7 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
393
398
|
// Track the local name (could be renamed: import { tw as customTw })
|
|
394
399
|
const localName = spec.local.name;
|
|
395
400
|
state.twImportNames.add(localName);
|
|
396
|
-
|
|
401
|
+
// Don't set hasTwImport yet - only set it when we successfully transform a call
|
|
397
402
|
}
|
|
398
403
|
}
|
|
399
404
|
});
|
|
@@ -438,13 +443,27 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
438
443
|
path.replaceWith(
|
|
439
444
|
t.objectExpression([t.objectProperty(t.identifier("style"), t.objectExpression([]))]),
|
|
440
445
|
);
|
|
446
|
+
// Mark as successfully transformed (even if empty)
|
|
447
|
+
state.hasTwImport = true;
|
|
441
448
|
return;
|
|
442
449
|
}
|
|
443
450
|
|
|
444
451
|
state.hasClassNames = true;
|
|
445
452
|
|
|
446
453
|
// Process the className with modifiers
|
|
447
|
-
processTwCall(
|
|
454
|
+
processTwCall(
|
|
455
|
+
className,
|
|
456
|
+
path,
|
|
457
|
+
state,
|
|
458
|
+
parseClassName,
|
|
459
|
+
generateStyleKey,
|
|
460
|
+
splitModifierClasses,
|
|
461
|
+
findComponentScope,
|
|
462
|
+
t,
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
// Mark as successfully transformed
|
|
466
|
+
state.hasTwImport = true;
|
|
448
467
|
},
|
|
449
468
|
|
|
450
469
|
// Handle twStyle('...') call expressions
|
|
@@ -488,13 +507,27 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
488
507
|
if (!className) {
|
|
489
508
|
// Replace with undefined
|
|
490
509
|
path.replaceWith(t.identifier("undefined"));
|
|
510
|
+
// Mark as successfully transformed (even if empty)
|
|
511
|
+
state.hasTwImport = true;
|
|
491
512
|
return;
|
|
492
513
|
}
|
|
493
514
|
|
|
494
515
|
state.hasClassNames = true;
|
|
495
516
|
|
|
496
517
|
// Process the className with modifiers
|
|
497
|
-
processTwCall(
|
|
518
|
+
processTwCall(
|
|
519
|
+
className,
|
|
520
|
+
path,
|
|
521
|
+
state,
|
|
522
|
+
parseClassName,
|
|
523
|
+
generateStyleKey,
|
|
524
|
+
splitModifierClasses,
|
|
525
|
+
findComponentScope,
|
|
526
|
+
t,
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// Mark as successfully transformed
|
|
530
|
+
state.hasTwImport = true;
|
|
498
531
|
},
|
|
499
532
|
|
|
500
533
|
JSXAttribute(path, state) {
|
|
@@ -517,20 +550,22 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
517
550
|
// Determine target style prop based on attribute name
|
|
518
551
|
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
519
552
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
553
|
+
/**
|
|
554
|
+
* Process static className string (handles both direct StringLiteral and StringLiteral in JSXExpressionContainer)
|
|
555
|
+
*/
|
|
556
|
+
const processStaticClassName = (className: string): boolean => {
|
|
557
|
+
const trimmedClassName = className.trim();
|
|
523
558
|
|
|
524
559
|
// Skip empty classNames
|
|
525
|
-
if (!
|
|
560
|
+
if (!trimmedClassName) {
|
|
526
561
|
path.remove();
|
|
527
|
-
return;
|
|
562
|
+
return true;
|
|
528
563
|
}
|
|
529
564
|
|
|
530
565
|
state.hasClassNames = true;
|
|
531
566
|
|
|
532
567
|
// Check if className contains modifiers (active:, hover:, focus:, placeholder:, ios:, android:, web:, dark:, light:, scheme:)
|
|
533
|
-
const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(
|
|
568
|
+
const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(trimmedClassName);
|
|
534
569
|
|
|
535
570
|
// Expand scheme: modifiers into dark: and light: modifiers
|
|
536
571
|
const modifierClasses: ParsedModifier[] = [];
|
|
@@ -697,7 +732,7 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
697
732
|
} else {
|
|
698
733
|
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
699
734
|
}
|
|
700
|
-
return;
|
|
735
|
+
return true;
|
|
701
736
|
} else {
|
|
702
737
|
// Component doesn't support state modifiers, but we can still use platform modifiers
|
|
703
738
|
// Fall through to platform-only handling
|
|
@@ -771,7 +806,7 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
771
806
|
path.node.name = t.jsxIdentifier(targetStyleProp);
|
|
772
807
|
path.node.value = t.jsxExpressionContainer(styleExpression);
|
|
773
808
|
}
|
|
774
|
-
return;
|
|
809
|
+
return true;
|
|
775
810
|
}
|
|
776
811
|
|
|
777
812
|
// If there are state modifiers (and no platform modifiers), check if this component supports them
|
|
@@ -829,12 +864,12 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
829
864
|
} else {
|
|
830
865
|
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
831
866
|
}
|
|
832
|
-
return;
|
|
867
|
+
return true;
|
|
833
868
|
}
|
|
834
869
|
} else {
|
|
835
870
|
// All modifiers are supported - process normally
|
|
836
871
|
const styleExpression = processStaticClassNameWithModifiers(
|
|
837
|
-
|
|
872
|
+
trimmedClassName,
|
|
838
873
|
state,
|
|
839
874
|
parseClassName,
|
|
840
875
|
generateStyleKey,
|
|
@@ -851,7 +886,7 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
851
886
|
} else {
|
|
852
887
|
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
853
888
|
}
|
|
854
|
-
return;
|
|
889
|
+
return true;
|
|
855
890
|
}
|
|
856
891
|
} else {
|
|
857
892
|
// Component doesn't support any modifiers
|
|
@@ -871,7 +906,7 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
871
906
|
if (!classNameForStyle) {
|
|
872
907
|
// No base classes, only had placeholder modifiers - just remove className
|
|
873
908
|
path.remove();
|
|
874
|
-
return;
|
|
909
|
+
return true;
|
|
875
910
|
}
|
|
876
911
|
|
|
877
912
|
const styleObject = parseClassName(classNameForStyle, state.customTheme);
|
|
@@ -888,7 +923,14 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
888
923
|
// Replace className with style prop
|
|
889
924
|
replaceWithStyleAttribute(path, styleKey, targetStyleProp, state.stylesIdentifier, t);
|
|
890
925
|
}
|
|
891
|
-
return;
|
|
926
|
+
return true;
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// Handle static string literals
|
|
930
|
+
if (t.isStringLiteral(value)) {
|
|
931
|
+
if (processStaticClassName(value.value)) {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
892
934
|
}
|
|
893
935
|
|
|
894
936
|
// Handle dynamic expressions (JSXExpressionContainer)
|
|
@@ -900,6 +942,13 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
900
942
|
return;
|
|
901
943
|
}
|
|
902
944
|
|
|
945
|
+
// Fast path: Support string literals wrapped in JSXExpressionContainer: className={"flex-row"}
|
|
946
|
+
if (t.isStringLiteral(expression)) {
|
|
947
|
+
if (processStaticClassName(expression.value)) {
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
903
952
|
try {
|
|
904
953
|
// Find component scope for color scheme modifiers
|
|
905
954
|
const componentScope = findComponentScope(path, t);
|
|
@@ -10,24 +10,44 @@ import type { StyleObject } from "../../types/core.js";
|
|
|
10
10
|
* Add StyleSheet import to the file or merge with existing react-native import
|
|
11
11
|
*/
|
|
12
12
|
export function addStyleSheetImport(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void {
|
|
13
|
-
// Check if there's already a react-native
|
|
13
|
+
// Check if there's already a value import from react-native
|
|
14
14
|
const body = path.node.body;
|
|
15
|
-
let
|
|
15
|
+
let existingValueImport: BabelTypes.ImportDeclaration | null = null;
|
|
16
16
|
|
|
17
17
|
for (const statement of body) {
|
|
18
18
|
if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Skip type-only imports (they get erased at runtime)
|
|
20
|
+
if (statement.importKind === "type") {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
// Skip namespace imports (import * as RN) - can't add named specifiers to them
|
|
24
|
+
const hasNamespaceImport = statement.specifiers.some((spec) => t.isImportNamespaceSpecifier(spec));
|
|
25
|
+
if (hasNamespaceImport) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
existingValueImport = statement;
|
|
29
|
+
break; // Found a value import, we can stop
|
|
21
30
|
}
|
|
22
31
|
}
|
|
23
32
|
|
|
24
|
-
if (
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
if (existingValueImport) {
|
|
34
|
+
// Check if StyleSheet is already imported
|
|
35
|
+
const hasStyleSheet = existingValueImport.specifiers.some(
|
|
36
|
+
(spec) =>
|
|
37
|
+
t.isImportSpecifier(spec) &&
|
|
38
|
+
spec.imported.type === "Identifier" &&
|
|
39
|
+
spec.imported.name === "StyleSheet",
|
|
28
40
|
);
|
|
41
|
+
|
|
42
|
+
if (!hasStyleSheet) {
|
|
43
|
+
// Add StyleSheet to existing value import
|
|
44
|
+
existingValueImport.specifiers.push(
|
|
45
|
+
t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet")),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
29
48
|
} else {
|
|
30
|
-
//
|
|
49
|
+
// No value import exists - create a new one
|
|
50
|
+
// (Don't merge with type-only or namespace imports)
|
|
31
51
|
const importDeclaration = t.importDeclaration(
|
|
32
52
|
[t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet"))],
|
|
33
53
|
t.stringLiteral("react-native"),
|
|
@@ -40,22 +60,42 @@ export function addStyleSheetImport(path: NodePath<BabelTypes.Program>, t: typeo
|
|
|
40
60
|
* Add Platform import to the file or merge with existing react-native import
|
|
41
61
|
*/
|
|
42
62
|
export function addPlatformImport(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void {
|
|
43
|
-
// Check if there's already a react-native
|
|
63
|
+
// Check if there's already a value import from react-native
|
|
44
64
|
const body = path.node.body;
|
|
45
|
-
let
|
|
65
|
+
let existingValueImport: BabelTypes.ImportDeclaration | null = null;
|
|
46
66
|
|
|
47
67
|
for (const statement of body) {
|
|
48
68
|
if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
// Skip type-only imports (they get erased at runtime)
|
|
70
|
+
if (statement.importKind === "type") {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// Skip namespace imports (import * as RN) - can't add named specifiers to them
|
|
74
|
+
const hasNamespaceImport = statement.specifiers.some((spec) => t.isImportNamespaceSpecifier(spec));
|
|
75
|
+
if (hasNamespaceImport) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
existingValueImport = statement;
|
|
79
|
+
break; // Found a value import, we can stop
|
|
51
80
|
}
|
|
52
81
|
}
|
|
53
82
|
|
|
54
|
-
if (
|
|
55
|
-
//
|
|
56
|
-
|
|
83
|
+
if (existingValueImport) {
|
|
84
|
+
// Check if Platform is already imported
|
|
85
|
+
const hasPlatform = existingValueImport.specifiers.some(
|
|
86
|
+
(spec) =>
|
|
87
|
+
t.isImportSpecifier(spec) && spec.imported.type === "Identifier" && spec.imported.name === "Platform",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (!hasPlatform) {
|
|
91
|
+
// Add Platform to existing value import
|
|
92
|
+
existingValueImport.specifiers.push(
|
|
93
|
+
t.importSpecifier(t.identifier("Platform"), t.identifier("Platform")),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
57
96
|
} else {
|
|
58
|
-
//
|
|
97
|
+
// No value import exists - create a new one
|
|
98
|
+
// (Don't merge with type-only or namespace imports)
|
|
59
99
|
const importDeclaration = t.importDeclaration(
|
|
60
100
|
[t.importSpecifier(t.identifier("Platform"), t.identifier("Platform"))],
|
|
61
101
|
t.stringLiteral("react-native"),
|
|
@@ -5,9 +5,16 @@
|
|
|
5
5
|
import type { NodePath } from "@babel/core";
|
|
6
6
|
import type * as BabelTypes from "@babel/types";
|
|
7
7
|
import type { CustomTheme, ModifierType, ParsedModifier } from "../../parser/index.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
expandSchemeModifier,
|
|
10
|
+
isColorSchemeModifier,
|
|
11
|
+
isPlatformModifier,
|
|
12
|
+
isSchemeModifier,
|
|
13
|
+
} from "../../parser/index.js";
|
|
9
14
|
import type { SchemeModifierConfig } from "../../types/config.js";
|
|
10
15
|
import type { StyleObject } from "../../types/core.js";
|
|
16
|
+
import { processColorSchemeModifiers } from "./colorSchemeModifierProcessing.js";
|
|
17
|
+
import { processPlatformModifiers } from "./platformModifierProcessing.js";
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* Plugin state interface (subset needed for tw processing)
|
|
@@ -18,11 +25,20 @@ export interface TwProcessingState {
|
|
|
18
25
|
customTheme: CustomTheme;
|
|
19
26
|
schemeModifierConfig: SchemeModifierConfig;
|
|
20
27
|
stylesIdentifier: string;
|
|
28
|
+
// Color scheme support (for dark:/light: modifiers)
|
|
29
|
+
needsColorSchemeImport: boolean;
|
|
30
|
+
colorSchemeVariableName: string;
|
|
31
|
+
functionComponentsNeedingColorScheme: Set<NodePath<BabelTypes.Function>>;
|
|
32
|
+
colorSchemeLocalIdentifier?: string;
|
|
33
|
+
// Platform support (for ios:/android:/web: modifiers)
|
|
34
|
+
needsPlatformImport: boolean;
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
/**
|
|
24
38
|
* Process tw`...` or twStyle('...') call and replace with TwStyle object
|
|
25
39
|
* Generates: { style: styles._base, activeStyle: styles._active, ... }
|
|
40
|
+
* When color-scheme modifiers are present, generates: { style: [base, _twColorScheme === 'dark' && dark, ...] }
|
|
41
|
+
* When platform modifiers are present, generates: { style: [base, Platform.select({ ios: ..., android: ... })] }
|
|
26
42
|
*/
|
|
27
43
|
export function processTwCall(
|
|
28
44
|
className: string,
|
|
@@ -31,6 +47,7 @@ export function processTwCall(
|
|
|
31
47
|
parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
|
|
32
48
|
generateStyleKey: (className: string) => string,
|
|
33
49
|
splitModifierClasses: (className: string) => { baseClasses: string[]; modifierClasses: ParsedModifier[] },
|
|
50
|
+
findComponentScope: (path: NodePath, t: typeof BabelTypes) => NodePath<BabelTypes.Function> | null,
|
|
34
51
|
t: typeof BabelTypes,
|
|
35
52
|
): void {
|
|
36
53
|
const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(className);
|
|
@@ -74,9 +91,200 @@ export function processTwCall(
|
|
|
74
91
|
objectProperties.push(t.objectProperty(t.identifier("style"), t.objectExpression([])));
|
|
75
92
|
}
|
|
76
93
|
|
|
77
|
-
//
|
|
94
|
+
// Separate color-scheme and platform modifiers from other modifiers
|
|
95
|
+
const colorSchemeModifiers = modifierClasses.filter((m) => isColorSchemeModifier(m.modifier));
|
|
96
|
+
const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
|
|
97
|
+
const otherModifiers = modifierClasses.filter(
|
|
98
|
+
(m) => !isColorSchemeModifier(m.modifier) && !isPlatformModifier(m.modifier),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Check if we need color scheme support
|
|
102
|
+
const hasColorSchemeModifiers = colorSchemeModifiers.length > 0;
|
|
103
|
+
let componentScope: NodePath<BabelTypes.Function> | null = null;
|
|
104
|
+
|
|
105
|
+
if (hasColorSchemeModifiers) {
|
|
106
|
+
// Find component scope for hook injection
|
|
107
|
+
componentScope = findComponentScope(path, t);
|
|
108
|
+
|
|
109
|
+
if (!componentScope) {
|
|
110
|
+
// Warning: color scheme modifiers used outside component scope
|
|
111
|
+
if (process.env.NODE_ENV !== "production") {
|
|
112
|
+
console.warn(
|
|
113
|
+
`[react-native-tailwind] Color scheme modifiers (dark:, light:) in tw/twStyle calls ` +
|
|
114
|
+
`must be used inside a React component. Modifiers will be ignored.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// Track this component as needing the color scheme hook
|
|
119
|
+
state.functionComponentsNeedingColorScheme.add(componentScope);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Process color scheme modifiers if we have a valid component scope
|
|
124
|
+
if (hasColorSchemeModifiers && componentScope) {
|
|
125
|
+
// Generate conditional expressions for color scheme
|
|
126
|
+
const colorSchemeConditionals = processColorSchemeModifiers(
|
|
127
|
+
colorSchemeModifiers,
|
|
128
|
+
state,
|
|
129
|
+
parseClassName,
|
|
130
|
+
generateStyleKey,
|
|
131
|
+
t,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Build style array: [baseStyle, _twColorScheme === 'dark' && darkStyle, ...]
|
|
135
|
+
const styleArrayElements: BabelTypes.Expression[] = [];
|
|
136
|
+
|
|
137
|
+
// Add base style if present
|
|
138
|
+
if (baseClasses.length > 0) {
|
|
139
|
+
const baseClassName = baseClasses.join(" ");
|
|
140
|
+
const baseStyleObject = parseClassName(baseClassName, state.customTheme);
|
|
141
|
+
const baseStyleKey = generateStyleKey(baseClassName);
|
|
142
|
+
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
143
|
+
styleArrayElements.push(
|
|
144
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Add color scheme conditionals
|
|
149
|
+
styleArrayElements.push(...colorSchemeConditionals);
|
|
150
|
+
|
|
151
|
+
// Replace style property with array
|
|
152
|
+
objectProperties[0] = t.objectProperty(t.identifier("style"), t.arrayExpression(styleArrayElements));
|
|
153
|
+
|
|
154
|
+
// Also add darkStyle/lightStyle properties for manual processing
|
|
155
|
+
// (e.g., extracting raw hex values for Reanimated animations)
|
|
156
|
+
const darkModifiers = colorSchemeModifiers.filter((m) => m.modifier === "dark");
|
|
157
|
+
const lightModifiers = colorSchemeModifiers.filter((m) => m.modifier === "light");
|
|
158
|
+
|
|
159
|
+
if (darkModifiers.length > 0) {
|
|
160
|
+
const darkClassNames = darkModifiers.map((m) => m.baseClass).join(" ");
|
|
161
|
+
const darkStyleObject = parseClassName(darkClassNames, state.customTheme);
|
|
162
|
+
const darkStyleKey = generateStyleKey(`dark_${darkClassNames}`);
|
|
163
|
+
state.styleRegistry.set(darkStyleKey, darkStyleObject);
|
|
164
|
+
|
|
165
|
+
objectProperties.push(
|
|
166
|
+
t.objectProperty(
|
|
167
|
+
t.identifier("darkStyle"),
|
|
168
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(darkStyleKey)),
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (lightModifiers.length > 0) {
|
|
174
|
+
const lightClassNames = lightModifiers.map((m) => m.baseClass).join(" ");
|
|
175
|
+
const lightStyleObject = parseClassName(lightClassNames, state.customTheme);
|
|
176
|
+
const lightStyleKey = generateStyleKey(`light_${lightClassNames}`);
|
|
177
|
+
state.styleRegistry.set(lightStyleKey, lightStyleObject);
|
|
178
|
+
|
|
179
|
+
objectProperties.push(
|
|
180
|
+
t.objectProperty(
|
|
181
|
+
t.identifier("lightStyle"),
|
|
182
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(lightStyleKey)),
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Process platform modifiers if present
|
|
189
|
+
const hasPlatformModifiers = platformModifiers.length > 0;
|
|
190
|
+
|
|
191
|
+
if (hasPlatformModifiers) {
|
|
192
|
+
// Mark that we need Platform import
|
|
193
|
+
state.needsPlatformImport = true;
|
|
194
|
+
|
|
195
|
+
// Generate Platform.select() expression
|
|
196
|
+
const platformSelectExpression = processPlatformModifiers(
|
|
197
|
+
platformModifiers,
|
|
198
|
+
state,
|
|
199
|
+
parseClassName,
|
|
200
|
+
generateStyleKey,
|
|
201
|
+
t,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// If we already have a style array (from color scheme modifiers), add to it
|
|
205
|
+
// Otherwise, convert style property to an array
|
|
206
|
+
if (hasColorSchemeModifiers && componentScope) {
|
|
207
|
+
// Already have style array from color scheme processing
|
|
208
|
+
// Get the current array expression and add Platform.select to it
|
|
209
|
+
const styleProperty = objectProperties.find(
|
|
210
|
+
(prop) => t.isIdentifier(prop.key) && prop.key.name === "style",
|
|
211
|
+
);
|
|
212
|
+
if (styleProperty && t.isArrayExpression(styleProperty.value)) {
|
|
213
|
+
styleProperty.value.elements.push(platformSelectExpression);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
// No color scheme modifiers, create style array with base + Platform.select
|
|
217
|
+
const styleArrayElements: BabelTypes.Expression[] = [];
|
|
218
|
+
|
|
219
|
+
// Add base style if present
|
|
220
|
+
if (baseClasses.length > 0) {
|
|
221
|
+
const baseClassName = baseClasses.join(" ");
|
|
222
|
+
const baseStyleObject = parseClassName(baseClassName, state.customTheme);
|
|
223
|
+
const baseStyleKey = generateStyleKey(baseClassName);
|
|
224
|
+
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
225
|
+
styleArrayElements.push(
|
|
226
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Add Platform.select() expression
|
|
231
|
+
styleArrayElements.push(platformSelectExpression);
|
|
232
|
+
|
|
233
|
+
// Replace style property with array
|
|
234
|
+
objectProperties[0] = t.objectProperty(t.identifier("style"), t.arrayExpression(styleArrayElements));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Also add iosStyle/androidStyle/webStyle properties for manual processing
|
|
238
|
+
const iosModifiers = platformModifiers.filter((m) => m.modifier === "ios");
|
|
239
|
+
const androidModifiers = platformModifiers.filter((m) => m.modifier === "android");
|
|
240
|
+
const webModifiers = platformModifiers.filter((m) => m.modifier === "web");
|
|
241
|
+
|
|
242
|
+
if (iosModifiers.length > 0) {
|
|
243
|
+
const iosClassNames = iosModifiers.map((m) => m.baseClass).join(" ");
|
|
244
|
+
const iosStyleObject = parseClassName(iosClassNames, state.customTheme);
|
|
245
|
+
const iosStyleKey = generateStyleKey(`ios_${iosClassNames}`);
|
|
246
|
+
state.styleRegistry.set(iosStyleKey, iosStyleObject);
|
|
247
|
+
|
|
248
|
+
objectProperties.push(
|
|
249
|
+
t.objectProperty(
|
|
250
|
+
t.identifier("iosStyle"),
|
|
251
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(iosStyleKey)),
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (androidModifiers.length > 0) {
|
|
257
|
+
const androidClassNames = androidModifiers.map((m) => m.baseClass).join(" ");
|
|
258
|
+
const androidStyleObject = parseClassName(androidClassNames, state.customTheme);
|
|
259
|
+
const androidStyleKey = generateStyleKey(`android_${androidClassNames}`);
|
|
260
|
+
state.styleRegistry.set(androidStyleKey, androidStyleObject);
|
|
261
|
+
|
|
262
|
+
objectProperties.push(
|
|
263
|
+
t.objectProperty(
|
|
264
|
+
t.identifier("androidStyle"),
|
|
265
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(androidStyleKey)),
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (webModifiers.length > 0) {
|
|
271
|
+
const webClassNames = webModifiers.map((m) => m.baseClass).join(" ");
|
|
272
|
+
const webStyleObject = parseClassName(webClassNames, state.customTheme);
|
|
273
|
+
const webStyleKey = generateStyleKey(`web_${webClassNames}`);
|
|
274
|
+
state.styleRegistry.set(webStyleKey, webStyleObject);
|
|
275
|
+
|
|
276
|
+
objectProperties.push(
|
|
277
|
+
t.objectProperty(
|
|
278
|
+
t.identifier("webStyle"),
|
|
279
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(webStyleKey)),
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Group other modifiers by type (non-color-scheme and non-platform modifiers)
|
|
78
286
|
const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
|
|
79
|
-
for (const mod of
|
|
287
|
+
for (const mod of otherModifiers) {
|
|
80
288
|
if (!modifiersByType.has(mod.modifier)) {
|
|
81
289
|
modifiersByType.set(mod.modifier, []);
|
|
82
290
|
}
|
|
@@ -86,7 +294,7 @@ export function processTwCall(
|
|
|
86
294
|
}
|
|
87
295
|
}
|
|
88
296
|
|
|
89
|
-
// Add modifier styles
|
|
297
|
+
// Add modifier styles (activeStyle, focusStyle, etc.) for non-color-scheme modifiers
|
|
90
298
|
for (const [modifierType, modifiers] of modifiersByType) {
|
|
91
299
|
const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
92
300
|
const modifierStyleObject = parseClassName(modifierClassNames, state.customTheme);
|
package/src/parser/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { parseTypography } from "./typography";
|
|
|
20
20
|
export type CustomTheme = {
|
|
21
21
|
colors?: Record<string, string>;
|
|
22
22
|
fontFamily?: Record<string, string>;
|
|
23
|
+
fontSize?: Record<string, number>;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -55,7 +56,7 @@ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject
|
|
|
55
56
|
parseBorder,
|
|
56
57
|
(cls: string) => parseColor(cls, customTheme?.colors),
|
|
57
58
|
parseLayout,
|
|
58
|
-
(cls: string) => parseTypography(cls, customTheme?.fontFamily),
|
|
59
|
+
(cls: string) => parseTypography(cls, customTheme?.fontFamily, customTheme?.fontSize),
|
|
59
60
|
parseSizing,
|
|
60
61
|
parseShadow,
|
|
61
62
|
parseAspectRatio,
|
|
@@ -73,6 +73,67 @@ describe("parseLayout - flex grow/shrink utilities", () => {
|
|
|
73
73
|
it("should parse shrink-0", () => {
|
|
74
74
|
expect(parseLayout("shrink-0")).toEqual({ flexShrink: 0 });
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
it("should parse grow with arbitrary values", () => {
|
|
78
|
+
expect(parseLayout("grow-[1.5]")).toEqual({ flexGrow: 1.5 });
|
|
79
|
+
expect(parseLayout("grow-[2]")).toEqual({ flexGrow: 2 });
|
|
80
|
+
expect(parseLayout("grow-[0.5]")).toEqual({ flexGrow: 0.5 });
|
|
81
|
+
expect(parseLayout("grow-[3]")).toEqual({ flexGrow: 3 });
|
|
82
|
+
expect(parseLayout("grow-[0]")).toEqual({ flexGrow: 0 });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should parse shrink with arbitrary values", () => {
|
|
86
|
+
expect(parseLayout("shrink-[0.5]")).toEqual({ flexShrink: 0.5 });
|
|
87
|
+
expect(parseLayout("shrink-[2]")).toEqual({ flexShrink: 2 });
|
|
88
|
+
expect(parseLayout("shrink-[1.5]")).toEqual({ flexShrink: 1.5 });
|
|
89
|
+
expect(parseLayout("shrink-[3]")).toEqual({ flexShrink: 3 });
|
|
90
|
+
expect(parseLayout("shrink-[0]")).toEqual({ flexShrink: 0 });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should parse CSS-style flex-grow aliases", () => {
|
|
94
|
+
expect(parseLayout("flex-grow")).toEqual({ flexGrow: 1 });
|
|
95
|
+
expect(parseLayout("flex-grow-0")).toEqual({ flexGrow: 0 });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should parse CSS-style flex-shrink aliases", () => {
|
|
99
|
+
expect(parseLayout("flex-shrink")).toEqual({ flexShrink: 1 });
|
|
100
|
+
expect(parseLayout("flex-shrink-0")).toEqual({ flexShrink: 0 });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should parse CSS-style flex-grow with arbitrary values", () => {
|
|
104
|
+
expect(parseLayout("flex-grow-[1.5]")).toEqual({ flexGrow: 1.5 });
|
|
105
|
+
expect(parseLayout("flex-grow-[2]")).toEqual({ flexGrow: 2 });
|
|
106
|
+
expect(parseLayout("flex-grow-[0.5]")).toEqual({ flexGrow: 0.5 });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should parse CSS-style flex-shrink with arbitrary values", () => {
|
|
110
|
+
expect(parseLayout("flex-shrink-[0.5]")).toEqual({ flexShrink: 0.5 });
|
|
111
|
+
expect(parseLayout("flex-shrink-[2]")).toEqual({ flexShrink: 2 });
|
|
112
|
+
expect(parseLayout("flex-shrink-[1.5]")).toEqual({ flexShrink: 1.5 });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should handle edge case values", () => {
|
|
116
|
+
expect(parseLayout("grow-[0.1]")).toEqual({ flexGrow: 0.1 });
|
|
117
|
+
expect(parseLayout("grow-[10]")).toEqual({ flexGrow: 10 });
|
|
118
|
+
expect(parseLayout("shrink-[0.01]")).toEqual({ flexShrink: 0.01 });
|
|
119
|
+
expect(parseLayout("shrink-[100]")).toEqual({ flexShrink: 100 });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should parse Tailwind shorthand decimals (no leading zero)", () => {
|
|
123
|
+
expect(parseLayout("grow-[.5]")).toEqual({ flexGrow: 0.5 });
|
|
124
|
+
expect(parseLayout("grow-[.75]")).toEqual({ flexGrow: 0.75 });
|
|
125
|
+
expect(parseLayout("shrink-[.5]")).toEqual({ flexShrink: 0.5 });
|
|
126
|
+
expect(parseLayout("shrink-[.25]")).toEqual({ flexShrink: 0.25 });
|
|
127
|
+
expect(parseLayout("flex-grow-[.5]")).toEqual({ flexGrow: 0.5 });
|
|
128
|
+
expect(parseLayout("flex-shrink-[.5]")).toEqual({ flexShrink: 0.5 });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should reject negative values", () => {
|
|
132
|
+
expect(parseLayout("grow-[-1]")).toBeNull();
|
|
133
|
+
expect(parseLayout("shrink-[-1]")).toBeNull();
|
|
134
|
+
expect(parseLayout("flex-grow-[-2]")).toBeNull();
|
|
135
|
+
expect(parseLayout("flex-shrink-[-0.5]")).toBeNull();
|
|
136
|
+
});
|
|
76
137
|
});
|
|
77
138
|
|
|
78
139
|
describe("parseLayout - justify content utilities", () => {
|