@kuratchi/js 0.0.10 → 0.0.12

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kuratchi contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -54,6 +54,7 @@ Kuratchi routes are server-first.
54
54
  - Top-level route `<script>` blocks run on the server.
55
55
  - Template expressions, `if`, and `for` blocks render on the server.
56
56
  - `src/server` is for private server-only modules and reusable backend logic.
57
+ - `src/server/runtime.hook.ts` is the server runtime hook entrypoint for request interception.
57
58
  - Reactive `$:` code is the browser-only escape hatch.
58
59
 
59
60
  Route files are not client files. They are server-rendered routes that can opt into small browser-side reactive behavior when needed.
@@ -542,6 +543,38 @@ const postSlug = slug;
542
543
  - `slug` is `params.slug` when the matched route defines a `slug` param.
543
544
  - `headers`, `method`, and `params` are also exported from `@kuratchi/js/request`.
544
545
  - Use `getRequest()` when you want the raw native `Request` object.
546
+
547
+ ## Runtime Hook
548
+
549
+ Optional server runtime hook file. Export a `RuntimeDefinition` from `src/server/runtime.hook.ts`
550
+ to intercept requests before they reach the framework router. Use it for agent routing,
551
+ pre-route auth, or custom response/error handling.
552
+
553
+ ```ts
554
+ import type { RuntimeDefinition } from '@kuratchi/js';
555
+
556
+ const runtime: RuntimeDefinition = {
557
+ agents: {
558
+ async request(ctx, next) {
559
+ if (!ctx.url.pathname.startsWith('/agents/')) {
560
+ return next();
561
+ }
562
+
563
+ return new Response('Agent response');
564
+ },
565
+ },
566
+ };
567
+
568
+ export default runtime;
569
+ ```
570
+
571
+ `ctx` includes:
572
+
573
+ - `ctx.url` - parsed URL
574
+ - `ctx.request` - raw Request
575
+ - `ctx.env` - Cloudflare env bindings
576
+ - `next()` - pass control to the next handler
577
+
545
578
  ## Environment bindings
546
579
 
547
580
  Cloudflare env is server-only.
@@ -725,6 +725,8 @@ export function compile(options) {
725
725
  // 500.html receives `error` as a variable; others don't need it
726
726
  compiledErrorPages.set(status, `function __error_${status}(error) {\n ${body}\n return __html;\n}`);
727
727
  }
728
+ // Read assets prefix from kuratchi.config.ts (default: /assets/)
729
+ const assetsPrefix = readAssetsPrefix(projectDir);
728
730
  // Read kuratchi.config.ts at build time to discover ORM database configs
729
731
  const ormDatabases = readOrmConfig(projectDir);
730
732
  // Read auth config from kuratchi.config.ts
@@ -1202,7 +1204,7 @@ export function compile(options) {
1202
1204
  });
1203
1205
  compiledRoutes.push(routeObj);
1204
1206
  }
1205
- // Scan src/assets/ for static files to embed
1207
+ // Scan src/assets/ for static files to embed (recursive)
1206
1208
  const assetsDir = path.join(srcDir, 'assets');
1207
1209
  const compiledAssets = [];
1208
1210
  if (fs.existsSync(assetsDir)) {
@@ -1213,15 +1215,23 @@ export function compile(options) {
1213
1215
  '.svg': 'image/svg+xml',
1214
1216
  '.txt': 'text/plain; charset=utf-8',
1215
1217
  };
1216
- for (const file of fs.readdirSync(assetsDir).sort()) {
1217
- const ext = path.extname(file).toLowerCase();
1218
- const mime = mimeTypes[ext];
1219
- if (!mime)
1220
- continue; // skip unknown file types
1221
- const content = fs.readFileSync(path.join(assetsDir, file), 'utf-8');
1222
- const etag = '"' + crypto.createHash('md5').update(content).digest('hex').slice(0, 12) + '"';
1223
- compiledAssets.push({ name: file, content, mime, etag });
1224
- }
1218
+ const scanAssets = (dir, prefix) => {
1219
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
1220
+ if (entry.isDirectory()) {
1221
+ scanAssets(path.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
1222
+ continue;
1223
+ }
1224
+ const ext = path.extname(entry.name).toLowerCase();
1225
+ const mime = mimeTypes[ext];
1226
+ if (!mime)
1227
+ continue;
1228
+ const content = fs.readFileSync(path.join(dir, entry.name), 'utf-8');
1229
+ const etag = '"' + crypto.createHash('md5').update(content).digest('hex').slice(0, 12) + '"';
1230
+ const name = prefix ? `${prefix}/${entry.name}` : entry.name;
1231
+ compiledAssets.push({ name, content, mime, etag });
1232
+ }
1233
+ };
1234
+ scanAssets(assetsDir, '');
1225
1235
  }
1226
1236
  // Collect only the components that were actually imported by routes
1227
1237
  const compiledComponents = Array.from(compiledComponentCache.values());
@@ -1254,6 +1264,7 @@ export function compile(options) {
1254
1264
  compiledLayoutActions,
1255
1265
  hasRuntime,
1256
1266
  runtimeImportPath,
1267
+ assetsPrefix,
1257
1268
  });
1258
1269
  // Write to .kuratchi/routes.js
1259
1270
  const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.js');
@@ -1720,7 +1731,7 @@ function buildRouteObject(opts) {
1720
1731
  return { ${loadReturnVars.join(', ')} };`;
1721
1732
  }
1722
1733
  }
1723
- parts.push(` async load(params = {}) {
1734
+ parts.push(` async load(__routeParams = {}) {
1724
1735
  ${loadBody}${returnObj}
1725
1736
  }`);
1726
1737
  }
@@ -2039,6 +2050,21 @@ function resolveClassExportFromFile(absPath, errorLabel) {
2039
2050
  }
2040
2051
  throw new Error(`[kuratchi] ${errorLabel} must export a class via "export class X" or "export default class X". File: ${absPath}`);
2041
2052
  }
