@mgcrea/react-native-tailwind 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +152 -0
  2. package/dist/babel/config-loader.ts +2 -0
  3. package/dist/babel/index.cjs +205 -17
  4. package/dist/babel/plugin.d.ts +4 -1
  5. package/dist/babel/plugin.test.ts +327 -0
  6. package/dist/babel/plugin.ts +194 -14
  7. package/dist/babel/utils/platformModifierProcessing.d.ts +30 -0
  8. package/dist/babel/utils/platformModifierProcessing.ts +80 -0
  9. package/dist/babel/utils/styleInjection.d.ts +5 -1
  10. package/dist/babel/utils/styleInjection.ts +52 -7
  11. package/dist/babel/utils/styleTransforms.ts +1 -0
  12. package/dist/parser/aspectRatio.js +1 -1
  13. package/dist/parser/aspectRatio.test.js +1 -1
  14. package/dist/parser/index.d.ts +2 -2
  15. package/dist/parser/index.js +1 -1
  16. package/dist/parser/modifiers.d.ts +20 -2
  17. package/dist/parser/modifiers.js +1 -1
  18. package/dist/parser/spacing.d.ts +1 -1
  19. package/dist/parser/spacing.js +1 -1
  20. package/dist/parser/spacing.test.js +1 -1
  21. package/dist/runtime.cjs +1 -1
  22. package/dist/runtime.cjs.map +4 -4
  23. package/dist/runtime.js +1 -1
  24. package/dist/runtime.js.map +4 -4
  25. package/dist/runtime.test.js +1 -1
  26. package/dist/stubs/tw.test.js +1 -0
  27. package/package.json +7 -7
  28. package/src/babel/config-loader.ts +2 -0
  29. package/src/babel/plugin.test.ts +327 -0
  30. package/src/babel/plugin.ts +194 -14
  31. package/src/babel/utils/platformModifierProcessing.ts +80 -0
  32. package/src/babel/utils/styleInjection.ts +52 -7
  33. package/src/babel/utils/styleTransforms.ts +1 -0
  34. package/src/parser/aspectRatio.test.ts +25 -2
  35. package/src/parser/aspectRatio.ts +4 -3
  36. package/src/parser/borders.ts +2 -0
  37. package/src/parser/colors.ts +2 -0
  38. package/src/parser/index.ts +9 -2
  39. package/src/parser/layout.ts +2 -0
  40. package/src/parser/modifiers.ts +38 -4
  41. package/src/parser/placeholder.ts +1 -0
  42. package/src/parser/sizing.ts +1 -0
  43. package/src/parser/spacing.test.ts +63 -0
  44. package/src/parser/spacing.ts +11 -6
  45. package/src/parser/transforms.ts +5 -0
  46. package/src/parser/typography.ts +2 -0
  47. package/src/runtime.test.ts +27 -0
  48. package/src/runtime.ts +2 -1
  49. package/src/stubs/tw.test.ts +27 -0
  50. package/dist/babel/index.test.ts +0 -481
  51. package/dist/config/palettes.d.ts +0 -302
  52. package/dist/config/palettes.js +0 -1
  53. package/dist/parser/__snapshots__/aspectRatio.test.js.snap +0 -9
  54. package/dist/parser/__snapshots__/borders.test.js.snap +0 -23
  55. package/dist/parser/__snapshots__/colors.test.js.snap +0 -251
  56. package/dist/parser/__snapshots__/shadows.test.js.snap +0 -76
  57. package/dist/parser/__snapshots__/sizing.test.js.snap +0 -61
  58. package/dist/parser/__snapshots__/spacing.test.js.snap +0 -40
  59. package/dist/parser/__snapshots__/transforms.test.js.snap +0 -58
  60. package/dist/parser/__snapshots__/typography.test.js.snap +0 -30
  61. package/dist/parser/aspectRatio.test.d.ts +0 -1
  62. package/dist/parser/borders.test.d.ts +0 -1
  63. package/dist/parser/colors.test.d.ts +0 -1
  64. package/dist/parser/layout.test.d.ts +0 -1
  65. package/dist/parser/modifiers.test.d.ts +0 -1
  66. package/dist/parser/shadows.test.d.ts +0 -1
  67. package/dist/parser/sizing.test.d.ts +0 -1
  68. package/dist/parser/spacing.test.d.ts +0 -1
  69. package/dist/parser/typography.test.d.ts +0 -1
  70. package/dist/types.d.ts +0 -42
  71. package/dist/types.js +0 -1
@@ -5,7 +5,14 @@
5
5
 
6
6
  import type { NodePath, PluginObj, PluginPass } from "@babel/core";
7
7
  import * as BabelTypes from "@babel/types";
