@shaferllc/keel 0.12.0 → 0.36.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.
Files changed (50) hide show
  1. package/README.md +30 -3
  2. package/dist/core/application.js +9 -0
  3. package/dist/core/auth.d.ts +41 -0
  4. package/dist/core/auth.js +70 -0
  5. package/dist/core/cache.d.ts +41 -0
  6. package/dist/core/cache.js +81 -0
  7. package/dist/core/casts.d.ts +19 -0
  8. package/dist/core/casts.js +69 -0
  9. package/dist/core/crypto.d.ts +24 -0
  10. package/dist/core/crypto.js +97 -0
  11. package/dist/core/database.d.ts +58 -0
  12. package/dist/core/database.js +140 -0
  13. package/dist/core/debug.d.ts +12 -0
  14. package/dist/core/debug.js +55 -0
  15. package/dist/core/events.d.ts +21 -0
  16. package/dist/core/events.js +46 -0
  17. package/dist/core/exceptions.d.ts +10 -1
  18. package/dist/core/exceptions.js +10 -1
  19. package/dist/core/factory.d.ts +84 -0
  20. package/dist/core/factory.js +154 -0
  21. package/dist/core/helpers.d.ts +13 -0
  22. package/dist/core/helpers.js +24 -0
  23. package/dist/core/http/kernel.js +21 -2
  24. package/dist/core/http/router.d.ts +32 -8
  25. package/dist/core/http/router.js +74 -5
  26. package/dist/core/index.d.ts +33 -2
  27. package/dist/core/index.js +18 -1
  28. package/dist/core/logger.d.ts +29 -0
  29. package/dist/core/logger.js +49 -0
  30. package/dist/core/mail.d.ts +94 -0
  31. package/dist/core/mail.js +157 -0
  32. package/dist/core/migrations.d.ts +76 -0
  33. package/dist/core/migrations.js +211 -0
  34. package/dist/core/model.d.ts +75 -0
  35. package/dist/core/model.js +202 -0
  36. package/dist/core/notification.d.ts +82 -0
  37. package/dist/core/notification.js +129 -0
  38. package/dist/core/queue.d.ts +73 -0
  39. package/dist/core/queue.js +88 -0
  40. package/dist/core/rate-limit.d.ts +23 -0
  41. package/dist/core/rate-limit.js +50 -0
  42. package/dist/core/relations.d.ts +90 -0
  43. package/dist/core/relations.js +229 -0
  44. package/dist/core/request.d.ts +53 -1
  45. package/dist/core/request.js +187 -0
  46. package/dist/core/session.d.ts +46 -0
  47. package/dist/core/session.js +112 -0
  48. package/dist/core/static.d.ts +27 -0
  49. package/dist/core/static.js +61 -0
  50. package/package.json +2 -1
@@ -11,7 +11,9 @@
11
11
  * These resolve the active context from async-context storage, which the HTTP
12
12
  * kernel enables for every request. They only work inside a request.
13
13
  */
14
+ import { setCookie } from "hono/cookie";
14
15
  import type { Context } from "hono";
16
+ type CookieOptions = Parameters<typeof setCookie>[3];
15
17
  /** The current request context. Throws if called outside a request. */
16
18
  export declare function ctx(): Context;
17
19
  export declare function json(data: unknown, status?: number): Response;
@@ -31,10 +33,28 @@ interface ResponseHelper {
31
33
  text(body: string, status?: number): Response;
32
34
  html(body: string, status?: number): Response;
33
35
  redirect(location: string, status?: number): Response;
36
+ /** Send a value — objects become JSON, everything else becomes text. */
37
+ send(data: unknown, status?: number): Response;
34
38
  /** Set the response status (chainable). */
35
39
  status(code: number): ResponseHelper;
36
40
  /** Set a response header (chainable). */
37
41
  header(name: string, value: string): ResponseHelper;
42
+ /** Set the Content-Type (chainable). */
43
+ type(mime: string): ResponseHelper;
44
+ /** Append a value to a (possibly multi-value) header (chainable). */
45
+ append(name: string, value: string): ResponseHelper;
46
+ /** Remove a response header (chainable). */
47
+ removeHeader(name: string): ResponseHelper;
48
+ /** Queue a Set-Cookie on the response (chainable). */
49
+ cookie(name: string, value: string, options?: CookieOptions): ResponseHelper;
50
+ /** Clear a cookie (chainable). */
51
+ clearCookie(name: string, options?: CookieOptions): ResponseHelper;
52
+ /** Abort the request with an HTTP exception. */
53
+ abort(message: string, status?: number): never;
54
+ /** Abort only if the condition is truthy. */
55
+ abortIf(condition: unknown, message: string, status?: number): void;
56
+ /** Abort unless the condition is truthy. */
57
+ abortUnless(condition: unknown, message: string, status?: number): void;
38
58
  }
