@gravito/cosmos 1.0.0-alpha.2 → 1.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.zh-TW.md +46 -0
- package/dist/index.js +65 -24
- package/dist/src/index.js +49 -1
- package/ion/src/index.js +2249 -0
- package/package.json +2 -2
- package/signal/src/index.js +101809 -0
- package/src/I18nService.ts +175 -34
- package/src/index.ts +7 -14
package/ion/src/index.js
ADDED
|
@@ -0,0 +1,2249 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
// ../core/package.json
|
|
4
|
+
var package_default = {
|
|
5
|
+
name: "gravito-core",
|
|
6
|
+
version: "1.0.0-beta.2",
|
|
7
|
+
description: "",
|
|
8
|
+
module: "./dist/index.mjs",
|
|
9
|
+
main: "./dist/index.cjs",
|
|
10
|
+
type: "module",
|
|
11
|
+
types: "./dist/index.d.ts",
|
|
12
|
+
exports: {
|
|
13
|
+
".": {
|
|
14
|
+
types: "./dist/index.d.ts",
|
|
15
|
+
import: "./dist/index.mjs",
|
|
16
|
+
require: "./dist/index.cjs"
|
|
17
|
+
},
|
|
18
|
+
"./compat": {
|
|
19
|
+
types: "./dist/compat.d.ts",
|
|
20
|
+
import: "./dist/compat.mjs",
|
|
21
|
+
require: "./dist/compat.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
files: [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
scripts: {
|
|
30
|
+
build: "bun run build.ts",
|
|
31
|
+
test: "bun test",
|
|
32
|
+
"test:coverage": "bun test --coverage",
|
|
33
|
+
"test:ci": "bun test --coverage --coverage-threshold=100",
|
|
34
|
+
lint: "biome lint ./src ./tests",
|
|
35
|
+
"lint:fix": "biome lint --write ./src ./tests",
|
|
36
|
+
format: "biome format --write ./src ./tests",
|
|
37
|
+
"format:check": "biome format ./src ./tests",
|
|
38
|
+
check: "biome check ./src ./tests",
|
|
39
|
+
"check:fix": "biome check --write ./src ./tests",
|
|
40
|
+
typecheck: "tsc --noEmit",
|
|
41
|
+
prepublishOnly: "bun run typecheck && bun run test && bun run build"
|
|
42
|
+
},
|
|
43
|
+
keywords: [],
|
|
44
|
+
author: "Carl Lee <carllee0520@gmail.com>",
|
|
45
|
+
license: "MIT",
|
|
46
|
+
repository: {
|
|
47
|
+
type: "git",
|
|
48
|
+
url: "git+https://github.com/gravito-framework/gravito.git",
|
|
49
|
+
directory: "packages/core"
|
|
50
|
+
},
|
|
51
|
+
bugs: {
|
|
52
|
+
url: "https://github.com/gravito-framework/gravito/issues"
|
|
53
|
+
},
|
|
54
|
+
homepage: "https://github.com/gravito-framework/gravito#readme",
|
|
55
|
+
engines: {
|
|
56
|
+
node: ">=18.0.0"
|
|
57
|
+
},
|
|
58
|
+
devDependencies: {
|
|
59
|
+
"bun-types": "latest",
|
|
60
|
+
typescript: "^5.9.3"
|
|
61
|
+
},
|
|
62
|
+
publishConfig: {
|
|
63
|
+
access: "public",
|
|
64
|
+
registry: "https://registry.npmjs.org/"
|
|
65
|
+
},
|
|
66
|
+
dependencies: {
|
|
67
|
+
hono: "^4.11.1"
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ../core/src/adapters/HonoAdapter.ts
|
|
72
|
+
class HonoRequestWrapper {
|
|
73
|
+
honoCtx;
|
|
74
|
+
constructor(honoCtx) {
|
|
75
|
+
this.honoCtx = honoCtx;
|
|
76
|
+
}
|
|
77
|
+
get url() {
|
|
78
|
+
return this.honoCtx.req.url;
|
|
79
|
+
}
|
|
80
|
+
get method() {
|
|
81
|
+
return this.honoCtx.req.method;
|
|
82
|
+
}
|
|
83
|
+
get path() {
|
|
84
|
+
return this.honoCtx.req.path;
|
|
85
|
+
}
|
|
86
|
+
param(name) {
|
|
87
|
+
return this.honoCtx.req.param(name);
|
|
88
|
+
}
|
|
89
|
+
params() {
|
|
90
|
+
return this.honoCtx.req.param();
|
|
91
|
+
}
|
|
92
|
+
query(name) {
|
|
93
|
+
return this.honoCtx.req.query(name);
|
|
94
|
+
}
|
|
95
|
+
queries() {
|
|
96
|
+
return this.honoCtx.req.queries();
|
|
97
|
+
}
|
|
98
|
+
header(name) {
|
|
99
|
+
if (name) {
|
|
100
|
+
return this.honoCtx.req.header(name);
|
|
101
|
+
}
|
|
102
|
+
return this.honoCtx.req.header();
|
|
103
|
+
}
|
|
104
|
+
async json() {
|
|
105
|
+
return this.honoCtx.req.json();
|
|
106
|
+
}
|
|
107
|
+
async text() {
|
|
108
|
+
return this.honoCtx.req.text();
|
|
109
|
+
}
|
|
110
|
+
async formData() {
|
|
111
|
+
return this.honoCtx.req.formData();
|
|
112
|
+
}
|
|
113
|
+
async arrayBuffer() {
|
|
114
|
+
return this.honoCtx.req.arrayBuffer();
|
|
115
|
+
}
|
|
116
|
+
get raw() {
|
|
117
|
+
return this.honoCtx.req.raw;
|
|
118
|
+
}
|
|
119
|
+
valid(target) {
|
|
120
|
+
return this.honoCtx.req.valid(target);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
class HonoContextWrapper {
|
|
125
|
+
honoCtx;
|
|
126
|
+
_req;
|
|
127
|
+
constructor(honoCtx) {
|
|
128
|
+
this.honoCtx = honoCtx;
|
|
129
|
+
this._req = new HonoRequestWrapper(honoCtx);
|
|
130
|
+
}
|
|
131
|
+
static create(honoCtx) {
|
|
132
|
+
const instance = new HonoContextWrapper(honoCtx);
|
|
133
|
+
return new Proxy(instance, {
|
|
134
|
+
get(target, prop, receiver) {
|
|
135
|
+
if (prop in target) {
|
|
136
|
+
const value = Reflect.get(target, prop, receiver);
|
|
137
|
+
if (typeof value === "function") {
|
|
138
|
+
return value.bind(target);
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
if (typeof prop === "string") {
|
|
143
|
+
return target.get(prop);
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
get req() {
|
|
150
|
+
return this._req;
|
|
151
|
+
}
|
|
152
|
+
json(data, status) {
|
|
153
|
+
if (status !== undefined) {
|
|
154
|
+
return this.honoCtx.json(data, status);
|
|
155
|
+
}
|
|
156
|
+
return this.honoCtx.json(data);
|
|
157
|
+
}
|
|
158
|
+
text(text, status) {
|
|
159
|
+
if (status !== undefined) {
|
|
160
|
+
return this.honoCtx.text(text, status);
|
|
161
|
+
}
|
|
162
|
+
return this.honoCtx.text(text);
|
|
163
|
+
}
|
|
164
|
+
html(html, status) {
|
|
165
|
+
if (status !== undefined) {
|
|
166
|
+
return this.honoCtx.html(html, status);
|
|
167
|
+
}
|
|
168
|
+
return this.honoCtx.html(html);
|
|
169
|
+
}
|
|
170
|
+
redirect(url, status = 302) {
|
|
171
|
+
return this.honoCtx.redirect(url, status);
|
|
172
|
+
}
|
|
173
|
+
body(data, status) {
|
|
174
|
+
if (data === null) {
|
|
175
|
+
return this.honoCtx.body(null, status);
|
|
176
|
+
}
|
|
177
|
+
return new Response(data, {
|
|
178
|
+
status: status ?? 200,
|
|
179
|
+
headers: new Headers
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
stream(stream, status) {
|
|
183
|
+
return new Response(stream, {
|
|
184
|
+
status: status ?? 200,
|
|
185
|
+
headers: {
|
|
186
|
+
"Content-Type": "application/octet-stream"
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
header(name, value, options) {
|
|
191
|
+
if (value !== undefined) {
|
|
192
|
+
if (options?.append) {
|
|
193
|
+
this.honoCtx.header(name, value, { append: true });
|
|
194
|
+
} else {
|
|
195
|
+
this.honoCtx.header(name, value);
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
return this.honoCtx.req.header(name);
|
|
200
|
+
}
|
|
201
|
+
status(code) {
|
|
202
|
+
this.honoCtx.status(code);
|
|
203
|
+
}
|
|
204
|
+
get(key) {
|
|
205
|
+
return this.honoCtx.get(key);
|
|
206
|
+
}
|
|
207
|
+
set(key, value) {
|
|
208
|
+
this.honoCtx.set(key, value);
|
|
209
|
+
}
|
|
210
|
+
get executionCtx() {
|
|
211
|
+
return this.honoCtx.executionCtx;
|
|
212
|
+
}
|
|
213
|
+
get env() {
|
|
214
|
+
return this.honoCtx.env;
|
|
215
|
+
}
|
|
216
|
+
get native() {
|
|
217
|
+
return this.honoCtx;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function toHonoMiddleware(middleware) {
|
|
221
|
+
return async (c, next) => {
|
|
222
|
+
const ctx = HonoContextWrapper.create(c);
|
|
223
|
+
const gravitoNext = async () => {
|
|
224
|
+
await next();
|
|
225
|
+
};
|
|
226
|
+
return middleware(ctx, gravitoNext);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function toHonoErrorHandler(handler) {
|
|
230
|
+
return async (err, c) => {
|
|
231
|
+
const ctx = HonoContextWrapper.create(c);
|
|
232
|
+
return handler(err, ctx);
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
class HonoAdapter {
|
|
237
|
+
config;
|
|
238
|
+
name = "hono";
|
|
239
|
+
version = "1.0.0";
|
|
240
|
+
app;
|
|
241
|
+
constructor(config = {}, honoInstance) {
|
|
242
|
+
this.config = config;
|
|
243
|
+
if (honoInstance) {
|
|
244
|
+
this.app = honoInstance;
|
|
245
|
+
} else {
|
|
246
|
+
const { Hono: HonoClass } = __require("hono");
|
|
247
|
+
this.app = new HonoClass;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
get native() {
|
|
251
|
+
return this.app;
|
|
252
|
+
}
|
|
253
|
+
setNative(app) {
|
|
254
|
+
this.app = app;
|
|
255
|
+
}
|
|
256
|
+
route(method, path, ...handlers) {
|
|
257
|
+
const fullPath = (this.config.basePath || "") + path;
|
|
258
|
+
const honoHandlers = handlers.map((h) => toHonoMiddleware(h));
|
|
259
|
+
const methodFn = this.app[method];
|
|
260
|
+
if (typeof methodFn !== "function") {
|
|
261
|
+
throw new Error(`Unsupported HTTP method: ${method}`);
|
|
262
|
+
}
|
|
263
|
+
methodFn.call(this.app, fullPath, ...honoHandlers);
|
|
264
|
+
}
|
|
265
|
+
routes(routes) {
|
|
266
|
+
for (const routeDef of routes) {
|
|
267
|
+
const middlewareHandlers = (routeDef.middleware || []).map((m) => m);
|
|
268
|
+
const allHandlers = [
|
|
269
|
+
...middlewareHandlers,
|
|
270
|
+
...routeDef.handlers
|
|
271
|
+
];
|
|
272
|
+
this.route(routeDef.method, routeDef.path, ...allHandlers);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
use(path, ...middleware) {
|
|
276
|
+
const fullPath = (this.config.basePath || "") + path;
|
|
277
|
+
const honoMiddleware = middleware.map((m) => toHonoMiddleware(m));
|
|
278
|
+
for (const m of honoMiddleware) {
|
|
279
|
+
this.app.use(fullPath, m);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
useGlobal(...middleware) {
|
|
283
|
+
this.use("*", ...middleware);
|
|
284
|
+
}
|
|
285
|
+
mount(path, subAdapter) {
|
|
286
|
+
if (subAdapter.name === "hono") {
|
|
287
|
+
this.app.route(path, subAdapter.native);
|
|
288
|
+
} else {
|
|
289
|
+
this.use(`${path}/*`, async (ctx) => {
|
|
290
|
+
const response = await subAdapter.fetch(ctx.req.raw);
|
|
291
|
+
return response;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
onError(handler) {
|
|
296
|
+
this.app.onError(toHonoErrorHandler(handler));
|
|
297
|
+
}
|
|
298
|
+
onNotFound(handler) {
|
|
299
|
+
this.app.notFound(async (c) => {
|
|
300
|
+
const ctx = HonoContextWrapper.create(c);
|
|
301
|
+
return handler(ctx);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
fetch = (request, server) => {
|
|
305
|
+
return this.app.fetch(request, server);
|
|
306
|
+
};
|
|
307
|
+
createContext(_request) {
|
|
308
|
+
throw new Error("HonoAdapter.createContext() should not be called directly. " + "Use the router pipeline instead.");
|
|
309
|
+
}
|
|
310
|
+
async init() {}
|
|
311
|
+
async shutdown() {}
|
|
312
|
+
}
|
|
313
|
+
// ../core/src/ConfigManager.ts
|
|
314
|
+
class ConfigManager {
|
|
315
|
+
config = new Map;
|
|
316
|
+
constructor(initialConfig = {}) {
|
|
317
|
+
for (const [key, value] of Object.entries(initialConfig)) {
|
|
318
|
+
this.config.set(key, value);
|
|
319
|
+
}
|
|
320
|
+
this.loadEnv();
|
|
321
|
+
}
|
|
322
|
+
loadEnv() {
|
|
323
|
+
const env = Bun.env;
|
|
324
|
+
for (const key of Object.keys(env)) {
|
|
325
|
+
if (env[key] !== undefined) {
|
|
326
|
+
this.config.set(key, env[key]);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
get(key, defaultValue) {
|
|
331
|
+
if (this.config.has(key)) {
|
|
332
|
+
return this.config.get(key);
|
|
333
|
+
}
|
|
334
|
+
if (defaultValue !== undefined) {
|
|
335
|
+
return defaultValue;
|
|
336
|
+
}
|
|
337
|
+
throw new Error(`Config key '${key}' not found`);
|
|
338
|
+
}
|
|
339
|
+
set(key, value) {
|
|
340
|
+
this.config.set(key, value);
|
|
341
|
+
}
|
|
342
|
+
has(key) {
|
|
343
|
+
return this.config.has(key);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// ../core/src/Container.ts
|
|
347
|
+
class Container {
|
|
348
|
+
bindings = new Map;
|
|
349
|
+
instances = new Map;
|
|
350
|
+
bind(key, factory) {
|
|
351
|
+
this.bindings.set(key, { factory, shared: false });
|
|
352
|
+
}
|
|
353
|
+
singleton(key, factory) {
|
|
354
|
+
this.bindings.set(key, { factory, shared: true });
|
|
355
|
+
}
|
|
356
|
+
instance(key, instance) {
|
|
357
|
+
this.instances.set(key, instance);
|
|
358
|
+
}
|
|
359
|
+
make(key) {
|
|
360
|
+
if (this.instances.has(key)) {
|
|
361
|
+
return this.instances.get(key);
|
|
362
|
+
}
|
|
363
|
+
const binding = this.bindings.get(key);
|
|
364
|
+
if (!binding) {
|
|
365
|
+
throw new Error(`Service '${key}' not found in container`);
|
|
366
|
+
}
|
|
367
|
+
const instance = binding.factory(this);
|
|
368
|
+
if (binding.shared) {
|
|
369
|
+
this.instances.set(key, instance);
|
|
370
|
+
}
|
|
371
|
+
return instance;
|
|
372
|
+
}
|
|
373
|
+
has(key) {
|
|
374
|
+
return this.bindings.has(key) || this.instances.has(key);
|
|
375
|
+
}
|
|
376
|
+
flush() {
|
|
377
|
+
this.bindings.clear();
|
|
378
|
+
this.instances.clear();
|
|
379
|
+
}
|
|
380
|
+
forget(key) {
|
|
381
|
+
this.instances.delete(key);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// ../core/src/types/events.ts
|
|
385
|
+
class Event {
|
|
386
|
+
shouldBroadcast() {
|
|
387
|
+
return "broadcastOn" in this && typeof this.broadcastOn === "function";
|
|
388
|
+
}
|
|
389
|
+
getBroadcastChannel() {
|
|
390
|
+
if (this.shouldBroadcast()) {
|
|
391
|
+
return this.broadcastOn();
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
getBroadcastData() {
|
|
396
|
+
if (this.shouldBroadcast()) {
|
|
397
|
+
const broadcast = this;
|
|
398
|
+
if (broadcast.broadcastWith) {
|
|
399
|
+
return broadcast.broadcastWith();
|
|
400
|
+
}
|
|
401
|
+
const data = {};
|
|
402
|
+
for (const [key, value] of Object.entries(this)) {
|
|
403
|
+
if (!key.startsWith("_") && typeof value !== "function") {
|
|
404
|
+
data[key] = value;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return data;
|
|
408
|
+
}
|
|
409
|
+
return {};
|
|
410
|
+
}
|
|
411
|
+
getBroadcastEventName() {
|
|
412
|
+
if (this.shouldBroadcast()) {
|
|
413
|
+
const broadcast = this;
|
|
414
|
+
if (broadcast.broadcastAs) {
|
|
415
|
+
return broadcast.broadcastAs();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return this.constructor.name;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// ../core/src/EventManager.ts
|
|
422
|
+
class EventManager {
|
|
423
|
+
core;
|
|
424
|
+
listeners = new Map;
|
|
425
|
+
broadcastManager;
|
|
426
|
+
queueManager;
|
|
427
|
+
constructor(core) {
|
|
428
|
+
this.core = core;
|
|
429
|
+
}
|
|
430
|
+
setBroadcastManager(manager) {
|
|
431
|
+
this.broadcastManager = manager;
|
|
432
|
+
}
|
|
433
|
+
setQueueManager(manager) {
|
|
434
|
+
this.queueManager = manager;
|
|
435
|
+
}
|
|
436
|
+
listen(event, listener, options) {
|
|
437
|
+
const eventKey = typeof event === "string" ? event : event;
|
|
438
|
+
if (!this.listeners.has(eventKey)) {
|
|
439
|
+
this.listeners.set(eventKey, []);
|
|
440
|
+
}
|
|
441
|
+
const registration = {
|
|
442
|
+
listener,
|
|
443
|
+
...options
|
|
444
|
+
};
|
|
445
|
+
this.listeners.get(eventKey)?.push(registration);
|
|
446
|
+
}
|
|
447
|
+
unlisten(event, listener) {
|
|
448
|
+
const eventKey = typeof event === "string" ? event : event;
|
|
449
|
+
const registrations = this.listeners.get(eventKey);
|
|
450
|
+
if (!registrations) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const filtered = registrations.filter((reg) => reg.listener !== listener);
|
|
454
|
+
if (filtered.length === 0) {
|
|
455
|
+
this.listeners.delete(eventKey);
|
|
456
|
+
} else {
|
|
457
|
+
this.listeners.set(eventKey, filtered);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async dispatch(event) {
|
|
461
|
+
const eventKey = event.constructor;
|
|
462
|
+
const eventName = event.constructor.name;
|
|
463
|
+
await this.core.hooks.doAction(`event:${eventName}`, event);
|
|
464
|
+
await this.core.hooks.doAction("event:dispatched", { event, eventName });
|
|
465
|
+
if (event instanceof Event && event.shouldBroadcast() && this.broadcastManager) {
|
|
466
|
+
const channel = event.getBroadcastChannel();
|
|
467
|
+
if (channel) {
|
|
468
|
+
const channelName = typeof channel === "string" ? channel : channel.name;
|
|
469
|
+
const channelType = typeof channel === "string" ? "public" : channel.type;
|
|
470
|
+
const data = event.getBroadcastData();
|
|
471
|
+
const broadcastEventName = event.getBroadcastEventName();
|
|
472
|
+
await this.broadcastManager.broadcast(event, { name: channelName, type: channelType }, data, broadcastEventName).catch((error) => {
|
|
473
|
+
this.core.logger.error(`[EventManager] Failed to broadcast event ${eventName}:`, error);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const registrations = this.listeners.get(eventKey) || [];
|
|
478
|
+
const stringRegistrations = this.listeners.get(eventName) || [];
|
|
479
|
+
const allRegistrations = [...registrations, ...stringRegistrations];
|
|
480
|
+
for (const registration of allRegistrations) {
|
|
481
|
+
try {
|
|
482
|
+
let listenerInstance;
|
|
483
|
+
if (typeof registration.listener === "function") {
|
|
484
|
+
listenerInstance = new registration.listener;
|
|
485
|
+
} else {
|
|
486
|
+
listenerInstance = registration.listener;
|
|
487
|
+
}
|
|
488
|
+
const shouldQueue = "queue" in listenerInstance || registration.queue !== undefined || registration.connection !== undefined || registration.delay !== undefined;
|
|
489
|
+
if (shouldQueue && this.queueManager) {
|
|
490
|
+
const queue = listenerInstance.queue || registration.queue;
|
|
491
|
+
const connection = listenerInstance.connection || registration.connection;
|
|
492
|
+
const delay = listenerInstance.delay || registration.delay;
|
|
493
|
+
const queueJob = {
|
|
494
|
+
type: "event-listener",
|
|
495
|
+
event: eventName,
|
|
496
|
+
listener: listenerInstance.constructor.name,
|
|
497
|
+
eventData: this.serializeEvent(event),
|
|
498
|
+
handle: async () => {
|
|
499
|
+
await listenerInstance.handle(event);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
await this.queueManager.push(queueJob, queue, connection, delay);
|
|
503
|
+
} else {
|
|
504
|
+
await listenerInstance.handle(event);
|
|
505
|
+
}
|
|
506
|
+
} catch (error) {
|
|
507
|
+
this.core.logger.error(`[EventManager] Error in listener for event ${eventName}:`, error);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
serializeEvent(event) {
|
|
512
|
+
const data = {};
|
|
513
|
+
for (const [key, value] of Object.entries(event)) {
|
|
514
|
+
if (!key.startsWith("_") && typeof value !== "function") {
|
|
515
|
+
data[key] = value;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return data;
|
|
519
|
+
}
|
|
520
|
+
getListeners(event) {
|
|
521
|
+
if (event) {
|
|
522
|
+
const eventKey = typeof event === "string" ? event : event;
|
|
523
|
+
return this.listeners.get(eventKey) || [];
|
|
524
|
+
}
|
|
525
|
+
const all = [];
|
|
526
|
+
for (const registrations of this.listeners.values()) {
|
|
527
|
+
all.push(...registrations);
|
|
528
|
+
}
|
|
529
|
+
return all;
|
|
530
|
+
}
|
|
531
|
+
clear() {
|
|
532
|
+
this.listeners.clear();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// ../core/src/exceptions/GravitoException.ts
|
|
536
|
+
class GravitoException extends Error {
|
|
537
|
+
status;
|
|
538
|
+
code;
|
|
539
|
+
i18nKey;
|
|
540
|
+
i18nParams;
|
|
541
|
+
constructor(status, code, options = {}) {
|
|
542
|
+
super(options.message);
|
|
543
|
+
this.name = "GravitoException";
|
|
544
|
+
this.status = status;
|
|
545
|
+
this.cause = options.cause;
|
|
546
|
+
this.code = code;
|
|
547
|
+
if (options.i18nKey) {
|
|
548
|
+
this.i18nKey = options.i18nKey;
|
|
549
|
+
}
|
|
550
|
+
if (options.i18nParams) {
|
|
551
|
+
this.i18nParams = options.i18nParams;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
getLocalizedMessage(t) {
|
|
555
|
+
if (this.i18nKey) {
|
|
556
|
+
return t(this.i18nKey, this.i18nParams);
|
|
557
|
+
}
|
|
558
|
+
return this.message;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// ../core/src/exceptions/HttpException.ts
|
|
562
|
+
class HttpException extends GravitoException {
|
|
563
|
+
constructor(status, options = {}) {
|
|
564
|
+
super(status, "HTTP_ERROR", options);
|
|
565
|
+
this.name = "HttpException";
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// ../core/src/exceptions/ModelNotFoundException.ts
|
|
569
|
+
class ModelNotFoundException extends GravitoException {
|
|
570
|
+
model;
|
|
571
|
+
id;
|
|
572
|
+
constructor(model, id) {
|
|
573
|
+
super(404, "NOT_FOUND", {
|
|
574
|
+
message: `${model} not found.`,
|
|
575
|
+
i18nKey: "errors.model.not_found",
|
|
576
|
+
i18nParams: { model, id: String(id ?? "") }
|
|
577
|
+
});
|
|
578
|
+
this.model = model;
|
|
579
|
+
if (id !== undefined) {
|
|
580
|
+
this.id = id;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// ../core/src/exceptions/ValidationException.ts
|
|
585
|
+
class ValidationException extends GravitoException {
|
|
586
|
+
errors;
|
|
587
|
+
redirectTo;
|
|
588
|
+
input;
|
|
589
|
+
constructor(errors, message = "Validation failed") {
|
|
590
|
+
super(422, "VALIDATION_ERROR", {
|
|
591
|
+
message,
|
|
592
|
+
i18nKey: "errors.validation.failed"
|
|
593
|
+
});
|
|
594
|
+
this.errors = errors;
|
|
595
|
+
}
|
|
596
|
+
withRedirect(url) {
|
|
597
|
+
this.redirectTo = url;
|
|
598
|
+
return this;
|
|
599
|
+
}
|
|
600
|
+
withInput(input) {
|
|
601
|
+
this.input = input;
|
|
602
|
+
return this;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// ../core/src/GlobalErrorHandlers.ts
|
|
606
|
+
var stateKey = Symbol.for("gravito.core.globalErrorHandlers");
|
|
607
|
+
function getGlobalState() {
|
|
608
|
+
const g = globalThis;
|
|
609
|
+
const existing = g[stateKey];
|
|
610
|
+
if (existing) {
|
|
611
|
+
return existing;
|
|
612
|
+
}
|
|
613
|
+
const created = {
|
|
614
|
+
nextId: 1,
|
|
615
|
+
sinks: new Map,
|
|
616
|
+
listenersInstalled: false,
|
|
617
|
+
onUnhandledRejection: undefined,
|
|
618
|
+
onUncaughtException: undefined
|
|
619
|
+
};
|
|
620
|
+
g[stateKey] = created;
|
|
621
|
+
return created;
|
|
622
|
+
}
|
|
623
|
+
function offProcess(event, listener) {
|
|
624
|
+
const p = process;
|
|
625
|
+
if (typeof p.off === "function") {
|
|
626
|
+
p.off(event, listener);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (typeof p.removeListener === "function") {
|
|
630
|
+
p.removeListener(event, listener);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function safeMessageFromUnknown(error) {
|
|
634
|
+
if (error instanceof Error) {
|
|
635
|
+
return error.message || "Error";
|
|
636
|
+
}
|
|
637
|
+
if (typeof error === "string") {
|
|
638
|
+
return error;
|
|
639
|
+
}
|
|
640
|
+
try {
|
|
641
|
+
return JSON.stringify(error);
|
|
642
|
+
} catch {
|
|
643
|
+
return String(error);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async function handleProcessError(kind, error) {
|
|
647
|
+
const state = getGlobalState();
|
|
648
|
+
if (state.sinks.size === 0) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const isProduction = false;
|
|
652
|
+
let shouldExit = false;
|
|
653
|
+
let exitCode = 1;
|
|
654
|
+
let exitTimer;
|
|
655
|
+
try {
|
|
656
|
+
const sinks = Array.from(state.sinks.values());
|
|
657
|
+
const prepared = await Promise.all(sinks.map(async (sink) => {
|
|
658
|
+
const defaultExit = sink.mode === "exit" || sink.mode === "exitInProduction" && isProduction;
|
|
659
|
+
let ctx = {
|
|
660
|
+
...sink.core ? { core: sink.core } : {},
|
|
661
|
+
kind,
|
|
662
|
+
error,
|
|
663
|
+
isProduction,
|
|
664
|
+
timestamp: Date.now(),
|
|
665
|
+
exit: defaultExit,
|
|
666
|
+
exitCode: sink.exitCode ?? 1,
|
|
667
|
+
gracePeriodMs: sink.gracePeriodMs ?? 250
|
|
668
|
+
};
|
|
669
|
+
if (sink.core) {
|
|
670
|
+
ctx = await sink.core.hooks.applyFilters("processError:context", ctx);
|
|
671
|
+
}
|
|
672
|
+
return { sink, ctx };
|
|
673
|
+
}));
|
|
674
|
+
const exitTargets = prepared.map((p) => p.ctx).filter((ctx) => (ctx.exit ?? false) && (ctx.exitCode ?? 1) >= 0);
|
|
675
|
+
shouldExit = exitTargets.length > 0;
|
|
676
|
+
const gracePeriodMs = Math.max(0, ...exitTargets.map((c) => c.gracePeriodMs ?? 250));
|
|
677
|
+
exitCode = Math.max(0, ...exitTargets.map((c) => c.exitCode ?? 1));
|
|
678
|
+
if (shouldExit) {
|
|
679
|
+
exitTimer = setTimeout(() => {
|
|
680
|
+
process.exit(exitCode);
|
|
681
|
+
}, gracePeriodMs);
|
|
682
|
+
exitTimer.unref?.();
|
|
683
|
+
}
|
|
684
|
+
await Promise.all(prepared.map(async ({ sink, ctx }) => {
|
|
685
|
+
const logger = sink.logger ?? sink.core?.logger;
|
|
686
|
+
const logLevel = ctx.logLevel ?? "error";
|
|
687
|
+
if (logger && logLevel !== "none") {
|
|
688
|
+
const message = safeMessageFromUnknown(ctx.error);
|
|
689
|
+
const msg = ctx.logMessage ?? `[${ctx.kind}] ${message}`;
|
|
690
|
+
if (logLevel === "error") {
|
|
691
|
+
logger.error(msg, ctx.error);
|
|
692
|
+
} else if (logLevel === "warn") {
|
|
693
|
+
logger.warn(msg, ctx.error);
|
|
694
|
+
} else {
|
|
695
|
+
logger.info(msg, ctx.error);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (sink.core) {
|
|
699
|
+
await sink.core.hooks.doAction("processError:report", ctx);
|
|
700
|
+
}
|
|
701
|
+
}));
|
|
702
|
+
} catch (e) {
|
|
703
|
+
console.error("[gravito-core] Failed to handle process-level error:", e);
|
|
704
|
+
} finally {
|
|
705
|
+
if (shouldExit) {
|
|
706
|
+
clearTimeout(exitTimer);
|
|
707
|
+
process.exit(exitCode);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function ensureListenersInstalled() {
|
|
712
|
+
const state = getGlobalState();
|
|
713
|
+
if (state.listenersInstalled) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (typeof process === "undefined" || typeof process.on !== "function") {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
state.onUnhandledRejection = (reason) => {
|
|
720
|
+
handleProcessError("unhandledRejection", reason);
|
|
721
|
+
};
|
|
722
|
+
state.onUncaughtException = (error) => {
|
|
723
|
+
handleProcessError("uncaughtException", error);
|
|
724
|
+
};
|
|
725
|
+
process.on("unhandledRejection", state.onUnhandledRejection);
|
|
726
|
+
process.on("uncaughtException", state.onUncaughtException);
|
|
727
|
+
state.listenersInstalled = true;
|
|
728
|
+
}
|
|
729
|
+
function teardownListenersIfUnused() {
|
|
730
|
+
const state = getGlobalState();
|
|
731
|
+
if (!state.listenersInstalled || state.sinks.size > 0) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (state.onUnhandledRejection) {
|
|
735
|
+
offProcess("unhandledRejection", state.onUnhandledRejection);
|
|
736
|
+
}
|
|
737
|
+
if (state.onUncaughtException) {
|
|
738
|
+
offProcess("uncaughtException", state.onUncaughtException);
|
|
739
|
+
}
|
|
740
|
+
state.onUnhandledRejection = undefined;
|
|
741
|
+
state.onUncaughtException = undefined;
|
|
742
|
+
state.listenersInstalled = false;
|
|
743
|
+
}
|
|
744
|
+
function registerGlobalErrorHandlers(options = {}) {
|
|
745
|
+
const state = getGlobalState();
|
|
746
|
+
ensureListenersInstalled();
|
|
747
|
+
const id = state.nextId++;
|
|
748
|
+
state.sinks.set(id, {
|
|
749
|
+
...options,
|
|
750
|
+
mode: options.mode ?? "exitInProduction"
|
|
751
|
+
});
|
|
752
|
+
return () => {
|
|
753
|
+
state.sinks.delete(id);
|
|
754
|
+
teardownListenersIfUnused();
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
// ../core/src/HookManager.ts
|
|
758
|
+
class HookManager {
|
|
759
|
+
filters = new Map;
|
|
760
|
+
actions = new Map;
|
|
761
|
+
addFilter(hook, callback) {
|
|
762
|
+
if (!this.filters.has(hook)) {
|
|
763
|
+
this.filters.set(hook, []);
|
|
764
|
+
}
|
|
765
|
+
this.filters.get(hook)?.push(callback);
|
|
766
|
+
}
|
|
767
|
+
async applyFilters(hook, initialValue, ...args) {
|
|
768
|
+
const callbacks = this.filters.get(hook) || [];
|
|
769
|
+
let value = initialValue;
|
|
770
|
+
for (const callback of callbacks) {
|
|
771
|
+
try {
|
|
772
|
+
value = await callback(value, ...args);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
console.error(`[HookManager] Error in filter '${hook}':`, error);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return value;
|
|
778
|
+
}
|
|
779
|
+
addAction(hook, callback) {
|
|
780
|
+
if (!this.actions.has(hook)) {
|
|
781
|
+
this.actions.set(hook, []);
|
|
782
|
+
}
|
|
783
|
+
this.actions.get(hook)?.push(callback);
|
|
784
|
+
}
|
|
785
|
+
async doAction(hook, args) {
|
|
786
|
+
const callbacks = this.actions.get(hook) || [];
|
|
787
|
+
for (const callback of callbacks) {
|
|
788
|
+
try {
|
|
789
|
+
await callback(args);
|
|
790
|
+
} catch (error) {
|
|
791
|
+
console.error(`[HookManager] Error in action '${hook}':`, error);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// ../core/src/helpers/response.ts
|
|
797
|
+
function fail(message, code, details) {
|
|
798
|
+
const error = { message };
|
|
799
|
+
if (code !== undefined) {
|
|
800
|
+
error.code = code;
|
|
801
|
+
}
|
|
802
|
+
if (details !== undefined) {
|
|
803
|
+
error.details = details;
|
|
804
|
+
}
|
|
805
|
+
return { success: false, error };
|
|
806
|
+
}
|
|
807
|
+
// ../core/src/http/CookieJar.ts
|
|
808
|
+
class CookieJar {
|
|
809
|
+
encrypter;
|
|
810
|
+
queued = new Map;
|
|
811
|
+
constructor(encrypter) {
|
|
812
|
+
this.encrypter = encrypter;
|
|
813
|
+
}
|
|
814
|
+
queue(name, value, minutes = 60, options = {}) {
|
|
815
|
+
if (minutes && !options.maxAge) {
|
|
816
|
+
options.maxAge = minutes * 60;
|
|
817
|
+
}
|
|
818
|
+
let finalValue = value;
|
|
819
|
+
if (options.encrypt) {
|
|
820
|
+
if (!this.encrypter) {
|
|
821
|
+
throw new Error("Encryption is not available. Ensure APP_KEY is set.");
|
|
822
|
+
}
|
|
823
|
+
finalValue = this.encrypter.encrypt(value);
|
|
824
|
+
}
|
|
825
|
+
this.queued.set(name, { value: finalValue, options });
|
|
826
|
+
}
|
|
827
|
+
forever(name, value, options = {}) {
|
|
828
|
+
this.queue(name, value, 2628000, options);
|
|
829
|
+
}
|
|
830
|
+
forget(name, options = {}) {
|
|
831
|
+
this.queue(name, "", 0, { ...options, maxAge: 0, expires: new Date(0) });
|
|
832
|
+
}
|
|
833
|
+
serializeCookie(name, value, opts) {
|
|
834
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
835
|
+
if (opts.maxAge !== undefined) {
|
|
836
|
+
parts.push(`Max-Age=${opts.maxAge}`);
|
|
837
|
+
}
|
|
838
|
+
if (opts.expires) {
|
|
839
|
+
parts.push(`Expires=${opts.expires.toUTCString()}`);
|
|
840
|
+
}
|
|
841
|
+
if (opts.path) {
|
|
842
|
+
parts.push(`Path=${opts.path}`);
|
|
843
|
+
}
|
|
844
|
+
if (opts.domain) {
|
|
845
|
+
parts.push(`Domain=${opts.domain}`);
|
|
846
|
+
}
|
|
847
|
+
if (opts.secure) {
|
|
848
|
+
parts.push("Secure");
|
|
849
|
+
}
|
|
850
|
+
if (opts.httpOnly) {
|
|
851
|
+
parts.push("HttpOnly");
|
|
852
|
+
}
|
|
853
|
+
if (opts.sameSite) {
|
|
854
|
+
parts.push(`SameSite=${opts.sameSite}`);
|
|
855
|
+
}
|
|
856
|
+
return parts.join("; ");
|
|
857
|
+
}
|
|
858
|
+
attach(c) {
|
|
859
|
+
for (const [name, { value, options }] of this.queued) {
|
|
860
|
+
c.header("Set-Cookie", this.serializeCookie(name, value, options), { append: true });
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
// ../core/src/Logger.ts
|
|
865
|
+
class ConsoleLogger {
|
|
866
|
+
debug(message, ...args) {
|
|
867
|
+
console.debug(`[DEBUG] ${message}`, ...args);
|
|
868
|
+
}
|
|
869
|
+
info(message, ...args) {
|
|
870
|
+
console.info(`[INFO] ${message}`, ...args);
|
|
871
|
+
}
|
|
872
|
+
warn(message, ...args) {
|
|
873
|
+
console.warn(`[WARN] ${message}`, ...args);
|
|
874
|
+
}
|
|
875
|
+
error(message, ...args) {
|
|
876
|
+
console.error(`[ERROR] ${message}`, ...args);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// ../core/src/adapters/bun/BunRequest.ts
|
|
880
|
+
class BunRequest {
|
|
881
|
+
raw;
|
|
882
|
+
_url;
|
|
883
|
+
_params = {};
|
|
884
|
+
_query = null;
|
|
885
|
+
_validated = {};
|
|
886
|
+
constructor(raw, params = {}) {
|
|
887
|
+
this.raw = raw;
|
|
888
|
+
this._url = new URL(raw.url);
|
|
889
|
+
this._params = params;
|
|
890
|
+
}
|
|
891
|
+
setParams(params) {
|
|
892
|
+
this._params = params;
|
|
893
|
+
}
|
|
894
|
+
get url() {
|
|
895
|
+
return this.raw.url;
|
|
896
|
+
}
|
|
897
|
+
get method() {
|
|
898
|
+
return this.raw.method;
|
|
899
|
+
}
|
|
900
|
+
get path() {
|
|
901
|
+
return this._url.pathname;
|
|
902
|
+
}
|
|
903
|
+
param(name) {
|
|
904
|
+
return this._params[name];
|
|
905
|
+
}
|
|
906
|
+
params() {
|
|
907
|
+
return this._params;
|
|
908
|
+
}
|
|
909
|
+
query(name) {
|
|
910
|
+
if (!this._query) {
|
|
911
|
+
this.parseQuery();
|
|
912
|
+
}
|
|
913
|
+
const val = this._query?.[name];
|
|
914
|
+
if (Array.isArray(val)) {
|
|
915
|
+
return val[0];
|
|
916
|
+
}
|
|
917
|
+
return val;
|
|
918
|
+
}
|
|
919
|
+
queries() {
|
|
920
|
+
if (!this._query) {
|
|
921
|
+
this.parseQuery();
|
|
922
|
+
}
|
|
923
|
+
return this._query;
|
|
924
|
+
}
|
|
925
|
+
header(name) {
|
|
926
|
+
if (name) {
|
|
927
|
+
return this.raw.headers.get(name) || undefined;
|
|
928
|
+
}
|
|
929
|
+
const headers = {};
|
|
930
|
+
this.raw.headers.forEach((value, key) => {
|
|
931
|
+
headers[key] = value;
|
|
932
|
+
});
|
|
933
|
+
return headers;
|
|
934
|
+
}
|
|
935
|
+
async json() {
|
|
936
|
+
return this.raw.json();
|
|
937
|
+
}
|
|
938
|
+
async text() {
|
|
939
|
+
return this.raw.text();
|
|
940
|
+
}
|
|
941
|
+
async formData() {
|
|
942
|
+
return this.raw.formData();
|
|
943
|
+
}
|
|
944
|
+
async arrayBuffer() {
|
|
945
|
+
return this.raw.arrayBuffer();
|
|
946
|
+
}
|
|
947
|
+
setValidated(target, data2) {
|
|
948
|
+
this._validated[target] = data2;
|
|
949
|
+
}
|
|
950
|
+
valid(target) {
|
|
951
|
+
const data2 = this._validated[target];
|
|
952
|
+
if (data2 === undefined) {
|
|
953
|
+
throw new Error(`Validation target '${target}' not found or validation failed.`);
|
|
954
|
+
}
|
|
955
|
+
return data2;
|
|
956
|
+
}
|
|
957
|
+
parseQuery() {
|
|
958
|
+
this._query = {};
|
|
959
|
+
for (const [key, value] of this._url.searchParams) {
|
|
960
|
+
if (this._query[key]) {
|
|
961
|
+
if (Array.isArray(this._query[key])) {
|
|
962
|
+
this._query[key].push(value);
|
|
963
|
+
} else {
|
|
964
|
+
this._query[key] = [this._query[key], value];
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
this._query[key] = value;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// ../core/src/adapters/bun/BunContext.ts
|
|
974
|
+
class BunContext {
|
|
975
|
+
env;
|
|
976
|
+
req;
|
|
977
|
+
_variables = new Map;
|
|
978
|
+
_status = 200;
|
|
979
|
+
_headers = new Headers;
|
|
980
|
+
_executionCtx;
|
|
981
|
+
res;
|
|
982
|
+
native;
|
|
983
|
+
constructor(request, env = {}, executionCtx) {
|
|
984
|
+
this.env = env;
|
|
985
|
+
this.req = new BunRequest(request);
|
|
986
|
+
this._executionCtx = executionCtx;
|
|
987
|
+
this.native = { request, env, executionCtx };
|
|
988
|
+
}
|
|
989
|
+
static create(request, env = {}, executionCtx) {
|
|
990
|
+
const instance = new BunContext(request, env, executionCtx);
|
|
991
|
+
return new Proxy(instance, {
|
|
992
|
+
get(target, prop, receiver) {
|
|
993
|
+
if (prop in target) {
|
|
994
|
+
const value = Reflect.get(target, prop, receiver);
|
|
995
|
+
if (typeof value === "function") {
|
|
996
|
+
return value.bind(target);
|
|
997
|
+
}
|
|
998
|
+
return value;
|
|
999
|
+
}
|
|
1000
|
+
if (typeof prop === "string") {
|
|
1001
|
+
return target.get(prop);
|
|
1002
|
+
}
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
json(data2, status = 200) {
|
|
1008
|
+
this.status(status);
|
|
1009
|
+
this.header("Content-Type", "application/json");
|
|
1010
|
+
this.res = new Response(JSON.stringify(data2), {
|
|
1011
|
+
status: this._status,
|
|
1012
|
+
headers: this._headers
|
|
1013
|
+
});
|
|
1014
|
+
return this.res;
|
|
1015
|
+
}
|
|
1016
|
+
text(text, status = 200) {
|
|
1017
|
+
this.status(status);
|
|
1018
|
+
this.header("Content-Type", "text/plain");
|
|
1019
|
+
this.res = new Response(text, {
|
|
1020
|
+
status: this._status,
|
|
1021
|
+
headers: this._headers
|
|
1022
|
+
});
|
|
1023
|
+
return this.res;
|
|
1024
|
+
}
|
|
1025
|
+
html(html, status = 200) {
|
|
1026
|
+
this.status(status);
|
|
1027
|
+
this.header("Content-Type", "text/html");
|
|
1028
|
+
this.res = new Response(html, {
|
|
1029
|
+
status: this._status,
|
|
1030
|
+
headers: this._headers
|
|
1031
|
+
});
|
|
1032
|
+
return this.res;
|
|
1033
|
+
}
|
|
1034
|
+
redirect(url, status = 302) {
|
|
1035
|
+
this.status(status);
|
|
1036
|
+
this.header("Location", url);
|
|
1037
|
+
this.res = new Response(null, {
|
|
1038
|
+
status: this._status,
|
|
1039
|
+
headers: this._headers
|
|
1040
|
+
});
|
|
1041
|
+
return this.res;
|
|
1042
|
+
}
|
|
1043
|
+
body(data2, status = 200) {
|
|
1044
|
+
this.status(status);
|
|
1045
|
+
this.res = new Response(data2, {
|
|
1046
|
+
status: this._status,
|
|
1047
|
+
headers: this._headers
|
|
1048
|
+
});
|
|
1049
|
+
return this.res;
|
|
1050
|
+
}
|
|
1051
|
+
stream(stream, status = 200) {
|
|
1052
|
+
this.status(status);
|
|
1053
|
+
this.res = new Response(stream, {
|
|
1054
|
+
status: this._status,
|
|
1055
|
+
headers: this._headers
|
|
1056
|
+
});
|
|
1057
|
+
return this.res;
|
|
1058
|
+
}
|
|
1059
|
+
header(name, value, options) {
|
|
1060
|
+
if (value === undefined) {
|
|
1061
|
+
return this.req.header(name);
|
|
1062
|
+
}
|
|
1063
|
+
if (options?.append) {
|
|
1064
|
+
this._headers.append(name, value);
|
|
1065
|
+
this.res?.headers.append(name, value);
|
|
1066
|
+
} else {
|
|
1067
|
+
this._headers.set(name, value);
|
|
1068
|
+
if (this.res) {
|
|
1069
|
+
this.res.headers.set(name, value);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
status(code) {
|
|
1075
|
+
this._status = code;
|
|
1076
|
+
}
|
|
1077
|
+
get(key) {
|
|
1078
|
+
return this._variables.get(key);
|
|
1079
|
+
}
|
|
1080
|
+
set(key, value) {
|
|
1081
|
+
this._variables.set(key, value);
|
|
1082
|
+
}
|
|
1083
|
+
get executionCtx() {
|
|
1084
|
+
return this._executionCtx;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ../core/src/adapters/bun/RadixNode.ts
|
|
1089
|
+
class RadixNode {
|
|
1090
|
+
segment;
|
|
1091
|
+
type;
|
|
1092
|
+
children = new Map;
|
|
1093
|
+
paramChild = null;
|
|
1094
|
+
wildcardChild = null;
|
|
1095
|
+
handlers = new Map;
|
|
1096
|
+
paramName = null;
|
|
1097
|
+
regex = null;
|
|
1098
|
+
constructor(segment = "", type = 0 /* STATIC */) {
|
|
1099
|
+
this.segment = segment;
|
|
1100
|
+
this.type = type;
|
|
1101
|
+
}
|
|
1102
|
+
toJSON() {
|
|
1103
|
+
return {
|
|
1104
|
+
segment: this.segment,
|
|
1105
|
+
type: this.type,
|
|
1106
|
+
children: Array.from(this.children.entries()).map(([k, v]) => [k, v.toJSON()]),
|
|
1107
|
+
paramChild: this.paramChild?.toJSON() || null,
|
|
1108
|
+
wildcardChild: this.wildcardChild?.toJSON() || null,
|
|
1109
|
+
paramName: this.paramName,
|
|
1110
|
+
regex: this.regex ? this.regex.source : null
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
static fromJSON(json) {
|
|
1114
|
+
const node = new RadixNode(json.segment, json.type);
|
|
1115
|
+
node.paramName = json.paramName;
|
|
1116
|
+
if (json.regex) {
|
|
1117
|
+
node.regex = new RegExp(json.regex);
|
|
1118
|
+
}
|
|
1119
|
+
if (json.children) {
|
|
1120
|
+
for (const [key, childJson] of json.children) {
|
|
1121
|
+
node.children.set(key, RadixNode.fromJSON(childJson));
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (json.paramChild) {
|
|
1125
|
+
node.paramChild = RadixNode.fromJSON(json.paramChild);
|
|
1126
|
+
}
|
|
1127
|
+
if (json.wildcardChild) {
|
|
1128
|
+
node.wildcardChild = RadixNode.fromJSON(json.wildcardChild);
|
|
1129
|
+
}
|
|
1130
|
+
return node;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// ../core/src/adapters/bun/RadixRouter.ts
|
|
1135
|
+
class RadixRouter {
|
|
1136
|
+
root = new RadixNode;
|
|
1137
|
+
globalConstraints = new Map;
|
|
1138
|
+
where(param, regex) {
|
|
1139
|
+
this.globalConstraints.set(param, regex);
|
|
1140
|
+
}
|
|
1141
|
+
add(method, path, handlers) {
|
|
1142
|
+
let node = this.root;
|
|
1143
|
+
const segments = this.splitPath(path);
|
|
1144
|
+
for (let i = 0;i < segments.length; i++) {
|
|
1145
|
+
const segment = segments[i];
|
|
1146
|
+
if (segment === "*") {
|
|
1147
|
+
if (!node.wildcardChild) {
|
|
1148
|
+
node.wildcardChild = new RadixNode("*", 2 /* WILDCARD */);
|
|
1149
|
+
}
|
|
1150
|
+
node = node.wildcardChild;
|
|
1151
|
+
break;
|
|
1152
|
+
} else if (segment.startsWith(":")) {
|
|
1153
|
+
const paramName = segment.slice(1);
|
|
1154
|
+
if (!node.paramChild) {
|
|
1155
|
+
const child = new RadixNode(segment, 1 /* PARAM */);
|
|
1156
|
+
child.paramName = paramName;
|
|
1157
|
+
const constraint = this.globalConstraints.get(paramName);
|
|
1158
|
+
if (constraint) {
|
|
1159
|
+
child.regex = constraint;
|
|
1160
|
+
}
|
|
1161
|
+
node.paramChild = child;
|
|
1162
|
+
}
|
|
1163
|
+
node = node.paramChild;
|
|
1164
|
+
} else {
|
|
1165
|
+
if (!node.children.has(segment)) {
|
|
1166
|
+
node.children.set(segment, new RadixNode(segment, 0 /* STATIC */));
|
|
1167
|
+
}
|
|
1168
|
+
node = node.children.get(segment);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
node.handlers.set(method.toLowerCase(), handlers);
|
|
1172
|
+
}
|
|
1173
|
+
match(method, path) {
|
|
1174
|
+
const normalizedMethod = method.toLowerCase();
|
|
1175
|
+
if (path === "/" || path === "") {
|
|
1176
|
+
const handlers = this.root.handlers.get(normalizedMethod);
|
|
1177
|
+
if (handlers) {
|
|
1178
|
+
return { handlers, params: {} };
|
|
1179
|
+
}
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
const searchPath = path.startsWith("/") ? path.slice(1) : path;
|
|
1183
|
+
const segments = searchPath.split("/");
|
|
1184
|
+
return this.matchRecursive(this.root, segments, 0, {}, normalizedMethod);
|
|
1185
|
+
}
|
|
1186
|
+
matchRecursive(node, segments, depth, params, method) {
|
|
1187
|
+
if (depth >= segments.length) {
|
|
1188
|
+
let handlers = node.handlers.get(method);
|
|
1189
|
+
if (!handlers) {
|
|
1190
|
+
handlers = node.handlers.get("all");
|
|
1191
|
+
}
|
|
1192
|
+
if (handlers) {
|
|
1193
|
+
return { handlers, params };
|
|
1194
|
+
}
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
const segment = segments[depth];
|
|
1198
|
+
const staticChild = node.children.get(segment);
|
|
1199
|
+
if (staticChild) {
|
|
1200
|
+
const match = this.matchRecursive(staticChild, segments, depth + 1, params, method);
|
|
1201
|
+
if (match) {
|
|
1202
|
+
return match;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const paramChild = node.paramChild;
|
|
1206
|
+
if (paramChild) {
|
|
1207
|
+
if (paramChild.regex && !paramChild.regex.test(segment)) {} else {
|
|
1208
|
+
if (paramChild.paramName) {
|
|
1209
|
+
params[paramChild.paramName] = decodeURIComponent(segment);
|
|
1210
|
+
const match = this.matchRecursive(paramChild, segments, depth + 1, params, method);
|
|
1211
|
+
if (match) {
|
|
1212
|
+
return match;
|
|
1213
|
+
}
|
|
1214
|
+
delete params[paramChild.paramName];
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (node.wildcardChild) {
|
|
1219
|
+
let handlers = node.wildcardChild.handlers.get(method);
|
|
1220
|
+
if (!handlers) {
|
|
1221
|
+
handlers = node.wildcardChild.handlers.get("all");
|
|
1222
|
+
}
|
|
1223
|
+
if (handlers) {
|
|
1224
|
+
return { handlers, params };
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
splitPath(path) {
|
|
1230
|
+
if (path === "/" || path === "") {
|
|
1231
|
+
return [];
|
|
1232
|
+
}
|
|
1233
|
+
let p = path;
|
|
1234
|
+
if (p.startsWith("/")) {
|
|
1235
|
+
p = p.slice(1);
|
|
1236
|
+
}
|
|
1237
|
+
if (p.endsWith("/")) {
|
|
1238
|
+
p = p.slice(0, -1);
|
|
1239
|
+
}
|
|
1240
|
+
return p.split("/");
|
|
1241
|
+
}
|
|
1242
|
+
serialize() {
|
|
1243
|
+
return JSON.stringify({
|
|
1244
|
+
root: this.root.toJSON(),
|
|
1245
|
+
globalConstraints: Array.from(this.globalConstraints.entries()).map(([k, v]) => [
|
|
1246
|
+
k,
|
|
1247
|
+
v.source
|
|
1248
|
+
])
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
static fromSerialized(json) {
|
|
1252
|
+
const data2 = JSON.parse(json);
|
|
1253
|
+
const router = new RadixRouter;
|
|
1254
|
+
router.root = RadixNode.fromJSON(data2.root);
|
|
1255
|
+
if (data2.globalConstraints) {
|
|
1256
|
+
for (const [key, source] of data2.globalConstraints) {
|
|
1257
|
+
router.globalConstraints.set(key, new RegExp(source));
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return router;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// ../core/src/adapters/bun/BunNativeAdapter.ts
|
|
1265
|
+
class BunNativeAdapter {
|
|
1266
|
+
name = "bun-native";
|
|
1267
|
+
version = "0.0.1";
|
|
1268
|
+
get native() {
|
|
1269
|
+
return this;
|
|
1270
|
+
}
|
|
1271
|
+
router = new RadixRouter;
|
|
1272
|
+
middlewares = [];
|
|
1273
|
+
errorHandler = null;
|
|
1274
|
+
notFoundHandler = null;
|
|
1275
|
+
route(method, path, ...handlers) {
|
|
1276
|
+
this.router.add(method, path, handlers);
|
|
1277
|
+
}
|
|
1278
|
+
routes(routes) {
|
|
1279
|
+
for (const route of routes) {
|
|
1280
|
+
this.route(route.method, route.path, ...route.handlers);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
use(path, ...middleware) {
|
|
1284
|
+
this.middlewares.push({ path, handlers: middleware });
|
|
1285
|
+
}
|
|
1286
|
+
useGlobal(...middleware) {
|
|
1287
|
+
this.use("*", ...middleware);
|
|
1288
|
+
}
|
|
1289
|
+
mount(path, subAdapter) {
|
|
1290
|
+
const fullPath = path.endsWith("/") ? `${path}*` : `${path}/*`;
|
|
1291
|
+
this.route("all", fullPath, async (ctx) => {
|
|
1292
|
+
const url = new URL(ctx.req.url);
|
|
1293
|
+
const prefix = path.endsWith("/") ? path.slice(0, -1) : path;
|
|
1294
|
+
console.log("[DEBUG] Mount Prefix:", prefix);
|
|
1295
|
+
console.log("[DEBUG] Original Path:", url.pathname);
|
|
1296
|
+
if (url.pathname.startsWith(prefix)) {
|
|
1297
|
+
const newPath = url.pathname.slice(prefix.length);
|
|
1298
|
+
url.pathname = newPath === "" ? "/" : newPath;
|
|
1299
|
+
}
|
|
1300
|
+
const newReq = new Request(url.toString(), {
|
|
1301
|
+
method: ctx.req.method,
|
|
1302
|
+
headers: ctx.req.raw.headers
|
|
1303
|
+
});
|
|
1304
|
+
const res = await subAdapter.fetch(newReq);
|
|
1305
|
+
if (ctx instanceof BunContext) {
|
|
1306
|
+
ctx.res = res;
|
|
1307
|
+
}
|
|
1308
|
+
return res;
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
createContext(request) {
|
|
1312
|
+
return BunContext.create(request);
|
|
1313
|
+
}
|
|
1314
|
+
onError(handler) {
|
|
1315
|
+
this.errorHandler = handler;
|
|
1316
|
+
}
|
|
1317
|
+
onNotFound(handler) {
|
|
1318
|
+
this.notFoundHandler = handler;
|
|
1319
|
+
}
|
|
1320
|
+
async fetch(request, _server) {
|
|
1321
|
+
const ctx = BunContext.create(request);
|
|
1322
|
+
try {
|
|
1323
|
+
const url = new URL(request.url);
|
|
1324
|
+
const path = url.pathname;
|
|
1325
|
+
const method = request.method;
|
|
1326
|
+
const match = this.router.match(method, path);
|
|
1327
|
+
const handlers = [];
|
|
1328
|
+
for (const mw of this.middlewares) {
|
|
1329
|
+
if (mw.path === "*" || path.startsWith(mw.path)) {
|
|
1330
|
+
handlers.push(...mw.handlers);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (match) {
|
|
1334
|
+
if (match.params) {
|
|
1335
|
+
ctx.req.setParams(match.params);
|
|
1336
|
+
}
|
|
1337
|
+
handlers.push(...match.handlers);
|
|
1338
|
+
} else {
|
|
1339
|
+
if (this.notFoundHandler) {
|
|
1340
|
+
handlers.push(this.notFoundHandler);
|
|
1341
|
+
} else {}
|
|
1342
|
+
}
|
|
1343
|
+
return await this.executeChain(ctx, handlers);
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
if (this.errorHandler) {
|
|
1346
|
+
try {
|
|
1347
|
+
const response2 = await this.errorHandler(err, ctx);
|
|
1348
|
+
if (response2) {
|
|
1349
|
+
return response2;
|
|
1350
|
+
}
|
|
1351
|
+
} catch (e) {
|
|
1352
|
+
console.error("Error handler failed", e);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
console.error(err);
|
|
1356
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
async executeChain(ctx, handlers) {
|
|
1360
|
+
let index = -1;
|
|
1361
|
+
const dispatch = async (i) => {
|
|
1362
|
+
if (i <= index) {
|
|
1363
|
+
throw new Error("next() called multiple times");
|
|
1364
|
+
}
|
|
1365
|
+
index = i;
|
|
1366
|
+
const fn = handlers[i];
|
|
1367
|
+
if (!fn) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const result = await fn(ctx, async () => {
|
|
1371
|
+
const res = await dispatch(i + 1);
|
|
1372
|
+
if (res && ctx.res !== res) {
|
|
1373
|
+
ctx.res = res;
|
|
1374
|
+
}
|
|
1375
|
+
return res;
|
|
1376
|
+
});
|
|
1377
|
+
return result;
|
|
1378
|
+
};
|
|
1379
|
+
const finalResponse = await dispatch(0);
|
|
1380
|
+
if (finalResponse && (finalResponse instanceof Response || typeof finalResponse.status === "number")) {
|
|
1381
|
+
return finalResponse;
|
|
1382
|
+
}
|
|
1383
|
+
if (ctx.res) {
|
|
1384
|
+
return ctx.res;
|
|
1385
|
+
}
|
|
1386
|
+
return new Response("Not Found", { status: 404 });
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// ../core/src/Route.ts
|
|
1391
|
+
class Route {
|
|
1392
|
+
router;
|
|
1393
|
+
method;
|
|
1394
|
+
path;
|
|
1395
|
+
options;
|
|
1396
|
+
constructor(router, method, path, options) {
|
|
1397
|
+
this.router = router;
|
|
1398
|
+
this.method = method;
|
|
1399
|
+
this.path = path;
|
|
1400
|
+
this.options = options;
|
|
1401
|
+
}
|
|
1402
|
+
name(name) {
|
|
1403
|
+
this.router.registerName(name, this.method, this.path, this.options);
|
|
1404
|
+
return this;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// ../core/src/Router.ts
|
|
1409
|
+
function isFormRequestClass(value) {
|
|
1410
|
+
if (typeof value !== "function") {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
try {
|
|
1414
|
+
const instance = new value;
|
|
1415
|
+
return instance !== null && typeof instance === "object" && "schema" in instance && "validate" in instance && typeof instance.validate === "function";
|
|
1416
|
+
} catch {
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
function formRequestToMiddleware(RequestClass) {
|
|
1421
|
+
return async (ctx, next) => {
|
|
1422
|
+
const request = new RequestClass;
|
|
1423
|
+
if (typeof request.validate !== "function") {
|
|
1424
|
+
throw new Error("Invalid FormRequest: validate() is missing.");
|
|
1425
|
+
}
|
|
1426
|
+
const result = await request.validate(ctx);
|
|
1427
|
+
if (!result.success) {
|
|
1428
|
+
const errorCode = result.error?.error?.code;
|
|
1429
|
+
const status = errorCode === "AUTHORIZATION_ERROR" ? 403 : 422;
|
|
1430
|
+
return ctx.json(result.error, status);
|
|
1431
|
+
}
|
|
1432
|
+
ctx.set("validated", result.data);
|
|
1433
|
+
await next();
|
|
1434
|
+
return;
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
class RouteGroup {
|
|
1439
|
+
router;
|
|
1440
|
+
options;
|
|
1441
|
+
constructor(router, options) {
|
|
1442
|
+
this.router = router;
|
|
1443
|
+
this.options = options;
|
|
1444
|
+
}
|
|
1445
|
+
prefix(path) {
|
|
1446
|
+
return new RouteGroup(this.router, {
|
|
1447
|
+
...this.options,
|
|
1448
|
+
prefix: (this.options.prefix || "") + path
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
middleware(...handlers) {
|
|
1452
|
+
const flattened = handlers.flat();
|
|
1453
|
+
return new RouteGroup(this.router, {
|
|
1454
|
+
...this.options,
|
|
1455
|
+
middleware: [...this.options.middleware || [], ...flattened]
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
group(callback) {
|
|
1459
|
+
callback(this);
|
|
1460
|
+
}
|
|
1461
|
+
get(path, requestOrHandler, handler) {
|
|
1462
|
+
return this.router.req("get", path, requestOrHandler, handler, this.options);
|
|
1463
|
+
}
|
|
1464
|
+
post(path, requestOrHandler, handler) {
|
|
1465
|
+
return this.router.req("post", path, requestOrHandler, handler, this.options);
|
|
1466
|
+
}
|
|
1467
|
+
put(path, requestOrHandler, handler) {
|
|
1468
|
+
return this.router.req("put", path, requestOrHandler, handler, this.options);
|
|
1469
|
+
}
|
|
1470
|
+
delete(path, requestOrHandler, handler) {
|
|
1471
|
+
return this.router.req("delete", path, requestOrHandler, handler, this.options);
|
|
1472
|
+
}
|
|
1473
|
+
patch(path, requestOrHandler, handler) {
|
|
1474
|
+
return this.router.req("patch", path, requestOrHandler, handler, this.options);
|
|
1475
|
+
}
|
|
1476
|
+
resource(name, controller, options = {}) {
|
|
1477
|
+
const actions = [
|
|
1478
|
+
"index",
|
|
1479
|
+
"create",
|
|
1480
|
+
"store",
|
|
1481
|
+
"show",
|
|
1482
|
+
"edit",
|
|
1483
|
+
"update",
|
|
1484
|
+
"destroy"
|
|
1485
|
+
];
|
|
1486
|
+
const map = {
|
|
1487
|
+
index: { method: "get", path: `/${name}` },
|
|
1488
|
+
create: { method: "get", path: `/${name}/create` },
|
|
1489
|
+
store: { method: "post", path: `/${name}` },
|
|
1490
|
+
show: { method: "get", path: `/${name}/:id` },
|
|
1491
|
+
edit: { method: "get", path: `/${name}/:id/edit` },
|
|
1492
|
+
update: { method: "put", path: `/${name}/:id` },
|
|
1493
|
+
destroy: { method: "delete", path: `/${name}/:id` }
|
|
1494
|
+
};
|
|
1495
|
+
const allowed = actions.filter((action) => {
|
|
1496
|
+
if (options.only) {
|
|
1497
|
+
return options.only.includes(action);
|
|
1498
|
+
}
|
|
1499
|
+
if (options.except) {
|
|
1500
|
+
return !options.except.includes(action);
|
|
1501
|
+
}
|
|
1502
|
+
return true;
|
|
1503
|
+
});
|
|
1504
|
+
for (const action of allowed) {
|
|
1505
|
+
const { method, path } = map[action];
|
|
1506
|
+
if (action === "update") {
|
|
1507
|
+
this.router.req("put", path, [controller, action], undefined, this.options).name(`${name}.${action}`);
|
|
1508
|
+
this.router.req("patch", path, [controller, action], undefined, this.options);
|
|
1509
|
+
} else {
|
|
1510
|
+
this.router.req(method, path, [controller, action], undefined, this.options).name(`${name}.${action}`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
class Router {
|
|
1517
|
+
core;
|
|
1518
|
+
controllers = new Map;
|
|
1519
|
+
namedRoutes = new Map;
|
|
1520
|
+
bindings = new Map;
|
|
1521
|
+
compile() {
|
|
1522
|
+
const routes = [];
|
|
1523
|
+
for (const [name, info] of this.namedRoutes) {
|
|
1524
|
+
routes.push({
|
|
1525
|
+
name,
|
|
1526
|
+
method: info.method,
|
|
1527
|
+
path: info.path,
|
|
1528
|
+
domain: info.domain
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
return routes;
|
|
1532
|
+
}
|
|
1533
|
+
registerName(name, method, path, options = {}) {
|
|
1534
|
+
const fullPath = (options.prefix || "") + path;
|
|
1535
|
+
this.namedRoutes.set(name, {
|
|
1536
|
+
method: method.toUpperCase(),
|
|
1537
|
+
path: fullPath,
|
|
1538
|
+
domain: options.domain
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
url(name, params = {}, query = {}) {
|
|
1542
|
+
const route = this.namedRoutes.get(name);
|
|
1543
|
+
if (!route) {
|
|
1544
|
+
throw new Error(`Named route '${name}' not found`);
|
|
1545
|
+
}
|
|
1546
|
+
let path = route.path;
|
|
1547
|
+
path = path.replace(/:([A-Za-z0-9_]+)/g, (_, key) => {
|
|
1548
|
+
const value = params[key];
|
|
1549
|
+
if (value === undefined || value === null) {
|
|
1550
|
+
throw new Error(`Missing route param '${key}' for route '${name}'`);
|
|
1551
|
+
}
|
|
1552
|
+
return encodeURIComponent(String(value));
|
|
1553
|
+
});
|
|
1554
|
+
const qs = new URLSearchParams;
|
|
1555
|
+
for (const [k, v] of Object.entries(query)) {
|
|
1556
|
+
if (v === undefined || v === null) {
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
qs.set(k, String(v));
|
|
1560
|
+
}
|
|
1561
|
+
const suffix = qs.toString();
|
|
1562
|
+
return suffix ? `${path}?${suffix}` : path;
|
|
1563
|
+
}
|
|
1564
|
+
exportNamedRoutes() {
|
|
1565
|
+
return Object.fromEntries(this.namedRoutes.entries());
|
|
1566
|
+
}
|
|
1567
|
+
loadNamedRoutes(manifest) {
|
|
1568
|
+
this.namedRoutes = new Map(Object.entries(manifest));
|
|
1569
|
+
}
|
|
1570
|
+
bind(param, resolver) {
|
|
1571
|
+
this.bindings.set(param, resolver);
|
|
1572
|
+
}
|
|
1573
|
+
model(param, modelClass) {
|
|
1574
|
+
this.bind(param, async (id) => {
|
|
1575
|
+
if (modelClass && typeof modelClass === "object" && "find" in modelClass && typeof modelClass.find === "function") {
|
|
1576
|
+
const instance = await modelClass.find(id);
|
|
1577
|
+
if (!instance) {
|
|
1578
|
+
throw new Error("ModelNotFound");
|
|
1579
|
+
}
|
|
1580
|
+
return instance;
|
|
1581
|
+
}
|
|
1582
|
+
throw new Error(`Invalid model class for binding '${param}'`);
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
constructor(core) {
|
|
1586
|
+
this.core = core;
|
|
1587
|
+
this.core.adapter.useGlobal(async (c, next) => {
|
|
1588
|
+
const routeModels = c.get("routeModels") ?? {};
|
|
1589
|
+
for (const [param, resolver] of this.bindings) {
|
|
1590
|
+
const value = c.req.param(param);
|
|
1591
|
+
if (value) {
|
|
1592
|
+
try {
|
|
1593
|
+
const resolved = await resolver(value);
|
|
1594
|
+
routeModels[param] = resolved;
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
const message = err instanceof Error ? err.message : undefined;
|
|
1597
|
+
if (message === "ModelNotFound") {
|
|
1598
|
+
throw new ModelNotFoundException(param, value);
|
|
1599
|
+
}
|
|
1600
|
+
throw err;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
c.set("routeModels", routeModels);
|
|
1605
|
+
await next();
|
|
1606
|
+
return;
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
prefix(path) {
|
|
1610
|
+
return new RouteGroup(this, { prefix: path });
|
|
1611
|
+
}
|
|
1612
|
+
domain(host) {
|
|
1613
|
+
return new RouteGroup(this, { domain: host });
|
|
1614
|
+
}
|
|
1615
|
+
middleware(...handlers) {
|
|
1616
|
+
return new RouteGroup(this, { middleware: handlers.flat() });
|
|
1617
|
+
}
|
|
1618
|
+
get(path, requestOrHandler, handler) {
|
|
1619
|
+
return this.req("get", path, requestOrHandler, handler);
|
|
1620
|
+
}
|
|
1621
|
+
post(path, requestOrHandler, handler) {
|
|
1622
|
+
return this.req("post", path, requestOrHandler, handler);
|
|
1623
|
+
}
|
|
1624
|
+
put(path, requestOrHandler, handler) {
|
|
1625
|
+
return this.req("put", path, requestOrHandler, handler);
|
|
1626
|
+
}
|
|
1627
|
+
delete(path, requestOrHandler, handler) {
|
|
1628
|
+
return this.req("delete", path, requestOrHandler, handler);
|
|
1629
|
+
}
|
|
1630
|
+
patch(path, requestOrHandler, handler) {
|
|
1631
|
+
return this.req("patch", path, requestOrHandler, handler);
|
|
1632
|
+
}
|
|
1633
|
+
resource(name, controller, options = {}) {
|
|
1634
|
+
const actions = [
|
|
1635
|
+
"index",
|
|
1636
|
+
"create",
|
|
1637
|
+
"store",
|
|
1638
|
+
"show",
|
|
1639
|
+
"edit",
|
|
1640
|
+
"update",
|
|
1641
|
+
"destroy"
|
|
1642
|
+
];
|
|
1643
|
+
const map = {
|
|
1644
|
+
index: { method: "get", path: `/${name}` },
|
|
1645
|
+
create: { method: "get", path: `/${name}/create` },
|
|
1646
|
+
store: { method: "post", path: `/${name}` },
|
|
1647
|
+
show: { method: "get", path: `/${name}/:id` },
|
|
1648
|
+
edit: { method: "get", path: `/${name}/:id/edit` },
|
|
1649
|
+
update: { method: "put", path: `/${name}/:id` },
|
|
1650
|
+
destroy: { method: "delete", path: `/${name}/:id` }
|
|
1651
|
+
};
|
|
1652
|
+
const allowed = actions.filter((action) => {
|
|
1653
|
+
if (options.only) {
|
|
1654
|
+
return options.only.includes(action);
|
|
1655
|
+
}
|
|
1656
|
+
if (options.except) {
|
|
1657
|
+
return !options.except.includes(action);
|
|
1658
|
+
}
|
|
1659
|
+
return true;
|
|
1660
|
+
});
|
|
1661
|
+
for (const action of allowed) {
|
|
1662
|
+
const { method, path } = map[action];
|
|
1663
|
+
if (action === "update") {
|
|
1664
|
+
this.req("put", path, [controller, action]).name(`${name}.${action}`);
|
|
1665
|
+
this.req("patch", path, [controller, action]);
|
|
1666
|
+
} else {
|
|
1667
|
+
this.req(method, path, [controller, action]).name(`${name}.${action}`);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
req(method, path, requestOrHandler, handler, options = {}) {
|
|
1672
|
+
const fullPath = (options.prefix || "") + path;
|
|
1673
|
+
let formRequestMiddleware = null;
|
|
1674
|
+
let finalRouteHandler;
|
|
1675
|
+
if (handler !== undefined) {
|
|
1676
|
+
if (isFormRequestClass(requestOrHandler)) {
|
|
1677
|
+
formRequestMiddleware = formRequestToMiddleware(requestOrHandler);
|
|
1678
|
+
}
|
|
1679
|
+
finalRouteHandler = handler;
|
|
1680
|
+
} else {
|
|
1681
|
+
finalRouteHandler = requestOrHandler;
|
|
1682
|
+
}
|
|
1683
|
+
let resolvedHandler;
|
|
1684
|
+
if (Array.isArray(finalRouteHandler)) {
|
|
1685
|
+
const [CtrlClass, methodName] = finalRouteHandler;
|
|
1686
|
+
resolvedHandler = this.resolveControllerHandler(CtrlClass, methodName);
|
|
1687
|
+
} else {
|
|
1688
|
+
resolvedHandler = finalRouteHandler;
|
|
1689
|
+
}
|
|
1690
|
+
const handlers = [];
|
|
1691
|
+
if (options.middleware) {
|
|
1692
|
+
handlers.push(...options.middleware);
|
|
1693
|
+
}
|
|
1694
|
+
if (formRequestMiddleware) {
|
|
1695
|
+
handlers.push(formRequestMiddleware);
|
|
1696
|
+
}
|
|
1697
|
+
handlers.push(resolvedHandler);
|
|
1698
|
+
if (options.domain) {
|
|
1699
|
+
const _wrappedHandler = async (c) => {
|
|
1700
|
+
if (c.req.header("host") !== options.domain) {
|
|
1701
|
+
return new Response("Not Found", { status: 404 });
|
|
1702
|
+
}
|
|
1703
|
+
return;
|
|
1704
|
+
};
|
|
1705
|
+
const domainCheck = async (c, next) => {
|
|
1706
|
+
if (c.req.header("host") !== options.domain) {
|
|
1707
|
+
return c.text("Not Found", 404);
|
|
1708
|
+
}
|
|
1709
|
+
await next();
|
|
1710
|
+
};
|
|
1711
|
+
handlers.unshift(domainCheck);
|
|
1712
|
+
}
|
|
1713
|
+
this.core.adapter.route(method, fullPath, ...handlers);
|
|
1714
|
+
return new Route(this, method, fullPath, options);
|
|
1715
|
+
}
|
|
1716
|
+
resolveControllerHandler(CtrlClass, methodName) {
|
|
1717
|
+
let instance = this.controllers.get(CtrlClass);
|
|
1718
|
+
if (!instance) {
|
|
1719
|
+
instance = new CtrlClass(this.core);
|
|
1720
|
+
this.controllers.set(CtrlClass, instance);
|
|
1721
|
+
}
|
|
1722
|
+
const handler = instance[methodName];
|
|
1723
|
+
if (typeof handler !== "function") {
|
|
1724
|
+
throw new Error(`Method '${methodName}' not found in controller '${CtrlClass.name}'`);
|
|
1725
|
+
}
|
|
1726
|
+
return handler.bind(instance);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// ../core/src/security/Encrypter.ts
|
|
1731
|
+
import crypto from "node:crypto";
|
|
1732
|
+
|
|
1733
|
+
class Encrypter {
|
|
1734
|
+
algorithm;
|
|
1735
|
+
key;
|
|
1736
|
+
constructor(options) {
|
|
1737
|
+
this.algorithm = options.cipher || "aes-256-cbc";
|
|
1738
|
+
if (options.key.startsWith("base64:")) {
|
|
1739
|
+
this.key = Buffer.from(options.key.substring(7), "base64");
|
|
1740
|
+
} else {
|
|
1741
|
+
this.key = Buffer.from(options.key);
|
|
1742
|
+
}
|
|
1743
|
+
if (this.algorithm === "aes-128-cbc" && this.key.length !== 16) {
|
|
1744
|
+
throw new Error("The key must be 16 bytes (128 bits) for AES-128-CBC.");
|
|
1745
|
+
}
|
|
1746
|
+
if (this.algorithm === "aes-256-cbc" && this.key.length !== 32) {
|
|
1747
|
+
throw new Error("The key must be 32 bytes (256 bits) for AES-256-CBC.");
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
encrypt(value, serialize = true) {
|
|
1751
|
+
const iv = crypto.randomBytes(16);
|
|
1752
|
+
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
|
|
1753
|
+
const stringValue = serialize ? JSON.stringify(value) : String(value);
|
|
1754
|
+
let encrypted = cipher.update(stringValue, "utf8", "base64");
|
|
1755
|
+
encrypted += cipher.final("base64");
|
|
1756
|
+
const mac = this.hash(iv.toString("base64"), encrypted);
|
|
1757
|
+
const payload = {
|
|
1758
|
+
iv: iv.toString("base64"),
|
|
1759
|
+
value: encrypted,
|
|
1760
|
+
mac,
|
|
1761
|
+
tag: ""
|
|
1762
|
+
};
|
|
1763
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1764
|
+
}
|
|
1765
|
+
decrypt(payload, deserialize = true) {
|
|
1766
|
+
const json = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
|
|
1767
|
+
if (!this.validPayload(json)) {
|
|
1768
|
+
throw new Error("The payload is invalid.");
|
|
1769
|
+
}
|
|
1770
|
+
if (!this.validMac(json)) {
|
|
1771
|
+
throw new Error("The MAC is invalid.");
|
|
1772
|
+
}
|
|
1773
|
+
const iv = Buffer.from(json.iv, "base64");
|
|
1774
|
+
const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv);
|
|
1775
|
+
let decrypted = decipher.update(json.value, "base64", "utf8");
|
|
1776
|
+
decrypted += decipher.final("utf8");
|
|
1777
|
+
return deserialize ? JSON.parse(decrypted) : decrypted;
|
|
1778
|
+
}
|
|
1779
|
+
hash(iv, value) {
|
|
1780
|
+
const hmac = crypto.createHmac("sha256", this.key);
|
|
1781
|
+
hmac.update(iv + value);
|
|
1782
|
+
return hmac.digest("hex");
|
|
1783
|
+
}
|
|
1784
|
+
validPayload(payload) {
|
|
1785
|
+
return typeof payload === "object" && payload !== null && "iv" in payload && "value" in payload && "mac" in payload;
|
|
1786
|
+
}
|
|
1787
|
+
validMac(payload) {
|
|
1788
|
+
const calculated = this.hash(payload.iv, payload.value);
|
|
1789
|
+
return crypto.timingSafeEqual(Buffer.from(calculated), Buffer.from(payload.mac));
|
|
1790
|
+
}
|
|
1791
|
+
static generateKey(cipher = "aes-256-cbc") {
|
|
1792
|
+
const bytes = cipher === "aes-128-cbc" ? 16 : 32;
|
|
1793
|
+
return `base64:${crypto.randomBytes(bytes).toString("base64")}`;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// ../core/src/security/Hasher.ts
|
|
1798
|
+
class BunHasher {
|
|
1799
|
+
async make(value, options) {
|
|
1800
|
+
const bun = Bun;
|
|
1801
|
+
return await bun.password.hash(value, options);
|
|
1802
|
+
}
|
|
1803
|
+
async check(value, hashedValue) {
|
|
1804
|
+
const bun = Bun;
|
|
1805
|
+
return await bun.password.verify(value, hashedValue);
|
|
1806
|
+
}
|
|
1807
|
+
needsRehash(_hashedValue, _options) {
|
|
1808
|
+
return false;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// ../core/src/PlanetCore.ts
|
|
1813
|
+
class PlanetCore {
|
|
1814
|
+
_adapter;
|
|
1815
|
+
get app() {
|
|
1816
|
+
return this._adapter.native;
|
|
1817
|
+
}
|
|
1818
|
+
get adapter() {
|
|
1819
|
+
return this._adapter;
|
|
1820
|
+
}
|
|
1821
|
+
logger;
|
|
1822
|
+
config;
|
|
1823
|
+
hooks;
|
|
1824
|
+
events;
|
|
1825
|
+
router;
|
|
1826
|
+
container = new Container;
|
|
1827
|
+
services = new Map;
|
|
1828
|
+
encrypter;
|
|
1829
|
+
hasher;
|
|
1830
|
+
providers = [];
|
|
1831
|
+
register(provider) {
|
|
1832
|
+
this.providers.push(provider);
|
|
1833
|
+
return this;
|
|
1834
|
+
}
|
|
1835
|
+
async bootstrap() {
|
|
1836
|
+
for (const provider of this.providers) {
|
|
1837
|
+
provider.register(this.container);
|
|
1838
|
+
}
|
|
1839
|
+
for (const provider of this.providers) {
|
|
1840
|
+
if (provider.boot) {
|
|
1841
|
+
await provider.boot(this);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
constructor(options = {}) {
|
|
1846
|
+
this.logger = options.logger ?? new ConsoleLogger;
|
|
1847
|
+
this.config = new ConfigManager(options.config ?? {});
|
|
1848
|
+
this.hooks = new HookManager;
|
|
1849
|
+
this.events = new EventManager(this);
|
|
1850
|
+
this.hasher = new BunHasher;
|
|
1851
|
+
const appKey = (this.config.has("APP_KEY") ? this.config.get("APP_KEY") : undefined) || process.env.APP_KEY;
|
|
1852
|
+
if (appKey) {
|
|
1853
|
+
try {
|
|
1854
|
+
this.encrypter = new Encrypter({ key: appKey });
|
|
1855
|
+
} catch (e) {
|
|
1856
|
+
this.logger.warn("Failed to initialize Encrypter (invalid APP_KEY?):", e);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
if (options.adapter) {
|
|
1860
|
+
this._adapter = options.adapter;
|
|
1861
|
+
} else if (typeof Bun !== "undefined") {
|
|
1862
|
+
this._adapter = new BunNativeAdapter;
|
|
1863
|
+
} else {
|
|
1864
|
+
this._adapter = new HonoAdapter;
|
|
1865
|
+
}
|
|
1866
|
+
this.adapter.use("*", async (c, next) => {
|
|
1867
|
+
c.set("core", this);
|
|
1868
|
+
c.set("logger", this.logger);
|
|
1869
|
+
c.set("config", this.config);
|
|
1870
|
+
const cookieJar = new CookieJar(this.encrypter);
|
|
1871
|
+
c.set("cookieJar", cookieJar);
|
|
1872
|
+
c.route = (name, params, query) => this.router.url(name, params, query);
|
|
1873
|
+
await next();
|
|
1874
|
+
return;
|
|
1875
|
+
});
|
|
1876
|
+
this.router = new Router(this);
|
|
1877
|
+
this.adapter.onError(async (err, c) => {
|
|
1878
|
+
const isProduction = false;
|
|
1879
|
+
const codeFromStatus = (status2) => {
|
|
1880
|
+
switch (status2) {
|
|
1881
|
+
case 400:
|
|
1882
|
+
return "BAD_REQUEST";
|
|
1883
|
+
case 401:
|
|
1884
|
+
return "UNAUTHENTICATED";
|
|
1885
|
+
case 403:
|
|
1886
|
+
return "FORBIDDEN";
|
|
1887
|
+
case 404:
|
|
1888
|
+
return "NOT_FOUND";
|
|
1889
|
+
case 405:
|
|
1890
|
+
return "METHOD_NOT_ALLOWED";
|
|
1891
|
+
case 409:
|
|
1892
|
+
return "CONFLICT";
|
|
1893
|
+
case 422:
|
|
1894
|
+
return "VALIDATION_ERROR";
|
|
1895
|
+
case 429:
|
|
1896
|
+
return "TOO_MANY_REQUESTS";
|
|
1897
|
+
default:
|
|
1898
|
+
return status2 >= 500 ? "INTERNAL_ERROR" : "HTTP_ERROR";
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
const messageFromStatus = (status2) => {
|
|
1902
|
+
switch (status2) {
|
|
1903
|
+
case 400:
|
|
1904
|
+
return "Bad Request";
|
|
1905
|
+
case 401:
|
|
1906
|
+
return "Unauthorized";
|
|
1907
|
+
case 403:
|
|
1908
|
+
return "Forbidden";
|
|
1909
|
+
case 404:
|
|
1910
|
+
return "Not Found";
|
|
1911
|
+
case 405:
|
|
1912
|
+
return "Method Not Allowed";
|
|
1913
|
+
case 409:
|
|
1914
|
+
return "Conflict";
|
|
1915
|
+
case 422:
|
|
1916
|
+
return "Unprocessable Content";
|
|
1917
|
+
case 429:
|
|
1918
|
+
return "Too Many Requests";
|
|
1919
|
+
case 500:
|
|
1920
|
+
return "Internal Server Error";
|
|
1921
|
+
case 502:
|
|
1922
|
+
return "Bad Gateway";
|
|
1923
|
+
case 503:
|
|
1924
|
+
return "Service Unavailable";
|
|
1925
|
+
case 504:
|
|
1926
|
+
return "Gateway Timeout";
|
|
1927
|
+
default:
|
|
1928
|
+
return status2 >= 500 ? "Internal Server Error" : "Request Error";
|
|
1929
|
+
}
|
|
1930
|
+
};
|
|
1931
|
+
const view = c.get("view");
|
|
1932
|
+
const i18n = c.get("i18n");
|
|
1933
|
+
const accept = c.req.header("Accept") || "";
|
|
1934
|
+
const wantsHtml = Boolean(view && accept.includes("text/html") && !accept.includes("application/json"));
|
|
1935
|
+
let status = 500;
|
|
1936
|
+
let message = messageFromStatus(500);
|
|
1937
|
+
let code = "INTERNAL_ERROR";
|
|
1938
|
+
let details;
|
|
1939
|
+
if (err instanceof GravitoException) {
|
|
1940
|
+
status = err.status;
|
|
1941
|
+
code = err.code;
|
|
1942
|
+
if (code === "HTTP_ERROR") {
|
|
1943
|
+
code = codeFromStatus(status);
|
|
1944
|
+
}
|
|
1945
|
+
if (i18n?.t && err.i18nKey) {
|
|
1946
|
+
message = i18n.t(err.i18nKey, err.i18nParams);
|
|
1947
|
+
} else {
|
|
1948
|
+
message = err.message || messageFromStatus(status);
|
|
1949
|
+
}
|
|
1950
|
+
if (err instanceof ValidationException) {
|
|
1951
|
+
details = err.errors;
|
|
1952
|
+
if (wantsHtml) {
|
|
1953
|
+
const session = c.get("session");
|
|
1954
|
+
if (session) {
|
|
1955
|
+
const errorBag = {};
|
|
1956
|
+
for (const e of err.errors) {
|
|
1957
|
+
if (!errorBag[e.field]) {
|
|
1958
|
+
errorBag[e.field] = [];
|
|
1959
|
+
}
|
|
1960
|
+
errorBag[e.field]?.push(e.message);
|
|
1961
|
+
}
|
|
1962
|
+
session.flash("errors", errorBag);
|
|
1963
|
+
if (err.input) {
|
|
1964
|
+
session.flash("_old_input", err.input);
|
|
1965
|
+
}
|
|
1966
|
+
const redirectUrl = err.redirectTo ?? c.req.header("Referer") ?? "/";
|
|
1967
|
+
return c.redirect(redirectUrl);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
} else if (err instanceof Error && !isProduction && err.cause) {
|
|
1971
|
+
details = { cause: err.cause };
|
|
1972
|
+
}
|
|
1973
|
+
} else if (err instanceof HttpException) {
|
|
1974
|
+
status = err.status;
|
|
1975
|
+
message = err.message;
|
|
1976
|
+
} else if (err instanceof Error && "status" in err && typeof err.status === "number") {
|
|
1977
|
+
status = err.status;
|
|
1978
|
+
message = err.message;
|
|
1979
|
+
code = codeFromStatus(status);
|
|
1980
|
+
} else if (err instanceof Error) {
|
|
1981
|
+
if (!isProduction) {
|
|
1982
|
+
message = err.message || message;
|
|
1983
|
+
}
|
|
1984
|
+
} else if (typeof err === "string") {
|
|
1985
|
+
if (!isProduction) {
|
|
1986
|
+
message = err;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (isProduction && status >= 500) {
|
|
1990
|
+
message = messageFromStatus(status);
|
|
1991
|
+
}
|
|
1992
|
+
if (!isProduction && err instanceof Error && !details) {
|
|
1993
|
+
details = { stack: err.stack, ...details };
|
|
1994
|
+
}
|
|
1995
|
+
let handlerContext = {
|
|
1996
|
+
core: this,
|
|
1997
|
+
c,
|
|
1998
|
+
error: err,
|
|
1999
|
+
isProduction,
|
|
2000
|
+
accept,
|
|
2001
|
+
wantsHtml,
|
|
2002
|
+
status,
|
|
2003
|
+
payload: fail(message, code, details),
|
|
2004
|
+
...wantsHtml ? {
|
|
2005
|
+
html: {
|
|
2006
|
+
templates: status === 500 ? ["errors/500"] : [`errors/${status}`, "errors/500"],
|
|
2007
|
+
data: {
|
|
2008
|
+
status,
|
|
2009
|
+
message,
|
|
2010
|
+
code,
|
|
2011
|
+
error: !isProduction && err instanceof Error ? err.stack : undefined,
|
|
2012
|
+
debug: !isProduction,
|
|
2013
|
+
details
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
} : {}
|
|
2017
|
+
};
|
|
2018
|
+
handlerContext = await this.hooks.applyFilters("error:context", handlerContext);
|
|
2019
|
+
const defaultLogLevel = handlerContext.status >= 500 ? "error" : "none";
|
|
2020
|
+
const logLevel = handlerContext.logLevel ?? defaultLogLevel;
|
|
2021
|
+
if (logLevel !== "none") {
|
|
2022
|
+
const rawErrorMessage = handlerContext.error instanceof Error ? handlerContext.error.message : typeof handlerContext.error === "string" ? handlerContext.error : handlerContext.payload.error.message;
|
|
2023
|
+
const msg = handlerContext.logMessage ?? (logLevel === "error" ? `Application Error: ${rawErrorMessage || handlerContext.payload.error.message}` : `HTTP ${handlerContext.status}: ${handlerContext.payload.error.message}`);
|
|
2024
|
+
if (logLevel === "error") {
|
|
2025
|
+
this.logger.error(msg, err);
|
|
2026
|
+
} else if (logLevel === "warn") {
|
|
2027
|
+
this.logger.warn(msg);
|
|
2028
|
+
} else {
|
|
2029
|
+
this.logger.info(msg);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
await this.hooks.doAction("error:report", handlerContext);
|
|
2033
|
+
const customResponse = await this.hooks.applyFilters("error:render", null, handlerContext);
|
|
2034
|
+
if (customResponse) {
|
|
2035
|
+
return customResponse;
|
|
2036
|
+
}
|
|
2037
|
+
if (handlerContext.wantsHtml && view && handlerContext.html) {
|
|
2038
|
+
let lastRenderError;
|
|
2039
|
+
for (const template of handlerContext.html.templates) {
|
|
2040
|
+
try {
|
|
2041
|
+
return c.html(view.render(template, handlerContext.html.data), handlerContext.status);
|
|
2042
|
+
} catch (renderError) {
|
|
2043
|
+
lastRenderError = renderError;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
this.logger.error("Failed to render error view", lastRenderError);
|
|
2047
|
+
}
|
|
2048
|
+
return c.json(handlerContext.payload, handlerContext.status);
|
|
2049
|
+
});
|
|
2050
|
+
this.adapter.onNotFound(async (c) => {
|
|
2051
|
+
const view = c.get("view");
|
|
2052
|
+
const accept = c.req.header("Accept") || "";
|
|
2053
|
+
const wantsHtml = view && accept.includes("text/html") && !accept.includes("application/json");
|
|
2054
|
+
const isProduction = false;
|
|
2055
|
+
let handlerContext = {
|
|
2056
|
+
core: this,
|
|
2057
|
+
c,
|
|
2058
|
+
error: new HttpException(404, { message: "Route not found" }),
|
|
2059
|
+
isProduction,
|
|
2060
|
+
accept,
|
|
2061
|
+
wantsHtml: Boolean(wantsHtml),
|
|
2062
|
+
status: 404,
|
|
2063
|
+
payload: fail("Route not found", "NOT_FOUND"),
|
|
2064
|
+
...wantsHtml ? {
|
|
2065
|
+
html: {
|
|
2066
|
+
templates: ["errors/404", "errors/500"],
|
|
2067
|
+
data: {
|
|
2068
|
+
status: 404,
|
|
2069
|
+
message: "Route not found",
|
|
2070
|
+
code: "NOT_FOUND",
|
|
2071
|
+
debug: !isProduction
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
} : {}
|
|
2075
|
+
};
|
|
2076
|
+
handlerContext = await this.hooks.applyFilters("notFound:context", handlerContext);
|
|
2077
|
+
const logLevel = handlerContext.logLevel ?? "info";
|
|
2078
|
+
if (logLevel !== "none") {
|
|
2079
|
+
const msg = handlerContext.logMessage ?? `404 Not Found: ${c.req.url}`;
|
|
2080
|
+
if (logLevel === "error") {
|
|
2081
|
+
this.logger.error(msg);
|
|
2082
|
+
} else if (logLevel === "warn") {
|
|
2083
|
+
this.logger.warn(msg);
|
|
2084
|
+
} else {
|
|
2085
|
+
this.logger.info(msg);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
await this.hooks.doAction("notFound:report", handlerContext);
|
|
2089
|
+
const customResponse = await this.hooks.applyFilters("notFound:render", null, handlerContext);
|
|
2090
|
+
if (customResponse) {
|
|
2091
|
+
return customResponse;
|
|
2092
|
+
}
|
|
2093
|
+
if (handlerContext.wantsHtml && view && handlerContext.html) {
|
|
2094
|
+
let lastRenderError;
|
|
2095
|
+
for (const template of handlerContext.html.templates) {
|
|
2096
|
+
try {
|
|
2097
|
+
return c.html(view.render(template, handlerContext.html.data), handlerContext.status);
|
|
2098
|
+
} catch (renderError) {
|
|
2099
|
+
lastRenderError = renderError;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
this.logger.error("Failed to render 404 view", lastRenderError);
|
|
2103
|
+
}
|
|
2104
|
+
return c.json(handlerContext.payload, handlerContext.status);
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
async orbit(orbit) {
|
|
2108
|
+
const instance = typeof orbit === "function" ? new orbit : orbit;
|
|
2109
|
+
await instance.install(this);
|
|
2110
|
+
return this;
|
|
2111
|
+
}
|
|
2112
|
+
async use(satellite) {
|
|
2113
|
+
if (typeof satellite === "function") {
|
|
2114
|
+
await satellite(this);
|
|
2115
|
+
} else {
|
|
2116
|
+
this.register(satellite);
|
|
2117
|
+
}
|
|
2118
|
+
return this;
|
|
2119
|
+
}
|
|
2120
|
+
registerGlobalErrorHandlers(options = {}) {
|
|
2121
|
+
return registerGlobalErrorHandlers({ ...options, core: this });
|
|
2122
|
+
}
|
|
2123
|
+
static async boot(config) {
|
|
2124
|
+
const core = new PlanetCore({
|
|
2125
|
+
...config.logger && { logger: config.logger },
|
|
2126
|
+
...config.config && { config: config.config },
|
|
2127
|
+
...config.adapter && { adapter: config.adapter }
|
|
2128
|
+
});
|
|
2129
|
+
if (config.orbits) {
|
|
2130
|
+
for (const OrbitClassOrInstance of config.orbits) {
|
|
2131
|
+
let orbit;
|
|
2132
|
+
if (typeof OrbitClassOrInstance === "function") {
|
|
2133
|
+
orbit = new OrbitClassOrInstance;
|
|
2134
|
+
} else {
|
|
2135
|
+
orbit = OrbitClassOrInstance;
|
|
2136
|
+
}
|
|
2137
|
+
await orbit.install(core);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
return core;
|
|
2141
|
+
}
|
|
2142
|
+
mountOrbit(path, orbitApp) {
|
|
2143
|
+
this.logger.info(`Mounting orbit at path: ${path}`);
|
|
2144
|
+
if (this.adapter.name === "hono") {
|
|
2145
|
+
this.adapter.native.route(path, orbitApp);
|
|
2146
|
+
} else {
|
|
2147
|
+
const subAdapter = new HonoAdapter({}, orbitApp);
|
|
2148
|
+
this.adapter.mount(path, subAdapter);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
liftoff(port) {
|
|
2152
|
+
const finalPort = port ?? this.config.get("PORT", 3000);
|
|
2153
|
+
this.hooks.doAction("app:liftoff", { port: finalPort });
|
|
2154
|
+
this.logger.info(`Ready to liftoff on port ${finalPort} \uD83D\uDE80`);
|
|
2155
|
+
return {
|
|
2156
|
+
port: finalPort,
|
|
2157
|
+
fetch: this.adapter.fetch.bind(this.adapter),
|
|
2158
|
+
core: this
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// ../core/src/index.ts
|
|
2164
|
+
var VERSION = package_default.version;
|
|
2165
|
+
|
|
2166
|
+
// ../ion/src/InertiaService.ts
|
|
2167
|
+
class InertiaService {
|
|
2168
|
+
context;
|
|
2169
|
+
config;
|
|
2170
|
+
sharedProps = {};
|
|
2171
|
+
constructor(context, config = {}) {
|
|
2172
|
+
this.context = context;
|
|
2173
|
+
this.config = config;
|
|
2174
|
+
}
|
|
2175
|
+
escapeForSingleQuotedHtmlAttribute(value) {
|
|
2176
|
+
return value.replace(/&/g, "&").replace(/\\"/g, "\\"").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'");
|
|
2177
|
+
}
|
|
2178
|
+
render(component, props = {}, rootVars = {}) {
|
|
2179
|
+
let pageUrl;
|
|
2180
|
+
try {
|
|
2181
|
+
const reqUrl = new URL(this.context.req.url, "http://localhost");
|
|
2182
|
+
pageUrl = reqUrl.pathname + reqUrl.search;
|
|
2183
|
+
} catch {
|
|
2184
|
+
pageUrl = this.context.req.url;
|
|
2185
|
+
}
|
|
2186
|
+
const resolveProps = (p) => {
|
|
2187
|
+
const resolved = {};
|
|
2188
|
+
for (const [key, value] of Object.entries(p)) {
|
|
2189
|
+
resolved[key] = typeof value === "function" ? value() : value;
|
|
2190
|
+
}
|
|
2191
|
+
return resolved;
|
|
2192
|
+
};
|
|
2193
|
+
const page = {
|
|
2194
|
+
component,
|
|
2195
|
+
props: resolveProps({ ...this.sharedProps, ...props }),
|
|
2196
|
+
url: pageUrl,
|
|
2197
|
+
version: this.config.version
|
|
2198
|
+
};
|
|
2199
|
+
if (this.context.req.header("X-Inertia")) {
|
|
2200
|
+
this.context.header("X-Inertia", "true");
|
|
2201
|
+
this.context.header("Vary", "Accept");
|
|
2202
|
+
return this.context.json(page);
|
|
2203
|
+
}
|
|
2204
|
+
const view = this.context.get("view");
|
|
2205
|
+
const rootView = this.config.rootView ?? "app";
|
|
2206
|
+
if (!view) {
|
|
2207
|
+
throw new Error("OrbitPrism is required for the initial page load in OrbitIon");
|
|
2208
|
+
}
|
|
2209
|
+
const isDev = true;
|
|
2210
|
+
return this.context.html(view.render(rootView, {
|
|
2211
|
+
...rootVars,
|
|
2212
|
+
page: this.escapeForSingleQuotedHtmlAttribute(JSON.stringify(page)),
|
|
2213
|
+
isDev
|
|
2214
|
+
}, { layout: "" }));
|
|
2215
|
+
}
|
|
2216
|
+
share(key, value) {
|
|
2217
|
+
this.sharedProps[key] = value;
|
|
2218
|
+
}
|
|
2219
|
+
shareAll(props) {
|
|
2220
|
+
Object.assign(this.sharedProps, props);
|
|
2221
|
+
}
|
|
2222
|
+
getSharedProps() {
|
|
2223
|
+
return { ...this.sharedProps };
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
// ../ion/src/index.ts
|
|
2228
|
+
class OrbitIon {
|
|
2229
|
+
install(core) {
|
|
2230
|
+
core.logger.info("\uD83D\uDEF0️ Orbit Inertia installed");
|
|
2231
|
+
const appVersion = core.config.get("APP_VERSION", "1.0.0");
|
|
2232
|
+
core.adapter.use("*", async (c, next) => {
|
|
2233
|
+
const gravitoCtx = new HonoContextWrapper(c);
|
|
2234
|
+
const inertia = new InertiaService(gravitoCtx, {
|
|
2235
|
+
version: String(appVersion),
|
|
2236
|
+
rootView: "app"
|
|
2237
|
+
});
|
|
2238
|
+
c.set("inertia", inertia);
|
|
2239
|
+
await next();
|
|
2240
|
+
return;
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
var src_default = OrbitIon;
|
|
2245
|
+
export {
|
|
2246
|
+
src_default as default,
|
|
2247
|
+
OrbitIon,
|
|
2248
|
+
InertiaService
|
|
2249
|
+
};
|