@kuratchi/js 0.0.20 → 0.0.21

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
@@ -831,12 +831,21 @@ import {
831
831
  } from '@kuratchi/js';
832
832
  ```
833
833
 
834
+ ### Virtual Modules
835
+
836
+ Kuratchi provides virtual modules for request-scoped state. Use these in route files:
837
+
838
+ | Virtual Module | Description |
839
+ |----------------|-------------|
840
+ | `kuratchi:request` | Request state: `url`, `params`, `searchParams`, `headers`, `locals`, etc. |
841
+ | `kuratchi:navigation` | Server-side redirect helper |
842
+
834
843
  ### Request helpers
835
844
 
836
- For a batteries-included request layer, import pre-parsed request state from `@kuratchi/js/request`:
845
+ For a batteries-included request layer, import pre-parsed request state from `kuratchi:request`:
837
846
 
838
847
  ```ts
839
- import { url, pathname, searchParams, params, slug } from '@kuratchi/js/request';
848
+ import { url, pathname, searchParams, params, slug, locals } from 'kuratchi:request';
840
849
 
841
850
  const page = pathname;
842
851
  const tab = searchParams.get('tab');
@@ -849,9 +858,23 @@ const postSlug = slug;
849
858
  - `searchParams` is `url.searchParams` for the current request.
850
859
  - `params` is the matched route params object, like `{ slug: 'hello-world' }`.
851
860
  - `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.
861
+ - `headers` and `method` are also exported from `kuratchi:request`.
862
+ - `locals` is the request-scoped locals object (typed via `App.Locals` in `app.d.ts`).
863
+ - Use `getRequest()` from `@kuratchi/js` when you want the raw native `Request` object.
864
+
865
+ ### Server-side redirect
866
+
867
+ Import `redirect` from `kuratchi:navigation` for server-side redirects:
868
+
869
+ ```ts
870
+ import { redirect } from 'kuratchi:navigation';
871
+
872
+ // Redirect to another page (throws RedirectError, caught by framework)
873
+ redirect('/dashboard');
874
+ redirect('/login', 302);
875
+ ```
876
+
877
+ `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
878
 
856
879
  ## Runtime Hook
857
880
 
package/dist/cli.js CHANGED
@@ -29,6 +29,9 @@ async function main() {
29
29
  case 'create':
30
30
  await runCreate();
31
31
  return;
32
+ case 'types':
33
+ await runTypes();
34
+ return;
32
35
  default:
33
36
  console.log(`
34
37
  KuratchiJS CLI
@@ -38,6 +41,7 @@ Usage:
38
41
  kuratchi build Compile routes once
39
42
  kuratchi dev Compile, watch for changes, and start wrangler dev server
40
43
  kuratchi watch Compile + watch only (no wrangler — for custom setups)
44
+ kuratchi types Generate TypeScript types from schema to src/app.d.ts
41
45
  `);
42
46
  process.exit(1);
43
47
  }
@@ -49,6 +53,10 @@ async function runCreate() {
49
53
  const positional = remaining.filter(a => !a.startsWith('-'));
50
54
  await create(positional[0], flags);
51
55
  }
