@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.
- package/README.md +129 -0
- package/dist/babel/index.cjs +229 -46
- package/dist/babel/plugin.d.ts +37 -0
- package/dist/babel/plugin.test.ts +773 -1
- package/dist/babel/plugin.ts +127 -30
- package/dist/babel/utils/styleInjection.d.ts +5 -3
- package/dist/babel/utils/styleInjection.ts +38 -23
- package/dist/babel/utils/twProcessing.d.ts +8 -1
- package/dist/babel/utils/twProcessing.ts +212 -4
- package/dist/parser/spacing.d.ts +1 -1
- package/dist/parser/spacing.js +1 -1
- package/dist/parser/spacing.test.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +2 -2
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +2 -2
- package/dist/runtime.test.js +1 -1
- package/dist/types/runtime.d.ts +8 -1
- package/package.json +1 -1
- package/src/babel/plugin.test.ts +773 -1
- package/src/babel/plugin.ts +127 -30
- package/src/babel/utils/styleInjection.ts +38 -23
- package/src/babel/utils/twProcessing.ts +212 -4
- package/src/parser/spacing.test.ts +62 -0
- package/src/parser/spacing.ts +7 -7
- package/src/runtime.test.ts +4 -1
- package/src/types/runtime.ts +8 -1
|
@@ -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,
|
|
@@ -332,6 +334,89 @@ describe("Babel plugin - className transformation (existing behavior)", () => {
|
|
|
332
334
|
// Should not have className in output
|
|
333
335
|
expect(output).not.toContain("className");
|
|
334
336
|
});
|
|
337
|
+
|
|
338
|
+
it('should transform className={"..."} (string literal in expression container)', () => {
|
|
339
|
+
const input = `
|
|
340
|
+
import { View } from 'react-native';
|
|
341
|
+
export function Component() {
|
|
342
|
+
return <View className={"flex-row items-center justify-start"} />;
|
|
343
|
+
}
|
|
344
|
+
`;
|
|
345
|
+
|
|
346
|
+
const output = transform(input, undefined, true);
|
|
347
|
+
|
|
348
|
+
// Should have StyleSheet
|
|
349
|
+
expect(output).toContain("StyleSheet.create");
|
|
350
|
+
expect(output).toContain("_twStyles");
|
|
351
|
+
|
|
352
|
+
// Should replace className with style
|
|
353
|
+
expect(output).not.toContain("className");
|
|
354
|
+
expect(output).toContain("style:");
|
|
355
|
+
|
|
356
|
+
// Should have the expected style keys
|
|
357
|
+
expect(output).toContain("_flex_row_items_center_justify_start");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should transform className={"..."} with modifiers', () => {
|
|
361
|
+
const input = `
|
|
362
|
+
import { Pressable } from 'react-native';
|
|
363
|
+
export function Component() {
|
|
364
|
+
return <Pressable className={"bg-blue-500 active:bg-blue-700 p-4"} />;
|
|
365
|
+
}
|
|
366
|
+
`;
|
|
367
|
+
|
|
368
|
+
const output = transform(input, undefined, true);
|
|
369
|
+
|
|
370
|
+
// Should have StyleSheet with both base and active styles
|
|
371
|
+
expect(output).toContain("_bg_blue_500_p_4");
|
|
372
|
+
expect(output).toContain("_active_bg_blue_700");
|
|
373
|
+
|
|
374
|
+
// Should have style function for active modifier (Pressable uses 'pressed' parameter)
|
|
375
|
+
expect(output).toMatch(/(pressed|_state)/);
|
|
376
|
+
|
|
377
|
+
// Should not have className in output
|
|
378
|
+
expect(output).not.toContain("className");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should transform className={"..."} with platform modifiers', () => {
|
|
382
|
+
const input = `
|
|
383
|
+
import { View } from 'react-native';
|
|
384
|
+
export function Component() {
|
|
385
|
+
return <View className={"p-4 ios:p-6 android:p-8"} />;
|
|
386
|
+
}
|
|
387
|
+
`;
|
|
388
|
+
|
|
389
|
+
const output = transform(input, undefined, true);
|
|
390
|
+
|
|
391
|
+
// Should have Platform import
|
|
392
|
+
expect(output).toContain("Platform");
|
|
393
|
+
expect(output).toMatch(/from ['"]react-native['"]/); // Match both single and double quotes
|
|
394
|
+
|
|
395
|
+
// Should have Platform.select
|
|
396
|
+
expect(output).toContain("Platform.select");
|
|
397
|
+
|
|
398
|
+
// Should have platform-specific styles
|
|
399
|
+
expect(output).toContain("_ios_p_6");
|
|
400
|
+
expect(output).toContain("_android_p_8");
|
|
401
|
+
|
|
402
|
+
// Should not have className in output
|
|
403
|
+
expect(output).not.toContain("className");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should handle empty className={""}', () => {
|
|
407
|
+
const input = `
|
|
408
|
+
import { View } from 'react-native';
|
|
409
|
+
export function Component() {
|
|
410
|
+
return <View className={""} />;
|
|
411
|
+
}
|
|
412
|
+
`;
|
|
413
|
+
|
|
414
|
+
const output = transform(input, undefined, true);
|
|
415
|
+
|
|
416
|
+
// Should remove empty className attribute entirely
|
|
417
|
+
expect(output).not.toContain("className");
|
|
418
|
+
expect(output).not.toContain("style=");
|
|
419
|
+
});
|
|
335
420
|
});
|
|
336
421
|
|
|
337
422
|
describe("Babel plugin - placeholder: modifier transformation", () => {
|
|
@@ -1022,6 +1107,306 @@ describe("Babel plugin - color scheme modifier transformation", () => {
|
|
|
1022
1107
|
});
|
|
1023
1108
|
});
|
|
1024
1109
|
|
|
1110
|
+
describe("Babel plugin - custom color scheme hook import", () => {
|
|
1111
|
+
it("should use custom import source for color scheme hook", () => {
|
|
1112
|
+
const input = `
|
|
1113
|
+
import React from 'react';
|
|
1114
|
+
import { View } from 'react-native';
|
|
1115
|
+
|
|
1116
|
+
export function Component() {
|
|
1117
|
+
return <View className="dark:bg-gray-900" />;
|
|
1118
|
+
}
|
|
1119
|
+
`;
|
|
1120
|
+
|
|
1121
|
+
const output = transform(
|
|
1122
|
+
input,
|
|
1123
|
+
{
|
|
1124
|
+
colorScheme: {
|
|
1125
|
+
importFrom: "@/hooks/useColorScheme",
|
|
1126
|
+
importName: "useColorScheme",
|
|
1127
|
+
},
|
|
1128
|
+
},
|
|
1129
|
+
true,
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
// Should import from custom source
|
|
1133
|
+
expect(output).toContain('from "@/hooks/useColorScheme"');
|
|
1134
|
+
expect(output).not.toContain('useColorScheme } from "react-native"');
|
|
1135
|
+
|
|
1136
|
+
// Should inject hook call
|
|
1137
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1138
|
+
|
|
1139
|
+
// Should have conditional styling
|
|
1140
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
it("should use custom hook name", () => {
|
|
1144
|
+
const input = `
|
|
1145
|
+
import React from 'react';
|
|
1146
|
+
import { View } from 'react-native';
|
|
1147
|
+
|
|
1148
|
+
export function Component() {
|
|
1149
|
+
return <View className="dark:bg-gray-900" />;
|
|
1150
|
+
}
|
|
1151
|
+
`;
|
|
1152
|
+
|
|
1153
|
+
const output = transform(
|
|
1154
|
+
input,
|
|
1155
|
+
{
|
|
1156
|
+
colorScheme: {
|
|
1157
|
+
importFrom: "@react-navigation/native",
|
|
1158
|
+
importName: "useTheme",
|
|
1159
|
+
},
|
|
1160
|
+
},
|
|
1161
|
+
true,
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
// Should import useTheme from React Navigation
|
|
1165
|
+
expect(output).toContain('from "@react-navigation/native"');
|
|
1166
|
+
expect(output).toContain("useTheme");
|
|
1167
|
+
|
|
1168
|
+
// Should call useTheme hook
|
|
1169
|
+
expect(output).toContain("_twColorScheme = useTheme()");
|
|
1170
|
+
|
|
1171
|
+
// Should have conditional styling
|
|
1172
|
+
expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
it("should merge custom hook with existing import from same source", () => {
|
|
1176
|
+
const input = `
|
|
1177
|
+
import React from 'react';
|
|
1178
|
+
import { View, Text } from 'react-native';
|
|
1179
|
+
import { useNavigation } from '@react-navigation/native';
|
|
1180
|
+
|
|
1181
|
+
export function Component() {
|
|
1182
|
+
const navigation = useNavigation();
|
|
1183
|
+
return (
|
|
1184
|
+
<View className="dark:bg-gray-900">
|
|
1185
|
+
<Text onPress={() => navigation.navigate('Home')}>Go Home</Text>
|
|
1186
|
+
</View>
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
`;
|
|
1190
|
+
|
|
1191
|
+
const output = transform(
|
|
1192
|
+
input,
|
|
1193
|
+
{
|
|
1194
|
+
colorScheme: {
|
|
1195
|
+
importFrom: "@react-navigation/native",
|
|
1196
|
+
importName: "useTheme",
|
|
1197
|
+
},
|
|
1198
|
+
},
|
|
1199
|
+
true,
|
|
1200
|
+
);
|
|
1201
|
+
|
|
1202
|
+
// Should merge with existing import (both useNavigation and useTheme in same import)
|
|
1203
|
+
expect(output).toMatch(
|
|
1204
|
+
/import\s+\{\s*useNavigation[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/,
|
|
1205
|
+
);
|
|
1206
|
+
expect(output).toContain("useNavigation()");
|
|
1207
|
+
expect(output).toContain("useTheme()");
|
|
1208
|
+
|
|
1209
|
+
// Should only have one import from that source
|
|
1210
|
+
const importCount = (output.match(/@react-navigation\/native/g) ?? []).length;
|
|
1211
|
+
expect(importCount).toBe(1);
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
it("should not duplicate custom hook if already imported", () => {
|
|
1215
|
+
const input = `
|
|
1216
|
+
import React from 'react';
|
|
1217
|
+
import { View } from 'react-native';
|
|
1218
|
+
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
1219
|
+
|
|
1220
|
+
export function Component() {
|
|
1221
|
+
return <View className="dark:bg-gray-900" />;
|
|
1222
|
+
}
|
|
1223
|
+
`;
|
|
1224
|
+
|
|
1225
|
+
const output = transform(
|
|
1226
|
+
input,
|
|
1227
|
+
{
|
|
1228
|
+
colorScheme: {
|
|
1229
|
+
importFrom: "@/hooks/useColorScheme",
|
|
1230
|
+
importName: "useColorScheme",
|
|
1231
|
+
},
|
|
1232
|
+
},
|
|
1233
|
+
true,
|
|
1234
|
+
);
|
|
1235
|
+
|
|
1236
|
+
// Should not add duplicate import
|
|
1237
|
+
const importMatches = output.match(/import.*useColorScheme.*from ['"]@\/hooks\/useColorScheme['"]/g);
|
|
1238
|
+
expect(importMatches).toHaveLength(1);
|
|
1239
|
+
|
|
1240
|
+
// Should still inject hook call
|
|
1241
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
it("should use react-native by default when no custom config provided", () => {
|
|
1245
|
+
const input = `
|
|
1246
|
+
import React from 'react';
|
|
1247
|
+
import { View } from 'react-native';
|
|
1248
|
+
|
|
1249
|
+
export function Component() {
|
|
1250
|
+
return <View className="dark:bg-gray-900" />;
|
|
1251
|
+
}
|
|
1252
|
+
`;
|
|
1253
|
+
|
|
1254
|
+
const output = transform(input, undefined, true);
|
|
1255
|
+
|
|
1256
|
+
// Should use default react-native import (can be single or double quotes)
|
|
1257
|
+
expect(output).toMatch(/useColorScheme\s*}\s*from\s+['"]react-native['"]/);
|
|
1258
|
+
expect(output).not.toContain("@/hooks");
|
|
1259
|
+
expect(output).not.toContain("@react-navigation");
|
|
1260
|
+
|
|
1261
|
+
// Should inject hook call with default name
|
|
1262
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
it("should create separate import when only type-only import exists", () => {
|
|
1266
|
+
const input = `
|
|
1267
|
+
import React from 'react';
|
|
1268
|
+
import { View } from 'react-native';
|
|
1269
|
+
import type { NavigationProp } from '@react-navigation/native';
|
|
1270
|
+
|
|
1271
|
+
export function Component() {
|
|
1272
|
+
return <View className="dark:bg-gray-900" />;
|
|
1273
|
+
}
|
|
1274
|
+
`;
|
|
1275
|
+
|
|
1276
|
+
const output = transform(
|
|
1277
|
+
input,
|
|
1278
|
+
{
|
|
1279
|
+
colorScheme: {
|
|
1280
|
+
importFrom: "@react-navigation/native",
|
|
1281
|
+
importName: "useTheme",
|
|
1282
|
+
},
|
|
1283
|
+
},
|
|
1284
|
+
true,
|
|
1285
|
+
);
|
|
1286
|
+
|
|
1287
|
+
// TypeScript preset strips type-only imports, but the important thing is:
|
|
1288
|
+
// 1. useTheme hook is imported (not skipped thinking it was already imported)
|
|
1289
|
+
// 2. Hook is correctly called in the component
|
|
1290
|
+
expect(output).toMatch(/import\s+\{\s*useTheme\s*\}\s+from\s+['"]@react-navigation\/native['"]/);
|
|
1291
|
+
expect(output).toContain("_twColorScheme = useTheme()");
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
it("should use aliased identifier when hook is already imported with alias", () => {
|
|
1295
|
+
const input = `
|
|
1296
|
+
import React from 'react';
|
|
1297
|
+
import { View, Text } from 'react-native';
|
|
1298
|
+
import { useTheme as navTheme } from '@react-navigation/native';
|
|
1299
|
+
|
|
1300
|
+
export function Component() {
|
|
1301
|
+
const theme = navTheme();
|
|
1302
|
+
return (
|
|
1303
|
+
<View className="dark:bg-gray-900">
|
|
1304
|
+
<Text>{theme.dark ? 'Dark' : 'Light'}</Text>
|
|
1305
|
+
</View>
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
`;
|
|
1309
|
+
|
|
1310
|
+
const output = transform(
|
|
1311
|
+
input,
|
|
1312
|
+
{
|
|
1313
|
+
colorScheme: {
|
|
1314
|
+
importFrom: "@react-navigation/native",
|
|
1315
|
+
importName: "useTheme",
|
|
1316
|
+
},
|
|
1317
|
+
},
|
|
1318
|
+
true,
|
|
1319
|
+
);
|
|
1320
|
+
|
|
1321
|
+
// Should not add duplicate import
|
|
1322
|
+
const importMatches = output.match(
|
|
1323
|
+
/import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g,
|
|
1324
|
+
);
|
|
1325
|
+
expect(importMatches).toHaveLength(1);
|
|
1326
|
+
|
|
1327
|
+
// Should still have the aliased import
|
|
1328
|
+
expect(output).toMatch(/useTheme\s+as\s+navTheme/);
|
|
1329
|
+
|
|
1330
|
+
// Should call the aliased name (navTheme), not the export name (useTheme)
|
|
1331
|
+
// Both the user's code and our injected hook should use navTheme
|
|
1332
|
+
expect(output).toContain("_twColorScheme = navTheme()");
|
|
1333
|
+
expect(output).not.toContain("_twColorScheme = useTheme()");
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it("should not treat type-only imports as having the hook", () => {
|
|
1337
|
+
const input = `
|
|
1338
|
+
import React from 'react';
|
|
1339
|
+
import { View } from 'react-native';
|
|
1340
|
+
import type { useColorScheme } from 'react-native';
|
|
1341
|
+
|
|
1342
|
+
export function Component() {
|
|
1343
|
+
return <View className="dark:bg-gray-900" />;
|
|
1344
|
+
}
|
|
1345
|
+
`;
|
|
1346
|
+
|
|
1347
|
+
const output = transform(input, undefined, true);
|
|
1348
|
+
|
|
1349
|
+
// Should add a VALUE import for useColorScheme (type import doesn't count)
|
|
1350
|
+
expect(output).toMatch(/import\s+\{[^}]*useColorScheme[^}]*\}\s+from\s+['"]react-native['"]/);
|
|
1351
|
+
|
|
1352
|
+
// Should inject the hook
|
|
1353
|
+
expect(output).toContain("_twColorScheme = useColorScheme()");
|
|
1354
|
+
|
|
1355
|
+
// Should have both type-only and value imports in output
|
|
1356
|
+
// (TypeScript preset keeps type imports for type checking)
|
|
1357
|
+
const colorSchemeMatches = output.match(/useColorScheme/g);
|
|
1358
|
+
expect(colorSchemeMatches).toBeTruthy();
|
|
1359
|
+
if (colorSchemeMatches) {
|
|
1360
|
+
expect(colorSchemeMatches.length).toBeGreaterThanOrEqual(2); // At least in import and hook call
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
it("should handle both type-only and aliased imports together", () => {
|
|
1365
|
+
const input = `
|
|
1366
|
+
import React from 'react';
|
|
1367
|
+
import { View, Text } from 'react-native';
|
|
1368
|
+
import type { Theme } from '@react-navigation/native';
|
|
1369
|
+
import { useTheme as getNavTheme } from '@react-navigation/native';
|
|
1370
|
+
|
|
1371
|
+
export function Component() {
|
|
1372
|
+
const theme = getNavTheme();
|
|
1373
|
+
return (
|
|
1374
|
+
<View className="dark:bg-gray-900">
|
|
1375
|
+
<Text>{theme.dark ? 'Dark Mode' : 'Light Mode'}</Text>
|
|
1376
|
+
</View>
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
`;
|
|
1380
|
+
|
|
1381
|
+
const output = transform(
|
|
1382
|
+
input,
|
|
1383
|
+
{
|
|
1384
|
+
colorScheme: {
|
|
1385
|
+
importFrom: "@react-navigation/native",
|
|
1386
|
+
importName: "useTheme",
|
|
1387
|
+
},
|
|
1388
|
+
},
|
|
1389
|
+
true,
|
|
1390
|
+
);
|
|
1391
|
+
|
|
1392
|
+
// TypeScript preset strips type-only imports
|
|
1393
|
+
// The important thing is: should not add duplicate import, and should use aliased name
|
|
1394
|
+
expect(output).toMatch(
|
|
1395
|
+
/import\s+\{[^}]*useTheme\s+as\s+getNavTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/,
|
|
1396
|
+
);
|
|
1397
|
+
|
|
1398
|
+
// Should not add duplicate import - useTheme should only appear in the aliased import
|
|
1399
|
+
const useThemeImports = output.match(
|
|
1400
|
+
/import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g,
|
|
1401
|
+
);
|
|
1402
|
+
expect(useThemeImports).toHaveLength(1);
|
|
1403
|
+
|
|
1404
|
+
// Should call the aliased name for both user code and our injected hook
|
|
1405
|
+
expect(output).toContain("_twColorScheme = getNavTheme()");
|
|
1406
|
+
expect(output).not.toContain("_twColorScheme = useTheme()");
|
|
1407
|
+
});
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1025
1410
|
describe("Babel plugin - import injection", () => {
|
|
1026
1411
|
it("should not add StyleSheet import to files without className usage", () => {
|
|
1027
1412
|
const input = `
|
|
@@ -1138,3 +1523,390 @@ describe("Babel plugin - scheme: modifier", () => {
|
|
|
1138
1523
|
expect(output).toContain("_twColorScheme === 'light'");
|
|
1139
1524
|
});
|
|
1140
1525
|
});
|
|
1526
|
+
|
|
1527
|
+
describe("Babel plugin - color scheme modifiers in tw/twStyle", () => {
|
|
1528
|
+
it("should transform tw with dark: modifier inside component", () => {
|
|
1529
|
+
const input = `
|
|
1530
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1531
|
+
|
|
1532
|
+
function MyComponent() {
|
|
1533
|
+
const styles = tw\`bg-white dark:bg-gray-900\`;
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
`;
|
|
1537
|
+
|
|
1538
|
+
const output = transform(input);
|
|
1539
|
+
|
|
1540
|
+
// Should inject useColorScheme hook
|
|
1541
|
+
expect(output).toContain("useColorScheme");
|
|
1542
|
+
expect(output).toContain("_twColorScheme");
|
|
1543
|
+
|
|
1544
|
+
// Should generate style array with conditionals
|
|
1545
|
+
expect(output).toContain("style: [");
|
|
1546
|
+
expect(output).toContain('_twColorScheme === "dark"');
|
|
1547
|
+
expect(output).toContain("_twStyles._dark_bg_gray_900");
|
|
1548
|
+
expect(output).toContain("_twStyles._bg_white");
|
|
1549
|
+
|
|
1550
|
+
// Should have StyleSheet.create
|
|
1551
|
+
expect(output).toContain("StyleSheet.create");
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
it("should transform twStyle with light: modifier inside component", () => {
|
|
1555
|
+
const input = `
|
|
1556
|
+
import { twStyle } from '@mgcrea/react-native-tailwind';
|
|
1557
|
+
|
|
1558
|
+
export const MyComponent = () => {
|
|
1559
|
+
const buttonStyles = twStyle('text-gray-900 light:text-gray-100');
|
|
1560
|
+
return null;
|
|
1561
|
+
};
|
|
1562
|
+
`;
|
|
1563
|
+
|
|
1564
|
+
const output = transform(input);
|
|
1565
|
+
|
|
1566
|
+
// Should inject useColorScheme hook
|
|
1567
|
+
expect(output).toContain("useColorScheme");
|
|
1568
|
+
expect(output).toContain("_twColorScheme");
|
|
1569
|
+
|
|
1570
|
+
// Should generate style array with conditionals
|
|
1571
|
+
expect(output).toContain("style: [");
|
|
1572
|
+
expect(output).toContain('_twColorScheme === "light"');
|
|
1573
|
+
expect(output).toContain("_twStyles._light_text_gray_100");
|
|
1574
|
+
expect(output).toContain("_twStyles._text_gray_900");
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
it("should transform tw with both dark: and light: modifiers", () => {
|
|
1578
|
+
const input = `
|
|
1579
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1580
|
+
|
|
1581
|
+
function MyComponent() {
|
|
1582
|
+
const styles = tw\`bg-blue-500 dark:bg-blue-900 light:bg-blue-100\`;
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
`;
|
|
1586
|
+
|
|
1587
|
+
const output = transform(input);
|
|
1588
|
+
|
|
1589
|
+
// Should have both conditionals
|
|
1590
|
+
expect(output).toContain('_twColorScheme === "dark"');
|
|
1591
|
+
expect(output).toContain('_twColorScheme === "light"');
|
|
1592
|
+
expect(output).toContain("_twStyles._dark_bg_blue_900");
|
|
1593
|
+
expect(output).toContain("_twStyles._light_bg_blue_100");
|
|
1594
|
+
expect(output).toContain("_twStyles._bg_blue_500");
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
it("should combine color scheme modifiers with state modifiers", () => {
|
|
1598
|
+
const input = `
|
|
1599
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1600
|
+
|
|
1601
|
+
function MyComponent() {
|
|
1602
|
+
const styles = tw\`bg-white dark:bg-gray-900 active:bg-blue-500\`;
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
`;
|
|
1606
|
+
|
|
1607
|
+
const output = transform(input);
|
|
1608
|
+
|
|
1609
|
+
// Should have color scheme conditionals in style array
|
|
1610
|
+
expect(output).toContain("style: [");
|
|
1611
|
+
expect(output).toContain('_twColorScheme === "dark"');
|
|
1612
|
+
|
|
1613
|
+
// Should have activeStyle property (separate from color scheme)
|
|
1614
|
+
expect(output).toContain("activeStyle:");
|
|
1615
|
+
expect(output).toContain("_twStyles._active_bg_blue_500");
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
it("should warn if tw with color scheme modifiers used outside component", () => {
|
|
1619
|
+
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1620
|
+
|
|
1621
|
+
const input = `
|
|
1622
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1623
|
+
|
|
1624
|
+
const globalStyles = tw\`bg-white dark:bg-gray-900\`;
|
|
1625
|
+
`;
|
|
1626
|
+
|
|
1627
|
+
const output = transform(input);
|
|
1628
|
+
|
|
1629
|
+
// Should warn about usage outside component
|
|
1630
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
1631
|
+
expect.stringContaining("Color scheme modifiers (dark:, light:) in tw/twStyle calls"),
|
|
1632
|
+
);
|
|
1633
|
+
|
|
1634
|
+
// Should not inject hook (no component scope)
|
|
1635
|
+
expect(output).not.toContain("useColorScheme");
|
|
1636
|
+
|
|
1637
|
+
// Should still generate styles but without runtime conditionals
|
|
1638
|
+
expect(output).toContain("_twStyles");
|
|
1639
|
+
|
|
1640
|
+
consoleWarnSpy.mockRestore();
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
it("should handle tw with only dark: modifier (no base class)", () => {
|
|
1644
|
+
const input = `
|
|
1645
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1646
|
+
|
|
1647
|
+
function MyComponent() {
|
|
1648
|
+
const styles = tw\`dark:bg-gray-900\`;
|
|
1649
|
+
return null;
|
|
1650
|
+
}
|
|
1651
|
+
`;
|
|
1652
|
+
|
|
1653
|
+
const output = transform(input);
|
|
1654
|
+
|
|
1655
|
+
// Should still generate style array
|
|
1656
|
+
expect(output).toContain("style: [");
|
|
1657
|
+
expect(output).toContain('_twColorScheme === "dark"');
|
|
1658
|
+
expect(output).toContain("_twStyles._dark_bg_gray_900");
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
it("should work with custom color scheme hook import", () => {
|
|
1662
|
+
const input = `
|
|
1663
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1664
|
+
import { useTheme } from '@react-navigation/native';
|
|
1665
|
+
|
|
1666
|
+
function MyComponent() {
|
|
1667
|
+
const styles = tw\`bg-white dark:bg-gray-900\`;
|
|
1668
|
+
return null;
|
|
1669
|
+
}
|
|
1670
|
+
`;
|
|
1671
|
+
|
|
1672
|
+
const options: PluginOptions = {
|
|
1673
|
+
colorScheme: {
|
|
1674
|
+
importFrom: "@react-navigation/native",
|
|
1675
|
+
importName: "useTheme",
|
|
1676
|
+
},
|
|
1677
|
+
};
|
|
1678
|
+
|
|
1679
|
+
const output = transform(input, options);
|
|
1680
|
+
|
|
1681
|
+
// Should use existing import (not duplicate)
|
|
1682
|
+
const themeImportCount = (output.match(/useTheme/g) ?? []).length;
|
|
1683
|
+
// Should appear in import statement and hook call
|
|
1684
|
+
expect(themeImportCount).toBeGreaterThanOrEqual(2);
|
|
1685
|
+
|
|
1686
|
+
// Should call the custom hook
|
|
1687
|
+
expect(output).toContain("useTheme()");
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
it("should generate both style array and darkStyle/lightStyle properties", () => {
|
|
1691
|
+
const input = `
|
|
1692
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1693
|
+
|
|
1694
|
+
function MyComponent() {
|
|
1695
|
+
const styles = tw\`bg-white dark:bg-gray-900 light:bg-gray-50\`;
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
`;
|
|
1699
|
+
|
|
1700
|
+
const output = transform(input);
|
|
1701
|
+
|
|
1702
|
+
// Should have runtime conditional in style array
|
|
1703
|
+
expect(output).toContain("style: [");
|
|
1704
|
+
expect(output).toContain('_twColorScheme === "dark"');
|
|
1705
|
+
expect(output).toContain('_twColorScheme === "light"');
|
|
1706
|
+
|
|
1707
|
+
// Should ALSO have darkStyle and lightStyle properties for manual access
|
|
1708
|
+
expect(output).toContain("darkStyle:");
|
|
1709
|
+
expect(output).toContain("lightStyle:");
|
|
1710
|
+
expect(output).toContain("_twStyles._dark_bg_gray_900");
|
|
1711
|
+
expect(output).toContain("_twStyles._light_bg_gray_50");
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
it("should allow accessing raw color values from darkStyle/lightStyle", () => {
|
|
1715
|
+
const input = `
|
|
1716
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1717
|
+
|
|
1718
|
+
function MyComponent() {
|
|
1719
|
+
const btnStyles = tw\`bg-blue-500 dark:bg-blue-900\`;
|
|
1720
|
+
// User can access raw hex for Reanimated
|
|
1721
|
+
const darkBgColor = btnStyles.darkStyle?.backgroundColor;
|
|
1722
|
+
return null;
|
|
1723
|
+
}
|
|
1724
|
+
`;
|
|
1725
|
+
|
|
1726
|
+
const output = transform(input);
|
|
1727
|
+
|
|
1728
|
+
// Should have darkStyle property available
|
|
1729
|
+
expect(output).toContain("darkStyle:");
|
|
1730
|
+
expect(output).toContain("_twStyles._dark_bg_blue_900");
|
|
1731
|
+
|
|
1732
|
+
// The actual usage line should be preserved (TypeScript/Babel doesn't remove it)
|
|
1733
|
+
expect(output).toContain("btnStyles.darkStyle");
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
// Platform modifier tests for tw/twStyle
|
|
1737
|
+
it("should transform tw with ios: modifier", () => {
|
|
1738
|
+
const input = `
|
|
1739
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1740
|
+
|
|
1741
|
+
function MyComponent() {
|
|
1742
|
+
const styles = tw\`bg-white ios:p-6\`;
|
|
1743
|
+
return null;
|
|
1744
|
+
}
|
|
1745
|
+
`;
|
|
1746
|
+
|
|
1747
|
+
const output = transform(input);
|
|
1748
|
+
|
|
1749
|
+
// Should add Platform import
|
|
1750
|
+
expect(output).toContain("Platform");
|
|
1751
|
+
expect(output).toContain('from "react-native"');
|
|
1752
|
+
|
|
1753
|
+
// Should generate style array with Platform.select()
|
|
1754
|
+
expect(output).toContain("style: [");
|
|
1755
|
+
expect(output).toContain("Platform.select");
|
|
1756
|
+
expect(output).toContain("ios:");
|
|
1757
|
+
expect(output).toContain("_twStyles._ios_p_6");
|
|
1758
|
+
expect(output).toContain("_twStyles._bg_white");
|
|
1759
|
+
|
|
1760
|
+
// Should have StyleSheet.create
|
|
1761
|
+
expect(output).toContain("StyleSheet.create");
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
it("should transform twStyle with android: modifier", () => {
|
|
1765
|
+
const input = `
|
|
1766
|
+
import { twStyle } from '@mgcrea/react-native-tailwind';
|
|
1767
|
+
|
|
1768
|
+
export const MyComponent = () => {
|
|
1769
|
+
const buttonStyles = twStyle('bg-blue-500 android:p-8');
|
|
1770
|
+
return null;
|
|
1771
|
+
};
|
|
1772
|
+
`;
|
|
1773
|
+
|
|
1774
|
+
const output = transform(input);
|
|
1775
|
+
|
|
1776
|
+
// Should add Platform import
|
|
1777
|
+
expect(output).toContain("Platform");
|
|
1778
|
+
|
|
1779
|
+
// Should generate style array with Platform.select()
|
|
1780
|
+
expect(output).toContain("style: [");
|
|
1781
|
+
expect(output).toContain("Platform.select");
|
|
1782
|
+
expect(output).toContain("android:");
|
|
1783
|
+
expect(output).toContain("_twStyles._android_p_8");
|
|
1784
|
+
expect(output).toContain("_twStyles._bg_blue_500");
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
it("should transform tw with multiple platform modifiers", () => {
|
|
1788
|
+
const input = `
|
|
1789
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1790
|
+
|
|
1791
|
+
function MyComponent() {
|
|
1792
|
+
const styles = tw\`bg-white ios:p-6 android:p-8 web:p-4\`;
|
|
1793
|
+
return null;
|
|
1794
|
+
}
|
|
1795
|
+
`;
|
|
1796
|
+
|
|
1797
|
+
const output = transform(input);
|
|
1798
|
+
|
|
1799
|
+
// Should generate Platform.select() with all platforms
|
|
1800
|
+
expect(output).toContain("Platform.select");
|
|
1801
|
+
expect(output).toContain("ios:");
|
|
1802
|
+
expect(output).toContain("android:");
|
|
1803
|
+
expect(output).toContain("web:");
|
|
1804
|
+
expect(output).toContain("_twStyles._ios_p_6");
|
|
1805
|
+
expect(output).toContain("_twStyles._android_p_8");
|
|
1806
|
+
expect(output).toContain("_twStyles._web_p_4");
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
it("should combine platform modifiers with color-scheme modifiers", () => {
|
|
1810
|
+
const input = `
|
|
1811
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1812
|
+
|
|
1813
|
+
function MyComponent() {
|
|
1814
|
+
const styles = tw\`bg-white ios:p-6 dark:bg-gray-900\`;
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
`;
|
|
1818
|
+
|
|
1819
|
+
const output = transform(input);
|
|
1820
|
+
|
|
1821
|
+
// Should have both Platform and useColorScheme
|
|
1822
|
+
expect(output).toContain("Platform");
|
|
1823
|
+
expect(output).toContain("useColorScheme");
|
|
1824
|
+
expect(output).toContain("_twColorScheme");
|
|
1825
|
+
|
|
1826
|
+
// Should have both conditionals in style array
|
|
1827
|
+
expect(output).toContain("Platform.select");
|
|
1828
|
+
expect(output).toContain('_twColorScheme === "dark"');
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
it("should generate iosStyle/androidStyle/webStyle properties for manual access", () => {
|
|
1832
|
+
const input = `
|
|
1833
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1834
|
+
|
|
1835
|
+
function MyComponent() {
|
|
1836
|
+
const styles = tw\`bg-white ios:p-6 android:p-8 web:p-4\`;
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
`;
|
|
1840
|
+
|
|
1841
|
+
const output = transform(input);
|
|
1842
|
+
|
|
1843
|
+
// Should have separate platform style properties
|
|
1844
|
+
expect(output).toContain("iosStyle:");
|
|
1845
|
+
expect(output).toContain("_twStyles._ios_p_6");
|
|
1846
|
+
expect(output).toContain("androidStyle:");
|
|
1847
|
+
expect(output).toContain("_twStyles._android_p_8");
|
|
1848
|
+
expect(output).toContain("webStyle:");
|
|
1849
|
+
expect(output).toContain("_twStyles._web_p_4");
|
|
1850
|
+
|
|
1851
|
+
// Should also have runtime Platform.select() in style array
|
|
1852
|
+
expect(output).toContain("Platform.select");
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
it("should work with only platform modifiers (no base class)", () => {
|
|
1856
|
+
const input = `
|
|
1857
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1858
|
+
|
|
1859
|
+
function MyComponent() {
|
|
1860
|
+
const styles = tw\`ios:p-6 android:p-8\`;
|
|
1861
|
+
return null;
|
|
1862
|
+
}
|
|
1863
|
+
`;
|
|
1864
|
+
|
|
1865
|
+
const output = transform(input);
|
|
1866
|
+
|
|
1867
|
+
// Should generate Platform.select() even without base classes
|
|
1868
|
+
expect(output).toContain("Platform.select");
|
|
1869
|
+
expect(output).toContain("_twStyles._ios_p_6");
|
|
1870
|
+
expect(output).toContain("_twStyles._android_p_8");
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
it("should allow accessing platform-specific styles manually", () => {
|
|
1874
|
+
const input = `
|
|
1875
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1876
|
+
|
|
1877
|
+
function MyComponent() {
|
|
1878
|
+
const btnStyles = tw\`bg-blue-500 ios:p-6\`;
|
|
1879
|
+
const iosPadding = btnStyles.iosStyle;
|
|
1880
|
+
return null;
|
|
1881
|
+
}
|
|
1882
|
+
`;
|
|
1883
|
+
|
|
1884
|
+
const output = transform(input);
|
|
1885
|
+
|
|
1886
|
+
// Should have iosStyle property available
|
|
1887
|
+
expect(output).toContain("iosStyle:");
|
|
1888
|
+
expect(output).toContain("_twStyles._ios_p_6");
|
|
1889
|
+
|
|
1890
|
+
// The actual usage line should be preserved
|
|
1891
|
+
expect(output).toContain("btnStyles.iosStyle");
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
it("should combine state modifiers with platform modifiers", () => {
|
|
1895
|
+
const input = `
|
|
1896
|
+
import { tw } from '@mgcrea/react-native-tailwind';
|
|
1897
|
+
|
|
1898
|
+
function MyComponent() {
|
|
1899
|
+
const styles = tw\`bg-white active:bg-blue-500 ios:p-6\`;
|
|
1900
|
+
return null;
|
|
1901
|
+
}
|
|
1902
|
+
`;
|
|
1903
|
+
|
|
1904
|
+
const output = transform(input);
|
|
1905
|
+
|
|
1906
|
+
// Should have both activeStyle and platform modifiers
|
|
1907
|
+
expect(output).toContain("activeStyle:");
|
|
1908
|
+
expect(output).toContain("_twStyles._active_bg_blue_500");
|
|
1909
|
+
expect(output).toContain("Platform.select");
|
|
1910
|
+
expect(output).toContain("_twStyles._ios_p_6");
|
|
1911
|
+
});
|
|
1912
|
+
});
|