@mgcrea/react-native-tailwind 0.11.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.
- package/README.md +129 -0
- package/dist/babel/index.cjs +41 -27
- package/dist/babel/plugin.d.ts +37 -0
- package/dist/babel/plugin.test.ts +275 -1
- package/dist/babel/plugin.ts +73 -15
- package/dist/babel/utils/styleInjection.d.ts +5 -3
- package/dist/babel/utils/styleInjection.ts +38 -23
- package/package.json +1 -1
- package/src/babel/plugin.test.ts +275 -1
- package/src/babel/plugin.ts +73 -15
- package/src/babel/utils/styleInjection.ts +38 -23
package/README.md
CHANGED
|
@@ -1887,6 +1887,135 @@ const styles = StyleSheet.create({
|
|
|
1887
1887
|
- Choose a name that won't conflict with existing variables in your files
|
|
1888
1888
|
- The same identifier is used across all files in your project
|
|
1889
1889
|
|
|
1890
|
+
### Custom Color Scheme Hook
|
|
1891
|
+
|
|
1892
|
+
By default, the plugin uses React Native's built-in `useColorScheme()` hook for `dark:` and `light:` modifiers. You can configure it to use a custom color scheme hook from theme providers like React Navigation, Expo, or your own implementation.
|
|
1893
|
+
|
|
1894
|
+
**Configuration:**
|
|
1895
|
+
|
|
1896
|
+
```javascript
|
|
1897
|
+
// babel.config.js
|
|
1898
|
+
module.exports = {
|
|
1899
|
+
plugins: [
|
|
1900
|
+
[
|
|
1901
|
+
"@mgcrea/react-native-tailwind/babel",
|
|
1902
|
+
{
|
|
1903
|
+
colorScheme: {
|
|
1904
|
+
importFrom: "@/hooks/useColorScheme", // Module to import from
|
|
1905
|
+
importName: "useColorScheme", // Hook name to import
|
|
1906
|
+
},
|
|
1907
|
+
},
|
|
1908
|
+
],
|
|
1909
|
+
],
|
|
1910
|
+
};
|
|
1911
|
+
```
|
|
1912
|
+
|
|
1913
|
+
**Use Cases:**
|
|
1914
|
+
|
|
1915
|
+
#### 1. Custom Theme Provider
|
|
1916
|
+
|
|
1917
|
+
Override system color scheme with user preferences from a store:
|
|
1918
|
+
|
|
1919
|
+
```typescript
|
|
1920
|
+
// src/hooks/useColorScheme.ts
|
|
1921
|
+
import { useColorScheme as useSystemColorScheme } from "react-native";
|
|
1922
|
+
import { profileStore } from "@/stores/profileStore";
|
|
1923
|
+
import { type ColorSchemeName } from "react-native";
|
|
1924
|
+
|
|
1925
|
+
export const useColorScheme = (): ColorSchemeName => {
|
|
1926
|
+
const systemColorScheme = useSystemColorScheme();
|
|
1927
|
+
const userTheme = profileStore.theme; // 'dark' | 'light' | 'auto'
|
|
1928
|
+
|
|
1929
|
+
// Return user preference, or fall back to system if set to 'auto'
|
|
1930
|
+
return userTheme === 'auto' ? systemColorScheme : userTheme;
|
|
1931
|
+
};
|
|
1932
|
+
```
|
|
1933
|
+
|
|
1934
|
+
```javascript
|
|
1935
|
+
// babel.config.js
|
|
1936
|
+
{
|
|
1937
|
+
colorScheme: {
|
|
1938
|
+
importFrom: "@/hooks/useColorScheme",
|
|
1939
|
+
importName: "useColorScheme"
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
```
|
|
1943
|
+
|
|
1944
|
+
#### 2. React Navigation Theme
|
|
1945
|
+
|
|
1946
|
+
Integrate with React Navigation's theme system:
|
|
1947
|
+
|
|
1948
|
+
```typescript
|
|
1949
|
+
// Wrap React Navigation's useTheme to return ColorSchemeName
|
|
1950
|
+
import { useTheme as useNavTheme } from "@react-navigation/native";
|
|
1951
|
+
import { type ColorSchemeName } from "react-native";
|
|
1952
|
+
|
|
1953
|
+
export const useColorScheme = (): ColorSchemeName => {
|
|
1954
|
+
const { dark } = useNavTheme();
|
|
1955
|
+
return dark ? "dark" : "light";
|
|
1956
|
+
};
|
|
1957
|
+
```
|
|
1958
|
+
|
|
1959
|
+
#### 3. Expo Router Theme
|
|
1960
|
+
|
|
1961
|
+
Use Expo Router's theme hook:
|
|
1962
|
+
|
|
1963
|
+
```javascript
|
|
1964
|
+
// babel.config.js
|
|
1965
|
+
{
|
|
1966
|
+
colorScheme: {
|
|
1967
|
+
importFrom: "expo-router",
|
|
1968
|
+
importName: "useColorScheme"
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
```
|
|
1972
|
+
|
|
1973
|
+
#### 4. Testing
|
|
1974
|
+
|
|
1975
|
+
Mock color scheme for tests:
|
|
1976
|
+
|
|
1977
|
+
```typescript
|
|
1978
|
+
// test/mocks/useColorScheme.ts
|
|
1979
|
+
export const useColorScheme = () => "light"; // Or "dark" for dark mode tests
|
|
1980
|
+
```
|
|
1981
|
+
|
|
1982
|
+
```javascript
|
|
1983
|
+
// babel.config.js (test environment)
|
|
1984
|
+
{
|
|
1985
|
+
colorScheme: {
|
|
1986
|
+
importFrom: "@/test/mocks/useColorScheme",
|
|
1987
|
+
importName: "useColorScheme"
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
```
|
|
1991
|
+
|
|
1992
|
+
#### How it works
|
|
1993
|
+
|
|
1994
|
+
When you use `dark:` or `light:` modifiers:
|
|
1995
|
+
|
|
1996
|
+
```tsx
|
|
1997
|
+
<View className="bg-white dark:bg-gray-900" />
|
|
1998
|
+
```
|
|
1999
|
+
|
|
2000
|
+
The plugin will:
|
|
2001
|
+
|
|
2002
|
+
1. Import your custom hook: `import { useColorScheme } from "@/hooks/useColorScheme"`
|
|
2003
|
+
2. Inject it in components: `const _twColorScheme = useColorScheme();`
|
|
2004
|
+
3. Generate conditionals: `_twColorScheme === "dark" && styles._dark_bg_gray_900`
|
|
2005
|
+
|
|
2006
|
+
#### Default behavior (no configuration)
|
|
2007
|
+
|
|
2008
|
+
Without custom configuration, the plugin uses React Native's built-in hook:
|
|
2009
|
+
|
|
2010
|
+
- Import: `import { useColorScheme } from "react-native"`
|
|
2011
|
+
- This works out of the box for basic system color scheme detection
|
|
2012
|
+
|
|
2013
|
+
#### Requirements
|
|
2014
|
+
|
|
2015
|
+
- Your custom hook must return `ColorSchemeName` (type from React Native: `"light" | "dark" | null | undefined`)
|
|
2016
|
+
- The hook must be compatible with React's rules of hooks (can only be called in function components)
|
|
2017
|
+
- Import merging works automatically if you already import from the same source
|
|
2018
|
+
|
|
1890
2019
|
### Arbitrary Values
|
|
1891
2020
|
|
|
1892
2021
|
Use arbitrary values for custom sizes, spacing, and borders not in the preset scales:
|
package/dist/babel/index.cjs
CHANGED
|
@@ -2518,33 +2518,33 @@ function addPlatformImport(path2, t) {
|
|
|
2518
2518
|
path2.unshiftContainer("body", importDeclaration);
|
|
2519
2519
|
}
|
|
2520
2520
|
}
|
|
2521
|
-
function addColorSchemeImport(path2, t) {
|
|
2521
|
+
function addColorSchemeImport(path2, importSource, hookName, t) {
|
|
2522
2522
|
const body = path2.node.body;
|
|
2523
|
-
let
|
|
2523
|
+
let existingValueImport = null;
|
|
2524
2524
|
for (const statement of body) {
|
|
2525
|
-
if (t.isImportDeclaration(statement) && statement.source.value ===
|
|
2526
|
-
|
|
2527
|
-
|
|
2525
|
+
if (t.isImportDeclaration(statement) && statement.source.value === importSource) {
|
|
2526
|
+
if (statement.importKind !== "type") {
|
|
2527
|
+
existingValueImport = statement;
|
|
2528
|
+
break;
|
|
2529
|
+
}
|
|
2528
2530
|
}
|
|
2529
2531
|
}
|
|
2530
|
-
if (
|
|
2531
|
-
const
|
|
2532
|
-
(spec) => t.isImportSpecifier(spec) && spec.imported.type === "Identifier" && spec.imported.name ===
|
|
2532
|
+
if (existingValueImport) {
|
|
2533
|
+
const hasHook = existingValueImport.specifiers.some(
|
|
2534
|
+
(spec) => t.isImportSpecifier(spec) && spec.imported.type === "Identifier" && spec.imported.name === hookName
|
|
2533
2535
|
);
|
|
2534
|
-
if (!
|
|
2535
|
-
|
|
2536
|
-
t.importSpecifier(t.identifier("useColorScheme"), t.identifier("useColorScheme"))
|
|
2537
|
-
);
|
|
2536
|
+
if (!hasHook) {
|
|
2537
|
+
existingValueImport.specifiers.push(t.importSpecifier(t.identifier(hookName), t.identifier(hookName)));
|
|
2538
2538
|
}
|
|
2539
2539
|
} else {
|
|
2540
2540
|
const importDeclaration = t.importDeclaration(
|
|
2541
|
-
[t.importSpecifier(t.identifier(
|
|
2542
|
-
t.stringLiteral(
|
|
2541
|
+
[t.importSpecifier(t.identifier(hookName), t.identifier(hookName))],
|
|
2542
|
+
t.stringLiteral(importSource)
|
|
2543
2543
|
);
|
|
2544
2544
|
path2.unshiftContainer("body", importDeclaration);
|
|
2545
2545
|
}
|
|
2546
2546
|
}
|
|
2547
|
-
function injectColorSchemeHook(functionPath, colorSchemeVariableName, t) {
|
|
2547
|
+
function injectColorSchemeHook(functionPath, colorSchemeVariableName, hookName, localIdentifier, t) {
|
|
2548
2548
|
let body = functionPath.node.body;
|
|
2549
2549
|
if (!t.isBlockStatement(body)) {
|
|
2550
2550
|
if (t.isArrowFunctionExpression(functionPath.node) && t.isExpression(body)) {
|
|
@@ -2566,10 +2566,11 @@ function injectColorSchemeHook(functionPath, colorSchemeVariableName, t) {
|
|
|
2566
2566
|
if (hasHook) {
|
|
2567
2567
|
return false;
|
|
2568
2568
|
}
|
|
2569
|
+
const identifierToCall = localIdentifier ?? hookName;
|
|
2569
2570
|
const hookCall = t.variableDeclaration("const", [
|
|
2570
2571
|
t.variableDeclarator(
|
|
2571
2572
|
t.identifier(colorSchemeVariableName),
|
|
2572
|
-
t.callExpression(t.identifier(
|
|
2573
|
+
t.callExpression(t.identifier(identifierToCall), [])
|
|
2573
2574
|
)
|
|
2574
2575
|
]);
|
|
2575
2576
|
body.body.unshift(hookCall);
|
|
@@ -2852,6 +2853,8 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2852
2853
|
darkSuffix: options?.schemeModifier?.darkSuffix ?? "-dark",
|
|
2853
2854
|
lightSuffix: options?.schemeModifier?.lightSuffix ?? "-light"
|
|
2854
2855
|
};
|
|
2856
|
+
const colorSchemeImportSource = options?.colorScheme?.importFrom ?? "react-native";
|
|
2857
|
+
const colorSchemeHookName = options?.colorScheme?.importName ?? "useColorScheme";
|
|
2855
2858
|
return {
|
|
2856
2859
|
name: "react-native-tailwind",
|
|
2857
2860
|
visitor: {
|
|
@@ -2865,6 +2868,8 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2865
2868
|
state.hasColorSchemeImport = false;
|
|
2866
2869
|
state.needsColorSchemeImport = false;
|
|
2867
2870
|
state.colorSchemeVariableName = "_twColorScheme";
|
|
2871
|
+
state.colorSchemeImportSource = colorSchemeImportSource;
|
|
2872
|
+
state.colorSchemeHookName = colorSchemeHookName;
|
|
2868
2873
|
state.supportedAttributes = exactMatches;
|
|
2869
2874
|
state.attributePatterns = patterns;
|
|
2870
2875
|
state.stylesIdentifier = stylesIdentifier;
|
|
@@ -2888,11 +2893,17 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2888
2893
|
addPlatformImport(path2, t);
|
|
2889
2894
|
}
|
|
2890
2895
|
if (state.needsColorSchemeImport && !state.hasColorSchemeImport) {
|
|
2891
|
-
addColorSchemeImport(path2, t);
|
|
2896
|
+
addColorSchemeImport(path2, state.colorSchemeImportSource, state.colorSchemeHookName, t);
|
|
2892
2897
|
}
|
|
2893
2898
|
if (state.needsColorSchemeImport) {
|
|
2894
2899
|
for (const functionPath of state.functionComponentsNeedingColorScheme) {
|
|
2895
|
-
injectColorSchemeHook(
|
|
2900
|
+
injectColorSchemeHook(
|
|
2901
|
+
functionPath,
|
|
2902
|
+
state.colorSchemeVariableName,
|
|
2903
|
+
state.colorSchemeHookName,
|
|
2904
|
+
state.colorSchemeLocalIdentifier,
|
|
2905
|
+
t
|
|
2906
|
+
);
|
|
2896
2907
|
}
|
|
2897
2908
|
}
|
|
2898
2909
|
injectStylesAtTop(path2, state.styleRegistry, state.stylesIdentifier, t);
|
|
@@ -2915,23 +2926,26 @@ function reactNativeTailwindBabelPlugin({ types: t }, options) {
|
|
|
2915
2926
|
}
|
|
2916
2927
|
return false;
|
|
2917
2928
|
});
|
|
2918
|
-
const hasUseColorScheme = specifiers.some((spec) => {
|
|
2919
|
-
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
2920
|
-
return spec.imported.name === "useColorScheme";
|
|
2921
|
-
}
|
|
2922
|
-
return false;
|
|
2923
|
-
});
|
|
2924
2929
|
if (hasStyleSheet) {
|
|
2925
2930
|
state.hasStyleSheetImport = true;
|
|
2926
2931
|
}
|
|
2927
2932
|
if (hasPlatform) {
|
|
2928
2933
|
state.hasPlatformImport = true;
|
|
2929
2934
|
}
|
|
2930
|
-
if (hasUseColorScheme) {
|
|
2931
|
-
state.hasColorSchemeImport = true;
|
|
2932
|
-
}
|
|
2933
2935
|
state.reactNativeImportPath = path2;
|
|
2934
2936
|
}
|
|
2937
|
+
if (node.source.value === state.colorSchemeImportSource) {
|
|
2938
|
+
const specifiers = node.specifiers;
|
|
2939
|
+
for (const spec of specifiers) {
|
|
2940
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
2941
|
+
if (spec.imported.name === state.colorSchemeHookName) {
|
|
2942
|
+
state.hasColorSchemeImport = true;
|
|
2943
|
+
state.colorSchemeLocalIdentifier = spec.local.name;
|
|
2944
|
+
break;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2935
2949
|
if (node.source.value === "@mgcrea/react-native-tailwind") {
|
|
2936
2950
|
const specifiers = node.specifiers;
|
|
2937
2951
|
specifiers.forEach((spec) => {
|
package/dist/babel/plugin.d.ts
CHANGED
|
@@ -41,6 +41,40 @@ export type PluginOptions = {
|
|
|
41
41
|
darkSuffix?: string;
|
|
42
42
|
lightSuffix?: string;
|
|
43
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* Configuration for color scheme hook import (dark:/light: modifiers)
|
|
46
|
+
*
|
|
47
|
+
* Allows using custom color scheme hooks from theme providers instead of
|
|
48
|
+
* React Native's built-in useColorScheme.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // Use custom hook from theme provider
|
|
52
|
+
* {
|
|
53
|
+
* importFrom: '@/hooks/useColorScheme',
|
|
54
|
+
* importName: 'useColorScheme'
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // Use React Navigation theme
|
|
59
|
+
* {
|
|
60
|
+
* importFrom: '@react-navigation/native',
|
|
61
|
+
* importName: 'useTheme' // You'd wrap this to return ColorSchemeName
|
|
62
|
+
* }
|
|
63
|
+
*
|
|
64
|
+
* @default { importFrom: 'react-native', importName: 'useColorScheme' }
|
|
65
|
+
*/
|
|
66
|
+
colorScheme?: {
|
|
67
|
+
/**
|
|
68
|
+
* Module to import the color scheme hook from
|
|
69
|
+
* @default 'react-native'
|
|
70
|
+
*/
|
|
71
|
+
importFrom?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Name of the hook to import
|
|
74
|
+
* @default 'useColorScheme'
|
|
75
|
+
*/
|
|
76
|
+
importName?: string;
|
|
77
|
+
};
|
|
44
78
|
};
|
|
45
79
|
type PluginState = PluginPass & {
|
|
46
80
|
styleRegistry: Map<string, StyleObject>;
|
|
@@ -51,6 +85,9 @@ type PluginState = PluginPass & {
|
|
|
51
85
|
hasColorSchemeImport: boolean;
|
|
52
86
|
needsColorSchemeImport: boolean;
|
|
53
87
|
colorSchemeVariableName: string;
|
|
88
|
+
colorSchemeImportSource: string;
|
|
89
|
+
colorSchemeHookName: string;
|
|
90
|
+
colorSchemeLocalIdentifier?: string;
|
|
54
91
|
customTheme: CustomTheme;
|
|
55
92
|
schemeModifierConfig: SchemeModifierConfig;
|
|
56
93
|
supportedAttributes: Set<string>;
|
|
@@ -7,7 +7,9 @@ import babelPlugin, { type PluginOptions } from "./plugin.js";
|
|
|
7
7
|
* Helper to transform code with the Babel plugin
|
|
8
8
|
*/
|
|
9
9
|
function transform(code: string, options?: PluginOptions, includeJsx = false) {
|
|
10
|
-
const presets = includeJsx
|
|
10
|
+
const presets = includeJsx
|
|
11
|
+
? ["@babel/preset-react", ["@babel/preset-typescript", { isTSX: true, allExtensions: true }]]
|
|
12
|
+
: [];
|
|
11
13
|
|
|
12
14
|
const result = transformSync(code, {
|
|
13
15
|
presets,
|
|
@@ -1022,6 +1024,278 @@ describe("Babel plugin - color scheme modifier transformation", () => {
|
|
|
1022
1024
|
});
|
|
1023
1025
|
});
|
|
1024
1026
|
|
|
1027
|
+
describe("Babel plugin - custom color scheme hook import", () => {
|
|
1028
|
+
it("should use custom import source for color scheme hook", () => {
|
|
1029
|
+
const input = `
|
|
1030
|
+
import React from 'react';
|
|
1031
|
+
import { View } from 'react-native';
|
|
1032
|
+
|
|
1033
|
+
export function Component() {
|
|
1034
|
+
return <View className="dark:bg-gray-900" />;
|
|
1035
|
+
}
|
|
1036
|
+
`;
|
|
1037
|
+
|
|
1038
|
+
const output = transform(
|
|
1039
|
+
input,
|
|
1040
|
+
{
|
|
1041
|
+
colorScheme: {
|
|
1042
|
+
importFrom: "@/hooks/useColorScheme",
|
|
1043
|
+
importName: "useColorScheme",
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
true,
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
// Should import from custom source
|
|
1050
|
+
expect(output).toContain('from "@/hooks/useColorScheme"');
|
|
1051
|
+
expect(output).not.toContain('useColorScheme } from "react-native"');
|
|
1052
|
+
|
|
1053
|
+
// Should inject hook call
|
|
1054
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1055
|
+
|
|
1056
|
+
// Should have conditional styling
|
|
1057
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it("should use custom hook name", () => {
|
|
1061
|
+
const input = `
|
|
1062
|
+
import React from 'react';
|
|
1063
|
+
import { View } from 'react-native';
|
|
1064
|
+
|
|
1065
|
+
export function Component() {
|
|
1066
|
+
return <View className="dark:bg-gray-900" />;
|
|
1067
|
+
}
|
|
1068
|
+
`;
|
|
1069
|
+
|
|
1070
|
+
const output = transform(
|
|
1071
|
+
input,
|
|
1072
|
+
{
|
|
1073
|
+
colorScheme: {
|
|
1074
|
+
importFrom: "@react-navigation/native",
|
|
1075
|
+
importName: "useTheme",
|
|
1076
|
+
},
|
|
1077
|
+
},
|
|
1078
|
+
true,
|
|
1079
|
+
);
|
|
1080
|
+
|
|
1081
|
+
// Should import useTheme from React Navigation
|
|
1082
|
+
expect(output).toContain('from "@react-navigation/native"');
|
|
1083
|
+
expect(output).toContain("useTheme");
|
|
1084
|
+
|
|
1085
|
+
// Should call useTheme hook
|
|
1086
|
+
expect(output).toContain("_twColorScheme = useTheme()");
|
|
1087
|
+
|
|
1088
|
+
// Should have conditional styling
|
|
1089
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it("should merge custom hook with existing import from same source", () => {
|
|
1093
|
+
const input = `
|
|
1094
|
+
import React from 'react';
|
|
1095
|
+
import { View, Text } from 'react-native';
|
|
1096
|
+
import { useNavigation } from '@react-navigation/native';
|
|
1097
|
+
|
|
1098
|
+
export function Component() {
|
|
1099
|
+
const navigation = useNavigation();
|
|
1100
|
+
return (
|
|
1101
|
+
<View className="dark:bg-gray-900">
|
|
1102
|
+
<Text onPress={() => navigation.navigate('Home')}>Go Home</Text>
|
|
1103
|
+
</View>
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
`;
|
|
1107
|
+
|
|
1108
|
+
const output = transform(
|
|
1109
|
+
input,
|
|
1110
|
+
{
|
|
1111
|
+
colorScheme: {
|
|
1112
|
+
importFrom: "@react-navigation/native",
|
|
1113
|
+
importName: "useTheme",
|
|
1114
|
+
},
|
|
1115
|
+
},
|
|
1116
|
+
true,
|
|
1117
|
+
);
|
|
1118
|
+
|
|
1119
|
+
// Should merge with existing import (both useNavigation and useTheme in same import)
|
|
1120
|
+
expect(output).toMatch(
|
|
1121
|
+
/import\s+\{\s*useNavigation[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/,
|
|
1122
|
+
);
|
|
1123
|
+
expect(output).toContain("useNavigation()");
|
|
1124
|
+
expect(output).toContain("useTheme()");
|
|
1125
|
+
|
|
1126
|
+
// Should only have one import from that source
|
|
1127
|
+
const importCount = (output.match(/@react-navigation\/native/g) ?? []).length;
|
|
1128
|
+
expect(importCount).toBe(1);
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
it("should not duplicate custom hook if already imported", () => {
|
|
1132
|
+
const input = `
|
|
1133
|
+
import React from 'react';
|
|
1134
|
+
import { View } from 'react-native';
|
|
1135
|
+
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
1136
|
+
|
|
1137
|
+
export function Component() {
|
|
1138
|
+
return <View className="dark:bg-gray-900" />;
|
|
1139
|
+
}
|
|
1140
|
+
`;
|
|
1141
|
+
|
|
1142
|
+
const output = transform(
|
|
1143
|
+
input,
|
|
1144
|
+
{
|
|
1145
|
+
colorScheme: {
|
|
1146
|
+
importFrom: "@/hooks/useColorScheme",
|
|
1147
|
+
importName: "useColorScheme",
|
|
1148
|
+
},
|
|
1149
|
+
},
|
|
1150
|
+
true,
|
|
1151
|
+
);
|
|
1152
|
+
|
|
1153
|
+
// Should not add duplicate import
|
|
1154
|
+
const importMatches = output.match(/import.*useColorScheme.*from ['"]@\/hooks\/useColorScheme['"]/g);
|
|
1155
|
+
expect(importMatches).toHaveLength(1);
|
|
1156
|
+
|
|
1157
|
+
// Should still inject hook call
|
|
1158
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
it("should use react-native by default when no custom config provided", () => {
|
|
1162
|
+
const input = `
|
|
1163
|
+
import React from 'react';
|
|
1164
|
+
import { View } from 'react-native';
|
|
1165
|
+
|
|
1166
|
+
export function Component() {
|
|
1167
|
+
return <View className="dark:bg-gray-900" />;
|
|
1168
|
+
}
|
|
1169
|
+
`;
|
|
1170
|
+
|
|
1171
|
+
const output = transform(input, undefined, true);
|
|
1172
|
+
|
|
1173
|
+
// Should use default react-native import (can be single or double quotes)
|
|
1174
|
+
expect(output).toMatch(/useColorScheme\s*}\s*from\s+['"]react-native['"]/);
|
|
1175
|
+
expect(output).not.toContain("@/hooks");
|
|
1176
|
+
expect(output).not.toContain("@react-navigation");
|
|
1177
|
+
|
|
1178
|
+
// Should inject hook call with default name
|
|
1179
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
it("should create separate import when only type-only import exists", () => {
|
|
1183
|
+
const input = `
|
|
1184
|
+
import React from 'react';
|
|
1185
|
+
import { View } from 'react-native';
|
|
1186
|
+
import type { NavigationProp } from '@react-navigation/native';
|
|
1187
|
+
|
|
1188
|
+
export function Component() {
|
|
1189
|
+
return <View className="dark:bg-gray-900" />;
|
|
1190
|
+
}
|
|
1191
|
+
`;
|
|
1192
|
+
|
|
1193
|
+
const output = transform(
|
|
1194
|
+
input,
|
|
1195
|
+
{
|
|
1196
|
+
colorScheme: {
|
|
1197
|
+
importFrom: "@react-navigation/native",
|
|
1198
|
+
importName: "useTheme",
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
true,
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
// TypeScript preset strips type-only imports, but the important thing is:
|
|
1205
|
+
// 1. useTheme hook is imported (not skipped thinking it was already imported)
|
|
1206
|
+
// 2. Hook is correctly called in the component
|
|
1207
|
+
expect(output).toMatch(/import\s+\{\s*useTheme\s*\}\s+from\s+['"]@react-navigation\/native['"]/);
|
|
1208
|
+
expect(output).toContain("_twColorScheme = useTheme()");
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
it("should use aliased identifier when hook is already imported with alias", () => {
|
|
1212
|
+
const input = `
|
|
1213
|
+
import React from 'react';
|
|
1214
|
+
import { View, Text } from 'react-native';
|
|
1215
|
+
import { useTheme as navTheme } from '@react-navigation/native';
|
|
1216
|
+
|
|
1217
|
+
export function Component() {
|
|
1218
|
+
const theme = navTheme();
|
|
1219
|
+
return (
|
|
1220
|
+
<View className="dark:bg-gray-900">
|
|
1221
|
+
<Text>{theme.dark ? 'Dark' : 'Light'}</Text>
|
|
1222
|
+
</View>
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
`;
|
|
1226
|
+
|
|
1227
|
+
const output = transform(
|
|
1228
|
+
input,
|
|
1229
|
+
{
|
|
1230
|
+
colorScheme: {
|
|
1231
|
+
importFrom: "@react-navigation/native",
|
|
1232
|
+
importName: "useTheme",
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
true,
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
// Should not add duplicate import
|
|
1239
|
+
const importMatches = output.match(
|
|
1240
|
+
/import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g,
|
|
1241
|
+
);
|
|
1242
|
+
expect(importMatches).toHaveLength(1);
|
|
1243
|
+
|
|
1244
|
+
// Should still have the aliased import
|
|
1245
|
+
expect(output).toMatch(/useTheme\s+as\s+navTheme/);
|
|
1246
|
+
|
|
1247
|
+
// Should call the aliased name (navTheme), not the export name (useTheme)
|
|
1248
|
+
// Both the user's code and our injected hook should use navTheme
|
|
1249
|
+
expect(output).toContain("_twColorScheme = navTheme()");
|
|
1250
|
+
expect(output).not.toContain("_twColorScheme = useTheme()");
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
it("should handle both type-only and aliased imports together", () => {
|
|
1254
|
+
const input = `
|
|
1255
|
+
import React from 'react';
|
|
1256
|
+
import { View, Text } from 'react-native';
|
|
1257
|
+
import type { Theme } from '@react-navigation/native';
|
|
1258
|
+
import { useTheme as getNavTheme } from '@react-navigation/native';
|
|
1259
|
+
|
|
1260
|
+
export function Component() {
|
|
1261
|
+
const theme = getNavTheme();
|
|
1262
|
+
return (
|
|
1263
|
+
<View className="dark:bg-gray-900">
|
|
1264
|
+
<Text>{theme.dark ? 'Dark Mode' : 'Light Mode'}</Text>
|
|
1265
|
+
</View>
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
`;
|
|
1269
|
+
|
|
1270
|
+
const output = transform(
|
|
1271
|
+
input,
|
|
1272
|
+
{
|
|
1273
|
+
colorScheme: {
|
|
1274
|
+
importFrom: "@react-navigation/native",
|
|
1275
|
+
importName: "useTheme",
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
true,
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
// TypeScript preset strips type-only imports
|
|
1282
|
+
// The important thing is: should not add duplicate import, and should use aliased name
|
|
1283
|
+
expect(output).toMatch(
|
|
1284
|
+
/import\s+\{[^}]*useTheme\s+as\s+getNavTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/,
|
|
1285
|
+
);
|
|
1286
|
+
|
|
1287
|
+
// Should not add duplicate import - useTheme should only appear in the aliased import
|
|
1288
|
+
const useThemeImports = output.match(
|
|
1289
|
+
/import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g,
|
|
1290
|
+
);
|
|
1291
|
+
expect(useThemeImports).toHaveLength(1);
|
|
1292
|
+
|
|
1293
|
+
// Should call the aliased name for both user code and our injected hook
|
|
1294
|
+
expect(output).toContain("_twColorScheme = getNavTheme()");
|
|
1295
|
+
expect(output).not.toContain("_twColorScheme = useTheme()");
|
|
1296
|
+
});
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1025
1299
|
describe("Babel plugin - import injection", () => {
|
|
1026
1300
|
it("should not add StyleSheet import to files without className usage", () => {
|
|
1027
1301
|
const input = `
|