@kuratchi/js 0.0.20 → 0.0.22

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
@@ -63,12 +63,12 @@ Route files are not client files. They are server-rendered routes that can opt i
63
63
 
64
64
  ### Route file structure
65
65
 
66
- ```html
67
- <script>
68
- import { getItems, addItem, deleteItem } from '$database/items';
69
-
70
- const items = await getItems();
71
- </script>
66
+ ```html
67
+ <script>
68
+ import { getItems, addItem, deleteItem } from '$server/items';
69
+
70
+ const items = await getItems();
71
+ </script>
72
72
 
73
73
  <!-- Template — plain HTML with minimal extensions -->
74
74
  <ul>
@@ -78,8 +78,8 @@ Route files are not client files. They are server-rendered routes that can opt i
78
78
  </ul>
79
79
  ```
80
80
 
81
- The `$database/` alias resolves to `src/database/`. You can use any path alias configured in your tsconfig.
82
- Private server logic should live in `src/server/` and be imported into routes explicitly.
81
+ The `$server/` alias resolves to `src/server/`. Use that as the canonical home for reusable server-only modules.
82
+ Private server logic should live in `src/server/` and be imported into routes explicitly.
83
83
 
84
84
  ### Layout file
85
85
 
@@ -132,6 +132,40 @@ for (const item of items) {
132
132
  }
133
133
  ```
134
134
 
135
+ ### Attribute expressions
136
+
137
+ Use `{expression}` in attribute values for dynamic content:
138
+
139
+ ```html
140
+ <!-- Ternary expressions -->
141
+ <div class={isActive ? 'active' : 'inactive'}>...</div>
142
+ <button class={count > 0 ? 'has-items' : ''}>View ({count})</button>
143
+
144
+ <!-- Any JS expression -->
145
+ <a href={`/items/${item.id}`}>{item.name}</a>
146
+ <img src={user.avatar} alt={user.name} />
147
+ ```
148
+
149
+ ### Boolean attributes
150
+
151
+ Boolean attributes like `disabled`, `checked`, `selected`, etc. are conditionally rendered based on the expression value:
152
+
153
+ ```html
154
+ <!-- Renders: <button disabled> or <button> -->
155
+ <button disabled={isLoading}>Submit</button>
156
+
157
+ <!-- Form elements -->
158
+ <input type="checkbox" checked={todo.completed} />
159
+ <option selected={item.id === selectedId}>{item.name}</option>
160
+
161
+ <!-- Other boolean attributes -->
162
+ <details open={showDetails}>...</details>
163
+ <input readonly={!canEdit} />
164
+ <input required={isRequired} />
165
+ ```
166
+
167
+ Supported boolean attributes: `disabled`, `checked`, `selected`, `readonly`, `required`, `hidden`, `open`, `autofocus`, `autoplay`, `controls`, `default`, `defer`, `formnovalidate`, `inert`, `loop`, `multiple`, `muted`, `novalidate`, `reversed`, `async`.
168
+
135
169
  ### Components
136
170
 
137
171
  Import `.html` components from your `src/lib/` directory or from packages:
@@ -241,10 +275,10 @@ Failure and edge behavior:
241
275
 
242
276
  Export server functions from a route's `<script>` block and reference them with `action={fn}`. The compiler automatically registers them as dispatchable actions.
243
277
 
244
- ```html
245
- <script>
246
- import { addItem, deleteItem } from '$database/items';
247
- </script>
278
+ ```html
279
+ <script>
280
+ import { addItem, deleteItem } from '$server/items';
281
+ </script>
248
282
 
249
283
  <!-- Standard form — POST-Redirect-GET -->
250
284
  <form action={addItem} method="POST">
