@mgcrea/react-native-tailwind 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -0
- package/dist/babel/config-loader.ts +2 -0
- package/dist/babel/index.cjs +177 -4
- package/dist/babel/plugin.d.ts +2 -0
- package/dist/babel/plugin.test.ts +241 -0
- package/dist/babel/plugin.ts +187 -10
- package/dist/babel/utils/platformModifierProcessing.d.ts +30 -0
- package/dist/babel/utils/platformModifierProcessing.ts +80 -0
- package/dist/babel/utils/styleInjection.d.ts +4 -0
- package/dist/babel/utils/styleInjection.ts +28 -0
- package/dist/babel/utils/styleTransforms.ts +1 -0
- 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/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/stubs/tw.test.js +1 -0
- package/package.json +6 -5
- package/src/babel/config-loader.ts +2 -0
- package/src/babel/plugin.test.ts +241 -0
- package/src/babel/plugin.ts +187 -10
- package/src/babel/utils/platformModifierProcessing.ts +80 -0
- package/src/babel/utils/styleInjection.ts +28 -0
- package/src/babel/utils/styleTransforms.ts +1 -0
- package/src/parser/aspectRatio.ts +1 -0
- 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.ts +1 -0
- package/src/parser/transforms.ts +5 -0
- package/src/parser/typography.ts +2 -0
- package/src/stubs/tw.test.ts +27 -0
|
@@ -19,6 +19,34 @@ export function addStyleSheetImport(path: NodePath<BabelTypes.Program>, t: typeo
|
|
|
19
19
|
path.unshiftContainer("body", importDeclaration);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Add Platform import to the file or merge with existing react-native import
|
|
24
|
+
*/
|
|
25
|
+
export function addPlatformImport(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void {
|
|
26
|
+
// Check if there's already a react-native import
|
|
27
|
+
const body = path.node.body;
|
|
28
|
+
let reactNativeImport: BabelTypes.ImportDeclaration | null = null;
|
|
29
|
+
|
|
30
|
+
for (const statement of body) {
|
|
31
|
+
if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
|
|
32
|
+
reactNativeImport = statement;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (reactNativeImport) {
|
|
38
|
+
// Add Platform to existing react-native import
|
|
39
|
+
reactNativeImport.specifiers.push(t.importSpecifier(t.identifier("Platform"), t.identifier("Platform")));
|
|
40
|
+
} else {
|
|
41
|
+
// Create new react-native import with Platform
|
|
42
|
+
const importDeclaration = t.importDeclaration(
|
|
43
|
+
[t.importSpecifier(t.identifier("Platform"), t.identifier("Platform"))],
|
|
44
|
+
t.stringLiteral("react-native"),
|
|
45
|
+
);
|
|
46
|
+
path.unshiftContainer("body", importDeclaration);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
22
50
|
/**
|
|
23
51
|
* Inject StyleSheet.create with all collected styles at the top of the file
|
|
24
52
|
* This ensures the styles object is defined before any code that references it
|
|
@@ -242,6 +242,7 @@ export function addOrMergePlaceholderTextColorProp(
|
|
|
242
242
|
if (existingProp) {
|
|
243
243
|
// If explicit prop exists, don't override it (explicit props take precedence)
|
|
244
244
|
// This matches the behavior of style prop precedence
|
|
245
|
+
/* v8 ignore next 5 */
|
|
245
246
|
if (process.env.NODE_ENV !== "production") {
|
|
246
247
|
console.warn(
|
|
247
248
|
`[react-native-tailwind] placeholderTextColor prop will be overridden by className placeholder: modifier. ` +
|
|
@@ -26,6 +26,7 @@ function parseArbitraryAspectRatio(value: string): number | null {
|
|
|
26
26
|
const denominator = Number.parseInt(match[2], 10);
|
|
27
27
|
|
|
28
28
|
if (denominator === 0) {
|
|
29
|
+
/* v8 ignore next 3 */
|
|
29
30
|
if (process.env.NODE_ENV !== "production") {
|
|
30
31
|
console.warn(`[react-native-tailwind] Invalid aspect ratio: ${value}. Denominator cannot be zero.`);
|
|
31
32
|
}
|
package/src/parser/borders.ts
CHANGED
|
@@ -69,6 +69,7 @@ function parseArbitraryBorderWidth(value: string): number | null {
|
|
|
69
69
|
|
|
70
70
|
// Warn about unsupported formats
|
|
71
71
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
72
|
+
/* v8 ignore next 5 */
|
|
72
73
|
if (process.env.NODE_ENV !== "production") {
|
|
73
74
|
console.warn(
|
|
74
75
|
`[react-native-tailwind] Unsupported arbitrary border width value: ${value}. Only px values are supported (e.g., [8px] or [8]).`,
|
|
@@ -93,6 +94,7 @@ function parseArbitraryBorderRadius(value: string): number | null {
|
|
|
93
94
|
|
|
94
95
|
// Warn about unsupported formats
|
|
95
96
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
97
|
+
/* v8 ignore next 5 */
|
|
96
98
|
if (process.env.NODE_ENV !== "production") {
|
|
97
99
|
console.warn(
|
|
98
100
|
`[react-native-tailwind] Unsupported arbitrary border radius value: ${value}. Only px values are supported (e.g., [12px] or [12]).`,
|
package/src/parser/colors.ts
CHANGED
|
@@ -70,6 +70,7 @@ function parseArbitraryColor(value: string): string | null {
|
|
|
70
70
|
|
|
71
71
|
// Warn about unsupported formats
|
|
72
72
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
73
|
+
/* v8 ignore next 5 */
|
|
73
74
|
if (process.env.NODE_ENV !== "production") {
|
|
74
75
|
console.warn(
|
|
75
76
|
`[react-native-tailwind] Unsupported arbitrary color value: ${value}. Only hex colors are supported (e.g., [#ff0000], [#f00], or [#ff0000aa]).`,
|
|
@@ -101,6 +102,7 @@ export function parseColor(cls: string, customColors?: Record<string, string>):
|
|
|
101
102
|
|
|
102
103
|
// Validate opacity range (0-100)
|
|
103
104
|
if (opacity < 0 || opacity > 100) {
|
|
105
|
+
/* v8 ignore next 5 */
|
|
104
106
|
if (process.env.NODE_ENV !== "production") {
|
|
105
107
|
console.warn(
|
|
106
108
|
`[react-native-tailwind] Invalid opacity value: ${opacity}. Opacity must be between 0 and 100.`,
|
package/src/parser/index.ts
CHANGED
|
@@ -62,6 +62,7 @@ export function parseClass(cls: string, customColors?: Record<string, string>):
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Warn about unknown class in development
|
|
65
|
+
/* v8 ignore next 3 */
|
|
65
66
|
if (process.env.NODE_ENV !== "production") {
|
|
66
67
|
console.warn(`[react-native-tailwind] Unknown class: "${cls}"`);
|
|
67
68
|
}
|
|
@@ -82,5 +83,11 @@ export { parseTransform } from "./transforms";
|
|
|
82
83
|
export { parseTypography } from "./typography";
|
|
83
84
|
|
|
84
85
|
// Re-export modifier utilities
|
|
85
|
-
export {
|
|
86
|
-
|
|
86
|
+
export {
|
|
87
|
+
hasModifier,
|
|
88
|
+
isPlatformModifier,
|
|
89
|
+
isStateModifier,
|
|
90
|
+
parseModifier,
|
|
91
|
+
splitModifierClasses,
|
|
92
|
+
} from "./modifiers";
|
|
93
|
+
export type { ModifierType, ParsedModifier, PlatformModifierType, StateModifierType } from "./modifiers";
|
package/src/parser/layout.ts
CHANGED
|
@@ -23,6 +23,7 @@ function parseArbitraryInset(value: string): number | string | null {
|
|
|
23
23
|
|
|
24
24
|
// Unsupported units (rem, em, vh, vw, etc.) - warn and reject
|
|
25
25
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
26
|
+
/* v8 ignore next 5 */
|
|
26
27
|
if (process.env.NODE_ENV !== "production") {
|
|
27
28
|
console.warn(
|
|
28
29
|
`[react-native-tailwind] Unsupported arbitrary inset unit: ${value}. Only px and % are supported.`,
|
|
@@ -47,6 +48,7 @@ function parseArbitraryZIndex(value: string): number | null {
|
|
|
47
48
|
|
|
48
49
|
// Unsupported format - warn and reject
|
|
49
50
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
51
|
+
/* v8 ignore next 5 */
|
|
50
52
|
if (process.env.NODE_ENV !== "production") {
|
|
51
53
|
console.warn(
|
|
52
54
|
`[react-native-tailwind] Invalid arbitrary z-index: ${value}. Only integers are supported.`,
|
package/src/parser/modifiers.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Modifier parsing utilities for state-based class names
|
|
2
|
+
* Modifier parsing utilities for state-based and platform-specific class names
|
|
3
|
+
* - State modifiers: active:, hover:, focus:, disabled:, placeholder:
|
|
4
|
+
* - Platform modifiers: ios:, android:, web:
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
|
-
export type
|
|
7
|
+
export type StateModifierType = "active" | "hover" | "focus" | "disabled" | "placeholder";
|
|
8
|
+
export type PlatformModifierType = "ios" | "android" | "web";
|
|
9
|
+
export type ModifierType = StateModifierType | PlatformModifierType;
|
|
6
10
|
|
|
7
11
|
export type ParsedModifier = {
|
|
8
12
|
modifier: ModifierType;
|
|
@@ -10,9 +14,9 @@ export type ParsedModifier = {
|
|
|
10
14
|
};
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
|
-
* Supported modifiers that map to component states or pseudo-elements
|
|
17
|
+
* Supported state modifiers that map to component states or pseudo-elements
|
|
14
18
|
*/
|
|
15
|
-
const
|
|
19
|
+
const STATE_MODIFIERS: readonly StateModifierType[] = [
|
|
16
20
|
"active",
|
|
17
21
|
"hover",
|
|
18
22
|
"focus",
|
|
@@ -20,6 +24,16 @@ const SUPPORTED_MODIFIERS: readonly ModifierType[] = [
|
|
|
20
24
|
"placeholder",
|
|
21
25
|
] as const;
|
|
22
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Supported platform modifiers that map to Platform.OS values
|
|
29
|
+
*/
|
|
30
|
+
const PLATFORM_MODIFIERS: readonly PlatformModifierType[] = ["ios", "android", "web"] as const;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* All supported modifiers (state + platform)
|
|
34
|
+
*/
|
|
35
|
+
const SUPPORTED_MODIFIERS: readonly ModifierType[] = [...STATE_MODIFIERS, ...PLATFORM_MODIFIERS] as const;
|
|
36
|
+
|
|
23
37
|
/**
|
|
24
38
|
* Parse a class name to detect and extract modifiers
|
|
25
39
|
*
|
|
@@ -73,6 +87,26 @@ export function hasModifier(cls: string): boolean {
|
|
|
73
87
|
return parseModifier(cls) !== null;
|
|
74
88
|
}
|
|
75
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Check if a modifier is a state modifier (active, hover, focus, disabled, placeholder)
|
|
92
|
+
*
|
|
93
|
+
* @param modifier - Modifier type to check
|
|
94
|
+
* @returns true if modifier is a state modifier
|
|
95
|
+
*/
|
|
96
|
+
export function isStateModifier(modifier: ModifierType): modifier is StateModifierType {
|
|
97
|
+
return STATE_MODIFIERS.includes(modifier as StateModifierType);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a modifier is a platform modifier (ios, android, web)
|
|
102
|
+
*
|
|
103
|
+
* @param modifier - Modifier type to check
|
|
104
|
+
* @returns true if modifier is a platform modifier
|
|
105
|
+
*/
|
|
106
|
+
export function isPlatformModifier(modifier: ModifierType): modifier is PlatformModifierType {
|
|
107
|
+
return PLATFORM_MODIFIERS.includes(modifier as PlatformModifierType);
|
|
108
|
+
}
|
|
109
|
+
|
|
76
110
|
/**
|
|
77
111
|
* Split a space-separated className string into base and modifier classes
|
|
78
112
|
*
|
|
@@ -27,6 +27,7 @@ export function parsePlaceholderClass(cls: string, customColors?: Record<string,
|
|
|
27
27
|
// Check if it's a text color class
|
|
28
28
|
if (!cls.startsWith("text-")) {
|
|
29
29
|
// Warn about unsupported utilities
|
|
30
|
+
/* v8 ignore next 5 */
|
|
30
31
|
if (process.env.NODE_ENV !== "production") {
|
|
31
32
|
console.warn(
|
|
32
33
|
`[react-native-tailwind] Only text color utilities are supported in placeholder: modifier. ` +
|
package/src/parser/sizing.ts
CHANGED
|
@@ -80,6 +80,7 @@ function parseArbitrarySize(value: string): number | string | null {
|
|
|
80
80
|
|
|
81
81
|
// Unsupported units (rem, em, vh, vw, etc.) - warn and reject
|
|
82
82
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
83
|
+
/* v8 ignore next 5 */
|
|
83
84
|
if (process.env.NODE_ENV !== "production") {
|
|
84
85
|
console.warn(
|
|
85
86
|
`[react-native-tailwind] Unsupported arbitrary size unit: ${value}. Only px and % are supported.`,
|
package/src/parser/spacing.ts
CHANGED
|
@@ -55,6 +55,7 @@ function parseArbitrarySpacing(value: string): number | null {
|
|
|
55
55
|
|
|
56
56
|
// Warn about unsupported formats
|
|
57
57
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
58
|
+
/* v8 ignore next 5 */
|
|
58
59
|
if (process.env.NODE_ENV !== "production") {
|
|
59
60
|
console.warn(
|
|
60
61
|
`[react-native-tailwind] Unsupported arbitrary spacing value: ${value}. Only px values are supported (e.g., [16px] or [16]).`,
|
package/src/parser/transforms.ts
CHANGED
|
@@ -70,6 +70,7 @@ function parseArbitraryScale(value: string): number | null {
|
|
|
70
70
|
|
|
71
71
|
// Unsupported format
|
|
72
72
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
73
|
+
/* v8 ignore next 5 */
|
|
73
74
|
if (process.env.NODE_ENV !== "production") {
|
|
74
75
|
console.warn(
|
|
75
76
|
`[react-native-tailwind] Invalid arbitrary scale value: ${value}. Only numbers are supported (e.g., [1.5], [0.75]).`,
|
|
@@ -93,6 +94,7 @@ function parseArbitraryRotation(value: string): string | null {
|
|
|
93
94
|
|
|
94
95
|
// Unsupported format
|
|
95
96
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
97
|
+
/* v8 ignore next 5 */
|
|
96
98
|
if (process.env.NODE_ENV !== "production") {
|
|
97
99
|
console.warn(
|
|
98
100
|
`[react-native-tailwind] Invalid arbitrary rotation value: ${value}. Only deg unit is supported (e.g., [45deg], [-15deg]).`,
|
|
@@ -123,6 +125,7 @@ function parseArbitraryTranslation(value: string): number | string | null {
|
|
|
123
125
|
|
|
124
126
|
// Unsupported units
|
|
125
127
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
128
|
+
/* v8 ignore next 5 */
|
|
126
129
|
if (process.env.NODE_ENV !== "production") {
|
|
127
130
|
console.warn(
|
|
128
131
|
`[react-native-tailwind] Unsupported arbitrary translation unit: ${value}. Only px and % are supported.`,
|
|
@@ -146,6 +149,7 @@ function parseArbitraryPerspective(value: string): number | null {
|
|
|
146
149
|
|
|
147
150
|
// Unsupported format
|
|
148
151
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
152
|
+
/* v8 ignore next 5 */
|
|
149
153
|
if (process.env.NODE_ENV !== "production") {
|
|
150
154
|
console.warn(
|
|
151
155
|
`[react-native-tailwind] Invalid arbitrary perspective value: ${value}. Only integers are supported (e.g., [1500]).`,
|
|
@@ -164,6 +168,7 @@ function parseArbitraryPerspective(value: string): number | null {
|
|
|
164
168
|
export function parseTransform(cls: string): StyleObject | null {
|
|
165
169
|
// Transform origin warning (not supported in React Native)
|
|
166
170
|
if (cls.startsWith("origin-")) {
|
|
171
|
+
/* v8 ignore next 5 */
|
|
167
172
|
if (process.env.NODE_ENV !== "production") {
|
|
168
173
|
console.warn(
|
|
169
174
|
`[react-native-tailwind] transform-origin is not supported in React Native. Class "${cls}" will be ignored.`,
|
package/src/parser/typography.ts
CHANGED
|
@@ -125,6 +125,7 @@ function parseArbitraryFontSize(value: string): number | null {
|
|
|
125
125
|
|
|
126
126
|
// Warn about unsupported formats
|
|
127
127
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
128
|
+
/* v8 ignore next 5 */
|
|
128
129
|
if (process.env.NODE_ENV !== "production") {
|
|
129
130
|
console.warn(
|
|
130
131
|
`[react-native-tailwind] Unsupported arbitrary font size value: ${value}. Only px values are supported (e.g., [18px] or [18]).`,
|
|
@@ -149,6 +150,7 @@ function parseArbitraryLineHeight(value: string): number | null {
|
|
|
149
150
|
|
|
150
151
|
// Warn about unsupported formats
|
|
151
152
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
153
|
+
/* v8 ignore next 5 */
|
|
152
154
|
if (process.env.NODE_ENV !== "production") {
|
|
153
155
|
console.warn(
|
|
154
156
|
`[react-native-tailwind] Unsupported arbitrary line height value: ${value}. Only px values are supported (e.g., [24px] or [24]).`,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { tw, twStyle } from "./tw";
|
|
3
|
+
|
|
4
|
+
describe("tw stub", () => {
|
|
5
|
+
it("should throw error when tw() is called without Babel transformation", () => {
|
|
6
|
+
expect(() => tw`bg-blue-500`).toThrow(
|
|
7
|
+
"tw() must be transformed by the Babel plugin. " +
|
|
8
|
+
"Ensure @mgcrea/react-native-tailwind/babel is configured in your babel.config.js. " +
|
|
9
|
+
"For runtime parsing, use: import { tw } from '@mgcrea/react-native-tailwind/runtime'",
|
|
10
|
+
);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should throw error when twStyle() is called without Babel transformation", () => {
|
|
14
|
+
expect(() => twStyle("bg-blue-500")).toThrow(
|
|
15
|
+
"twStyle() must be transformed by the Babel plugin. " +
|
|
16
|
+
"Ensure @mgcrea/react-native-tailwind/babel is configured in your babel.config.js. " +
|
|
17
|
+
"For runtime parsing, use: import { twStyle } from '@mgcrea/react-native-tailwind/runtime'",
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should throw error with template literal interpolation", () => {
|
|
22
|
+
const dynamic = "active";
|
|
23
|
+
expect(() => tw`bg-blue-500 ${dynamic}:bg-blue-700`).toThrow(
|
|
24
|
+
"tw() must be transformed by the Babel plugin",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
});
|