@kuratchi/js 0.0.8 → 0.0.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
@@ -46,6 +46,18 @@ src/routes/blog/[slug]/page.html → /blog/:slug
46
46
  src/routes/layout.html → shared layout wrapping all routes
47
47
  ```
48
48
 
49
+ ### Execution model
50
+
51
+ Kuratchi routes are server-first.
52
+
53
+ - `src/routes` defines server-rendered route modules.
54
+ - Top-level route `<script>` blocks run on the server.
55
+ - Template expressions, `if`, and `for` blocks render on the server.
56
+ - `src/server` is for private server-only modules and reusable backend logic.
57
+ - Reactive `$:` code is the browser-only escape hatch.
58
+
59
+ Route files are not client files. They are server-rendered routes that can opt into small browser-side reactive behavior when needed.
60
+
49
61
  ### Route file structure
50
62
 
51
63
  ```html
@@ -64,6 +76,7 @@ src/routes/layout.html → shared layout wrapping all routes
64
76
  ```
65
77
 
66
78
  The `$database/` alias resolves to `src/database/`. You can use any path alias configured in your tsconfig.
79
+ Private server logic should live in `src/server/` and be imported into routes explicitly.
67
80
 
68
81
  ### Layout file
69
82
 
@@ -161,9 +174,11 @@ Block form is also supported:
161
174
  ```
162
175
 
163
176
  Notes:
164
- - This reactivity runs in browser scripts rendered in the template markup, not in the top server route `<script>` load/action block.
177
+ - Route files are server-rendered by default. `$:` is the only browser-side execution primitive in a route template.
178
+ - This reactivity runs in browser scripts rendered in the template markup, not in the top server route `<script>` block.
165
179
  - Object/array `let` bindings are proxy-backed automatically when `$:` is used.
166
180
  - `$: name = expr` works; when replacing proxy-backed values, the compiler preserves reactivity under the hood.
181
+ - You should not need `if (browser)` style guards in normal Kuratchi route code. If browser checks become necessary outside `$:`, the boundary is likely in the wrong place.
167
182
 
168
183
  ## Form actions
169
184
 
@@ -360,6 +375,7 @@ For Durable Objects, RPC is file-driven and automatic.
360
375
  - Put handler logic in a `.do.ts` file.
361
376
  - Exported functions in that file become RPC methods.
362
377
  - Import RPC methods from `$durable-objects/<file-name-without-.do>`.
378
+ - RPC methods are still server-side code. They are exposed intentionally by the framework runtime, not because route files are client-side.
363
379
 
364
380
  ```html
365
381
  <script>
@@ -497,6 +513,24 @@ import { env } from 'cloudflare:workers';
497
513
  const result = await env.DB.prepare('SELECT 1').run();
498
514
  ```
499
515
 
516
+ ## Framework environment
517
+
518
+ Kuratchi also exposes a framework build-mode flag:
519
+
520
+ ```html
521
+ <script>
522
+ import { dev } from '@kuratchi/js/environment';
523
+ import { env } from 'cloudflare:workers';
524
+
525
+ const turnstileSiteKey = dev ? '' : (env.TURNSTILE_SITE_KEY || '');
526
+ </script>
527
+ ```
528
+
529
+ - `dev` is `true` for Kuratchi development builds
530
+ - `dev` is `false` for production builds
531
+ - `dev` is compile-time framework state, not a generic process env var
532
+ - `@kuratchi/js/environment` is intended for server route code, not client `$:` scripts
533
+
500
534
  ## `kuratchi.config.ts`
501
535
 
502
536
  Optional. Required only when using framework integrations or Durable Objects.
@@ -52,6 +52,11 @@ function rewriteWorkerEnvAliases(source, aliases) {
52
52
  }
53
53
  return out;
54
54
  }
55
+ function buildDevAliasDeclarations(aliases, isDev) {
56
+ if (!aliases || aliases.length === 0)
57
+ return '';
58
+ return aliases.map((alias) => `const ${alias} = ${isDev ? 'true' : 'false'};`).join('\n');
59
+ }
55
60
  function parseNamedImportBindings(line) {
56
61
  const namesMatch = line.match(/import\s*\{([^}]+)\}/);
