@kbach/react 0.5.23 → 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,87 +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
- function toBuffer(chunk, enc) {
3457
- if (chunk == null) return Buffer.alloc(0);
3458
- if (typeof chunk === "string") {
3459
- return Buffer.from(chunk, typeof enc === "string" ? enc : "utf-8");
3460
- }
3461
- if (ArrayBuffer.isView(chunk)) {
3462
- return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
3463
- }
3464
- if (chunk instanceof ArrayBuffer) return Buffer.from(chunk);
3465
- return Buffer.from(String(chunk));
3466
- }
3467
- res.write = function(chunk, enc, cb) {
3468
- if (done) return origWrite(chunk, enc, cb);
3469
- if (chunk != null) chunks.push(toBuffer(chunk, enc));
3470
- if (typeof enc === "function") enc();
3471
- else if (typeof cb === "function") cb();
3472
- return true;
3473
- };
3474
- res.end = function(chunk, enc, cb) {
3475
- done = true;
3476
- if (typeof chunk === "function") {
3477
- cb = chunk;
3478
- chunk = null;
3479
- enc = null;
3480
- } else if (typeof enc === "function") {
3481
- cb = enc;
3482
- enc = null;
3483
- }
3484
- if (chunk != null) chunks.push(toBuffer(chunk, enc));
3485
- if (chunks.length > 0) {
3486
- const html = Buffer.concat(chunks).toString("utf-8");
3487
- if (html.includes("</head>")) {
3488
- const injected = injectIntoHtml(html);
3489
- const buf = Buffer.from(injected, "utf-8");
3490
- try {
3491
- res.removeHeader("transfer-encoding");
3492
- } catch {
3493
- }
3494
- try {
3495
- res.setHeader("content-length", buf.length);
3496
- } catch {
3497
- }
3498
- return origEnd(buf, cb);
3499
- }
3500
- return origEnd(Buffer.concat(chunks), enc, cb);
3501
- }
3502
- return origEnd(void 0, enc, cb);
3503
- };
3504
- next();
3505
- });
3506
- },
3507
- // ── 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
3508
3457
  transform(code, id) {
3509
3458
  const cleanId = id.split("?")[0];
3510
3459
  if (cleanId.endsWith(".css") && !id.includes("node_modules") && !id.includes("\0")) {
3460
+ processedCSSFiles.add(cleanId);
3511
3461
  const kbachCSS = getAllCSS();
3512
3462
  if (!kbachCSS) return null;
3513
- return { code: code + "\n/* kbach */\n" + kbachCSS, map: null };
3463
+ return { code: `${code}
3464
+ /* kbach */
3465
+ ${kbachCSS}`, map: null };
3514
3466
  }
3515
3467
  if (/\.(tsx?|jsx?)$/.test(cleanId) && !id.includes("node_modules")) {
3516
- extractFromCode(code);
3468
+ const added = extractFromCode(code);
3469
+ if (added && server && processedCSSFiles.size > 0) {
3470
+ setTimeout(() => invalidateCSSModules(), 0);
3471
+ }
3517
3472
  }
3518
3473
  },
3519
3474
  // ── Virtual module (opt-in explicit import) ───────────────────────────────