39
59
  export declare const response: ResponseHelper;
40
60
  /**
@@ -60,12 +80,44 @@ export declare const request: {
60
80
  readonly route: {
61
81
  name?: string;
62
82
  pattern: string;
63
- methods: import(".").Method[];
83
+ methods: import("./index.js").Method[];
64
84
  } | undefined;
65
85
  /** Whether the matched route has the given name. */
66
86
  routeIs(name: string): boolean;
67
87
  /** A subdomain parameter captured from a domain-bound route. */
68
88
  subdomain(name: string): string | undefined;
89
+ /** A request cookie by name, or all cookies when called with no argument. */
90
+ cookie(name?: string): string | undefined | Record<string, string>;
91
+ /** The client IP, from X-Forwarded-For / X-Real-IP. */
92
+ ip(): string | undefined;
93
+ /** All inputs — query string merged with the parsed body (async). */
94
+ all(): Promise<Record<string, unknown>>;
95
+ /** An uploaded file by field name (a web `File`), or undefined. */
96
+ file(name: string): Promise<File | undefined>;
97
+ /** All uploaded files for a field name. */
98
+ files(name: string): Promise<File[]>;
99
+ /** Every uploaded file, grouped by field name. */
100
+ allFiles(): Promise<Record<string, File | File[]>>;
101
+ /** True if the request declares a body. */
102
+ hasBody(): boolean;
103
+ /** All request headers as an object. */
104
+ headers(): Record<string, string>;
105
+ /** The full X-Forwarded-For chain (client first). */
106
+ ips(): string[];
107
+ /** The best of the offered content types per the Accept header, or null. */
108
+ accepts(types: string[]): string | null;
109
+ /** Accepted content types, ordered by preference. */
110
+ types(): string[];
111
+ /** The best of the offered languages per Accept-Language, or null. */
112
+ language(languages: string[]): string | null;
113
+ /** Accepted languages, ordered by preference. */
114
+ languages(): string[];
115
+ /** A single input (from query or body), with an optional fallback (async). */
116
+ input<T = unknown>(key: string, fallback?: T): Promise<T>;
117
+ /** Only the named inputs (async). */
118
+ only(keys: string[]): Promise<Record<string, unknown>>;
119
+ /** Every input except the named ones (async). */
120
+ except(keys: string[]): Promise<Record<string, unknown>>;
69
121
  };
70
122
  export declare function param(): Record<string, string>;
71
123
  export declare function param(name: string): string;
@@ -12,6 +12,8 @@
12
12
  * kernel enables for every request. They only work inside a request.
13
13
  */
14
14
  import { getContext } from "hono/context-storage";
15
+ import { getCookie, setCookie, deleteCookie } from "hono/cookie";
16
+ import { HttpException } from "./exceptions.js";
15
17
  /** The current request context. Throws if called outside a request. */
16
18
  export function ctx() {
17
19
  return getContext();
@@ -25,6 +27,46 @@ function maybeCtx() {
25
27
  return undefined;
26
28
  }
27
29
  }
30
+ /** Parse the FormData once per request (web-standard, edge-safe). */
31
+ const FORM_CACHE = new WeakMap();
32
+ async function cachedFormData() {
33
+ const raw = ctx().req.raw;
34
+ if (FORM_CACHE.has(raw))
35
+ return FORM_CACHE.get(raw);
36
+ let fd = null;
37
+ try {
38
+ fd = await raw.formData();
39
+ }
40
+ catch {
41
+ fd = null;
42
+ }
43
+ FORM_CACHE.set(raw, fd);
44
+ return fd;
45
+ }
46
+ /** Parse an Accept-style header into values ordered by q-weight. */
47
+ function parseAccept(header) {
48
+ if (!header)
49
+ return [];
50
+ return header
51
+ .split(",")
52
+ .map((part) => {
53
+ const [value, ...params] = part.trim().split(";");
54
+ const q = params.find((p) => p.trim().startsWith("q="));
55
+ return { value: value.trim(), q: q ? parseFloat(q.split("=")[1]) : 1 };
56
+ })
57
+ .filter((x) => x.q > 0)
58
+ .sort((a, b) => b.q - a.q)
59
+ .map((x) => x.value);
60
+ }
61
+ function negotiate(headerName, offered) {
62
+ const accepted = parseAccept(ctx().req.header(headerName));
63
+ if (accepted.includes("*/*") || accepted.includes("*"))
64
+ return offered[0] ?? null;
65
+ for (const a of accepted)
66
+ if (offered.includes(a))
67
+ return a;
68
+ return null;
69
+ }
28
70
  /* ------------------------------ responses ------------------------------ */