8
- import { parseClassName, parsePlaceholderClasses, splitModifierClasses } from "../parser/index.js";
8
+ import type { ParsedModifier, StateModifierType } from "../parser/index.js";
9
+ import {
10
+ isPlatformModifier,
11
+ isStateModifier,
12
+ parseClassName,
13
+ parsePlaceholderClasses,
14
+ splitModifierClasses,
15
+ } from "../parser/index.js";
9
16
  import type { StyleObject } from "../types/core.js";
10
17
  import { generateStyleKey } from "../utils/styleKey.js";
11
18
  import { extractCustomColors } from "./config-loader.js";
@@ -17,10 +24,11 @@ import {
17
24
  getTargetStyleProp,
18
25
  isAttributeSupported,
19
26
  } from "./utils/attributeMatchers.js";
20
- import { getComponentModifierSupport } from "./utils/componentSupport.js";
27
+ import { getComponentModifierSupport, getStatePropertyForModifier } from "./utils/componentSupport.js";
21
28
  import { processDynamicExpression } from "./utils/dynamicProcessing.js";
22
29
  import { createStyleFunction, processStaticClassNameWithModifiers } from "./utils/modifierProcessing.js";
23
- import { addStyleSheetImport, injectStylesAtTop } from "./utils/styleInjection.js";
30
+ import { processPlatformModifiers } from "./utils/platformModifierProcessing.js";
31
+ import { addPlatformImport, addStyleSheetImport, injectStylesAtTop } from "./utils/styleInjection.js";
24
32
  import {
25
33
  addOrMergePlaceholderTextColorProp,
26
34
  findStyleAttribute,
@@ -59,6 +67,8 @@ type PluginState = PluginPass & {
59
67
  styleRegistry: Map<string, StyleObject>;
60
68
  hasClassNames: boolean;
61
69
  hasStyleSheetImport: boolean;
70
+ hasPlatformImport: boolean;
71
+ needsPlatformImport: boolean;
62
72
  customColors: Record<string, string>;
63
73
  supportedAttributes: Set<string>;
64
74
  attributePatterns: RegExp[];
@@ -66,6 +76,8 @@ type PluginState = PluginPass & {
66
76
  // Track tw/twStyle imports from main package
67
77
  twImportNames: Set<string>; // e.g., ['tw', 'twStyle'] or ['tw as customTw']
68
78
  hasTwImport: boolean;
79
+ // Track react-native import path for conditional StyleSheet/Platform injection
80
+ reactNativeImportPath?: NodePath<BabelTypes.ImportDeclaration>;
69
81
  };
70
82
 
71
83
  // Default identifier for the generated StyleSheet constant
@@ -90,6 +102,8 @@ export default function reactNativeTailwindBabelPlugin(
90
102
  state.styleRegistry = new Map();
91
103
  state.hasClassNames = false;
92
104
  state.hasStyleSheetImport = false;
105
+ state.hasPlatformImport = false;
106
+ state.needsPlatformImport = false;
93
107
  state.supportedAttributes = exactMatches;
94
108
  state.attributePatterns = patterns;
95
109
  state.stylesIdentifier = stylesIdentifier;
@@ -116,19 +130,25 @@ export default function reactNativeTailwindBabelPlugin(
116
130
  addStyleSheetImport(path, t);
117
131
  }
118
132
 
133
+ // Add Platform import if platform modifiers were used and not already present
134
+ if (state.needsPlatformImport && !state.hasPlatformImport) {
135
+ addPlatformImport(path, t);
136
+ }
137
+
119
138
  // Generate and inject StyleSheet.create at the beginning of the file (after imports)
120
139
  // This ensures _twStyles is defined before any code that references it
121
140
  injectStylesAtTop(path, state.styleRegistry, state.stylesIdentifier, t);
122
141
  },
123
142
  },
124
143
 
125
- // Check if StyleSheet is already imported and track tw/twStyle imports
144
+ // Check if StyleSheet/Platform are already imported and track tw/twStyle imports
126
145
  ImportDeclaration(path, state) {
127
146
  const node = path.node;
128
147
 
129
- // Track react-native StyleSheet import
148
+ // Track react-native StyleSheet and Platform imports
130
149
  if (node.source.value === "react-native") {
131
150
  const specifiers = node.specifiers;
151
+
132
152
  const hasStyleSheet = specifiers.some((spec) => {
133
153
  if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
134
154
  return spec.imported.name === "StyleSheet";
@@ -136,13 +156,25 @@ export default function reactNativeTailwindBabelPlugin(
136
156
  return false;
137
157
  });
138
158
 
159
+ const hasPlatform = specifiers.some((spec) => {
160
+ if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
161
+ return spec.imported.name === "Platform";
162
+ }
163
+ return false;
164
+ });
165
+
166
+ // Only track if imports exist - don't mutate yet
167
+ // Actual import injection happens in Program.exit only if needed
139
168
  if (hasStyleSheet) {
140
169
  state.hasStyleSheetImport = true;
141
- } else {
142
- // Add StyleSheet to existing import
143
- node.specifiers.push(t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet")));
144
- state.hasStyleSheetImport = true;
145
170
  }
171
+
172
+ if (hasPlatform) {
173
+ state.hasPlatformImport = true;
174
+ }
175
+
176
+ // Store reference to the react-native import for later modification if needed
177
+ state.reactNativeImportPath = path;
146
178
  }
147
179
 
148
180
  // Track tw/twStyle imports from main package (for compile-time transformation)
@@ -291,12 +323,15 @@ export default function reactNativeTailwindBabelPlugin(
291
323
 
292
324
  state.hasClassNames = true;
293
325
 
294
- // Check if className contains modifiers (active:, hover:, focus:, placeholder:)
326
+ // Check if className contains modifiers (active:, hover:, focus:, placeholder:, ios:, android:, web:)
295
327
  const { baseClasses, modifierClasses } = splitModifierClasses(className);
296
328
 
297
- // Separate placeholder modifiers from state modifiers
329
+ // Separate modifiers by type
298
330
  const placeholderModifiers = modifierClasses.filter((m) => m.modifier === "placeholder");
299
- const stateModifiers = modifierClasses.filter((m) => m.modifier !== "placeholder");
331
+ const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
332
+ const stateModifiers = modifierClasses.filter(
333
+ (m) => isStateModifier(m.modifier) && m.modifier !== "placeholder",
334
+ );
300
335
 
301
336
  // Handle placeholder modifiers first (they generate placeholderTextColor prop, not style)
302
337
  if (placeholderModifiers.length > 0) {
@@ -322,8 +357,153 @@ export default function reactNativeTailwindBabelPlugin(
322
357
  }
323
358
  }
324
359
 
325
- // If there are state modifiers, check if this component supports them
326
- if (stateModifiers.length > 0) {
360
+ // Handle combination of modifiers
361
+ const hasPlatformModifiers = platformModifiers.length > 0;
362
+ const hasStateModifiers = stateModifiers.length > 0;
363
+ const hasBaseClasses = baseClasses.length > 0;
364
+
365
+ // If we have both state and platform modifiers, or platform modifiers with complex state,
366
+ // we need to combine them in an array expression wrapped in an arrow function
367
+ if (hasStateModifiers && hasPlatformModifiers) {
368
+ // Get the JSX opening element for component support checking
369
+ const jsxOpeningElement = path.parent;
370
+ const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
371
+
372
+ if (componentSupport) {
373
+ // Build style array: [baseStyle, Platform.select(...), stateConditionals]
374
+ const styleArrayElements: BabelTypes.Expression[] = [];
375
+
376
+ // Add base classes
377
+ if (hasBaseClasses) {
378
+ const baseClassName = baseClasses.join(" ");
379
+ const baseStyleObject = parseClassName(baseClassName, state.customColors);
380
+ const baseStyleKey = generateStyleKey(baseClassName);
381
+ state.styleRegistry.set(baseStyleKey, baseStyleObject);
382
+ styleArrayElements.push(
383
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
384
+ );
385
+ }
386
+
387
+ // Add platform modifiers as Platform.select()
388
+ const platformSelectExpression = processPlatformModifiers(
389
+ platformModifiers,
390
+ state,
391
+ parseClassName,
392
+ generateStyleKey,
393
+ t,
394
+ );
395
+ styleArrayElements.push(platformSelectExpression);
396
+
397
+ // Add state modifiers as conditionals
398
+ // Group by modifier type
399
+ const modifiersByType = new Map<StateModifierType, ParsedModifier[]>();
400
+ for (const mod of stateModifiers) {
401
+ const modType = mod.modifier as StateModifierType;
402
+ if (!modifiersByType.has(modType)) {
403
+ modifiersByType.set(modType, []);
404
+ }
405
+ modifiersByType.get(modType)?.push(mod);
406
+ }
407
+
408
+ // Build conditionals for each state modifier type
409
+ for (const [modifierType, modifiers] of modifiersByType) {
410
+ if (!componentSupport.supportedModifiers.includes(modifierType)) {
411
+ continue; // Skip unsupported modifiers
412
+ }
413
+
414
+ const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
415
+ const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
416
+ const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
417
+ state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
418
+
419
+ const stateProperty = getStatePropertyForModifier(modifierType);
420
+ const conditionalExpression = t.logicalExpression(
421
+ "&&",
422
+ t.identifier(stateProperty),
423
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey)),
424
+ );
425
+
426
+ styleArrayElements.push(conditionalExpression);
427
+ }
428
+
429
+ // Wrap in arrow function for state support
430
+ const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier))).filter((mod) =>
431
+ componentSupport.supportedModifiers.includes(mod),
432
+ );
433
+ const styleArrayExpression = t.arrayExpression(styleArrayElements);
434
+ const styleFunctionExpression = createStyleFunction(styleArrayExpression, usedModifiers, t);
435
+
436
+ const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
437
+ if (styleAttribute) {
438
+ mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
439
+ } else {
440
+ replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
441
+ }
442
+ return;
443
+ } else {
444
+ // Component doesn't support state modifiers, but we can still use platform modifiers
445
+ // Fall through to platform-only handling
446
+ }
447
+ }
448
+
449
+ // Handle platform-only modifiers (no state modifiers)
450
+ if (hasPlatformModifiers && !hasStateModifiers) {
451
+ // Build style array/expression: [baseStyle, Platform.select(...)]
452
+ const styleExpressions: BabelTypes.Expression[] = [];
453
+
454
+ // Add base classes
455
+ if (hasBaseClasses) {
456
+ const baseClassName = baseClasses.join(" ");
457
+ const baseStyleObject = parseClassName(baseClassName, state.customColors);
458
+ const baseStyleKey = generateStyleKey(baseClassName);
459
+ state.styleRegistry.set(baseStyleKey, baseStyleObject);
460
+ styleExpressions.push(
461
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
462
+ );
463
+ }
464
+
465
+ // Add platform modifiers as Platform.select()
466
+ const platformSelectExpression = processPlatformModifiers(
467
+ platformModifiers,
468
+ state,
469
+ parseClassName,
470
+ generateStyleKey,
471
+ t,
472
+ );
473
+ styleExpressions.push(platformSelectExpression);
474
+
475
+ // Generate style attribute
476
+ const styleExpression =
477
+ styleExpressions.length === 1 ? styleExpressions[0] : t.arrayExpression(styleExpressions);
478
+
479
+ const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
480
+ if (styleAttribute) {
481
+ // Merge with existing style attribute
482
+ const existingStyle = styleAttribute.value;
483
+ if (
484
+ t.isJSXExpressionContainer(existingStyle) &&
485
+ !t.isJSXEmptyExpression(existingStyle.expression)
486
+ ) {
487
+ const existing = existingStyle.expression;
488
+ // Merge as array: [ourStyles, existingStyles]
489
+ const mergedArray = t.isArrayExpression(existing)
490
+ ? t.arrayExpression([styleExpression, ...existing.elements])
491
+ : t.arrayExpression([styleExpression, existing]);
492
+ styleAttribute.value = t.jsxExpressionContainer(mergedArray);
493
+ } else {
494
+ styleAttribute.value = t.jsxExpressionContainer(styleExpression);
495
+ }
496
+ path.remove();
497
+ } else {
498
+ // Replace className with style prop containing our expression
499
+ path.node.name = t.jsxIdentifier(targetStyleProp);
500
+ path.node.value = t.jsxExpressionContainer(styleExpression);
501
+ }
502
+ return;
503
+ }
504
+
505
+ // If there are state modifiers (and no platform modifiers), check if this component supports them
506
+ if (hasStateModifiers) {
327
507
  // Get the JSX opening element (the direct parent of the attribute)
328
508
  const jsxOpeningElement = path.parent;
329
509
  const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
@@ -0,0 +1,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
- const importDeclaration = t.importDeclaration(
14
- [t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet"))],
15
- t.stringLiteral("react-native"),
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
- // Add import at the top of the file
19
- path.unshiftContainer("body", importDeclaration);
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 returning empty object
55
- // (this effectively unsets the aspectRatio property)
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
  }
@@ -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]).`,
@@ -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.`,
@@ -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 { hasModifier, parseModifier, splitModifierClasses } from "./modifiers";
86
- export type { ModifierType, ParsedModifier } from "./modifiers";
86
+ export {
87
+ hasModifier,
88
+ isPlatformModifier,
89
+ isStateModifier,
90
+ parseModifier,
91
+ splitModifierClasses,
92
+ } from "./modifiers";
93
+ export type { ModifierType, ParsedModifier, PlatformModifierType, StateModifierType } from "./modifiers";
@@ -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.`,
@@ -1,8 +1,12 @@
1
1
  /**
2
- * Modifier parsing utilities for state-based class names (active:, hover:, focus:, placeholder:)
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 ModifierType = "active" | "hover" | "focus" | "disabled" | "placeholder";
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 SUPPORTED_MODIFIERS: readonly ModifierType[] = [
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. ` +