@mgcrea/react-native-tailwind 0.12.1 → 0.14.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 +45 -2031
- package/dist/babel/index.cjs +1726 -1094
- package/dist/babel/plugin/componentScope.d.ts +26 -0
- package/dist/babel/plugin/componentScope.ts +87 -0
- package/dist/babel/plugin/state.d.ts +123 -0
- package/dist/babel/plugin/state.ts +185 -0
- package/dist/babel/plugin/visitors/className.d.ts +11 -0
- package/{src/babel/plugin.test.ts → dist/babel/plugin/visitors/className.test.ts} +285 -572
- package/dist/babel/plugin/visitors/className.ts +652 -0
- package/dist/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
- package/dist/babel/plugin/visitors/imports.d.ts +11 -0
- package/dist/babel/plugin/visitors/imports.test.ts +88 -0
- package/dist/babel/plugin/visitors/imports.ts +116 -0
- package/dist/babel/plugin/visitors/program.d.ts +15 -0
- package/dist/babel/plugin/visitors/program.test.ts +325 -0
- package/dist/babel/plugin/visitors/program.ts +116 -0
- package/dist/babel/plugin/visitors/tw.d.ts +16 -0
- package/dist/babel/plugin/visitors/tw.test.ts +771 -0
- package/dist/babel/plugin/visitors/tw.ts +148 -0
- package/dist/babel/plugin.d.ts +3 -96
- package/dist/babel/plugin.test.ts +470 -0
- package/dist/babel/plugin.ts +28 -963
- package/dist/babel/utils/colorSchemeModifierProcessing.ts +11 -0
- package/dist/babel/utils/componentSupport.test.ts +20 -7
- package/dist/babel/utils/componentSupport.ts +2 -0
- package/dist/babel/utils/directionalModifierProcessing.d.ts +34 -0
- package/dist/babel/utils/directionalModifierProcessing.ts +99 -0
- package/dist/babel/utils/modifierProcessing.ts +21 -0
- package/dist/babel/utils/platformModifierProcessing.ts +11 -0
- package/dist/babel/utils/styleInjection.d.ts +31 -0
- package/dist/babel/utils/styleInjection.ts +253 -7
- package/dist/babel/utils/twProcessing.d.ts +2 -0
- package/dist/babel/utils/twProcessing.ts +103 -3
- package/dist/babel/utils/windowDimensionsProcessing.d.ts +56 -0
- package/dist/babel/utils/windowDimensionsProcessing.ts +121 -0
- package/dist/components/TouchableOpacity.d.ts +35 -0
- package/dist/components/TouchableOpacity.js +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +1 -0
- package/dist/config/markers.d.ts +5 -0
- package/dist/config/markers.js +1 -0
- package/dist/index.d.ts +2 -5
- package/dist/index.js +1 -1
- package/dist/parser/borders.d.ts +3 -1
- package/dist/parser/borders.js +1 -1
- package/dist/parser/borders.test.js +1 -1
- package/dist/parser/colors.js +1 -1
- package/dist/parser/colors.test.js +1 -1
- package/dist/parser/index.d.ts +2 -2
- package/dist/parser/index.js +1 -1
- package/dist/parser/layout.js +1 -1
- package/dist/parser/layout.test.js +1 -1
- package/dist/parser/modifiers.d.ts +32 -2
- package/dist/parser/modifiers.js +1 -1
- package/dist/parser/modifiers.test.js +1 -1
- package/dist/parser/sizing.js +1 -1
- 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/parser/typography.test.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +4 -4
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +4 -4
- package/package.json +6 -6
- package/src/babel/plugin/componentScope.ts +87 -0
- package/src/babel/plugin/state.ts +185 -0
- package/src/babel/plugin/visitors/className.test.ts +1625 -0
- package/src/babel/plugin/visitors/className.ts +652 -0
- package/src/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
- package/src/babel/plugin/visitors/imports.test.ts +88 -0
- package/src/babel/plugin/visitors/imports.ts +116 -0
- package/src/babel/plugin/visitors/program.test.ts +325 -0
- package/src/babel/plugin/visitors/program.ts +116 -0
- package/src/babel/plugin/visitors/tw.test.ts +771 -0
- package/src/babel/plugin/visitors/tw.ts +148 -0
- package/src/babel/plugin.ts +28 -963
- package/src/babel/utils/colorSchemeModifierProcessing.ts +11 -0
- package/src/babel/utils/componentSupport.test.ts +20 -7
- package/src/babel/utils/componentSupport.ts +2 -0
- package/src/babel/utils/directionalModifierProcessing.ts +99 -0
- package/src/babel/utils/modifierProcessing.ts +21 -0
- package/src/babel/utils/platformModifierProcessing.ts +11 -0
- package/src/babel/utils/styleInjection.ts +253 -7
- package/src/babel/utils/twProcessing.ts +103 -3
- package/src/babel/utils/windowDimensionsProcessing.ts +121 -0
- package/src/components/TouchableOpacity.tsx +71 -0
- package/src/components/index.ts +3 -0
- package/src/config/markers.ts +5 -0
- package/src/index.ts +4 -5
- package/src/parser/borders.test.ts +162 -0
- package/src/parser/borders.ts +67 -9
- package/src/parser/colors.test.ts +249 -0
- package/src/parser/colors.ts +38 -0
- package/src/parser/index.ts +4 -2
- package/src/parser/layout.test.ts +74 -0
- package/src/parser/layout.ts +94 -0
- package/src/parser/modifiers.test.ts +206 -0
- package/src/parser/modifiers.ts +62 -3
- package/src/parser/sizing.ts +11 -0
- package/src/parser/spacing.test.ts +66 -0
- package/src/parser/spacing.ts +15 -5
- package/src/parser/typography.test.ts +8 -0
- package/src/parser/typography.ts +4 -0
package/src/babel/plugin.ts
CHANGED
|
@@ -3,245 +3,25 @@
|
|
|
3
3
|
* Transforms className props to style props at compile time
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { PluginObj } from "@babel/core";
|
|
7
7
|
import * as BabelTypes from "@babel/types";
|
|
8
|
-
import type {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
isStateModifier,
|
|
15
|
-
parseClassName,
|
|
16
|
-
parsePlaceholderClasses,
|
|
17
|
-
splitModifierClasses,
|
|
18
|
-
} from "../parser/index.js";
|
|
19
|
-
import type { StyleObject } from "../types/core.js";
|
|
20
|
-
import { generateStyleKey } from "../utils/styleKey.js";
|
|
21
|
-
import type { CustomTheme } from "./config-loader.js";
|
|
22
|
-
import { extractCustomTheme } from "./config-loader.js";
|
|
8
|
+
import type { PluginOptions, PluginState } from "./plugin/state.js";
|
|
9
|
+
import { createInitialState } from "./plugin/state.js";
|
|
10
|
+
import { jsxAttributeVisitor } from "./plugin/visitors/className.js";
|
|
11
|
+
import { importDeclarationVisitor } from "./plugin/visitors/imports.js";
|
|
12
|
+
import { programEnter, programExit } from "./plugin/visitors/program.js";
|
|
13
|
+
import { callExpressionVisitor, taggedTemplateVisitor } from "./plugin/visitors/tw.js";
|
|
23
14
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
DEFAULT_CLASS_ATTRIBUTES,
|
|
28
|
-
buildAttributeMatchers,
|
|
29
|
-
getTargetStyleProp,
|
|
30
|
-
isAttributeSupported,
|
|
31
|
-
} from "./utils/attributeMatchers.js";
|
|
32
|
-
import { processColorSchemeModifiers } from "./utils/colorSchemeModifierProcessing.js";
|
|
33
|
-
import { getComponentModifierSupport, getStatePropertyForModifier } from "./utils/componentSupport.js";
|
|
34
|
-
import { processDynamicExpression } from "./utils/dynamicProcessing.js";
|
|
35
|
-
import { createStyleFunction, processStaticClassNameWithModifiers } from "./utils/modifierProcessing.js";
|
|
36
|
-
import { processPlatformModifiers } from "./utils/platformModifierProcessing.js";
|
|
37
|
-
import {
|
|
38
|
-
addColorSchemeImport,
|
|
39
|
-
addPlatformImport,
|
|
40
|
-
addStyleSheetImport,
|
|
41
|
-
injectColorSchemeHook,
|
|
42
|
-
injectStylesAtTop,
|
|
43
|
-
} from "./utils/styleInjection.js";
|
|
44
|
-
import {
|
|
45
|
-
addOrMergePlaceholderTextColorProp,
|
|
46
|
-
findStyleAttribute,
|
|
47
|
-
mergeDynamicStyleAttribute,
|
|
48
|
-
mergeStyleAttribute,
|
|
49
|
-
mergeStyleFunctionAttribute,
|
|
50
|
-
replaceDynamicWithStyleAttribute,
|
|
51
|
-
replaceWithStyleAttribute,
|
|
52
|
-
replaceWithStyleFunctionAttribute,
|
|
53
|
-
} from "./utils/styleTransforms.js";
|
|
54
|
-
import { processTwCall, removeTwImports } from "./utils/twProcessing.js";
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Plugin options
|
|
58
|
-
*/
|
|
59
|
-
export type PluginOptions = {
|
|
60
|
-
/**
|
|
61
|
-
* List of JSX attribute names to transform (in addition to or instead of 'className')
|
|
62
|
-
* Supports exact matches and glob patterns:
|
|
63
|
-
* - Exact: 'className', 'containerClassName'
|
|
64
|
-
* - Glob: '*ClassName' (matches any attribute ending in 'ClassName')
|
|
65
|
-
*
|
|
66
|
-
* @default ['className', 'contentContainerClassName', 'columnWrapperClassName', 'ListHeaderComponentClassName', 'ListFooterComponentClassName']
|
|
67
|
-
*/
|
|
68
|
-
attributes?: string[];
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Custom identifier name for the generated StyleSheet constant
|
|
72
|
-
*
|
|
73
|
-
* @default '_twStyles'
|
|
74
|
-
*/
|
|
75
|
-
stylesIdentifier?: string;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Configuration for the scheme: modifier that expands to both dark: and light: modifiers
|
|
79
|
-
*
|
|
80
|
-
* @example
|
|
81
|
-
* {
|
|
82
|
-
* darkSuffix: '-dark', // scheme:bg-primary -> dark:bg-primary-dark
|
|
83
|
-
* lightSuffix: '-light' // scheme:bg-primary -> light:bg-primary-light
|
|
84
|
-
* }
|
|
85
|
-
*
|
|
86
|
-
* @default { darkSuffix: '-dark', lightSuffix: '-light' }
|
|
87
|
-
*/
|
|
88
|
-
schemeModifier?: {
|
|
89
|
-
darkSuffix?: string;
|
|
90
|
-
lightSuffix?: string;
|
|
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
|
-
};
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
type PluginState = PluginPass & {
|
|
131
|
-
styleRegistry: Map<string, StyleObject>;
|
|
132
|
-
hasClassNames: boolean;
|
|
133
|
-
hasStyleSheetImport: boolean;
|
|
134
|
-
hasPlatformImport: boolean;
|
|
135
|
-
needsPlatformImport: boolean;
|
|
136
|
-
hasColorSchemeImport: boolean;
|
|
137
|
-
needsColorSchemeImport: boolean;
|
|
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
|
|
142
|
-
customTheme: CustomTheme;
|
|
143
|
-
schemeModifierConfig: SchemeModifierConfig;
|
|
144
|
-
supportedAttributes: Set<string>;
|
|
145
|
-
attributePatterns: RegExp[];
|
|
146
|
-
stylesIdentifier: string;
|
|
147
|
-
// Track tw/twStyle imports from main package
|
|
148
|
-
twImportNames: Set<string>; // e.g., ['tw', 'twStyle'] or ['tw as customTw']
|
|
149
|
-
hasTwImport: boolean;
|
|
150
|
-
// Track react-native import path for conditional StyleSheet/Platform injection
|
|
151
|
-
reactNativeImportPath?: NodePath<BabelTypes.ImportDeclaration>;
|
|
152
|
-
// Track function components that need colorScheme hook injection
|
|
153
|
-
functionComponentsNeedingColorScheme: Set<NodePath<BabelTypes.Function>>;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
// Default identifier for the generated StyleSheet constant
|
|
157
|
-
const DEFAULT_STYLES_IDENTIFIER = "_twStyles";
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Check if a function path represents a valid component scope for hook injection
|
|
161
|
-
* Valid scopes:
|
|
162
|
-
* - Top-level FunctionDeclaration
|
|
163
|
-
* - FunctionExpression/ArrowFunctionExpression in top-level VariableDeclarator (with PascalCase name)
|
|
164
|
-
* - NOT class methods, NOT nested functions, NOT inline callbacks
|
|
165
|
-
*
|
|
166
|
-
* @param functionPath - Path to the function to check
|
|
167
|
-
* @param t - Babel types
|
|
168
|
-
* @returns true if function is a valid component scope
|
|
169
|
-
*/
|
|
170
|
-
function isComponentScope(functionPath: NodePath<BabelTypes.Function>, t: typeof BabelTypes): boolean {
|
|
171
|
-
const node = functionPath.node;
|
|
172
|
-
const parent = functionPath.parent;
|
|
173
|
-
const parentPath = functionPath.parentPath;
|
|
174
|
-
|
|
175
|
-
// Reject class methods (class components not supported for hooks)
|
|
176
|
-
if (t.isClassMethod(parent)) {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Reject if inside a class body
|
|
181
|
-
if (functionPath.findParent((p) => t.isClassBody(p.node))) {
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Accept top-level FunctionDeclaration
|
|
186
|
-
if (t.isFunctionDeclaration(node)) {
|
|
187
|
-
// Check if it's at program level or in export
|
|
188
|
-
if (t.isProgram(parent) || t.isExportNamedDeclaration(parent) || t.isExportDefaultDeclaration(parent)) {
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Accept FunctionExpression/ArrowFunctionExpression in VariableDeclarator
|
|
194
|
-
if (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) {
|
|
195
|
-
if (t.isVariableDeclarator(parent)) {
|
|
196
|
-
// Check if it's at program level (via VariableDeclaration)
|
|
197
|
-
const varDeclarationPath = parentPath?.parentPath;
|
|
198
|
-
if (
|
|
199
|
-
varDeclarationPath &&
|
|
200
|
-
t.isVariableDeclaration(varDeclarationPath.node) &&
|
|
201
|
-
(t.isProgram(varDeclarationPath.parent) || t.isExportNamedDeclaration(varDeclarationPath.parent))
|
|
202
|
-
) {
|
|
203
|
-
// Check for PascalCase naming (component convention)
|
|
204
|
-
if (t.isIdentifier(parent.id)) {
|
|
205
|
-
const name = parent.id.name;
|
|
206
|
-
return /^[A-Z]/.test(name); // Starts with uppercase
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Find the nearest valid component scope for hook injection
|
|
217
|
-
* Climbs the AST from the current path to find a component-level function
|
|
218
|
-
*
|
|
219
|
-
* @param path - Starting path (e.g., JSXAttribute)
|
|
220
|
-
* @param t - Babel types
|
|
221
|
-
* @returns NodePath to component function, or null if not found
|
|
222
|
-
*/
|
|
223
|
-
function findComponentScope(path: NodePath, t: typeof BabelTypes): NodePath<BabelTypes.Function> | null {
|
|
224
|
-
let current = path.getFunctionParent();
|
|
225
|
-
|
|
226
|
-
while (current) {
|
|
227
|
-
if (t.isFunction(current.node) && isComponentScope(current, t)) {
|
|
228
|
-
return current;
|
|
229
|
-
}
|
|
230
|
-
// Climb to next parent function
|
|
231
|
-
current = current.getFunctionParent();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
15
|
+
// Re-export PluginOptions for external use
|
|
16
|
+
export type { PluginOptions };
|
|
236
17
|
|
|
237
18
|
export default function reactNativeTailwindBabelPlugin(
|
|
238
19
|
{ types: t }: { types: typeof BabelTypes },
|
|
239
20
|
options?: PluginOptions,
|
|
240
21
|
): PluginObj<PluginState> {
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
const stylesIdentifier = options?.stylesIdentifier ?? DEFAULT_STYLES_IDENTIFIER;
|
|
22
|
+
// Color scheme hook configuration from plugin options
|
|
23
|
+
const colorSchemeImportSource = options?.colorScheme?.importFrom ?? "react-native";
|
|
24
|
+
const colorSchemeHookName = options?.colorScheme?.importName ?? "useColorScheme";
|
|
245
25
|
|
|
246
26
|
// Scheme modifier configuration from plugin options
|
|
247
27
|
const schemeModifierConfig = {
|
|
@@ -249,760 +29,45 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
249
29
|
lightSuffix: options?.schemeModifier?.lightSuffix ?? "-light",
|
|
250
30
|
};
|
|
251
31
|
|
|
252
|
-
// Color scheme hook configuration from plugin options
|
|
253
|
-
const colorSchemeImportSource = options?.colorScheme?.importFrom ?? "react-native";
|
|
254
|
-
const colorSchemeHookName = options?.colorScheme?.importName ?? "useColorScheme";
|
|
255
|
-
|
|
256
32
|
return {
|
|
257
33
|
name: "react-native-tailwind",
|
|
258
34
|
|
|
259
35
|
visitor: {
|
|
260
36
|
Program: {
|
|
261
|
-
enter(
|
|
37
|
+
enter(path, state) {
|
|
262
38
|
// Initialize state for this file
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
state
|
|
271
|
-
state.colorSchemeImportSource = colorSchemeImportSource;
|
|
272
|
-
state.colorSchemeHookName = colorSchemeHookName;
|
|
273
|
-
state.supportedAttributes = exactMatches;
|
|
274
|
-
state.attributePatterns = patterns;
|
|
275
|
-
state.stylesIdentifier = stylesIdentifier;
|
|
276
|
-
state.twImportNames = new Set();
|
|
277
|
-
state.hasTwImport = false;
|
|
278
|
-
state.functionComponentsNeedingColorScheme = new Set();
|
|
279
|
-
state.hasColorSchemeImport = false;
|
|
280
|
-
state.colorSchemeLocalIdentifier = undefined;
|
|
281
|
-
state.needsPlatformImport = false;
|
|
282
|
-
state.hasPlatformImport = false;
|
|
283
|
-
|
|
284
|
-
// Load custom theme from tailwind.config.*
|
|
285
|
-
state.customTheme = extractCustomTheme(state.file.opts.filename ?? "");
|
|
39
|
+
const initialState = createInitialState(
|
|
40
|
+
options,
|
|
41
|
+
state.file.opts.filename ?? "",
|
|
42
|
+
colorSchemeImportSource,
|
|
43
|
+
colorSchemeHookName,
|
|
44
|
+
schemeModifierConfig,
|
|
45
|
+
);
|
|
46
|
+
Object.assign(state, initialState);
|
|
286
47
|
|
|
287
|
-
//
|
|
288
|
-
state
|
|
48
|
+
// Call programEnter (currently a no-op, but kept for consistency)
|
|
49
|
+
programEnter(path, state);
|
|
289
50
|
},
|
|
290
51
|
|
|
291
52
|
exit(path, state) {
|
|
292
|
-
|
|
293
|
-
if (state.hasTwImport) {
|
|
294
|
-
removeTwImports(path, t);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// If no classNames were found, skip StyleSheet generation
|
|
298
|
-
if (!state.hasClassNames || state.styleRegistry.size === 0) {
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Add StyleSheet import if not already present
|
|
303
|
-
if (!state.hasStyleSheetImport) {
|
|
304
|
-
addStyleSheetImport(path, t);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Add Platform import if platform modifiers were used and not already present
|
|
308
|
-
if (state.needsPlatformImport && !state.hasPlatformImport) {
|
|
309
|
-
addPlatformImport(path, t);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Add color scheme hook import if color scheme modifiers were used and not already present
|
|
313
|
-
if (state.needsColorSchemeImport && !state.hasColorSchemeImport) {
|
|
314
|
-
addColorSchemeImport(path, state.colorSchemeImportSource, state.colorSchemeHookName, t);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Inject color scheme hook in function components that need it
|
|
318
|
-
if (state.needsColorSchemeImport) {
|
|
319
|
-
for (const functionPath of state.functionComponentsNeedingColorScheme) {
|
|
320
|
-
injectColorSchemeHook(
|
|
321
|
-
functionPath,
|
|
322
|
-
state.colorSchemeVariableName,
|
|
323
|
-
state.colorSchemeHookName,
|
|
324
|
-
state.colorSchemeLocalIdentifier,
|
|
325
|
-
t,
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Generate and inject StyleSheet.create at the beginning of the file (after imports)
|
|
331
|
-
// This ensures _twStyles is defined before any code that references it
|
|
332
|
-
injectStylesAtTop(path, state.styleRegistry, state.stylesIdentifier, t);
|
|
53
|
+
programExit(path, state, t);
|
|
333
54
|
},
|
|
334
55
|
},
|
|
335
56
|
|
|
336
|
-
// Check if StyleSheet/Platform are already imported and track tw/twStyle imports
|
|
337
57
|
ImportDeclaration(path, state) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// Track react-native StyleSheet and Platform imports
|
|
341
|
-
if (node.source.value === "react-native") {
|
|
342
|
-
const specifiers = node.specifiers;
|
|
343
|
-
|
|
344
|
-
const hasStyleSheet = specifiers.some((spec) => {
|
|
345
|
-
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
346
|
-
return spec.imported.name === "StyleSheet";
|
|
347
|
-
}
|
|
348
|
-
return false;
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
const hasPlatform = specifiers.some((spec) => {
|
|
352
|
-
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
353
|
-
return spec.imported.name === "Platform";
|
|
354
|
-
}
|
|
355
|
-
return false;
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
// Only track if imports exist - don't mutate yet
|
|
359
|
-
// Actual import injection happens in Program.exit only if needed
|
|
360
|
-
if (hasStyleSheet) {
|
|
361
|
-
state.hasStyleSheetImport = true;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (hasPlatform) {
|
|
365
|
-
state.hasPlatformImport = true;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Store reference to the react-native import for later modification if needed
|
|
369
|
-
state.reactNativeImportPath = path;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Track color scheme hook import from the configured source
|
|
373
|
-
// (default: react-native, but can be custom like @/hooks/useColorScheme)
|
|
374
|
-
// Only track value imports (not type-only imports which get erased)
|
|
375
|
-
if (node.source.value === state.colorSchemeImportSource && node.importKind !== "type") {
|
|
376
|
-
const specifiers = node.specifiers;
|
|
377
|
-
|
|
378
|
-
for (const spec of specifiers) {
|
|
379
|
-
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
380
|
-
if (spec.imported.name === state.colorSchemeHookName) {
|
|
381
|
-
state.hasColorSchemeImport = true;
|
|
382
|
-
// Track the local identifier (handles aliased imports)
|
|
383
|
-
// e.g., import { useTheme as navTheme } → local name is 'navTheme'
|
|
384
|
-
state.colorSchemeLocalIdentifier = spec.local.name;
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Track tw/twStyle imports from main package (for compile-time transformation)
|
|
392
|
-
if (node.source.value === "@mgcrea/react-native-tailwind") {
|
|
393
|
-
const specifiers = node.specifiers;
|
|
394
|
-
specifiers.forEach((spec) => {
|
|
395
|
-
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
396
|
-
const importedName = spec.imported.name;
|
|
397
|
-
if (importedName === "tw" || importedName === "twStyle") {
|
|
398
|
-
// Track the local name (could be renamed: import { tw as customTw })
|
|
399
|
-
const localName = spec.local.name;
|
|
400
|
-
state.twImportNames.add(localName);
|
|
401
|
-
// Don't set hasTwImport yet - only set it when we successfully transform a call
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
}
|
|
58
|
+
importDeclarationVisitor(path, state, t);
|
|
406
59
|
},
|
|
407
60
|
|
|
408
|
-
// Handle tw`...` tagged template expressions
|
|
409
61
|
TaggedTemplateExpression(path, state) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// Check if the tag is a tracked tw import
|
|
413
|
-
if (!t.isIdentifier(node.tag)) {
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const tagName = node.tag.name;
|
|
418
|
-
if (!state.twImportNames.has(tagName)) {
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Extract static className from template literal
|
|
423
|
-
const quasi = node.quasi;
|
|
424
|
-
if (!t.isTemplateLiteral(quasi)) {
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Only support static strings (no interpolations)
|
|
429
|
-
if (quasi.expressions.length > 0) {
|
|
430
|
-
if (process.env.NODE_ENV !== "production") {
|
|
431
|
-
console.warn(
|
|
432
|
-
`[react-native-tailwind] Dynamic tw\`...\` with interpolations is not supported at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
433
|
-
`Use style prop for dynamic values.`,
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Get the static className string
|
|
440
|
-
const className = quasi.quasis[0]?.value.cooked?.trim() ?? "";
|
|
441
|
-
if (!className) {
|
|
442
|
-
// Replace with empty object
|
|
443
|
-
path.replaceWith(
|
|
444
|
-
t.objectExpression([t.objectProperty(t.identifier("style"), t.objectExpression([]))]),
|
|
445
|
-
);
|
|
446
|
-
// Mark as successfully transformed (even if empty)
|
|
447
|
-
state.hasTwImport = true;
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
state.hasClassNames = true;
|
|
452
|
-
|
|
453
|
-
// Process the className with modifiers
|
|
454
|
-
processTwCall(
|
|
455
|
-
className,
|
|
456
|
-
path,
|
|
457
|
-
state,
|
|
458
|
-
parseClassName,
|
|
459
|
-
generateStyleKey,
|
|
460
|
-
splitModifierClasses,
|
|
461
|
-
findComponentScope,
|
|
462
|
-
t,
|
|
463
|
-
);
|
|
464
|
-
|
|
465
|
-
// Mark as successfully transformed
|
|
466
|
-
state.hasTwImport = true;
|
|
62
|
+
taggedTemplateVisitor(path, state, t);
|
|
467
63
|
},
|
|
468
64
|
|
|
469
|
-
// Handle twStyle('...') call expressions
|
|
470
65
|
CallExpression(path, state) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// Check if the callee is a tracked twStyle import
|
|
474
|
-
if (!t.isIdentifier(node.callee)) {
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const calleeName = node.callee.name;
|
|
479
|
-
if (!state.twImportNames.has(calleeName)) {
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Must have exactly one argument
|
|
484
|
-
if (node.arguments.length !== 1) {
|
|
485
|
-
if (process.env.NODE_ENV !== "production") {
|
|
486
|
-
console.warn(
|
|
487
|
-
`[react-native-tailwind] twStyle() expects exactly one argument at ${state.file.opts.filename ?? "unknown"}`,
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const arg = node.arguments[0];
|
|
494
|
-
|
|
495
|
-
// Only support static string literals
|
|
496
|
-
if (!t.isStringLiteral(arg)) {
|
|
497
|
-
if (process.env.NODE_ENV !== "production") {
|
|
498
|
-
console.warn(
|
|
499
|
-
`[react-native-tailwind] twStyle() only supports static string literals at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
500
|
-
`Use style prop for dynamic values.`,
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const className = arg.value.trim();
|
|
507
|
-
if (!className) {
|
|
508
|
-
// Replace with undefined
|
|
509
|
-
path.replaceWith(t.identifier("undefined"));
|
|
510
|
-
// Mark as successfully transformed (even if empty)
|
|
511
|
-
state.hasTwImport = true;
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
state.hasClassNames = true;
|
|
516
|
-
|
|
517
|
-
// Process the className with modifiers
|
|
518
|
-
processTwCall(
|
|
519
|
-
className,
|
|
520
|
-
path,
|
|
521
|
-
state,
|
|
522
|
-
parseClassName,
|
|
523
|
-
generateStyleKey,
|
|
524
|
-
splitModifierClasses,
|
|
525
|
-
findComponentScope,
|
|
526
|
-
t,
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
// Mark as successfully transformed
|
|
530
|
-
state.hasTwImport = true;
|
|
66
|
+
callExpressionVisitor(path, state, t);
|
|
531
67
|
},
|
|
532
68
|
|
|
533
69
|
JSXAttribute(path, state) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
// Ensure we have a JSXIdentifier name (not JSXNamespacedName)
|
|
537
|
-
if (!t.isJSXIdentifier(node.name)) {
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const attributeName = node.name.name;
|
|
542
|
-
|
|
543
|
-
// Only process configured className-like attributes
|
|
544
|
-
if (!isAttributeSupported(attributeName, state.supportedAttributes, state.attributePatterns)) {
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const value = node.value;
|
|
549
|
-
|
|
550
|
-
// Determine target style prop based on attribute name
|
|
551
|
-
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Process static className string (handles both direct StringLiteral and StringLiteral in JSXExpressionContainer)
|
|
555
|
-
*/
|
|
556
|
-
const processStaticClassName = (className: string): boolean => {
|
|
557
|
-
const trimmedClassName = className.trim();
|
|
558
|
-
|
|
559
|
-
// Skip empty classNames
|
|
560
|
-
if (!trimmedClassName) {
|
|
561
|
-
path.remove();
|
|
562
|
-
return true;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
state.hasClassNames = true;
|
|
566
|
-
|
|
567
|
-
// Check if className contains modifiers (active:, hover:, focus:, placeholder:, ios:, android:, web:, dark:, light:, scheme:)
|
|
568
|
-
const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(trimmedClassName);
|
|
569
|
-
|
|
570
|
-
// Expand scheme: modifiers into dark: and light: modifiers
|
|
571
|
-
const modifierClasses: ParsedModifier[] = [];
|
|
572
|
-
for (const modifier of rawModifierClasses) {
|
|
573
|
-
if (isSchemeModifier(modifier.modifier)) {
|
|
574
|
-
// Expand scheme: into dark: and light:
|
|
575
|
-
const expanded = expandSchemeModifier(
|
|
576
|
-
modifier,
|
|
577
|
-
state.customTheme.colors ?? {},
|
|
578
|
-
state.schemeModifierConfig.darkSuffix,
|
|
579
|
-
state.schemeModifierConfig.lightSuffix,
|
|
580
|
-
);
|
|
581
|
-
modifierClasses.push(...expanded);
|
|
582
|
-
} else {
|
|
583
|
-
// Keep other modifiers as-is
|
|
584
|
-
modifierClasses.push(modifier);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Separate modifiers by type
|
|
589
|
-
const placeholderModifiers = modifierClasses.filter((m) => m.modifier === "placeholder");
|
|
590
|
-
const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
|
|
591
|
-
const colorSchemeModifiers = modifierClasses.filter((m) => isColorSchemeModifier(m.modifier));
|
|
592
|
-
const stateModifiers = modifierClasses.filter(
|
|
593
|
-
(m) => isStateModifier(m.modifier) && m.modifier !== "placeholder",
|
|
594
|
-
);
|
|
595
|
-
|
|
596
|
-
// Handle placeholder modifiers first (they generate placeholderTextColor prop, not style)
|
|
597
|
-
if (placeholderModifiers.length > 0) {
|
|
598
|
-
// Check if this is a TextInput component (placeholder only works on TextInput)
|
|
599
|
-
const jsxOpeningElement = path.parent as BabelTypes.JSXOpeningElement;
|
|
600
|
-
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
601
|
-
|
|
602
|
-
if (componentSupport?.supportedModifiers.includes("placeholder")) {
|
|
603
|
-
const placeholderClasses = placeholderModifiers.map((m) => m.baseClass).join(" ");
|
|
604
|
-
const placeholderColor = parsePlaceholderClasses(placeholderClasses, state.customTheme.colors);
|
|
605
|
-
|
|
606
|
-
if (placeholderColor) {
|
|
607
|
-
// Add or merge placeholderTextColor prop
|
|
608
|
-
addOrMergePlaceholderTextColorProp(jsxOpeningElement, placeholderColor, t);
|
|
609
|
-
}
|
|
610
|
-
} else {
|
|
611
|
-
// Warn if placeholder modifier used on non-TextInput element
|
|
612
|
-
if (process.env.NODE_ENV !== "production") {
|
|
613
|
-
console.warn(
|
|
614
|
-
`[react-native-tailwind] placeholder: modifier can only be used on TextInput component at ${state.file.opts.filename ?? "unknown"}`,
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Handle combination of modifiers
|
|
621
|
-
const hasPlatformModifiers = platformModifiers.length > 0;
|
|
622
|
-
const hasColorSchemeModifiers = colorSchemeModifiers.length > 0;
|
|
623
|
-
const hasStateModifiers = stateModifiers.length > 0;
|
|
624
|
-
const hasBaseClasses = baseClasses.length > 0;
|
|
625
|
-
|
|
626
|
-
// If we have color scheme modifiers, we need to track the parent function component
|
|
627
|
-
let componentScope: NodePath<BabelTypes.Function> | null = null;
|
|
628
|
-
if (hasColorSchemeModifiers) {
|
|
629
|
-
componentScope = findComponentScope(path, t);
|
|
630
|
-
if (componentScope) {
|
|
631
|
-
state.functionComponentsNeedingColorScheme.add(componentScope);
|
|
632
|
-
} else {
|
|
633
|
-
// Warn if color scheme modifiers used in invalid context (class component, nested function)
|
|
634
|
-
if (process.env.NODE_ENV !== "production") {
|
|
635
|
-
console.warn(
|
|
636
|
-
`[react-native-tailwind] dark:/light: modifiers require a function component scope. ` +
|
|
637
|
-
`Found in non-component context at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
638
|
-
`These modifiers are not supported in class components or nested callbacks.`,
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// If we have multiple modifier types, combine them in an array expression
|
|
645
|
-
// For state modifiers, wrap in arrow function; for color scheme, they're just conditionals
|
|
646
|
-
if (hasStateModifiers && (hasPlatformModifiers || hasColorSchemeModifiers)) {
|
|
647
|
-
// Get the JSX opening element for component support checking
|
|
648
|
-
const jsxOpeningElement = path.parent;
|
|
649
|
-
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
650
|
-
|
|
651
|
-
if (componentSupport) {
|
|
652
|
-
// Build style array: [baseStyle, Platform.select(...), colorSchemeConditionals, stateConditionals]
|
|
653
|
-
const styleArrayElements: BabelTypes.Expression[] = [];
|
|
654
|
-
|
|
655
|
-
// Add base classes
|
|
656
|
-
if (hasBaseClasses) {
|
|
657
|
-
const baseClassName = baseClasses.join(" ");
|
|
658
|
-
const baseStyleObject = parseClassName(baseClassName, state.customTheme);
|
|
659
|
-
const baseStyleKey = generateStyleKey(baseClassName);
|
|
660
|
-
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
661
|
-
styleArrayElements.push(
|
|
662
|
-
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Add platform modifiers as Platform.select()
|
|
667
|
-
if (hasPlatformModifiers) {
|
|
668
|
-
const platformSelectExpression = processPlatformModifiers(
|
|
669
|
-
platformModifiers,
|
|
670
|
-
state,
|
|
671
|
-
parseClassName,
|
|
672
|
-
generateStyleKey,
|
|
673
|
-
t,
|
|
674
|
-
);
|
|
675
|
-
styleArrayElements.push(platformSelectExpression);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Add color scheme modifiers as conditionals (only if component scope exists)
|
|
679
|
-
if (hasColorSchemeModifiers && componentScope) {
|
|
680
|
-
const colorSchemeConditionals = processColorSchemeModifiers(
|
|
681
|
-
colorSchemeModifiers,
|
|
682
|
-
state,
|
|
683
|
-
parseClassName,
|
|
684
|
-
generateStyleKey,
|
|
685
|
-
t,
|
|
686
|
-
);
|
|
687
|
-
styleArrayElements.push(...colorSchemeConditionals);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// Add state modifiers as conditionals
|
|
691
|
-
// Group by modifier type
|
|
692
|
-
const modifiersByType = new Map<StateModifierType, ParsedModifier[]>();
|
|
693
|
-
for (const mod of stateModifiers) {
|
|
694
|
-
const modType = mod.modifier as StateModifierType;
|
|
695
|
-
if (!modifiersByType.has(modType)) {
|
|
696
|
-
modifiersByType.set(modType, []);
|
|
697
|
-
}
|
|
698
|
-
modifiersByType.get(modType)?.push(mod);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// Build conditionals for each state modifier type
|
|
702
|
-
for (const [modifierType, modifiers] of modifiersByType) {
|
|
703
|
-
if (!componentSupport.supportedModifiers.includes(modifierType)) {
|
|
704
|
-
continue; // Skip unsupported modifiers
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
708
|
-
const modifierStyleObject = parseClassName(modifierClassNames, state.customTheme);
|
|
709
|
-
const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
|
|
710
|
-
state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
|
|
711
|
-
|
|
712
|
-
const stateProperty = getStatePropertyForModifier(modifierType);
|
|
713
|
-
const conditionalExpression = t.logicalExpression(
|
|
714
|
-
"&&",
|
|
715
|
-
t.identifier(stateProperty),
|
|
716
|
-
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey)),
|
|
717
|
-
);
|
|
718
|
-
|
|
719
|
-
styleArrayElements.push(conditionalExpression);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Wrap in arrow function for state support
|
|
723
|
-
const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier))).filter((mod) =>
|
|
724
|
-
componentSupport.supportedModifiers.includes(mod),
|
|
725
|
-
);
|
|
726
|
-
const styleArrayExpression = t.arrayExpression(styleArrayElements);
|
|
727
|
-
const styleFunctionExpression = createStyleFunction(styleArrayExpression, usedModifiers, t);
|
|
728
|
-
|
|
729
|
-
const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
|
|
730
|
-
if (styleAttribute) {
|
|
731
|
-
mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
|
|
732
|
-
} else {
|
|
733
|
-
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
734
|
-
}
|
|
735
|
-
return true;
|
|
736
|
-
} else {
|
|
737
|
-
// Component doesn't support state modifiers, but we can still use platform modifiers
|
|
738
|
-
// Fall through to platform-only handling
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// Handle platform and/or color scheme modifiers (no state modifiers)
|
|
743
|
-
if ((hasPlatformModifiers || hasColorSchemeModifiers) && !hasStateModifiers) {
|
|
744
|
-
// Build style array/expression: [baseStyle, Platform.select(...), colorSchemeConditionals]
|
|
745
|
-
const styleExpressions: BabelTypes.Expression[] = [];
|
|
746
|
-
|
|
747
|
-
// Add base classes
|
|
748
|
-
if (hasBaseClasses) {
|
|
749
|
-
const baseClassName = baseClasses.join(" ");
|
|
750
|
-
const baseStyleObject = parseClassName(baseClassName, state.customTheme);
|
|
751
|
-
const baseStyleKey = generateStyleKey(baseClassName);
|
|
752
|
-
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
753
|
-
styleExpressions.push(
|
|
754
|
-
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
|
|
755
|
-
);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// Add platform modifiers as Platform.select()
|
|
759
|
-
if (hasPlatformModifiers) {
|
|
760
|
-
const platformSelectExpression = processPlatformModifiers(
|
|
761
|
-
platformModifiers,
|
|
762
|
-
state,
|
|
763
|
-
parseClassName,
|
|
764
|
-
generateStyleKey,
|
|
765
|
-
t,
|
|
766
|
-
);
|
|
767
|
-
styleExpressions.push(platformSelectExpression);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// Add color scheme modifiers as conditionals (only if we have a valid component scope)
|
|
771
|
-
if (hasColorSchemeModifiers && componentScope) {
|
|
772
|
-
const colorSchemeConditionals = processColorSchemeModifiers(
|
|
773
|
-
colorSchemeModifiers,
|
|
774
|
-
state,
|
|
775
|
-
parseClassName,
|
|
776
|
-
generateStyleKey,
|
|
777
|
-
t,
|
|
778
|
-
);
|
|
779
|
-
styleExpressions.push(...colorSchemeConditionals);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// Generate style attribute
|
|
783
|
-
const styleExpression =
|
|
784
|
-
styleExpressions.length === 1 ? styleExpressions[0] : t.arrayExpression(styleExpressions);
|
|
785
|
-
|
|
786
|
-
const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
|
|
787
|
-
if (styleAttribute) {
|
|
788
|
-
// Merge with existing style attribute
|
|
789
|
-
const existingStyle = styleAttribute.value;
|
|
790
|
-
if (
|
|
791
|
-
t.isJSXExpressionContainer(existingStyle) &&
|
|
792
|
-
!t.isJSXEmptyExpression(existingStyle.expression)
|
|
793
|
-
) {
|
|
794
|
-
const existing = existingStyle.expression;
|
|
795
|
-
// Merge as array: [ourStyles, existingStyles]
|
|
796
|
-
const mergedArray = t.isArrayExpression(existing)
|
|
797
|
-
? t.arrayExpression([styleExpression, ...existing.elements])
|
|
798
|
-
: t.arrayExpression([styleExpression, existing]);
|
|
799
|
-
styleAttribute.value = t.jsxExpressionContainer(mergedArray);
|
|
800
|
-
} else {
|
|
801
|
-
styleAttribute.value = t.jsxExpressionContainer(styleExpression);
|
|
802
|
-
}
|
|
803
|
-
path.remove();
|
|
804
|
-
} else {
|
|
805
|
-
// Replace className with style prop containing our expression
|
|
806
|
-
path.node.name = t.jsxIdentifier(targetStyleProp);
|
|
807
|
-
path.node.value = t.jsxExpressionContainer(styleExpression);
|
|
808
|
-
}
|
|
809
|
-
return true;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// If there are state modifiers (and no platform modifiers), check if this component supports them
|
|
813
|
-
if (hasStateModifiers) {
|
|
814
|
-
// Get the JSX opening element (the direct parent of the attribute)
|
|
815
|
-
const jsxOpeningElement = path.parent;
|
|
816
|
-
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
817
|
-
|
|
818
|
-
if (componentSupport) {
|
|
819
|
-
// Get modifier types used in className
|
|
820
|
-
const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier)));
|
|
821
|
-
|
|
822
|
-
// Check if all modifiers are supported by this component
|
|
823
|
-
const unsupportedModifiers = usedModifiers.filter(
|
|
824
|
-
(mod) => !componentSupport.supportedModifiers.includes(mod),
|
|
825
|
-
);
|
|
826
|
-
|
|
827
|
-
if (unsupportedModifiers.length > 0) {
|
|
828
|
-
// Warn about unsupported modifiers
|
|
829
|
-
if (process.env.NODE_ENV !== "production") {
|
|
830
|
-
console.warn(
|
|
831
|
-
`[react-native-tailwind] Modifiers (${unsupportedModifiers.map((m) => `${m}:`).join(", ")}) are not supported on ${componentSupport.component} component at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
832
|
-
`Supported modifiers: ${componentSupport.supportedModifiers.join(", ")}`,
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
// Filter out unsupported modifiers
|
|
836
|
-
const supportedModifierClasses = stateModifiers.filter((m) =>
|
|
837
|
-
componentSupport.supportedModifiers.includes(m.modifier),
|
|
838
|
-
);
|
|
839
|
-
|
|
840
|
-
// If no supported modifiers remain, fall through to normal processing
|
|
841
|
-
if (supportedModifierClasses.length === 0) {
|
|
842
|
-
// Continue to normal processing
|
|
843
|
-
} else {
|
|
844
|
-
// Process only supported modifiers
|
|
845
|
-
const filteredClassName =
|
|
846
|
-
baseClasses.join(" ") +
|
|
847
|
-
" " +
|
|
848
|
-
supportedModifierClasses.map((m) => `${m.modifier}:${m.baseClass}`).join(" ");
|
|
849
|
-
const styleExpression = processStaticClassNameWithModifiers(
|
|
850
|
-
filteredClassName.trim(),
|
|
851
|
-
state,
|
|
852
|
-
parseClassName,
|
|
853
|
-
generateStyleKey,
|
|
854
|
-
splitModifierClasses,
|
|
855
|
-
t,
|
|
856
|
-
);
|
|
857
|
-
const modifierTypes = Array.from(new Set(supportedModifierClasses.map((m) => m.modifier)));
|
|
858
|
-
const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
|
|
859
|
-
|
|
860
|
-
const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
|
|
861
|
-
|
|
862
|
-
if (styleAttribute) {
|
|
863
|
-
mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
|
|
864
|
-
} else {
|
|
865
|
-
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
866
|
-
}
|
|
867
|
-
return true;
|
|
868
|
-
}
|
|
869
|
-
} else {
|
|
870
|
-
// All modifiers are supported - process normally
|
|
871
|
-
const styleExpression = processStaticClassNameWithModifiers(
|
|
872
|
-
trimmedClassName,
|
|
873
|
-
state,
|
|
874
|
-
parseClassName,
|
|
875
|
-
generateStyleKey,
|
|
876
|
-
splitModifierClasses,
|
|
877
|
-
t,
|
|
878
|
-
);
|
|
879
|
-
const modifierTypes = usedModifiers;
|
|
880
|
-
const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
|
|
881
|
-
|
|
882
|
-
const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
|
|
883
|
-
|
|
884
|
-
if (styleAttribute) {
|
|
885
|
-
mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
|
|
886
|
-
} else {
|
|
887
|
-
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
888
|
-
}
|
|
889
|
-
return true;
|
|
890
|
-
}
|
|
891
|
-
} else {
|
|
892
|
-
// Component doesn't support any modifiers
|
|
893
|
-
if (process.env.NODE_ENV !== "production") {
|
|
894
|
-
const usedModifiers = Array.from(new Set(stateModifiers.map((m) => m.modifier)));
|
|
895
|
-
console.warn(
|
|
896
|
-
`[react-native-tailwind] Modifiers (${usedModifiers.map((m) => `${m}:`).join(", ")}) can only be used on compatible components (Pressable, TextInput). Found on unsupported element at ${state.file.opts.filename ?? "unknown"}`,
|
|
897
|
-
);
|
|
898
|
-
}
|
|
899
|
-
// Fall through to normal processing (ignore modifiers)
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// Normal processing without modifiers
|
|
904
|
-
// Use baseClasses only (placeholder modifiers already handled separately)
|
|
905
|
-
const classNameForStyle = baseClasses.join(" ");
|
|
906
|
-
if (!classNameForStyle) {
|
|
907
|
-
// No base classes, only had placeholder modifiers - just remove className
|
|
908
|
-
path.remove();
|
|
909
|
-
return true;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
const styleObject = parseClassName(classNameForStyle, state.customTheme);
|
|
913
|
-
const styleKey = generateStyleKey(classNameForStyle);
|
|
914
|
-
state.styleRegistry.set(styleKey, styleObject);
|
|
915
|
-
|
|
916
|
-
// Check if there's already a style prop on this element
|
|
917
|
-
const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
|
|
918
|
-
|
|
919
|
-
if (styleAttribute) {
|
|
920
|
-
// Merge with existing style prop
|
|
921
|
-
mergeStyleAttribute(path, styleAttribute, styleKey, state.stylesIdentifier, t);
|
|
922
|
-
} else {
|
|
923
|
-
// Replace className with style prop
|
|
924
|
-
replaceWithStyleAttribute(path, styleKey, targetStyleProp, state.stylesIdentifier, t);
|
|
925
|
-
}
|
|
926
|
-
return true;
|
|
927
|
-
};
|
|
928
|
-
|
|
929
|
-
// Handle static string literals
|
|
930
|
-
if (t.isStringLiteral(value)) {
|
|
931
|
-
if (processStaticClassName(value.value)) {
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
// Handle dynamic expressions (JSXExpressionContainer)
|
|
937
|
-
if (t.isJSXExpressionContainer(value)) {
|
|
938
|
-
const expression = value.expression;
|
|
939
|
-
|
|
940
|
-
// Skip JSXEmptyExpression
|
|
941
|
-
if (t.isJSXEmptyExpression(expression)) {
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// Fast path: Support string literals wrapped in JSXExpressionContainer: className={"flex-row"}
|
|
946
|
-
if (t.isStringLiteral(expression)) {
|
|
947
|
-
if (processStaticClassName(expression.value)) {
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
try {
|
|
953
|
-
// Find component scope for color scheme modifiers
|
|
954
|
-
const componentScope = findComponentScope(path, t);
|
|
955
|
-
|
|
956
|
-
// Process dynamic expression with modifier support
|
|
957
|
-
const result = processDynamicExpression(
|
|
958
|
-
expression,
|
|
959
|
-
state,
|
|
960
|
-
parseClassName,
|
|
961
|
-
generateStyleKey,
|
|
962
|
-
splitModifierClasses,
|
|
963
|
-
processPlatformModifiers,
|
|
964
|
-
processColorSchemeModifiers,
|
|
965
|
-
componentScope,
|
|
966
|
-
isPlatformModifier as (modifier: unknown) => boolean,
|
|
967
|
-
isColorSchemeModifier as (modifier: unknown) => boolean,
|
|
968
|
-
isSchemeModifier as (modifier: unknown) => boolean,
|
|
969
|
-
expandSchemeModifier,
|
|
970
|
-
t,
|
|
971
|
-
);
|
|
972
|
-
|
|
973
|
-
if (result) {
|
|
974
|
-
state.hasClassNames = true;
|
|
975
|
-
|
|
976
|
-
// Check if there's already a style prop on this element
|
|
977
|
-
const styleAttribute = findStyleAttribute(path, targetStyleProp, t);
|
|
978
|
-
|
|
979
|
-
if (styleAttribute) {
|
|
980
|
-
// Merge with existing style prop
|
|
981
|
-
mergeDynamicStyleAttribute(path, styleAttribute, result, t);
|
|
982
|
-
} else {
|
|
983
|
-
// Replace className with style prop
|
|
984
|
-
replaceDynamicWithStyleAttribute(path, result, targetStyleProp, t);
|
|
985
|
-
}
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
} catch (error) {
|
|
989
|
-
// Fall through to warning
|
|
990
|
-
if (process.env.NODE_ENV !== "production") {
|
|
991
|
-
console.warn(
|
|
992
|
-
`[react-native-tailwind] Failed to process dynamic ${attributeName} at ${state.file.opts.filename ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
993
|
-
);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// Unsupported dynamic className - warn in development
|
|
999
|
-
if (process.env.NODE_ENV !== "production") {
|
|
1000
|
-
const filename = state.file.opts.filename ?? "unknown";
|
|
1001
|
-
console.warn(
|
|
1002
|
-
`[react-native-tailwind] Dynamic ${attributeName} values are not fully supported at ${filename}. ` +
|
|
1003
|
-
`Use the ${targetStyleProp} prop for dynamic values.`,
|
|
1004
|
-
);
|
|
1005
|
-
}
|
|
70
|
+
jsxAttributeVisitor(path, state, t);
|
|
1006
71
|
},
|
|
1007
72
|
},
|
|
1008
73
|
};
|