@kbach/react 1.0.0-beta.7 → 1.0.0-beta.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
@@ -24,11 +24,10 @@ export default defineConfig({
24
24
  });
25
25
  ```
26
26
 
27
- ```css
28
- /* src/kbach.css */
29
- /* kbach:start */
30
- /* kbach:end */
31
- ```
27
+ That's it — the plugin finds or creates your `kbach.css` on its own,
28
+ wherever your source actually lives (`src/`, `app/`, ...; see "Where does
29
+ `kbach.css` go?" below if you want to know exactly where or control it).
30
+ Import it once from your entry point and render normally:
32
31
 
33
32
  ```tsx
34
33
  // main.tsx
@@ -45,12 +44,25 @@ createRoot(document.getElementById('root')!).render(<App />);
45
44
  </div>
46
45
  ```
47
46
 
48
- That's the whole setup — the Vite plugin config plus a `kbach.css` with the
49
- two marker comments. No `initKbach()`/async step before your first render:
50
- `className` here is a plain string prop, resolved entirely by the CSS
51
- `@kbach/react/vite` already generated at build time, same as any other
52
- static stylesheet — nothing here calls into the WASM engine at render time,
53
- so there's nothing to await.
47
+ No `initKbach()`/async step before your first render: `className` here is
48
+ a plain string prop, resolved entirely by the CSS `@kbach/react/vite`
49
+ already generated at build time, same as any other static stylesheet —
50
+ nothing here calls into the WASM engine at render time, so there's nothing
51
+ to await.
52
+
53
+ ### Where does `kbach.css` go?
54
+
55
+ You don't need to think about this for the common case — skip to
56
+ [React Router](#react-router)/[Next.js](#nextjs) if that's what brought you
57
+ here. For everyone else: the plugin looks for a file already containing
58
+ `/* kbach:start */`/`/* kbach:end */` markers under each of your `include`
59
+ directories (`src`, `app`, `pages`, `components` by default) and the
60
+ project root, in that order, and keeps using whichever one it finds. If
61
+ none exists yet, it creates `<first-existing-include-dir>/kbach.css` for
62
+ you, markers included — you never have to hand-author that file. Pass
63
+ `cssFile` to `kbach()` only if you want to pin the location explicitly
64
+ (a path outside every `include` dir, or more than one candidate present at
65
+ once).
54
66
 
55
67
  `initKbach()` (still exported from `@kbach/react`) is only needed for two
56
68
  opt-in features that genuinely do call into the Rust/WASM engine at
@@ -88,34 +100,35 @@ createRoot(document.getElementById('root')!).render(<RouterProvider router={rout
88
100
  ```
89
101
 
90
102
  **Framework mode** (`npx create-react-router@latest` — file-based routing
91
- under `app/`, its own Vite plugin, SSR by default): two things differ from
92
- the Quick Start, both because this mode owns your project's structure and
93
- render lifecycle, not you:
94
-
95
- 1. Its convention is `app/`, not `src/` — pass `cssFile` so the plugin
96
- writes to `app/kbach.css` instead of the (here, nonexistent) default:
97
- ```ts
98
- // vite.config.ts
99
- import { reactRouter } from '@react-router/dev/vite';
100
- import { kbach } from '@kbach/react/vite';
101
- import { defineConfig } from 'vite';
102
-
103
- export default defineConfig({
104
- plugins: [kbach({ cssFile: 'app/kbach.css' }), reactRouter()],
105
- });
106
- ```
107
- 2. **Don't call `createRoot` yourself in `app/root.tsx`** — the framework's
108
- own generated client/server entry points already do this; `root.tsx`
109
- only ever *exports* a `Layout`/default component. Adding your own
110
- `createRoot(...).render(...)` at that file's top level runs during SSR
111
- too, where `document` doesn't exist, and crashes the build immediately.
112
- `app/root.tsx` needs only:
113
- ```tsx
114
- import './kbach.css';
115
- export default function App() {
116
- return <Outlet />;
117
- }
118
- ```
103
+ under `app/`, its own Vite plugin, SSR by default):
104
+
105
+ ```ts
106
+ // vite.config.ts
107
+ import { reactRouter } from '@react-router/dev/vite';
108
+ import { kbach } from '@kbach/react/vite';
109
+ import { defineConfig } from 'vite';
110
+
111
+ export default defineConfig({
112
+ plugins: [kbach(), reactRouter()],
113
+ });
114
+ ```
115
+
116
+ Same `kbach()` call as everywhere else — its `app/`-vs-`src/` convention
117
+ is exactly what auto-detection (above) exists for, so there's nothing
118
+ React-Router-specific to configure. The one real gotcha is unrelated to
119
+ Kbach at all: **don't call `createRoot` yourself in `app/root.tsx`** — the
120
+ framework's own generated client/server entry points already do this;
121
+ `root.tsx` only ever *exports* a `Layout`/default component. Adding your
122
+ own `createRoot(...).render(...)` at that file's top level runs during SSR
123
+ too, where `document` doesn't exist, and crashes the build immediately.
124
+ `app/root.tsx` needs only:
125
+
126
+ ```tsx
127
+ import './kbach.css';
128
+ export default function App() {
129
+ return <Outlet />;
130
+ }
131
+ ```
119
132
 
120
133
  Either mode: every route's own components use `className` exactly like any
121
134
  other component — routing has no effect on how Kbach resolves classes,
