@mgcrea/react-native-tailwind 0.11.0 → 0.12.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.
@@ -89,6 +89,42 @@ export type PluginOptions = {
89
89
  darkSuffix?: string;
90
90
  lightSuffix?: string;
91
91
  };
92
+
93
+ /**
94
+ * Configuration for color scheme hook import (dark:/light: modifiers)
95
+ *
96
+ * Allows using custom color scheme hooks from theme providers instead of
97
+ * React Native's built-in useColorScheme.
98
+ *
99
+ * @example
100
+ * // Use custom hook from theme provider
101
+ * {
102
+ * importFrom: '@/hooks/useColorScheme',
103
+ * importName: 'useColorScheme'
104
+ * }
105
+ *
106
+ * @example
107
+ * // Use React Navigation theme
108
+ * {
109
+ * importFrom: '@react-navigation/native',
110
+ * importName: 'useTheme' // You'd wrap this to return ColorSchemeName
111
+ * }
112
+ *
113
+ * @default { importFrom: 'react-native', importName: 'useColorScheme' }
114
+ */
115
+ colorScheme?: {
116
+ /**
117
+ * Module to import the color scheme hook from
118
+ * @default 'react-native'
119
+ */
120
+ importFrom?: string;
121
+
122
+ /**
123
+ * Name of the hook to import
124
+ * @default 'useColorScheme'
125
+ */
126
+ importName?: string;
127
+ };
92
128
  };
93
129
 
