@shaferllc/keel 0.12.0
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 +21 -0
- package/README.md +222 -0
- package/dist/core/application.d.ts +40 -0
- package/dist/core/application.js +115 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.js +55 -0
- package/dist/core/container.d.ts +30 -0
- package/dist/core/container.js +59 -0
- package/dist/core/exceptions.d.ts +26 -0
- package/dist/core/exceptions.js +56 -0
- package/dist/core/helpers.d.ts +42 -0
- package/dist/core/helpers.js +55 -0
- package/dist/core/http/kernel.d.ts +29 -0
- package/dist/core/http/kernel.js +173 -0
- package/dist/core/http/router.d.ts +160 -0
- package/dist/core/http/router.js +344 -0
- package/dist/core/index.d.ts +21 -0
- package/dist/core/index.js +13 -0
- package/dist/core/inertia.d.ts +34 -0
- package/dist/core/inertia.js +68 -0
- package/dist/core/provider.d.ts +16 -0
- package/dist/core/provider.js +16 -0
- package/dist/core/request.d.ts +76 -0
- package/dist/core/request.js +151 -0
- package/dist/core/validation.d.ts +32 -0
- package/dist/core/validation.js +33 -0
- package/dist/core/view.d.ts +29 -0
- package/dist/core/view.js +27 -0
- package/package.json +57 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request helpers — reach the current request/response without threading the
|
|
3
|
+
* Hono context (`c`) through every function.
|
|
4
|
+
*
|
|
5
|
+
* index() {
|
|
6
|
+
* return json({ id: param("id") });
|
|
7
|
+
* }
|
|
8
|
+
*
|
|
9
|
+
* `${request.method} ${request.path} → ${request.status}`
|
|
10
|
+
*
|
|
11
|
+
* These resolve the active context from async-context storage, which the HTTP
|
|
12
|
+
* kernel enables for every request. They only work inside a request.
|
|
13
|
+
*/
|
|
14
|
+
import { getContext } from "hono/context-storage";
|
|
15
|
+
/** The current request context. Throws if called outside a request. */
|
|
16
|
+
export function ctx() {
|
|
17
|
+
return getContext();
|
|
18
|
+
}
|
|
19
|
+
/** The context if there is one, else undefined — never throws. */
|
|
20
|
+
function maybeCtx() {
|
|
21
|
+
try {
|
|
22
|
+
return getContext();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/* ------------------------------ responses ------------------------------ */
|
|
29
|
+
/* These work inside a handler AND standalone (e.g. as a static route value).
|
|
30
|
+
* Inside a request they build on the context (merging any headers); outside a
|
|
31
|
+
* request they return a plain Response, so `router.get("/ping", json({...}))`
|
|
32
|
+
* works — the router clones it per request. */
|
|
33
|
+
export function json(data, status) {
|
|
34
|
+
const c = maybeCtx();
|
|
35
|
+
return c
|
|
36
|
+
? c.json(data, status)
|
|
37
|
+
: Response.json(data, status ? { status } : undefined);
|
|
38
|
+
}
|
|
39
|
+
export function text(body, status) {
|
|
40
|
+
const c = maybeCtx();
|
|
41
|
+
return c
|
|
42
|
+
? c.text(body, status)
|
|
43
|
+
: new Response(body, {
|
|
44
|
+
status,
|
|
45
|
+
headers: { "content-type": "text/plain; charset=UTF-8" },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function html(body, status) {
|
|
49
|
+
const c = maybeCtx();
|
|
50
|
+
return c
|
|
51
|
+
? c.html(body, status)
|
|
52
|
+
: new Response(body, {
|
|
53
|
+
status,
|
|
54
|
+
headers: { "content-type": "text/html; charset=UTF-8" },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export function redirect(location, status) {
|
|
58
|
+
const c = maybeCtx();
|
|
59
|
+
return c
|
|
60
|
+
? c.redirect(location, status)
|
|
61
|
+
: new Response(null, { status: status ?? 302, headers: { location } });
|
|
62
|
+
}
|
|
63
|
+
export const response = {
|
|
64
|
+
json(data, status) {
|
|
65
|
+
return json(data, status);
|
|
66
|
+
},
|
|
67
|
+
text(body, status) {
|
|
68
|
+
return text(body, status);
|
|
69
|
+
},
|
|
70
|
+
html(body, status) {
|
|
71
|
+
return html(body, status);
|
|
72
|
+
},
|
|
73
|
+
redirect(location, status) {
|
|
74
|
+
return redirect(location, status);
|
|
75
|
+
},
|
|
76
|
+
status(code) {
|
|
77
|
+
ctx().status(code);
|
|
78
|
+
return response;
|
|
79
|
+
},
|
|
80
|
+
header(name, value) {
|
|
81
|
+
ctx().header(name, value);
|
|
82
|
+
return response;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
/* ---------------------------- request access --------------------------- */
|
|
86
|
+
/**
|
|
87
|
+
* The current request/response, as a flat accessor:
|
|
88
|
+
*
|
|
89
|
+
* request.method request.path request.url request.status
|
|
90
|
+
* request.header("authorization") request.param("id")
|
|
91
|
+
* await request.json() request.raw
|
|
92
|
+
*/
|
|
93
|
+
export const request = {
|
|
94
|
+
get method() {
|
|
95
|
+
return ctx().req.method;
|
|
96
|
+
},
|
|
97
|
+
get path() {
|
|
98
|
+
return ctx().req.path;
|
|
99
|
+
},
|
|
100
|
+
get url() {
|
|
101
|
+
return ctx().req.url;
|
|
102
|
+
},
|
|
103
|
+
/** The response status (useful after `await next()` in middleware). */
|
|
104
|
+
get status() {
|
|
105
|
+
return ctx().res.status;
|
|
106
|
+
},
|
|
107
|
+
header(name) {
|
|
108
|
+
return ctx().req.header(name);
|
|
109
|
+
},
|
|
110
|
+
param(name) {
|
|
111
|
+
const c = ctx();
|
|
112
|
+
return name ? c.req.param(name) : c.req.param();
|
|
113
|
+
},
|
|
114
|
+
query(name) {
|
|
115
|
+
const c = ctx();
|
|
116
|
+
return name ? c.req.query(name) : c.req.query();
|
|
117
|
+
},
|
|
118
|
+
json() {
|
|
119
|
+
return ctx().req.json();
|
|
120
|
+
},
|
|
121
|
+
/** The raw web Request. */
|
|
122
|
+
get raw() {
|
|
123
|
+
return ctx().req.raw;
|
|
124
|
+
},
|
|
125
|
+
/** The matched route: `{ name, pattern, methods }`. */
|
|
126
|
+
get route() {
|
|
127
|
+
return ctx().get("route");
|
|
128
|
+
},
|
|
129
|
+
/** Whether the matched route has the given name. */
|
|
130
|
+
routeIs(name) {
|
|
131
|
+
return ctx().get("route")?.name === name;
|
|
132
|
+
},
|
|
133
|
+
/** A subdomain parameter captured from a domain-bound route. */
|
|
134
|
+
subdomain(name) {
|
|
135
|
+
return ctx().get("subdomains")?.[name];
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
export function param(name) {
|
|
139
|
+
const c = ctx();
|
|
140
|
+
return name ? c.req.param(name) : c.req.param();
|
|
141
|
+
}
|
|
142
|
+
export function query(name) {
|
|
143
|
+
const c = ctx();
|
|
144
|
+
return name ? c.req.query(name) : c.req.query();
|
|
145
|
+
}
|
|
146
|
+
export function header(name) {
|
|
147
|
+
return ctx().req.header(name);
|
|
148
|
+
}
|
|
149
|
+
export function body() {
|
|
150
|
+
return ctx().req.json();
|
|
151
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request validation. `validate()` parses input against a schema and returns
|
|
3
|
+
* typed data, or throws a ValidationException (which the HTTP kernel renders as
|
|
4
|
+
* a 422 with per-field errors).
|
|
5
|
+
*
|
|
6
|
+
* It is schema-library-agnostic: any schema with a Zod-style `safeParse` works
|
|
7
|
+
* (Zod, and anything that mirrors its shape), so the framework never bundles a
|
|
8
|
+
* validation library — bring your own.
|
|
9
|
+
*/
|
|
10
|
+
/** The minimal shape Keel needs from a schema — Zod satisfies this. */
|
|
11
|
+
export interface Schema<T> {
|
|
12
|
+
safeParse(data: unknown): {
|
|
13
|
+
success: true;
|
|
14
|
+
data: T;
|
|
15
|
+
} | {
|
|
16
|
+
success: false;
|
|
17
|
+
error: {
|
|
18
|
+
issues: ReadonlyArray<{
|
|
19
|
+
path: PropertyKey[];
|
|
20
|
+
message: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validate `data` (or the request JSON body, if omitted) against a schema.
|
|
27
|
+
* Returns the parsed, typed value; throws `ValidationException` on failure.
|
|
28
|
+
*
|
|
29
|
+
* const data = await validate(NewUser); // validates the body
|
|
30
|
+
* const q = validate(SearchQuery, request.query()); // validates given data
|
|
31
|
+
*/
|
|
32
|
+
export declare function validate<T>(schema: Schema<T>, data?: unknown): Promise<T>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request validation. `validate()` parses input against a schema and returns
|
|
3
|
+
* typed data, or throws a ValidationException (which the HTTP kernel renders as
|
|
4
|
+
* a 422 with per-field errors).
|
|
5
|
+
*
|
|
6
|
+
* It is schema-library-agnostic: any schema with a Zod-style `safeParse` works
|
|
7
|
+
* (Zod, and anything that mirrors its shape), so the framework never bundles a
|
|
8
|
+
* validation library — bring your own.
|
|
9
|
+
*/
|
|
10
|
+
import { ValidationException } from "./exceptions.js";
|
|
11
|
+
import { body } from "./request.js";
|
|
12
|
+
/**
|
|
13
|
+
* Validate `data` (or the request JSON body, if omitted) against a schema.
|
|
14
|
+
* Returns the parsed, typed value; throws `ValidationException` on failure.
|
|
15
|
+
*
|
|
16
|
+
* const data = await validate(NewUser); // validates the body
|
|
17
|
+
* const q = validate(SearchQuery, request.query()); // validates given data
|
|
18
|
+
*/
|
|
19
|
+
export async function validate(schema, data) {
|
|
20
|
+
const input = data !== undefined ? data : await body();
|
|
21
|
+
const result = schema.safeParse(input);
|
|
22
|
+
if (result.success) {
|
|
23
|
+
return result.data;
|
|
24
|
+
}
|
|
25
|
+
const errors = {};
|
|
26
|
+
for (const issue of result.error.issues) {
|
|
27
|
+
const key = issue.path
|
|
28
|
+
.map((p) => (typeof p === "symbol" ? (p.description ?? "") : String(p)))
|
|
29
|
+
.join(".") || "_";
|
|
30
|
+
(errors[key] ??= []).push(issue.message);
|
|
31
|
+
}
|
|
32
|
+
throw new ValidationException(errors);
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The view layer. Keel views are components (Hono JSX elements or any object
|
|
3
|
+
* with a toString()) that render to an HTML string.
|
|
4
|
+
*
|
|
5
|
+
* This is intentionally platform-neutral — no filesystem, no Node built-ins —
|
|
6
|
+
* so the same views run under Node and on Cloudflare Workers. Components live
|
|
7
|
+
* by convention in resources/views/.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Anything renderable to HTML: a raw string, a JSX node (whose toString()
|
|
11
|
+
* yields HTML), a promise of either, or nullish (renders empty). This matches
|
|
12
|
+
* the return type of Hono JSX function components.
|
|
13
|
+
*/
|
|
14
|
+
export type Renderable = string | Promise<string> | {
|
|
15
|
+
toString(): string | Promise<string>;
|
|
16
|
+
} | null | undefined;
|
|
17
|
+
export interface ViewConfig {
|
|
18
|
+
/** Prepend an HTML5 doctype to rendered documents. Default: true. */
|
|
19
|
+
doctype?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare class View {
|
|
22
|
+
private config;
|
|
23
|
+
constructor(config?: ViewConfig);
|
|
24
|
+
/**
|
|
25
|
+
* Render a component to a complete HTML string. Handles both synchronous
|
|
26
|
+
* and async (Suspense) JSX trees.
|
|
27
|
+
*/
|
|
28
|
+
render(content: Renderable): Promise<string>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The view layer. Keel views are components (Hono JSX elements or any object
|
|
3
|
+
* with a toString()) that render to an HTML string.
|
|
4
|
+
*
|
|
5
|
+
* This is intentionally platform-neutral — no filesystem, no Node built-ins —
|
|
6
|
+
* so the same views run under Node and on Cloudflare Workers. Components live
|
|
7
|
+
* by convention in resources/views/.
|
|
8
|
+
*/
|
|
9
|
+
export class View {
|
|
10
|
+
config;
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Render a component to a complete HTML string. Handles both synchronous
|
|
16
|
+
* and async (Suspense) JSX trees.
|
|
17
|
+
*/
|
|
18
|
+
async render(content) {
|
|
19
|
+
const doctype = this.config.doctype === false ? "" : "<!DOCTYPE html>\n";
|
|
20
|
+
if (content == null)
|
|
21
|
+
return doctype;
|
|
22
|
+
// `await` resolves promises and passes strings/JSX nodes through; String()
|
|
23
|
+
// then collapses a JSX node to its rendered HTML.
|
|
24
|
+
const resolved = await content;
|
|
25
|
+
return doctype + String(resolved);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shaferllc/keel",
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The house framework for Node.js — a service container, providers, routing, JSX views, and a code-generating console.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Tom Shafer",
|
|
8
|
+
"homepage": "https://github.com/shaferllc/keel#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/shaferllc/keel.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"framework",
|
|
15
|
+
"typescript",
|
|
16
|
+
"nodejs",
|
|
17
|
+
"hono",
|
|
18
|
+
"service-container",
|
|
19
|
+
"web-framework",
|
|
20
|
+
"edge",
|
|
21
|
+
"cloudflare-workers"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
"./core": {
|
|
25
|
+
"types": "./dist/core/index.d.ts",
|
|
26
|
+
"import": "./dist/core/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"keel": "tsx bin/keel.ts",
|
|
37
|
+
"serve": "tsx bin/keel.ts serve",
|
|
38
|
+
"dev": "tsx watch bin/keel.ts serve",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"test": "node --import tsx --test tests/*.test.ts",
|
|
41
|
+
"test:coverage": "node --import tsx --test --experimental-test-coverage --test-coverage-exclude='tests/**' tests/*.test.ts",
|
|
42
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
43
|
+
"prepare": "npm run build"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"dotenv": "^16.4.7",
|
|
47
|
+
"hono": "^4.6.14"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@hono/node-server": "^1.13.7",
|
|
51
|
+
"@types/node": "^26.1.1",
|
|
52
|
+
"commander": "^12.1.0",
|
|
53
|
+
"tsx": "^4.19.2",
|
|
54
|
+
"typescript": "^5.7.2",
|
|
55
|
+
"zod": "^4.4.3"
|
|
56
|
+
}
|
|
57
|
+
}
|