@mgcrea/react-native-tailwind 0.10.0 → 0.11.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 (45) hide show
  1. package/README.md +159 -13
  2. package/dist/babel/config-loader.d.ts +12 -3
  3. package/dist/babel/config-loader.test.ts +14 -12
  4. package/dist/babel/config-loader.ts +41 -9
  5. package/dist/babel/index.cjs +91 -54
  6. package/dist/babel/plugin.d.ts +39 -1
  7. package/dist/babel/plugin.test.ts +275 -1
  8. package/dist/babel/plugin.ts +84 -25
  9. package/dist/babel/utils/colorSchemeModifierProcessing.d.ts +3 -3
  10. package/dist/babel/utils/colorSchemeModifierProcessing.ts +4 -4
  11. package/dist/babel/utils/dynamicProcessing.d.ts +5 -5
  12. package/dist/babel/utils/dynamicProcessing.ts +11 -11
  13. package/dist/babel/utils/modifierProcessing.d.ts +3 -3
  14. package/dist/babel/utils/modifierProcessing.ts +5 -5
  15. package/dist/babel/utils/platformModifierProcessing.d.ts +3 -3
  16. package/dist/babel/utils/platformModifierProcessing.ts +4 -4
  17. package/dist/babel/utils/styleInjection.d.ts +5 -3
  18. package/dist/babel/utils/styleInjection.ts +38 -23
  19. package/dist/babel/utils/twProcessing.d.ts +3 -3
  20. package/dist/babel/utils/twProcessing.ts +6 -6
  21. package/dist/parser/index.d.ts +11 -4
  22. package/dist/parser/index.js +1 -1
  23. package/dist/parser/typography.d.ts +3 -1
  24. package/dist/parser/typography.js +1 -1
  25. package/dist/runtime.cjs +1 -1
  26. package/dist/runtime.cjs.map +3 -3
  27. package/dist/runtime.d.ts +8 -1
  28. package/dist/runtime.js +1 -1
  29. package/dist/runtime.js.map +3 -3
  30. package/dist/runtime.test.js +1 -1
  31. package/package.json +1 -1
  32. package/src/babel/config-loader.test.ts +14 -12
  33. package/src/babel/config-loader.ts +41 -9
  34. package/src/babel/plugin.test.ts +275 -1
  35. package/src/babel/plugin.ts +84 -25
  36. package/src/babel/utils/colorSchemeModifierProcessing.ts +4 -4
  37. package/src/babel/utils/dynamicProcessing.ts +11 -11
  38. package/src/babel/utils/modifierProcessing.ts +5 -5
  39. package/src/babel/utils/platformModifierProcessing.ts +4 -4
  40. package/src/babel/utils/styleInjection.ts +38 -23
  41. package/src/babel/utils/twProcessing.ts +6 -6
  42. package/src/parser/index.ts +16 -8
  43. package/src/parser/typography.ts +14 -2
  44. package/src/runtime.test.ts +7 -7
  45. package/src/runtime.ts +37 -14
@@ -18,7 +18,8 @@ import {
18
18
  } from "../parser/index.js";
19
19
  import type { StyleObject } from "../types/core.js";
20
20
  import { generateStyleKey } from "../utils/styleKey.js";
21
- import { extractCustomColors } from "./config-loader.js";
21
+ import type { CustomTheme } from "./config-loader.js";
22
+ import { extractCustomTheme } from "./config-loader.js";
22
23
 
23
24
  // Import utility functions
24
25
  import type { SchemeModifierConfig } from "../types/config.js";
@@ -88,6 +89,42 @@ export type PluginOptions = {
88
89
  darkSuffix?: string;
89
90
  lightSuffix?: string;
90
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
+ };
91
128
  };
92
129
 
