@shaferllc/keel 0.66.0 → 0.74.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/AGENTS.md +169 -0
- package/README.md +44 -6
- package/bin/keel-mcp.mjs +9 -0
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +91 -0
- package/dist/api/resource.js +188 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +8 -2
- package/dist/core/cache.d.ts +81 -5
- package/dist/core/cache.js +188 -23
- package/dist/core/cli/stubs.d.ts +26 -0
- package/dist/core/cli/stubs.js +225 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -0
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +9 -3
- package/dist/core/helpers.js +11 -3
- package/dist/core/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +39 -13
- package/dist/core/index.js +19 -6
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +89 -4
- package/dist/core/logger.js +167 -22
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +120 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +325 -14
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +299 -7
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +170 -1
- package/dist/core/testing.js +504 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +20 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +3206 -0
- package/docs/ai.md +128 -0
- package/docs/api-resources.md +118 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +385 -0
- package/docs/console.md +536 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +631 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +121 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +208 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +166 -0
- package/docs/examples/mail.ts +263 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +215 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/i18n.md +302 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +436 -0
- package/docs/mail.md +751 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +452 -0
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +314 -0
- package/docs/testing.md +376 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +19816 -0
- package/llms.txt +127 -0
- package/package.json +42 -7
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-based routing — a file *is* a route.
|
|
3
|
+
*
|
|
4
|
+
* resources/pages/index.tsx -> /
|
|
5
|
+
* resources/pages/about.tsx -> /about
|
|
6
|
+
* resources/pages/users/index.tsx -> /users
|
|
7
|
+
* resources/pages/users/[id].tsx -> /users/:id
|
|
8
|
+
* resources/pages/docs/[...slug].tsx -> /docs/* (catch-all)
|
|
9
|
+
*
|
|
10
|
+
* A page is a module whose default export is a component. Everything else is
|
|
11
|
+
* optional: a `loader` to fetch its data, `middleware` to guard it, a `name` for
|
|
12
|
+
* URL generation.
|
|
13
|
+
*
|
|
14
|
+
* // resources/pages/users/[id].tsx
|
|
15
|
+
* export const middleware = [authGuard()];
|
|
16
|
+
* export const loader = async (c: Ctx) => db("users").find(c.req.param("id"));
|
|
17
|
+
*
|
|
18
|
+
* export default function UserPage({ params, data }: PageProps<{ id: string }, User>) {
|
|
19
|
+
* return <h1>{data.name}</h1>;
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* Register them once, in a provider:
|
|
23
|
+
*
|
|
24
|
+
* await pages(); // Node — scans resources/pages
|
|
25
|
+
* definePages(import.meta.glob("./pages/**\/*.tsx", { eager: true })); // edge/Vite
|
|
26
|
+
*
|
|
27
|
+
* This doesn't replace the router — it *drives* it. Every page becomes an
|
|
28
|
+
* ordinary named route, so `url()`, route middleware, and `keel routes` all see
|
|
29
|
+
* them, and you can mix pages and hand-written routes freely.
|
|
30
|
+
*/
|
|
31
|
+
import type { MiddlewareHandler } from "hono";
|
|
32
|
+
import { Router, type Ctx, type MiddlewareRef } from "./http/router.js";
|
|
33
|
+
import { type Renderable } from "./view.js";
|
|
34
|
+
/** What a page component receives. */
|
|
35
|
+
export interface PageProps<P extends Record<string, string> = Record<string, string>, D = unknown> {
|
|
36
|
+
/** The route parameters — `[id].tsx` gives you `params.id`. */
|
|
37
|
+
params: P;
|
|
38
|
+
/** Whatever the page's `loader` returned, or `undefined` if it has none. */
|
|
39
|
+
data: D;
|
|
40
|
+
/** The request context, for the rare page that needs it. */
|
|
41
|
+
ctx: Ctx;
|
|
42
|
+
}
|
|
43
|
+
/** The shape of a page module. Only the default export is required. */
|
|
44
|
+
export interface PageModule<P extends Record<string, string> = Record<string, string>, D = unknown> {
|
|
45
|
+
/** The component. It may be async. */
|
|
46
|
+
default: (props: PageProps<P, D>) => Renderable;
|
|
47
|
+
/** Fetch this page's data before it renders. Its return value becomes `data`. */
|
|
48
|
+
loader?: (ctx: Ctx) => D | Promise<D>;
|
|
49
|
+
/** Middleware for this page alone. */
|
|
50
|
+
middleware?: MiddlewareRef | MiddlewareRef[];
|
|
51
|
+
/** The route name. Defaults to one derived from the file path. */
|
|
52
|
+
name?: string;
|
|
53
|
+
/** Override the URL entirely, ignoring the file's location. */
|
|
54
|
+
path?: string;
|
|
55
|
+
}
|
|
56
|
+
/** A page, once it's been turned into a route. */
|
|
57
|
+
export interface RegisteredPage {
|
|
58
|
+
/** The file it came from, relative to the pages directory. */
|
|
59
|
+
file: string;
|
|
60
|
+
/** The URL pattern it was registered at. */
|
|
61
|
+
pattern: string;
|
|
62
|
+
/** Its route name. */
|
|
63
|
+
name: string;
|
|
64
|
+
}
|
|
65
|
+
export interface PagesOptions {
|
|
66
|
+
/** Where the pages live. Default: `"resources/pages"`. */
|
|
67
|
+
dir?: string;
|
|
68
|
+
/** Prefix every page's URL — `"/app"` puts `index.tsx` at `/app`. */
|
|
69
|
+
prefix?: string;
|
|
70
|
+
/** Middleware applied to every page. */
|
|
71
|
+
middleware?: MiddlewareRef[];
|
|
72
|
+
/** The router to register on. Defaults to the application's. */
|
|
73
|
+
router?: Router;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Turn a file path into a URL pattern.
|
|
77
|
+
*
|
|
78
|
+
* index.tsx -> /
|
|
79
|
+
* about.tsx -> /about
|
|
80
|
+
* users/index.tsx -> /users
|
|
81
|
+
* users/[id].tsx -> /users/:id
|
|
82
|
+
* docs/[...slug].tsx -> /docs/:slug{.+}
|
|
83
|
+
*/
|
|
84
|
+
export declare function routePattern(file: string): string;
|
|
85
|
+
/** A route name derived from the file path: `users/[id].tsx` -> `users.id`. */
|
|
86
|
+
export declare function routeName(file: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* Turn a map of `file path -> page module` into routes.
|
|
89
|
+
*
|
|
90
|
+
* This is the edge-safe half: it does no filesystem work, so on Workers you hand
|
|
91
|
+
* it a build-time manifest —
|
|
92
|
+
*
|
|
93
|
+
* definePages(import.meta.glob("./pages/**\/*.tsx", { eager: true }));
|
|
94
|
+
*
|
|
95
|
+
* On Node, `pages()` builds that map for you by scanning a directory.
|
|
96
|
+
*/
|
|
97
|
+
export declare function definePages(modules: Record<string, PageModule<never, never>>, options?: PagesOptions): RegisteredPage[];
|
|
98
|
+
/**
|
|
99
|
+
* Scan a directory and register every page in it (Node only — it reads the
|
|
100
|
+
* filesystem). `node:fs` is imported dynamically, so the core still loads on the
|
|
101
|
+
* edge; there, use `definePages()` with a build-time manifest instead.
|
|
102
|
+
*
|
|
103
|
+
* await pages(); // resources/pages
|
|
104
|
+
* await pages({ dir: "app/pages", prefix: "/app" });
|
|
105
|
+
*/
|
|
106
|
+
export declare function pages(options?: PagesOptions): Promise<RegisteredPage[]>;
|
|
107
|
+
/** The middleware type a page may export, re-exported for convenience. */
|
|
108
|
+
export type PageMiddleware = MiddlewareRef | MiddlewareRef[] | MiddlewareHandler;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-based routing — a file *is* a route.
|
|
3
|
+
*
|
|
4
|
+
* resources/pages/index.tsx -> /
|
|
5
|
+
* resources/pages/about.tsx -> /about
|
|
6
|
+
* resources/pages/users/index.tsx -> /users
|
|
7
|
+
* resources/pages/users/[id].tsx -> /users/:id
|
|
8
|
+
* resources/pages/docs/[...slug].tsx -> /docs/* (catch-all)
|
|
9
|
+
*
|
|
10
|
+
* A page is a module whose default export is a component. Everything else is
|
|
11
|
+
* optional: a `loader` to fetch its data, `middleware` to guard it, a `name` for
|
|
12
|
+
* URL generation.
|
|
13
|
+
*
|
|
14
|
+
* // resources/pages/users/[id].tsx
|
|
15
|
+
* export const middleware = [authGuard()];
|
|
16
|
+
* export const loader = async (c: Ctx) => db("users").find(c.req.param("id"));
|
|
17
|
+
*
|
|
18
|
+
* export default function UserPage({ params, data }: PageProps<{ id: string }, User>) {
|
|
19
|
+
* return <h1>{data.name}</h1>;
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* Register them once, in a provider:
|
|
23
|
+
*
|
|
24
|
+
* await pages(); // Node — scans resources/pages
|
|
25
|
+
* definePages(import.meta.glob("./pages/**\/*.tsx", { eager: true })); // edge/Vite
|
|
26
|
+
*
|
|
27
|
+
* This doesn't replace the router — it *drives* it. Every page becomes an
|
|
28
|
+
* ordinary named route, so `url()`, route middleware, and `keel routes` all see
|
|
29
|
+
* them, and you can mix pages and hand-written routes freely.
|
|
30
|
+
*/
|
|
31
|
+
import { Router } from "./http/router.js";
|
|
32
|
+
import { View } from "./view.js";
|
|
33
|
+
import { app, make } from "./helpers.js";
|
|
34
|
+
/* ------------------------------ path -> route ----------------------------- */
|
|
35
|
+
const PAGE_FILE = /\.(tsx|jsx|ts|js)$/;
|
|
36
|
+
/** A `[...slug]` segment — the catch-all. */
|
|
37
|
+
const CATCH_ALL = /^\[\.\.\.(.+)\]$/;
|
|
38
|
+
/** A `[id]` segment — a route parameter. */
|
|
39
|
+
const PARAM = /^\[(.+)\]$/;
|
|
40
|
+
/**
|
|
41
|
+
* Turn a file path into a URL pattern.
|
|
42
|
+
*
|
|
43
|
+
* index.tsx -> /
|
|
44
|
+
* about.tsx -> /about
|
|
45
|
+
* users/index.tsx -> /users
|
|
46
|
+
* users/[id].tsx -> /users/:id
|
|
47
|
+
* docs/[...slug].tsx -> /docs/:slug{.+}
|
|
48
|
+
*/
|
|
49
|
+
export function routePattern(file) {
|
|
50
|
+
const segments = file
|
|
51
|
+
.replace(PAGE_FILE, "")
|
|
52
|
+
.split("/")
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
// A trailing `index` names its directory, not a child of it.
|
|
55
|
+
if (segments[segments.length - 1] === "index")
|
|
56
|
+
segments.pop();
|
|
57
|
+
const path = segments
|
|
58
|
+
.map((segment) => {
|
|
59
|
+
const catchAll = CATCH_ALL.exec(segment);
|
|
60
|
+
// Hono's `{.+}` makes the param greedy, so it swallows the slashes too.
|
|
61
|
+
if (catchAll)
|
|
62
|
+
return `:${catchAll[1]}{.+}`;
|
|
63
|
+
const param = PARAM.exec(segment);
|
|
64
|
+
if (param)
|
|
65
|
+
return `:${param[1]}`;
|
|
66
|
+
return segment;
|
|
67
|
+
})
|
|
68
|
+
.join("/");
|
|
69
|
+
return `/${path}`.replace(/\/+$/, "") || "/";
|
|
70
|
+
}
|
|
71
|
+
/** A route name derived from the file path: `users/[id].tsx` -> `users.id`. */
|
|
72
|
+
export function routeName(file) {
|
|
73
|
+
const segments = file
|
|
74
|
+
.replace(PAGE_FILE, "")
|
|
75
|
+
.split("/")
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.map((segment) => segment.replace(CATCH_ALL, "$1").replace(PARAM, "$1"));
|
|
78
|
+
return segments.join(".") || "index";
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* How specific a pattern is: static beats dynamic beats catch-all.
|
|
82
|
+
*
|
|
83
|
+
* This ordering is the whole game. Register `/users/:id` before `/users/new` and
|
|
84
|
+
* the literal page is unreachable — `:id` matches "new" and wins. So pages are
|
|
85
|
+
* sorted before they're registered, most specific first, and the file layout
|
|
86
|
+
* stops being a trap.
|
|
87
|
+
*/
|
|
88
|
+
function specificity(pattern) {
|
|
89
|
+
return pattern
|
|
90
|
+
.split("/")
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.map((segment) => (segment.includes("{.+}") ? 2 : segment.startsWith(":") ? 1 : 0));
|
|
93
|
+
}
|
|
94
|
+
/** Sort patterns so the most specific route is matched first. */
|
|
95
|
+
function byPrecedence(a, b) {
|
|
96
|
+
const sa = specificity(a);
|
|
97
|
+
const sb = specificity(b);
|
|
98
|
+
// A catch-all anywhere in the path sinks it to the bottom.
|
|
99
|
+
const maxA = Math.max(0, ...sa);
|
|
100
|
+
const maxB = Math.max(0, ...sb);
|
|
101
|
+
if (maxA !== maxB)
|
|
102
|
+
return maxA - maxB;
|
|
103
|
+
// Then compare segment by segment: a literal beats a param at the same depth.
|
|
104
|
+
for (let i = 0; i < Math.max(sa.length, sb.length); i++) {
|
|
105
|
+
const va = sa[i] ?? -1;
|
|
106
|
+
const vb = sb[i] ?? -1;
|
|
107
|
+
if (va !== vb)
|
|
108
|
+
return va - vb;
|
|
109
|
+
}
|
|
110
|
+
// Same shape — longer paths (more segments) are more specific.
|
|
111
|
+
return sb.length - sa.length;
|
|
112
|
+
}
|
|
113
|
+
/* ------------------------------- registering ------------------------------ */
|
|
114
|
+
function list(mw) {
|
|
115
|
+
if (!mw)
|
|
116
|
+
return [];
|
|
117
|
+
return Array.isArray(mw) ? mw : [mw];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Turn a map of `file path -> page module` into routes.
|
|
121
|
+
*
|
|
122
|
+
* This is the edge-safe half: it does no filesystem work, so on Workers you hand
|
|
123
|
+
* it a build-time manifest —
|
|
124
|
+
*
|
|
125
|
+
* definePages(import.meta.glob("./pages/**\/*.tsx", { eager: true }));
|
|
126
|
+
*
|
|
127
|
+
* On Node, `pages()` builds that map for you by scanning a directory.
|
|
128
|
+
*/
|
|
129
|
+
export function definePages(modules, options = {}) {
|
|
130
|
+
const router = options.router ?? app().make(Router);
|
|
131
|
+
const prefix = (options.prefix ?? "").replace(/\/+$/, "");
|
|
132
|
+
// Normalize the keys a glob gives us ("./pages/users/[id].tsx") down to the
|
|
133
|
+
// part that matters ("users/[id].tsx"), so both halves agree on the file name.
|
|
134
|
+
const dir = (options.dir ?? "resources/pages").replace(/^\.?\//, "").replace(/\/+$/, "");
|
|
135
|
+
const relative = (key) => key
|
|
136
|
+
.replace(/^\.?\//, "")
|
|
137
|
+
.replace(new RegExp(`^${dir}/`), "")
|
|
138
|
+
.replace(/^pages\//, "");
|
|
139
|
+
const entries = Object.entries(modules).map(([key, module]) => {
|
|
140
|
+
const file = relative(key);
|
|
141
|
+
// A prefix plus the root page would otherwise give "/app/".
|
|
142
|
+
const pattern = (prefix + (module.path ?? routePattern(file))).replace(/\/+$/, "") || "/";
|
|
143
|
+
return { file, module, pattern };
|
|
144
|
+
});
|
|
145
|
+
// Most specific first — see `specificity()` for why this matters.
|
|
146
|
+
entries.sort((a, b) => byPrecedence(a.pattern, b.pattern));
|
|
147
|
+
return entries.map(({ file, module, pattern }) => {
|
|
148
|
+
const name = module.name ?? routeName(file);
|
|
149
|
+
const route = router.get(pattern || "/", async (c) => {
|
|
150
|
+
const data = module.loader ? await module.loader(c) : undefined;
|
|
151
|
+
const html = await make(View).render(module.default({
|
|
152
|
+
params: c.req.param(),
|
|
153
|
+
data: data,
|
|
154
|
+
ctx: c,
|
|
155
|
+
}));
|
|
156
|
+
return c.html(html);
|
|
157
|
+
});
|
|
158
|
+
route.name(name);
|
|
159
|
+
const middleware = [...(options.middleware ?? []), ...list(module.middleware)];
|
|
160
|
+
if (middleware.length)
|
|
161
|
+
route.middleware(middleware);
|
|
162
|
+
return { file, pattern: pattern || "/", name };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Scan a directory and register every page in it (Node only — it reads the
|
|
167
|
+
* filesystem). `node:fs` is imported dynamically, so the core still loads on the
|
|
168
|
+
* edge; there, use `definePages()` with a build-time manifest instead.
|
|
169
|
+
*
|
|
170
|
+
* await pages(); // resources/pages
|
|
171
|
+
* await pages({ dir: "app/pages", prefix: "/app" });
|
|
172
|
+
*/
|
|
173
|
+
export async function pages(options = {}) {
|
|
174
|
+
const { readdir } = await import("node:fs/promises");
|
|
175
|
+
const { join, relative: relativePath } = await import("node:path");
|
|
176
|
+
const { pathToFileURL } = await import("node:url");
|
|
177
|
+
const base = options.dir ?? "resources/pages";
|
|
178
|
+
const root = join(app().basePath, base);
|
|
179
|
+
const files = [];
|
|
180
|
+
async function walk(dir) {
|
|
181
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
182
|
+
for (const entry of entries) {
|
|
183
|
+
const full = join(dir, entry.name);
|
|
184
|
+
if (entry.isDirectory())
|
|
185
|
+
await walk(full);
|
|
186
|
+
// A leading underscore marks a file as private — a layout, a partial, a
|
|
187
|
+
// helper — so it can live beside the pages without becoming a URL.
|
|
188
|
+
else if (PAGE_FILE.test(entry.name) && !entry.name.startsWith("_"))
|
|
189
|
+
files.push(full);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
await walk(root);
|
|
193
|
+
const modules = {};
|
|
194
|
+
for (const file of files) {
|
|
195
|
+
const key = relativePath(root, file).split(/[\\/]/).join("/");
|
|
196
|
+
modules[key] = (await import(pathToFileURL(file).href));
|
|
197
|
+
}
|
|
198
|
+
return definePages(modules, { ...options, dir: base });
|
|
199
|
+
}
|
package/dist/core/queue.d.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* so it stays edge-safe.
|
|
8
8
|
*
|
|
9
9
|
* class SendWelcome extends Job {
|
|
10
|
+
* static maxRetries = 3;
|
|
10
11
|
* constructor(private userId: number) { super(); }
|
|
11
12
|
* async handle() { await mail().to(...).send(); }
|
|
13
|
+
* async failed(error: unknown) { logger().error("welcome failed", { error }); }
|
|
12
14
|
* }
|
|
13
15
|
*
|
|
14
16
|
* await dispatch(new SendWelcome(user.id)); // runs now with the sync driver
|
|
@@ -17,18 +19,91 @@
|
|
|
17
19
|
* setQueue(new MemoryDriver());
|
|
18
20
|
* await dispatch(new SendWelcome(user.id)); // queued
|
|
19
21
|
* await work(); // runs everything pending
|
|
22
|
+
*
|
|
23
|
+
* A job that throws is **retried** with backoff up to `maxRetries`, and only then
|
|
24
|
+
* declared failed — at which point `failed()` runs and it lands in the driver's
|
|
25
|
+
* dead-letter list rather than vanishing.
|
|
26
|
+
*/
|
|
27
|
+
/** What a job knows about its own execution while `handle()` runs. */
|
|
28
|
+
export interface JobContext {
|
|
29
|
+
/** This dispatch's id — stable across retries. */
|
|
30
|
+
jobId: string;
|
|
31
|
+
/** 1 on the first run, 2 on the first retry, and so on. */
|
|
32
|
+
attempt: number;
|
|
33
|
+
/** The lane it was placed on. */
|
|
34
|
+
queue: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A unit of background work. Subclass and implement `handle`.
|
|
38
|
+
*
|
|
39
|
+
* Retry policy is per-class, via statics:
|
|
40
|
+
*
|
|
41
|
+
* class ChargeCard extends Job {
|
|
42
|
+
* static maxRetries = 5;
|
|
43
|
+
* static backoff = exponentialBackoff(1_000);
|
|
44
|
+
* async handle() { … }
|
|
45
|
+
* }
|
|
20
46
|
*/
|
|
21
|
-
/** A unit of background work. Subclass and implement `handle`. */
|
|
22
47
|
export declare abstract class Job {
|
|
48
|
+
/** How many times to retry after the first failure. Default: 0 (no retries). */
|
|
49
|
+
static maxRetries: number;
|
|
50
|
+
/** How long to wait before each retry. Default: exponential from 1s. */
|
|
51
|
+
static backoff: Backoff;
|
|
52
|
+
/** Default lane for this job class. */
|
|
53
|
+
static queue?: string;
|
|
54
|
+
/** Default priority for this job class (lower runs first). */
|
|
55
|
+
static priority?: number;
|
|
56
|
+
/** Set by the driver before `handle()` runs. */
|
|
57
|
+
context?: JobContext;
|
|
23
58
|
abstract handle(): void | Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Called once the job has exhausted its retries — the last chance to record
|
|
61
|
+
* the failure, alert, or compensate. A throw in here is logged and swallowed:
|
|
62
|
+
* failing to handle a failure must not itself crash the worker.
|
|
63
|
+
*/
|
|
64
|
+
failed(_error: unknown): void | Promise<void>;
|
|
24
65
|
}
|
|
25
66
|
/** Anything dispatchable: a `Job` instance or a plain function. */
|
|
26
67
|
export type Dispatchable = Job | (() => void | Promise<void>);
|
|
68
|
+
/** Milliseconds to wait before attempt `attempt` (1 = the first retry). */
|
|
69
|
+
export type Backoff = (attempt: number) => number;
|
|
70
|
+
/** Doubles each time: 1s, 2s, 4s, 8s… Capped at `maxMs`. */
|
|
71
|
+
export declare function exponentialBackoff(baseMs?: number, maxMs?: number): Backoff;
|
|
72
|
+
/** Adds a fixed step each time: 5s, 10s, 15s… Capped at `maxMs`. */
|
|
73
|
+
export declare function linearBackoff(stepMs?: number, maxMs?: number): Backoff;
|
|
74
|
+
/** The same delay every time. */
|
|
75
|
+
export declare function fixedBackoff(delayMs?: number): Backoff;
|
|
76
|
+
/** No delay at all — retry straight away. */
|
|
77
|
+
export declare const noBackoff: Backoff;
|
|
27
78
|
export interface JobOptions {
|
|
28
79
|
/** Seconds to wait before the job becomes available (drivers may honor it). */
|
|
29
80
|
delay?: number;
|
|
30
|
-
/** Named queue/lane to place the job on. */
|
|
81
|
+
/** Named queue/lane to place the job on. Default: `"default"`. */
|
|
31
82
|
queue?: string;
|
|
83
|
+
/** Lower runs first. Default: 0. */
|
|
84
|
+
priority?: number;
|
|
85
|
+
/** Override the job class's `maxRetries`. */
|
|
86
|
+
maxRetries?: number;
|
|
87
|
+
/** Override the job class's `backoff`. */
|
|
88
|
+
backoff?: Backoff;
|
|
89
|
+
}
|
|
90
|
+
/** A job sitting on a queue, with everything needed to run and retry it. */
|
|
91
|
+
export interface QueuedJob {
|
|
92
|
+
id: string;
|
|
93
|
+
job: Dispatchable;
|
|
94
|
+
options: JobOptions;
|
|
95
|
+
/** How many times it has been attempted so far. */
|
|
96
|
+
attempts: number;
|
|
97
|
+
/** Epoch ms before which it must not run (delay / backoff). */
|
|
98
|
+
availableAt: number;
|
|
99
|
+
}
|
|
100
|
+
/** A job that exhausted its retries. */
|
|
101
|
+
export interface FailedJob {
|
|
102
|
+
id: string;
|
|
103
|
+
job: Dispatchable;
|
|
104
|
+
options: JobOptions;
|
|
105
|
+
attempts: number;
|
|
106
|
+
error: unknown;
|
|
32
107
|
}
|
|
33
108
|
/** The bridge to your queue backend. */
|
|
34
109
|
export interface QueueDriver {
|
|
@@ -39,20 +114,36 @@ export interface Drainable {
|
|
|
39
114
|
readonly size: number;
|
|
40
115
|
work(): Promise<number>;
|
|
41
116
|
}
|
|
42
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Runs jobs the moment they're dispatched — the default; ideal for dev/tests.
|
|
119
|
+
*
|
|
120
|
+
* Retries still apply, but the backoff delay is **not** slept through: an
|
|
121
|
+
* inline driver blocking the request for a 30-second backoff would be worse than
|
|
122
|
+
* useless. Use `MemoryDriver` or a real broker to exercise real delays.
|
|
123
|
+
*/
|
|
43
124
|
export declare class SyncDriver implements QueueDriver {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
job: Dispatchable;
|
|
48
|
-
options: JobOptions;
|
|
125
|
+
/** Jobs that exhausted their retries. */
|
|
126
|
+
readonly failed: FailedJob[];
|
|
127
|
+
push(job: Dispatchable, options: JobOptions): Promise<void>;
|
|
49
128
|
}
|
|
50
129
|
/** Holds jobs in memory; `work()` drains them. Assert on `.jobs` in tests. */
|
|
51
130
|
export declare class MemoryDriver implements QueueDriver, Drainable {
|
|
52
131
|
readonly jobs: QueuedJob[];
|
|
132
|
+
/** Jobs that exhausted their retries. */
|
|
133
|
+
readonly failed: FailedJob[];
|
|
53
134
|
push(job: Dispatchable, options: JobOptions): Promise<void>;
|
|
54
135
|
get size(): number;
|
|
55
|
-
/**
|
|
136
|
+
/** The next job that's due, by priority then insertion order. */
|
|
137
|
+
private takeNext;
|
|
138
|
+
/**
|
|
139
|
+
* Run every job that's currently due, highest priority first; returns how many
|
|
140
|
+
* ran. A job that throws is re-queued with its backoff delay until it runs out
|
|
141
|
+
* of retries, then moves to `failed`.
|
|
142
|
+
*
|
|
143
|
+
* Jobs whose delay or backoff hasn't elapsed are left in place — `work()`
|
|
144
|
+
* drains what's *due*, it doesn't sleep. Call it again later, or advance your
|
|
145
|
+
* test's clock.
|
|
146
|
+
*/
|
|
56
147
|
work(): Promise<number>;
|
|
57
148
|
}
|
|
58
149
|
export declare class Queue {
|
|
@@ -62,7 +153,29 @@ export declare class Queue {
|
|
|
62
153
|
dispatch(job: Dispatchable, options?: JobOptions): Promise<void>;
|
|
63
154
|
/** Drain the driver if it holds jobs locally; returns how many ran. */
|
|
64
155
|
work(): Promise<number>;
|
|
156
|
+
/** Jobs that exhausted their retries, if the driver tracks them. */
|
|
157
|
+
get failed(): FailedJob[];
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* A driver that records dispatches without running anything, plus assertions —
|
|
161
|
+
* what `fakeQueue()` installs so a test can check a job was *queued* without
|
|
162
|
+
* paying for it to run.
|
|
163
|
+
*/
|
|
164
|
+
export declare class FakeQueue extends Queue {
|
|
165
|
+
readonly pushed: QueuedJob[];
|
|
166
|
+
constructor();
|
|
167
|
+
/** Dispatches of a job class (or all of them, with no argument). */
|
|
168
|
+
private matching;
|
|
169
|
+
assertPushed<T extends Job>(type: JobClass<T>, where?: (job: T) => boolean): void;
|
|
170
|
+
assertNotPushed<T extends Job>(type: JobClass<T>, where?: (job: T) => boolean): void;
|
|
171
|
+
assertPushedCount(expected: number, type?: JobClass): void;
|
|
172
|
+
assertNothingPushed(): void;
|
|
173
|
+
/** The queued entry for a job class — to assert on its delay, lane, or priority. */
|
|
174
|
+
pushedJobs<T extends Job>(type: JobClass<T>): QueuedJob[];
|
|
175
|
+
private summary;
|
|
65
176
|
}
|
|
177
|
+
/** A `Job` subclass, as passed to the fake's assertions. */
|
|
178
|
+
export type JobClass<T extends Job = Job> = abstract new (...args: never[]) => T;
|
|
66
179
|
/** Register the default queue driver used by `dispatch()`. */
|
|
67
180
|
export declare function setQueue(driver: QueueDriver): Queue;
|
|
68
181
|
/** The default queue instance. */
|
|
@@ -71,3 +184,15 @@ export declare function getQueue(): Queue;
|
|
|
71
184
|
export declare function dispatch(job: Dispatchable, options?: JobOptions): Promise<void>;
|
|
72
185
|
/** Drain the default queue's pending jobs (no-op for immediate drivers). */
|
|
73
186
|
export declare function work(): Promise<number>;
|
|
187
|
+
/**
|
|
188
|
+
* Swap the queue for one that records dispatches without running them, so a test
|
|
189
|
+
* can assert a job was queued without paying for it to run. Undo with
|
|
190
|
+
* `restoreQueue()`.
|
|
191
|
+
*
|
|
192
|
+
* const q = fakeQueue();
|
|
193
|
+
* await register(user);
|
|
194
|
+
* q.assertPushed(SendWelcome, (job) => job.userId === user.id);
|
|
195
|
+
*/
|
|
196
|
+
export declare function fakeQueue(): FakeQueue;
|
|
197
|
+
/** Restore the real queue after `fakeQueue()`. */
|
|
198
|
+
export declare function restoreQueue(): void;
|