@buenojs/bueno 0.8.3 → 0.8.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.md +136 -16
- package/dist/cli/{index.js → bin.js} +3036 -1421
- package/dist/container/index.js +250 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +11043 -6482
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3346 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +776 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/package.json +121 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +392 -438
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +61 -0
- package/src/cli/templates/database/mysql.ts +14 -0
- package/src/cli/templates/database/none.ts +16 -0
- package/src/cli/templates/database/postgresql.ts +14 -0
- package/src/cli/templates/database/sqlite.ts +14 -0
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +63 -0
- package/src/cli/templates/frontend/none.ts +17 -0
- package/src/cli/templates/frontend/react.ts +140 -0
- package/src/cli/templates/frontend/solid.ts +134 -0
- package/src/cli/templates/frontend/svelte.ts +131 -0
- package/src/cli/templates/frontend/vue.ts +130 -0
- package/src/cli/templates/generators/index.ts +339 -0
- package/src/cli/templates/generators/types.ts +56 -0
- package/src/cli/templates/index.ts +35 -2
- package/src/cli/templates/project/api.ts +81 -0
- package/src/cli/templates/project/default.ts +140 -0
- package/src/cli/templates/project/fullstack.ts +111 -0
- package/src/cli/templates/project/index.ts +95 -0
- package/src/cli/templates/project/minimal.ts +45 -0
- package/src/cli/templates/project/types.ts +94 -0
- package/src/cli/templates/project/website.ts +263 -0
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +47 -0
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +545 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +179 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +409 -298
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
set: (newValue) => all[name] = () => newValue
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
var __legacyDecorateClassTS = function(decorators, target, key, desc) {
|
|
13
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
14
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
15
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
16
|
+
else
|
|
17
|
+
for (var i = decorators.length - 1;i >= 0; i--)
|
|
18
|
+
if (d = decorators[i])
|
|
19
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
20
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
21
|
+
};
|
|
22
|
+
var __legacyMetadataTS = (k, v) => {
|
|
23
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
|
|
24
|
+
return Reflect.metadata(k, v);
|
|
25
|
+
};
|
|
26
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
27
|
+
var __require = import.meta.require;
|
|
28
|
+
|
|
29
|
+
// src/health/index.ts
|
|
30
|
+
class HealthCheckManager {
|
|
31
|
+
checks = new Map;
|
|
32
|
+
startTime = Date.now();
|
|
33
|
+
version;
|
|
34
|
+
constructor(version) {
|
|
35
|
+
this.version = version ?? "0.1.0";
|
|
36
|
+
}
|
|
37
|
+
registerCheck(name, checkFn, options = {}) {
|
|
38
|
+
this.checks.set(name, {
|
|
39
|
+
name,
|
|
40
|
+
fn: checkFn,
|
|
41
|
+
options: {
|
|
42
|
+
critical: options.critical ?? true,
|
|
43
|
+
timeout: options.timeout ?? 5000,
|
|
44
|
+
description: options.description ?? ""
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
removeCheck(name) {
|
|
50
|
+
return this.checks.delete(name);
|
|
51
|
+
}
|
|
52
|
+
getCheckNames() {
|
|
53
|
+
return Array.from(this.checks.keys());
|
|
54
|
+
}
|
|
55
|
+
hasCheck(name) {
|
|
56
|
+
return this.checks.has(name);
|
|
57
|
+
}
|
|
58
|
+
async runSingleCheck(check) {
|
|
59
|
+
const start = Date.now();
|
|
60
|
+
try {
|
|
61
|
+
const result = await Promise.race([
|
|
62
|
+
check.fn(),
|
|
63
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Check timed out after ${check.options.timeout}ms`)), check.options.timeout))
|
|
64
|
+
]);
|
|
65
|
+
return {
|
|
66
|
+
...result,
|
|
67
|
+
latency: Date.now() - start
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
status: "unhealthy",
|
|
72
|
+
latency: Date.now() - start,
|
|
73
|
+
message: error instanceof Error ? error.message : "Check failed"
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async runChecks() {
|
|
78
|
+
const results = {};
|
|
79
|
+
const entries = Array.from(this.checks.values());
|
|
80
|
+
const outcomes = await Promise.all(entries.map(async (check) => ({
|
|
81
|
+
name: check.name,
|
|
82
|
+
result: await this.runSingleCheck(check),
|
|
83
|
+
critical: check.options.critical
|
|
84
|
+
})));
|
|
85
|
+
for (const { name, result } of outcomes) {
|
|
86
|
+
results[name] = result;
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
getHealth() {
|
|
91
|
+
return {
|
|
92
|
+
status: "healthy",
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
version: this.version,
|
|
95
|
+
uptime: Math.floor((Date.now() - this.startTime) / 1000)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async getReadiness() {
|
|
99
|
+
const checks = await this.runChecks();
|
|
100
|
+
let status = "healthy";
|
|
101
|
+
let hasUnhealthyCritical = false;
|
|
102
|
+
let hasUnhealthyNonCritical = false;
|
|
103
|
+
let hasDegraded = false;
|
|
104
|
+
const entries = Array.from(this.checks.values());
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
const checkResult = checks[entry.name];
|
|
107
|
+
if (checkResult) {
|
|
108
|
+
if (entry.options.critical && checkResult.status === "unhealthy") {
|
|
109
|
+
hasUnhealthyCritical = true;
|
|
110
|
+
}
|
|
111
|
+
if (!entry.options.critical && checkResult.status === "unhealthy") {
|
|
112
|
+
hasUnhealthyNonCritical = true;
|
|
113
|
+
}
|
|
114
|
+
if (checkResult.status === "degraded") {
|
|
115
|
+
hasDegraded = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (hasUnhealthyCritical) {
|
|
120
|
+
status = "unhealthy";
|
|
121
|
+
} else if (hasUnhealthyNonCritical || hasDegraded) {
|
|
122
|
+
status = "degraded";
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
status,
|
|
126
|
+
timestamp: new Date().toISOString(),
|
|
127
|
+
version: this.version,
|
|
128
|
+
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
|
129
|
+
checks
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
getUptime() {
|
|
133
|
+
return Math.floor((Date.now() - this.startTime) / 1000);
|
|
134
|
+
}
|
|
135
|
+
resetStartTime() {
|
|
136
|
+
this.startTime = Date.now();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function createHealthMiddleware(options = {}) {
|
|
140
|
+
const {
|
|
141
|
+
healthPath = "/health",
|
|
142
|
+
readyPath = "/ready",
|
|
143
|
+
exposeMetrics = true,
|
|
144
|
+
checks = {},
|
|
145
|
+
version
|
|
146
|
+
} = options;
|
|
147
|
+
const manager = new HealthCheckManager(version);
|
|
148
|
+
for (const [name, check] of Object.entries(checks)) {
|
|
149
|
+
if (typeof check === "function") {
|
|
150
|
+
manager.registerCheck(name, check);
|
|
151
|
+
} else {
|
|
152
|
+
manager.registerCheck(name, check.fn, check.options);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const middleware = async (context, next) => {
|
|
156
|
+
const path = context.path;
|
|
157
|
+
if (path === healthPath && context.method === "GET") {
|
|
158
|
+
const health = manager.getHealth();
|
|
159
|
+
return context.json(health);
|
|
160
|
+
}
|
|
161
|
+
if (path === readyPath && context.method === "GET") {
|
|
162
|
+
const readiness = await manager.getReadiness();
|
|
163
|
+
if (readiness.status === "unhealthy") {
|
|
164
|
+
context.status(503);
|
|
165
|
+
} else if (readiness.status === "degraded") {
|
|
166
|
+
context.status(200);
|
|
167
|
+
}
|
|
168
|
+
const response = exposeMetrics ? readiness : { status: readiness.status, timestamp: readiness.timestamp };
|
|
169
|
+
return context.json(response);
|
|
170
|
+
}
|
|
171
|
+
return next();
|
|
172
|
+
};
|
|
173
|
+
return { middleware, manager };
|
|
174
|
+
}
|
|
175
|
+
function createDatabaseCheck(db, options = {}) {
|
|
176
|
+
const { query = "SELECT 1" } = options;
|
|
177
|
+
return async () => {
|
|
178
|
+
const start = Date.now();
|
|
179
|
+
try {
|
|
180
|
+
if (typeof db.healthCheck === "function") {
|
|
181
|
+
const isHealthy = await db.healthCheck();
|
|
182
|
+
return {
|
|
183
|
+
status: isHealthy ? "healthy" : "unhealthy",
|
|
184
|
+
latency: Date.now() - start,
|
|
185
|
+
message: isHealthy ? "Database connection OK" : "Database health check failed"
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (typeof db.query === "function") {
|
|
189
|
+
await db.query(query);
|
|
190
|
+
return {
|
|
191
|
+
status: "healthy",
|
|
192
|
+
latency: Date.now() - start,
|
|
193
|
+
message: "Database connection OK"
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (typeof db.execute === "function") {
|
|
197
|
+
await db.execute(query);
|
|
198
|
+
return {
|
|
199
|
+
status: "healthy",
|
|
200
|
+
latency: Date.now() - start,
|
|
201
|
+
message: "Database connection OK"
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
status: "degraded",
|
|
206
|
+
latency: Date.now() - start,
|
|
207
|
+
message: "No compatible database method found"
|
|
208
|
+
};
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return {
|
|
211
|
+
status: "unhealthy",
|
|
212
|
+
latency: Date.now() - start,
|
|
213
|
+
message: error instanceof Error ? error.message : "Database check failed"
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function createCacheCheck(cache, options = {}) {
|
|
219
|
+
const { testKey = "__health_check__" } = options;
|
|
220
|
+
return async () => {
|
|
221
|
+
const start = Date.now();
|
|
222
|
+
try {
|
|
223
|
+
if (typeof cache.healthCheck === "function") {
|
|
224
|
+
const isHealthy = await cache.healthCheck();
|
|
225
|
+
return {
|
|
226
|
+
status: isHealthy ? "healthy" : "unhealthy",
|
|
227
|
+
latency: Date.now() - start,
|
|
228
|
+
message: isHealthy ? "Cache connection OK" : "Cache health check failed"
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (typeof cache.ping === "function") {
|
|
232
|
+
await cache.ping();
|
|
233
|
+
return {
|
|
234
|
+
status: "healthy",
|
|
235
|
+
latency: Date.now() - start,
|
|
236
|
+
message: "Cache ping OK"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (typeof cache.set === "function" && typeof cache.get === "function") {
|
|
240
|
+
const testValue = Date.now().toString();
|
|
241
|
+
await cache.set(testKey, testValue);
|
|
242
|
+
const retrieved = await cache.get(testKey);
|
|
243
|
+
if (retrieved === testValue || retrieved?.toString() === testValue) {
|
|
244
|
+
return {
|
|
245
|
+
status: "healthy",
|
|
246
|
+
latency: Date.now() - start,
|
|
247
|
+
message: "Cache read/write OK"
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
status: "degraded",
|
|
252
|
+
latency: Date.now() - start,
|
|
253
|
+
message: "Cache read/write mismatch"
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
status: "degraded",
|
|
258
|
+
latency: Date.now() - start,
|
|
259
|
+
message: "No compatible cache method found"
|
|
260
|
+
};
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return {
|
|
263
|
+
status: "unhealthy",
|
|
264
|
+
latency: Date.now() - start,
|
|
265
|
+
message: error instanceof Error ? error.message : "Cache check failed"
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function createCustomCheck(checkFn, options = {}) {
|
|
271
|
+
const { message = "Custom check" } = options;
|
|
272
|
+
return async () => {
|
|
273
|
+
const start = Date.now();
|
|
274
|
+
try {
|
|
275
|
+
const result = await checkFn();
|
|
276
|
+
return {
|
|
277
|
+
status: result ? "healthy" : "unhealthy",
|
|
278
|
+
latency: Date.now() - start,
|
|
279
|
+
message: result ? `${message} OK` : `${message} failed`
|
|
280
|
+
};
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return {
|
|
283
|
+
status: "unhealthy",
|
|
284
|
+
latency: Date.now() - start,
|
|
285
|
+
message: error instanceof Error ? error.message : `${message} failed`
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function createTCPCheck(host, port, options = {}) {
|
|
291
|
+
const { timeout = 5000 } = options;
|
|
292
|
+
return async () => {
|
|
293
|
+
const start = Date.now();
|
|
294
|
+
try {
|
|
295
|
+
const socket = await Bun.connect({
|
|
296
|
+
hostname: host,
|
|
297
|
+
port,
|
|
298
|
+
socket: {
|
|
299
|
+
data() {},
|
|
300
|
+
error() {}
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
socket.end();
|
|
304
|
+
return {
|
|
305
|
+
status: "healthy",
|
|
306
|
+
latency: Date.now() - start,
|
|
307
|
+
message: `TCP ${host}:${port} reachable`
|
|
308
|
+
};
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return {
|
|
311
|
+
status: "unhealthy",
|
|
312
|
+
latency: Date.now() - start,
|
|
313
|
+
message: `TCP ${host}:${port} unreachable: ${error instanceof Error ? error.message : "unknown error"}`
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function createHTTPCheck(url, options = {}) {
|
|
319
|
+
const { expectedStatus = 200, timeout = 5000, headers = {} } = options;
|
|
320
|
+
return async () => {
|
|
321
|
+
const start = Date.now();
|
|
322
|
+
try {
|
|
323
|
+
const controller = new AbortController;
|
|
324
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
325
|
+
const response = await fetch(url, {
|
|
326
|
+
method: "GET",
|
|
327
|
+
headers,
|
|
328
|
+
signal: controller.signal
|
|
329
|
+
});
|
|
330
|
+
clearTimeout(timeoutId);
|
|
331
|
+
if (response.status === expectedStatus) {
|
|
332
|
+
return {
|
|
333
|
+
status: "healthy",
|
|
334
|
+
latency: Date.now() - start,
|
|
335
|
+
message: `HTTP ${url} returned ${response.status}`
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
status: "unhealthy",
|
|
340
|
+
latency: Date.now() - start,
|
|
341
|
+
message: `HTTP ${url} returned ${response.status}, expected ${expectedStatus}`
|
|
342
|
+
};
|
|
343
|
+
} catch (error) {
|
|
344
|
+
return {
|
|
345
|
+
status: "unhealthy",
|
|
346
|
+
latency: Date.now() - start,
|
|
347
|
+
message: `HTTP ${url} failed: ${error instanceof Error ? error.message : "unknown error"}`
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function createHealthManager(version) {
|
|
353
|
+
return new HealthCheckManager(version);
|
|
354
|
+
}
|
|
355
|
+
export {
|
|
356
|
+
createTCPCheck,
|
|
357
|
+
createHealthMiddleware,
|
|
358
|
+
createHealthManager,
|
|
359
|
+
createHTTPCheck,
|
|
360
|
+
createDatabaseCheck,
|
|
361
|
+
createCustomCheck,
|
|
362
|
+
createCacheCheck,
|
|
363
|
+
HealthCheckManager
|
|
364
|
+
};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
set: (newValue) => all[name] = () => newValue
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
var __legacyDecorateClassTS = function(decorators, target, key, desc) {
|
|
13
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
14
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
15
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
16
|
+
else
|
|
17
|
+
for (var i = decorators.length - 1;i >= 0; i--)
|
|
18
|
+
if (d = decorators[i])
|
|
19
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
20
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
21
|
+
};
|
|
22
|
+
var __legacyMetadataTS = (k, v) => {
|
|
23
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
|
|
24
|
+
return Reflect.metadata(k, v);
|
|
25
|
+
};
|
|
26
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
27
|
+
var __require = import.meta.require;
|
|
28
|
+
|
|
29
|
+
// src/i18n/loader.ts
|
|
30
|
+
import { existsSync, readFileSync, watch } from "fs";
|
|
31
|
+
import { join, resolve } from "path";
|
|
32
|
+
function flattenTranslations(obj, prefix = "", result = new Map) {
|
|
33
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
34
|
+
const dotKey = prefix ? `${prefix}.${key}` : key;
|
|
35
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
36
|
+
flattenTranslations(value, dotKey, result);
|
|
37
|
+
} else {
|
|
38
|
+
result.set(dotKey, String(value ?? ""));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class TranslationLoader {
|
|
45
|
+
cache = new Map;
|
|
46
|
+
watchers = new Map;
|
|
47
|
+
config;
|
|
48
|
+
constructor(config) {
|
|
49
|
+
this.config = config;
|
|
50
|
+
}
|
|
51
|
+
load(locale) {
|
|
52
|
+
const cached = this.cache.get(locale);
|
|
53
|
+
if (cached)
|
|
54
|
+
return cached;
|
|
55
|
+
return this._loadFromDisk(locale);
|
|
56
|
+
}
|
|
57
|
+
preload() {
|
|
58
|
+
for (const locale of this.config.supportedLocales) {
|
|
59
|
+
try {
|
|
60
|
+
this._loadFromDisk(locale);
|
|
61
|
+
} catch {
|
|
62
|
+
if (locale !== this.config.defaultLocale) {
|
|
63
|
+
this.cache.set(locale, {
|
|
64
|
+
locale,
|
|
65
|
+
translations: new Map,
|
|
66
|
+
loadedAt: Date.now()
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
invalidate(locale) {
|
|
73
|
+
this.cache.delete(locale);
|
|
74
|
+
}
|
|
75
|
+
loadedLocales() {
|
|
76
|
+
return Array.from(this.cache.keys());
|
|
77
|
+
}
|
|
78
|
+
watch(locale) {
|
|
79
|
+
const filePath = this._resolvePath(locale);
|
|
80
|
+
if (!existsSync(filePath))
|
|
81
|
+
return;
|
|
82
|
+
if (this.watchers.has(locale))
|
|
83
|
+
return;
|
|
84
|
+
const watcher = watch(filePath, () => {
|
|
85
|
+
this.invalidate(locale);
|
|
86
|
+
try {
|
|
87
|
+
this._loadFromDisk(locale);
|
|
88
|
+
} catch {}
|
|
89
|
+
});
|
|
90
|
+
this.watchers.set(locale, watcher);
|
|
91
|
+
}
|
|
92
|
+
stopWatching() {
|
|
93
|
+
for (const watcher of this.watchers.values()) {
|
|
94
|
+
watcher.close();
|
|
95
|
+
}
|
|
96
|
+
this.watchers.clear();
|
|
97
|
+
}
|
|
98
|
+
_resolvePath(locale) {
|
|
99
|
+
return resolve(join(this.config.basePath, `${locale}.json`));
|
|
100
|
+
}
|
|
101
|
+
_loadFromDisk(locale) {
|
|
102
|
+
const filePath = this._resolvePath(locale);
|
|
103
|
+
if (!existsSync(filePath)) {
|
|
104
|
+
if (locale === this.config.defaultLocale) {
|
|
105
|
+
throw new Error(`[i18n] Default locale file not found: ${filePath}. ` + `Create ${locale}.json in ${this.config.basePath}`);
|
|
106
|
+
}
|
|
107
|
+
const empty = {
|
|
108
|
+
locale,
|
|
109
|
+
translations: new Map,
|
|
110
|
+
loadedAt: Date.now()
|
|
111
|
+
};
|
|
112
|
+
this.cache.set(locale, empty);
|
|
113
|
+
return empty;
|
|
114
|
+
}
|
|
115
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
116
|
+
let parsed;
|
|
117
|
+
try {
|
|
118
|
+
parsed = JSON.parse(raw);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
throw new Error(`[i18n] Failed to parse locale file ${filePath}: ${String(err)}`);
|
|
121
|
+
}
|
|
122
|
+
const bundle = {
|
|
123
|
+
locale,
|
|
124
|
+
translations: flattenTranslations(parsed),
|
|
125
|
+
loadedAt: Date.now()
|
|
126
|
+
};
|
|
127
|
+
this.cache.set(locale, bundle);
|
|
128
|
+
return bundle;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/i18n/negotiator.ts
|
|
133
|
+
function parseAcceptLanguage(header) {
|
|
134
|
+
if (!header.trim())
|
|
135
|
+
return [];
|
|
136
|
+
return header.split(",").map((part) => {
|
|
137
|
+
const [localeRaw, qRaw] = part.trim().split(";");
|
|
138
|
+
const locale = localeRaw?.trim() ?? "";
|
|
139
|
+
const quality = qRaw ? Number.parseFloat(qRaw.trim().replace("q=", "")) : 1;
|
|
140
|
+
return { locale, quality: isNaN(quality) ? 1 : quality };
|
|
141
|
+
}).filter((e) => e.locale.length > 0).sort((a, b) => b.quality - a.quality);
|
|
142
|
+
}
|
|
143
|
+
function normaliseLocale(locale) {
|
|
144
|
+
const parts = locale.replace("_", "-").split("-");
|
|
145
|
+
if (parts.length === 0)
|
|
146
|
+
return locale;
|
|
147
|
+
const lang = parts[0].toLowerCase();
|
|
148
|
+
if (parts.length === 1)
|
|
149
|
+
return lang;
|
|
150
|
+
const region = parts[1].toUpperCase();
|
|
151
|
+
return `${lang}-${region}`;
|
|
152
|
+
}
|
|
153
|
+
function languageSubtag(locale) {
|
|
154
|
+
return locale.split("-")[0].toLowerCase();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
class LocaleNegotiator {
|
|
158
|
+
supported;
|
|
159
|
+
defaultLocale;
|
|
160
|
+
constructor(supportedLocales, defaultLocale) {
|
|
161
|
+
this.supported = supportedLocales;
|
|
162
|
+
this.defaultLocale = defaultLocale;
|
|
163
|
+
}
|
|
164
|
+
negotiate(acceptLanguageHeader) {
|
|
165
|
+
const entries = parseAcceptLanguage(acceptLanguageHeader);
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
const norm = normaliseLocale(entry.locale);
|
|
168
|
+
const exact = this.supported.find((s) => normaliseLocale(s) === norm);
|
|
169
|
+
if (exact) {
|
|
170
|
+
return { locale: exact, strategy: "exact", source: entry.locale };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const entry of entries) {
|
|
174
|
+
const lang = languageSubtag(entry.locale);
|
|
175
|
+
const prefix = this.supported.find((s) => languageSubtag(s) === lang);
|
|
176
|
+
if (prefix) {
|
|
177
|
+
return { locale: prefix, strategy: "prefix", source: entry.locale };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
locale: this.defaultLocale,
|
|
182
|
+
strategy: "default",
|
|
183
|
+
source: ""
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
isSupported(locale) {
|
|
187
|
+
return this.supported.includes(locale);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/i18n/engine.ts
|
|
192
|
+
var DEFAULT_I18N_CONFIG = {
|
|
193
|
+
defaultLocale: "en",
|
|
194
|
+
supportedLocales: ["en"],
|
|
195
|
+
basePath: "resources/i18n",
|
|
196
|
+
fallbackToDefault: true,
|
|
197
|
+
cookieName: "bueno_locale",
|
|
198
|
+
cookieMaxAge: 31536000
|
|
199
|
+
};
|
|
200
|
+
function selectPluralKey(count, availableKeys, base) {
|
|
201
|
+
const candidates = count === 0 ? ["zero", "other"] : count === 1 ? ["one", "other"] : ["other"];
|
|
202
|
+
for (const form of candidates) {
|
|
203
|
+
const candidate = `${base}_${form}`;
|
|
204
|
+
if (availableKeys.has(candidate))
|
|
205
|
+
return candidate;
|
|
206
|
+
}
|
|
207
|
+
return base;
|
|
208
|
+
}
|
|
209
|
+
function interpolate(template, params) {
|
|
210
|
+
return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => {
|
|
211
|
+
const val = params[key];
|
|
212
|
+
return val === undefined || val === null ? "" : String(val);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
class I18n {
|
|
217
|
+
loader;
|
|
218
|
+
negotiator;
|
|
219
|
+
config;
|
|
220
|
+
metrics = {
|
|
221
|
+
totalLookups: 0,
|
|
222
|
+
hits: 0,
|
|
223
|
+
fallbacks: 0,
|
|
224
|
+
misses: 0,
|
|
225
|
+
loadedLocales: []
|
|
226
|
+
};
|
|
227
|
+
constructor(config = {}) {
|
|
228
|
+
this.config = {
|
|
229
|
+
defaultLocale: config.defaultLocale ?? DEFAULT_I18N_CONFIG.defaultLocale,
|
|
230
|
+
supportedLocales: config.supportedLocales ?? DEFAULT_I18N_CONFIG.supportedLocales,
|
|
231
|
+
basePath: config.basePath ?? DEFAULT_I18N_CONFIG.basePath,
|
|
232
|
+
fallbackToDefault: config.fallbackToDefault ?? DEFAULT_I18N_CONFIG.fallbackToDefault,
|
|
233
|
+
cookieName: config.cookieName ?? DEFAULT_I18N_CONFIG.cookieName,
|
|
234
|
+
cookieMaxAge: config.cookieMaxAge ?? DEFAULT_I18N_CONFIG.cookieMaxAge
|
|
235
|
+
};
|
|
236
|
+
this.loader = new TranslationLoader(this.config);
|
|
237
|
+
this.negotiator = new LocaleNegotiator(this.config.supportedLocales, this.config.defaultLocale);
|
|
238
|
+
}
|
|
239
|
+
preload() {
|
|
240
|
+
this.loader.preload();
|
|
241
|
+
}
|
|
242
|
+
watchAll() {
|
|
243
|
+
for (const locale of this.config.supportedLocales) {
|
|
244
|
+
this.loader.watch(locale);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
stopWatching() {
|
|
248
|
+
this.loader.stopWatching();
|
|
249
|
+
}
|
|
250
|
+
createTranslator(locale) {
|
|
251
|
+
return (key, params) => {
|
|
252
|
+
return this.t(locale, key, params);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
t(locale, key, params) {
|
|
256
|
+
this.metrics.totalLookups++;
|
|
257
|
+
const hasCount = params !== undefined && "count" in params && typeof params.count === "number";
|
|
258
|
+
const result = this._resolve(locale, key, params, hasCount);
|
|
259
|
+
if (result !== null) {
|
|
260
|
+
this.metrics.hits++;
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
if (this.config.fallbackToDefault && locale !== this.config.defaultLocale) {
|
|
264
|
+
const fallbackResult = this._resolve(this.config.defaultLocale, key, params, hasCount);
|
|
265
|
+
if (fallbackResult !== null) {
|
|
266
|
+
this.metrics.fallbacks++;
|
|
267
|
+
return fallbackResult;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
this.metrics.misses++;
|
|
271
|
+
return key;
|
|
272
|
+
}
|
|
273
|
+
getNegotiator() {
|
|
274
|
+
return this.negotiator;
|
|
275
|
+
}
|
|
276
|
+
getMetrics() {
|
|
277
|
+
return {
|
|
278
|
+
...this.metrics,
|
|
279
|
+
loadedLocales: this.loader.loadedLocales()
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
_resolve(locale, key, params, hasCount) {
|
|
283
|
+
const bundle = this.loader.load(locale);
|
|
284
|
+
const translations = bundle.translations;
|
|
285
|
+
const availableKeys = new Set(translations.keys());
|
|
286
|
+
let resolvedKey = key;
|
|
287
|
+
if (hasCount) {
|
|
288
|
+
resolvedKey = selectPluralKey(params.count, availableKeys, key);
|
|
289
|
+
}
|
|
290
|
+
const raw = translations.get(resolvedKey);
|
|
291
|
+
if (raw === undefined)
|
|
292
|
+
return null;
|
|
293
|
+
return params ? interpolate(raw, params) : raw;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function createI18n(config) {
|
|
297
|
+
return new I18n(config);
|
|
298
|
+
}
|
|
299
|
+
// src/i18n/middleware.ts
|
|
300
|
+
function getLocale(ctx) {
|
|
301
|
+
return ctx.get("locale") ?? "en";
|
|
302
|
+
}
|
|
303
|
+
function getT(ctx) {
|
|
304
|
+
return ctx.get("t") ?? ((key) => key);
|
|
305
|
+
}
|
|
306
|
+
function i18nMiddleware(options = {}) {
|
|
307
|
+
const engine = options.i18n ?? createI18n(options);
|
|
308
|
+
const negotiator = engine.getNegotiator();
|
|
309
|
+
const cookieName = engine.config.cookieName;
|
|
310
|
+
const cookieMaxAge = engine.config.cookieMaxAge;
|
|
311
|
+
return async (ctx, next) => {
|
|
312
|
+
let locale;
|
|
313
|
+
const cookieLocale = ctx.getCookie(cookieName);
|
|
314
|
+
if (cookieLocale && negotiator.isSupported(cookieLocale)) {
|
|
315
|
+
locale = cookieLocale;
|
|
316
|
+
} else {
|
|
317
|
+
const acceptLanguage = ctx.getHeader("accept-language") ?? "";
|
|
318
|
+
if (acceptLanguage) {
|
|
319
|
+
const match = negotiator.negotiate(acceptLanguage);
|
|
320
|
+
locale = match.locale;
|
|
321
|
+
} else {
|
|
322
|
+
locale = engine.config.defaultLocale;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
ctx.set("locale", locale);
|
|
326
|
+
ctx.set("t", engine.createTranslator(locale));
|
|
327
|
+
const response = await next();
|
|
328
|
+
const cookieValue = `${cookieName}=${locale}; Max-Age=${cookieMaxAge}; Path=/; SameSite=Lax`;
|
|
329
|
+
response.headers.append("Set-Cookie", cookieValue);
|
|
330
|
+
const existing = response.headers.get("Vary");
|
|
331
|
+
response.headers.set("Vary", existing ? `${existing}, Accept-Language` : "Accept-Language");
|
|
332
|
+
return response;
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
export {
|
|
336
|
+
parseAcceptLanguage,
|
|
337
|
+
normaliseLocale,
|
|
338
|
+
i18nMiddleware,
|
|
339
|
+
getT,
|
|
340
|
+
getLocale,
|
|
341
|
+
createI18n,
|
|
342
|
+
TranslationLoader,
|
|
343
|
+
LocaleNegotiator,
|
|
344
|
+
I18n
|
|
345
|
+
};
|