@@ -256,8 +290,8 @@ Export server functions from a route's `<script>` block and reference them with
256
290
  The action function receives the raw `FormData`. Throw `ActionError` to surface a message back to the form — see [Error handling](#error-handling).
257
291
 
258
292
  ```ts
259
- // src/database/items.ts
260
- import { ActionError } from '@kuratchi/js';
293
+ // src/server/items.ts
294
+ import { ActionError } from '@kuratchi/js';
261
295
 
262
296
  export async function addItem(formData: FormData): Promise<void> {
263
297
  const title = (formData.get('title') as string)?.trim();
@@ -303,10 +337,10 @@ export async function signIn(formData: FormData) {
303
337
 
304
338
  In the template, the action's state object is available under its function name:
305
339
 
306
- ```html
307
- <script>
308
- import { signIn } from '$database/auth';
309
- </script>
340
+ ```html
341
+ <script>
342
+ import { signIn } from '$server/auth';
343
+ </script>
310
344
 
311
345
  <form action={signIn}>
312
346
  (signIn.error ? `<p class="error">${signIn.error}</p>` : '')
@@ -364,16 +398,18 @@ for (const rec of (recommendations ?? [])) {
364
398
 
365
399
  These `data-*` attributes wire up client-side interactivity without writing JavaScript.
366
400
 
367
- ### `data-action` — fetch action (no page reload)
368
-
369
- Calls a server action via `fetch` and refreshes `data-refresh` targets when done:
370
-
371
- ```html
372
- <button data-action="deleteItem" data-args={JSON.stringify([item.id])}>Delete</button>
373
- <button data-action="toggleItem" data-args={JSON.stringify([item.id, true])}>Done</button>
374
- ```
375
-
376
- The action function receives the args array as individual arguments:
401
+ ### `data-post` — client-triggered server action
402
+
403
+ Use `data-post={fn(args)}` for button-style server actions without exposing compiler transport details in templates:
404
+
405
+ ```html
406
+ <button data-post={deleteItem(item.id)} data-refresh="" type="button">Delete</button>
407
+ <button data-post={toggleItem(item.id, true)} data-refresh="" type="button">Done</button>
408
+ ```
409
+
410
+ Kuratchi lowers `data-post` into its internal action transport automatically. Authored `data-action` / `data-args` attributes are deprecated and should be treated as compiler output only.
411
+
412
+ The action function receives the direct arguments you passed in the template:
377
413
 
378
414
  ```ts
379
415
  export async function deleteItem(id: number): Promise<void> {
@@ -387,7 +423,7 @@ export async function toggleItem(id: number, done: boolean): Promise<void> {
387
423
 
388
424
  ### `data-refresh` — partial refresh
389
425
 
390
- After a `data-action` call succeeds, elements with `data-refresh` re-fetch their content:
426
+ After a `data-post` call succeeds, elements with `data-refresh` re-fetch their content:
391
427
 
392
428
  ```html
393
429
  <section data-refresh="/items">
@@ -831,12 +867,21 @@ import {
831
867
  } from '@kuratchi/js';
832
868
  ```
833
869
 
870
+ ### Virtual Modules
871
+
872
+ Kuratchi provides virtual modules for request-scoped state. Use these in route files:
873
+
874
+ | Virtual Module | Description |
875
+ |----------------|-------------|
876
+ | `kuratchi:request` | Request state: `url`, `params`, `searchParams`, `headers`, `locals`, etc. |
877
+ | `kuratchi:navigation` | Server-side redirect helper |
878
+
834
879
  ### Request helpers
835
880
 
836
- For a batteries-included request layer, import pre-parsed request state from `@kuratchi/js/request`:
881
+ For a batteries-included request layer, import pre-parsed request state from `kuratchi:request`:
837
882
 
838
883
  ```ts
839
- import { url, pathname, searchParams, params, slug } from '@kuratchi/js/request';
884
+ import { url, pathname, searchParams, params, slug, locals } from 'kuratchi:request';
840
885
 
841
886
  const page = pathname;
842
887
  const tab = searchParams.get('tab');
@@ -849,9 +894,23 @@ const postSlug = slug;
849
894
  - `searchParams` is `url.searchParams` for the current request.
850
895
  - `params` is the matched route params object, like `{ slug: 'hello-world' }`.
851
896
  - `slug` is `params.slug` when the matched route defines a `slug` param.
852
- - `headers` and `method` are also exported from `@kuratchi/js/request`.
853
- - `params` is not ambient; import it from `@kuratchi/js/request` or use `getParams()` / `getParam()` from `@kuratchi/js`.
854
- - Use `getRequest()` when you want the raw native `Request` object.
897
+ - `headers` and `method` are also exported from `kuratchi:request`.
898
+ - `locals` is the request-scoped locals object (typed via `App.Locals` in `app.d.ts`).
899
+ - Use `getRequest()` from `@kuratchi/js` when you want the raw native `Request` object.
900
+
901
+ ### Server-side redirect
902
+
903
+ Import `redirect` from `kuratchi:navigation` for server-side redirects:
904
+
905
+ ```ts
906
+ import { redirect } from 'kuratchi:navigation';
907
+
908
+ // Redirect to another page (throws RedirectError, caught by framework)
909
+ redirect('/dashboard');
910
+ redirect('/login', 302);
911
+ ```
912
+
913
+ `redirect()` works in route scripts, `$server/` modules, and form actions. It throws a `RedirectError` that the framework catches and converts to a proper HTTP redirect response (default 303 for POST-Redirect-GET).
855
914
 
856
915
  ## Runtime Hook
857
916
 
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * CLI entry point — kuratchi build | watch | create
3
+ * CLI entry point kuratchi build | watch | create
4
4
  */
5
5
  export {};
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * CLI entry point — kuratchi build | watch | create
3
+ * CLI entry point kuratchi build | watch | create
4
4
  */
5
5
  import { compile } from './compiler/index.js';
6
6
  import * as path from 'node:path';
@@ -11,6 +11,7 @@ import { spawn } from 'node:child_process';
11
11
  const args = process.argv.slice(2);
12
12
  const command = args[0];
13
13
  const projectDir = process.cwd();
14
+ let cachedScriptRuntimeExecutable = null;
14
15
  void main().catch((err) => {
15
16
  console.error(`[kuratchi] ${err?.message ?? err}`);
16
17
  process.exit(1);
@@ -29,15 +30,19 @@ async function main() {
29
30
  case 'create':
30
31
  await runCreate();
31
32
  return;
33
+ case 'types':
34
+ await runTypes();
35
+ return;
32
36
  default:
33
- console.log(`
34
- KuratchiJS CLI
35
-
36
- Usage:
37
- kuratchi create [name] Scaffold a new KuratchiJS project
38
- kuratchi build Compile routes once
39
- kuratchi dev Compile, watch for changes, and start wrangler dev server
40
- kuratchi watch Compile + watch only (no wrangler for custom setups)
37
+ console.log(`
38
+ KuratchiJS CLI
39
+
40
+ Usage:
41
+ kuratchi create [name] Scaffold a new KuratchiJS project
42
+ kuratchi build Compile routes once
43
+ kuratchi dev Compile, watch for changes, and start wrangler dev server
44
+ kuratchi watch Compile + watch only (no wrangler for custom setups)
45
+ kuratchi types Generate TypeScript types from schema to src/app.d.ts
41
46
  `);
42
47
  process.exit(1);
43
48
  }
@@ -49,11 +54,15 @@ async function runCreate() {
49
54
  const positional = remaining.filter(a => !a.startsWith('-'));
50
55
  await create(positional[0], flags);
51
56
  }
57
+ async function runTypes() {
58
+ const { writeAppTypes } = await import('./compiler/type-generator.js');
59
+ writeAppTypes({ projectDir });
60
+ }
52
61
  async function runBuild(isDev = false) {
53
62
  console.log('[kuratchi] Compiling...');
54
63
  try {
55
64
  const outFile = await compile({ projectDir, isDev });
56
- console.log(`[kuratchi] Built → ${path.relative(projectDir, outFile)}`);
65
+ console.log(`[kuratchi] Built -> ${path.relative(projectDir, outFile)}`);
57
66
  }
58
67
  catch (err) {
59
68
  console.error(`[kuratchi] Build failed: ${err.message}`);
@@ -62,30 +71,8 @@ async function runBuild(isDev = false) {
62
71
  }
63
72
  async function runWatch(withWrangler = false) {
64
73
  await runBuild(true);
65
- const routesDir = path.join(projectDir, 'src', 'routes');
66
- const serverDir = path.join(projectDir, 'src', 'server');
67
- const watchDirs = [routesDir, serverDir].filter(d => fs.existsSync(d));
68
- let rebuildTimeout = null;
69
- const triggerRebuild = () => {
70
- if (rebuildTimeout)
71
- clearTimeout(rebuildTimeout);
72
- rebuildTimeout = setTimeout(async () => {
73
- console.log('[kuratchi] File changed, rebuilding...');
74
- try {
75
- await compile({ projectDir, isDev: true });
76
- console.log('[kuratchi] Rebuilt.');
77
- }
78
- catch (err) {
79
- console.error(`[kuratchi] Rebuild failed: ${err.message}`);
80
- }
81
- }, 100);
82
- };
83
- for (const dir of watchDirs) {
84
- fs.watch(dir, { recursive: true }, triggerRebuild);
85
- }
74
+ startCompilerWatch();
86
75
  console.log('[kuratchi] Watching for changes...');
87
- // `kuratchi dev` also starts the wrangler dev server.
88
- // `kuratchi watch` is the compiler-only mode for custom setups.
89
76
  if (withWrangler) {
90
77
  await startWranglerDev();
91
78
  return;
@@ -163,16 +150,52 @@ function resolveWranglerBin() {
163
150
  return null;
164
151
  }
165
152
  }
166
- function getNodeExecutable() {
167
- if (!process.versions.bun)
168
- return process.execPath;
169
- return 'node';
153
+ function getScriptRuntimeExecutable() {
154
+ if (cachedScriptRuntimeExecutable)
155
+ return cachedScriptRuntimeExecutable;
156
+ if (!process.versions.bun) {
157
+ cachedScriptRuntimeExecutable = process.execPath;
158
+ return cachedScriptRuntimeExecutable;
159
+ }
160
+ const nodeExecutable = findExecutableOnPath('node');
161
+ cachedScriptRuntimeExecutable = nodeExecutable ?? process.execPath;
162
+ return cachedScriptRuntimeExecutable;
163
+ }
164
+ function startCompilerWatch() {
165
+ const routesDir = path.join(projectDir, 'src', 'routes');
166
+ const serverDir = path.join(projectDir, 'src', 'server');
167
+ const watchDirs = [routesDir, serverDir].filter(d => fs.existsSync(d));
168
+ const watchers = [];
169
+ let rebuildTimeout = null;
170
+ const triggerRebuild = () => {
171
+ if (rebuildTimeout)
172
+ clearTimeout(rebuildTimeout);
173
+ rebuildTimeout = setTimeout(async () => {
174
+ console.log('[kuratchi] File changed, rebuilding...');
175
+ try {
176
+ await compile({ projectDir, isDev: true });
177
+ console.log('[kuratchi] Rebuilt.');
178
+ }
179
+ catch (err) {
180
+ console.error(`[kuratchi] Rebuild failed: ${err.message}`);
181
+ }
182
+ }, 100);
183
+ };
184
+ for (const dir of watchDirs) {
185
+ watchers.push(fs.watch(dir, { recursive: true }, triggerRebuild));
186
+ }
187
+ return () => {
188
+ if (rebuildTimeout)
189
+ clearTimeout(rebuildTimeout);
190
+ for (const watcher of watchers)
191
+ watcher.close();
192
+ };
170
193
  }
171
194
  function spawnWranglerProcess(wranglerArgs) {
172
195
  const localWranglerBin = resolveWranglerBin();
173
196
  const stdio = ['pipe', 'inherit', 'inherit'];
174
197
  if (localWranglerBin) {
175
- return spawn(getNodeExecutable(), [localWranglerBin, ...wranglerArgs], {
198
+ return spawn(getScriptRuntimeExecutable(), [localWranglerBin, ...wranglerArgs], {
176
199
  cwd: projectDir,
177
200
  stdio,
178
201
  });
@@ -183,3 +206,39 @@ function spawnWranglerProcess(wranglerArgs) {
183
206
  stdio,
184
207
  });
185
208
  }
209
+ function findExecutableOnPath(command) {
210
+ const pathEnv = process.env.PATH;
211
+ if (!pathEnv)
212
+ return null;
213
+ const pathEntries = pathEnv.split(path.delimiter).filter(Boolean);
214
+ const extensions = process.platform === 'win32'
215
+ ? (process.env.PATHEXT?.split(';').filter(Boolean) ?? ['.EXE', '.CMD', '.BAT', '.COM'])
216
+ : [''];
217
+ const hasExtension = !!path.extname(command);
218
+ for (const entry of pathEntries) {
219
+ if (hasExtension) {
220
+ const candidate = path.join(entry, command);
221
+ if (isExecutableFile(candidate))
222
+ return candidate;
223
+ continue;
224
+ }
225
+ for (const ext of extensions) {
226
+ const candidate = path.join(entry, `${command}${process.platform === 'win32' ? ext.toLowerCase() : ext}`);
227
+ if (isExecutableFile(candidate))
228
+ return candidate;
229
+ const exactCaseCandidate = path.join(entry, `${command}${ext}`);
230
+ if (exactCaseCandidate !== candidate && isExecutableFile(exactCaseCandidate))
231
+ return exactCaseCandidate;
232
+ }
233
+ }
234
+ return null;
235
+ }
236
+ function isExecutableFile(filePath) {
237
+ try {
238
+ const stat = fs.statSync(filePath);
239
+ return stat.isFile();
240
+ }
241
+ catch {
242
+ return false;
243
+ }
244
+ }
@@ -4,6 +4,7 @@ export interface OrmDatabaseEntry {
4
4
  schemaExportName: string;
5
5
  skipMigrations: boolean;
6
6
  type: 'd1' | 'do';
7
+ remote: boolean;
7
8
  }
8
9
  export interface AuthConfigEntry {
9
10
  cookieName: string;
@@ -97,7 +97,15 @@ export function createComponentCompiler(options) {
97
97
  const scopeOpen = `__parts.push('<div class="${scopeHash}">');`;
98
98
  const scopeClose = `__parts.push('</div>');`;
99
99
  const bodyLines = body.split('\n');
100
- const scopedBody = [bodyLines[0], scopeOpen, ...bodyLines.slice(1), scopeClose].join('\n');
100
+ const insertIndex = bodyLines.findIndex(l => l.startsWith('let __html'));
101
+ const safeInsertIndex = insertIndex === -1 ? bodyLines.length : insertIndex;
102
+ const scopedBody = [
103
+ bodyLines[0],
104
+ scopeOpen,
105
+ ...bodyLines.slice(1, safeInsertIndex),
106
+ scopeClose,
107
+ ...bodyLines.slice(safeInsertIndex)
108
+ ].join('\n');
101
109
  const fnBody = effectivePropsCode ? `${effectivePropsCode}\n ${scopedBody}` : scopedBody;
102
110
  const compiled = `function ${funcName}(props, __esc) {\n ${fnBody}\n return __html;\n}`;
103
111
  compiledComponentCache.set(fileName, compiled);
@@ -1,4 +1,10 @@
1
1
  import { type AuthConfigEntry, type DoConfigEntry, type OrmDatabaseEntry, type SecurityConfigEntry, type WorkerClassConfigEntry } from './compiler-shared.js';
2
+ export interface UiConfigEntry {
3
+ theme: string;
4
+ radius: string;
5
+ library?: 'tailwindcss';
6
+ plugins: string[];
7
+ }
2
8
  export declare function readUiTheme(projectDir: string): string | null;
3
9
  export declare function readUiConfigValues(projectDir: string): {
4
10
  theme: string;
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
3
4
  function skipWhitespace(source, start) {
4
5
  let i = start;
5
6
  while (i < source.length && /\s/.test(source[i]))
@@ -51,7 +52,15 @@ function readConfigBlock(source, key) {
51
52
  }
52
53
  return { kind: 'call-empty', body: '' };
53
54
  }
54
- export function readUiTheme(projectDir) {
55
+ function normalizeUiPluginName(plugin) {
56
+ const normalized = plugin.trim();
57
+ if (!normalized)
58
+ return normalized;
59
+ if (normalized === 'forms')
60
+ return '@tailwindcss/forms';
61
+ return normalized;
62
+ }
63
+ function readUiConfig(projectDir) {
55
64
  const configPath = path.join(projectDir, 'kuratchi.config.ts');
56
65
  if (!fs.existsSync(configPath))
57
66
  return null;
@@ -60,7 +69,78 @@ export function readUiTheme(projectDir) {
60
69
  if (!uiBlock)
61
70
  return null;
62
71
  const themeMatch = uiBlock.body.match(/theme\s*:\s*['"]([^'"]+)['"]/);
63
- const themeValue = themeMatch?.[1] ?? 'default';
72
+ const radiusMatch = uiBlock.body.match(/radius\s*:\s*['"]([^'"]+)['"]/);
73
+ const libraryMatch = uiBlock.body.match(/library\s*:\s*['"]([^'"]+)['"]/);
74
+ const pluginsMatch = uiBlock.body.match(/plugins\s*:\s*\[([\s\S]*?)\]/);
75
+ const plugins = [];
76
+ if (pluginsMatch) {
77
+ const itemRegex = /['"]([^'"]+)['"]/g;
78
+ let pluginMatch;
79
+ while ((pluginMatch = itemRegex.exec(pluginsMatch[1])) !== null) {
80
+ const plugin = normalizeUiPluginName(pluginMatch[1]);
81
+ if (plugin)
82
+ plugins.push(plugin);
83
+ }
84
+ }
85
+ return {
86
+ theme: themeMatch?.[1] ?? 'dark',
87
+ radius: radiusMatch?.[1] ?? 'default',
88
+ library: libraryMatch?.[1] === 'tailwindcss' ? 'tailwindcss' : undefined,
89
+ plugins,
90
+ };
91
+ }
92
+ function findTailwindCliPath(projectDir) {
93
+ const candidates = [
94
+ path.join(projectDir, 'node_modules', '@tailwindcss', 'cli', 'dist', 'index.mjs'),
95
+ path.join(projectDir, 'node_modules', '@tailwindcss', 'cli', 'dist', 'index.js'),
96
+ path.join(projectDir, '..', 'node_modules', '@tailwindcss', 'cli', 'dist', 'index.mjs'),
97
+ path.join(projectDir, '..', 'node_modules', '@tailwindcss', 'cli', 'dist', 'index.js'),
98
+ path.join(projectDir, '..', '..', 'node_modules', '@tailwindcss', 'cli', 'dist', 'index.mjs'),
99
+ path.join(projectDir, '..', '..', 'node_modules', '@tailwindcss', 'cli', 'dist', 'index.js'),
100
+ path.join(path.resolve(projectDir, '../..'), 'node_modules', '@tailwindcss', 'cli', 'dist', 'index.mjs'),
101
+ path.join(path.resolve(projectDir, '../..'), 'node_modules', '@tailwindcss', 'cli', 'dist', 'index.js'),
102
+ ];
103
+ for (const candidate of candidates) {
104
+ if (fs.existsSync(candidate))
105
+ return candidate;
106
+ }
107
+ return null;
108
+ }
109
+ function buildTailwindCss(projectDir, uiConfig) {
110
+ const cliPath = findTailwindCliPath(projectDir);
111
+ if (!cliPath) {
112
+ console.warn('[kuratchi] ui.library: "tailwindcss" configured but @tailwindcss/cli could not be resolved');
113
+ return null;
114
+ }
115
+ const uiDir = path.join(projectDir, '.kuratchi', 'ui');
116
+ if (!fs.existsSync(uiDir))
117
+ fs.mkdirSync(uiDir, { recursive: true });
118
+ const inputPath = path.join(uiDir, 'tailwind.input.css');
119
+ const outputPath = path.join(uiDir, 'tailwind.output.css');
120
+ const sourcePath = path.relative(uiDir, path.join(projectDir, 'src')).replace(/\\/g, '/');
121
+ const configSource = path.relative(uiDir, path.join(projectDir, 'kuratchi.config.ts')).replace(/\\/g, '/');
122
+ const lines = [
123
+ '@import "tailwindcss";',
124
+ `@source "${sourcePath}";`,
125
+ `@source "${configSource}";`,
126
+ ...uiConfig.plugins.map((plugin) => `@plugin "${plugin}";`),
127
+ '',
128
+ ];
129
+ fs.writeFileSync(inputPath, lines.join('\n'), 'utf-8');
130
+ execFileSync(process.execPath, [cliPath, '-i', inputPath, '-o', outputPath], {
131
+ cwd: projectDir,
132
+ stdio: 'pipe',
133
+ });
134
+ return fs.existsSync(outputPath) ? fs.readFileSync(outputPath, 'utf-8') : null;
135
+ }
136
+ export function readUiTheme(projectDir) {
137
+ const uiConfig = readUiConfig(projectDir);
138
+ if (!uiConfig)
139
+ return null;
140
+ if (uiConfig.library === 'tailwindcss') {
141
+ return buildTailwindCss(projectDir, uiConfig);
142
+ }
143
+ const themeValue = uiConfig.theme ?? 'default';
64
144
  if (themeValue === 'default' || themeValue === 'dark' || themeValue === 'light' || themeValue === 'system') {
65
145
  const candidates = [
66
146
  path.join(projectDir, 'node_modules', '@kuratchi/ui', 'src', 'styles', 'theme.css'),
@@ -83,18 +163,12 @@ export function readUiTheme(projectDir) {
83
163
  return null;
84
164
  }
85
165
  export function readUiConfigValues(projectDir) {
86
- const configPath = path.join(projectDir, 'kuratchi.config.ts');
87
- if (!fs.existsSync(configPath))
166
+ const uiConfig = readUiConfig(projectDir);
167
+ if (!uiConfig)
88
168
  return null;
89
- const source = fs.readFileSync(configPath, 'utf-8');
90
- const uiBlock = readConfigBlock(source, 'ui');
91
- if (!uiBlock)
92
- return null;
93
- const themeMatch = uiBlock.body.match(/theme\s*:\s*['"]([^'"]+)['"]/);
94
- const radiusMatch = uiBlock.body.match(/radius\s*:\s*['"]([^'"]+)['"]/);
95
169
  return {
96
- theme: themeMatch?.[1] ?? 'dark',
97
- radius: radiusMatch?.[1] ?? 'default',
170
+ theme: uiConfig.theme,
171
+ radius: uiConfig.radius,
98
172
  };
99
173
  }
100
174
  export function readOrmConfig(projectDir) {
@@ -134,10 +208,12 @@ export function readOrmConfig(projectDir) {
134
208
  const skipMigrations = skipMatch?.[1] === 'true';
135
209
  const typeMatch = rest.match(/type\s*:\s*['"]?(d1|do)['"]?/);
136
210
  const type = typeMatch?.[1] ?? 'd1';
211
+ const remoteMatch = rest.match(/remote\s*:\s*(true|false)/);
212
+ const remote = remoteMatch?.[1] === 'true';
137
213
  const schemaImportPath = importMap.get(schemaExportName);
138
214
  if (!schemaImportPath)
139
215
  continue;
140
- entries.push({ binding, schemaImportPath, schemaExportName, skipMigrations, type });
216
+ entries.push({ binding, schemaImportPath, schemaExportName, skipMigrations, type, remote });
141
217
  }
142
218
  return entries;
143
219
  }
@@ -174,10 +250,10 @@ export function readDoConfig(projectDir) {
174
250
  const source = fs.readFileSync(configPath, 'utf-8');
175
251
  const doIdx = source.search(/durableObjects\s*:\s*\{/);
176
252
  if (doIdx === -1)
177
- return [];
253
+ return readWranglerDoConfig(projectDir);
178
254
  const braceStart = source.indexOf('{', doIdx);
179
255
  if (braceStart === -1)
180
- return [];
256
+ return readWranglerDoConfig(projectDir);
181
257
  let depth = 0;
182
258
  let braceEnd = braceStart;
183
259
  for (let i = braceStart; i < source.length; i++) {
@@ -227,7 +303,7 @@ export function readDoConfig(projectDir) {
227
303
  continue;
228
304
  entries.push({ binding: match[1], className: match[2] });
229
305
  }
230
- return entries;
306
+ return entries.length > 0 ? entries : readWranglerDoConfig(projectDir);
231
307
  }
232
308
  /** Read durable_objects.bindings from wrangler.jsonc / wrangler.json as fallback. */
233
309
  function readWranglerDoConfig(projectDir) {
@@ -0,0 +1,48 @@
1
+ import type { DesktopConfigEntry, OrmDatabaseEntry } from './compiler-shared.js';
2
+ export interface DesktopManifest {
3
+ formatVersion: 1;
4
+ generatedAt: string;
5
+ projectDir: string;
6
+ app: {
7
+ name: string;
8
+ id: string;
9
+ initialPath: string;
10
+ window: {
11
+ title: string;
12
+ width: number;
13
+ height: number;
14
+ };
15
+ };
16
+ runtime: {
17
+ workerEntrypoint: string;
18
+ assetsRoot: string | null;
19
+ compatibilityDate: string;
20
+ compatibilityFlags: string[];
21
+ cloudflareAccountId: string | null;
22
+ };
23
+ bindings: {
24
+ desktop: {
25
+ notifications: boolean;
26
+ files: boolean;
27
+ };
28
+ remote: Array<{
29
+ binding: string;
30
+ type: 'd1';
31
+ remote: boolean;
32
+ databaseId: string;
33
+ databaseName: string | null;
34
+ } | {
35
+ binding: string;
36
+ type: 'r2';
37
+ remote: boolean;
38
+ bucketName: string;
39
+ }>;
40
+ };
41
+ }
42
+ export declare function buildDesktopManifest(opts: {
43
+ projectDir: string;
44
+ workerFile: string;
45
+ desktopConfig: DesktopConfigEntry | null;
46
+ ormDatabases: OrmDatabaseEntry[];
47
+ }): DesktopManifest | null;
48
+ export declare function writeDesktopManifest(projectDir: string, manifest: DesktopManifest): string;