@@ -7,8 +7,9 @@ import {
7
7
  } from "./chunk-N5HCQIBB.mjs";
8
8
 
9
9
  // src/vite-plugin/index.ts
10
- import { writeFileSync, mkdirSync, existsSync } from "fs";
10
+ import { readFileSync as readFileSync3, writeFileSync, mkdirSync, existsSync, statSync as statSync2 } from "fs";
11
11
  import { join as join4, dirname } from "path";
12
+ import { pathToFileURL } from "url";
12
13
  import { createRequire } from "module";
13
14
 
14
15
  // src/vite-plugin/cssVars.ts
@@ -1235,22 +1236,63 @@ function readSourceFile(path) {
1235
1236
  }
1236
1237
 
1237
1238
  // src/vite-plugin/index.ts
1239
+ function findMainCSSFile(root, includeDirs, explicitCssFile) {
1240
+ if (explicitCssFile) return join4(root, explicitCssFile);
1241
+ const searchDirs = [...includeDirs, "."];
1242
+ for (const dir of searchDirs) {
1243
+ const candidate = join4(root, dir, "kbach.css");
1244
+ if (!existsSync(candidate)) continue;
1245
+ try {
1246
+ const content = readFileSync3(candidate, "utf-8");
1247
+ if (content.includes(KBACH_START) && content.includes(KBACH_END)) return candidate;
1248
+ } catch {
1249
+ }
1250
+ }
1251
+ for (const dir of includeDirs) {
1252
+ const full = join4(root, dir);
1253
+ try {
1254
+ if (statSync2(full).isDirectory()) return join4(full, "kbach.css");
1255
+ } catch {
1256
+ }
1257
+ }
1258
+ return join4(root, includeDirs[0] ?? "src", "kbach.css");
1259
+ }
1238
1260
  var DEFAULT_SCAN_DIRS = ["src", "app", "pages", "components"];
1239
1261
  var CSS_SYNC_DEBOUNCE_MS = 50;
1240
1262
  function writeFileEnsuringDir(path, content) {
1241
1263
  mkdirSync(dirname(path), { recursive: true });
1242
1264
  writeFileSync(path, content, "utf-8");
1243
1265
  }
