@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/dist/babel/plugin.ts
CHANGED
|
@@ -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,6 +268,8 @@ 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;
|
|
@@ -260,15 +305,21 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
260
305
|
addPlatformImport(path, t);
|
|
261
306
|
}
|
|
262
307
|
|
|
263
|
-
// Add
|
|
308
|
+
// Add color scheme hook import if color scheme modifiers were used and not already present
|
|
264
309
|
if (state.needsColorSchemeImport && !state.hasColorSchemeImport) {
|
|
265
|
-
addColorSchemeImport(path, t);
|
|
310
|
+
addColorSchemeImport(path, state.colorSchemeImportSource, state.colorSchemeHookName, t);
|
|
266
311
|
}
|
|
267
312
|
|
|
268
|
-
// Inject
|
|
313
|
+
// Inject color scheme hook in function components that need it
|
|
269
314
|
if (state.needsColorSchemeImport) {
|
|
270
315
|
for (const functionPath of state.functionComponentsNeedingColorScheme) {
|
|
271
|
-
injectColorSchemeHook(
|
|
316
|
+
injectColorSchemeHook(
|
|
317
|
+
functionPath,
|
|
318
|
+
state.colorSchemeVariableName,
|
|
319
|
+
state.colorSchemeHookName,
|
|
320
|
+
state.colorSchemeLocalIdentifier,
|
|
321
|
+
t,
|
|
322
|
+
);
|
|
272
323
|
}
|
|
273
324
|
}
|
|
274
325
|
|
|
@@ -300,13 +351,6 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
300
351
|
return false;
|
|
301
352
|
});
|
|
302
353
|
|
|
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
354
|
// Only track if imports exist - don't mutate yet
|
|
311
355
|
// Actual import injection happens in Program.exit only if needed
|
|
312
356
|
if (hasStyleSheet) {
|
|
@@ -317,14 +361,28 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
317
361
|
state.hasPlatformImport = true;
|
|
318
362
|
}
|
|
319
363
|
|
|
320
|
-
if (hasUseColorScheme) {
|
|
321
|
-
state.hasColorSchemeImport = true;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
364
|
// Store reference to the react-native import for later modification if needed
|
|
325
365
|
state.reactNativeImportPath = path;
|
|
326
366
|
}
|
|
327
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
|
+
|
|
328
386
|
// Track tw/twStyle imports from main package (for compile-time transformation)
|
|
329
387
|
if (node.source.value === "@mgcrea/react-native-tailwind") {
|
|
330
388
|
const specifiers = node.specifiers;
|
|
@@ -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
|
|
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(
|
|
71
|
-
|
|
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
|
|
78
|
+
let existingValueImport: BabelTypes.ImportDeclaration | null = null;
|
|
74
79
|
|
|
75
80
|
for (const statement of body) {
|
|
76
|
-
if (t.isImportDeclaration(statement) && statement.source.value ===
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 (!
|
|
92
|
-
// Add
|
|
93
|
-
|
|
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
|
-
//
|
|
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(
|
|
101
|
-
t.stringLiteral(
|
|
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
|
|
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
|
-
//
|
|
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(
|
|
173
|
+
t.callExpression(t.identifier(identifierToCall), []),
|
|
159
174
|
),
|
|
160
175
|
]);
|
|
161
176
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgcrea/react-native-tailwind",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Compile-time Tailwind CSS for React Native with zero runtime overhead",
|
|
5
5
|
"author": "Olivier Louvignes <olivier@mgcrea.io> (https://github.com/mgcrea)",
|
|
6
6
|
"homepage": "https://github.com/mgcrea/react-native-tailwind#readme",
|
package/src/babel/plugin.test.ts
CHANGED
|
@@ -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 = `
|