@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.
- package/README.md +576 -0
- package/dist/babel/config-loader.d.ts +25 -0
- package/dist/babel/config-loader.ts +134 -0
- package/dist/babel/index.cjs +1111 -0
- package/dist/babel/index.d.ts +16 -0
- package/dist/babel/index.ts +286 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1 -0
- package/dist/parser/borders.d.ts +10 -0
- package/dist/parser/borders.js +1 -0
- package/dist/parser/colors.d.ts +9 -0
- package/dist/parser/colors.js +1 -0
- package/dist/parser/index.d.ts +25 -0
- package/dist/parser/index.js +1 -0
- package/dist/parser/layout.d.ts +8 -0
- package/dist/parser/layout.js +1 -0
- package/dist/parser/sizing.d.ts +10 -0
- package/dist/parser/sizing.js +1 -0
- package/dist/parser/spacing.d.ts +10 -0
- package/dist/parser/spacing.js +1 -0
- package/dist/parser/typography.d.ts +9 -0
- package/dist/parser/typography.js +1 -0
- package/dist/react-native.d.js +1 -0
- package/dist/react-native.d.ts +138 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +1 -0
- package/dist/utils/styleKey.d.ts +9 -0
- package/dist/utils/styleKey.js +1 -0
- package/package.json +83 -0
- package/src/babel/config-loader.ts +134 -0
- package/src/babel/index.ts +286 -0
- package/src/index.ts +20 -0
- package/src/parser/borders.ts +198 -0
- package/src/parser/colors.ts +160 -0
- package/src/parser/index.ts +71 -0
- package/src/parser/layout.ts +114 -0
- package/src/parser/sizing.ts +239 -0
- package/src/parser/spacing.ts +222 -0
- package/src/parser/typography.ts +156 -0
- package/src/react-native.d.ts +138 -0
- package/src/types.ts +15 -0
- 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";
|