@@ -3523,13 +3478,14 @@ function kbach(userConfig) {
3523
3478
  load(id) {
3524
3479
  if (id === RESOLVED_ID) return getAllCSS();
3525
3480
  },
3526
- // ── HMR: full reload when new classes are added ───────────────────────────
3527
- handleHotUpdate({ file }) {
3481
+ // ── HMR: invalidate CSS when new classes are found in changed files ───────
3482
+ handleHotUpdate({ file, server: s }) {
3528
3483
  if (file.includes("node_modules") || !/\.(tsx?|jsx?)$/.test(file)) return;
3529
3484
  try {
3530
3485
  const added = extractFromCode((0, import_fs.readFileSync)(file, "utf-8"));
3531
- if (added && server) {
3532
- (server.hot ?? server.ws)?.send({ type: "full-reload" });
3486
+ if (added) {
3487
+ server = s;
3488
+ invalidateCSSModules();
3533
3489
  }
3534
3490
  } catch {
3535
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,87 +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
- function toBuffer(chunk, enc) {
115
- if (chunk == null) return Buffer.alloc(0);
116
- if (typeof chunk === "string") {
117
- return Buffer.from(chunk, typeof enc === "string" ? enc : "utf-8");
118
- }
119
- if (ArrayBuffer.isView(chunk)) {
120
- return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
121
- }
122
- if (chunk instanceof ArrayBuffer) return Buffer.from(chunk);
123
- return Buffer.from(String(chunk));
124
- }
125
- res.write = function(chunk, enc, cb) {
126
- if (done) return origWrite(chunk, enc, cb);
127
- if (chunk != null) chunks.push(toBuffer(chunk, enc));
128
- if (typeof enc === "function") enc();
129
- else if (typeof cb === "function") cb();
130
- return true;
131
- };
132
- res.end = function(chunk, enc, cb) {
133
- done = true;
134
- if (typeof chunk === "function") {
135
- cb = chunk;
136
- chunk = null;
137
- enc = null;
138
- } else if (typeof enc === "function") {
139
- cb = enc;
140
- enc = null;
141
- }
142
- if (chunk != null) chunks.push(toBuffer(chunk, enc));
143
- if (chunks.length > 0) {
144
- const html = Buffer.concat(chunks).toString("utf-8");
145
- if (html.includes("</head>")) {
146
- const injected = injectIntoHtml(html);
147
- const buf = Buffer.from(injected, "utf-8");
148
- try {
149
- res.removeHeader("transfer-encoding");
150
- } catch {
151
- }
152
- try {
153
- res.setHeader("content-length", buf.length);
154
- } catch {
155
- }
156
- return origEnd(buf, cb);
157
- }
158
- return origEnd(Buffer.concat(chunks), enc, cb);
159
- }
160
- return origEnd(void 0, enc, cb);
161
- };
162
- next();
163
- });
164
- },
165
- // ── 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
166
115
  transform(code, id) {
167
116
  const cleanId = id.split("?")[0];
168
117
  if (cleanId.endsWith(".css") && !id.includes("node_modules") && !id.includes("\0")) {
118
+ processedCSSFiles.add(cleanId);
169
119
  const kbachCSS = getAllCSS();
170
120
  if (!kbachCSS) return null;
171
- return { code: code + "\n/* kbach */\n" + kbachCSS, map: null };
121
+ return { code: `${code}
122
+ /* kbach */
123
+ ${kbachCSS}`, map: null };
172
124
  }
173
125
  if (/\.(tsx?|jsx?)$/.test(cleanId) && !id.includes("node_modules")) {
174
- extractFromCode(code);
126
+ const added = extractFromCode(code);
127
+ if (added && server && processedCSSFiles.size > 0) {
128
+ setTimeout(() => invalidateCSSModules(), 0);
129
+ }
175
130
  }
176
131
  },
177
132
  // ── Virtual module (opt-in explicit import) ───────────────────────────────
@@ -181,13 +136,14 @@ function kbach(userConfig) {
181
136
  load(id) {
182
137
  if (id === RESOLVED_ID) return getAllCSS();
183
138
  },
184
- // ── HMR: full reload when new classes are added ───────────────────────────
185
- handleHotUpdate({ file }) {
139
+ // ── HMR: invalidate CSS when new classes are found in changed files ───────
140
+ handleHotUpdate({ file, server: s }) {
186
141
  if (file.includes("node_modules") || !/\.(tsx?|jsx?)$/.test(file)) return;
187
142
  try {
188
143
  const added = extractFromCode(readFileSync(file, "utf-8"));
189
- if (added && server) {
190
- (server.hot ?? server.ws)?.send({ type: "full-reload" });
144
+ if (added) {
145
+ server = s;
146
+ invalidateCSSModules();
191
147
  }
192
148
  } catch {
193
149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.5.23",
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",