@mgcrea/react-native-tailwind 0.2.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.
Files changed (42) hide show
  1. package/README.md +576 -0
  2. package/dist/babel/config-loader.d.ts +25 -0
  3. package/dist/babel/config-loader.ts +134 -0
  4. package/dist/babel/index.cjs +1111 -0
  5. package/dist/babel/index.d.ts +16 -0
  6. package/dist/babel/index.ts +286 -0
  7. package/dist/index.d.ts +12 -0
  8. package/dist/index.js +1 -0
  9. package/dist/parser/borders.d.ts +10 -0
  10. package/dist/parser/borders.js +1 -0
  11. package/dist/parser/colors.d.ts +9 -0
  12. package/dist/parser/colors.js +1 -0
  13. package/dist/parser/index.d.ts +25 -0
  14. package/dist/parser/index.js +1 -0
  15. package/dist/parser/layout.d.ts +8 -0
  16. package/dist/parser/layout.js +1 -0
  17. package/dist/parser/sizing.d.ts +10 -0
  18. package/dist/parser/sizing.js +1 -0
  19. package/dist/parser/spacing.d.ts +10 -0
  20. package/dist/parser/spacing.js +1 -0
  21. package/dist/parser/typography.d.ts +9 -0
  22. package/dist/parser/typography.js +1 -0
  23. package/dist/react-native.d.js +1 -0
  24. package/dist/react-native.d.ts +138 -0
  25. package/dist/types.d.ts +11 -0
  26. package/dist/types.js +1 -0
  27. package/dist/utils/styleKey.d.ts +9 -0
  28. package/dist/utils/styleKey.js +1 -0
  29. package/package.json +83 -0
  30. package/src/babel/config-loader.ts +134 -0
  31. package/src/babel/index.ts +286 -0
  32. package/src/index.ts +20 -0
  33. package/src/parser/borders.ts +198 -0
  34. package/src/parser/colors.ts +160 -0
  35. package/src/parser/index.ts +71 -0
  36. package/src/parser/layout.ts +114 -0
  37. package/src/parser/sizing.ts +239 -0
  38. package/src/parser/spacing.ts +222 -0
  39. package/src/parser/typography.ts +156 -0
  40. package/src/react-native.d.ts +138 -0
  41. package/src/types.ts +15 -0
  42. package/src/utils/styleKey.ts +23 -0
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@mgcrea/react-native-tailwind",
3
+ "version": "0.2.0",
4
+ "description": "Compile-time Tailwind CSS for React Native with zero runtime overhead",
5
+ "author": "Olivier Louvignes <olivier@mgcrea.io> (https://github.com/mgcrea)",
6
+ "homepage": "https://github.com/mgcrea/react-native-tailwind#readme",
7
+ "repository": "github:mgcrea/react-native-tailwind",
8
+ "license": "MIT",
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "require": "./dist/index.js"
17
+ },
18
+ "./babel": {
19
+ "import": "./dist/babel/index.cjs",
20
+ "require": "./dist/babel/index.cjs"
21
+ },
22
+ "./react-native": {
23
+ "types": "./dist/react-native.d.ts"
24
+ }
25
+ },
26
+ "files": [
27
+ "*.podspec",
28
+ "src",
29
+ "dist",
30
+ "ios"
31
+ ],
32
+ "scripts": {
33
+ "dev": "cd example; npm run dev",
34
+ "build": "npm run build:babel && npm run build:babel-plugin && npm run build:types",
35
+ "build:babel": "babel src --out-dir dist --extensions \".ts,.tsx,.js,.jsx\" --copy-files --ignore 'src/babel/**' --ignore '**/*.d.ts'",
36
+ "build:babel-plugin": "node scripts/bundle-babel-plugin.cjs",
37
+ "build:types": "tsc --emitDeclarationOnly && node scripts/post-build-types.cjs",
38
+ "install:ios": "cd example; npm run install:ios",
39
+ "open:ios": "cd example; npm run open:ios",
40
+ "lint": "eslint .",
41
+ "prettify": "prettier --write src/",
42
+ "check": "tsc --noEmit",
43
+ "spec": "NODE_OPTIONS=--experimental-require-module jest",
44
+ "test": "npm run lint && npm run check && npm run spec",
45
+ "prepare": "npm run build"
46
+ },
47
+ "peerDependencies": {
48
+ "react": "*",
49
+ "react-native": "*"
50
+ },
51
+ "devDependencies": {
52
+ "@babel/cli": "^7.28.3",
53
+ "@babel/core": "^7.28.5",
54
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
55
+ "@babel/preset-typescript": "^7.28.5",
56
+ "@babel/runtime": "^7.28.4",
57
+ "@babel/types": "^7.28.5",
58
+ "@mgcrea/eslint-config-react-native": "^0.14.0",
59
+ "@react-native-community/cli": "^20.0.2",
60
+ "@react-native/babel-preset": "0.82.1",
61
+ "@react-native/typescript-config": "0.82.1",
62
+ "@testing-library/react-native": "^13.3.3",
63
+ "@types/babel__core": "^7.20.5",
64
+ "@types/babel__traverse": "^7.28.0",
65
+ "@types/react": "^19.2.5",
66
+ "babel-plugin-module-resolver": "^5.0.2",
67
+ "esbuild": "^0.27.0",
68
+ "eslint": "^9.39.1",
69
+ "jest": "^30.2.0",
70
+ "prettier": "^3.6.2",
71
+ "prettier-plugin-organize-imports": "^4.3.0",
72
+ "react": "^19.2.0",
73
+ "react-native": "0.82.1",
74
+ "typescript": "^5.9.3"
75
+ },
76
+ "engines": {
77
+ "node": ">=18"
78
+ },
79
+ "packageManager": "pnpm@10.22.0",
80
+ "publishConfig": {
81
+ "access": "public"
82
+ }
83
+ }
@@ -0,0 +1,134 @@
1
+ /* eslint-disable @typescript-eslint/no-require-imports */
2
+ /* eslint-disable @typescript-eslint/no-dynamic-delete */
3
+ /**
4
+ * Tailwind config loader for Babel plugin
5
+ * Discovers and loads tailwind.config.* files from the project
6
+ */
7
+
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+
11
+ export type TailwindConfig = {
12
+ theme?: {
13
+ extend?: {
14
+ colors?: Record<string, string | Record<string, string>>;
15
+ };
16
+ colors?: Record<string, string | Record<string, string>>;
17
+ };
18
+ };
19
+
20
+ // Cache configs per path to avoid repeated file I/O
21
+ const configCache = new Map<string, TailwindConfig | null>();
22
+
23
+ /**
24
+ * Find tailwind.config.* file by traversing up from startDir
25
+ */
26
+ export function findTailwindConfig(startDir: string): string | null {
27
+ let currentDir = startDir;
28
+ const root = path.parse(currentDir).root;
29
+
30
+ const configNames = [
31
+ "tailwind.config.mjs",
32
+ "tailwind.config.js",
33
+ "tailwind.config.cjs",
34
+ "tailwind.config.ts",
35
+ ];
36
+
37
+ while (currentDir !== root) {
38
+ for (const configName of configNames) {
39
+ const configPath = path.join(currentDir, configName);
40
+ if (fs.existsSync(configPath)) {
41
+ return configPath;
42
+ }
43
+ }
44
+ currentDir = path.dirname(currentDir);
45
+ }
46
+
47
+ return null;
48
+ }
49
+
50
+ /**
51
+ * Load and parse tailwind config file
52
+ */
53
+ export function loadTailwindConfig(configPath: string): TailwindConfig | null {
54
+ // Check cache
55
+ if (configCache.has(configPath)) {
56
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
57
+ return configCache.get(configPath)!;
58
+ }
59
+
60
+ try {
61
+ // Clear require cache to allow hot reloading
62
+ const resolvedPath = require.resolve(configPath);
63
+ delete require.cache[resolvedPath];
64
+
65
+ // Load config
66
+ const config = require(configPath) as TailwindConfig | { default: TailwindConfig };
67
+
68
+ // Handle both default export and direct export
69
+ const resolved: TailwindConfig = "default" in config ? config.default : config;
70
+
71
+ configCache.set(configPath, resolved);
72
+ return resolved;
73
+ } catch (error) {
74
+ if (process.env.NODE_ENV !== "production") {
75
+ console.warn(`[react-native-tailwind] Failed to load config from ${configPath}:`, error);
76
+ }
77
+ configCache.set(configPath, null);
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Flatten nested color objects into dot notation
84
+ * Example: { brand: { light: '#fff', dark: '#000' } } -> { 'brand-light': '#fff', 'brand-dark': '#000' }
85
+ */
86
+ function flattenColors(
87
+ colors: Record<string, string | Record<string, string>>,
88
+ prefix = "",
89
+ ): Record<string, string> {
90
+ const result: Record<string, string> = {};
91
+
92
+ for (const [key, value] of Object.entries(colors)) {
93
+ const newKey = prefix ? `${prefix}-${key}` : key;
94
+
95
+ if (typeof value === "string") {
96
+ result[newKey] = value;
97
+ } else if (typeof value === "object" && value !== null) {
98
+ Object.assign(result, flattenColors(value, newKey));
99
+ }
100
+ }
101
+
102
+ return result;
103
+ }
104
+
105
+ /**
106
+ * Extract custom colors from tailwind config
107
+ * Prefers theme.extend.colors over theme.colors to avoid overriding defaults
108
+ */
109
+ export function extractCustomColors(filename: string): Record<string, string> {
110
+ const projectDir = path.dirname(filename);
111
+ const configPath = findTailwindConfig(projectDir);
112
+
113
+ if (!configPath) {
114
+ return {};
115
+ }
116
+
117
+ const config = loadTailwindConfig(configPath);
118
+ if (!config?.theme) {
119
+ return {};
120
+ }
121
+
122
+ // Warn if using theme.colors instead of theme.extend.colors
123
+ if (config.theme.colors && !config.theme.extend?.colors && process.env.NODE_ENV !== "production") {
124
+ console.warn(
125
+ "[react-native-tailwind] Using theme.colors will override all default colors. " +
126
+ "Use theme.extend.colors to add custom colors while keeping defaults.",
127
+ );
128
+ }
129
+
130
+ // Prefer theme.extend.colors
131
+ const colors = config.theme.extend?.colors ?? config.theme.colors ?? {};
132
+
133
+ return flattenColors(colors);
134
+ }
@@ -0,0 +1,286 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
4
+ /* eslint-disable @typescript-eslint/no-explicit-any */
5
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
6
+
7
+ /**
8
+ * Babel plugin for react-native-tailwind
9
+ * Transforms className props to style props at compile time
10
+ */
11
+
12
+ import type { NodePath, PluginObj, PluginPass } from "@babel/core";
13
+ import * as BabelTypes from "@babel/types";
14
+ import { parseClassName as parseClassNameFn } from "../parser/index.js";
15
+ import { generateStyleKey as generateStyleKeyFn } from "../utils/styleKey.js";
16
+ import { extractCustomColors } from "./config-loader.js";
17
+
18
+ type PluginState = PluginPass & {
19
+ styleRegistry: Map<string, Record<string, string | number>>;
20
+ hasClassNames: boolean;
21
+ hasStyleSheetImport: boolean;
22
+ customColors: Record<string, string>;
23
+ };
24
+
25
+ /**
26
+ * Supported className-like attributes
27
+ */
28
+ const SUPPORTED_CLASS_ATTRIBUTES = [
29
+ "className",
30
+ "contentContainerClassName",
31
+ "columnWrapperClassName",
32
+ "ListHeaderComponentClassName",
33
+ "ListFooterComponentClassName",
34
+ ] as const;
35
+
36
+ /**
37
+ * Get the target style prop name based on the className attribute
38
+ */
39
+ function getTargetStyleProp(attributeName: string): string {
40
+ if (attributeName === "contentContainerClassName") {
41
+ return "contentContainerStyle";
42
+ }
43
+ if (attributeName === "columnWrapperClassName") {
44
+ return "columnWrapperStyle";
45
+ }
46
+ if (attributeName === "ListHeaderComponentClassName") {
47
+ return "ListHeaderComponentStyle";
48
+ }
49
+ if (attributeName === "ListFooterComponentClassName") {
50
+ return "ListFooterComponentStyle";
51
+ }
52
+ return "style";
53
+ }
54
+
55
+ export default function reactNativeTailwindBabelPlugin({
56
+ types: t,
57
+ }: {
58
+ types: typeof BabelTypes;
59
+ }): PluginObj<PluginState> {
60
+ return {
61
+ name: "react-native-tailwind",
62
+
63
+ visitor: {
64
+ Program: {
65
+ enter(_path: NodePath, state: PluginState) {
66
+ // Initialize state for this file
67
+ state.styleRegistry = new Map();
68
+ state.hasClassNames = false;
69
+ state.hasStyleSheetImport = false;
70
+
71
+ // Load custom colors from tailwind.config.*
72
+ state.customColors = extractCustomColors(state.file.opts.filename ?? "");
73
+ },
74
+
75
+ exit(path: NodePath, state: PluginState) {
76
+ // If no classNames were found, skip StyleSheet generation
77
+ if (!state.hasClassNames || state.styleRegistry.size === 0) {
78
+ return;
79
+ }
80
+
81
+ // Add StyleSheet import if not already present
82
+ if (!state.hasStyleSheetImport) {
83
+ addStyleSheetImport(path, t);
84
+ }
85
+
86
+ // Generate and inject StyleSheet.create at the end of the file
87
+ injectStyles(path, state.styleRegistry, t);
88
+ },
89
+ },
90
+
91
+ // Check if StyleSheet is already imported
92
+ ImportDeclaration(path: NodePath, state: PluginState) {
93
+ const node = path.node as any;
94
+ if (node.source.value === "react-native") {
95
+ const specifiers = node.specifiers;
96
+ const hasStyleSheet = specifiers.some((spec: any) => {
97
+ if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
98
+ return spec.imported.name === "StyleSheet";
99
+ }
100
+ return false;
101
+ });
102
+
103
+ if (hasStyleSheet) {
104
+ state.hasStyleSheetImport = true;
105
+ } else {
106
+ // Add StyleSheet to existing import
107
+ node.specifiers.push(t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet")));
108
+ state.hasStyleSheetImport = true;
109
+ }
110
+ }
111
+ },
112
+
113
+ JSXAttribute(path: NodePath, state: PluginState) {
114
+ const node = path.node as any;
115
+ const attributeName = node.name.name;
116
+
117
+ // Only process className-like attributes
118
+ if (!SUPPORTED_CLASS_ATTRIBUTES.includes(attributeName)) {
119
+ return;
120
+ }
121
+
122
+ const value = node.value;
123
+
124
+ // Only handle static string literals
125
+ if (!t.isStringLiteral(value)) {
126
+ // Warn about dynamic className in development
127
+ if (process.env.NODE_ENV !== "production") {
128
+ const filename = state.file.opts.filename ?? "unknown";
129
+ const targetStyleProp = getTargetStyleProp(attributeName);
130
+ console.warn(
131
+ `[react-native-tailwind] Dynamic ${attributeName} values are not supported at ${filename}. ` +
132
+ `Use the ${targetStyleProp} prop for dynamic values.`,
133
+ );
134
+ }
135
+ return;
136
+ }
137
+
138
+ const className = value.value.trim();
139
+
140
+ // Skip empty classNames
141
+ if (!className) {
142
+ path.remove();
143
+ return;
144
+ }
145
+
146
+ state.hasClassNames = true;
147
+
148
+ // Parse className to React Native styles
149
+ const styleObject = parseClassName(className, state.customColors);
150
+
151
+ // Generate unique style key
152
+ const styleKey = generateStyleKey(className);
153
+
154
+ // Store in registry
155
+ state.styleRegistry.set(styleKey, styleObject);
156
+
157
+ // Determine target style prop based on attribute name
158
+ const targetStyleProp = getTargetStyleProp(attributeName);
159
+
160
+ // Check if there's already a style prop on this element
161
+ const parent = path.parent as any;
162
+ const styleAttribute = parent.attributes.find(
163
+ (attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
164
+ );
165
+
166
+ if (styleAttribute) {
167
+ // Merge with existing style prop
168
+ mergeStyleAttribute(path, styleAttribute, styleKey, t);
169
+ } else {
170
+ // Replace className with style prop
171
+ replaceWithStyleAttribute(path, styleKey, targetStyleProp, t);
172
+ }
173
+ },
174
+ },
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Add StyleSheet import to the file
180
+ */
181
+ function addStyleSheetImport(path: NodePath, t: typeof BabelTypes) {
182
+ const importDeclaration = t.importDeclaration(
183
+ [t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet"))],
184
+ t.stringLiteral("react-native"),
185
+ );
186
+
187
+ // Add import at the top of the file
188
+ (path as any).unshiftContainer("body", importDeclaration);
189
+ }
190
+
191
+ /**
192
+ * Replace className with style attribute
193
+ */
194
+ function replaceWithStyleAttribute(
195
+ classNamePath: NodePath,
196
+ styleKey: string,
197
+ targetStyleProp: string,
198
+ t: typeof BabelTypes,
199
+ ) {
200
+ const styleAttribute = t.jsxAttribute(
201
+ t.jsxIdentifier(targetStyleProp),
202
+ t.jsxExpressionContainer(t.memberExpression(t.identifier("styles"), t.identifier(styleKey))),
203
+ );
204
+
205
+ classNamePath.replaceWith(styleAttribute);
206
+ }
207
+
208
+ /**
209
+ * Merge className styles with existing style prop
210
+ */
211
+ function mergeStyleAttribute(
212
+ classNamePath: NodePath,
213
+ styleAttribute: any,
214
+ styleKey: string,
215
+ t: typeof BabelTypes,
216
+ ) {
217
+ const existingStyle = styleAttribute.value.expression;
218
+
219
+ // Create array with className styles first, then existing styles
220
+ // This allows existing styles to override className styles
221
+ const styleArray = t.arrayExpression([
222
+ t.memberExpression(t.identifier("styles"), t.identifier(styleKey)),
223
+ existingStyle,
224
+ ]);
225
+
226
+ styleAttribute.value = t.jsxExpressionContainer(styleArray);
227
+
228
+ // Remove the className attribute
229
+ classNamePath.remove();
230
+ }
231
+
232
+ /**
233
+ * Inject StyleSheet.create with all collected styles
234
+ */
235
+ function injectStyles(
236
+ path: NodePath,
237
+ styleRegistry: Map<string, Record<string, string | number>>,
238
+ t: typeof BabelTypes,
239
+ ) {
240
+ // Build style object properties
241
+ const styleProperties: any[] = [];
242
+
243
+ for (const [key, styleObject] of styleRegistry) {
244
+ const properties = Object.entries(styleObject).map(([styleProp, styleValue]) => {
245
+ let valueNode;
246
+
247
+ if (typeof styleValue === "number") {
248
+ valueNode = t.numericLiteral(styleValue);
249
+ } else if (typeof styleValue === "string") {
250
+ valueNode = t.stringLiteral(styleValue);
251
+ } else {
252
+ // Fallback for other types
253
+ valueNode = t.valueToNode(styleValue);
254
+ }
255
+
256
+ return t.objectProperty(t.identifier(styleProp), valueNode);
257
+ });
258
+
259
+ styleProperties.push(t.objectProperty(t.identifier(key), t.objectExpression(properties)));
260
+ }
261
+
262
+ // Create: const styles = StyleSheet.create({ ... })
263
+ const styleSheet = t.variableDeclaration("const", [
264
+ t.variableDeclarator(
265
+ t.identifier("styles"),
266
+ t.callExpression(t.memberExpression(t.identifier("StyleSheet"), t.identifier("create")), [
267
+ t.objectExpression(styleProperties),
268
+ ]),
269
+ ),
270
+ ]);
271
+
272
+ // Add StyleSheet.create at the end of the file
273
+ (path as any).pushContainer("body", styleSheet);
274
+ }
275
+
276
+ // Helper functions that use the imported parser
277
+ function parseClassName(
278
+ className: string,
279
+ customColors: Record<string, string>,
280
+ ): Record<string, string | number> {
281
+ return parseClassNameFn(className, customColors);
282
+ }
283
+
284
+ function generateStyleKey(className: string): string {
285
+ return generateStyleKeyFn(className);
286
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @mgcrea/react-native-tailwind
3
+ * Compile-time Tailwind CSS for React Native
4
+ */
5
+
6
+ // Main parser functions
7
+ export { parseClass, parseClassName } from "./parser";
8
+ export { generateStyleKey } from "./utils/styleKey";
9
+
10
+ // Re-export types
11
+ export type { RNStyle, StyleObject } from "./types";
12
+
13
+ // Re-export individual parsers for advanced usage
14
+ export { parseBorder, parseColor, parseLayout, parseSizing, parseSpacing, parseTypography } from "./parser";
15
+
16
+ // Re-export constants for customization
17
+ export { COLORS } from "./parser/colors";
18
+ export { SIZE_PERCENTAGES, SIZE_SCALE } from "./parser/sizing";
19
+ export { SPACING_SCALE } from "./parser/spacing";
20
+ export { FONT_SIZES } from "./parser/typography";