2053
+ function readAssetsPrefix(projectDir) {
2054
+ const configPath = path.join(projectDir, 'kuratchi.config.ts');
2055
+ if (!fs.existsSync(configPath))
2056
+ return '/assets/';
2057
+ const source = fs.readFileSync(configPath, 'utf-8');
2058
+ const match = source.match(/assetsPrefix\s*:\s*['"]([^'"]+)['"]/);
2059
+ if (!match)
2060
+ return '/assets/';
2061
+ let prefix = match[1];
2062
+ if (!prefix.startsWith('/'))
2063
+ prefix = '/' + prefix;
2064
+ if (!prefix.endsWith('/'))
2065
+ prefix += '/';
2066
+ return prefix;
2067
+ }
2042
2068
  function discoverConventionClassFiles(projectDir, dir, suffix, errorLabel) {
2043
2069
  const absDir = path.join(projectDir, dir);
2044
2070
  const files = discoverFilesWithSuffix(absDir, suffix);
@@ -2933,9 +2959,9 @@ ${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __init
2933
2959
  const url = __runtimeCtx.url;
2934
2960
  ${ac?.hasRateLimit ? '\n // Rate limiting - check before route handlers\n { const __rlRes = await __checkRL(); if (__rlRes) return __secHeaders(__rlRes); }\n' : ''}${ac?.hasTurnstile ? ' // Turnstile bot protection\n { const __tsRes = await __checkTS(); if (__tsRes) return __secHeaders(__tsRes); }\n' : ''}${ac?.hasGuards ? ' // Route guards - redirect if not authenticated\n { const __gRes = __checkGuard(); if (__gRes) return __secHeaders(__gRes); }\n' : ''}
2935
2961
 
2936
- // Serve static assets from src/assets/ at /_assets/*
2937
- if (url.pathname.startsWith('/_assets/')) {
2938
- const name = url.pathname.slice('/_assets/'.length);
2962
+ // Serve static assets from src/assets/
2963
+ if (url.pathname.startsWith('${opts.assetsPrefix}')) {
2964
+ const name = url.pathname.slice('${opts.assetsPrefix}'.length);
2939
2965
  const asset = __assets[name];
2940
2966
  if (asset) {
2941
2967
  if (request.headers.get('if-none-match') === asset.etag) {
@@ -3116,12 +3142,7 @@ ${ac?.hasRateLimit ? '\n // Rate limiting - check before route handlers\n
3116
3142
  }
3117
3143
  function resolveRuntimeImportPath(projectDir) {
3118
3144
  const candidates = [
3119
- { file: 'src/kuratchi.runtime.ts', importPath: '../src/kuratchi.runtime' },
3120
- { file: 'src/kuratchi.runtime.js', importPath: '../src/kuratchi.runtime' },
3121
- { file: 'src/kuratchi.runtime.mjs', importPath: '../src/kuratchi.runtime' },
3122
- { file: 'kuratchi.runtime.ts', importPath: '../kuratchi.runtime' },
3123
- { file: 'kuratchi.runtime.js', importPath: '../kuratchi.runtime' },
3124
- { file: 'kuratchi.runtime.mjs', importPath: '../kuratchi.runtime' },
3145
+ { file: 'src/server/runtime.hook.ts', importPath: '../src/server/runtime.hook' },
3125
3146
  ];
3126
3147
  for (const candidate of candidates) {
3127
3148
  if (fs.existsSync(path.join(projectDir, candidate.file))) {
@@ -770,6 +770,21 @@ export function parseFile(source, options = {}) {
770
770
  if (!dataVars.includes(name))
771
771
  dataVars.push(name);
772
772
  }
773
+ // Server import named bindings are also data vars (available in templates)
774
+ for (const line of serverImports) {
775
+ const namesMatch = line.match(/import\s*\{([^}]+)\}/);
776
+ if (!namesMatch)
777
+ continue;
778
+ for (const part of namesMatch[1].split(',')) {
779
+ const trimmed = part.trim();
780
+ if (!trimmed)
781
+ continue;
782
+ const segments = trimmed.split(/\s+as\s+/);
783
+ const localName = (segments[1] || segments[0]).trim();
784
+ if (localName && !dataVars.includes(localName))
785
+ dataVars.push(localName);
786
+ }
787
+ }
773
788
  }
774
789
  const hasLoad = scriptBody.length > 0 || !!loadFunction;
775
790
  // Strip HTML comments from the template before scanning for action references.
@@ -8,7 +8,7 @@ export let slug = undefined;
8
8
  function __syncDerivedState() {
9
9
  pathname = url.pathname;
10
10
  searchParams = url.searchParams;
11
- slug = params.slug;
11
+ slug = params.slug ?? Object.values(params)[0];
12
12
  }
13
13
  export function __setRequestState(request) {
14
14
  url = new URL(request.url);
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@kuratchi/js",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax",
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "main": "./dist/index.js",
7
8
  "types": "./dist/index.d.ts",
8
9
  "bin": {
9
- "kuratchi": "./dist/cli.js"
10
+ "kuratchi": "dist/cli.js"
10
11
  },
11
12
  "files": [
12
13
  "dist",
13
- "README.md"
14
+ "README.md",
15
+ "LICENSE"
14
16
  ],
15
17
  "scripts": {
16
18
  "build": "tsc -p tsconfig.build.json",