@kbach/react 0.5.22 → 0.5.24

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.
@@ -7,6 +7,13 @@ import { F as FrameworkConfig } from './types-CV5vXnSH.mjs';
7
7
  * Vite plugin — scans source files for Kbach className/kb props and generates CSS.
8
8
  * Zero setup: just add kbach() to your plugins array.
9
9
  * Only generates CSS for classes you actually use (like Tailwind).
10
+ *
11
+ * How it works (same as Tailwind):
12
+ * 1. Scans your source files at startup for className/kb props
13
+ * 2. Generates CSS rules for only those classes
14
+ * 3. Appends them to your imported CSS file (e.g. app.css) via Vite's transform
15
+ * 4. Vite serves the CSS through its normal pipeline → <link rel="stylesheet">
16
+ * 5. When you add a new class in dev, the CSS file is hot-reloaded
10
17
  */
11
18
 
12
19
  declare const VIRTUAL_ID = "virtual:kbach/styles.css";
@@ -7,6 +7,13 @@ import { F as FrameworkConfig } from './types-CV5vXnSH.js';
7
7
  * Vite plugin — scans source files for Kbach className/kb props and generates CSS.
8
8
  * Zero setup: just add kbach() to your plugins array.
9
9
  * Only generates CSS for classes you actually use (like Tailwind).
10
+ *
11
+ * How it works (same as Tailwind):
12
+ * 1. Scans your source files at startup for className/kb props
13
+ * 2. Generates CSS rules for only those classes
14
+ * 3. Appends them to your imported CSS file (e.g. app.css) via Vite's transform
15
+ * 4. Vite serves the CSS through its normal pipeline → <link rel="stylesheet">
16
+ * 5. When you add a new class in dev, the CSS file is hot-reloaded
10
17
  */
11
18
 
12
19
  declare const VIRTUAL_ID = "virtual:kbach/styles.css";