93
130
  type PluginState = PluginPass & {
@@ -99,7 +136,10 @@ type PluginState = PluginPass & {
99
136
  hasColorSchemeImport: boolean;
100
137
  needsColorSchemeImport: boolean;
101
138
  colorSchemeVariableName: string;
102
- customColors: Record<string, 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
142
+ customTheme: CustomTheme;
103
143
  schemeModifierConfig: SchemeModifierConfig;
104
144
  supportedAttributes: Set<string>;
105
145
  attributePatterns: RegExp[];
@@ -209,6 +249,10 @@ export default function reactNativeTailwindBabelPlugin(
209
249
  lightSuffix: options?.schemeModifier?.lightSuffix ?? "-light",
210
250
  };
211
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
+
212
256
  return {
213
257
  name: "react-native-tailwind",
214
258
 
@@ -224,6 +268,8 @@ export default function reactNativeTailwindBabelPlugin(
224
268
  state.hasColorSchemeImport = false;
225
269
  state.needsColorSchemeImport = false;
226
270
  state.colorSchemeVariableName = "_twColorScheme";
271
+ state.colorSchemeImportSource = colorSchemeImportSource;
272
+ state.colorSchemeHookName = colorSchemeHookName;
227
273
  state.supportedAttributes = exactMatches;
228
274
  state.attributePatterns = patterns;
229
275
  state.stylesIdentifier = stylesIdentifier;
@@ -231,8 +277,8 @@ export default function reactNativeTailwindBabelPlugin(
231
277
  state.hasTwImport = false;
232
278
  state.functionComponentsNeedingColorScheme = new Set();
233
279
 
234
- // Load custom colors from tailwind.config.*
235
- state.customColors = extractCustomColors(state.file.opts.filename ?? "");
280
+ // Load custom theme from tailwind.config.*
281
+ state.customTheme = extractCustomTheme(state.file.opts.filename ?? "");
236
282
 
237
283
  // Use scheme modifier config from plugin options
238
284
  state.schemeModifierConfig = schemeModifierConfig;
@@ -259,15 +305,21 @@ export default function reactNativeTailwindBabelPlugin(
259
305
  addPlatformImport(path, t);
260
306
  }
261
307
 
262
- // Add useColorScheme import if color scheme modifiers were used and not already present
308
+ // Add color scheme hook import if color scheme modifiers were used and not already present
263
309
  if (state.needsColorSchemeImport && !state.hasColorSchemeImport) {
264
- addColorSchemeImport(path, t);
310
+ addColorSchemeImport(path, state.colorSchemeImportSource, state.colorSchemeHookName, t);
265
311
  }
266
312
 
267
- // Inject useColorScheme hook in function components that need it
313
+ // Inject color scheme hook in function components that need it
268
314
  if (state.needsColorSchemeImport) {
269
315
  for (const functionPath of state.functionComponentsNeedingColorScheme) {
270
- injectColorSchemeHook(functionPath, state.colorSchemeVariableName, t);
316
+ injectColorSchemeHook(
317
+ functionPath,
318
+ state.colorSchemeVariableName,
319
+ state.colorSchemeHookName,
320
+ state.colorSchemeLocalIdentifier,
321
+ t,
322
+ );
271
323
  }
272
324
  }
273
325
 
@@ -299,13 +351,6 @@ export default function reactNativeTailwindBabelPlugin(
299
351
  return false;
300
352
  });
301
353
 
302
- const hasUseColorScheme = specifiers.some((spec) => {
303
- if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
304
- return spec.imported.name === "useColorScheme";
305
- }
306
- return false;
307
- });
308
-
309
354
  // Only track if imports exist - don't mutate yet
310
355
  // Actual import injection happens in Program.exit only if needed
311
356
  if (hasStyleSheet) {
@@ -316,14 +361,28 @@ export default function reactNativeTailwindBabelPlugin(
316
361
  state.hasPlatformImport = true;
317
362
  }
318
363
 
319
- if (hasUseColorScheme) {
320
- state.hasColorSchemeImport = true;
321
- }
322
-
323
364
  // Store reference to the react-native import for later modification if needed
324
365
  state.reactNativeImportPath = path;
325
366
  }
326
367
 
368
+ // Track color scheme hook import from the configured source
369
+ // (default: react-native, but can be custom like @/hooks/useColorScheme)
370
+ if (node.source.value === state.colorSchemeImportSource) {
371
+ const specifiers = node.specifiers;
372
+
373
+ for (const spec of specifiers) {
374
+ if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
375
+ if (spec.imported.name === state.colorSchemeHookName) {
376
+ state.hasColorSchemeImport = true;
377
+ // Track the local identifier (handles aliased imports)
378
+ // e.g., import { useTheme as navTheme } → local name is 'navTheme'
379
+ state.colorSchemeLocalIdentifier = spec.local.name;
380
+ break;
381
+ }
382
+ }
383
+ }
384
+ }
385
+
327
386
  // Track tw/twStyle imports from main package (for compile-time transformation)
328
387
  if (node.source.value === "@mgcrea/react-native-tailwind") {
329
388
  const specifiers = node.specifiers;
@@ -480,7 +539,7 @@ export default function reactNativeTailwindBabelPlugin(
480
539
  // Expand scheme: into dark: and light:
481
540
  const expanded = expandSchemeModifier(
482
541
  modifier,
483
- state.customColors,
542
+ state.customTheme.colors ?? {},
484
543
  state.schemeModifierConfig.darkSuffix,
485
544
  state.schemeModifierConfig.lightSuffix,
486
545
  );
@@ -507,7 +566,7 @@ export default function reactNativeTailwindBabelPlugin(
507
566
 
508
567
  if (componentSupport?.supportedModifiers.includes("placeholder")) {
509
568
  const placeholderClasses = placeholderModifiers.map((m) => m.baseClass).join(" ");
510
- const placeholderColor = parsePlaceholderClasses(placeholderClasses, state.customColors);
569
+ const placeholderColor = parsePlaceholderClasses(placeholderClasses, state.customTheme.colors);
511
570
 
512
571
  if (placeholderColor) {
513
572
  // Add or merge placeholderTextColor prop
@@ -561,7 +620,7 @@ export default function reactNativeTailwindBabelPlugin(
561
620
  // Add base classes
562
621
  if (hasBaseClasses) {
563
622
  const baseClassName = baseClasses.join(" ");
564
- const baseStyleObject = parseClassName(baseClassName, state.customColors);
623
+ const baseStyleObject = parseClassName(baseClassName, state.customTheme);
565
624
  const baseStyleKey = generateStyleKey(baseClassName);
566
625
  state.styleRegistry.set(baseStyleKey, baseStyleObject);
567
626
  styleArrayElements.push(
@@ -611,7 +670,7 @@ export default function reactNativeTailwindBabelPlugin(
611
670
  }
612
671
 
613
672
  const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
614
- const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
673
+ const modifierStyleObject = parseClassName(modifierClassNames, state.customTheme);
615
674
  const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
616
675
  state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
617
676
 
@@ -653,7 +712,7 @@ export default function reactNativeTailwindBabelPlugin(
653
712
  // Add base classes
654
713
  if (hasBaseClasses) {
655
714
  const baseClassName = baseClasses.join(" ");
656
- const baseStyleObject = parseClassName(baseClassName, state.customColors);
715
+ const baseStyleObject = parseClassName(baseClassName, state.customTheme);
657
716
  const baseStyleKey = generateStyleKey(baseClassName);
658
717
  state.styleRegistry.set(baseStyleKey, baseStyleObject);
659
718
  styleExpressions.push(
@@ -815,7 +874,7 @@ export default function reactNativeTailwindBabelPlugin(
815
874
  return;
816
875
  }
817
876
 
818
- const styleObject = parseClassName(classNameForStyle, state.customColors);
877
+ const styleObject = parseClassName(classNameForStyle, state.customTheme);
819
878
  const styleKey = generateStyleKey(classNameForStyle);
820
879
  state.styleRegistry.set(styleKey, styleObject);
821
880
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type * as BabelTypes from "@babel/types";
6
- import type { ColorSchemeModifierType, ParsedModifier } from "../../parser/index.js";
6
+ import type { ColorSchemeModifierType, CustomTheme, ParsedModifier } from "../../parser/index.js";
7
7
  import type { StyleObject } from "../../types/core.js";
8
8
 
9
9
  /**
@@ -12,7 +12,7 @@ import type { StyleObject } from "../../types/core.js";
12
12
  // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
13
13
  export interface ColorSchemeModifierProcessingState {
14
14
  styleRegistry: Map<string, StyleObject>;
15
- customColors: Record<string, string>;
15
+ customTheme: CustomTheme;
16
16
  stylesIdentifier: string;
17
17
  needsColorSchemeImport: boolean;
18
18
  colorSchemeVariableName: string;
@@ -38,7 +38,7 @@ export interface ColorSchemeModifierProcessingState {
38
38
  export function processColorSchemeModifiers(
39
39
  colorSchemeModifiers: ParsedModifier[],
40
40
  state: ColorSchemeModifierProcessingState,
41
- parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
41
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
42
42
  generateStyleKey: (className: string) => string,
43
43
  t: typeof BabelTypes,
44
44
  ): BabelTypes.Expression[] {
@@ -65,7 +65,7 @@ export function processColorSchemeModifiers(
65
65
  for (const [scheme, modifiers] of modifiersByScheme) {
66
66
  // Parse all classes for this color scheme together
67
67
  const classNames = modifiers.map((m) => m.baseClass).join(" ");
68
- const styleObject = parseClassName(classNames, state.customColors);
68
+ const styleObject = parseClassName(classNames, state.customTheme);
69
69
  const styleKey = generateStyleKey(`${scheme}_${classNames}`);
70
70
 
71
71
  // Register style in the registry
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { NodePath } from "@babel/core";
6
6
  import type * as BabelTypes from "@babel/types";
7
- import type { ParsedModifier } from "../../parser/index.js";
7
+ import type { CustomTheme, ParsedModifier } from "../../parser/index.js";
8
8
  import type { SchemeModifierConfig } from "../../types/config.js";
9
9
  import type { StyleObject } from "../../types/core.js";
10
10
 
@@ -14,7 +14,7 @@ import type { StyleObject } from "../../types/core.js";
14
14
  // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
15
15
  export interface DynamicProcessingState {
16
16
  styleRegistry: Map<string, StyleObject>;
17
- customColors: Record<string, string>;
17
+ customTheme: CustomTheme;
18
18
  schemeModifierConfig: SchemeModifierConfig;
19
19
  stylesIdentifier: string;
20
20
  needsPlatformImport: boolean;
@@ -37,7 +37,7 @@ export type SplitModifierClassesFn = (className: string) => {
37
37
  export type ProcessPlatformModifiersFn = (
38
38
  modifiers: ParsedModifier[],
39
39
  state: DynamicProcessingState,
40
- parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
40
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
41
41
  generateStyleKey: (className: string) => string,
42
42
  t: typeof BabelTypes,
43
43
  ) => BabelTypes.Expression;
@@ -48,7 +48,7 @@ export type ProcessPlatformModifiersFn = (
48
48
  export type ProcessColorSchemeModifiersFn = (
49
49
  modifiers: ParsedModifier[],
50
50
  state: DynamicProcessingState,
51
- parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
51
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
52
52
  generateStyleKey: (className: string) => string,
53
53
  t: typeof BabelTypes,
54
54
  ) => BabelTypes.Expression[];
@@ -85,7 +85,7 @@ export type DynamicExpressionResult = {
85
85
  export function processDynamicExpression(
86
86
  expression: BabelTypes.Expression,
87
87
  state: DynamicProcessingState,
88
- parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
88
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
89
89
  generateStyleKey: (className: string) => string,
90
90
  splitModifierClasses: SplitModifierClassesFn,
91
91
  processPlatformModifiers: ProcessPlatformModifiersFn,
@@ -164,7 +164,7 @@ export function processDynamicExpression(
164
164
  function processTemplateLiteral(
165
165
  node: BabelTypes.TemplateLiteral,
166
166
  state: DynamicProcessingState,
167
- parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
167
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
168
168
  generateStyleKey: (className: string) => string,
169
169
  splitModifierClasses: SplitModifierClassesFn,
170
170
  processPlatformModifiers: ProcessPlatformModifiersFn,
@@ -262,7 +262,7 @@ function processTemplateLiteral(
262
262
  function processConditionalExpression(
263
263
  node: BabelTypes.ConditionalExpression,
264
264
  state: DynamicProcessingState,
265
- parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
265
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
266
266
  generateStyleKey: (className: string) => string,
267
267
  splitModifierClasses: SplitModifierClassesFn,
268
268
  processPlatformModifiers: ProcessPlatformModifiersFn,
@@ -325,7 +325,7 @@ function processConditionalExpression(
325
325
  function processLogicalExpression(
326
326
  node: BabelTypes.LogicalExpression,
327
327
  state: DynamicProcessingState,
328
- parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
328
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
329
329
  generateStyleKey: (className: string) => string,
330
330
  splitModifierClasses: SplitModifierClassesFn,
331
331
  processPlatformModifiers: ProcessPlatformModifiersFn,
@@ -376,7 +376,7 @@ function processLogicalExpression(
376
376
  function processStringOrExpressionHelper(
377
377
  node: BabelTypes.StringLiteral | BabelTypes.Expression,
378
378
  state: DynamicProcessingState,
379
- parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
379
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
380
380
  generateStyleKey: (className: string) => string,
381
381
  splitModifierClasses: SplitModifierClassesFn,
382
382
  processPlatformModifiers: ProcessPlatformModifiersFn,
@@ -405,7 +405,7 @@ function processStringOrExpressionHelper(
405
405
  // Expand scheme: into dark: and light:
406
406
  const expanded = expandSchemeModifier(
407
407
  modifier,
408
- state.customColors,
408
+ state.customTheme.colors ?? {},
409
409
  state.schemeModifierConfig.darkSuffix ?? "-dark",
410
410
  state.schemeModifierConfig.lightSuffix ?? "-light",
411
411
  );
@@ -425,7 +425,7 @@ function processStringOrExpressionHelper(
425
425
  // Process base classes
426
426
  if (baseClasses.length > 0) {
427
427
  const baseClassName = baseClasses.join(" ");
428
- const styleObject = parseClassName(baseClassName, state.customColors);
428
+ const styleObject = parseClassName(baseClassName, state.customTheme);
429
429
  const styleKey = generateStyleKey(baseClassName);
430
430
  state.styleRegistry.set(styleKey, styleObject);
431
431
  styleElements.push(t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey)));
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type * as BabelTypes from "@babel/types";
6
- import type { ModifierType, ParsedModifier } from "../../parser/index.js";
6
+ import type { CustomTheme, ModifierType, ParsedModifier } from "../../parser/index.js";
7
7
  import type { StyleObject } from "../../types/core.js";
8
8
  import { getStatePropertyForModifier } from "./componentSupport.js";
9
9
 
@@ -13,7 +13,7 @@ import { getStatePropertyForModifier } from "./componentSupport.js";
13
13
  // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
14
14
  export interface ModifierProcessingState {
15
15
  styleRegistry: Map<string, StyleObject>;
16
- customColors: Record<string, string>;
16
+ customTheme: CustomTheme;
17
17
  stylesIdentifier: string;
18
18
  }
19
19
 
@@ -24,7 +24,7 @@ export interface ModifierProcessingState {
24
24
  export function processStaticClassNameWithModifiers(
25
25
  className: string,
26
26
  state: ModifierProcessingState,
27
- parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
27
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
28
28
  generateStyleKey: (className: string) => string,
29
29
  splitModifierClasses: (className: string) => { baseClasses: string[]; modifierClasses: ParsedModifier[] },
30
30
  t: typeof BabelTypes,
@@ -35,7 +35,7 @@ export function processStaticClassNameWithModifiers(
35
35
  let baseStyleExpression: BabelTypes.Node | null = null;
36
36
  if (baseClasses.length > 0) {
37
37
  const baseClassName = baseClasses.join(" ");
38
- const baseStyleObject = parseClassName(baseClassName, state.customColors);
38
+ const baseStyleObject = parseClassName(baseClassName, state.customTheme);
39
39
  const baseStyleKey = generateStyleKey(baseClassName);
40
40
  state.styleRegistry.set(baseStyleKey, baseStyleObject);
41
41
  baseStyleExpression = t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey));
@@ -66,7 +66,7 @@ export function processStaticClassNameWithModifiers(
66
66
  for (const [modifierType, modifiers] of modifiersByType) {
67
67
  // Parse all modifier classes together
68
68
  const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
69
- const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
69
+ const modifierStyleObject = parseClassName(modifierClassNames, state.customTheme);
70
70
  const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
71
71
  state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
72
72
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type * as BabelTypes from "@babel/types";
6
- import type { ParsedModifier, PlatformModifierType } from "../../parser/index.js";
6
+ import type { CustomTheme, ParsedModifier, PlatformModifierType } from "../../parser/index.js";
7
7
  import type { StyleObject } from "../../types/core.js";
8
8
 
9
9
  /**
@@ -12,7 +12,7 @@ import type { StyleObject } from "../../types/core.js";
12
12
  // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
13
13
  export interface PlatformModifierProcessingState {
14
14
  styleRegistry: Map<string, StyleObject>;
15
- customColors: Record<string, string>;
15
+ customTheme: CustomTheme;
16
16
  stylesIdentifier: string;
17
17
  needsPlatformImport: boolean;
18
18
  }
@@ -34,7 +34,7 @@ export interface PlatformModifierProcessingState {
34
34
  export function processPlatformModifiers(
35
35
  platformModifiers: ParsedModifier[],
36
36
  state: PlatformModifierProcessingState,
37
- parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
37
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
38
38
  generateStyleKey: (className: string) => string,
39
39
  t: typeof BabelTypes,
40
40
  ): BabelTypes.Expression {
@@ -61,7 +61,7 @@ export function processPlatformModifiers(
61
61
  for (const [platform, modifiers] of modifiersByPlatform) {
62
62
  // Parse all classes for this platform together
63
63
  const classNames = modifiers.map((m) => m.baseClass).join(" ");
64
- const styleObject = parseClassName(classNames, state.customColors);
64
+ const styleObject = parseClassName(classNames, state.customTheme);
65
65
  const styleKey = generateStyleKey(`${platform}_${classNames}`);
66
66
 
67
67
  // Register style in the registry
@@ -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
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { NodePath } from "@babel/core";
6
6
  import type * as BabelTypes from "@babel/types";
7
- import type { ModifierType, ParsedModifier } from "../../parser/index.js";
7
+ import type { CustomTheme, ModifierType, ParsedModifier } from "../../parser/index.js";
8
8
  import { expandSchemeModifier, isSchemeModifier } from "../../parser/index.js";
9
9
  import type { SchemeModifierConfig } from "../../types/config.js";
10
10
  import type { StyleObject } from "../../types/core.js";
@@ -15,7 +15,7 @@ import type { StyleObject } from "../../types/core.js";
15
15
  // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
16
16
  export interface TwProcessingState {
17
17
  styleRegistry: Map<string, StyleObject>;
18
- customColors: Record<string, string>;
18
+ customTheme: CustomTheme;
19
19
  schemeModifierConfig: SchemeModifierConfig;
20
20
  stylesIdentifier: string;
21
21
  }
@@ -28,7 +28,7 @@ export function processTwCall(
28
28
  className: string,
29
29
  path: NodePath,
30
30
  state: TwProcessingState,
31
- parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
31
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
32
32
  generateStyleKey: (className: string) => string,
33
33
  splitModifierClasses: (className: string) => { baseClasses: string[]; modifierClasses: ParsedModifier[] },
34
34
  t: typeof BabelTypes,
@@ -42,7 +42,7 @@ export function processTwCall(
42
42
  // Expand scheme: into dark: and light:
43
43
  const expanded = expandSchemeModifier(
44
44
  modifier,
45
- state.customColors,
45
+ state.customTheme.colors ?? {},
46
46
  state.schemeModifierConfig.darkSuffix ?? "-dark",
47
47
  state.schemeModifierConfig.lightSuffix ?? "-light",
48
48
  );
@@ -59,7 +59,7 @@ export function processTwCall(
59
59
  // Parse and add base styles
60
60
  if (baseClasses.length > 0) {
61
61
  const baseClassName = baseClasses.join(" ");
62
- const baseStyleObject = parseClassName(baseClassName, state.customColors);
62
+ const baseStyleObject = parseClassName(baseClassName, state.customTheme);
63
63
  const baseStyleKey = generateStyleKey(baseClassName);
64
64
  state.styleRegistry.set(baseStyleKey, baseStyleObject);
65
65
 
@@ -89,7 +89,7 @@ export function processTwCall(
89
89
  // Add modifier styles
90
90
  for (const [modifierType, modifiers] of modifiersByType) {
91
91
  const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
92
- const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
92
+ const modifierStyleObject = parseClassName(modifierClassNames, state.customTheme);
93
93
  const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
94
94
  state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
95
95
 
@@ -14,18 +14,26 @@ import { parseSpacing } from "./spacing";
14
14
  import { parseTransform } from "./transforms";
15
15
  import { parseTypography } from "./typography";
16
16
 
17
+ /**
18
+ * Custom theme configuration (subset of tailwind.config theme extensions)
19
+ */
20
+ export type CustomTheme = {
21
+ colors?: Record<string, string>;
22
+ fontFamily?: Record<string, string>;
23
+ };
24
+
17
25
  /**
18
26
  * Parse a className string and return a React Native style object
19
27
  * @param className - Space-separated class names
20
- * @param customColors - Optional custom colors from tailwind.config
28
+ * @param customTheme - Optional custom theme from tailwind.config
21
29
  * @returns React Native style object
22
30
  */
23
- export function parseClassName(className: string, customColors?: Record<string, string>): StyleObject {
31
+ export function parseClassName(className: string, customTheme?: CustomTheme): StyleObject {
24
32
  const classes = className.split(/\s+/).filter(Boolean);
25
33
  const style: StyleObject = {};
26
34
 
27
35
  for (const cls of classes) {
28
- const parsedStyle = parseClass(cls, customColors);
36
+ const parsedStyle = parseClass(cls, customTheme);
29
37
  Object.assign(style, parsedStyle);
30
38
  }
31
39
 
@@ -35,19 +43,19 @@ export function parseClassName(className: string, customColors?: Record<string,
35
43
  /**
36
44
  * Parse a single class name
37
45
  * @param cls - Single class name
38
- * @param customColors - Optional custom colors from tailwind.config
46
+ * @param customTheme - Optional custom theme from tailwind.config
39
47
  * @returns React Native style object
40
48
  */
41
- export function parseClass(cls: string, customColors?: Record<string, string>): StyleObject {
49
+ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject {
42
50
  // Try each parser in order
43
51
  // Note: parseBorder must come before parseColor to avoid border-[3px] being parsed as a color
44
- // parseColor gets custom colors, others don't need it
52
+ // parseColor and parseTypography get custom theme, others don't need it
45
53
  const parsers: Array<(cls: string) => StyleObject | null> = [
46
54
  parseSpacing,
47
55
  parseBorder,
48
- (cls: string) => parseColor(cls, customColors),
56
+ (cls: string) => parseColor(cls, customTheme?.colors),
49
57
  parseLayout,
50
- parseTypography,
58
+ (cls: string) => parseTypography(cls, customTheme?.fontFamily),
51
59
  parseSizing,
52
60
  parseShadow,
53
61
  parseAspectRatio,