56
+ async function runTypes() {
57
+ const { writeAppTypes } = await import('./compiler/type-generator.js');
58
+ writeAppTypes({ projectDir });
59
+ }
52
60
  async function runBuild(isDev = false) {
53
61
  console.log('[kuratchi] Compiling...');
54
62
  try {
@@ -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);
@@ -37,6 +37,12 @@ export function createServerModuleCompiler(options) {
37
37
  return resolveExistingModuleFile(proxyNoExt) ?? (fs.existsSync(proxyNoExt + '.ts') ? proxyNoExt + '.ts' : null);
38
38
  }
39
39
  function resolveImportTarget(importerAbs, spec) {
40
+ // Handle kuratchi:* virtual modules
41
+ if (spec.startsWith('kuratchi:')) {
42
+ // These are resolved at bundle time to @kuratchi/js runtime modules
43
+ // Return null to keep the specifier as-is for later rewriting
44
+ return null;
45
+ }
40
46
  if (spec.startsWith('$')) {
41
47
  const slashIdx = spec.indexOf('/');
42
48
  const folder = slashIdx === -1 ? spec.slice(1) : spec.slice(1, slashIdx);
@@ -70,6 +76,15 @@ export function createServerModuleCompiler(options) {
70
76
  }
71
77
  const source = fs.readFileSync(resolved, 'utf-8');
72
78
  const rewriteSpecifier = (spec) => {
79
+ // Rewrite kuratchi:* virtual modules to @kuratchi/js runtime paths
80
+ if (spec.startsWith('kuratchi:')) {
81
+ const moduleName = spec.slice('kuratchi:'.length);
82
+ const moduleMap = {
83
+ 'request': '@kuratchi/js/request',
84
+ 'navigation': '@kuratchi/js/navigation',
85
+ };
86
+ return moduleMap[moduleName] ?? spec;
87
+ }
73
88
  const target = resolveImportTarget(resolved, spec);
74
89
  if (!target)
75
90
  return spec;
@@ -96,6 +111,15 @@ export function createServerModuleCompiler(options) {
96
111
  return outPath;
97
112
  }
98
113
  function resolveCompiledImportPath(origPath, importerDir, outFileDir) {
114
+ // Rewrite kuratchi:* virtual modules to @kuratchi/js runtime paths
115
+ if (origPath.startsWith('kuratchi:')) {
116
+ const moduleName = origPath.slice('kuratchi:'.length);
117
+ const moduleMap = {
118
+ 'request': '@kuratchi/js/request',
119
+ 'navigation': '@kuratchi/js/navigation',
120
+ };
121
+ return moduleMap[moduleName] ?? origPath;
122
+ }
99
123
  const isBareModule = !origPath.startsWith('.') && !origPath.startsWith('/') && !origPath.startsWith('$');
100
124
  if (isBareModule)
101
125
  return origPath;
@@ -0,0 +1,8 @@
1
+ export interface GenerateTypesOptions {
2
+ projectDir: string;
3
+ schemaPath?: string;
4
+ outputPath?: string;
5
+ localsInterface?: string;
6
+ }
7
+ export declare function generateAppTypes(options: GenerateTypesOptions): string;
8
+ export declare function writeAppTypes(options: GenerateTypesOptions): void;
@@ -0,0 +1,124 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ function sqliteTypeToTs(sqlType) {
4
+ const lower = sqlType.toLowerCase();
5
+ if (lower.includes('integer') || lower.includes('int') || lower.includes('real') || lower.includes('numeric')) {
6
+ return 'number';
7
+ }
8
+ if (lower.includes('text') || lower.includes('varchar') || lower.includes('char')) {
9
+ return 'string';
10
+ }
11
+ if (lower.includes('blob')) {
12
+ return 'Uint8Array';
13
+ }
14
+ if (lower.includes('json')) {
15
+ return 'Record<string, unknown>';
16
+ }
17
+ if (lower.includes('boolean') || lower.includes('bool')) {
18
+ return 'boolean';
19
+ }
20
+ return 'unknown';
21
+ }
22
+ function parseSchemaColumn(name, definition) {
23
+ const lower = definition.toLowerCase();
24
+ const nullable = !lower.includes('not null');
25
+ const hasDefault = lower.includes('default');
26
+ const type = sqliteTypeToTs(definition);
27
+ return { name, type, nullable, hasDefault };
28
+ }
29
+ function parseSchemaFromSource(source) {
30
+ const tables = [];
31
+ // Match tables: { tableName: { col: 'def', ... }, ... }
32
+ const tablesMatch = source.match(/tables\s*:\s*\{([\s\S]*?)\n\t?\}/);
33
+ if (!tablesMatch)
34
+ return tables;
35
+ const tablesBlock = tablesMatch[1];
36
+ // Match each table definition
37
+ const tableRegex = /(\w+)\s*:\s*\{([^}]+)\}/g;
38
+ let match;
39
+ while ((match = tableRegex.exec(tablesBlock)) !== null) {
40
+ const tableName = match[1];
41
+ const columnsBlock = match[2];
42
+ const columns = [];
43
+ // Match each column: name: 'definition'
44
+ const colRegex = /(\w+)\s*:\s*['"]([^'"]+)['"]/g;
45
+ let colMatch;
46
+ while ((colMatch = colRegex.exec(columnsBlock)) !== null) {
47
+ columns.push(parseSchemaColumn(colMatch[1], colMatch[2]));
48
+ }
49
+ tables.push({ name: tableName, columns });
50
+ }
51
+ return tables;
52
+ }
53
+ function generateTableTypes(tables) {
54
+ const lines = [];
55
+ for (const table of tables) {
56
+ const pascalName = table.name
57
+ .split('_')
58
+ .map(s => s.charAt(0).toUpperCase() + s.slice(1))
59
+ .join('');
60
+ lines.push(` /** Row type for ${table.name} table */`);
61
+ lines.push(` interface ${pascalName}Row {`);
62
+ for (const col of table.columns) {
63
+ const optional = col.nullable || col.hasDefault ? '?' : '';
64
+ lines.push(` ${col.name}${optional}: ${col.type};`);
65
+ }
66
+ lines.push(` }`);
67
+ lines.push('');
68
+ }
69
+ return lines.join('\n');
70
+ }
71
+ export function generateAppTypes(options) {
72
+ const { projectDir, schemaPath = 'src/server/schema.ts', outputPath = 'src/app.d.ts', localsInterface, } = options;
73
+ const schemaFullPath = path.join(projectDir, schemaPath);
74
+ let tables = [];
75
+ if (fs.existsSync(schemaFullPath)) {
76
+ const schemaSource = fs.readFileSync(schemaFullPath, 'utf-8');
77
+ tables = parseSchemaFromSource(schemaSource);
78
+ }
79
+ const tableTypes = tables.length > 0 ? generateTableTypes(tables) : '';
80
+ // Check if user has existing Locals definition to preserve
81
+ const outputFullPath = path.join(projectDir, outputPath);
82
+ let existingLocals = null;
83
+ if (fs.existsSync(outputFullPath)) {
84
+ const existing = fs.readFileSync(outputFullPath, 'utf-8');
85
+ // Extract user-defined Locals interface (between USER LOCALS START/END markers)
86
+ const localsMatch = existing.match(/\/\/ USER LOCALS START\n([\s\S]*?)\/\/ USER LOCALS END/);
87
+ if (localsMatch) {
88
+ existingLocals = localsMatch[1];
89
+ }
90
+ }
91
+ const localsBlock = existingLocals || localsInterface || ` interface Locals {
92
+ userId: number;
93
+ userEmail: string;
94
+ }`;
95
+ const output = `/**
96
+ * Type declarations for kuratchi app.
97
+ *
98
+ * DB types are auto-generated from schema.ts - regenerate with: kuratchi types
99
+ * Edit the Locals interface below to match your runtime.hook.ts
100
+ */
101
+ declare global {
102
+ namespace App {
103
+ /** Request-scoped locals set by runtime hooks */
104
+ // USER LOCALS START
105
+ ${localsBlock}
106
+ // USER LOCALS END
107
+
108
+ ${tableTypes ? ` // Database table row types (auto-generated from schema.ts)\n${tableTypes}` : ''} }
109
+ }
110
+
111
+ export {};
112
+ `;
113
+ return output;
114
+ }
115
+ export function writeAppTypes(options) {
116
+ const output = generateAppTypes(options);
117
+ const outputPath = path.join(options.projectDir, options.outputPath || 'src/app.d.ts');
118
+ const dir = path.dirname(outputPath);
119
+ if (!fs.existsSync(dir)) {
120
+ fs.mkdirSync(dir, { recursive: true });
121
+ }
122
+ fs.writeFileSync(outputPath, output, 'utf-8');
123
+ console.log(`[kuratchi] Generated types → ${path.relative(options.projectDir, outputPath)}`);
124
+ }
@@ -43,7 +43,9 @@ export declare function getParam(name: string): string | undefined;
43
43
  * Throws a redirect signal consumed by the framework's PRG flow.
44
44
  */
45
45
  export declare function redirect(path: string, status?: number): never;
46
- /** Backward-compatible alias for redirect() */
46
+ /**
47
+ * @deprecated Use redirect() instead. This alias will be removed in a future version.
48
+ */
47
49
  export declare function goto(path: string, status?: number): never;
48
50
  export declare function setBreadcrumbs(items: BreadcrumbItem[]): void;
49
51
  export declare function getBreadcrumbs(): BreadcrumbItem[];
@@ -104,7 +104,9 @@ export function redirect(path, status = 303) {
104
104
  __locals.__redirectStatus = status;
105
105
  throw new RedirectError(path, status);
106
106
  }
107
- /** Backward-compatible alias for redirect() */
107
+ /**
108
+ * @deprecated Use redirect() instead. This alias will be removed in a future version.
109
+ */
108
110
  export function goto(path, status = 303) {
109
111
  redirect(path, status);
110
112
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Navigation helpers for kuratchi routes.
3
+ * Import via: import { redirect } from 'kuratchi:navigation';
4
+ *
5
+ * redirect() is server-side only — works in route scripts, server modules, and form actions.
6
+ * It throws a RedirectError that the framework catches and converts to a 303 redirect response.
7
+ */
8
+ export { redirect } from './context.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Navigation helpers for kuratchi routes.
3
+ * Import via: import { redirect } from 'kuratchi:navigation';
4
+ *
5
+ * redirect() is server-side only — works in route scripts, server modules, and form actions.
6
+ * It throws a RedirectError that the framework catches and converts to a 303 redirect response.
7
+ */
8
+ export { redirect } from './context.js';
@@ -1,3 +1,11 @@
1
+ declare global {
2
+ namespace App {
3
+ /** Request-scoped locals set by runtime hooks. Extend in your app.d.ts */
4
+ interface Locals {
5
+ [key: string]: unknown;
6
+ }
7
+ }
8
+ }
1
9
  export declare let url: URL;
2
10
  export declare let pathname: string;
3
11
  export declare let searchParams: URLSearchParams;
@@ -5,5 +13,25 @@ export declare let headers: Headers;
5
13
  export declare let method: string;
6
14
  export declare let params: Record<string, string>;
7
15
  export declare let slug: string | undefined;
16
+ /**
17
+ * Get request-scoped locals with full type safety.
18
+ * Define your Locals type in src/app.d.ts:
19
+ * ```
20
+ * declare global {
21
+ * namespace App {
22
+ * interface Locals {
23
+ * userId: number;
24
+ * userEmail: string;
25
+ * }
26
+ * }
27
+ * }
28
+ * ```
29
+ */
30
+ export declare function getLocals(): App.Locals;
31
+ /**
32
+ * Direct access to request-scoped locals.
33
+ * Type is inferred from App.Locals declared in your app.d.ts.
34
+ */
35
+ export declare const locals: App.Locals;
8
36
  export declare function __setRequestState(request: Request): void;
9
37
  export declare function __setRequestParams(nextParams: Record<string, string> | null | undefined): void;
@@ -1,3 +1,4 @@
1
+ import { __getLocals } from './context.js';
1
2
  export let url = new URL('http://localhost/');
2
3
  export let pathname = '/';
3
4
  export let searchParams = url.searchParams;
@@ -5,6 +6,49 @@ export let headers = new Headers();
5
6
  export let method = 'GET';
6
7
  export let params = {};
7
8
  export let slug = undefined;
9
+ /**
10
+ * Get request-scoped locals with full type safety.
11
+ * Define your Locals type in src/app.d.ts:
12
+ * ```
13
+ * declare global {
14
+ * namespace App {
15
+ * interface Locals {
16
+ * userId: number;
17
+ * userEmail: string;
18
+ * }
19
+ * }
20
+ * }
21
+ * ```
22
+ */
23
+ export function getLocals() {
24
+ return __getLocals();
25
+ }
26
+ /**
27
+ * Direct access to request-scoped locals.
28
+ * Type is inferred from App.Locals declared in your app.d.ts.
29
+ */
30
+ export const locals = new Proxy({}, {
31
+ get(_target, prop) {
32
+ return __getLocals()[prop];
33
+ },
34
+ set(_target, prop, value) {
35
+ __getLocals()[prop] = value;
36
+ return true;
37
+ },
38
+ has(_target, prop) {
39
+ return prop in __getLocals();
40
+ },
41
+ ownKeys() {
42
+ return Reflect.ownKeys(__getLocals());
43
+ },
44
+ getOwnPropertyDescriptor(_target, prop) {
45
+ const locals = __getLocals();
46
+ if (prop in locals) {
47
+ return { configurable: true, enumerable: true, value: locals[prop] };
48
+ }
49
+ return undefined;
50
+ },
51
+ });
8
52
  function __syncDerivedState() {
9
53
  pathname = url.pathname;
10
54
  searchParams = url.searchParams;
package/package.json CHANGED
@@ -1,72 +1,76 @@
1
- {
2
- "name": "@kuratchi/js",
3
- "version": "0.0.20",
4
- "description": "A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax",
5
- "license": "MIT",
6
- "type": "module",
7
- "main": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "bin": {
10
- "kuratchi": "dist/cli.js"
11
- },
12
- "files": [
13
- "dist",
14
- "README.md",
15
- "LICENSE"
16
- ],
17
- "scripts": {
18
- "build": "tsc -p tsconfig.build.json",
19
- "check": "tsc -p tsconfig.build.json --noEmit",
20
- "test": "bun test",
21
- "test:watch": "bun test --watch",
22
- "prepublishOnly": "npm run build"
23
- },
24
- "exports": {
25
- ".": {
26
- "types": "./dist/index.d.ts",
27
- "import": "./dist/index.js"
28
- },
29
- "./runtime/context.js": {
30
- "types": "./dist/runtime/context.d.ts",
31
- "import": "./dist/runtime/context.js"
32
- },
33
- "./request": {
34
- "types": "./dist/runtime/request.d.ts",
35
- "import": "./dist/runtime/request.js"
36
- },
37
- "./runtime/do.js": {
38
- "types": "./dist/runtime/do.d.ts",
39
- "import": "./dist/runtime/do.js"
40
- },
41
- "./runtime/schema.js": {
42
- "types": "./dist/runtime/schema.d.ts",
43
- "import": "./dist/runtime/schema.js"
44
- },
45
- "./runtime/generated-worker.js": {
46
- "types": "./dist/runtime/generated-worker.d.ts",
47
- "import": "./dist/runtime/generated-worker.js"
48
- },
49
- "./compiler": {
50
- "types": "./dist/compiler/index.d.ts",
51
- "import": "./dist/compiler/index.js"
52
- },
53
- "./environment": {
54
- "types": "./dist/index.d.ts",
55
- "import": "./dist/index.js"
56
- },
57
- "./package.json": "./package.json"
58
- },
59
- "engines": {
60
- "node": ">=18"
61
- },
62
- "publishConfig": {
63
- "access": "public"
64
- },
65
- "dependencies": {
66
- "typescript": "^5.8.0"
67
- },
68
- "devDependencies": {
69
- "@cloudflare/workers-types": "^4.20260223.0",
70
- "@types/node": "^24.4.0"
71
- }
72
- }
1
+ {
2
+ "name": "@kuratchi/js",
3
+ "version": "0.0.21",
4
+ "description": "A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "kuratchi": "dist/cli.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.build.json",
19
+ "check": "tsc -p tsconfig.build.json --noEmit",
20
+ "test": "bun test",
21
+ "test:watch": "bun test --watch",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ },
29
+ "./runtime/context.js": {
30
+ "types": "./dist/runtime/context.d.ts",
31
+ "import": "./dist/runtime/context.js"
32
+ },
33
+ "./request": {
34
+ "types": "./dist/runtime/request.d.ts",
35
+ "import": "./dist/runtime/request.js"
36
+ },
37
+ "./navigation": {
38
+ "types": "./dist/runtime/navigation.d.ts",
39
+ "import": "./dist/runtime/navigation.js"
40
+ },
41
+ "./runtime/do.js": {
42
+ "types": "./dist/runtime/do.d.ts",
43
+ "import": "./dist/runtime/do.js"
44
+ },
45
+ "./runtime/schema.js": {
46
+ "types": "./dist/runtime/schema.d.ts",
47
+ "import": "./dist/runtime/schema.js"
48
+ },
49
+ "./runtime/generated-worker.js": {
50
+ "types": "./dist/runtime/generated-worker.d.ts",
51
+ "import": "./dist/runtime/generated-worker.js"
52
+ },
53
+ "./compiler": {
54
+ "types": "./dist/compiler/index.d.ts",
55
+ "import": "./dist/compiler/index.js"
56
+ },
57
+ "./environment": {
58
+ "types": "./dist/index.d.ts",
59
+ "import": "./dist/index.js"
60
+ },
61
+ "./package.json": "./package.json"
62
+ },
63
+ "engines": {
64
+ "node": ">=18"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "dependencies": {
70
+ "typescript": "^5.8.0"
71
+ },
72
+ "devDependencies": {
73
+ "@cloudflare/workers-types": "^4.20260223.0",
74
+ "@types/node": "^24.4.0"
75
+ }
76
+ }