@kuratchi/js 0.0.1
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 +29 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +78 -0
- package/dist/compiler/index.d.ts +34 -0
- package/dist/compiler/index.js +2200 -0
- package/dist/compiler/parser.d.ts +40 -0
- package/dist/compiler/parser.js +534 -0
- package/dist/compiler/template.d.ts +30 -0
- package/dist/compiler/template.js +625 -0
- package/dist/create.d.ts +7 -0
- package/dist/create.js +876 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +15 -0
- package/dist/runtime/app.d.ts +12 -0
- package/dist/runtime/app.js +118 -0
- package/dist/runtime/config.d.ts +5 -0
- package/dist/runtime/config.js +6 -0
- package/dist/runtime/containers.d.ts +61 -0
- package/dist/runtime/containers.js +127 -0
- package/dist/runtime/context.d.ts +54 -0
- package/dist/runtime/context.js +134 -0
- package/dist/runtime/do.d.ts +81 -0
- package/dist/runtime/do.js +123 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/router.d.ts +29 -0
- package/dist/runtime/router.js +73 -0
- package/dist/runtime/types.d.ts +207 -0
- package/dist/runtime/types.js +4 -0
- package/package.json +50 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KuratchiJS — Public API
|
|
3
|
+
*
|
|
4
|
+
* A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax.
|
|
5
|
+
*/
|
|
6
|
+
export { createApp } from './runtime/app.js';
|
|
7
|
+
export { defineConfig } from './runtime/config.js';
|
|
8
|
+
export { getEnv, getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './runtime/context.js';
|
|
9
|
+
export { kuratchiDO, doRpc } from './runtime/do.js';
|
|
10
|
+
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './runtime/containers.js';
|
|
11
|
+
export type { AppConfig, kuratchiConfig, DatabaseConfig, AuthConfig, RouteContext, RouteModule } from './runtime/types.js';
|
|
12
|
+
export type { RpcOf } from './runtime/do.js';
|
|
13
|
+
export { compile } from './compiler/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KuratchiJS — Public API
|
|
3
|
+
*
|
|
4
|
+
* A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax.
|
|
5
|
+
*/
|
|
6
|
+
// Runtime
|
|
7
|
+
export { createApp } from './runtime/app.js';
|
|
8
|
+
export { defineConfig } from './runtime/config.js';
|
|
9
|
+
export { getEnv, getCtx, getRequest, getLocals, getParams, getParam, redirect, goto, setBreadcrumbs, getBreadcrumbs, breadcrumbsHome, breadcrumbsPrev, breadcrumbsNext, breadcrumbsCurrent, buildDefaultBreadcrumbs, } from './runtime/context.js';
|
|
10
|
+
export { kuratchiDO, doRpc } from './runtime/do.js';
|
|
11
|
+
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO,
|
|
12
|
+
// Compatibility aliases
|
|
13
|
+
matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './runtime/containers.js';
|
|
14
|
+
// Compiler (for build tooling)
|
|
15
|
+
export { compile } from './compiler/index.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core application — the Worker fetch() handler.
|
|
3
|
+
*
|
|
4
|
+
* Takes an AppConfig, returns a standard Cloudflare Worker fetch handler.
|
|
5
|
+
*/
|
|
6
|
+
import type { AppConfig, Env } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Create a Cloudflare Worker fetch handler from an AppConfig.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createApp<E extends Env = Env>(config: AppConfig<E>): {
|
|
11
|
+
fetch(request: Request, env: E, ctx: ExecutionContext): Promise<Response>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core application — the Worker fetch() handler.
|
|
3
|
+
*
|
|
4
|
+
* Takes an AppConfig, returns a standard Cloudflare Worker fetch handler.
|
|
5
|
+
*/
|
|
6
|
+
import { Router } from './router.js';
|
|
7
|
+
/**
|
|
8
|
+
* Create a Cloudflare Worker fetch handler from an AppConfig.
|
|
9
|
+
*/
|
|
10
|
+
export function createApp(config) {
|
|
11
|
+
const router = new Router();
|
|
12
|
+
const routes = config.routes ?? [];
|
|
13
|
+
const layouts = config.layouts ?? {};
|
|
14
|
+
// Register routes
|
|
15
|
+
for (let i = 0; i < routes.length; i++) {
|
|
16
|
+
router.add(routes[i].pattern, i);
|
|
17
|
+
}
|
|
18
|
+
// The Worker fetch handler
|
|
19
|
+
return {
|
|
20
|
+
async fetch(request, env, ctx) {
|
|
21
|
+
const url = new URL(request.url);
|
|
22
|
+
// Build base context (params filled in after routing)
|
|
23
|
+
const context = {
|
|
24
|
+
request,
|
|
25
|
+
env,
|
|
26
|
+
ctx,
|
|
27
|
+
params: {},
|
|
28
|
+
locals: {},
|
|
29
|
+
url,
|
|
30
|
+
};
|
|
31
|
+
// --- Static files from public/ ---
|
|
32
|
+
// Handled by wrangler's [site] config, not here.
|
|
33
|
+
// --- Route matching ---
|
|
34
|
+
const match = router.match(url.pathname);
|
|
35
|
+
// Build the final handler (route dispatch)
|
|
36
|
+
const routeHandler = async () => {
|
|
37
|
+
if (!match) {
|
|
38
|
+
return new Response('Not Found', { status: 404, headers: { 'content-type': 'text/html' } });
|
|
39
|
+
}
|
|
40
|
+
const route = routes[match.index];
|
|
41
|
+
context.params = match.params;
|
|
42
|
+
// --- RPC calls: POST ?/_rpc/functionName ---
|
|
43
|
+
if (request.method === 'POST' && url.searchParams.has('_rpc')) {
|
|
44
|
+
const fnName = url.searchParams.get('_rpc');
|
|
45
|
+
const rpcFn = route.rpc?.[fnName];
|
|
46
|
+
if (!rpcFn) {
|
|
47
|
+
return new Response(JSON.stringify({ error: `RPC function '${fnName}' not found` }), {
|
|
48
|
+
status: 404,
|
|
49
|
+
headers: { 'content-type': 'application/json' },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const body = await request.json();
|
|
54
|
+
const result = await rpcFn(body.args ?? [], env, context);
|
|
55
|
+
return new Response(JSON.stringify(result), {
|
|
56
|
+
headers: { 'content-type': 'application/json' },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return new Response(JSON.stringify({ error: err.message }), {
|
|
61
|
+
status: 500,
|
|
62
|
+
headers: { 'content-type': 'application/json' },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// --- Form actions: POST ?/actionName ---
|
|
67
|
+
if (request.method === 'POST') {
|
|
68
|
+
const actionParam = [...url.searchParams.keys()].find(k => k.startsWith('/'));
|
|
69
|
+
if (actionParam) {
|
|
70
|
+
const actionName = actionParam.slice(1); // remove leading /
|
|
71
|
+
const actionFn = route.actions?.[actionName];
|
|
72
|
+
if (!actionFn) {
|
|
73
|
+
return new Response(`Action '${actionName}' not found`, { status: 404 });
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const formData = await request.formData();
|
|
77
|
+
const actionResult = await actionFn(formData, env, context);
|
|
78
|
+
// After action, re-run load and re-render with action result
|
|
79
|
+
const loadData = route.load ? await route.load(context) : {};
|
|
80
|
+
const data = { ...loadData, actionResult, actionName };
|
|
81
|
+
return renderPage(route, data, layouts);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
// Re-render with error
|
|
85
|
+
const loadData = route.load ? await route.load(context) : {};
|
|
86
|
+
const data = { ...loadData, actionError: err.message, actionName };
|
|
87
|
+
return renderPage(route, data, layouts);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// --- GET: load + render ---
|
|
92
|
+
try {
|
|
93
|
+
const data = route.load ? await route.load(context) : {};
|
|
94
|
+
return renderPage(route, data, layouts);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
return new Response(`Server Error: ${err.message}`, {
|
|
98
|
+
status: 500,
|
|
99
|
+
headers: { 'content-type': 'text/html' },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return routeHandler();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Render a page through its layout */
|
|
108
|
+
function renderPage(route, data, layouts) {
|
|
109
|
+
const content = route.render(data);
|
|
110
|
+
const layoutName = route.layout ?? 'default';
|
|
111
|
+
const layout = layouts[layoutName];
|
|
112
|
+
const html = layout
|
|
113
|
+
? layout.render({ content, data })
|
|
114
|
+
: content;
|
|
115
|
+
return new Response(html, {
|
|
116
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
type ContainerLike = {
|
|
2
|
+
start?: (options: {
|
|
3
|
+
envVars?: Record<string, string>;
|
|
4
|
+
}) => Promise<unknown>;
|
|
5
|
+
fetch: (request: Request) => Promise<Response>;
|
|
6
|
+
};
|
|
7
|
+
type ContainerFactory = (namespace: DurableObjectNamespace, slug: string) => ContainerLike;
|
|
8
|
+
export interface StartContainerOptions {
|
|
9
|
+
namespace: DurableObjectNamespace;
|
|
10
|
+
slug: string;
|
|
11
|
+
containerFactory: ContainerFactory;
|
|
12
|
+
envVars?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export interface ProxyToContainerOptions extends StartContainerOptions {
|
|
15
|
+
request: Request;
|
|
16
|
+
onError?: (slug: string, err: unknown) => Response;
|
|
17
|
+
}
|
|
18
|
+
export interface ContainerPathMatch {
|
|
19
|
+
slug: string;
|
|
20
|
+
containerPath: string;
|
|
21
|
+
}
|
|
22
|
+
export interface HandleContainerRoutingOptions {
|
|
23
|
+
request: Request;
|
|
24
|
+
appDomain: string;
|
|
25
|
+
proxyToSlug: (slug: string, request: Request) => Promise<Response>;
|
|
26
|
+
sitePrefix?: string;
|
|
27
|
+
viewSegment?: string;
|
|
28
|
+
blockContainerPathPrefix?: string;
|
|
29
|
+
skipSubdomainWhenPathStartsWith?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function extractSubdomainSlug(host: string, appDomain: string): string | null;
|
|
32
|
+
export declare function extractSlugFromPrefix(pathname: string, prefix: string): string | null;
|
|
33
|
+
export declare function matchContainerViewPath(pathname: string, opts?: {
|
|
34
|
+
sitePrefix?: string;
|
|
35
|
+
viewSegment?: string;
|
|
36
|
+
}): ContainerPathMatch | null;
|
|
37
|
+
export declare function rewriteProxyLocationHeader(location: string, slug: string, opts?: {
|
|
38
|
+
sitePrefix?: string;
|
|
39
|
+
viewSegment?: string;
|
|
40
|
+
}): string;
|
|
41
|
+
export declare function buildContainerRequest(request: Request, containerPath: string): Request;
|
|
42
|
+
export declare function createContainerEnvVars(opts: {
|
|
43
|
+
slug: string;
|
|
44
|
+
vars: Record<string, string | ((slug: string) => string)>;
|
|
45
|
+
}): Record<string, string>;
|
|
46
|
+
export declare function startContainer(options: StartContainerOptions): Promise<ContainerLike>;
|
|
47
|
+
export declare function proxyToContainer(options: ProxyToContainerOptions): Promise<Response>;
|
|
48
|
+
export declare function forwardJsonPostToContainerDO(options: {
|
|
49
|
+
namespace: DurableObjectNamespace;
|
|
50
|
+
slug: string;
|
|
51
|
+
containerFactory: ContainerFactory;
|
|
52
|
+
request: Request;
|
|
53
|
+
targetPath?: string;
|
|
54
|
+
}): Promise<Response>;
|
|
55
|
+
export declare function handleContainerRouting(options: HandleContainerRoutingOptions): Promise<Response | null>;
|
|
56
|
+
export declare const startSiteContainer: typeof startContainer;
|
|
57
|
+
export declare const proxyToSiteContainer: typeof proxyToContainer;
|
|
58
|
+
export declare const matchSiteViewPath: typeof matchContainerViewPath;
|
|
59
|
+
export declare const buildSiteContainerRequest: typeof buildContainerRequest;
|
|
60
|
+
export declare const createWpContainerEnvVars: typeof createContainerEnvVars;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export function extractSubdomainSlug(host, appDomain) {
|
|
2
|
+
if (host.endsWith('.localhost')) {
|
|
3
|
+
const slug = host.slice(0, -'.localhost'.length);
|
|
4
|
+
return slug || null;
|
|
5
|
+
}
|
|
6
|
+
if (host.endsWith('.' + appDomain)) {
|
|
7
|
+
const slug = host.slice(0, -(appDomain.length + 1));
|
|
8
|
+
return slug && !slug.includes('.') ? slug : null;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
export function extractSlugFromPrefix(pathname, prefix) {
|
|
13
|
+
if (!pathname.startsWith(prefix))
|
|
14
|
+
return null;
|
|
15
|
+
const rest = pathname.slice(prefix.length);
|
|
16
|
+
const slug = rest.split('/')[0];
|
|
17
|
+
return slug || null;
|
|
18
|
+
}
|
|
19
|
+
export function matchContainerViewPath(pathname, opts) {
|
|
20
|
+
const sitePrefix = opts?.sitePrefix ?? '/sites/';
|
|
21
|
+
const viewSegment = opts?.viewSegment ?? '/view';
|
|
22
|
+
if (!pathname.startsWith(sitePrefix))
|
|
23
|
+
return null;
|
|
24
|
+
const rest = pathname.slice(sitePrefix.length);
|
|
25
|
+
const slashIdx = rest.indexOf('/');
|
|
26
|
+
if (slashIdx <= 0)
|
|
27
|
+
return null;
|
|
28
|
+
const slug = rest.slice(0, slashIdx);
|
|
29
|
+
const after = rest.slice(slashIdx);
|
|
30
|
+
if (!(after === viewSegment || after.startsWith(viewSegment + '/')))
|
|
31
|
+
return null;
|
|
32
|
+
const containerPath = after.slice(viewSegment.length) || '/';
|
|
33
|
+
return { slug, containerPath };
|
|
34
|
+
}
|
|
35
|
+
export function rewriteProxyLocationHeader(location, slug, opts) {
|
|
36
|
+
const sitePrefix = opts?.sitePrefix ?? '/sites';
|
|
37
|
+
const viewSegment = opts?.viewSegment ?? '/view';
|
|
38
|
+
const prefix = `${sitePrefix}/${slug}${viewSegment}`;
|
|
39
|
+
if (location.startsWith('/'))
|
|
40
|
+
return prefix + location;
|
|
41
|
+
if (location.startsWith('http://') || location.startsWith('https://')) {
|
|
42
|
+
const loc = new URL(location);
|
|
43
|
+
loc.pathname = prefix + loc.pathname;
|
|
44
|
+
return loc.toString();
|
|
45
|
+
}
|
|
46
|
+
return location;
|
|
47
|
+
}
|
|
48
|
+
export function buildContainerRequest(request, containerPath) {
|
|
49
|
+
const url = new URL(request.url);
|
|
50
|
+
const target = new URL(containerPath, url.origin);
|
|
51
|
+
target.search = url.search;
|
|
52
|
+
return new Request(target.toString(), request);
|
|
53
|
+
}
|
|
54
|
+
export function createContainerEnvVars(opts) {
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const [key, value] of Object.entries(opts.vars)) {
|
|
57
|
+
out[key] = typeof value === 'function' ? value(opts.slug) : value;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
export async function startContainer(options) {
|
|
62
|
+
const container = options.containerFactory(options.namespace, options.slug);
|
|
63
|
+
if (options.envVars && container.start) {
|
|
64
|
+
await container.start({ envVars: options.envVars });
|
|
65
|
+
}
|
|
66
|
+
return container;
|
|
67
|
+
}
|
|
68
|
+
export async function proxyToContainer(options) {
|
|
69
|
+
try {
|
|
70
|
+
const container = await startContainer(options);
|
|
71
|
+
return await container.fetch(options.request);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (options.onError)
|
|
75
|
+
return options.onError(options.slug, err);
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function forwardJsonPostToContainerDO(options) {
|
|
80
|
+
const container = options.containerFactory(options.namespace, options.slug);
|
|
81
|
+
const body = await options.request.text();
|
|
82
|
+
const targetPath = options.targetPath ?? '/sql';
|
|
83
|
+
const doRequest = new Request(`https://do-internal${targetPath}`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'content-type': 'application/json' },
|
|
86
|
+
body,
|
|
87
|
+
});
|
|
88
|
+
return container.fetch(doRequest);
|
|
89
|
+
}
|
|
90
|
+
export async function handleContainerRouting(options) {
|
|
91
|
+
const sitePrefix = options.sitePrefix ?? '/sites/';
|
|
92
|
+
const viewSegment = options.viewSegment ?? '/view';
|
|
93
|
+
const blockedPrefix = options.blockContainerPathPrefix ?? '/__kuratchi/';
|
|
94
|
+
const skipSubdomainPrefix = options.skipSubdomainWhenPathStartsWith ?? '/sites/';
|
|
95
|
+
const url = new URL(options.request.url);
|
|
96
|
+
const hostHeader = options.request.headers.get('host') || url.host;
|
|
97
|
+
const host = hostHeader.split(':')[0];
|
|
98
|
+
const subdomainSlug = extractSubdomainSlug(host, options.appDomain);
|
|
99
|
+
if (subdomainSlug && !url.pathname.startsWith(skipSubdomainPrefix)) {
|
|
100
|
+
return options.proxyToSlug(subdomainSlug, options.request);
|
|
101
|
+
}
|
|
102
|
+
const pathMatch = matchContainerViewPath(url.pathname, { sitePrefix, viewSegment });
|
|
103
|
+
if (!pathMatch)
|
|
104
|
+
return null;
|
|
105
|
+
if (pathMatch.containerPath.startsWith(blockedPrefix)) {
|
|
106
|
+
return new Response('Not Found', { status: 404 });
|
|
107
|
+
}
|
|
108
|
+
const containerRequest = buildContainerRequest(options.request, pathMatch.containerPath);
|
|
109
|
+
const res = await options.proxyToSlug(pathMatch.slug, containerRequest);
|
|
110
|
+
const location = res.headers.get('location');
|
|
111
|
+
if (location && res.status >= 300 && res.status < 400) {
|
|
112
|
+
const rewritten = rewriteProxyLocationHeader(location, pathMatch.slug, {
|
|
113
|
+
sitePrefix: sitePrefix.endsWith('/') ? sitePrefix.slice(0, -1) : sitePrefix,
|
|
114
|
+
viewSegment,
|
|
115
|
+
});
|
|
116
|
+
const newRes = new Response(res.body, res);
|
|
117
|
+
newRes.headers.set('location', rewritten);
|
|
118
|
+
return newRes;
|
|
119
|
+
}
|
|
120
|
+
return res;
|
|
121
|
+
}
|
|
122
|
+
// Backwards-compatible aliases (to avoid breaking early adopters)
|
|
123
|
+
export const startSiteContainer = startContainer;
|
|
124
|
+
export const proxyToSiteContainer = proxyToContainer;
|
|
125
|
+
export const matchSiteViewPath = matchContainerViewPath;
|
|
126
|
+
export const buildSiteContainerRequest = buildContainerRequest;
|
|
127
|
+
export const createWpContainerEnvVars = createContainerEnvVars;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-scoped context.
|
|
3
|
+
*
|
|
4
|
+
* Env bindings: use `import { env } from 'cloudflare:workers'` directly.
|
|
5
|
+
* Request/ctx/locals: set per-request by the framework, accessed via helpers.
|
|
6
|
+
*
|
|
7
|
+
* Workers are single-threaded per request — module-scoped
|
|
8
|
+
* variables are safe and require no Node.js compat flags.
|
|
9
|
+
*/
|
|
10
|
+
export interface BreadcrumbItem {
|
|
11
|
+
label: string;
|
|
12
|
+
href?: string;
|
|
13
|
+
current?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Called by the framework at the start of each request */
|
|
16
|
+
export declare function __setRequestContext(ctx: any, request: Request): void;
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Use `import { env } from 'cloudflare:workers'` instead.
|
|
19
|
+
* Kept for backward compatibility — delegates to the native env import.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getEnv<T = any>(): T;
|
|
22
|
+
/** @internal — called by compiler to stash env ref for getEnv() compat */
|
|
23
|
+
export declare function __setEnvCompat(env: any): void;
|
|
24
|
+
/** Get the execution context (waitUntil, passThroughOnException) */
|
|
25
|
+
export declare function getCtx(): ExecutionContext;
|
|
26
|
+
/** Get the current request */
|
|
27
|
+
export declare function getRequest(): Request;
|
|
28
|
+
/** Get request-scoped locals (session, auth, custom data) */
|
|
29
|
+
export declare function getLocals<T = Record<string, any>>(): T;
|
|
30
|
+
/** Get matched route params for the current request (e.g. { slug: 'my-post' }) */
|
|
31
|
+
export declare function getParams<T = Record<string, string>>(): T;
|
|
32
|
+
/** Get one matched route param by key (e.g. getParam('slug')) */
|
|
33
|
+
export declare function getParam(name: string): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Server-side redirect helper for actions/load logic.
|
|
36
|
+
* Sets the post-action redirect target consumed by the framework's PRG flow.
|
|
37
|
+
*/
|
|
38
|
+
export declare function redirect(path: string, status?: number): void;
|
|
39
|
+
/** Backward-compatible alias for redirect() */
|
|
40
|
+
export declare function goto(path: string, status?: number): void;
|
|
41
|
+
export declare function setBreadcrumbs(items: BreadcrumbItem[]): void;
|
|
42
|
+
export declare function getBreadcrumbs(): BreadcrumbItem[];
|
|
43
|
+
export declare function breadcrumbsHome(label?: string, href?: string): BreadcrumbItem;
|
|
44
|
+
export declare function breadcrumbsPrev(label: string, href: string): BreadcrumbItem;
|
|
45
|
+
export declare function breadcrumbsNext(label: string, href: string): BreadcrumbItem;
|
|
46
|
+
export declare function breadcrumbsCurrent(label: string): BreadcrumbItem;
|
|
47
|
+
/** Build a reasonable default breadcrumb trail from pathname + params */
|
|
48
|
+
export declare function buildDefaultBreadcrumbs(pathname: string, _params?: Record<string, string>): BreadcrumbItem[];
|
|
49
|
+
/** Set a value on request-scoped locals (used by framework internals) */
|
|
50
|
+
export declare function __setLocal(key: string, value: any): void;
|
|
51
|
+
/** Get the full locals object reference (used by framework internals) */
|
|
52
|
+
export declare function __getLocals(): Record<string, any>;
|
|
53
|
+
/** HTML-escape a value for safe output in templates */
|
|
54
|
+
export declare function __esc(v: any): string;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-scoped context.
|
|
3
|
+
*
|
|
4
|
+
* Env bindings: use `import { env } from 'cloudflare:workers'` directly.
|
|
5
|
+
* Request/ctx/locals: set per-request by the framework, accessed via helpers.
|
|
6
|
+
*
|
|
7
|
+
* Workers are single-threaded per request — module-scoped
|
|
8
|
+
* variables are safe and require no Node.js compat flags.
|
|
9
|
+
*/
|
|
10
|
+
let __ctx = null;
|
|
11
|
+
let __request = null;
|
|
12
|
+
let __locals = {};
|
|
13
|
+
/** Called by the framework at the start of each request */
|
|
14
|
+
export function __setRequestContext(ctx, request) {
|
|
15
|
+
__ctx = ctx;
|
|
16
|
+
__request = request;
|
|
17
|
+
__locals = {};
|
|
18
|
+
// Expose context on globalThis for @kuratchi/auth and other packages
|
|
19
|
+
// Workers are single-threaded per request — this is safe
|
|
20
|
+
globalThis.__kuratchi_context__ = {
|
|
21
|
+
get request() { return __request; },
|
|
22
|
+
get locals() { return __locals; },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* @deprecated Use `import { env } from 'cloudflare:workers'` instead.
|
|
27
|
+
* Kept for backward compatibility — delegates to the native env import.
|
|
28
|
+
*/
|
|
29
|
+
export function getEnv() {
|
|
30
|
+
return globalThis.__cloudflare_env__;
|
|
31
|
+
}
|
|
32
|
+
/** @internal — called by compiler to stash env ref for getEnv() compat */
|
|
33
|
+
export function __setEnvCompat(env) {
|
|
34
|
+
globalThis.__cloudflare_env__ = env;
|
|
35
|
+
}
|
|
36
|
+
/** Get the execution context (waitUntil, passThroughOnException) */
|
|
37
|
+
export function getCtx() {
|
|
38
|
+
if (!__ctx)
|
|
39
|
+
throw new Error('getCtx() called outside of a request context');
|
|
40
|
+
return __ctx;
|
|
41
|
+
}
|
|
42
|
+
/** Get the current request */
|
|
43
|
+
export function getRequest() {
|
|
44
|
+
if (!__request)
|
|
45
|
+
throw new Error('getRequest() called outside of a request context');
|
|
46
|
+
return __request;
|
|
47
|
+
}
|
|
48
|
+
/** Get request-scoped locals (session, auth, custom data) */
|
|
49
|
+
export function getLocals() {
|
|
50
|
+
return __locals;
|
|
51
|
+
}
|
|
52
|
+
/** Get matched route params for the current request (e.g. { slug: 'my-post' }) */
|
|
53
|
+
export function getParams() {
|
|
54
|
+
return (__locals?.params ?? {});
|
|
55
|
+
}
|
|
56
|
+
/** Get one matched route param by key (e.g. getParam('slug')) */
|
|
57
|
+
export function getParam(name) {
|
|
58
|
+
return (__locals?.params ?? {})[name];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Server-side redirect helper for actions/load logic.
|
|
62
|
+
* Sets the post-action redirect target consumed by the framework's PRG flow.
|
|
63
|
+
*/
|
|
64
|
+
export function redirect(path, status = 303) {
|
|
65
|
+
__locals.__redirectTo = path;
|
|
66
|
+
__locals.__redirectStatus = status;
|
|
67
|
+
}
|
|
68
|
+
/** Backward-compatible alias for redirect() */
|
|
69
|
+
export function goto(path, status = 303) {
|
|
70
|
+
redirect(path, status);
|
|
71
|
+
}
|
|
72
|
+
export function setBreadcrumbs(items) {
|
|
73
|
+
__locals.__breadcrumbs = items;
|
|
74
|
+
}
|
|
75
|
+
export function getBreadcrumbs() {
|
|
76
|
+
return (__locals.__breadcrumbs ?? []);
|
|
77
|
+
}
|
|
78
|
+
export function breadcrumbsHome(label = 'Home', href = '/') {
|
|
79
|
+
return { label, href };
|
|
80
|
+
}
|
|
81
|
+
export function breadcrumbsPrev(label, href) {
|
|
82
|
+
return { label, href };
|
|
83
|
+
}
|
|
84
|
+
export function breadcrumbsNext(label, href) {
|
|
85
|
+
return { label, href };
|
|
86
|
+
}
|
|
87
|
+
export function breadcrumbsCurrent(label) {
|
|
88
|
+
return { label, current: true };
|
|
89
|
+
}
|
|
90
|
+
function titleizeSegment(segment) {
|
|
91
|
+
const decoded = decodeURIComponent(segment);
|
|
92
|
+
if (!decoded)
|
|
93
|
+
return '';
|
|
94
|
+
return decoded
|
|
95
|
+
.replace(/[-_]+/g, ' ')
|
|
96
|
+
.replace(/\b\w/g, (m) => m.toUpperCase());
|
|
97
|
+
}
|
|
98
|
+
/** Build a reasonable default breadcrumb trail from pathname + params */
|
|
99
|
+
export function buildDefaultBreadcrumbs(pathname, _params = {}) {
|
|
100
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
101
|
+
if (parts.length === 0)
|
|
102
|
+
return [{ label: 'Home', current: true }];
|
|
103
|
+
const items = [{ label: 'Home', href: '/' }];
|
|
104
|
+
let acc = '';
|
|
105
|
+
for (let i = 0; i < parts.length; i++) {
|
|
106
|
+
acc += `/${parts[i]}`;
|
|
107
|
+
const isLast = i === parts.length - 1;
|
|
108
|
+
items.push({
|
|
109
|
+
label: titleizeSegment(parts[i]),
|
|
110
|
+
href: isLast ? undefined : acc,
|
|
111
|
+
current: isLast,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return items;
|
|
115
|
+
}
|
|
116
|
+
/** Set a value on request-scoped locals (used by framework internals) */
|
|
117
|
+
export function __setLocal(key, value) {
|
|
118
|
+
__locals[key] = value;
|
|
119
|
+
}
|
|
120
|
+
/** Get the full locals object reference (used by framework internals) */
|
|
121
|
+
export function __getLocals() {
|
|
122
|
+
return __locals;
|
|
123
|
+
}
|
|
124
|
+
/** HTML-escape a value for safe output in templates */
|
|
125
|
+
export function __esc(v) {
|
|
126
|
+
if (v == null)
|
|
127
|
+
return '';
|
|
128
|
+
return String(v)
|
|
129
|
+
.replace(/&/g, '&')
|
|
130
|
+
.replace(/</g, '<')
|
|
131
|
+
.replace(/>/g, '>')
|
|
132
|
+
.replace(/"/g, '"')
|
|
133
|
+
.replace(/'/g, ''');
|
|
134
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KuratchiJS — Durable Object helpers
|
|
3
|
+
*
|
|
4
|
+
* kuratchiDO
|
|
5
|
+
* Base class for DO handler modules. Extend it, write your methods,
|
|
6
|
+
* declare `static binding = 'BINDING_NAME'` — the compiler does the rest.
|
|
7
|
+
*
|
|
8
|
+
* - Methods are copied onto the generated DO class prototype
|
|
9
|
+
* - RPC proxy exports are auto-generated for pages
|
|
10
|
+
* - Stub resolvers are registered from config
|
|
11
|
+
*
|
|
12
|
+
* The compiler uses __registerDoResolver / __getDoStub internally.
|
|
13
|
+
* User code never touches these — they're wired up from kuratchi.config.ts.
|
|
14
|
+
*/
|
|
15
|
+
/** @internal — called by compiler-generated init code */
|
|
16
|
+
export declare function __registerDoResolver(binding: string, resolver: () => Promise<any>): void;
|
|
17
|
+
/** @internal — called by compiler-generated init code */
|
|
18
|
+
export declare function __registerDoClassBinding(klass: Function, binding: string): void;
|
|
19
|
+
/** @internal — called by compiler-generated RPC proxy modules */
|
|
20
|
+
export declare function __getDoStub(binding: string): Promise<any>;
|
|
21
|
+
/**
|
|
22
|
+
* Base class for Durable Object handler modules.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* // sites.ts — extend, write methods, done.
|
|
26
|
+
* export default class Sites extends kuratchiDO {
|
|
27
|
+
* static binding = 'ORG_DB';
|
|
28
|
+
*
|
|
29
|
+
* async getSites(userId: number) {
|
|
30
|
+
* return (await this.db.sites.where({ userId }).many()).data ?? [];
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* `this.db` is the ORM instance — set by the generated DO class at runtime.
|
|
36
|
+
*
|
|
37
|
+
* Call `Sites.rpc()` in the same file if you need to call DO methods
|
|
38
|
+
* from worker-side helper functions:
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* const remote = Sites.rpc();
|
|
42
|
+
*
|
|
43
|
+
* export async function createSite(formData: FormData) {
|
|
44
|
+
* await remote.createSiteRecord({ name, slug, userId });
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare class kuratchiDO {
|
|
49
|
+
db: Record<string, any>;
|
|
50
|
+
/** The DO namespace binding this handler belongs to (e.g. 'ORG_DB'). */
|
|
51
|
+
static binding: string;
|
|
52
|
+
/**
|
|
53
|
+
* Implicit RPC proxy getter.
|
|
54
|
+
*
|
|
55
|
+
* Lets handler modules call `MyDO.remote.someMethod()` without declaring
|
|
56
|
+
* `const remote = MyDO.rpc();` boilerplate.
|
|
57
|
+
*/
|
|
58
|
+
static get remote(): any;
|
|
59
|
+
/**
|
|
60
|
+
* Create a typed RPC proxy for calling DO methods from worker-side code.
|
|
61
|
+
* The actual stub resolution is lazy — happens at call time, not import time.
|
|
62
|
+
*/
|
|
63
|
+
static rpc<T extends typeof kuratchiDO>(this: T): RpcOf<InstanceType<T>>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* DX helper for worker-side code in DO handler modules.
|
|
67
|
+
*
|
|
68
|
+
* Usage:
|
|
69
|
+
* `const doSites = doRpc(Sites);`
|
|
70
|
+
* `await doSites.getSiteBySlug(slug);`
|
|
71
|
+
*/
|
|
72
|
+
export declare function doRpc<T extends typeof kuratchiDO>(klass: T): RpcOf<InstanceType<T>>;
|
|
73
|
+
/** Extract only the method keys from a class instance (excludes 'db'). */
|
|
74
|
+
type MethodKeys<T> = {
|
|
75
|
+
[K in keyof T]: K extends 'db' ? never : T[K] extends (...args: any[]) => any ? K : never;
|
|
76
|
+
}[keyof T];
|
|
77
|
+
/** Map class methods → RPC callers (same args, Promise-wrapped return). */
|
|
78
|
+
export type RpcOf<T> = {
|
|
79
|
+
[K in MethodKeys<T>]: T[K] extends (...args: infer A) => infer R ? (...args: A) => Promise<Awaited<R>> : never;
|
|
80
|
+
};
|
|
81
|
+
export {};
|