@kbach/native 0.1.8 → 0.1.9

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 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 plugin for compile-time optimization, and Metro config helper.
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
- That's it. Core engine, Babel plugin, and Metro helpers are bundled — nothing else to install.
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
- If you need to keep an existing Babel config:
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 `tw` prop:
124
+ Pass extra classes at use time with the `kb` prop:
109
125
 
110
- ```
111
- <Card tw="mt-4 mb-2">
112
- <Title tw="text-3xl">Hello</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: | Dark mode active |
198
- | light: | Light mode active |
199
- | pressed: | Touch pressed |
200
- | focus: | Element focused |
201
- | active: | Element active |
202
- | disabled: | Element disabled |
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: ['tw', 'className'] */
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 plugin to an existing Babel config.
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: ['tw', 'className'] */
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 plugin to an existing Babel config.
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 = ["tw", "className"],
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 === "babel-preset-expo" || name === "@babel/preset-react" || typeof name === "string" && name.includes("react")) {
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 ?? ["tw", "className"],
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 = ["tw", "className"],
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 === "babel-preset-expo" || name === "@babel/preset-react" || typeof name === "string" && name.includes("react")) {
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 ?? ["tw", "className"],
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/native",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Single-package Kbach install for React Native and Expo — includes core, components, Babel plugin, and Metro config",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./dist/index.js",
@@ -24,7 +24,9 @@
24
24
  "source": "./src/jsx-dev-runtime.ts",
25
25
  "import": "./dist/jsx-dev-runtime.mjs",
26
26
  "require": "./dist/jsx-dev-runtime.js"
27
- }
27
+ },
28
+ "./babel-plugin": "./src/babel-plugin/index.js",
29
+ "./babel": "./src/babel/index.js"
28
30
  },
29
31
  "scripts": {
30
32
  "build": "tsup src/index.ts src/jsx-runtime.ts src/jsx-dev-runtime.ts --format esm,cjs --dts --clean",
@@ -33,9 +35,7 @@
33
35
  "lint": "tsc --noEmit"
34
36
  },
35
37
  "dependencies": {
36
- "@kbach/core": "0.1.1",
37
- "@kbach/react": "0.1.6",
38
- "babel-plugin-kbach": "0.1.2"
38
+ "@kbach/react": "0.1.9"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/react": "^19.2.0",
@@ -53,6 +53,6 @@
53
53
  "publishConfig": {
54
54
  "access": "public"
55
55
  },
56
- "files": ["dist"],
57
- "keywords": ["react-native", "expo", "tailwind", "css-in-js", "styling", "khmer"]
56
+ "files": ["dist", "src/babel-plugin", "src/babel"],
57
+ "keywords": ["react-native", "expo", "tailwind", "css-in-js", "styling", "framework", "frontend", "mobile"]
58
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,259 @@
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
+ // Two-pronged JSX runtime setup (NativeWind-style — plugin is self-sufficient):
116
+ //
117
+ // 1. If the project has a React preset (babel-preset-expo, @babel/preset-react):
118
+ // it already includes @babel/plugin-transform-react-jsx. We leave that transform
119
+ // in place and just inject a @jsxImportSource comment in pre() so the transform
120
+ // picks up our runtime. This runs after our pre() and before any other pre() calls.
121
+ //
122
+ // 2. If there is NO React preset, we add @babel/plugin-transform-react-jsx ourselves
123
+ // (pushed after kbach so kbach's JSXAttribute visitor runs first).
124
+ manipulateOptions(opts) {
125
+ const presets = opts.presets || [];
126
+ const hasReactPreset = presets.some(p => {
127
+ const name = Array.isArray(p) ? p[0] : p;
128
+ return (
129
+ typeof name === 'string' &&
130
+ (name.includes('babel-preset-expo') || name.includes('preset-react'))
131
+ );
132
+ });
133
+
134
+ if (hasReactPreset) return; // pre() comment pragma handles it
135
+
136
+ // No preset — add the JSX transform plugin directly so JSX files work
137
+ // without any manual configuration.
138
+ const plugins = opts.plugins || (opts.plugins = []);
139
+ const hasJsxTransform = plugins.some(p => {
140
+ const n = Array.isArray(p) ? p[0] : p;
141
+ return typeof n === 'string' && n.includes('transform-react-jsx');
142
+ });
143
+ if (!hasJsxTransform) {
144
+ plugins.push([
145
+ require.resolve('@babel/plugin-transform-react-jsx'),
146
+ { runtime: 'automatic', importSource: '@kbach/native' },
147
+ ]);
148
+ }
149
+ },
150
+
151
+ pre(file) {
152
+ // Skip node_modules — they have their own JSX runtime configuration
153
+ const filename = file.opts.filename || '';
154
+ if (filename.includes(`${SEP}node_modules${SEP}`) && !filename.includes('kbach')) return;
155
+
156
+ // Inject @jsxImportSource so @babel/plugin-transform-react-jsx (from a React preset)
157
+ // uses our runtime. This pre() runs before the preset's pre() calls.
158
+ const comments = file.ast.comments;
159
+ if (!Array.isArray(comments)) return;
160
+ const alreadySet = comments.some(c => /@jsxImportSource|@jsxRuntime/.test(c.value));
161
+ if (!alreadySet) {
162
+ comments.unshift({ type: 'CommentLine', value: ' @jsxImportSource @kbach/native' });
163
+ }
164
+ },
165
+
166
+ visitor: {
167
+ Program: {
168
+ enter(programPath, state) {
169
+ state.kbachDeclarations = new Map();
170
+ },
171
+
172
+ exit(programPath, state) {
173
+ if (!state.kbachDeclarations || !state.kbachDeclarations.size) return;
174
+
175
+ const body = programPath.get('body');
176
+ const imports = body.filter(p => p.isImportDeclaration());
177
+ const insertAfterPath = imports.length > 0 ? imports[imports.length - 1] : null;
178
+
179
+ const entries = [...state.kbachDeclarations.values()];
180
+ for (let i = entries.length - 1; i >= 0; i--) {
181
+ const { uid, astNode } = entries[i];
182
+ const decl = t.variableDeclaration('const', [t.variableDeclarator(uid, astNode)]);
183
+ if (insertAfterPath) {
184
+ insertAfterPath.insertAfter(decl);
185
+ } else {
186
+ programPath.unshiftContainer('body', decl);
187
+ }
188
+ }
189
+ },
190
+ },
191
+
192
+ JSXAttribute(nodePath, state) {
193
+ // Skip third-party node_modules — they won't have kbach className attrs
194
+ const filename = state.file.opts.filename || '';
195
+ if (
196
+ filename.includes(`${SEP}node_modules${SEP}`) &&
197
+ !filename.includes('kbach')
198
+ ) return;
199
+
200
+ const attrName = nodePath.node.name;
201
+ const name = t.isJSXIdentifier(attrName) ? attrName.name : null;
202
+
203
+ if (!name || !attributes.includes(name)) return;
204
+
205
+ const value = nodePath.node.value;
206
+
207
+ if (!t.isStringLiteral(value) && !(t.isJSXExpressionContainer(value) && t.isStringLiteral(value.expression))) {
208
+ return;
209
+ }
210
+
211
+ const classString = t.isStringLiteral(value)
212
+ ? value.value
213
+ : value.expression.value;
214
+
215
+ if (!classString || !classString.trim()) return;
216
+
217
+ try {
218
+ // Use global cache to avoid re-resolving the same class string
219
+ // across different files in the same build.
220
+ let resolved;
221
+ if (_resolveCache.has(classString)) {
222
+ resolved = _resolveCache.get(classString);
223
+ } else {
224
+ const { resolve } = getCore();
225
+ const config = getUserConfig(configFile);
226
+ resolved = resolve(classString, config.theme, config.darkMode);
227
+ _resolveCache.set(classString, resolved);
228
+ }
229
+
230
+ if (debug) {
231
+ console.log(`[Kbach] Transformed: "${classString}"`);
232
+ }
233
+
234
+ let uid;
235
+ if (state.kbachDeclarations.has(classString)) {
236
+ uid = state.kbachDeclarations.get(classString).uid;
237
+ } else {
238
+ const astNode = resolvedStyleToAST(t, resolved);
239
+ uid = nodePath.scope.getProgramParent().generateUidIdentifier('kbach');
240
+ state.kbachDeclarations.set(classString, { uid, astNode });
241
+ }
242
+
243
+ nodePath.insertAfter(
244
+ t.jSXAttribute(
245
+ t.jSXIdentifier('__kbachStyles'),
246
+ t.jSXExpressionContainer(t.identifier(uid.name)),
247
+ ),
248
+ );
249
+
250
+ nodePath.node.name = t.jSXIdentifier('__kbachClasses');
251
+ } catch (err) {
252
+ if (debug) {
253
+ console.warn(`[Kbach] Could not transform "${classString}":`, err.message);
254
+ }
255
+ }
256
+ },
257
+ },
258
+ };
259
+ };