29
71
  /* These work inside a handler AND standalone (e.g. as a static route value).
30
72
  * Inside a request they build on the context (merging any headers); outside a
@@ -73,6 +115,11 @@ export const response = {
73
115
  redirect(location, status) {
74
116
  return redirect(location, status);
75
117
  },
118
+ send(data, status) {
119
+ return typeof data === "object" && data !== null
120
+ ? json(data, status)
121
+ : text(String(data), status);
122
+ },
76
123
  status(code) {
77
124
  ctx().status(code);
78
125
  return response;
@@ -81,6 +128,37 @@ export const response = {
81
128
  ctx().header(name, value);
82
129
  return response;
83
130
  },
131
+ type(mime) {
132
+ ctx().header("content-type", mime);
133
+ return response;
134
+ },
135
+ append(name, value) {
136
+ ctx().header(name, value, { append: true });
137
+ return response;
138
+ },
139
+ removeHeader(name) {
140
+ ctx().res.headers.delete(name);
141
+ return response;
142
+ },
143
+ cookie(name, value, options) {
144
+ setCookie(ctx(), name, value, options);
145
+ return response;
146
+ },
147
+ clearCookie(name, options) {
148
+ deleteCookie(ctx(), name, options);
149
+ return response;
150
+ },
151
+ abort(message, status = 400) {
152
+ throw new HttpException(status, message);
153
+ },
154
+ abortIf(condition, message, status = 400) {
155
+ if (condition)
156
+ throw new HttpException(status, message);
157
+ },
158
+ abortUnless(condition, message, status = 400) {
159
+ if (!condition)
160
+ throw new HttpException(status, message);
161
+ },
84
162
  };
85
163
  /* ---------------------------- request access --------------------------- */
86
164
  /**
@@ -134,6 +212,115 @@ export const request = {
134
212
  subdomain(name) {
135
213
  return ctx().get("subdomains")?.[name];
136
214
  },
215
+ /** A request cookie by name, or all cookies when called with no argument. */
216
+ cookie(name) {
217
+ return name ? getCookie(ctx(), name) : getCookie(ctx());
218
+ },
219
+ /** The client IP, from X-Forwarded-For / X-Real-IP. */
220
+ ip() {
221
+ const c = ctx();
222
+ return (c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ??
223
+ c.req.header("x-real-ip") ??
224
+ undefined);
225
+ },
226
+ /** All inputs — query string merged with the parsed body (async). */
227
+ async all() {
228
+ const c = ctx();
229
+ const query = c.req.query();
230
+ const body = {};
231
+ const ct = c.req.header("content-type") ?? "";
232
+ try {
233
+ if (ct.includes("application/json")) {
234
+ Object.assign(body, await c.req.json());
235
+ }
236
+ else if (ct.includes("form")) {
237
+ const fd = await cachedFormData();
238
+ if (fd)
239
+ for (const [k, v] of fd.entries())
240
+ if (!(v instanceof File))
241
+ body[k] = v;
242
+ }
243
+ }
244
+ catch {
245
+ /* no or invalid body — ignore */
246
+ }
247
+ return { ...query, ...body };
248
+ },
249
+ /** An uploaded file by field name (a web `File`), or undefined. */
250
+ async file(name) {
251
+ const fd = await cachedFormData();
252
+ const value = fd?.get(name);
253
+ return value instanceof File ? value : undefined;
254
+ },
255
+ /** All uploaded files for a field name. */
256
+ async files(name) {
257
+ const fd = await cachedFormData();
258
+ return (fd?.getAll(name) ?? []).filter((v) => v instanceof File);
259
+ },
260
+ /** Every uploaded file, grouped by field name. */
261
+ async allFiles() {
262
+ const fd = await cachedFormData();
263
+ const out = {};
264
+ if (!fd)
265
+ return out;
266
+ for (const [key, value] of fd.entries()) {
267
+ if (!(value instanceof File))
268
+ continue;
269
+ const existing = out[key];
270
+ if (existing === undefined)
271
+ out[key] = value;
272
+ else if (Array.isArray(existing))
273
+ existing.push(value);
274
+ else
275
+ out[key] = [existing, value];
276
+ }
277
+ return out;
278
+ },
279
+ /** True if the request declares a body. */
280
+ hasBody() {
281
+ const c = ctx();
282
+ return !!(c.req.header("content-length") || c.req.header("transfer-encoding"));
283
+ },
284
+ /** All request headers as an object. */
285
+ headers() {
286
+ return Object.fromEntries(ctx().req.raw.headers);
287
+ },
288
+ /** The full X-Forwarded-For chain (client first). */
289
+ ips() {
290
+ const xff = ctx().req.header("x-forwarded-for");
291
+ return xff ? xff.split(",").map((s) => s.trim()) : [];
292
+ },
293
+ /** The best of the offered content types per the Accept header, or null. */
294
+ accepts(types) {
295
+ return negotiate("accept", types);
296
+ },
297
+ /** Accepted content types, ordered by preference. */
298
+ types() {
299
+ return parseAccept(ctx().req.header("accept"));
300
+ },
301
+ /** The best of the offered languages per Accept-Language, or null. */
302
+ language(languages) {
303
+ return negotiate("accept-language", languages);
304
+ },
305
+ /** Accepted languages, ordered by preference. */
306
+ languages() {
307
+ return parseAccept(ctx().req.header("accept-language"));
308
+ },
309
+ /** A single input (from query or body), with an optional fallback (async). */
310
+ async input(key, fallback) {
311
+ const all = await this.all();
312
+ return (key in all ? all[key] : fallback);
313
+ },
314
+ /** Only the named inputs (async). */
315
+ async only(keys) {
316
+ const all = await this.all();
317
+ return Object.fromEntries(keys.filter((k) => k in all).map((k) => [k, all[k]]));
318
+ },
319
+ /** Every input except the named ones (async). */
320
+ async except(keys) {
321
+ const all = await this.all();
322
+ return Object.fromEntries(Object.entries(all).filter(([k]) => !keys.includes(k)));
323
+ },
137
324
  };