1244
- function loadConfigFile(root) {
1245
- const path = join4(root, "kbach.config.js");
1246
- if (!existsSync(path)) return void 0;
1266
+ function isEsmRequireFailure(err) {
1267
+ if (!(err instanceof Error)) return false;
1268
+ return err.code === "ERR_REQUIRE_ESM" || /is not defined in ES module scope/.test(err.message);
1269
+ }
1270
+ async function importConfigModule(path) {
1271
+ const mod = await import(pathToFileURL(path).href);
1272
+ return mod.default ?? mod;
1273
+ }
1274
+ function requireConfigModule(root, path) {
1247
1275
  const mod = createRequire(root)(path);
1248
1276
  return mod.default ?? mod;
1249
1277
  }
1250
- function resolveEffectiveTheme(root, options) {
1278
+ async function loadConfigFile(root) {
1279
+ const cjsPath = join4(root, "kbach.config.cjs");
1280
+ if (existsSync(cjsPath)) return requireConfigModule(root, cjsPath);
1281
+ const mjsPath = join4(root, "kbach.config.mjs");
1282
+ if (existsSync(mjsPath)) return importConfigModule(mjsPath);
1283
+ const jsPath = join4(root, "kbach.config.js");
1284
+ if (!existsSync(jsPath)) return void 0;
1285
+ try {
1286
+ return requireConfigModule(root, jsPath);
1287
+ } catch (err) {
1288
+ if (!isEsmRequireFailure(err)) throw err;
1289
+ return importConfigModule(jsPath);
1290
+ }
1291
+ }
1292
+ async function resolveEffectiveTheme(root, options) {
1251
1293
  if (options.theme) return options.theme;
1252
1294
  if (options.config) return resolveKbachConfig(options.config);
1253
- const discovered = loadConfigFile(root);
1295
+ const discovered = await loadConfigFile(root);
1254
1296
  return discovered ? resolveKbachConfig(discovered) : defaultTheme;
1255
1297
  }
1256
1298
  function kbach(options = {}) {
@@ -1259,8 +1301,9 @@ function kbach(options = {}) {
1259
1301
  let root = process.cwd();
1260
1302
  let theme = defaultTheme;
1261
1303
  let engine = createKbachStaticCssEngine({ root, theme, themeJson: JSON.stringify(theme), includeDirs, safelist });
1304
+ let mainCSSFilePath = join4(root, "src", "kbach.css");
1262
1305
  function mainCSSFile() {
1263
- return join4(root, options.cssFile ?? "src/kbach.css");
1306
+ return mainCSSFilePath;
1264
1307
  }
1265
1308
  function classNameHintsFile() {
1266
1309
  return join4(root, ".kbach", "kbach-classnames.d.ts");
@@ -1270,7 +1313,7 @@ function kbach(options = {}) {
1270
1313
  writeFileEnsuringDir(classNameHintsFile(), dts);
1271
1314
  }
1272
1315
  function syncMainCSSFile(server) {
1273
- const changed = writeKbachToFile(mainCSSFile(), engine.generateCSS(), readSourceFile, (p, c) => writeFileSync(p, c, "utf-8"));
1316
+ const changed = writeKbachToFile(mainCSSFile(), engine.generateCSS(), readSourceFile, writeFileEnsuringDir);
1274
1317
  if (changed && server) server.watcher.emit("change", mainCSSFile());
1275
1318
  }
1276
1319
  let syncTimer = null;
@@ -1284,16 +1327,17 @@ function kbach(options = {}) {
1284
1327
  return {
1285
1328
  name: "kbach",
1286
1329
  enforce: "pre",
1287
- configResolved(resolved) {
1330
+ async configResolved(resolved) {
1288
1331
  root = resolved.root;
1289
- theme = resolveEffectiveTheme(root, options);
1332
+ theme = await resolveEffectiveTheme(root, options);
1290
1333
  engine = createKbachStaticCssEngine({ root, theme, themeJson: JSON.stringify(theme), includeDirs, safelist });
1334
+ mainCSSFilePath = findMainCSSFile(root, includeDirs, options.cssFile);
1291
1335
  },
1292
1336
  buildStart() {
1293
1337
  engine.scanProjectCssSelectorsOnce();
1294
1338
  engine.initialScan();
1295
1339
  engine.processSafelist();
1296
- writeKbachToFile(mainCSSFile(), engine.generateCSS(), readSourceFile, (p, c) => writeFileSync(p, c, "utf-8"));
1340
+ writeKbachToFile(mainCSSFile(), engine.generateCSS(), readSourceFile, writeFileEnsuringDir);
1297
1341
  writeClassNameHints();
1298
1342
  },
1299
1343
  // handleHotUpdate covers edits; add/unlink (create/delete) land on the
@@ -1332,6 +1376,7 @@ function kbach(options = {}) {
1332
1376
 
1333
1377
  export {
1334
1378
  createKbachStaticCssEngine,
1379
+ findMainCSSFile,
1335
1380
  DEFAULT_SCAN_DIRS,
1336
1381
  CSS_SYNC_DEBOUNCE_MS,
1337
1382
  loadConfigFile,
@@ -618,6 +618,7 @@ function createKbachStaticCssEngine(options) {
618
618
  // src/vite-plugin/index.ts
619
619
  var import_fs2 = require("fs");
620
620
  var import_path4 = require("path");
621
+ var import_url = require("url");
621
622
  var import_module = require("module");
622
623
 
623
624
  // src/generatedPalette.ts
@@ -1038,16 +1039,36 @@ function resolveKbachConfig(config) {
1038
1039
 
1039
1040
  // src/vite-plugin/index.ts
1040
1041
  var DEFAULT_SCAN_DIRS = ["src", "app", "pages", "components"];
1041
- function loadConfigFile(root) {
1042
- const path = (0, import_path4.join)(root, "kbach.config.js");
1043
- if (!(0, import_fs2.existsSync)(path)) return void 0;
1042
+ function isEsmRequireFailure(err) {
1043
+ if (!(err instanceof Error)) return false;
1044
+ return err.code === "ERR_REQUIRE_ESM" || /is not defined in ES module scope/.test(err.message);
1045
+ }
1046
+ async function importConfigModule(path) {
1047
+ const mod = await import((0, import_url.pathToFileURL)(path).href);
1048
+ return mod.default ?? mod;
1049
+ }
1050
+ function requireConfigModule(root, path) {
1044
1051
  const mod = (0, import_module.createRequire)(root)(path);
1045
1052
  return mod.default ?? mod;
1046
1053
  }
1047
- function resolveEffectiveTheme(root, options) {
1054
+ async function loadConfigFile(root) {
1055
+ const cjsPath = (0, import_path4.join)(root, "kbach.config.cjs");
1056
+ if ((0, import_fs2.existsSync)(cjsPath)) return requireConfigModule(root, cjsPath);
1057
+ const mjsPath = (0, import_path4.join)(root, "kbach.config.mjs");
1058
+ if ((0, import_fs2.existsSync)(mjsPath)) return importConfigModule(mjsPath);
1059
+ const jsPath = (0, import_path4.join)(root, "kbach.config.js");
1060
+ if (!(0, import_fs2.existsSync)(jsPath)) return void 0;
1061
+ try {
1062
+ return requireConfigModule(root, jsPath);
1063
+ } catch (err) {
1064
+ if (!isEsmRequireFailure(err)) throw err;
1065
+ return importConfigModule(jsPath);
1066
+ }
1067
+ }
1068
+ async function resolveEffectiveTheme(root, options) {
1048
1069
  if (options.theme) return options.theme;
1049
1070
  if (options.config) return resolveKbachConfig(options.config);
1050
- const discovered = loadConfigFile(root);
1071
+ const discovered = await loadConfigFile(root);
1051
1072
  return discovered ? resolveKbachConfig(discovered) : defaultTheme;
1052
1073
  }
1053
1074
 
@@ -1089,9 +1110,9 @@ function spliceGeneratedCss(root, generatedCss) {
1089
1110
  function kbachPostcss(options = {}) {
1090
1111
  return {
1091
1112
  postcssPlugin: "kbach",
1092
- Once(root, { result }) {
1113
+ async Once(root, { result }) {
1093
1114
  const projectRoot = options.root ?? process.cwd();
1094
- const theme = resolveEffectiveTheme(projectRoot, options);
1115
+ const theme = await resolveEffectiveTheme(projectRoot, options);
1095
1116
  const includeDirs = options.include ?? DEFAULT_SCAN_DIRS;
1096
1117
  const safelist = options.safelist ?? [];
1097
1118
  const engine = createKbachStaticCssEngine({
@@ -2,7 +2,7 @@ import {
2
2
  DEFAULT_SCAN_DIRS,
3
3
  createKbachStaticCssEngine,
4
4
  resolveEffectiveTheme
5
- } from "./chunk-P4CQSYIL.mjs";
5
+ } from "./chunk-7JBSTM5D.mjs";
6
6
  import "./chunk-NKR3PSWO.mjs";
7
7
  import "./chunk-N5HCQIBB.mjs";
8
8
 
@@ -46,9 +46,9 @@ function spliceGeneratedCss(root, generatedCss) {
46
46
  function kbachPostcss(options = {}) {
47
47
  return {
48
48
  postcssPlugin: "kbach",
49
- Once(root, { result }) {
49
+ async Once(root, { result }) {
50
50
  const projectRoot = options.root ?? process.cwd();
51
- const theme = resolveEffectiveTheme(projectRoot, options);
51
+ const theme = await resolveEffectiveTheme(projectRoot, options);
52
52
  const includeDirs = options.include ?? DEFAULT_SCAN_DIRS;
53
53
  const safelist = options.safelist ?? [];
54
54
  const engine = createKbachStaticCssEngine({
@@ -29,37 +29,76 @@ interface KbachPluginOptions {
29
29
  safelist?: string[];
30
30
  /**
31
31
  * Path to the `kbach:start`/`kbach:end` marker file, relative to Vite's
32
- * resolved root. Defaults to `"src/kbach.css"` — override this for any
33
- * project whose source root isn't `src/`: React Router v7's framework
34
- * mode (`app/kbach.css`), a Pages-Router-style Next.js app using this
35
- * plugin directly (`styles/kbach.css`), or any other convention. Getting
36
- * this wrong fails loudly at build start (`ENOENT` writing the file's
37
- * parent directory) rather than silently generating nothing, but it's
38
- * still worth setting explicitly if your project doesn't use `src/`.
32
+ * resolved root. Not needed for the common case — `kbach()` auto-detects
33
+ * it (see `findMainCSSFile`'s own doc comment) by finding wherever you
34
+ * actually put the two marker comments, regardless of whether your
35
+ * project's source root is `src/`, `app/` (React Router v7's framework
36
+ * mode), or anything else in `include`. Set this explicitly only to
37
+ * disambiguate a genuinely unusual layout (a marker file outside every
38
+ * `include` directory, or more than one candidate present at once).
39
39
  */
40
40
  cssFile?: string;
41
41
  }
42
+ /**
43
+ * Finds where to read/write the `kbach:start`/`kbach:end` marker file,
44
+ * with no configuration required for the common case:
45
+ *
46
+ * 1. An explicit `cssFile` always wins (the one supported escape hatch).
47
+ * 2. Otherwise, look for a `kbach.css` already containing BOTH markers in
48
+ * each `include` directory (in order) and the project root itself —
49
+ * wherever you actually created it is where this plugin keeps using it,
50
+ * regardless of which framework convention (`src/`, `app/`, ...) that
51
+ * is. This is what makes React Router v7's framework mode (`app/
52
+ * kbach.css`) and a plain Vite app (`src/kbach.css`) both "just work"
53
+ * with the exact same `kbach()` call — no per-framework option needed.
54
+ * 3. No marked file exists yet (first run in a fresh project) — pick the
55
+ * first `include` directory that already exists as a real directory on
56
+ * disk, so the file this plugin is about to CREATE lands somewhere that
57
+ * won't need its own parent directory made up out of nothing.
58
+ * 4. Nothing in `include` exists yet either (a genuinely empty project,
59
+ * scaffolded before its first source file) — fall back to the first
60
+ * configured `include` entry regardless; `writeMainCSSFile` (below)
61
+ * creates its parent directory unconditionally, so this never crashes
62
+ * even here.
63
+ */
64
+ declare function findMainCSSFile(root: string, includeDirs: string[], explicitCssFile?: string): string;
42
65
  declare const DEFAULT_SCAN_DIRS: string[];
43
66
  declare const CSS_SYNC_DEBOUNCE_MS = 50;
44
67
  /**
45
- * Auto-discovers `kbach.config.js` at the project root — `undefined` if it
46
- * doesn't exist (the common case: most projects have no customization at
68
+ * Auto-discovers a `kbach.config.*` file at the project root — `undefined`
69
+ * if none exists (the common case: most projects have no customization at
47
70
  * all, so this is a silent, expected no-op, not a warning).
48
71
  *
49
- * Uses `require()` (a real project's `kbach.config.js` is plain CommonJS,
50
- * `module.exports = {...}`, matching every example in this package's own
51
- * README), built via `createRequire(root)` rather than
72
+ * Checks three filenames, in order, mirroring Node's own module-format
73
+ * conventions rather than assuming one: `kbach.config.cjs` (always
74
+ * CommonJS, `module.exports = {...}` — the one unambiguous choice
75
+ * regardless of the project's package.json) and `kbach.config.mjs`
76
+ * (always real ESM, `export default {...}`) are tried first, each loaded
77
+ * the one way that's guaranteed correct for that extension. Only the bare
78
+ * `kbach.config.js` — whose actual meaning depends on the nearest
79
+ * package.json's `"type"` field, exactly like every other `.js` file in
80
+ * the project — needs the fallback below: try `require()` first (correct
81
+ * for `"type": "commonjs"`/no `"type"` field, the more common case), and
82
+ * only on a failure that specifically indicates an ESM/CJS mismatch (see
83
+ * `isEsmRequireFailure`), retry via a real dynamic `import()` instead. A
84
+ * genuine syntax error in an otherwise-correctly-formatted config file is
85
+ * NOT caught here — it propagates as a real, actionable build failure,
86
+ * rather than being silently swallowed into a confusing second attempt.
87
+ *
88
+ * `require()` is built via `createRequire(root)` rather than
52
89
  * `createRequire(import.meta.url)` — this file is written as ESM source,
53
90
  * but tsup's CJS build output has no real `import.meta.url` (esbuild
54
91
  * leaves it `undefined` there), which would make `createRequire` throw for
55
92
  * anyone consuming this package's `require('@kbach/react/vite')` entry.
56
93
  * `root` works as `createRequire`'s base in either build: it's always an
57
- * absolute, already-`existsSync`-validated-by-caller directory, and
58
- * `require()` given `path` (itself already absolute) doesn't consult the
59
- * base for resolution anyway — the base only matters for a RELATIVE
60
- * specifier, which this never passes it.
94
+ * absolute, already-`existsSync`-validated directory, and `require()`
95
+ * given an already-absolute `path` doesn't consult the base for
96
+ * resolution anyway — the base only matters for a relative specifier,
97
+ * which this never passes it. `import()` needs a real `file://` URL, not
98
+ * a bare Windows path (`C:\...` isn't a valid ESM specifier) — hence
99
+ * `pathToFileURL`.
61
100
  */
62
- declare function loadConfigFile(root: string): KbachConfig | undefined;
101
+ declare function loadConfigFile(root: string): Promise<KbachConfig | undefined>;
63
102
  /**
64
103
  * Resolves the theme to use, applying the same "explicit theme/config
65
104
  * always wins over kbach.config.js auto-discovery" precedence both
@@ -68,16 +107,17 @@ declare function loadConfigFile(root: string): KbachConfig | undefined;
68
107
  declare function resolveEffectiveTheme(root: string, options: {
69
108
  theme?: ThemeConfig;
70
109
  config?: KbachConfig;
71
- }): ThemeConfig;
110
+ }): Promise<ThemeConfig>;
72
111
  /**
73
112
  * Build-time static CSS generation for Vite — scans source files for
74
113
  * Kbach class strings, resolves them through the same Rust engine the
75
114
  * runtime uses (via the Node-target WASM build, see wasmNode.ts), and
76
- * writes a real kbach.css. Simplified vs old-kbach's version: expects a
77
- * marker file to already exist at a fixed conventional path — `src/
78
- * kbach.css` by default, override via `cssFile` for any other source root
79
- * (create it with `/* kbach:start *\/` / `/* kbach:end *\/` markers)
80
- * rather than scanning the whole project for a file named kbach.css.
115
+ * writes a real kbach.css. Auto-detects WHERE that file is (or should be
116
+ * created) via `findMainCSSFile` — no per-framework config needed for
117
+ * `src/`, `app/` (React Router v7's framework mode), or any other
118
+ * `include` convention; create a file with `/* kbach:start *\/` / `/*
119
+ * kbach:end *\/` markers wherever your project's source actually lives
120
+ * and this plugin finds it there.
81
121
  *
82
122
  * The actual scanning/resolution/generation logic lives in
83
123
  * `staticCss/engine.ts`, shared with `postcss-plugin/index.ts` (Next.js/
@@ -87,4 +127,4 @@ declare function resolveEffectiveTheme(root: string, options: {
87
127
  */
88
128
  declare function kbach(options?: KbachPluginOptions): Plugin;
89
129
 
90
- export { CSS_SYNC_DEBOUNCE_MS, DEFAULT_SCAN_DIRS, type KbachPluginOptions, kbach, loadConfigFile, resolveEffectiveTheme };
130
+ export { CSS_SYNC_DEBOUNCE_MS, DEFAULT_SCAN_DIRS, type KbachPluginOptions, findMainCSSFile, kbach, loadConfigFile, resolveEffectiveTheme };
@@ -29,37 +29,76 @@ interface KbachPluginOptions {
29
29
  safelist?: string[];
30
30
  /**
31
31
  * Path to the `kbach:start`/`kbach:end` marker file, relative to Vite's
32
- * resolved root. Defaults to `"src/kbach.css"` — override this for any
33
- * project whose source root isn't `src/`: React Router v7's framework
34
- * mode (`app/kbach.css`), a Pages-Router-style Next.js app using this
35
- * plugin directly (`styles/kbach.css`), or any other convention. Getting
36
- * this wrong fails loudly at build start (`ENOENT` writing the file's
37
- * parent directory) rather than silently generating nothing, but it's
38
- * still worth setting explicitly if your project doesn't use `src/`.
32
+ * resolved root. Not needed for the common case — `kbach()` auto-detects
33
+ * it (see `findMainCSSFile`'s own doc comment) by finding wherever you
34
+ * actually put the two marker comments, regardless of whether your
35
+ * project's source root is `src/`, `app/` (React Router v7's framework
36
+ * mode), or anything else in `include`. Set this explicitly only to
37
+ * disambiguate a genuinely unusual layout (a marker file outside every
38
+ * `include` directory, or more than one candidate present at once).
39
39
  */
40
40
  cssFile?: string;
41
41
  }
42
+ /**
43
+ * Finds where to read/write the `kbach:start`/`kbach:end` marker file,
44
+ * with no configuration required for the common case:
45
+ *
46
+ * 1. An explicit `cssFile` always wins (the one supported escape hatch).
47
+ * 2. Otherwise, look for a `kbach.css` already containing BOTH markers in
48
+ * each `include` directory (in order) and the project root itself —
49
+ * wherever you actually created it is where this plugin keeps using it,
50
+ * regardless of which framework convention (`src/`, `app/`, ...) that
51
+ * is. This is what makes React Router v7's framework mode (`app/
52
+ * kbach.css`) and a plain Vite app (`src/kbach.css`) both "just work"
53
+ * with the exact same `kbach()` call — no per-framework option needed.
54
+ * 3. No marked file exists yet (first run in a fresh project) — pick the
55
+ * first `include` directory that already exists as a real directory on
56
+ * disk, so the file this plugin is about to CREATE lands somewhere that
57
+ * won't need its own parent directory made up out of nothing.
58
+ * 4. Nothing in `include` exists yet either (a genuinely empty project,
59
+ * scaffolded before its first source file) — fall back to the first
60
+ * configured `include` entry regardless; `writeMainCSSFile` (below)
61
+ * creates its parent directory unconditionally, so this never crashes
62
+ * even here.
63
+ */
64
+ declare function findMainCSSFile(root: string, includeDirs: string[], explicitCssFile?: string): string;
42
65
  declare const DEFAULT_SCAN_DIRS: string[];
43
66
  declare const CSS_SYNC_DEBOUNCE_MS = 50;
44
67
  /**
45
- * Auto-discovers `kbach.config.js` at the project root — `undefined` if it
46
- * doesn't exist (the common case: most projects have no customization at
68
+ * Auto-discovers a `kbach.config.*` file at the project root — `undefined`
69
+ * if none exists (the common case: most projects have no customization at
47
70
  * all, so this is a silent, expected no-op, not a warning).
48
71
  *
49
- * Uses `require()` (a real project's `kbach.config.js` is plain CommonJS,
50
- * `module.exports = {...}`, matching every example in this package's own
51
- * README), built via `createRequire(root)` rather than
72
+ * Checks three filenames, in order, mirroring Node's own module-format
73
+ * conventions rather than assuming one: `kbach.config.cjs` (always
74
+ * CommonJS, `module.exports = {...}` — the one unambiguous choice
75
+ * regardless of the project's package.json) and `kbach.config.mjs`
76
+ * (always real ESM, `export default {...}`) are tried first, each loaded
77
+ * the one way that's guaranteed correct for that extension. Only the bare
78
+ * `kbach.config.js` — whose actual meaning depends on the nearest
79
+ * package.json's `"type"` field, exactly like every other `.js` file in
80
+ * the project — needs the fallback below: try `require()` first (correct
81
+ * for `"type": "commonjs"`/no `"type"` field, the more common case), and
82
+ * only on a failure that specifically indicates an ESM/CJS mismatch (see
83
+ * `isEsmRequireFailure`), retry via a real dynamic `import()` instead. A
84
+ * genuine syntax error in an otherwise-correctly-formatted config file is
85
+ * NOT caught here — it propagates as a real, actionable build failure,
86
+ * rather than being silently swallowed into a confusing second attempt.
87
+ *
88
+ * `require()` is built via `createRequire(root)` rather than
52
89
  * `createRequire(import.meta.url)` — this file is written as ESM source,
53
90
  * but tsup's CJS build output has no real `import.meta.url` (esbuild
54
91
  * leaves it `undefined` there), which would make `createRequire` throw for
55
92
  * anyone consuming this package's `require('@kbach/react/vite')` entry.
56
93
  * `root` works as `createRequire`'s base in either build: it's always an
57
- * absolute, already-`existsSync`-validated-by-caller directory, and
58
- * `require()` given `path` (itself already absolute) doesn't consult the
59
- * base for resolution anyway — the base only matters for a RELATIVE
60
- * specifier, which this never passes it.
94
+ * absolute, already-`existsSync`-validated directory, and `require()`
95
+ * given an already-absolute `path` doesn't consult the base for
96
+ * resolution anyway — the base only matters for a relative specifier,
97
+ * which this never passes it. `import()` needs a real `file://` URL, not
98
+ * a bare Windows path (`C:\...` isn't a valid ESM specifier) — hence
99
+ * `pathToFileURL`.
61
100
  */
62
- declare function loadConfigFile(root: string): KbachConfig | undefined;
101
+ declare function loadConfigFile(root: string): Promise<KbachConfig | undefined>;
63
102
  /**
64
103
  * Resolves the theme to use, applying the same "explicit theme/config
65
104
  * always wins over kbach.config.js auto-discovery" precedence both
@@ -68,16 +107,17 @@ declare function loadConfigFile(root: string): KbachConfig | undefined;
68
107
  declare function resolveEffectiveTheme(root: string, options: {
69
108
  theme?: ThemeConfig;
70
109
  config?: KbachConfig;
71
- }): ThemeConfig;
110
+ }): Promise<ThemeConfig>;
72
111
  /**
73
112
  * Build-time static CSS generation for Vite — scans source files for
74
113
  * Kbach class strings, resolves them through the same Rust engine the
75
114
  * runtime uses (via the Node-target WASM build, see wasmNode.ts), and
76
- * writes a real kbach.css. Simplified vs old-kbach's version: expects a
77
- * marker file to already exist at a fixed conventional path — `src/
78
- * kbach.css` by default, override via `cssFile` for any other source root
79
- * (create it with `/* kbach:start *\/` / `/* kbach:end *\/` markers)
80
- * rather than scanning the whole project for a file named kbach.css.
115
+ * writes a real kbach.css. Auto-detects WHERE that file is (or should be
116
+ * created) via `findMainCSSFile` — no per-framework config needed for
117
+ * `src/`, `app/` (React Router v7's framework mode), or any other
118
+ * `include` convention; create a file with `/* kbach:start *\/` / `/*
119
+ * kbach:end *\/` markers wherever your project's source actually lives
120
+ * and this plugin finds it there.
81
121
  *
82
122
  * The actual scanning/resolution/generation logic lives in
83
123
  * `staticCss/engine.ts`, shared with `postcss-plugin/index.ts` (Next.js/
@@ -87,4 +127,4 @@ declare function resolveEffectiveTheme(root: string, options: {
87
127
  */
88
128
  declare function kbach(options?: KbachPluginOptions): Plugin;
89
129
 
90
- export { CSS_SYNC_DEBOUNCE_MS, DEFAULT_SCAN_DIRS, type KbachPluginOptions, kbach, loadConfigFile, resolveEffectiveTheme };
130
+ export { CSS_SYNC_DEBOUNCE_MS, DEFAULT_SCAN_DIRS, type KbachPluginOptions, findMainCSSFile, kbach, loadConfigFile, resolveEffectiveTheme };
@@ -22,6 +22,7 @@ var vite_plugin_exports = {};
22
22
  __export(vite_plugin_exports, {
23
23
  CSS_SYNC_DEBOUNCE_MS: () => CSS_SYNC_DEBOUNCE_MS,
24
24
  DEFAULT_SCAN_DIRS: () => DEFAULT_SCAN_DIRS,
25
+ findMainCSSFile: () => findMainCSSFile,
25
26
  kbach: () => kbach,
26
27
  loadConfigFile: () => loadConfigFile,
27
28
  resolveEffectiveTheme: () => resolveEffectiveTheme
@@ -29,6 +30,7 @@ __export(vite_plugin_exports, {
29
30
  module.exports = __toCommonJS(vite_plugin_exports);
30
31
  var import_fs3 = require("fs");
31
32
  var import_path4 = require("path");
33
+ var import_url = require("url");
32
34
  var import_module = require("module");
33
35
 
34
36
  // src/vite-plugin/cssVars.ts
@@ -1754,22 +1756,63 @@ function readSourceFile(path) {
1754
1756
  }
1755
1757
 
1756
1758
  // src/vite-plugin/index.ts
1759
+ function findMainCSSFile(root, includeDirs, explicitCssFile) {
1760
+ if (explicitCssFile) return (0, import_path4.join)(root, explicitCssFile);
1761
+ const searchDirs = [...includeDirs, "."];
1762
+ for (const dir of searchDirs) {
1763
+ const candidate = (0, import_path4.join)(root, dir, "kbach.css");
1764
+ if (!(0, import_fs3.existsSync)(candidate)) continue;
1765
+ try {
1766
+ const content = (0, import_fs3.readFileSync)(candidate, "utf-8");
1767
+ if (content.includes(KBACH_START) && content.includes(KBACH_END)) return candidate;
1768
+ } catch {
1769
+ }
1770
+ }
1771
+ for (const dir of includeDirs) {
1772
+ const full = (0, import_path4.join)(root, dir);
1773
+ try {
1774
+ if ((0, import_fs3.statSync)(full).isDirectory()) return (0, import_path4.join)(full, "kbach.css");
1775
+ } catch {
1776
+ }
1777
+ }
1778
+ return (0, import_path4.join)(root, includeDirs[0] ?? "src", "kbach.css");
1779
+ }
1757
1780
  var DEFAULT_SCAN_DIRS = ["src", "app", "pages", "components"];
1758
1781
  var CSS_SYNC_DEBOUNCE_MS = 50;
1759
1782
  function writeFileEnsuringDir(path, content) {
1760
1783
  (0, import_fs3.mkdirSync)((0, import_path4.dirname)(path), { recursive: true });
1761
1784
  (0, import_fs3.writeFileSync)(path, content, "utf-8");
1762
1785
  }
1763
- function loadConfigFile(root) {
1764
- const path = (0, import_path4.join)(root, "kbach.config.js");
1765
- if (!(0, import_fs3.existsSync)(path)) return void 0;
1786
+ function isEsmRequireFailure(err) {
1787
+ if (!(err instanceof Error)) return false;
1788
+ return err.code === "ERR_REQUIRE_ESM" || /is not defined in ES module scope/.test(err.message);
1789
+ }
1790
+ async function importConfigModule(path) {
1791
+ const mod = await import((0, import_url.pathToFileURL)(path).href);
1792
+ return mod.default ?? mod;
1793
+ }
1794
+ function requireConfigModule(root, path) {
1766
1795
  const mod = (0, import_module.createRequire)(root)(path);
1767
1796
  return mod.default ?? mod;
1768
1797
  }
1769
- function resolveEffectiveTheme(root, options) {
1798
+ async function loadConfigFile(root) {
1799
+ const cjsPath = (0, import_path4.join)(root, "kbach.config.cjs");
1800
+ if ((0, import_fs3.existsSync)(cjsPath)) return requireConfigModule(root, cjsPath);
1801
+ const mjsPath = (0, import_path4.join)(root, "kbach.config.mjs");
1802
+ if ((0, import_fs3.existsSync)(mjsPath)) return importConfigModule(mjsPath);
1803
+ const jsPath = (0, import_path4.join)(root, "kbach.config.js");
1804
+ if (!(0, import_fs3.existsSync)(jsPath)) return void 0;
1805
+ try {
1806
+ return requireConfigModule(root, jsPath);
1807
+ } catch (err) {
1808
+ if (!isEsmRequireFailure(err)) throw err;
1809
+ return importConfigModule(jsPath);
1810
+ }
1811
+ }
1812
+ async function resolveEffectiveTheme(root, options) {
1770
1813
  if (options.theme) return options.theme;
1771
1814
  if (options.config) return resolveKbachConfig(options.config);
1772
- const discovered = loadConfigFile(root);
1815
+ const discovered = await loadConfigFile(root);
1773
1816
  return discovered ? resolveKbachConfig(discovered) : defaultTheme;
1774
1817
  }
1775
1818
  function kbach(options = {}) {
@@ -1778,8 +1821,9 @@ function kbach(options = {}) {
1778
1821
  let root = process.cwd();
1779
1822
  let theme = defaultTheme;
1780
1823
  let engine = createKbachStaticCssEngine({ root, theme, themeJson: JSON.stringify(theme), includeDirs, safelist });
1824
+ let mainCSSFilePath = (0, import_path4.join)(root, "src", "kbach.css");
1781
1825
  function mainCSSFile() {
1782
- return (0, import_path4.join)(root, options.cssFile ?? "src/kbach.css");
1826
+ return mainCSSFilePath;
1783
1827
  }
1784
1828
  function classNameHintsFile() {
1785
1829
  return (0, import_path4.join)(root, ".kbach", "kbach-classnames.d.ts");
@@ -1789,7 +1833,7 @@ function kbach(options = {}) {
1789
1833
  writeFileEnsuringDir(classNameHintsFile(), dts);
1790
1834
  }
1791
1835
  function syncMainCSSFile(server) {
1792
- const changed = writeKbachToFile(mainCSSFile(), engine.generateCSS(), readSourceFile, (p, c) => (0, import_fs3.writeFileSync)(p, c, "utf-8"));
1836
+ const changed = writeKbachToFile(mainCSSFile(), engine.generateCSS(), readSourceFile, writeFileEnsuringDir);
1793
1837
  if (changed && server) server.watcher.emit("change", mainCSSFile());
1794
1838
  }
1795
1839
  let syncTimer = null;
@@ -1803,16 +1847,17 @@ function kbach(options = {}) {
1803
1847
  return {
1804
1848
  name: "kbach",
1805
1849
  enforce: "pre",
1806
- configResolved(resolved) {
1850
+ async configResolved(resolved) {
1807
1851
  root = resolved.root;
1808
- theme = resolveEffectiveTheme(root, options);
1852
+ theme = await resolveEffectiveTheme(root, options);
1809
1853
  engine = createKbachStaticCssEngine({ root, theme, themeJson: JSON.stringify(theme), includeDirs, safelist });
1854
+ mainCSSFilePath = findMainCSSFile(root, includeDirs, options.cssFile);
1810
1855
  },
1811
1856
  buildStart() {
1812
1857
  engine.scanProjectCssSelectorsOnce();
1813
1858
  engine.initialScan();
1814
1859
  engine.processSafelist();
1815
- writeKbachToFile(mainCSSFile(), engine.generateCSS(), readSourceFile, (p, c) => (0, import_fs3.writeFileSync)(p, c, "utf-8"));
1860
+ writeKbachToFile(mainCSSFile(), engine.generateCSS(), readSourceFile, writeFileEnsuringDir);
1816
1861
  writeClassNameHints();
1817
1862
  },
1818
1863
  // handleHotUpdate covers edits; add/unlink (create/delete) land on the
@@ -1852,6 +1897,7 @@ function kbach(options = {}) {
1852
1897
  0 && (module.exports = {
1853
1898
  CSS_SYNC_DEBOUNCE_MS,
1854
1899
  DEFAULT_SCAN_DIRS,
1900
+ findMainCSSFile,
1855
1901
  kbach,
1856
1902
  loadConfigFile,
1857
1903
  resolveEffectiveTheme
@@ -1,15 +1,17 @@
1
1
  import {
2
2
  CSS_SYNC_DEBOUNCE_MS,
3
3
  DEFAULT_SCAN_DIRS,
4
+ findMainCSSFile,
4
5
  kbach,
5
6
  loadConfigFile,
6
7
  resolveEffectiveTheme
7
- } from "./chunk-P4CQSYIL.mjs";
8
+ } from "./chunk-7JBSTM5D.mjs";
8
9
  import "./chunk-NKR3PSWO.mjs";
9
10
  import "./chunk-N5HCQIBB.mjs";
10
11
  export {
11
12
  CSS_SYNC_DEBOUNCE_MS,
12
13
  DEFAULT_SCAN_DIRS,
14
+ findMainCSSFile,
13
15
  kbach,
14
16
  loadConfigFile,
15
17
  resolveEffectiveTheme
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "1.0.0-beta.7",
3
+ "version": "1.0.0-beta.9",
4
4
  "description": "A utility-first styling engine for React, powered by a real Rust/WASM core — Tailwind CSS v4 feature parity with Kbach's own color palette.",
5
5
  "keywords": ["css", "react", "tailwind", "utility-first", "wasm", "styling"],
6
6
  "license": "MIT",