@mgcrea/react-native-tailwind 0.6.0 → 0.7.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 +437 -10
- package/dist/babel/config-loader.ts +1 -23
- package/dist/babel/index.cjs +543 -150
- package/dist/babel/index.d.ts +27 -2
- package/dist/babel/index.test.ts +268 -0
- package/dist/babel/index.ts +352 -44
- package/dist/components/Pressable.d.ts +2 -0
- package/dist/components/TextInput.d.ts +2 -0
- package/dist/config/palettes.d.ts +302 -0
- package/dist/config/palettes.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -1
- package/dist/parser/__snapshots__/colors.test.js.snap +242 -90
- package/dist/parser/__snapshots__/transforms.test.js.snap +58 -0
- package/dist/parser/colors.js +1 -1
- package/dist/parser/colors.test.js +1 -1
- package/dist/parser/layout.js +1 -1
- package/dist/parser/layout.test.js +1 -1
- package/dist/parser/typography.js +1 -1
- package/dist/parser/typography.test.js +1 -1
- package/dist/runtime.cjs +2 -0
- package/dist/runtime.cjs.map +7 -0
- package/dist/runtime.d.ts +139 -0
- package/dist/runtime.js +2 -0
- package/dist/runtime.js.map +7 -0
- package/dist/runtime.test.js +1 -0
- package/dist/stubs/tw.d.ts +60 -0
- package/dist/stubs/tw.js +1 -0
- package/dist/utils/flattenColors.d.ts +16 -0
- package/dist/utils/flattenColors.js +1 -0
- package/dist/utils/flattenColors.test.js +1 -0
- package/dist/utils/modifiers.d.ts +29 -0
- package/dist/utils/modifiers.js +1 -0
- package/dist/utils/modifiers.test.js +1 -0
- package/dist/utils/styleKey.test.js +1 -0
- package/package.json +15 -3
- package/src/babel/config-loader.ts +1 -23
- package/src/babel/index.test.ts +268 -0
- package/src/babel/index.ts +352 -44
- package/src/components/Pressable.tsx +1 -0
- package/src/components/TextInput.tsx +1 -0
- package/src/config/palettes.ts +304 -0
- package/src/index.ts +5 -0
- package/src/parser/colors.test.ts +47 -31
- package/src/parser/colors.ts +5 -110
- package/src/parser/layout.test.ts +35 -0
- package/src/parser/layout.ts +26 -0
- package/src/parser/typography.test.ts +10 -0
- package/src/parser/typography.ts +8 -0
- package/src/runtime.test.ts +325 -0
- package/src/runtime.ts +280 -0
- package/src/stubs/tw.ts +80 -0
- package/src/utils/flattenColors.test.ts +361 -0
- package/src/utils/flattenColors.ts +32 -0
- package/src/utils/modifiers.test.ts +286 -0
- package/src/utils/modifiers.ts +63 -0
- package/src/utils/styleKey.test.ts +168 -0
package/dist/babel/index.ts
CHANGED
|
@@ -17,22 +17,49 @@ import { parseClassName as parseClassNameFn, splitModifierClasses } from "../par
|
|
|
17
17
|
import { generateStyleKey as generateStyleKeyFn } from "../utils/styleKey.js";
|
|
18
18
|
import { extractCustomColors } from "./config-loader.js";
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Plugin options
|
|
22
|
+
*/
|
|
23
|
+
export type PluginOptions = {
|
|
24
|
+
/**
|
|
25
|
+
* List of JSX attribute names to transform (in addition to or instead of 'className')
|
|
26
|
+
* Supports exact matches and glob patterns:
|
|
27
|
+
* - Exact: 'className', 'containerClassName'
|
|
28
|
+
* - Glob: '*ClassName' (matches any attribute ending in 'ClassName')
|
|
29
|
+
*
|
|
30
|
+
* @default ['className', 'contentContainerClassName', 'columnWrapperClassName', 'ListHeaderComponentClassName', 'ListFooterComponentClassName']
|
|
31
|
+
*/
|
|
32
|
+
attributes?: string[];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Custom identifier name for the generated StyleSheet constant
|
|
36
|
+
*
|
|
37
|
+
* @default '_twStyles'
|
|
38
|
+
*/
|
|
39
|
+
stylesIdentifier?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
20
42
|
type PluginState = PluginPass & {
|
|
21
43
|
styleRegistry: Map<string, StyleObject>;
|
|
22
44
|
hasClassNames: boolean;
|
|
23
45
|
hasStyleSheetImport: boolean;
|
|
24
46
|
customColors: Record<string, string>;
|
|
47
|
+
supportedAttributes: Set<string>;
|
|
48
|
+
attributePatterns: RegExp[];
|
|
49
|
+
stylesIdentifier: string;
|
|
50
|
+
// Track tw/twStyle imports from main package
|
|
51
|
+
twImportNames: Set<string>; // e.g., ['tw', 'twStyle'] or ['tw as customTw']
|
|
52
|
+
hasTwImport: boolean;
|
|
25
53
|
};
|
|
26
54
|
|
|
27
|
-
//
|
|
28
|
-
const
|
|
55
|
+
// Default identifier for the generated StyleSheet constant
|
|
56
|
+
const DEFAULT_STYLES_IDENTIFIER = "_twStyles";
|
|
29
57
|
|
|
30
58
|
/**
|
|
31
|
-
*
|
|
59
|
+
* Default className-like attributes (used when no custom attributes are provided)
|
|
32
60
|
*/
|
|
33
|
-
const
|
|
61
|
+
const DEFAULT_CLASS_ATTRIBUTES = [
|
|
34
62
|
"className",
|
|
35
|
-
"containerClassName",
|
|
36
63
|
"contentContainerClassName",
|
|
37
64
|
"columnWrapperClassName",
|
|
38
65
|
"ListHeaderComponentClassName",
|
|
@@ -40,25 +67,56 @@ const SUPPORTED_CLASS_ATTRIBUTES = [
|
|
|
40
67
|
] as const;
|
|
41
68
|
|
|
42
69
|
/**
|
|
43
|
-
*
|
|
70
|
+
* Build attribute matching structures from plugin options
|
|
71
|
+
* Separates exact matches from pattern-based matches
|
|
44
72
|
*/
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
function buildAttributeMatchers(attributes: string[]): {
|
|
74
|
+
exactMatches: Set<string>;
|
|
75
|
+
patterns: RegExp[];
|
|
76
|
+
} {
|
|
77
|
+
const exactMatches = new Set<string>();
|
|
78
|
+
const patterns: RegExp[] = [];
|
|
79
|
+
|
|
80
|
+
for (const attr of attributes) {
|
|
81
|
+
if (attr.includes("*")) {
|
|
82
|
+
// Convert glob pattern to regex
|
|
83
|
+
// *ClassName -> /^.*ClassName$/
|
|
84
|
+
// container* -> /^container.*$/
|
|
85
|
+
const regexPattern = "^" + attr.replace(/\*/g, ".*") + "$";
|
|
86
|
+
patterns.push(new RegExp(regexPattern));
|
|
87
|
+
} else {
|
|
88
|
+
// Exact match
|
|
89
|
+
exactMatches.add(attr);
|
|
90
|
+
}
|
|
54
91
|
}
|
|
55
|
-
|
|
56
|
-
|
|
92
|
+
|
|
93
|
+
return { exactMatches, patterns };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if an attribute name matches the configured attributes
|
|
98
|
+
*/
|
|
99
|
+
function isAttributeSupported(attributeName: string, exactMatches: Set<string>, patterns: RegExp[]): boolean {
|
|
100
|
+
// Check exact matches first (faster)
|
|
101
|
+
if (exactMatches.has(attributeName)) {
|
|
102
|
+
return true;
|
|
57
103
|
}
|
|
58
|
-
|
|
59
|
-
|
|
104
|
+
|
|
105
|
+
// Check pattern matches
|
|
106
|
+
for (const pattern of patterns) {
|
|
107
|
+
if (pattern.test(attributeName)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
60
110
|
}
|
|
61
|
-
|
|
111
|
+
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the target style prop name based on the className attribute
|
|
117
|
+
*/
|
|
118
|
+
function getTargetStyleProp(attributeName: string): string {
|
|
119
|
+
return attributeName.endsWith("ClassName") ? attributeName.replace("ClassName", "Style") : "style";
|
|
62
120
|
}
|
|
63
121
|
|
|
64
122
|
/**
|
|
@@ -169,7 +227,7 @@ function processTemplateLiteral(
|
|
|
169
227
|
staticParts.push(cls);
|
|
170
228
|
|
|
171
229
|
// Add to parts array
|
|
172
|
-
parts.push(t.memberExpression(t.identifier(
|
|
230
|
+
parts.push(t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey)));
|
|
173
231
|
}
|
|
174
232
|
}
|
|
175
233
|
|
|
@@ -268,7 +326,7 @@ function processStringOrExpression(node: any, state: PluginState, t: typeof Babe
|
|
|
268
326
|
const styleKey = generateStyleKey(className);
|
|
269
327
|
state.styleRegistry.set(styleKey, styleObject);
|
|
270
328
|
|
|
271
|
-
return t.memberExpression(t.identifier(
|
|
329
|
+
return t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey));
|
|
272
330
|
}
|
|
273
331
|
|
|
274
332
|
// Handle nested expressions recursively
|
|
@@ -309,7 +367,7 @@ function processStaticClassNameWithModifiers(
|
|
|
309
367
|
const baseStyleObject = parseClassName(baseClassName, state.customColors);
|
|
310
368
|
const baseStyleKey = generateStyleKey(baseClassName);
|
|
311
369
|
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
312
|
-
baseStyleExpression = t.memberExpression(t.identifier(
|
|
370
|
+
baseStyleExpression = t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey));
|
|
313
371
|
}
|
|
314
372
|
|
|
315
373
|
// Parse and register modifier classes
|
|
@@ -346,7 +404,7 @@ function processStaticClassNameWithModifiers(
|
|
|
346
404
|
const conditionalExpression = t.logicalExpression(
|
|
347
405
|
"&&",
|
|
348
406
|
t.identifier(stateProperty),
|
|
349
|
-
t.memberExpression(t.identifier(
|
|
407
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey)),
|
|
350
408
|
);
|
|
351
409
|
|
|
352
410
|
styleArrayElements.push(conditionalExpression);
|
|
@@ -403,11 +461,78 @@ function createStyleFunction(styleExpression: any, modifierTypes: ModifierType[]
|
|
|
403
461
|
return t.arrowFunctionExpression([param], styleExpression);
|
|
404
462
|
}
|
|
405
463
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
464
|
+
/**
|
|
465
|
+
* Process tw`...` or twStyle('...') call and replace with TwStyle object
|
|
466
|
+
* Generates: { style: styles._base, activeStyle: styles._active, ... }
|
|
467
|
+
*/
|
|
468
|
+
function processTwCall(className: string, path: NodePath, state: PluginState, t: typeof BabelTypes): void {
|
|
469
|
+
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
470
|
+
|
|
471
|
+
// Build TwStyle object properties
|
|
472
|
+
const objectProperties: any[] = [];
|
|
473
|
+
|
|
474
|
+
// Parse and add base styles
|
|
475
|
+
if (baseClasses.length > 0) {
|
|
476
|
+
const baseClassName = baseClasses.join(" ");
|
|
477
|
+
const baseStyleObject = parseClassName(baseClassName, state.customColors);
|
|
478
|
+
const baseStyleKey = generateStyleKey(baseClassName);
|
|
479
|
+
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
480
|
+
|
|
481
|
+
objectProperties.push(
|
|
482
|
+
t.objectProperty(
|
|
483
|
+
t.identifier("style"),
|
|
484
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
|
|
485
|
+
),
|
|
486
|
+
);
|
|
487
|
+
} else {
|
|
488
|
+
// No base classes - add empty style object
|
|
489
|
+
objectProperties.push(t.objectProperty(t.identifier("style"), t.objectExpression([])));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Group modifiers by type
|
|
493
|
+
const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
|
|
494
|
+
for (const mod of modifierClasses) {
|
|
495
|
+
if (!modifiersByType.has(mod.modifier)) {
|
|
496
|
+
modifiersByType.set(mod.modifier, []);
|
|
497
|
+
}
|
|
498
|
+
const modGroup = modifiersByType.get(mod.modifier);
|
|
499
|
+
if (modGroup) {
|
|
500
|
+
modGroup.push(mod);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Add modifier styles
|
|
505
|
+
for (const [modifierType, modifiers] of modifiersByType) {
|
|
506
|
+
const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
507
|
+
const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
|
|
508
|
+
const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
|
|
509
|
+
state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
|
|
510
|
+
|
|
511
|
+
// Map modifier type to property name: active -> activeStyle
|
|
512
|
+
const propertyName = `${modifierType}Style`;
|
|
513
|
+
|
|
514
|
+
objectProperties.push(
|
|
515
|
+
t.objectProperty(
|
|
516
|
+
t.identifier(propertyName),
|
|
517
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey)),
|
|
518
|
+
),
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Replace the tw`...` or twStyle('...') with the object
|
|
523
|
+
const twStyleObject = t.objectExpression(objectProperties);
|
|
524
|
+
path.replaceWith(twStyleObject);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export default function reactNativeTailwindBabelPlugin(
|
|
528
|
+
{ types: t }: { types: typeof BabelTypes },
|
|
529
|
+
options?: PluginOptions,
|
|
530
|
+
): PluginObj<PluginState> {
|
|
531
|
+
// Build attribute matchers from options
|
|
532
|
+
const attributes = options?.attributes ?? [...DEFAULT_CLASS_ATTRIBUTES];
|
|
533
|
+
const { exactMatches, patterns } = buildAttributeMatchers(attributes);
|
|
534
|
+
const stylesIdentifier = options?.stylesIdentifier ?? DEFAULT_STYLES_IDENTIFIER;
|
|
535
|
+
|
|
411
536
|
return {
|
|
412
537
|
name: "react-native-tailwind",
|
|
413
538
|
|
|
@@ -418,12 +543,22 @@ export default function reactNativeTailwindBabelPlugin({
|
|
|
418
543
|
state.styleRegistry = new Map();
|
|
419
544
|
state.hasClassNames = false;
|
|
420
545
|
state.hasStyleSheetImport = false;
|
|
546
|
+
state.supportedAttributes = exactMatches;
|
|
547
|
+
state.attributePatterns = patterns;
|
|
548
|
+
state.stylesIdentifier = stylesIdentifier;
|
|
549
|
+
state.twImportNames = new Set();
|
|
550
|
+
state.hasTwImport = false;
|
|
421
551
|
|
|
422
552
|
// Load custom colors from tailwind.config.*
|
|
423
553
|
state.customColors = extractCustomColors(state.file.opts.filename ?? "");
|
|
424
554
|
},
|
|
425
555
|
|
|
426
556
|
exit(path: NodePath, state: PluginState) {
|
|
557
|
+
// Remove tw/twStyle imports if they were used (and transformed)
|
|
558
|
+
if (state.hasTwImport) {
|
|
559
|
+
removeTwImports(path, t);
|
|
560
|
+
}
|
|
561
|
+
|
|
427
562
|
// If no classNames were found, skip StyleSheet generation
|
|
428
563
|
if (!state.hasClassNames || state.styleRegistry.size === 0) {
|
|
429
564
|
return;
|
|
@@ -434,14 +569,17 @@ export default function reactNativeTailwindBabelPlugin({
|
|
|
434
569
|
addStyleSheetImport(path, t);
|
|
435
570
|
}
|
|
436
571
|
|
|
437
|
-
// Generate and inject StyleSheet.create at the
|
|
438
|
-
|
|
572
|
+
// Generate and inject StyleSheet.create at the beginning of the file (after imports)
|
|
573
|
+
// This ensures _twStyles is defined before any code that references it
|
|
574
|
+
injectStylesAtTop(path, state.styleRegistry, state.stylesIdentifier, t);
|
|
439
575
|
},
|
|
440
576
|
},
|
|
441
577
|
|
|
442
|
-
// Check if StyleSheet is already imported
|
|
578
|
+
// Check if StyleSheet is already imported and track tw/twStyle imports
|
|
443
579
|
ImportDeclaration(path: NodePath, state: PluginState) {
|
|
444
580
|
const node = path.node as any;
|
|
581
|
+
|
|
582
|
+
// Track react-native StyleSheet import
|
|
445
583
|
if (node.source.value === "react-native") {
|
|
446
584
|
const specifiers = node.specifiers;
|
|
447
585
|
const hasStyleSheet = specifiers.some((spec: any) => {
|
|
@@ -459,14 +597,127 @@ export default function reactNativeTailwindBabelPlugin({
|
|
|
459
597
|
state.hasStyleSheetImport = true;
|
|
460
598
|
}
|
|
461
599
|
}
|
|
600
|
+
|
|
601
|
+
// Track tw/twStyle imports from main package (for compile-time transformation)
|
|
602
|
+
if (node.source.value === "@mgcrea/react-native-tailwind") {
|
|
603
|
+
const specifiers = node.specifiers;
|
|
604
|
+
specifiers.forEach((spec: any) => {
|
|
605
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
606
|
+
const importedName = spec.imported.name;
|
|
607
|
+
if (importedName === "tw" || importedName === "twStyle") {
|
|
608
|
+
// Track the local name (could be renamed: import { tw as customTw })
|
|
609
|
+
const localName = spec.local.name;
|
|
610
|
+
state.twImportNames.add(localName);
|
|
611
|
+
state.hasTwImport = true;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
|
|
618
|
+
// Handle tw`...` tagged template expressions
|
|
619
|
+
TaggedTemplateExpression(path: NodePath, state: PluginState) {
|
|
620
|
+
const node = path.node as any;
|
|
621
|
+
|
|
622
|
+
// Check if the tag is a tracked tw import
|
|
623
|
+
if (!t.isIdentifier(node.tag)) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const tagName = node.tag.name;
|
|
628
|
+
if (!state.twImportNames.has(tagName)) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Extract static className from template literal
|
|
633
|
+
const quasi = node.quasi;
|
|
634
|
+
if (!t.isTemplateLiteral(quasi)) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Only support static strings (no interpolations)
|
|
639
|
+
if (quasi.expressions.length > 0) {
|
|
640
|
+
if (process.env.NODE_ENV !== "production") {
|
|
641
|
+
console.warn(
|
|
642
|
+
`[react-native-tailwind] Dynamic tw\`...\` with interpolations is not supported at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
643
|
+
`Use style prop for dynamic values.`,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Get the static className string
|
|
650
|
+
const className = quasi.quasis[0]?.value.cooked?.trim() ?? "";
|
|
651
|
+
if (!className) {
|
|
652
|
+
// Replace with empty object
|
|
653
|
+
path.replaceWith(
|
|
654
|
+
t.objectExpression([t.objectProperty(t.identifier("style"), t.objectExpression([]))]),
|
|
655
|
+
);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
state.hasClassNames = true;
|
|
660
|
+
|
|
661
|
+
// Process the className with modifiers
|
|
662
|
+
processTwCall(className, path, state, t);
|
|
663
|
+
},
|
|
664
|
+
|
|
665
|
+
// Handle twStyle('...') call expressions
|
|
666
|
+
CallExpression(path: NodePath, state: PluginState) {
|
|
667
|
+
const node = path.node as any;
|
|
668
|
+
|
|
669
|
+
// Check if the callee is a tracked twStyle import
|
|
670
|
+
if (!t.isIdentifier(node.callee)) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const calleeName = node.callee.name;
|
|
675
|
+
if (!state.twImportNames.has(calleeName)) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Must have exactly one argument
|
|
680
|
+
if (node.arguments.length !== 1) {
|
|
681
|
+
if (process.env.NODE_ENV !== "production") {
|
|
682
|
+
console.warn(
|
|
683
|
+
`[react-native-tailwind] twStyle() expects exactly one argument at ${state.file.opts.filename ?? "unknown"}`,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const arg = node.arguments[0];
|
|
690
|
+
|
|
691
|
+
// Only support static string literals
|
|
692
|
+
if (!t.isStringLiteral(arg)) {
|
|
693
|
+
if (process.env.NODE_ENV !== "production") {
|
|
694
|
+
console.warn(
|
|
695
|
+
`[react-native-tailwind] twStyle() only supports static string literals at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
696
|
+
`Use style prop for dynamic values.`,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const className = arg.value.trim();
|
|
703
|
+
if (!className) {
|
|
704
|
+
// Replace with undefined
|
|
705
|
+
path.replaceWith(t.identifier("undefined"));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
state.hasClassNames = true;
|
|
710
|
+
|
|
711
|
+
// Process the className with modifiers
|
|
712
|
+
processTwCall(className, path, state, t);
|
|
462
713
|
},
|
|
463
714
|
|
|
464
715
|
JSXAttribute(path: NodePath, state: PluginState) {
|
|
465
716
|
const node = path.node as any;
|
|
466
717
|
const attributeName = node.name.name;
|
|
467
718
|
|
|
468
|
-
// Only process className-like attributes
|
|
469
|
-
if (!
|
|
719
|
+
// Only process configured className-like attributes
|
|
720
|
+
if (!isAttributeSupported(attributeName, state.supportedAttributes, state.attributePatterns)) {
|
|
470
721
|
return;
|
|
471
722
|
}
|
|
472
723
|
|
|
@@ -590,10 +841,10 @@ export default function reactNativeTailwindBabelPlugin({
|
|
|
590
841
|
|
|
591
842
|
if (styleAttribute) {
|
|
592
843
|
// Merge with existing style prop
|
|
593
|
-
mergeStyleAttribute(path, styleAttribute, styleKey, t);
|
|
844
|
+
mergeStyleAttribute(path, styleAttribute, styleKey, state.stylesIdentifier, t);
|
|
594
845
|
} else {
|
|
595
846
|
// Replace className with style prop
|
|
596
|
-
replaceWithStyleAttribute(path, styleKey, targetStyleProp, t);
|
|
847
|
+
replaceWithStyleAttribute(path, styleKey, targetStyleProp, state.stylesIdentifier, t);
|
|
597
848
|
}
|
|
598
849
|
return;
|
|
599
850
|
}
|
|
@@ -665,6 +916,41 @@ function addStyleSheetImport(path: NodePath, t: typeof BabelTypes) {
|
|
|
665
916
|
(path as any).unshiftContainer("body", importDeclaration);
|
|
666
917
|
}
|
|
667
918
|
|
|
919
|
+
/**
|
|
920
|
+
* Remove tw/twStyle imports from @mgcrea/react-native-tailwind
|
|
921
|
+
* This is called after all tw calls have been transformed
|
|
922
|
+
*/
|
|
923
|
+
function removeTwImports(path: NodePath, t: typeof BabelTypes) {
|
|
924
|
+
// Traverse the program to find and remove tw/twStyle imports
|
|
925
|
+
path.traverse({
|
|
926
|
+
ImportDeclaration(importPath: NodePath) {
|
|
927
|
+
const node = importPath.node as any;
|
|
928
|
+
|
|
929
|
+
// Only process imports from main package
|
|
930
|
+
if (node.source.value !== "@mgcrea/react-native-tailwind") {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Filter out tw/twStyle specifiers
|
|
935
|
+
const remainingSpecifiers = node.specifiers.filter((spec: any) => {
|
|
936
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
937
|
+
const importedName = spec.imported.name;
|
|
938
|
+
return importedName !== "tw" && importedName !== "twStyle";
|
|
939
|
+
}
|
|
940
|
+
return true;
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
if (remainingSpecifiers.length === 0) {
|
|
944
|
+
// Remove entire import if no specifiers remain
|
|
945
|
+
importPath.remove();
|
|
946
|
+
} else if (remainingSpecifiers.length < node.specifiers.length) {
|
|
947
|
+
// Update import with remaining specifiers
|
|
948
|
+
node.specifiers = remainingSpecifiers;
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
668
954
|
/**
|
|
669
955
|
* Replace className with style attribute
|
|
670
956
|
*/
|
|
@@ -672,11 +958,12 @@ function replaceWithStyleAttribute(
|
|
|
672
958
|
classNamePath: NodePath,
|
|
673
959
|
styleKey: string,
|
|
674
960
|
targetStyleProp: string,
|
|
961
|
+
stylesIdentifier: string,
|
|
675
962
|
t: typeof BabelTypes,
|
|
676
963
|
) {
|
|
677
964
|
const styleAttribute = t.jsxAttribute(
|
|
678
965
|
t.jsxIdentifier(targetStyleProp),
|
|
679
|
-
t.jsxExpressionContainer(t.memberExpression(t.identifier(
|
|
966
|
+
t.jsxExpressionContainer(t.memberExpression(t.identifier(stylesIdentifier), t.identifier(styleKey))),
|
|
680
967
|
);
|
|
681
968
|
|
|
682
969
|
classNamePath.replaceWith(styleAttribute);
|
|
@@ -689,6 +976,7 @@ function mergeStyleAttribute(
|
|
|
689
976
|
classNamePath: NodePath,
|
|
690
977
|
styleAttribute: any,
|
|
691
978
|
styleKey: string,
|
|
979
|
+
stylesIdentifier: string,
|
|
692
980
|
t: typeof BabelTypes,
|
|
693
981
|
) {
|
|
694
982
|
const existingStyle = styleAttribute.value.expression;
|
|
@@ -696,7 +984,7 @@ function mergeStyleAttribute(
|
|
|
696
984
|
// Create array with className styles first, then existing styles
|
|
697
985
|
// This allows existing styles to override className styles
|
|
698
986
|
const styleArray = t.arrayExpression([
|
|
699
|
-
t.memberExpression(t.identifier(
|
|
987
|
+
t.memberExpression(t.identifier(stylesIdentifier), t.identifier(styleKey)),
|
|
700
988
|
existingStyle,
|
|
701
989
|
]);
|
|
702
990
|
|
|
@@ -815,9 +1103,15 @@ function mergeStyleFunctionAttribute(
|
|
|
815
1103
|
}
|
|
816
1104
|
|
|
817
1105
|
/**
|
|
818
|
-
* Inject StyleSheet.create with all collected styles
|
|
1106
|
+
* Inject StyleSheet.create with all collected styles at the top of the file
|
|
1107
|
+
* This ensures the styles object is defined before any code that references it
|
|
819
1108
|
*/
|
|
820
|
-
function
|
|
1109
|
+
function injectStylesAtTop(
|
|
1110
|
+
path: NodePath,
|
|
1111
|
+
styleRegistry: Map<string, StyleObject>,
|
|
1112
|
+
stylesIdentifier: string,
|
|
1113
|
+
t: typeof BabelTypes,
|
|
1114
|
+
) {
|
|
821
1115
|
// Build style object properties
|
|
822
1116
|
const styleProperties: any[] = [];
|
|
823
1117
|
|
|
@@ -840,18 +1134,32 @@ function injectStyles(path: NodePath, styleRegistry: Map<string, StyleObject>, t
|
|
|
840
1134
|
styleProperties.push(t.objectProperty(t.identifier(key), t.objectExpression(properties)));
|
|
841
1135
|
}
|
|
842
1136
|
|
|
843
|
-
// Create: const
|
|
1137
|
+
// Create: const _twStyles = StyleSheet.create({ ... })
|
|
844
1138
|
const styleSheet = t.variableDeclaration("const", [
|
|
845
1139
|
t.variableDeclarator(
|
|
846
|
-
t.identifier(
|
|
1140
|
+
t.identifier(stylesIdentifier),
|
|
847
1141
|
t.callExpression(t.memberExpression(t.identifier("StyleSheet"), t.identifier("create")), [
|
|
848
1142
|
t.objectExpression(styleProperties),
|
|
849
1143
|
]),
|
|
850
1144
|
),
|
|
851
1145
|
]);
|
|
852
1146
|
|
|
853
|
-
//
|
|
854
|
-
(path as any).
|
|
1147
|
+
// Find the index to insert after all imports
|
|
1148
|
+
const body = (path as any).node.body;
|
|
1149
|
+
let insertIndex = 0;
|
|
1150
|
+
|
|
1151
|
+
// Find the last import statement
|
|
1152
|
+
for (let i = 0; i < body.length; i++) {
|
|
1153
|
+
if (t.isImportDeclaration(body[i])) {
|
|
1154
|
+
insertIndex = i + 1;
|
|
1155
|
+
} else {
|
|
1156
|
+
// Stop at the first non-import statement
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Insert StyleSheet.create after imports
|
|
1162
|
+
body.splice(insertIndex, 0, styleSheet);
|
|
855
1163
|
}
|
|
856
1164
|
|
|
857
1165
|
// Helper functions that use the imported parser
|
|
@@ -11,6 +11,7 @@ export type PressableProps = Omit<RNPressableProps, "style"> & {
|
|
|
11
11
|
* Style can be a static style object/array or a function that receives Pressable state + disabled
|
|
12
12
|
*/
|
|
13
13
|
style?: StyleProp<ViewStyle> | ((state: EnhancedPressableState) => StyleProp<ViewStyle>);
|
|
14
|
+
className?: string;
|
|
14
15
|
};
|
|
15
16
|
/**
|
|
16
17
|
* Enhanced Pressable that supports the disabled: modifier
|
|
@@ -28,5 +29,6 @@ export declare const Pressable: import("react").ForwardRefExoticComponent<Omit<R
|
|
|
28
29
|
* Style can be a static style object/array or a function that receives Pressable state + disabled
|
|
29
30
|
*/
|
|
30
31
|
style?: StyleProp<ViewStyle> | ((state: EnhancedPressableState) => StyleProp<ViewStyle>);
|
|
32
|
+
className?: string;
|
|
31
33
|
} & import("react").RefAttributes<import("react-native").View>>;
|
|
32
34
|
export {};
|
|
@@ -23,6 +23,7 @@ export type TextInputProps = Omit<RNTextInputProps, "style"> & {
|
|
|
23
23
|
focused: boolean;
|
|
24
24
|
disabled: boolean;
|
|
25
25
|
}) => RNTextInputProps["style"]);
|
|
26
|
+
className?: string;
|
|
26
27
|
/**
|
|
27
28
|
* Convenience prop for disabled state (overrides editable if provided)
|
|
28
29
|
* When true, sets editable to false
|
|
@@ -48,6 +49,7 @@ export declare const TextInput: import("react").ForwardRefExoticComponent<Omit<R
|
|
|
48
49
|
focused: boolean;
|
|
49
50
|
disabled: boolean;
|
|
50
51
|
}) => RNTextInputProps["style"]);
|
|
52
|
+
className?: string;
|
|
51
53
|
/**
|
|
52
54
|
* Convenience prop for disabled state (overrides editable if provided)
|
|
53
55
|
* When true, sets editable to false
|