94
130
  type PluginState = PluginPass & {
@@ -100,6 +136,9 @@ type PluginState = PluginPass & {
100
136
  hasColorSchemeImport: boolean;
101
137
  needsColorSchemeImport: boolean;
102
138
  colorSchemeVariableName: string;
139
+ colorSchemeImportSource: string; // Where to import the hook from (e.g., 'react-native')
140
+ colorSchemeHookName: string; // Name of the hook to import (e.g., 'useColorScheme')
141
+ colorSchemeLocalIdentifier?: string; // Local identifier if hook is already imported with an alias
103
142
  customTheme: CustomTheme;
104
143
  schemeModifierConfig: SchemeModifierConfig;
105
144
  supportedAttributes: Set<string>;
@@ -210,6 +249,10 @@ export default function reactNativeTailwindBabelPlugin(
210
249
  lightSuffix: options?.schemeModifier?.lightSuffix ?? "-light",
211
250
  };
212
251
 
252
+ // Color scheme hook configuration from plugin options
253
+ const colorSchemeImportSource = options?.colorScheme?.importFrom ?? "react-native";
254
+ const colorSchemeHookName = options?.colorScheme?.importName ?? "useColorScheme";
255
+
213
256
  return {
214
257
  name: "react-native-tailwind",
215
258
 
@@ -225,12 +268,18 @@ export default function reactNativeTailwindBabelPlugin(
225
268
  state.hasColorSchemeImport = false;
226
269
  state.needsColorSchemeImport = false;
227
270
  state.colorSchemeVariableName = "_twColorScheme";
271
+ state.colorSchemeImportSource = colorSchemeImportSource;
272
+ state.colorSchemeHookName = colorSchemeHookName;
228
273
  state.supportedAttributes = exactMatches;
229
274
  state.attributePatterns = patterns;
230
275
  state.stylesIdentifier = stylesIdentifier;
231
276
  state.twImportNames = new Set();
232
277
  state.hasTwImport = false;
233
278
  state.functionComponentsNeedingColorScheme = new Set();
279
+ state.hasColorSchemeImport = false;
280
+ state.colorSchemeLocalIdentifier = undefined;
281
+ state.needsPlatformImport = false;
282
+ state.hasPlatformImport = false;
234
283
 
235
284
  // Load custom theme from tailwind.config.*
236
285
  state.customTheme = extractCustomTheme(state.file.opts.filename ?? "");
@@ -260,15 +309,21 @@ export default function reactNativeTailwindBabelPlugin(
260
309
  addPlatformImport(path, t);
261
310
  }
262
311
 
263
- // Add useColorScheme import if color scheme modifiers were used and not already present
312
+ // Add color scheme hook import if color scheme modifiers were used and not already present
264
313
  if (state.needsColorSchemeImport && !state.hasColorSchemeImport) {
265
- addColorSchemeImport(path, t);
314
+ addColorSchemeImport(path, state.colorSchemeImportSource, state.colorSchemeHookName, t);
266
315
  }
267
316
 
268
- // Inject useColorScheme hook in function components that need it
317
+ // Inject color scheme hook in function components that need it
269
318
  if (state.needsColorSchemeImport) {
270
319
  for (const functionPath of state.functionComponentsNeedingColorScheme) {
271
- injectColorSchemeHook(functionPath, state.colorSchemeVariableName, t);
320
+ injectColorSchemeHook(
321
+ functionPath,
322
+ state.colorSchemeVariableName,
323
+ state.colorSchemeHookName,
324
+ state.colorSchemeLocalIdentifier,
325
+ t,
326
+ );
272
327
  }
273
328
  }
274
329
 
@@ -300,13 +355,6 @@ export default function reactNativeTailwindBabelPlugin(
300
355
  return false;
301
356
  });
302
357
 
303
- const hasUseColorScheme = specifiers.some((spec) => {
304
- if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
305
- return spec.imported.name === "useColorScheme";
306
- }
307
- return false;
308
- });
309
-
310
358
  // Only track if imports exist - don't mutate yet
311
359
  // Actual import injection happens in Program.exit only if needed
312
360
  if (hasStyleSheet) {
@@ -317,14 +365,29 @@ export default function reactNativeTailwindBabelPlugin(
317
365
  state.hasPlatformImport = true;
318
366
  }
319
367
 
320
- if (hasUseColorScheme) {
321
- state.hasColorSchemeImport = true;
322
- }
323
-
324
368
  // Store reference to the react-native import for later modification if needed
325
369
  state.reactNativeImportPath = path;
326
370
  }
327
371
 
372
+ // Track color scheme hook import from the configured source
373
+ // (default: react-native, but can be custom like @/hooks/useColorScheme)
374
+ // Only track value imports (not type-only imports which get erased)
375
+ if (node.source.value === state.colorSchemeImportSource && node.importKind !== "type") {
376
+ const specifiers = node.specifiers;
377
+
378
+ for (const spec of specifiers) {
379
+ if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
380
+ if (spec.imported.name === state.colorSchemeHookName) {
381
+ state.hasColorSchemeImport = true;
382
+ // Track the local identifier (handles aliased imports)
383
+ // e.g., import { useTheme as navTheme } → local name is 'navTheme'
384
+ state.colorSchemeLocalIdentifier = spec.local.name;
385
+ break;
386
+ }
387
+ }
388
+ }
389
+ }
390
+
328
391
  // Track tw/twStyle imports from main package (for compile-time transformation)
329
392
  if (node.source.value === "@mgcrea/react-native-tailwind") {
330
393
  const specifiers = node.specifiers;
@@ -386,7 +449,16 @@ export default function reactNativeTailwindBabelPlugin(
386
449
  state.hasClassNames = true;
387
450
 
388
451
  // Process the className with modifiers
389
- processTwCall(className, path, state, parseClassName, generateStyleKey, splitModifierClasses, t);
452
+ processTwCall(
453
+ className,
454
+ path,
455
+ state,
456
+ parseClassName,
457
+ generateStyleKey,
458
+ splitModifierClasses,
459
+ findComponentScope,
460
+ t,
461
+ );
390
462
  },
391
463
 
392
464
  // Handle twStyle('...') call expressions
@@ -436,7 +508,16 @@ export default function reactNativeTailwindBabelPlugin(
436
508
  state.hasClassNames = true;
437
509
 
438
510
  // Process the className with modifiers
439
- processTwCall(className, path, state, parseClassName, generateStyleKey, splitModifierClasses, t);
511
+ processTwCall(
512
+ className,
513
+ path,
514
+ state,
515
+ parseClassName,
516
+ generateStyleKey,
517
+ splitModifierClasses,
518
+ findComponentScope,
519
+ t,
520
+ );
440
521
  },
441
522
 
442
523
  JSXAttribute(path, state) {
@@ -459,20 +540,22 @@ export default function reactNativeTailwindBabelPlugin(
459
540
  // Determine target style prop based on attribute name
460
541
  const targetStyleProp = getTargetStyleProp(attributeName);
461
542
 
462
- // Handle static string literals
463
- if (t.isStringLiteral(value)) {
464
- const className = value.value.trim();
543
+ /**
544
+ * Process static className string (handles both direct StringLiteral and StringLiteral in JSXExpressionContainer)
545
+ */
546
+ const processStaticClassName = (className: string): boolean => {
547
+ const trimmedClassName = className.trim();
465
548
 
466
549
  // Skip empty classNames
467
- if (!className) {
550
+ if (!trimmedClassName) {
468
551
  path.remove();
469
- return;
552
+ return true;
470
553
  }
471
554
 
472
555
  state.hasClassNames = true;
473
556
 
474
557
  // Check if className contains modifiers (active:, hover:, focus:, placeholder:, ios:, android:, web:, dark:, light:, scheme:)
475
- const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(className);
558
+ const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(trimmedClassName);
476
559
 
477
560
  // Expand scheme: modifiers into dark: and light: modifiers
478
561
  const modifierClasses: ParsedModifier[] = [];
@@ -639,7 +722,7 @@ export default function reactNativeTailwindBabelPlugin(
639
722
  } else {
640
723
  replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
641
724
  }
642
- return;
725
+ return true;
643
726
  } else {
644
727
  // Component doesn't support state modifiers, but we can still use platform modifiers
645
728
  // Fall through to platform-only handling
@@ -713,7 +796,7 @@ export default function reactNativeTailwindBabelPlugin(
713
796
  path.node.name = t.jsxIdentifier(targetStyleProp);
714
797
  path.node.value = t.jsxExpressionContainer(styleExpression);
715
798
  }
716
- return;
799
+ return true;
717
800
  }
718
801
 
719
802
  // If there are state modifiers (and no platform modifiers), check if this component supports them
@@ -771,12 +854,12 @@ export default function reactNativeTailwindBabelPlugin(
771
854
  } else {
772
855
  replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
773
856
  }
774
- return;
857
+ return true;
775
858
  }
776
859
  } else {
777
860
  // All modifiers are supported - process normally
778
861
  const styleExpression = processStaticClassNameWithModifiers(
779
- className,
862
+ trimmedClassName,
780
863
  state,
781
864
  parseClassName,
782
865
  generateStyleKey,
@@ -793,7 +876,7 @@ export default function reactNativeTailwindBabelPlugin(
793
876
  } else {
794
877
  replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
795
878
  }
796
- return;
879
+ return true;
797
880
  }
798
881
  } else {
799
882
  // Component doesn't support any modifiers
@@ -813,7 +896,7 @@ export default function reactNativeTailwindBabelPlugin(
813
896
  if (!classNameForStyle) {
814
897
  // No base classes, only had placeholder modifiers - just remove className
815
898
  path.remove();
816
- return;
899
+ return true;
817
900
  }
818
901
 
819
902
  const styleObject = parseClassName(classNameForStyle, state.customTheme);
@@ -830,7 +913,14 @@ export default function reactNativeTailwindBabelPlugin(
830
913
  // Replace className with style prop
831
914
  replaceWithStyleAttribute(path, styleKey, targetStyleProp, state.stylesIdentifier, t);
832
915
  }
833
- return;
916
+ return true;
917
+ };
918
+
919
+ // Handle static string literals
920
+ if (t.isStringLiteral(value)) {
921
+ if (processStaticClassName(value.value)) {
922
+ return;
923
+ }
834
924
  }
835
925
 
836
926
  // Handle dynamic expressions (JSXExpressionContainer)
@@ -842,6 +932,13 @@ export default function reactNativeTailwindBabelPlugin(
842
932
  return;
843
933
  }
844
934
 
935
+ // Fast path: Support string literals wrapped in JSXExpressionContainer: className={"flex-row"}
936
+ if (t.isStringLiteral(expression)) {
937
+ if (processStaticClassName(expression.value)) {
938
+ return;
939
+ }
940
+ }
941
+
845
942
  try {
846
943
  // Find component scope for color scheme modifiers
847
944
  const componentScope = findComponentScope(path, t);
@@ -15,16 +15,18 @@ export declare function addPlatformImport(path: NodePath<BabelTypes.Program>, t:
15
15
  /**
16
16
  * Add useColorScheme import to the file or merge with existing react-native import
17
17
  */
18
- export declare function addColorSchemeImport(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void;
18
+ export declare function addColorSchemeImport(path: NodePath<BabelTypes.Program>, importSource: string, hookName: string, t: typeof BabelTypes): void;
19
19
  /**
20
- * Inject useColorScheme hook call at the top of a function component
20
+ * Inject color scheme hook call at the top of a function component
21
21
  *
22
22
  * @param functionPath - Path to the function component
23
23
  * @param colorSchemeVariableName - Name for the color scheme variable
24
+ * @param hookName - Name of the hook to call (e.g., 'useColorScheme')
25
+ * @param localIdentifier - Local identifier if hook is already imported with an alias
24
26
  * @param t - Babel types
25
27
  * @returns true if hook was injected, false if already exists
26
28
  */
27
- export declare function injectColorSchemeHook(functionPath: NodePath<BabelTypes.Function>, colorSchemeVariableName: string, t: typeof BabelTypes): boolean;
29
+ export declare function injectColorSchemeHook(functionPath: NodePath<BabelTypes.Function>, colorSchemeVariableName: string, hookName: string, localIdentifier: string | undefined, t: typeof BabelTypes): boolean;
28
30
  /**
29
31
  * Inject StyleSheet.create with all collected styles at the top of the file
30
32
  * This ensures the styles object is defined before any code that references it
@@ -67,54 +67,64 @@ export function addPlatformImport(path: NodePath<BabelTypes.Program>, t: typeof
67
67
  /**
68
68
  * Add useColorScheme import to the file or merge with existing react-native import
69
69
  */
70
- export function addColorSchemeImport(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void {
71
- // Check if there's already a react-native import
70
+ export function addColorSchemeImport(
71
+ path: NodePath<BabelTypes.Program>,
72
+ importSource: string,
73
+ hookName: string,
74
+ t: typeof BabelTypes,
75
+ ): void {
76
+ // Check if there's already an import from the specified source
72
77
  const body = path.node.body;
73
- let reactNativeImport: BabelTypes.ImportDeclaration | null = null;
78
+ let existingValueImport: BabelTypes.ImportDeclaration | null = null;
74
79
 
75
80
  for (const statement of body) {
76
- if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
77
- reactNativeImport = statement;
78
- break;
81
+ if (t.isImportDeclaration(statement) && statement.source.value === importSource) {
82
+ // Only consider value imports (not type-only imports which get erased)
83
+ if (statement.importKind !== "type") {
84
+ existingValueImport = statement;
85
+ break; // Found a value import, we can stop
86
+ }
79
87
  }
80
88
  }
81
89
 
82
- if (reactNativeImport) {
83
- // Check if useColorScheme is already imported
84
- const hasUseColorScheme = reactNativeImport.specifiers.some(
90
+ // If we found a value import (not type-only), merge with it
91
+ if (existingValueImport) {
92
+ // Check if the hook is already imported
93
+ const hasHook = existingValueImport.specifiers.some(
85
94
  (spec) =>
86
- t.isImportSpecifier(spec) &&
87
- spec.imported.type === "Identifier" &&
88
- spec.imported.name === "useColorScheme",
95
+ t.isImportSpecifier(spec) && spec.imported.type === "Identifier" && spec.imported.name === hookName,
89
96
  );
90
97
 
91
- if (!hasUseColorScheme) {
92
- // Add useColorScheme to existing react-native import
93
- reactNativeImport.specifiers.push(
94
- t.importSpecifier(t.identifier("useColorScheme"), t.identifier("useColorScheme")),
95
- );
98
+ if (!hasHook) {
99
+ // Add hook to existing value import
100
+ existingValueImport.specifiers.push(t.importSpecifier(t.identifier(hookName), t.identifier(hookName)));
96
101
  }
97
102
  } else {
98
- // Create new react-native import with useColorScheme
103
+ // No value import exists - create a new one
104
+ // (Don't merge with type-only imports as they get erased by Babel/TypeScript)
99
105
  const importDeclaration = t.importDeclaration(
100
- [t.importSpecifier(t.identifier("useColorScheme"), t.identifier("useColorScheme"))],
101
- t.stringLiteral("react-native"),
106
+ [t.importSpecifier(t.identifier(hookName), t.identifier(hookName))],
107
+ t.stringLiteral(importSource),
102
108
  );
103
109
  path.unshiftContainer("body", importDeclaration);
104
110
  }
105
111
  }
106
112
 
107
113
  /**
108
- * Inject useColorScheme hook call at the top of a function component
114
+ * Inject color scheme hook call at the top of a function component
109
115
  *
110
116
  * @param functionPath - Path to the function component
111
117
  * @param colorSchemeVariableName - Name for the color scheme variable
118
+ * @param hookName - Name of the hook to call (e.g., 'useColorScheme')
119
+ * @param localIdentifier - Local identifier if hook is already imported with an alias
112
120
  * @param t - Babel types
113
121
  * @returns true if hook was injected, false if already exists
114
122
  */
115
123
  export function injectColorSchemeHook(
116
124
  functionPath: NodePath<BabelTypes.Function>,
117
125
  colorSchemeVariableName: string,
126
+ hookName: string,
127
+ localIdentifier: string | undefined,
118
128
  t: typeof BabelTypes,
119
129
  ): boolean {
120
130
  let body = functionPath.node.body;
@@ -151,11 +161,16 @@ export function injectColorSchemeHook(
151
161
  return false; // Already injected
152
162
  }
153
163
 
154
- // Create: const _twColorScheme = useColorScheme();
164
+ // Use the local identifier if hook was already imported with an alias,
165
+ // otherwise use the configured hook name
166
+ // e.g., import { useTheme as navTheme } → call navTheme()
167
+ const identifierToCall = localIdentifier ?? hookName;
168
+
169
+ // Create: const _twColorScheme = useColorScheme(); (or aliased name if already imported)
155
170
  const hookCall = t.variableDeclaration("const", [
156
171
  t.variableDeclarator(
157
172
  t.identifier(colorSchemeVariableName),
158
- t.callExpression(t.identifier("useColorScheme"), []),
173
+ t.callExpression(t.identifier(identifierToCall), []),
159
174
  ),
160
175
  ]);
161
176
 
@@ -14,15 +14,22 @@ export interface TwProcessingState {
14
14
  customTheme: CustomTheme;
15
15
  schemeModifierConfig: SchemeModifierConfig;
16
16
  stylesIdentifier: string;
17
+ needsColorSchemeImport: boolean;
18
+ colorSchemeVariableName: string;
19
+ functionComponentsNeedingColorScheme: Set<NodePath<BabelTypes.Function>>;
20
+ colorSchemeLocalIdentifier?: string;
21
+ needsPlatformImport: boolean;
17
22
  }
18
23
  /**
19
24
  * Process tw`...` or twStyle('...') call and replace with TwStyle object
20
25
  * Generates: { style: styles._base, activeStyle: styles._active, ... }
26
+ * When color-scheme modifiers are present, generates: { style: [base, _twColorScheme === 'dark' && dark, ...] }
27
+ * When platform modifiers are present, generates: { style: [base, Platform.select({ ios: ..., android: ... })] }
21
28
  */
22
29
  export declare function processTwCall(className: string, path: NodePath, state: TwProcessingState, parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject, generateStyleKey: (className: string) => string, splitModifierClasses: (className: string) => {
23
30
  baseClasses: string[];
24
31
  modifierClasses: ParsedModifier[];
25
- }, t: typeof BabelTypes): void;
32
+ }, findComponentScope: (path: NodePath, t: typeof BabelTypes) => NodePath<BabelTypes.Function> | null, t: typeof BabelTypes): void;
26
33
  /**
27
34
  * Remove tw/twStyle imports from @mgcrea/react-native-tailwind
28
35
  * This is called after all tw calls have been transformed