@kbach/native 0.1.8 → 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 +47 -30
- package/dist/index.d.mts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +7 -12
- package/dist/index.mjs +7 -12
- package/package.json +58 -58
- package/src/babel/index.js +30 -0
- package/src/babel-plugin/index.js +234 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @kbach/native
|
|
2
2
|
|
|
3
|
-
Tailwind-like utility classes for **React Native and Expo**. One package includes everything: the core engine, JSX runtime, Babel
|
|
3
|
+
Tailwind-like utility classes for **React Native and Expo**. One package includes everything: the core engine, JSX runtime, Babel preset for compile-time optimization, and Metro config helper.
|
|
4
4
|
|
|
5
|
-
```
|
|
5
|
+
```jsx
|
|
6
6
|
import { View, Text, TouchableOpacity } from 'react-native';
|
|
7
7
|
import { styled } from '@kbach/native';
|
|
8
8
|
|
|
@@ -31,20 +31,36 @@ export default function Screen() {
|
|
|
31
31
|
npm install @kbach/native
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Core engine, Babel preset, and Metro helpers are all bundled — nothing else to install.
|
|
35
35
|
|
|
36
36
|
## Setup
|
|
37
37
|
|
|
38
38
|
### 1. babel.config.js
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
One-liner using the helper:
|
|
41
|
+
|
|
42
|
+
```js
|
|
41
43
|
const { createKbachConfig } = require('@kbach/native');
|
|
42
44
|
module.exports = createKbachConfig();
|
|
43
45
|
```
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
Or written manually (identical to NativeWind's config shape):
|
|
46
48
|
|
|
49
|
+
```js
|
|
50
|
+
module.exports = function (api) {
|
|
51
|
+
api.cache(true);
|
|
52
|
+
return {
|
|
53
|
+
presets: [
|
|
54
|
+
['babel-preset-expo', { jsxImportSource: '@kbach/native' }],
|
|
55
|
+
'@kbach/native/babel',
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
};
|
|
47
59
|
```
|
|
60
|
+
|
|
61
|
+
If you need to keep an existing Babel config:
|
|
62
|
+
|
|
63
|
+
```js
|
|
48
64
|
const { withKbachBabel } = require('@kbach/native');
|
|
49
65
|
|
|
50
66
|
module.exports = withKbachBabel({
|
|
@@ -55,7 +71,7 @@ module.exports = withKbachBabel({
|
|
|
55
71
|
|
|
56
72
|
### 2. Wrap your app
|
|
57
73
|
|
|
58
|
-
```
|
|
74
|
+
```jsx
|
|
59
75
|
import { ThemeProvider } from '@kbach/native';
|
|
60
76
|
|
|
61
77
|
export default function App() {
|
|
@@ -79,7 +95,7 @@ npx expo start --clear
|
|
|
79
95
|
|
|
80
96
|
Works on any React Native component — View, Text, TouchableOpacity, TextInput, and third-party components.
|
|
81
97
|
|
|
82
|
-
```
|
|
98
|
+
```jsx
|
|
83
99
|
<View className="flex-1 bg-gray-2 dark:bg-gray-11 p-4" />
|
|
84
100
|
<Text className="text-lg font-bold text-gray-10 dark:text-white" />
|
|
85
101
|
<TouchableOpacity className="bg-blue-7 pressed:bg-blue-8 rounded-xl px-6 py-3" />
|
|
@@ -89,7 +105,7 @@ Works on any React Native component — View, Text, TouchableOpacity, TextInput,
|
|
|
89
105
|
|
|
90
106
|
Pre-style any component. Handles interaction states (pressed, focus, etc.) automatically.
|
|
91
107
|
|
|
92
|
-
```
|
|
108
|
+
```jsx
|
|
93
109
|
import { styled } from '@kbach/native';
|
|
94
110
|
import { View, Text, TouchableOpacity, TextInput } from 'react-native';
|
|
95
111
|
|
|
@@ -105,11 +121,11 @@ const Input = styled(
|
|
|
105
121
|
);
|
|
106
122
|
```
|
|
107
123
|
|
|
108
|
-
Pass extra classes at use time with the `
|
|
124
|
+
Pass extra classes at use time with the `kb` prop:
|
|
109
125
|
|
|
110
|
-
```
|
|
111
|
-
<Card
|
|
112
|
-
<Title
|
|
126
|
+
```jsx
|
|
127
|
+
<Card kb="mt-4 mb-2">
|
|
128
|
+
<Title kb="text-3xl">Hello</Title>
|
|
113
129
|
</Card>
|
|
114
130
|
```
|
|
115
131
|
|
|
@@ -117,7 +133,7 @@ Pass extra classes at use time with the `tw` prop:
|
|
|
117
133
|
|
|
118
134
|
Resolve classes to a style object inside a component. Use this when you need a value for the `style` prop.
|
|
119
135
|
|
|
120
|
-
```
|
|
136
|
+
```jsx
|
|
121
137
|
import { useStyles } from '@kbach/native';
|
|
122
138
|
import { View, Text } from 'react-native';
|
|
123
139
|
|
|
@@ -137,7 +153,7 @@ function Badge() {
|
|
|
137
153
|
|
|
138
154
|
Resolve classes outside a component, for use in StyleSheet.create() or static contexts.
|
|
139
155
|
|
|
140
|
-
```
|
|
156
|
+
```js
|
|
141
157
|
import { StyleSheet } from 'react-native';
|
|
142
158
|
import { tw } from '@kbach/native';
|
|
143
159
|
|
|
@@ -151,7 +167,7 @@ const styles = StyleSheet.create({
|
|
|
151
167
|
|
|
152
168
|
Conditionally join class strings. Falsy values are ignored.
|
|
153
169
|
|
|
154
|
-
```
|
|
170
|
+
```jsx
|
|
155
171
|
import { cx } from '@kbach/native';
|
|
156
172
|
|
|
157
173
|
<View className={cx(
|
|
@@ -163,7 +179,7 @@ import { cx } from '@kbach/native';
|
|
|
163
179
|
|
|
164
180
|
### useTheme()
|
|
165
181
|
|
|
166
|
-
```
|
|
182
|
+
```js
|
|
167
183
|
import { useTheme } from '@kbach/native';
|
|
168
184
|
|
|
169
185
|
const { mode, resolvedMode, isDark, setMode, toggle } = useTheme();
|
|
@@ -181,7 +197,7 @@ const { mode, resolvedMode, isDark, setMode, toggle } = useTheme();
|
|
|
181
197
|
|
|
182
198
|
Drop-in toggle component that works on both web and native.
|
|
183
199
|
|
|
184
|
-
```
|
|
200
|
+
```jsx
|
|
185
201
|
<ThemeToggle /> // button (default)
|
|
186
202
|
<ThemeToggle variant="switch" /> // toggle switch
|
|
187
203
|
<ThemeToggle variant="icon-button" /> // icon button
|
|
@@ -194,21 +210,22 @@ Up to 3 modifiers can be chained in any order.
|
|
|
194
210
|
|
|
195
211
|
| Modifier | Trigger |
|
|
196
212
|
|---|---|
|
|
197
|
-
| dark
|
|
198
|
-
| light
|
|
199
|
-
|
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
|
|
204
|
-
|
|
213
|
+
| `dark:` | Dark mode active |
|
|
214
|
+
| `light:` | Light mode active |
|
|
215
|
+
| `hover:` | Mouse hover |
|
|
216
|
+
| `pressed:` | Touch pressed |
|
|
217
|
+
| `focus:` | Element focused |
|
|
218
|
+
| `active:` | Element active |
|
|
219
|
+
| `disabled:` | Element disabled |
|
|
220
|
+
|
|
221
|
+
```jsx
|
|
205
222
|
<View className="dark:bg-gray-10" />
|
|
206
223
|
<TouchableOpacity className="dark:pressed:bg-indigo-8" />
|
|
207
224
|
```
|
|
208
225
|
|
|
209
226
|
## Arbitrary values
|
|
210
227
|
|
|
211
|
-
```
|
|
228
|
+
```jsx
|
|
212
229
|
<View className="bg-[#6366f1]" />
|
|
213
230
|
<View className="p-[14px]" />
|
|
214
231
|
<Text className="text-[18px]" />
|
|
@@ -218,14 +235,14 @@ Up to 3 modifiers can be chained in any order.
|
|
|
218
235
|
|
|
219
236
|
## Negative values
|
|
220
237
|
|
|
221
|
-
```
|
|
238
|
+
```jsx
|
|
222
239
|
<View className="-mt-4" /> // marginTop: -16
|
|
223
240
|
<View className="-mx-2" /> // marginHorizontal: -8
|
|
224
241
|
```
|
|
225
242
|
|
|
226
243
|
## Color with opacity
|
|
227
244
|
|
|
228
|
-
```
|
|
245
|
+
```jsx
|
|
229
246
|
<View className="bg-blue-6/50" /> // 50% opacity
|
|
230
247
|
<View className="bg-gray-10/75" /> // 75% opacity
|
|
231
248
|
```
|
|
@@ -257,7 +274,7 @@ Available families: slate, gray, zinc, neutral, stone, red, orange, amber, yello
|
|
|
257
274
|
|
|
258
275
|
## Configuration
|
|
259
276
|
|
|
260
|
-
```
|
|
277
|
+
```js
|
|
261
278
|
// kbach.config.js
|
|
262
279
|
module.exports = {
|
|
263
280
|
darkMode: 'attribute', // 'attribute' | 'class' | 'media'
|
|
@@ -288,7 +305,7 @@ module.exports = {
|
|
|
288
305
|
|
|
289
306
|
Apply at runtime:
|
|
290
307
|
|
|
291
|
-
```
|
|
308
|
+
```js
|
|
292
309
|
import { updateConfig } from '@kbach/native';
|
|
293
310
|
|
|
294
311
|
updateConfig({ extend: { theme: { colors: { brand: { 6: '#6366f1' } } } } });
|
package/dist/index.d.mts
CHANGED
|
@@ -10,7 +10,7 @@ export * from '@kbach/react';
|
|
|
10
10
|
interface KbachOptions {
|
|
11
11
|
/** Path to kbach.config.js, relative to project root. Default: 'kbach.config.js' */
|
|
12
12
|
configFile?: string;
|
|
13
|
-
/** JSX attribute names to transform at build time. Default: ['
|
|
13
|
+
/** JSX attribute names to transform at build time. Default: ['kb', 'className'] */
|
|
14
14
|
attributes?: string[];
|
|
15
15
|
/** Log transformed class strings to the Metro console. Default: false */
|
|
16
16
|
debug?: boolean;
|
|
@@ -36,7 +36,7 @@ interface KbachOptions {
|
|
|
36
36
|
*/
|
|
37
37
|
declare function withKbach(metroConfig: Record<string, any>, _options?: KbachOptions): Record<string, any>;
|
|
38
38
|
/**
|
|
39
|
-
* Add the Kbach
|
|
39
|
+
* Add the Kbach preset to an existing Babel config.
|
|
40
40
|
* Use this when you have a custom babel.config.js and want to keep it.
|
|
41
41
|
*
|
|
42
42
|
* babel.config.js:
|
|
@@ -57,6 +57,19 @@ declare function withKbachBabel(babelConfig: Record<string, any>, options?: Kbac
|
|
|
57
57
|
* const { createKbachConfig } = require('@kbach/native');
|
|
58
58
|
* module.exports = createKbachConfig();
|
|
59
59
|
* ```
|
|
60
|
+
*
|
|
61
|
+
* Or written manually (identical to NativeWind's config shape):
|
|
62
|
+
* ```js
|
|
63
|
+
* module.exports = function(api) {
|
|
64
|
+
* api.cache(true);
|
|
65
|
+
* return {
|
|
66
|
+
* presets: [
|
|
67
|
+
* ['babel-preset-expo', { jsxImportSource: '@kbach/native' }],
|
|
68
|
+
* '@kbach/native/babel',
|
|
69
|
+
* ],
|
|
70
|
+
* };
|
|
71
|
+
* };
|
|
72
|
+
* ```
|
|
60
73
|
*/
|
|
61
74
|
declare function createKbachConfig(options?: KbachOptions): Record<string, unknown>;
|
|
62
75
|
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export * from '@kbach/react';
|
|
|
10
10
|
interface KbachOptions {
|
|
11
11
|
/** Path to kbach.config.js, relative to project root. Default: 'kbach.config.js' */
|
|
12
12
|
configFile?: string;
|
|
13
|
-
/** JSX attribute names to transform at build time. Default: ['
|
|
13
|
+
/** JSX attribute names to transform at build time. Default: ['kb', 'className'] */
|
|
14
14
|
attributes?: string[];
|
|
15
15
|
/** Log transformed class strings to the Metro console. Default: false */
|
|
16
16
|
debug?: boolean;
|
|
@@ -36,7 +36,7 @@ interface KbachOptions {
|
|
|
36
36
|
*/
|
|
37
37
|
declare function withKbach(metroConfig: Record<string, any>, _options?: KbachOptions): Record<string, any>;
|
|
38
38
|
/**
|
|
39
|
-
* Add the Kbach
|
|
39
|
+
* Add the Kbach preset to an existing Babel config.
|
|
40
40
|
* Use this when you have a custom babel.config.js and want to keep it.
|
|
41
41
|
*
|
|
42
42
|
* babel.config.js:
|
|
@@ -57,6 +57,19 @@ declare function withKbachBabel(babelConfig: Record<string, any>, options?: Kbac
|
|
|
57
57
|
* const { createKbachConfig } = require('@kbach/native');
|
|
58
58
|
* module.exports = createKbachConfig();
|
|
59
59
|
* ```
|
|
60
|
+
*
|
|
61
|
+
* Or written manually (identical to NativeWind's config shape):
|
|
62
|
+
* ```js
|
|
63
|
+
* module.exports = function(api) {
|
|
64
|
+
* api.cache(true);
|
|
65
|
+
* return {
|
|
66
|
+
* presets: [
|
|
67
|
+
* ['babel-preset-expo', { jsxImportSource: '@kbach/native' }],
|
|
68
|
+
* '@kbach/native/babel',
|
|
69
|
+
* ],
|
|
70
|
+
* };
|
|
71
|
+
* };
|
|
72
|
+
* ```
|
|
60
73
|
*/
|
|
61
74
|
declare function createKbachConfig(options?: KbachOptions): Record<string, unknown>;
|
|
62
75
|
|
package/dist/index.js
CHANGED
|
@@ -35,34 +35,29 @@ function withKbach(metroConfig, _options = {}) {
|
|
|
35
35
|
function withKbachBabel(babelConfig, options = {}) {
|
|
36
36
|
const {
|
|
37
37
|
configFile = "kbach.config.js",
|
|
38
|
-
attributes = ["
|
|
38
|
+
attributes = ["kb", "className"],
|
|
39
39
|
debug = false
|
|
40
40
|
} = options;
|
|
41
|
-
const pluginEntry = ["babel-plugin-kbach", { configFile, attributes, debug, jsxRuntime: false }];
|
|
42
41
|
const presets = (babelConfig.presets ?? []).map((preset) => {
|
|
43
42
|
const [name, presetOpts = {}] = Array.isArray(preset) ? preset : [preset, {}];
|
|
44
|
-
if (name === "
|
|
43
|
+
if (typeof name === "string" && (name.includes("babel-preset-expo") || name.includes("preset-react"))) {
|
|
45
44
|
return [name, { ...presetOpts, jsxImportSource: "@kbach/native" }];
|
|
46
45
|
}
|
|
47
46
|
return preset;
|
|
48
47
|
});
|
|
49
48
|
return {
|
|
50
49
|
...babelConfig,
|
|
51
|
-
presets,
|
|
52
|
-
plugins: [pluginEntry, ...babelConfig.plugins ?? []]
|
|
50
|
+
presets: [...presets, ["@kbach/native/babel", { configFile, attributes, debug }]]
|
|
53
51
|
};
|
|
54
52
|
}
|
|
55
53
|
function createKbachConfig(options = {}) {
|
|
56
54
|
return {
|
|
57
55
|
presets: [
|
|
58
|
-
["babel-preset-expo", { jsxImportSource: "@kbach/native" }]
|
|
59
|
-
|
|
60
|
-
plugins: [
|
|
61
|
-
["babel-plugin-kbach", {
|
|
56
|
+
["babel-preset-expo", { jsxImportSource: "@kbach/native" }],
|
|
57
|
+
["@kbach/native/babel", {
|
|
62
58
|
configFile: options.configFile ?? "kbach.config.js",
|
|
63
|
-
attributes: options.attributes ?? ["
|
|
64
|
-
debug: options.debug ?? false
|
|
65
|
-
jsxRuntime: false
|
|
59
|
+
attributes: options.attributes ?? ["kb", "className"],
|
|
60
|
+
debug: options.debug ?? false
|
|
66
61
|
}]
|
|
67
62
|
]
|
|
68
63
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -8,34 +8,29 @@ function withKbach(metroConfig, _options = {}) {
|
|
|
8
8
|
function withKbachBabel(babelConfig, options = {}) {
|
|
9
9
|
const {
|
|
10
10
|
configFile = "kbach.config.js",
|
|
11
|
-
attributes = ["
|
|
11
|
+
attributes = ["kb", "className"],
|
|
12
12
|
debug = false
|
|
13
13
|
} = options;
|
|
14
|
-
const pluginEntry = ["babel-plugin-kbach", { configFile, attributes, debug, jsxRuntime: false }];
|
|
15
14
|
const presets = (babelConfig.presets ?? []).map((preset) => {
|
|
16
15
|
const [name, presetOpts = {}] = Array.isArray(preset) ? preset : [preset, {}];
|
|
17
|
-
if (name === "
|
|
16
|
+
if (typeof name === "string" && (name.includes("babel-preset-expo") || name.includes("preset-react"))) {
|
|
18
17
|
return [name, { ...presetOpts, jsxImportSource: "@kbach/native" }];
|
|
19
18
|
}
|
|
20
19
|
return preset;
|
|
21
20
|
});
|
|
22
21
|
return {
|
|
23
22
|
...babelConfig,
|
|
24
|
-
presets,
|
|
25
|
-
plugins: [pluginEntry, ...babelConfig.plugins ?? []]
|
|
23
|
+
presets: [...presets, ["@kbach/native/babel", { configFile, attributes, debug }]]
|
|
26
24
|
};
|
|
27
25
|
}
|
|
28
26
|
function createKbachConfig(options = {}) {
|
|
29
27
|
return {
|
|
30
28
|
presets: [
|
|
31
|
-
["babel-preset-expo", { jsxImportSource: "@kbach/native" }]
|
|
32
|
-
|
|
33
|
-
plugins: [
|
|
34
|
-
["babel-plugin-kbach", {
|
|
29
|
+
["babel-preset-expo", { jsxImportSource: "@kbach/native" }],
|
|
30
|
+
["@kbach/native/babel", {
|
|
35
31
|
configFile: options.configFile ?? "kbach.config.js",
|
|
36
|
-
attributes: options.attributes ?? ["
|
|
37
|
-
debug: options.debug ?? false
|
|
38
|
-
jsxRuntime: false
|
|
32
|
+
attributes: options.attributes ?? ["kb", "className"],
|
|
33
|
+
debug: options.debug ?? false
|
|
39
34
|
}]
|
|
40
35
|
]
|
|
41
36
|
};
|
package/package.json
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@kbach/native",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Single-package Kbach install for React Native and Expo — includes core, components, Babel plugin, and Metro config",
|
|
5
|
-
"source": "./src/index.ts",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"module": "./dist/index.mjs",
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"types": "./dist/index.d.ts",
|
|
12
|
-
"source": "./src/index.ts",
|
|
13
|
-
"import": "./dist/index.mjs",
|
|
14
|
-
"require": "./dist/index.js"
|
|
15
|
-
},
|
|
16
|
-
"./jsx-runtime": {
|
|
17
|
-
"types": "./dist/jsx-runtime.d.ts",
|
|
18
|
-
"source": "./src/jsx-runtime.ts",
|
|
19
|
-
"import": "./dist/jsx-runtime.mjs",
|
|
20
|
-
"require": "./dist/jsx-runtime.js"
|
|
21
|
-
},
|
|
22
|
-
"./jsx-dev-runtime": {
|
|
23
|
-
"types": "./dist/jsx-dev-runtime.d.ts",
|
|
24
|
-
"source": "./src/jsx-dev-runtime.ts",
|
|
25
|
-
"import": "./dist/jsx-dev-runtime.mjs",
|
|
26
|
-
"require": "./dist/jsx-dev-runtime.js"
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
},
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@types/react": "^19.2.0",
|
|
42
|
-
"tsup": "^8.0.0"
|
|
43
|
-
},
|
|
44
|
-
"peerDependencies": {
|
|
45
|
-
"@babel/core": "^7.0.0",
|
|
46
|
-
"react": ">=17.0.0",
|
|
47
|
-
"react-native": ">=0.70.0"
|
|
48
|
-
},
|
|
49
|
-
"peerDependenciesMeta": {
|
|
50
|
-
"@babel/core": { "optional": true },
|
|
51
|
-
"react-native": { "optional": true }
|
|
52
|
-
},
|
|
53
|
-
"publishConfig": {
|
|
54
|
-
"access": "public"
|
|
55
|
-
},
|
|
56
|
-
"files": ["dist"],
|
|
57
|
-
"keywords": ["react-native", "expo", "tailwind", "css-in-js", "styling", "
|
|
58
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@kbach/native",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Single-package Kbach install for React Native and Expo — includes core, components, Babel plugin, and Metro config",
|
|
5
|
+
"source": "./src/index.ts",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"source": "./src/index.ts",
|
|
13
|
+
"import": "./dist/index.mjs",
|
|
14
|
+
"require": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./jsx-runtime": {
|
|
17
|
+
"types": "./dist/jsx-runtime.d.ts",
|
|
18
|
+
"source": "./src/jsx-runtime.ts",
|
|
19
|
+
"import": "./dist/jsx-runtime.mjs",
|
|
20
|
+
"require": "./dist/jsx-runtime.js"
|
|
21
|
+
},
|
|
22
|
+
"./jsx-dev-runtime": {
|
|
23
|
+
"types": "./dist/jsx-dev-runtime.d.ts",
|
|
24
|
+
"source": "./src/jsx-dev-runtime.ts",
|
|
25
|
+
"import": "./dist/jsx-dev-runtime.mjs",
|
|
26
|
+
"require": "./dist/jsx-dev-runtime.js"
|
|
27
|
+
},
|
|
28
|
+
"./babel-plugin": "./src/babel-plugin/index.js",
|
|
29
|
+
"./babel": "./src/babel/index.js"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup src/index.ts src/jsx-runtime.ts src/jsx-dev-runtime.ts --format esm,cjs --dts --clean",
|
|
33
|
+
"dev": "tsup src/index.ts src/jsx-runtime.ts src/jsx-dev-runtime.ts --format esm,cjs --dts --watch",
|
|
34
|
+
"test": "jest --passWithNoTests",
|
|
35
|
+
"lint": "tsc --noEmit"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@kbach/react": "0.2.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/react": "^19.2.0",
|
|
42
|
+
"tsup": "^8.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@babel/core": "^7.0.0",
|
|
46
|
+
"react": ">=17.0.0",
|
|
47
|
+
"react-native": ">=0.70.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@babel/core": { "optional": true },
|
|
51
|
+
"react-native": { "optional": true }
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"files": ["dist", "src/babel-plugin", "src/babel"],
|
|
57
|
+
"keywords": ["react-native", "expo", "tailwind", "css-in-js", "styling", "framework", "frontend", "mobile"]
|
|
58
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @kbach/native/babel — Babel preset (NativeWind-style).
|
|
5
|
+
*
|
|
6
|
+
* babel.config.js:
|
|
7
|
+
* ```js
|
|
8
|
+
* module.exports = function(api) {
|
|
9
|
+
* api.cache(true);
|
|
10
|
+
* return {
|
|
11
|
+
* presets: [
|
|
12
|
+
* ['babel-preset-expo', { jsxImportSource: '@kbach/native' }],
|
|
13
|
+
* '@kbach/native/babel',
|
|
14
|
+
* ],
|
|
15
|
+
* };
|
|
16
|
+
* };
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Presets run in reverse order, so this preset's plugin runs BEFORE
|
|
20
|
+
* babel-preset-expo's JSX transform — which is the correct ordering:
|
|
21
|
+
* kbach renames className/tw attributes first, then the JSX transform
|
|
22
|
+
* compiles JSX using @kbach/native/jsx-runtime.
|
|
23
|
+
*/
|
|
24
|
+
module.exports = function kbachBabelPreset(api, options = {}) {
|
|
25
|
+
return {
|
|
26
|
+
plugins: [
|
|
27
|
+
[require('../babel-plugin'), options],
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
// ─── Cross-file resolve cache ─────────────────────────────────────────────────
|
|
7
|
+
// Keyed by classString. Config is process-global so one entry covers all files.
|
|
8
|
+
const _resolveCache = new Map();
|
|
9
|
+
|
|
10
|
+
// ─── Load core lazily, reloading when the dist changes ───────────────────────
|
|
11
|
+
let _core = null;
|
|
12
|
+
let _coreMtime = 0;
|
|
13
|
+
let _corePath = null;
|
|
14
|
+
let _lastStatMs = 0;
|
|
15
|
+
const STAT_INTERVAL_MS = 500;
|
|
16
|
+
|
|
17
|
+
function getCore() {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
if (_core && (now - _lastStatMs) < STAT_INTERVAL_MS) return _core;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (!_corePath) _corePath = require.resolve('@kbach/react');
|
|
23
|
+
const mtime = fs.statSync(_corePath).mtimeMs;
|
|
24
|
+
_lastStatMs = now;
|
|
25
|
+
if (_core && mtime === _coreMtime) return _core;
|
|
26
|
+
for (const id of Object.keys(require.cache)) {
|
|
27
|
+
if (id.includes(`${path.sep}@kbach${path.sep}react`) || id.includes(`${path.sep}packages${path.sep}react${path.sep}`)) {
|
|
28
|
+
delete require.cache[id];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
_core = require('@kbach/react');
|
|
32
|
+
_coreMtime = mtime;
|
|
33
|
+
_config = null;
|
|
34
|
+
_resolveCache.clear(); // config changed — invalidate resolve cache too
|
|
35
|
+
} catch {
|
|
36
|
+
if (!_core) _core = require('@kbach/react');
|
|
37
|
+
}
|
|
38
|
+
return _core;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Load user config ─────────────────────────────────────────────────────────
|
|
42
|
+
let _config = null;
|
|
43
|
+
function getUserConfig(configFile) {
|
|
44
|
+
if (_config) return _config;
|
|
45
|
+
try {
|
|
46
|
+
const cfgPath = path.resolve(process.cwd(), configFile);
|
|
47
|
+
// eslint-disable-next-line import/no-dynamic-require
|
|
48
|
+
const userCfg = require(cfgPath);
|
|
49
|
+
const { buildConfig } = getCore();
|
|
50
|
+
_config = buildConfig(userCfg);
|
|
51
|
+
} catch {
|
|
52
|
+
const { getConfig } = getCore();
|
|
53
|
+
_config = getConfig();
|
|
54
|
+
}
|
|
55
|
+
return _config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Convert a StyleValue to a Babel AST ObjectExpression ────────────────────
|
|
59
|
+
function styleToAST(t, styles) {
|
|
60
|
+
if (!styles || typeof styles !== 'object') return t.nullLiteral();
|
|
61
|
+
|
|
62
|
+
const props = [];
|
|
63
|
+
for (const [key, val] of Object.entries(styles)) {
|
|
64
|
+
if (val === undefined || val === null) continue;
|
|
65
|
+
|
|
66
|
+
let valueNode;
|
|
67
|
+
if (typeof val === 'number') {
|
|
68
|
+
valueNode = val < 0 ? t.unaryExpression('-', t.numericLiteral(-val)) : t.numericLiteral(val);
|
|
69
|
+
} else if (typeof val === 'string') {
|
|
70
|
+
valueNode = t.stringLiteral(val);
|
|
71
|
+
} else if (typeof val === 'object' && !Array.isArray(val)) {
|
|
72
|
+
valueNode = styleToAST(t, val);
|
|
73
|
+
} else if (Array.isArray(val)) {
|
|
74
|
+
valueNode = t.arrayExpression(val.map(item =>
|
|
75
|
+
typeof item === 'object' ? styleToAST(t, item) : t.stringLiteral(String(item)),
|
|
76
|
+
));
|
|
77
|
+
} else {
|
|
78
|
+
valueNode = t.stringLiteral(String(val));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
props.push(t.objectProperty(t.stringLiteral(key), valueNode));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return t.objectExpression(props);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Convert a ResolvedStyle to a Babel AST ObjectExpression ─────────────────
|
|
88
|
+
function resolvedStyleToAST(t, resolved) {
|
|
89
|
+
const props = [];
|
|
90
|
+
for (const [bucketKey, styles] of Object.entries(resolved)) {
|
|
91
|
+
if (!styles || Object.keys(styles).length === 0) continue;
|
|
92
|
+
props.push(
|
|
93
|
+
t.objectProperty(t.stringLiteral(bucketKey), styleToAST(t, styles)),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return t.objectExpression(props);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Plugin ───────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
module.exports = function kbachBabelPlugin(api, options = {}) {
|
|
102
|
+
const { types: t } = api;
|
|
103
|
+
|
|
104
|
+
const {
|
|
105
|
+
configFile = 'kbach.config.js',
|
|
106
|
+
attributes = ['kb', 'className'],
|
|
107
|
+
debug = false,
|
|
108
|
+
} = options;
|
|
109
|
+
|
|
110
|
+
const SEP = path.sep;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
name: 'babel-plugin-kbach',
|
|
114
|
+
|
|
115
|
+
// JSX runtime setup: pre() injects a @jsxImportSource comment so that
|
|
116
|
+
// @babel/plugin-transform-react-jsx (from babel-preset-expo or any React preset)
|
|
117
|
+
// uses @kbach/native/jsx-runtime. Our pre() runs before the preset's pre(), so the
|
|
118
|
+
// comment is in place when the JSX transform reads it.
|
|
119
|
+
//
|
|
120
|
+
// NOTE: Do NOT add plugins dynamically inside manipulateOptions. By the time
|
|
121
|
+
// manipulateOptions runs, opts.presets has already been resolved from strings to
|
|
122
|
+
// functions — preset detection by name is impossible — and pushing a new entry into
|
|
123
|
+
// opts.plugins at that stage produces an uninstantiated plugin with visitor: undefined,
|
|
124
|
+
// which crashes @babel/traverse's visitors.merge().
|
|
125
|
+
|
|
126
|
+
pre(file) {
|
|
127
|
+
// Skip node_modules — they have their own JSX runtime configuration
|
|
128
|
+
const filename = file.opts.filename || '';
|
|
129
|
+
if (filename.includes(`${SEP}node_modules${SEP}`) && !filename.includes('kbach')) return;
|
|
130
|
+
|
|
131
|
+
// Inject @jsxImportSource so @babel/plugin-transform-react-jsx (from a React preset)
|
|
132
|
+
// uses our runtime. This pre() runs before the preset's pre() calls.
|
|
133
|
+
const comments = file.ast.comments;
|
|
134
|
+
if (!Array.isArray(comments)) return;
|
|
135
|
+
const alreadySet = comments.some(c => /@jsxImportSource|@jsxRuntime/.test(c.value));
|
|
136
|
+
if (!alreadySet) {
|
|
137
|
+
comments.unshift({ type: 'CommentLine', value: ' @jsxImportSource @kbach/native' });
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
visitor: {
|
|
142
|
+
Program: {
|
|
143
|
+
enter(programPath, state) {
|
|
144
|
+
state.kbachDeclarations = new Map();
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
exit(programPath, state) {
|
|
148
|
+
if (!state.kbachDeclarations || !state.kbachDeclarations.size) return;
|
|
149
|
+
|
|
150
|
+
const body = programPath.get('body');
|
|
151
|
+
const imports = body.filter(p => p.isImportDeclaration());
|
|
152
|
+
const insertAfterPath = imports.length > 0 ? imports[imports.length - 1] : null;
|
|
153
|
+
|
|
154
|
+
const entries = [...state.kbachDeclarations.values()];
|
|
155
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
156
|
+
const { uid, astNode } = entries[i];
|
|
157
|
+
const decl = t.variableDeclaration('const', [t.variableDeclarator(uid, astNode)]);
|
|
158
|
+
if (insertAfterPath) {
|
|
159
|
+
insertAfterPath.insertAfter(decl);
|
|
160
|
+
} else {
|
|
161
|
+
programPath.unshiftContainer('body', decl);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
JSXAttribute(nodePath, state) {
|
|
168
|
+
// Skip third-party node_modules — they won't have kbach className attrs
|
|
169
|
+
const filename = state.file.opts.filename || '';
|
|
170
|
+
if (
|
|
171
|
+
filename.includes(`${SEP}node_modules${SEP}`) &&
|
|
172
|
+
!filename.includes('kbach')
|
|
173
|
+
) return;
|
|
174
|
+
|
|
175
|
+
const attrName = nodePath.node.name;
|
|
176
|
+
const name = t.isJSXIdentifier(attrName) ? attrName.name : null;
|
|
177
|
+
|
|
178
|
+
if (!name || !attributes.includes(name)) return;
|
|
179
|
+
|
|
180
|
+
const value = nodePath.node.value;
|
|
181
|
+
|
|
182
|
+
if (!t.isStringLiteral(value) && !(t.isJSXExpressionContainer(value) && t.isStringLiteral(value.expression))) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const classString = t.isStringLiteral(value)
|
|
187
|
+
? value.value
|
|
188
|
+
: value.expression.value;
|
|
189
|
+
|
|
190
|
+
if (!classString || !classString.trim()) return;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// Use global cache to avoid re-resolving the same class string
|
|
194
|
+
// across different files in the same build.
|
|
195
|
+
let resolved;
|
|
196
|
+
if (_resolveCache.has(classString)) {
|
|
197
|
+
resolved = _resolveCache.get(classString);
|
|
198
|
+
} else {
|
|
199
|
+
const { resolve } = getCore();
|
|
200
|
+
const config = getUserConfig(configFile);
|
|
201
|
+
resolved = resolve(classString, config.theme, config.darkMode);
|
|
202
|
+
_resolveCache.set(classString, resolved);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (debug) {
|
|
206
|
+
console.log(`[Kbach] Transformed: "${classString}"`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let uid;
|
|
210
|
+
if (state.kbachDeclarations.has(classString)) {
|
|
211
|
+
uid = state.kbachDeclarations.get(classString).uid;
|
|
212
|
+
} else {
|
|
213
|
+
const astNode = resolvedStyleToAST(t, resolved);
|
|
214
|
+
uid = nodePath.scope.getProgramParent().generateUidIdentifier('kbach');
|
|
215
|
+
state.kbachDeclarations.set(classString, { uid, astNode });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
nodePath.insertAfter(
|
|
219
|
+
t.jSXAttribute(
|
|
220
|
+
t.jSXIdentifier('__kbachStyles'),
|
|
221
|
+
t.jSXExpressionContainer(t.identifier(uid.name)),
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
nodePath.node.name = t.jSXIdentifier('__kbachClasses');
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if (debug) {
|
|
228
|
+
console.warn(`[Kbach] Could not transform "${classString}":`, err.message);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
};
|