@@ -3395,21 +3395,20 @@ function kbach(userConfig) {
3395
3395
  const cfg = buildConfig(userConfig ?? {});
3396
3396
  const screens = toNumericScreens(cfg.theme.screens ?? {});
3397
3397
  const tokenCSS = /* @__PURE__ */ new Map();
3398
+ const processedCSSFiles = /* @__PURE__ */ new Set();
3398
3399
  let root = process.cwd();
3399
3400
  let server = null;
3400
- function processClassString(classStr) {
3401
- let added = false;
3402
- for (const token of classStr.split(/\s+/).filter(Boolean)) {
3403
- if (tokenCSS.has(token)) continue;
3404
- tokenCSS.set(token, generateClassCSS(token, cfg.theme, cfg.darkMode, screens));
3405
- added = true;
3406
- }
3407
- return added;
3401
+ function processToken(token) {
3402
+ if (tokenCSS.has(token)) return false;
3403
+ tokenCSS.set(token, generateClassCSS(token, cfg.theme, cfg.darkMode, screens));
3404
+ return true;
3408
3405
  }
3409
3406
  function extractFromCode(code) {
3410
3407
  let added = false;
3411
3408
  for (const cls of extractClassStrings(code)) {
3412
- if (processClassString(cls)) added = true;
3409
+ for (const token of cls.split(/\s+/).filter(Boolean)) {
3410
+ if (processToken(token)) added = true;
3411
+ }
3413
3412
  }
3414
3413
  return added;
3415
3414
  }
@@ -3422,10 +3421,18 @@ function kbach(userConfig) {
3422
3421
  }
3423
3422
  return parts.join("\n");
3424
3423
  }
3425
- function injectIntoHtml(html) {
3426
- const css = getAllCSS();
3427
- if (!css || html.includes("data-kbach")) return html;
3428
- return html.replace("</head>", `<style data-kbach>${css}</style></head>`);
3424
+ function invalidateCSSModules() {
3425
+ if (!server) return;
3426
+ for (const id of processedCSSFiles) {
3427
+ const mod = server.moduleGraph?.getModuleById(id);
3428
+ if (mod) server.moduleGraph.invalidateModule(mod);
3429
+ }
3430
+ server.hot?.send({ type: "update", updates: [...processedCSSFiles].map((id) => ({
3431
+ type: "js-update",
3432
+ path: id,
3433
+ acceptedPath: id,
3434
+ timestamp: Date.now()
3435
+ })) });
3429
3436
  }
3430
3437
  return {
3431
3438
  name: "kbach",
@@ -3433,80 +3440,35 @@ function kbach(userConfig) {
3433
3440
  configResolved(resolved) {
3434
3441
  root = resolved.root;
3435
3442
  },
3443
+ configureServer(s) {
3444
+ server = s;
3445
+ },
3436
3446
  buildStart() {
3437
3447
  for (const dir of ["src", "app", "pages", "components", "views", "layouts"]) {
3438
3448
  scanDir((0, import_path.join)(root, dir), (code) => extractFromCode(code));
3439
3449
  }
3440
3450
  },
3441
- // ── DEV: intercept every HTML response and inject <style data-kbach> ──────
3442
- // React Router v8 uses chunked streaming SSR — </head> comes in an early
3443
- // res.write() chunk, not res.end(). Buffer all writes, concatenate at end,
3444
- // inject CSS into the full HTML, then send in one go.
3445
- configureServer(s) {
3446
- server = s;
3447
- s.middlewares.use((req, res, next) => {
3448
- const url = req.url ?? "";
3449
- if (/\.(js|ts|jsx|tsx|css|png|jpg|svg|ico|woff2?|ttf|json|map)(\?|$)/.test(url) || url.startsWith("/@") || url.startsWith("/__")) {
3450
- return next();
3451
- }
3452
- const origWrite = res.write.bind(res);
3453
- const origEnd = res.end.bind(res);
3454
- const chunks = [];
3455
- let done = false;
3456
- res.write = function(chunk, enc, cb) {
3457
- if (done) return origWrite(chunk, enc, cb);
3458
- if (chunk != null) {
3459
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? "")));
3460
- }
3461
- if (typeof enc === "function") enc();
3462
- else if (typeof cb === "function") cb();
3463
- return true;
3464
- };
3465
- res.end = function(chunk, enc, cb) {
3466
- done = true;
3467
- if (typeof chunk === "function") {
3468
- cb = chunk;
3469
- chunk = null;
3470
- enc = null;
3471
- } else if (typeof enc === "function") {
3472
- cb = enc;
3473
- enc = null;
3474
- }
3475
- if (chunk != null) {
3476
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? "")));
3477
- }
3478
- if (chunks.length > 0) {
3479
- const html = Buffer.concat(chunks).toString("utf-8");
3480
- if (html.includes("</head>")) {
3481
- const injected = injectIntoHtml(html);
3482
- const buf = Buffer.from(injected, "utf-8");
3483
- try {
3484
- res.removeHeader("transfer-encoding");
3485
- } catch {
3486
- }
3487
- try {
3488
- res.setHeader("content-length", buf.length);
3489
- } catch {
3490
- }
3491
- return origEnd(buf, cb);
3492
- }
3493
- return origEnd(Buffer.concat(chunks), enc, cb);
3494
- }
3495
- return origEnd(void 0, enc, cb);
3496
- };
3497
- next();
3498
- });
3499
- },
3500
- // ── PROD: append to CSS files so they land in the extracted CSS bundle ────
3451
+ // ── CSS transform: append generated rules to the user's CSS file ──────────
3452
+ // Works exactly like Tailwind's PostCSS plugin:
3453
+ // - Your app.css content stays untouched (empty or your own rules)
3454
+ // - Kbach appends its generated rules to the transform output
3455
+ // - Vite serves the result via its normal CSS pipeline (<link rel="stylesheet">)
3456
+ // - In production, it lands in the extracted CSS bundle file
3501
3457
  transform(code, id) {
3502
3458
  const cleanId = id.split("?")[0];
3503
3459
  if (cleanId.endsWith(".css") && !id.includes("node_modules") && !id.includes("\0")) {
3460
+ processedCSSFiles.add(cleanId);
3504
3461
  const kbachCSS = getAllCSS();
3505
3462
  if (!kbachCSS) return null;
3506
- return { code: code + "\n/* kbach */\n" + kbachCSS, map: null };
3463
+ return { code: `${code}
3464
+ /* kbach */
3465
+ ${kbachCSS}`, map: null };
3507
3466
  }
3508
3467
  if (/\.(tsx?|jsx?)$/.test(cleanId) && !id.includes("node_modules")) {
3509
- extractFromCode(code);
3468
+ const added = extractFromCode(code);
3469
+ if (added && server && processedCSSFiles.size > 0) {
3470
+ setTimeout(() => invalidateCSSModules(), 0);
3471
+ }
3510
3472
  }
3511
3473
  },
