@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
package/src/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,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for processing platform modifiers (ios:, android:, web:)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type * as BabelTypes from "@babel/types";
|
|
6
|
+
import type { ParsedModifier, PlatformModifierType } from "../../parser/index.js";
|
|
7
|
+
import type { StyleObject } from "../../types/core.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Plugin state interface (subset needed for platform modifier processing)
|
|
11
|
+
*/
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
13
|
+
export interface PlatformModifierProcessingState {
|
|
14
|
+
styleRegistry: Map<string, StyleObject>;
|
|
15
|
+
customColors: Record<string, string>;
|
|
16
|
+
stylesIdentifier: string;
|
|
17
|
+
needsPlatformImport: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Process platform modifiers and generate Platform.select() expression
|
|
22
|
+
*
|
|
23
|
+
* @param platformModifiers - Array of parsed platform modifiers
|
|
24
|
+
* @param state - Plugin state
|
|
25
|
+
* @param parseClassName - Function to parse class names into style objects
|
|
26
|
+
* @param generateStyleKey - Function to generate unique style keys
|
|
27
|
+
* @param t - Babel types
|
|
28
|
+
* @returns AST node for Platform.select() call
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* Input: [{ modifier: "ios", baseClass: "shadow-lg" }, { modifier: "android", baseClass: "elevation-4" }]
|
|
32
|
+
* Output: Platform.select({ ios: styles._ios_shadow_lg, android: styles._android_elevation_4 })
|
|
33
|
+
*/
|
|
34
|
+
export function processPlatformModifiers(
|
|
35
|
+
platformModifiers: ParsedModifier[],
|
|
36
|
+
state: PlatformModifierProcessingState,
|
|
37
|
+
parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
|
|
38
|
+
generateStyleKey: (className: string) => string,
|
|
39
|
+
t: typeof BabelTypes,
|
|
40
|
+
): BabelTypes.Expression {
|
|
41
|
+
// Mark that we need Platform import
|
|
42
|
+
state.needsPlatformImport = true;
|
|
43
|
+
|
|
44
|
+
// Group modifiers by platform
|
|
45
|
+
const modifiersByPlatform = new Map<PlatformModifierType, ParsedModifier[]>();
|
|
46
|
+
|
|
47
|
+
for (const mod of platformModifiers) {
|
|
48
|
+
const platform = mod.modifier as PlatformModifierType;
|
|
49
|
+
if (!modifiersByPlatform.has(platform)) {
|
|
50
|
+
modifiersByPlatform.set(platform, []);
|
|
51
|
+
}
|
|
52
|
+
const platformGroup = modifiersByPlatform.get(platform);
|
|
53
|
+
if (platformGroup) {
|
|
54
|
+
platformGroup.push(mod);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Build Platform.select() object properties
|
|
59
|
+
const selectProperties: BabelTypes.ObjectProperty[] = [];
|
|
60
|
+
|
|
61
|
+
for (const [platform, modifiers] of modifiersByPlatform) {
|
|
62
|
+
// Parse all classes for this platform together
|
|
63
|
+
const classNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
64
|
+
const styleObject = parseClassName(classNames, state.customColors);
|
|
65
|
+
const styleKey = generateStyleKey(`${platform}_${classNames}`);
|
|
66
|
+
|
|
67
|
+
// Register style in the registry
|
|
68
|
+
state.styleRegistry.set(styleKey, styleObject);
|
|
69
|
+
|
|
70
|
+
// Create property: ios: styles._ios_shadow_lg
|
|
71
|
+
const styleReference = t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey));
|
|
72
|
+
|
|
73
|
+
selectProperties.push(t.objectProperty(t.identifier(platform), styleReference));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Create Platform.select({ ios: ..., android: ... })
|
|
77
|
+
return t.callExpression(t.memberExpression(t.identifier("Platform"), t.identifier("select")), [
|
|
78
|
+
t.objectExpression(selectProperties),
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
@@ -7,16 +7,61 @@ import type * as BabelTypes from "@babel/types";
|
|
|
7
7
|
import type { StyleObject } from "../../types/core.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Add StyleSheet import to the file
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
// Check if there's already a react-native import
|
|
14
|
+
const body = path.node.body;
|
|
15
|
+
let reactNativeImport: BabelTypes.ImportDeclaration | null = null;
|
|
16
|
+
|
|
17
|
+
for (const statement of body) {
|
|
18
|
+
if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
|
|
19
|
+
reactNativeImport = statement;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
if (reactNativeImport) {
|
|
25
|
+
// Add StyleSheet to existing react-native import
|
|
26
|
+
reactNativeImport.specifiers.push(
|
|
27
|
+
t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet")),
|
|
28
|
+
);
|
|
29
|
+
} else {
|
|
30
|
+
// Create new react-native import with StyleSheet
|
|
31
|
+
const importDeclaration = t.importDeclaration(
|
|
32
|
+
[t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet"))],
|
|
33
|
+
t.stringLiteral("react-native"),
|
|
34
|
+
);
|
|
35
|
+
path.unshiftContainer("body", importDeclaration);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Add Platform import to the file or merge with existing react-native import
|
|
41
|
+
*/
|
|
42
|
+
export function addPlatformImport(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void {
|
|
43
|
+
// Check if there's already a react-native import
|
|
44
|
+
const body = path.node.body;
|
|
45
|
+
let reactNativeImport: BabelTypes.ImportDeclaration | null = null;
|
|
46
|
+
|
|
47
|
+
for (const statement of body) {
|
|
48
|
+
if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
|
|
49
|
+
reactNativeImport = statement;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (reactNativeImport) {
|
|
55
|
+
// Add Platform to existing react-native import
|
|
56
|
+
reactNativeImport.specifiers.push(t.importSpecifier(t.identifier("Platform"), t.identifier("Platform")));
|
|
57
|
+
} else {
|
|
58
|
+
// Create new react-native import with Platform
|
|
59
|
+
const importDeclaration = t.importDeclaration(
|
|
60
|
+
[t.importSpecifier(t.identifier("Platform"), t.identifier("Platform"))],
|
|
61
|
+
t.stringLiteral("react-native"),
|
|
62
|
+
);
|
|
63
|
+
path.unshiftContainer("body", importDeclaration);
|
|
64
|
+
}
|
|
20
65
|
}
|
|
21
66
|
|
|
22
67
|
/**
|
|
@@ -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. ` +
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { ASPECT_RATIO_PRESETS, parseAspectRatio } from "./aspectRatio";
|
|
3
|
+
import { parseClassName } from "./index";
|
|
3
4
|
|
|
4
5
|
describe("ASPECT_RATIO_PRESETS", () => {
|
|
5
6
|
it("should export aspect ratio presets", () => {
|
|
@@ -29,8 +30,8 @@ describe("parseAspectRatio - preset values", () => {
|
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
it("should parse aspect-auto", () => {
|
|
32
|
-
// aspect-auto removes the aspect ratio constraint
|
|
33
|
-
expect(parseAspectRatio("aspect-auto")).toEqual({});
|
|
33
|
+
// aspect-auto removes the aspect ratio constraint by explicitly setting to undefined
|
|
34
|
+
expect(parseAspectRatio("aspect-auto")).toEqual({ aspectRatio: undefined });
|
|
34
35
|
});
|
|
35
36
|
});
|
|
36
37
|
|
|
@@ -131,6 +132,28 @@ describe("parseAspectRatio - type validation", () => {
|
|
|
131
132
|
});
|
|
132
133
|
});
|
|
133
134
|
|
|
135
|
+
describe("parseAspectRatio - override behavior", () => {
|
|
136
|
+
it("should allow aspect-auto to override previously set aspect ratios", () => {
|
|
137
|
+
// This tests the fix for issue #3 - aspect-auto must explicitly set aspectRatio: undefined
|
|
138
|
+
// to override previous aspect ratio values when using Object.assign
|
|
139
|
+
const result = parseClassName("aspect-square aspect-auto");
|
|
140
|
+
expect(result).toEqual({ aspectRatio: undefined });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should allow aspect ratios to override aspect-auto", () => {
|
|
144
|
+
const result = parseClassName("aspect-auto aspect-square");
|
|
145
|
+
expect(result).toEqual({ aspectRatio: 1 });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should apply last aspect ratio in sequence", () => {
|
|
149
|
+
const result = parseClassName("aspect-square aspect-video aspect-auto");
|
|
150
|
+
expect(result).toEqual({ aspectRatio: undefined });
|
|
151
|
+
|
|
152
|
+
const result2 = parseClassName("aspect-auto aspect-square aspect-video");
|
|
153
|
+
expect(result2).toEqual({ aspectRatio: 16 / 9 });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
134
157
|
describe("parseAspectRatio - comprehensive coverage", () => {
|
|
135
158
|
it("should parse all preset variants without errors", () => {
|
|
136
159
|
const presets = ["aspect-auto", "aspect-square", "aspect-video"];
|
|
@@ -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
|
}
|
|
@@ -51,10 +52,10 @@ export function parseAspectRatio(cls: string): StyleObject | null {
|
|
|
51
52
|
// Check for preset values
|
|
52
53
|
if (cls in ASPECT_RATIO_PRESETS) {
|
|
53
54
|
const aspectRatio = ASPECT_RATIO_PRESETS[cls];
|
|
54
|
-
// aspect-auto removes the aspect ratio constraint by
|
|
55
|
-
//
|
|
55
|
+
// aspect-auto removes the aspect ratio constraint by explicitly setting to undefined
|
|
56
|
+
// This ensures it overrides any previously set aspectRatio in Object.assign
|
|
56
57
|
if (aspectRatio === undefined) {
|
|
57
|
-
return {};
|
|
58
|
+
return { aspectRatio: undefined };
|
|
58
59
|
}
|
|
59
60
|
return { aspectRatio };
|
|
60
61
|
}
|
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. ` +
|