57
62
  if (!namesMatch)
@@ -141,9 +146,13 @@ export function compile(options) {
141
146
  .replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '')
142
147
  .trim()
143
148
  : '';
149
+ const devDecls = buildDevAliasDeclarations(compParsed.devAliases, !!options.isDev);
150
+ const effectivePropsCode = [devDecls, propsCode].filter(Boolean).join('\n');
144
151
  const transpiledPropsCode = propsCode
145
- ? transpileTypeScript(propsCode, `component-script:${fileName}.ts`)
146
- : '';
152
+ ? transpileTypeScript(effectivePropsCode, `component-script:${fileName}.ts`)
153
+ : devDecls
154
+ ? transpileTypeScript(devDecls, `component-script:${fileName}.ts`)
155
+ : '';
147
156
  // template source (parseFile already removes the <script> block)
148
157
  let source = compParsed.template;
149
158
  // Extract optional <style> block â€" CSS is scoped and injected once per route at compile time
@@ -651,6 +660,8 @@ export function compile(options) {
651
660
  }
652
661
  // Build the layout script body (data vars, etc.)
653
662
  let layoutScriptBody = layoutParsed.script.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '').trim();
663
+ const layoutDevDecls = buildDevAliasDeclarations(layoutParsed.devAliases, !!options.isDev);
664
+ layoutScriptBody = [layoutDevDecls, layoutScriptBody].filter(Boolean).join('\n');
654
665
  compiledLayout = `function __layout(__content) {
655
666
  const __esc = (v) => { if (v == null) return ''; return String(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); };
656
667
  ${layoutScriptBody ? layoutScriptBody + '\n ' : ''}${finalLayoutBody}
@@ -1068,6 +1079,7 @@ export function compile(options) {
1068
1079
  index: i,
1069
1080
  pattern,
1070
1081
  renderBody,
1082
+ isDev: !!options.isDev,
1071
1083
  parsed,
1072
1084
  fnToModule,
1073
1085
  rpcNameMap,
@@ -1373,7 +1385,7 @@ function discoverRoutes(routesDir) {
1373
1385
  return results;
1374
1386
  }
1375
1387
  function buildRouteObject(opts) {
1376
- const { pattern, renderBody, parsed, fnToModule, rpcNameMap, componentStyles } = opts;
1388
+ const { pattern, renderBody, isDev, parsed, fnToModule, rpcNameMap, componentStyles } = opts;
1377
1389
  const hasFns = Object.keys(fnToModule).length > 0;
1378
1390
  const parts = [];
1379
1391
  parts.push(` pattern: '${pattern}'`);
@@ -1381,12 +1393,15 @@ function buildRouteObject(opts) {
1381
1393
  let scriptBody = parsed.script
1382
1394
  ? parsed.script.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '').trim()
1383
1395
  : '';
1396
+ const routeDevDecls = buildDevAliasDeclarations(parsed.devAliases, isDev);
1397
+ scriptBody = [routeDevDecls, scriptBody].filter(Boolean).join('\n');
1384
1398
  scriptBody = rewriteImportedFunctionCalls(scriptBody, fnToModule);
1385
1399
  scriptBody = rewriteWorkerEnvAliases(scriptBody, parsed.workerEnvAliases);
1386
1400
  let explicitLoadFunction = parsed.loadFunction
1387
1401
  ? parsed.loadFunction.replace(/^export\s+/, '').trim()
1388
1402
  : '';
1389
1403
  if (explicitLoadFunction) {
1404
+ explicitLoadFunction = [routeDevDecls, explicitLoadFunction].filter(Boolean).join('\n');
1390
1405
  explicitLoadFunction = rewriteImportedFunctionCalls(explicitLoadFunction, fnToModule);
1391
1406
  explicitLoadFunction = rewriteWorkerEnvAliases(explicitLoadFunction, parsed.workerEnvAliases);
1392
1407
  }
@@ -40,6 +40,8 @@ export interface ParsedFile {
40
40
  loadReturnVars: string[];
41
41
  /** Local aliases for Cloudflare Workers env imported from cloudflare:workers */
42
42
  workerEnvAliases: string[];
43
+ /** Local aliases for dev imported from @kuratchi/js/environment */
44
+ devAliases: string[];
43
45
  }
44
46
  interface ParseFileOptions {
45
47
  kind?: 'route' | 'layout' | 'component';
@@ -158,6 +158,26 @@ function extractCloudflareEnvAliases(importLine) {
158
158
  }
159
159
  return aliases;
160
160
  }
161
+ function extractKuratchiEnvironmentDevAliases(importLine) {
162
+ if (!/from\s+['"]@kuratchi\/js\/environment['"]/.test(importLine))
163
+ return [];
164
+ const namedMatch = importLine.match(/import\s*\{([\s\S]*?)\}\s*from\s+['"]@kuratchi\/js\/environment['"]/);
165
+ if (!namedMatch) {
166
+ throw new Error('[kuratchi compiler] @kuratchi/js/environment only supports named imports.');
167
+ }
168
+ const aliases = [];
169
+ for (const rawPart of splitTopLevel(namedMatch[1], ',')) {
170
+ const part = rawPart.trim();
171
+ if (!part)
172
+ continue;
173
+ const devMatch = part.match(/^dev(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
174
+ if (!devMatch) {
175
+ throw new Error('[kuratchi compiler] @kuratchi/js/environment currently only exports `dev`.');
176
+ }
177
+ aliases.push(devMatch[1] || 'dev');
178
+ }
179
+ return aliases;
180
+ }
161
181
  function extractReturnObjectKeys(body) {
162
182
  const keys = [];
163
183
  let i = 0;
@@ -648,6 +668,7 @@ export function parseFile(source, options = {}) {
648
668
  const clientImports = [];
649
669
  const componentImports = {};
650
670
  const workerEnvAliases = [];
671
+ const devAliases = [];
651
672
  if (script) {
652
673
  // Support both single-line and multiline static imports.
653
674
  const importRegex = /^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm;
@@ -669,6 +690,14 @@ export function parseFile(source, options = {}) {
669
690
  componentImports[componentName] = `${pkg}:${fileName}`; // e.g. "@kuratchi/ui:badge"
670
691
  }
671
692
  else {
693
+ const devImportAliases = extractKuratchiEnvironmentDevAliases(line);
694
+ if (devImportAliases.length > 0) {
695
+ for (const alias of devImportAliases) {
696
+ if (!devAliases.includes(alias))
697
+ devAliases.push(alias);
698
+ }
699
+ continue;
700
+ }
672
701
  serverImports.push(line);
673
702
  }
674
703
  for (const alias of extractCloudflareEnvAliases(line)) {
@@ -683,6 +712,9 @@ export function parseFile(source, options = {}) {
683
712
  while ((m = importRegex.exec(clientScript)) !== null) {
684
713
  const line = m[0].trim();
685
714
  clientImports.push(line);
715
+ if (extractKuratchiEnvironmentDevAliases(line).length > 0) {
716
+ throw new Error(`[kuratchi compiler] ${options.filePath || kind}\nClient <script> blocks cannot import from @kuratchi/js/environment.\nUse route server script code and pass values into the template explicitly.`);
717
+ }
686
718
  if (extractCloudflareEnvAliases(line).length > 0) {
687
719
  throw buildEnvAccessError(kind, options.filePath, 'Client <script> blocks cannot import env from cloudflare:workers.');
688
720
  }
@@ -813,6 +845,7 @@ export function parseFile(source, options = {}) {
813
845
  clientImports,
814
846
  loadReturnVars,
815
847
  workerEnvAliases,
848
+ devAliases,
816
849
  };
817
850
  }
818
851
  import { transpileTypeScript } from './transpile.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuratchi/js",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -36,6 +36,10 @@
36
36
  "types": "./dist/compiler/index.d.ts",
37
37
  "import": "./dist/compiler/index.js"
38
38
  },
39
+ "./environment": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.js"
42
+ },
39
43
  "./package.json": "./package.json"
40
44
  },
41
45
  "engines": {