@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,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router facade. Collects route definitions declaratively; the HTTP kernel
|
|
3
|
+
* compiles them onto the underlying Hono instance at boot.
|
|
4
|
+
*
|
|
5
|
+
* Fluent, AdonisJS-inspired API: named routes, per-route and group middleware,
|
|
6
|
+
* prefixes, param constraints, resource routes, and URL generation.
|
|
7
|
+
*/
|
|
8
|
+
import { view } from "../helpers.js";
|
|
9
|
+
import { redirect as makeRedirect } from "../request.js";
|
|
10
|
+
import { inertia } from "../inertia.js";
|
|
11
|
+
function matcherSource(m) {
|
|
12
|
+
if (typeof m === "string")
|
|
13
|
+
return m;
|
|
14
|
+
if (m instanceof RegExp)
|
|
15
|
+
return m.source;
|
|
16
|
+
return m.match.source;
|
|
17
|
+
}
|
|
18
|
+
/** Built-in parameter matchers, à la `router.matchers.number()`. */
|
|
19
|
+
export const matchers = {
|
|
20
|
+
number: () => /\d+/,
|
|
21
|
+
uuid: () => /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/,
|
|
22
|
+
slug: () => /[a-z0-9]+(?:-[a-z0-9]+)*/,
|
|
23
|
+
alpha: () => /[a-zA-Z]+/,
|
|
24
|
+
};
|
|
25
|
+
const ALL = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
|
|
26
|
+
/** A single registered route — chain to name it, guard it, or constrain params. */
|
|
27
|
+
export class Route {
|
|
28
|
+
def;
|
|
29
|
+
constructor(def) {
|
|
30
|
+
this.def = def;
|
|
31
|
+
}
|
|
32
|
+
/** Give the route a name for URL generation. */
|
|
33
|
+
name(name) {
|
|
34
|
+
this.def.name = name;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
/** Alias for name(). */
|
|
38
|
+
as(name) {
|
|
39
|
+
return this.name(name);
|
|
40
|
+
}
|
|
41
|
+
/** Attach middleware that runs only for this route (after group middleware). */
|
|
42
|
+
middleware(mw) {
|
|
43
|
+
this.def.middleware.push(...(Array.isArray(mw) ? mw : [mw]));
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
/** Alias for middleware(), matching AdonisJS. */
|
|
47
|
+
use(mw) {
|
|
48
|
+
return this.middleware(mw);
|
|
49
|
+
}
|
|
50
|
+
/** Constrain a route parameter with a regex, source string, or matcher. */
|
|
51
|
+
where(param, matcher) {
|
|
52
|
+
this.def.wheres[param] = matcherSource(matcher);
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
/** Bind this route to a host pattern (supports `:subdomain` segments). */
|
|
56
|
+
domain(pattern) {
|
|
57
|
+
this.def.domain = pattern;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** A group of routes sharing a prefix, middleware, and/or name prefix. */
|
|
62
|
+
export class RouteGroup {
|
|
63
|
+
routes;
|
|
64
|
+
constructor(routes) {
|
|
65
|
+
this.routes = routes;
|
|
66
|
+
}
|
|
67
|
+
prefix(prefix) {
|
|
68
|
+
const p = "/" + prefix.replace(/^\/|\/$/g, "");
|
|
69
|
+
for (const r of this.routes)
|
|
70
|
+
r.path = (p + r.path).replace(/\/$/, "") || "/";
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
middleware(mw) {
|
|
74
|
+
const list = Array.isArray(mw) ? mw : [mw];
|
|
75
|
+
for (const r of this.routes)
|
|
76
|
+
r.middleware.unshift(...list); // group runs first
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
/** Alias for middleware(), matching AdonisJS. */
|
|
80
|
+
use(mw) {
|
|
81
|
+
return this.middleware(mw);
|
|
82
|
+
}
|
|
83
|
+
/** Constrain a parameter across every route in the group. */
|
|
84
|
+
where(param, matcher) {
|
|
85
|
+
for (const r of this.routes) {
|
|
86
|
+
if (!(param in r.wheres))
|
|
87
|
+
r.wheres[param] = matcherSource(matcher);
|
|
88
|
+
}
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
as(namePrefix) {
|
|
92
|
+
for (const r of this.routes)
|
|
93
|
+
if (r.name)
|
|
94
|
+
r.name = `${namePrefix}.${r.name}`;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
/** Bind every route in the group to a host pattern. */
|
|
98
|
+
domain(pattern) {
|
|
99
|
+
for (const r of this.routes)
|
|
100
|
+
r.domain = pattern;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function singular(s) {
|
|
105
|
+
if (s.endsWith("ies"))
|
|
106
|
+
return s.slice(0, -3) + "y";
|
|
107
|
+
if (s.endsWith("s"))
|
|
108
|
+
return s.slice(0, -1);
|
|
109
|
+
return s;
|
|
110
|
+
}
|
|
111
|
+
/** RESTful resource routes; chain to trim, rename, or guard actions. */
|
|
112
|
+
export class RouteResource {
|
|
113
|
+
byAction;
|
|
114
|
+
resourceName;
|
|
115
|
+
child;
|
|
116
|
+
constructor(byAction, resourceName, child) {
|
|
117
|
+
this.byAction = byAction;
|
|
118
|
+
this.resourceName = resourceName;
|
|
119
|
+
this.child = child;
|
|
120
|
+
}
|
|
121
|
+
only(actions) {
|
|
122
|
+
for (const [a, def] of this.byAction)
|
|
123
|
+
if (!actions.includes(a))
|
|
124
|
+
def.methods = [];
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
except(actions) {
|
|
128
|
+
for (const a of actions) {
|
|
129
|
+
const def = this.byAction.get(a);
|
|
130
|
+
if (def)
|
|
131
|
+
def.methods = [];
|
|
132
|
+
}
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
/** Drop the HTML-form actions (create, edit). */
|
|
136
|
+
apiOnly() {
|
|
137
|
+
return this.except(["create", "edit"]);
|
|
138
|
+
}
|
|
139
|
+
/** Rename the route-name prefix, e.g. `.as("articles")` → `articles.index`. */
|
|
140
|
+
as(name) {
|
|
141
|
+
for (const [action, def] of this.byAction)
|
|
142
|
+
def.name = `${name}.${action}`;
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
/** Rename a route parameter, e.g. `.params({ posts: "post" })`. */
|
|
146
|
+
params(map) {
|
|
147
|
+
for (const [segment, newParam] of Object.entries(map)) {
|
|
148
|
+
const oldParam = segment === this.child || segment === this.resourceName
|
|
149
|
+
? "id"
|
|
150
|
+
: `${singular(segment)}_id`;
|
|
151
|
+
for (const def of this.byAction.values()) {
|
|
152
|
+
def.path = def.path.replace(`:${oldParam}`, `:${newParam}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
/** Attach middleware to specific actions (or "*" for all). */
|
|
158
|
+
use(actions, mw) {
|
|
159
|
+
const list = Array.isArray(mw) ? mw : [mw];
|
|
160
|
+
for (const [action, def] of this.byAction) {
|
|
161
|
+
if (actions === "*" || actions.includes(action))
|
|
162
|
+
def.middleware.push(...list);
|
|
163
|
+
}
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Fluent matcher for `on(path)` convenience routes. */
|
|
168
|
+
class RouteMatcher {
|
|
169
|
+
router;
|
|
170
|
+
path;
|
|
171
|
+
constructor(router, path) {
|
|
172
|
+
this.router = router;
|
|
173
|
+
this.path = path;
|
|
174
|
+
}
|
|
175
|
+
/** Redirect to a path or URL. */
|
|
176
|
+
redirect(to, status = 302) {
|
|
177
|
+
return this.router.get(this.path, () => makeRedirect(to, status));
|
|
178
|
+
}
|
|
179
|
+
/** Alias for redirect(), matching AdonisJS. */
|
|
180
|
+
redirectToPath(to, status = 302) {
|
|
181
|
+
return this.redirect(to, status);
|
|
182
|
+
}
|
|
183
|
+
/** Redirect to a named route, optionally with params and a query string. */
|
|
184
|
+
redirectToRoute(name, params = {}, options = {}) {
|
|
185
|
+
return this.router.get(this.path, () => {
|
|
186
|
+
let url = this.router.url(name, params);
|
|
187
|
+
if (options.qs) {
|
|
188
|
+
const qs = new URLSearchParams(Object.fromEntries(Object.entries(options.qs).map(([k, v]) => [k, String(v)])));
|
|
189
|
+
url += `?${qs}`;
|
|
190
|
+
}
|
|
191
|
+
return makeRedirect(url, options.status ?? 302);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** Render a view component directly. */
|
|
195
|
+
render(component, props) {
|
|
196
|
+
return this.router.get(this.path, () => view(component, props));
|
|
197
|
+
}
|
|
198
|
+
/** Render an Inertia page component directly. */
|
|
199
|
+
renderInertia(component, props) {
|
|
200
|
+
return this.router.get(this.path, () => inertia(component, props));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export class Router {
|
|
204
|
+
container;
|
|
205
|
+
routes = [];
|
|
206
|
+
group_prefix = "";
|
|
207
|
+
group_mw = [];
|
|
208
|
+
globalWheres = {};
|
|
209
|
+
/** Built-in parameter matchers: `router.matchers.number()`. */
|
|
210
|
+
matchers = matchers;
|
|
211
|
+
constructor(container) {
|
|
212
|
+
this.container = container;
|
|
213
|
+
}
|
|
214
|
+
get(path, handler) {
|
|
215
|
+
return this.add(["GET"], path, handler);
|
|
216
|
+
}
|
|
217
|
+
post(path, handler) {
|
|
218
|
+
return this.add(["POST"], path, handler);
|
|
219
|
+
}
|
|
220
|
+
put(path, handler) {
|
|
221
|
+
return this.add(["PUT"], path, handler);
|
|
222
|
+
}
|
|
223
|
+
patch(path, handler) {
|
|
224
|
+
return this.add(["PATCH"], path, handler);
|
|
225
|
+
}
|
|
226
|
+
delete(path, handler) {
|
|
227
|
+
return this.add(["DELETE"], path, handler);
|
|
228
|
+
}
|
|
229
|
+
/** Match any HTTP verb. */
|
|
230
|
+
any(path, handler) {
|
|
231
|
+
return this.add(ALL, path, handler);
|
|
232
|
+
}
|
|
233
|
+
/** Match a specific set of verbs. */
|
|
234
|
+
route(methods, path, handler) {
|
|
235
|
+
return this.add(methods, path, handler);
|
|
236
|
+
}
|
|
237
|
+
/** A fluent matcher: `router.on("/").redirect("/home")`. */
|
|
238
|
+
on(path) {
|
|
239
|
+
return new RouteMatcher(this, path);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Group routes under a shared prefix / middleware / name prefix:
|
|
243
|
+
* router.group(() => { … }).prefix("/api").middleware([auth]).as("api");
|
|
244
|
+
*/
|
|
245
|
+
group(callback) {
|
|
246
|
+
const start = this.routes.length;
|
|
247
|
+
callback();
|
|
248
|
+
return new RouteGroup(this.routes.slice(start));
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* RESTful resource routes for a controller:
|
|
252
|
+
* index, create, store, show, edit, update, destroy.
|
|
253
|
+
*/
|
|
254
|
+
resource(name, controller) {
|
|
255
|
+
// Dotted names nest resources: "posts.comments" -> /posts/:post_id/comments.
|
|
256
|
+
const segments = name.split(".");
|
|
257
|
+
const child = segments.pop().replace(/^\/|\/$/g, "");
|
|
258
|
+
let prefix = "";
|
|
259
|
+
for (const seg of segments) {
|
|
260
|
+
const s = seg.replace(/^\/|\/$/g, "");
|
|
261
|
+
prefix += `/${s}/:${singular(s)}_id`;
|
|
262
|
+
}
|
|
263
|
+
const p = `${prefix}/${child}`;
|
|
264
|
+
const id = `${p}/:id`;
|
|
265
|
+
const defs = new Map();
|
|
266
|
+
const reg = (action, methods, path) => {
|
|
267
|
+
const route = this.add(methods, path, [controller, action]);
|
|
268
|
+
route.name(`${name}.${action}`);
|
|
269
|
+
defs.set(action, route.def);
|
|
270
|
+
};
|
|
271
|
+
reg("index", ["GET"], p);
|
|
272
|
+
reg("create", ["GET"], `${p}/create`);
|
|
273
|
+
reg("store", ["POST"], p);
|
|
274
|
+
reg("show", ["GET"], id);
|
|
275
|
+
reg("edit", ["GET"], `${id}/edit`);
|
|
276
|
+
reg("update", ["PUT", "PATCH"], id);
|
|
277
|
+
reg("destroy", ["DELETE"], id);
|
|
278
|
+
return new RouteResource(defs, name, child);
|
|
279
|
+
}
|
|
280
|
+
add(methods, path, handler) {
|
|
281
|
+
const full = (this.group_prefix + "/" + path.replace(/^\//, "")).replace(/\/$/, "") || "/";
|
|
282
|
+
const def = {
|
|
283
|
+
methods,
|
|
284
|
+
path: full,
|
|
285
|
+
handler,
|
|
286
|
+
middleware: [...this.group_mw],
|
|
287
|
+
wheres: {},
|
|
288
|
+
};
|
|
289
|
+
this.routes.push(def);
|
|
290
|
+
return new Route(def);
|
|
291
|
+
}
|
|
292
|
+
/** Register a global parameter constraint, applied to every matching route. */
|
|
293
|
+
where(param, matcher) {
|
|
294
|
+
this.globalWheres[param] = matcherSource(matcher);
|
|
295
|
+
return this;
|
|
296
|
+
}
|
|
297
|
+
/** All registered routes (excluding those trimmed to zero methods). */
|
|
298
|
+
all() {
|
|
299
|
+
for (const r of this.routes) {
|
|
300
|
+
for (const [param, src] of Object.entries(this.globalWheres)) {
|
|
301
|
+
if (!(param in r.wheres) && r.path.includes(`:${param}`)) {
|
|
302
|
+
r.wheres[param] = src;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return this.routes.filter((r) => r.methods.length > 0);
|
|
307
|
+
}
|
|
308
|
+
/** Generate a URL for a named route, substituting `:params`. */
|
|
309
|
+
url(name, params = {}) {
|
|
310
|
+
const def = this.routes.find((r) => r.name === name);
|
|
311
|
+
if (!def)
|
|
312
|
+
throw new Error(`No route named [${name}].`);
|
|
313
|
+
let path = def.path;
|
|
314
|
+
for (const [k, v] of Object.entries(params)) {
|
|
315
|
+
path = path.replace(new RegExp(`:${k}\\??`), encodeURIComponent(String(v)));
|
|
316
|
+
}
|
|
317
|
+
return path.replace(/\/:[^/]+\?/g, "").replace(/:[^/]+/g, "");
|
|
318
|
+
}
|
|
319
|
+
/** Turn a route handler into an executable function, resolving controllers. */
|
|
320
|
+
resolve(handler) {
|
|
321
|
+
if (handler instanceof Response) {
|
|
322
|
+
const res = handler;
|
|
323
|
+
return () => res.clone();
|
|
324
|
+
}
|
|
325
|
+
if (Array.isArray(handler)) {
|
|
326
|
+
const [ref, method = "handle"] = handler;
|
|
327
|
+
const isLazy = !ref.prototype; // arrow = lazy loader
|
|
328
|
+
return async (c) => {
|
|
329
|
+
let ctor = ref;
|
|
330
|
+
if (isLazy) {
|
|
331
|
+
const mod = await ref();
|
|
332
|
+
ctor = (mod.default ?? mod);
|
|
333
|
+
}
|
|
334
|
+
const controller = this.container.make(ctor);
|
|
335
|
+
const action = controller[method];
|
|
336
|
+
if (typeof action !== "function") {
|
|
337
|
+
throw new Error(`Controller [${ctor.name}] has no method [${method}].`);
|
|
338
|
+
}
|
|
339
|
+
return action.call(controller, c);
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return handler;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Public framework surface. Userland imports everything from "@keel/core". */
|
|
2
|
+
export { Container } from "./container.js";
|
|
3
|
+
export type { Token, Constructor, Factory } from "./container.js";
|
|
4
|
+
export { Application } from "./application.js";
|
|
5
|
+
export type { BootOptions } from "./application.js";
|
|
6
|
+
export { Config, env } from "./config.js";
|
|
7
|
+
export { app, config, view, bind, singleton, instance, make, bound, } from "./helpers.js";
|
|
8
|
+
export { ctx, json, text, html, redirect, param, query, header, body, request, response, } from "./request.js";
|
|
9
|
+
export type { ConfigData } from "./config.js";
|
|
10
|
+
export { View } from "./view.js";
|
|
11
|
+
export type { Renderable, ViewConfig } from "./view.js";
|
|
12
|
+
export { ServiceProvider } from "./provider.js";
|
|
13
|
+
export type { ProviderClass } from "./provider.js";
|
|
14
|
+
export { Router, Route, RouteGroup, RouteResource, matchers } from "./http/router.js";
|
|
15
|
+
export type { Ctx, RouteHandler, RouteDefinition, Method, Matcher } from "./http/router.js";
|
|
16
|
+
export { Inertia, inertia, inertiaPageAttr } from "./inertia.js";
|
|
17
|
+
export type { InertiaPage, InertiaOptions } from "./inertia.js";
|
|
18
|
+
export { HttpKernel } from "./http/kernel.js";
|
|
19
|
+
export { HttpException, NotFoundException, UnauthorizedException, ForbiddenException, ValidationException, STATUS_TEXT, } from "./exceptions.js";
|
|
20
|
+
export { validate } from "./validation.js";
|
|
21
|
+
export type { Schema } from "./validation.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Public framework surface. Userland imports everything from "@keel/core". */
|
|
2
|
+
export { Container } from "./container.js";
|
|
3
|
+
export { Application } from "./application.js";
|
|
4
|
+
export { Config, env } from "./config.js";
|
|
5
|
+
export { app, config, view, bind, singleton, instance, make, bound, } from "./helpers.js";
|
|
6
|
+
export { ctx, json, text, html, redirect, param, query, header, body, request, response, } from "./request.js";
|
|
7
|
+
export { View } from "./view.js";
|
|
8
|
+
export { ServiceProvider } from "./provider.js";
|
|
9
|
+
export { Router, Route, RouteGroup, RouteResource, matchers } from "./http/router.js";
|
|
10
|
+
export { Inertia, inertia, inertiaPageAttr } from "./inertia.js";
|
|
11
|
+
export { HttpKernel } from "./http/kernel.js";
|
|
12
|
+
export { HttpException, NotFoundException, UnauthorizedException, ForbiddenException, ValidationException, STATUS_TEXT, } from "./exceptions.js";
|
|
13
|
+
export { validate } from "./validation.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inertia.js server adapter. Pair Keel's server-side routing with an Inertia
|
|
3
|
+
* client (React/Vue/Svelte) — `inertia("Page", props)` returns the right thing
|
|
4
|
+
* automatically: a full HTML document on the first visit, or the Inertia JSON
|
|
5
|
+
* page object on subsequent XHR navigations.
|
|
6
|
+
*
|
|
7
|
+
* Implements the Inertia protocol: the X-Inertia header, asset versioning
|
|
8
|
+
* (409 + X-Inertia-Location on mismatch), and partial reloads.
|
|
9
|
+
*/
|
|
10
|
+
export interface InertiaPage {
|
|
11
|
+
component: string;
|
|
12
|
+
props: Record<string, unknown>;
|
|
13
|
+
url: string;
|
|
14
|
+
version: string;
|
|
15
|
+
}
|
|
16
|
+
export interface InertiaOptions {
|
|
17
|
+
/** Asset version; a mismatch forces the client to hard-reload (409). */
|
|
18
|
+
version?: string;
|
|
19
|
+
/** Renders the HTML document shell for a first (non-XHR) load. */
|
|
20
|
+
rootView: (page: InertiaPage) => string;
|
|
21
|
+
}
|
|
22
|
+
export declare class Inertia {
|
|
23
|
+
private version;
|
|
24
|
+
private rootView;
|
|
25
|
+
constructor(options: InertiaOptions);
|
|
26
|
+
render(component: string, props?: Record<string, unknown>): Response | string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Render an Inertia response for the current request. Requires an `Inertia`
|
|
30
|
+
* instance bound in the container (configure it in a service provider).
|
|
31
|
+
*/
|
|
32
|
+
export declare function inertia(component: string, props?: Record<string, unknown>): Response | string;
|
|
33
|
+
/** HTML-escape a value for the `data-page` attribute of the root element. */
|
|
34
|
+
export declare function inertiaPageAttr(page: InertiaPage): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inertia.js server adapter. Pair Keel's server-side routing with an Inertia
|
|
3
|
+
* client (React/Vue/Svelte) — `inertia("Page", props)` returns the right thing
|
|
4
|
+
* automatically: a full HTML document on the first visit, or the Inertia JSON
|
|
5
|
+
* page object on subsequent XHR navigations.
|
|
6
|
+
*
|
|
7
|
+
* Implements the Inertia protocol: the X-Inertia header, asset versioning
|
|
8
|
+
* (409 + X-Inertia-Location on mismatch), and partial reloads.
|
|
9
|
+
*/
|
|
10
|
+
import { ctx } from "./request.js";
|
|
11
|
+
import { bound, make } from "./helpers.js";
|
|
12
|
+
export class Inertia {
|
|
13
|
+
version;
|
|
14
|
+
rootView;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.version = options.version ?? "1";
|
|
17
|
+
this.rootView = options.rootView;
|
|
18
|
+
}
|
|
19
|
+
render(component, props = {}) {
|
|
20
|
+
const c = ctx();
|
|
21
|
+
const requestUrl = new URL(c.req.url);
|
|
22
|
+
const url = requestUrl.pathname + requestUrl.search;
|
|
23
|
+
const isInertia = c.req.header("X-Inertia") === "true";
|
|
24
|
+
// Asset version changed → tell the client to do a full reload.
|
|
25
|
+
if (isInertia &&
|
|
26
|
+
c.req.method === "GET" &&
|
|
27
|
+
(c.req.header("X-Inertia-Version") ?? "") !== this.version) {
|
|
28
|
+
return new Response(null, { status: 409, headers: { "X-Inertia-Location": url } });
|
|
29
|
+
}
|
|
30
|
+
// Partial reload: send only the requested props for the matching component.
|
|
31
|
+
let finalProps = props;
|
|
32
|
+
const partialComponent = c.req.header("X-Inertia-Partial-Component");
|
|
33
|
+
const partialData = c.req.header("X-Inertia-Partial-Data");
|
|
34
|
+
if (isInertia && partialComponent === component && partialData) {
|
|
35
|
+
const only = new Set(partialData.split(",").map((s) => s.trim()));
|
|
36
|
+
finalProps = Object.fromEntries(Object.entries(props).filter(([k]) => only.has(k)));
|
|
37
|
+
}
|
|
38
|
+
const page = { component, props: finalProps, url, version: this.version };
|
|
39
|
+
if (isInertia) {
|
|
40
|
+
return c.json(page, 200, {
|
|
41
|
+
"X-Inertia": "true",
|
|
42
|
+
Vary: "X-Inertia",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// First load — the full HTML document with the page data embedded.
|
|
46
|
+
return this.rootView(page);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Render an Inertia response for the current request. Requires an `Inertia`
|
|
51
|
+
* instance bound in the container (configure it in a service provider).
|
|
52
|
+
*/
|
|
53
|
+
export function inertia(component, props = {}) {
|
|
54
|
+
if (!bound(Inertia)) {
|
|
55
|
+
throw new Error("Inertia is not configured. Bind it in a provider: " +
|
|
56
|
+
"singleton(Inertia, () => new Inertia({ version, rootView })).");
|
|
57
|
+
}
|
|
58
|
+
return make(Inertia).render(component, props);
|
|
59
|
+
}
|
|
60
|
+
/** HTML-escape a value for the `data-page` attribute of the root element. */
|
|
61
|
+
export function inertiaPageAttr(page) {
|
|
62
|
+
return JSON.stringify(page)
|
|
63
|
+
.replace(/&/g, "&")
|
|
64
|
+
.replace(/"/g, """)
|
|
65
|
+
.replace(/'/g, "'")
|
|
66
|
+
.replace(/</g, "<")
|
|
67
|
+
.replace(/>/g, ">");
|
|
68
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service providers are the central place to configure the application.
|
|
3
|
+
*
|
|
4
|
+
* register(): bind things into the container. Do NOT resolve other services
|
|
5
|
+
* here — nothing is guaranteed to be registered yet.
|
|
6
|
+
* boot(): called after every provider has registered. Safe to resolve
|
|
7
|
+
* and wire things together here.
|
|
8
|
+
*/
|
|
9
|
+
import type { Application } from "./application.js";
|
|
10
|
+
export declare abstract class ServiceProvider {
|
|
11
|
+
protected app: Application;
|
|
12
|
+
constructor(app: Application);
|
|
13
|
+
register(): void | Promise<void>;
|
|
14
|
+
boot(): void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export type ProviderClass = new (app: Application) => ServiceProvider;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service providers are the central place to configure the application.
|
|
3
|
+
*
|
|
4
|
+
* register(): bind things into the container. Do NOT resolve other services
|
|
5
|
+
* here — nothing is guaranteed to be registered yet.
|
|
6
|
+
* boot(): called after every provider has registered. Safe to resolve
|
|
7
|
+
* and wire things together here.
|
|
8
|
+
*/
|
|
9
|
+
export class ServiceProvider {
|
|
10
|
+
app;
|
|
11
|
+
constructor(app) {
|
|
12
|
+
this.app = app;
|
|
13
|
+
}
|
|
14
|
+
register() { }
|
|
15
|
+
boot() { }
|
|
16
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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 type { Context } from "hono";
|
|
15
|
+
/** The current request context. Throws if called outside a request. */
|
|
16
|
+
export declare function ctx(): Context;
|
|
17
|
+
export declare function json(data: unknown, status?: number): Response;
|
|
18
|
+
export declare function text(body: string, status?: number): Response;
|
|
19
|
+
export declare function html(body: string, status?: number): Response;
|
|
20
|
+
export declare function redirect(location: string, status?: number): Response;
|
|
21
|
+
/**
|
|
22
|
+
* The response, as a flat accessor mirroring `request`:
|
|
23
|
+
*
|
|
24
|
+
* response.json({ ok: true });
|
|
25
|
+
* response.text("hello"); response.html("<h1>Hi</h1>");
|
|
26
|
+
* response.redirect("/login");
|
|
27
|
+
* response.status(201).json(created); // chainable
|
|
28
|
+
*/
|
|
29
|
+
interface ResponseHelper {
|
|
30
|
+
json(data: unknown, status?: number): Response;
|
|
31
|
+
text(body: string, status?: number): Response;
|
|
32
|
+
html(body: string, status?: number): Response;
|
|
33
|
+
redirect(location: string, status?: number): Response;
|
|
34
|
+
/** Set the response status (chainable). */
|
|
35
|
+
status(code: number): ResponseHelper;
|
|
36
|
+
/** Set a response header (chainable). */
|
|
37
|
+
header(name: string, value: string): ResponseHelper;
|
|
38
|
+
}
|
|
39
|
+
export declare const response: ResponseHelper;
|
|
40
|
+
/**
|
|
41
|
+
* The current request/response, as a flat accessor:
|
|
42
|
+
*
|
|
43
|
+
* request.method request.path request.url request.status
|
|
44
|
+
* request.header("authorization") request.param("id")
|
|
45
|
+
* await request.json() request.raw
|
|
46
|
+
*/
|
|
47
|
+
export declare const request: {
|
|
48
|
+
readonly method: string;
|
|
49
|
+
readonly path: string;
|
|
50
|
+
readonly url: string;
|
|
51
|
+
/** The response status (useful after `await next()` in middleware). */
|
|
52
|
+
readonly status: number;
|
|
53
|
+
header(name: string): string | undefined;
|
|
54
|
+
param(name?: string): string | Record<string, string>;
|
|
55
|
+
query(name?: string): string | undefined | Record<string, string>;
|
|
56
|
+
json<T = unknown>(): Promise<T>;
|
|
57
|
+
/** The raw web Request. */
|
|
58
|
+
readonly raw: Request;
|
|
59
|
+
/** The matched route: `{ name, pattern, methods }`. */
|
|
60
|
+
readonly route: {
|
|
61
|
+
name?: string;
|
|
62
|
+
pattern: string;
|
|
63
|
+
methods: import(".").Method[];
|
|
64
|
+
} | undefined;
|
|
65
|
+
/** Whether the matched route has the given name. */
|
|
66
|
+
routeIs(name: string): boolean;
|
|
67
|
+
/** A subdomain parameter captured from a domain-bound route. */
|
|
68
|
+
subdomain(name: string): string | undefined;
|
|
69
|
+
};
|
|
70
|
+
export declare function param(): Record<string, string>;
|
|
71
|
+
export declare function param(name: string): string;
|
|
72
|
+
export declare function query(): Record<string, string>;
|
|
73
|
+
export declare function query(name: string): string | undefined;
|
|
74
|
+
export declare function header(name: string): string | undefined;
|
|
75
|
+
export declare function body<T = unknown>(): Promise<T>;
|
|
76
|
+
export {};
|