3512
3474
  // ── Virtual module (opt-in explicit import) ───────────────────────────────
@@ -3516,13 +3478,14 @@ function kbach(userConfig) {
3516
3478
  load(id) {
3517
3479
  if (id === RESOLVED_ID) return getAllCSS();
3518
3480
  },
3519
- // ── HMR: full reload when new classes are added ───────────────────────────
3520
- handleHotUpdate({ file }) {
3481
+ // ── HMR: invalidate CSS when new classes are found in changed files ───────
3482
+ handleHotUpdate({ file, server: s }) {
3521
3483
  if (file.includes("node_modules") || !/\.(tsx?|jsx?)$/.test(file)) return;
3522
3484
  try {
3523
3485
  const added = extractFromCode((0, import_fs.readFileSync)(file, "utf-8"));
3524
- if (added && server) {
3525
- (server.hot ?? server.ws)?.send({ type: "full-reload" });
3486
+ if (added) {
3487
+ server = s;
3488
+ invalidateCSSModules();
3526
3489
  }
3527
3490
  } catch {
3528
3491
  }
@@ -53,21 +53,20 @@ function kbach(userConfig) {
53
53
  const cfg = buildConfig(userConfig ?? {});
54
54
  const screens = toNumericScreens(cfg.theme.screens ?? {});
55
55
  const tokenCSS = /* @__PURE__ */ new Map();
56
+ const processedCSSFiles = /* @__PURE__ */ new Set();
56
57
  let root = process.cwd();
57
58
  let server = null;
58
- function processClassString(classStr) {
59
- let added = false;
60
- for (const token of classStr.split(/\s+/).filter(Boolean)) {
61
- if (tokenCSS.has(token)) continue;
62
- tokenCSS.set(token, generateClassCSS(token, cfg.theme, cfg.darkMode, screens));
63
- added = true;
64
- }
65
- return added;
59
+ function processToken(token) {
60
+ if (tokenCSS.has(token)) return false;
61
+ tokenCSS.set(token, generateClassCSS(token, cfg.theme, cfg.darkMode, screens));
62
+ return true;
66
63
  }
67
64
  function extractFromCode(code) {
68
65
  let added = false;
69
66
  for (const cls of extractClassStrings(code)) {
70
- if (processClassString(cls)) added = true;
67
+ for (const token of cls.split(/\s+/).filter(Boolean)) {
68
+ if (processToken(token)) added = true;
69
+ }
71
70
  }
72
71
  return added;
73
72
  }
@@ -80,10 +79,18 @@ function kbach(userConfig) {
80
79
  }
81
80
  return parts.join("\n");
82
81
  }
83
- function injectIntoHtml(html) {
84
- const css = getAllCSS();
85
- if (!css || html.includes("data-kbach")) return html;
86
- return html.replace("</head>", `<style data-kbach>${css}</style></head>`);
82
+ function invalidateCSSModules() {
83
+ if (!server) return;
84
+ for (const id of processedCSSFiles) {
85
+ const mod = server.moduleGraph?.getModuleById(id);
86
+ if (mod) server.moduleGraph.invalidateModule(mod);
87
+ }
88
+ server.hot?.send({ type: "update", updates: [...processedCSSFiles].map((id) => ({
89
+ type: "js-update",
90
+ path: id,
91
+ acceptedPath: id,
92
+ timestamp: Date.now()
93
+ })) });
87
94
  }
88
95
  return {
89
96
  name: "kbach",
@@ -91,80 +98,35 @@ function kbach(userConfig) {
91
98
  configResolved(resolved) {
92
99
  root = resolved.root;
93
100
  },
101
+ configureServer(s) {
102
+ server = s;
103
+ },
94
104
  buildStart() {
95
105
  for (const dir of ["src", "app", "pages", "components", "views", "layouts"]) {
96
106
  scanDir(join(root, dir), (code) => extractFromCode(code));
97
107
  }
98
108
  },
99
- // ── DEV: intercept every HTML response and inject <style data-kbach> ──────
100
- // React Router v8 uses chunked streaming SSR — </head> comes in an early
101
- // res.write() chunk, not res.end(). Buffer all writes, concatenate at end,
102
- // inject CSS into the full HTML, then send in one go.
103
- configureServer(s) {
104
- server = s;
105
- s.middlewares.use((req, res, next) => {
106
- const url = req.url ?? "";
107
- if (/\.(js|ts|jsx|tsx|css|png|jpg|svg|ico|woff2?|ttf|json|map)(\?|$)/.test(url) || url.startsWith("/@") || url.startsWith("/__")) {
108
- return next();
109
- }
110
- const origWrite = res.write.bind(res);
111
- const origEnd = res.end.bind(res);
112
- const chunks = [];
113
- let done = false;
114
- res.write = function(chunk, enc, cb) {
115
- if (done) return origWrite(chunk, enc, cb);
116
- if (chunk != null) {
117
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? "")));
118
- }
119
- if (typeof enc === "function") enc();
120
- else if (typeof cb === "function") cb();
121
- return true;
122
- };
123
- res.end = function(chunk, enc, cb) {
124
- done = true;
125
- if (typeof chunk === "function") {
126
- cb = chunk;
127
- chunk = null;
128
- enc = null;
129
- } else if (typeof enc === "function") {
130
- cb = enc;
131
- enc = null;
132
- }
133
- if (chunk != null) {
134
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? "")));
135
- }
136
- if (chunks.length > 0) {
137
- const html = Buffer.concat(chunks).toString("utf-8");
138
- if (html.includes("</head>")) {
139
- const injected = injectIntoHtml(html);
140
- const buf = Buffer.from(injected, "utf-8");
141
- try {
142
- res.removeHeader("transfer-encoding");
143
- } catch {
144
- }
145
- try {
146
- res.setHeader("content-length", buf.length);
147
- } catch {
148
- }
149
- return origEnd(buf, cb);
150
- }
151
- return origEnd(Buffer.concat(chunks), enc, cb);
152
- }
153
- return origEnd(void 0, enc, cb);
154
- };
155
- next();
156
- });
157
- },
158
- // ── PROD: append to CSS files so they land in the extracted CSS bundle ────
109
+ // ── CSS transform: append generated rules to the user's CSS file ──────────
110
+ // Works exactly like Tailwind's PostCSS plugin:
111
+ // - Your app.css content stays untouched (empty or your own rules)
112
+ // - Kbach appends its generated rules to the transform output
113
+ // - Vite serves the result via its normal CSS pipeline (<link rel="stylesheet">)
114
+ // - In production, it lands in the extracted CSS bundle file
159
115
  transform(code, id) {
160
116
  const cleanId = id.split("?")[0];
161
117
  if (cleanId.endsWith(".css") && !id.includes("node_modules") && !id.includes("\0")) {
118
+ processedCSSFiles.add(cleanId);
162
119
  const kbachCSS = getAllCSS();
163
120
  if (!kbachCSS) return null;
164
- return { code: code + "\n/* kbach */\n" + kbachCSS, map: null };
121
+ return { code: `${code}
122
+ /* kbach */
123
+ ${kbachCSS}`, map: null };
165
124
  }
166
125
  if (/\.(tsx?|jsx?)$/.test(cleanId) && !id.includes("node_modules")) {
167
- extractFromCode(code);
126
+ const added = extractFromCode(code);
127
+ if (added && server && processedCSSFiles.size > 0) {
128
+ setTimeout(() => invalidateCSSModules(), 0);
129
+ }
168
130
  }
169
131
  },
170
132
  // ── Virtual module (opt-in explicit import) ───────────────────────────────
@@ -174,13 +136,14 @@ function kbach(userConfig) {
174
136
  load(id) {
175
137
  if (id === RESOLVED_ID) return getAllCSS();
176
138
  },
177
- // ── HMR: full reload when new classes are added ───────────────────────────
178
- handleHotUpdate({ file }) {
139
+ // ── HMR: invalidate CSS when new classes are found in changed files ───────
140
+ handleHotUpdate({ file, server: s }) {
179
141
  if (file.includes("node_modules") || !/\.(tsx?|jsx?)$/.test(file)) return;
180
142
  try {
181
143
  const added = extractFromCode(readFileSync(file, "utf-8"));
182
- if (added && server) {
183
- (server.hot ?? server.ws)?.send({ type: "full-reload" });
144
+ if (added) {
145
+ server = s;
146
+ invalidateCSSModules();
184
147
  }
185
148
  } catch {
186
149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.5.22",
3
+ "version": "0.5.24",
4
4
  "description": "React / React Native components and hooks for the Kbach framework",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./dist/index.js",