138
325
  export function param(name) {
139
326
  const c = ctx();
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Sessions. A cookie-backed session store — no external service, so it works
3
+ * the same on Node and the edge. Install `sessionMiddleware()` in your HTTP
4
+ * kernel, then reach the session anywhere with `session()`.
5
+ *
6
+ * session().put("userId", user.id);
7
+ * const id = session().get("userId");
8
+ * session().flash("status", "Saved!"); // available on the next request
9
+ */
10
+ import type { MiddlewareHandler } from "hono";
11
+ import { setCookie } from "hono/cookie";
12
+ type SessionData = Record<string, unknown>;
13
+ type CookieOptions = Parameters<typeof setCookie>[3];
14
+ export declare class Session {
15
+ private data;
16
+ constructor(data: SessionData);
17
+ /** Everything in the session (excluding internal flash keys). */
18
+ all(): SessionData;
19
+ get<T = unknown>(key: string, fallback?: T): T;
20
+ put(key: string, value: unknown): this;
21
+ /** Alias for put(). */
22
+ set(key: string, value: unknown): this;
23
+ has(key: string): boolean;
24
+ forget(key: string): this;
25
+ /** Read and remove a value. */
26
+ pull<T = unknown>(key: string, fallback?: T): T;
27
+ increment(key: string, by?: number): this;
28
+ decrement(key: string, by?: number): this;
29
+ clear(): this;
30
+ /** Flash a value for the next request only. */
31
+ flash(key: string, value: unknown): this;
32
+ /** Read a value flashed on the previous request. */
33
+ flashed<T = unknown>(key: string, fallback?: T): T;
34
+ }
35
+ export interface SessionOptions {
36
+ cookieName?: string;
37
+ cookie?: CookieOptions;
38
+ }
39
+ /**
40
+ * Loads the session from its cookie before the request and writes it back
41
+ * after. Register it in your HTTP kernel with `this.use(sessionMiddleware())`.
42
+ */
43
+ export declare function sessionMiddleware(options?: SessionOptions): MiddlewareHandler;
44
+ /** The current request's session. Requires `sessionMiddleware()` installed. */
45
+ export declare function session(): Session;
46
+ export {};
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Sessions. A cookie-backed session store — no external service, so it works
3
+ * the same on Node and the edge. Install `sessionMiddleware()` in your HTTP
4
+ * kernel, then reach the session anywhere with `session()`.
5
+ *
6
+ * session().put("userId", user.id);
7
+ * const id = session().get("userId");
8
+ * session().flash("status", "Saved!"); // available on the next request
9
+ */
10
+ import { getCookie, setCookie } from "hono/cookie";
11
+ import { ctx } from "./request.js";
12
+ const FLASH = "__flash";
13
+ const OLD = "__old";
14
+ export class Session {
15
+ data;
16
+ constructor(data) {
17
+ this.data = data;
18
+ }
19
+ /** Everything in the session (excluding internal flash keys). */
20
+ all() {
21
+ const { [FLASH]: _f, [OLD]: _o, ...rest } = this.data;
22
+ return rest;
23
+ }
24
+ get(key, fallback) {
25
+ return (key in this.data ? this.data[key] : fallback);
26
+ }
27
+ put(key, value) {
28
+ this.data[key] = value;
29
+ return this;
30
+ }
31
+ /** Alias for put(). */
32
+ set(key, value) {
33
+ return this.put(key, value);
34
+ }
35
+ has(key) {
36
+ return key in this.data && this.data[key] != null;
37
+ }
38
+ forget(key) {
39
+ delete this.data[key];
40
+ return this;
41
+ }
42
+ /** Read and remove a value. */
43
+ pull(key, fallback) {
44
+ const value = this.get(key, fallback);
45
+ this.forget(key);
46
+ return value;
47
+ }
48
+ increment(key, by = 1) {
49
+ this.data[key] = (this.data[key] ?? 0) + by;
50
+ return this;
51
+ }
52
+ decrement(key, by = 1) {
53
+ return this.increment(key, -by);
54
+ }
55
+ clear() {
56
+ for (const key of Object.keys(this.data))
57
+ delete this.data[key];
58
+ return this;
59
+ }
60
+ /** Flash a value for the next request only. */
61
+ flash(key, value) {
62
+ const flash = this.data[FLASH] ?? {};
63
+ flash[key] = value;
64
+ this.data[FLASH] = flash;
65
+ return this;
66
+ }
67
+ /** Read a value flashed on the previous request. */
68
+ flashed(key, fallback) {
69
+ const old = this.data[OLD] ?? {};
70
+ return (key in old ? old[key] : fallback);
71
+ }
72
+ }
73
+ /**
74
+ * Loads the session from its cookie before the request and writes it back
75
+ * after. Register it in your HTTP kernel with `this.use(sessionMiddleware())`.
76
+ */
77
+ export function sessionMiddleware(options = {}) {
78
+ const name = options.cookieName ?? "keel_session";
79
+ return async (c, next) => {
80
+ let data = {};
81
+ const raw = getCookie(c, name);
82
+ if (raw) {
83
+ try {
84
+ data = JSON.parse(atob(raw));
85
+ }
86
+ catch {
87
+ /* tampered/expired — start fresh */
88
+ }
89
+ }
90
+ // Rotate flash: last request's flash becomes this request's "old".
91
+ data[OLD] = data[FLASH] ?? {};
92
+ delete data[FLASH];
93
+ c.set("session", data);
94
+ await next();
95
+ const toStore = { ...data };
96
+ delete toStore[OLD];
97
+ setCookie(c, name, btoa(JSON.stringify(toStore)), {
98
+ httpOnly: true,
99
+ path: "/",
100
+ sameSite: "Lax",
101
+ ...options.cookie,
102
+ });
103
+ };
104
+ }
105
+ /** The current request's session. Requires `sessionMiddleware()` installed. */
106
+ export function session() {
107
+ const data = ctx().get("session");
108
+ if (!data) {
109
+ throw new Error("Session is not available. Add sessionMiddleware() to your HTTP kernel.");
110
+ }
111
+ return new Session(data);
112
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A static file server middleware. Serves files from a directory (default
3
+ * `public/`) before your routes run — if a file matches the request path it's
4
+ * sent with caching headers; otherwise the request continues to your routes.
5
+ *
6
+ * `node:fs` is imported dynamically, so the core still loads on the edge; add
7
+ * this middleware only in Node apps (on Workers, serve assets via the platform).
8
+ *
9
+ * this.use(serveStatic()); // ./public
10
+ * this.use(serveStatic({ root: "./assets", maxAge: 86400, immutable: true }));
11
+ */
12
+ import type { MiddlewareHandler } from "hono";
13
+ export interface StaticOptions {
14
+ /** Directory to serve from. Default: "./public". */
15
+ root?: string;
16
+ /** Index file for directory requests. Default: "index.html". */
17
+ index?: string;
18
+ /** Dot-file policy: "ignore" (404 → next), "deny" (403), "allow". Default: "ignore". */
19
+ dotFiles?: "ignore" | "deny" | "allow";
20
+ /** Cache-Control max-age in seconds. Omit for no Cache-Control header. */
21
+ maxAge?: number;
22
+ /** Add the `immutable` Cache-Control directive (for hashed filenames). */
23
+ immutable?: boolean;
24
+ /** Extra headers per file. */
25
+ headers?: (path: string) => Record<string, string> | undefined;
26
+ }
27
+ export declare function serveStatic(options?: StaticOptions): MiddlewareHandler;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * A static file server middleware. Serves files from a directory (default
3
+ * `public/`) before your routes run — if a file matches the request path it's
4
+ * sent with caching headers; otherwise the request continues to your routes.
5
+ *
6
+ * `node:fs` is imported dynamically, so the core still loads on the edge; add
7
+ * this middleware only in Node apps (on Workers, serve assets via the platform).
8
+ *
9
+ * this.use(serveStatic()); // ./public
10
+ * this.use(serveStatic({ root: "./assets", maxAge: 86400, immutable: true }));
11
+ */
12
+ import { getMimeType } from "hono/utils/mime";
13
+ export function serveStatic(options = {}) {
14
+ const root = (options.root ?? "./public").replace(/\/+$/, "");
15
+ const index = options.index ?? "index.html";
16
+ const dotFiles = options.dotFiles ?? "ignore";
17
+ return async (c, next) => {
18
+ if (c.req.method !== "GET" && c.req.method !== "HEAD")
19
+ return next();
20
+ const urlPath = decodeURIComponent(new URL(c.req.url).pathname);
21
+ if (urlPath.includes(".."))
22
+ return next(); // path traversal guard
23
+ const hasDotSegment = urlPath.split("/").some((seg) => seg.startsWith("."));
24
+ if (hasDotSegment) {
25
+ if (dotFiles === "deny")
26
+ return c.text("Forbidden", 403);
27
+ if (dotFiles === "ignore")
28
+ return next();
29
+ }
30
+ try {
31
+ const { stat, readFile } = await import("node:fs/promises");
32
+ let filePath = root + urlPath;
33
+ let stats = await stat(filePath).catch(() => null);
34
+ if (stats?.isDirectory()) {
35
+ filePath = `${filePath.replace(/\/+$/, "")}/${index}`;
36
+ stats = await stat(filePath).catch(() => null);
37
+ }
38
+ if (!stats || !stats.isFile())
39
+ return next();
40
+ const etag = `W/"${stats.size}-${Math.round(stats.mtimeMs)}"`;
41
+ c.header("Content-Type", getMimeType(filePath) ?? "application/octet-stream");
42
+ c.header("Last-Modified", stats.mtime.toUTCString());
43
+ c.header("ETag", etag);
44
+ if (options.maxAge != null) {
45
+ c.header("Cache-Control", `public, max-age=${options.maxAge}${options.immutable ? ", immutable" : ""}`);
46
+ }
47
+ const extra = options.headers?.(filePath);
48
+ if (extra)
49
+ for (const [k, v] of Object.entries(extra))
50
+ c.header(k, v);
51
+ if (c.req.header("If-None-Match") === etag)
52
+ return c.body(null, 304);
53
+ if (c.req.method === "HEAD")
54
+ return c.body(null, 200);
55
+ return c.body(await readFile(filePath), 200);
56
+ }
57
+ catch {
58
+ return next();
59
+ }
60
+ };
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shaferllc/keel",
3
- "version": "0.12.0",
3
+ "version": "0.36.0",
4
4
  "type": "module",
5
5
  "description": "The house framework for Node.js — a service container, providers, routing, JSX views, and a code-generating console.",
6
6
  "license": "MIT",
@@ -37,6 +37,7 @@
37
37
  "serve": "tsx bin/keel.ts serve",
38
38
  "dev": "tsx watch bin/keel.ts serve",
39
39
  "typecheck": "tsc --noEmit",
40
+ "typecheck:docs": "npm run build && tsc -p tsconfig.docs.json",
40
41
  "test": "node --import tsx --test tests/*.test.ts",
41
42
  "test:coverage": "node --import tsx --test --experimental-test-coverage --test-coverage-exclude='tests/**' tests/*.test.ts",
42
43
  "build": "rm -rf dist && tsc -p tsconfig.build.json",