@buenojs/bueno 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Built-in Middleware
3
+ *
4
+ * Common middleware utilities for logging, CORS, request ID, etc.
5
+ */
6
+
7
+ import type { Context } from "../context";
8
+ import type { Middleware } from "../index";
9
+
10
+ // ============= Logger Middleware =============
11
+
12
+ export interface LoggerOptions {
13
+ format?: "json" | "text";
14
+ level?: "debug" | "info" | "warn" | "error";
15
+ }
16
+
17
+ export function logger(options: LoggerOptions = {}): Middleware {
18
+ const { format = "text", level = "info" } = options;
19
+
20
+ return async (context: Context, next: () => Promise<Response>) => {
21
+ const start = Date.now();
22
+ const { method, path } = context;
23
+
24
+ // Log request
25
+ if (format === "json") {
26
+ console.log(
27
+ JSON.stringify({
28
+ type: "request",
29
+ method,
30
+ path,
31
+ timestamp: new Date().toISOString(),
32
+ }),
33
+ );
34
+ } else {
35
+ console.log(`--> ${method} ${path}`);
36
+ }
37
+
38
+ try {
39
+ const response = await next();
40
+ const duration = Date.now() - start;
41
+
42
+ // Log response
43
+ if (format === "json") {
44
+ console.log(
45
+ JSON.stringify({
46
+ type: "response",
47
+ method,
48
+ path,
49
+ status: response.status,
50
+ duration,
51
+ timestamp: new Date().toISOString(),
52
+ }),
53
+ );
54
+ } else {
55
+ console.log(`<-- ${method} ${path} ${response.status} (${duration}ms)`);
56
+ }
57
+
58
+ return response;
59
+ } catch (error) {
60
+ const duration = Date.now() - start;
61
+
62
+ if (format === "json") {
63
+ console.error(
64
+ JSON.stringify({
65
+ type: "error",
66
+ method,
67
+ path,
68
+ error: error instanceof Error ? error.message : String(error),
69
+ duration,
70
+ timestamp: new Date().toISOString(),
71
+ }),
72
+ );
73
+ } else {
74
+ console.error(`<-- ${method} ${path} ERROR (${duration}ms)`, error);
75
+ }
76
+
77
+ throw error;
78
+ }
79
+ };
80
+ }
81
+
82
+ // ============= CORS Middleware =============
83
+
84
+ export interface CorsOptions {
85
+ origin?: string | string[] | ((origin: string) => string | undefined);
86
+ methods?: string[];
87
+ allowedHeaders?: string[];
88
+ exposedHeaders?: string[];
89
+ credentials?: boolean;
90
+ maxAge?: number;
91
+ }
92
+
93
+ export function cors(options: CorsOptions = {}): Middleware {
94
+ const {
95
+ origin = "*",
96
+ methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
97
+ allowedHeaders = ["Content-Type", "Authorization"],
98
+ exposedHeaders = [],
99
+ credentials = false,
100
+ maxAge = 86400,
101
+ } = options;
102
+
103
+ return async (context: Context, next: () => Promise<Response>) => {
104
+ const requestOrigin = context.getHeader("origin") ?? "";
105
+
106
+ // Determine allowed origin
107
+ let allowedOrigin: string;
108
+ if (typeof origin === "function") {
109
+ allowedOrigin = origin(requestOrigin) ?? "*";
110
+ } else if (Array.isArray(origin)) {
111
+ allowedOrigin = origin.includes(requestOrigin) ? requestOrigin : "*";
112
+ } else {
113
+ allowedOrigin = origin;
114
+ }
115
+
116
+ // Handle preflight request
117
+ if (context.method === "OPTIONS") {
118
+ return new Response(null, {
119
+ status: 204,
120
+ headers: {
121
+ "Access-Control-Allow-Origin": allowedOrigin,
122
+ "Access-Control-Allow-Methods": methods.join(", "),
123
+ "Access-Control-Allow-Headers": allowedHeaders.join(", "),
124
+ "Access-Control-Allow-Credentials": String(credentials),
125
+ "Access-Control-Max-Age": String(maxAge),
126
+ },
127
+ });
128
+ }
129
+
130
+ const response = await next();
131
+
132
+ // Add CORS headers to response
133
+ response.headers.set("Access-Control-Allow-Origin", allowedOrigin);
134
+ response.headers.set(
135
+ "Access-Control-Allow-Credentials",
136
+ String(credentials),
137
+ );
138
+
139
+ if (exposedHeaders.length > 0) {
140
+ response.headers.set(
141
+ "Access-Control-Expose-Headers",
142
+ exposedHeaders.join(", "),
143
+ );
144
+ }
145
+
146
+ return response;
147
+ };
148
+ }
149
+
150
+ // ============= Request ID Middleware =============
151
+
152
+ export interface RequestIdOptions {
153
+ header?: string;
154
+ generator?: () => string;
155
+ }
156
+
157
+ function generateId(): string {
158
+ return crypto.randomUUID();
159
+ }
160
+
161
+ export function requestId(options: RequestIdOptions = {}): Middleware {
162
+ const { header = "X-Request-Id", generator = generateId } = options;
163
+
164
+ return async (context: Context, next: () => Promise<Response>) => {
165
+ // Check for existing request ID header
166
+ const existingId = context.getHeader(header.toLowerCase());
167
+ const id = existingId ?? generator();
168
+
169
+ // Store in context
170
+ context.set("requestId", id);
171
+
172
+ const response = await next();
173
+
174
+ // Add to response header
175
+ response.headers.set(header, id);
176
+
177
+ return response;
178
+ };
179
+ }
180
+
181
+ // ============= Timing Middleware =============
182
+
183
+ export function timing(): Middleware {
184
+ return async (context: Context, next: () => Promise<Response>) => {
185
+ const start = performance.now();
186
+
187
+ const response = await next();
188
+
189
+ const duration = performance.now() - start;
190
+ response.headers.set("X-Response-Time", `${duration.toFixed(3)}ms`);
191
+ response.headers.set("Server-Timing", `total;dur=${duration.toFixed(3)}`);
192
+
193
+ return response;
194
+ };
195
+ }
196
+
197
+ // ============= Security Headers Middleware =============
198
+
199
+ export interface SecurityHeadersOptions {
200
+ contentSecurityPolicy?: string;
201
+ xssProtection?: boolean;
202
+ frameGuard?: "DENY" | "SAMEORIGIN" | "ALLOW-FROM";
203
+ hsts?: boolean | { maxAge?: number; includeSubDomains?: boolean };
204
+ }
205
+
206
+ export function securityHeaders(
207
+ options: SecurityHeadersOptions = {},
208
+ ): Middleware {
209
+ const {
210
+ xssProtection = true,
211
+ frameGuard = "SAMEORIGIN",
212
+ hsts = false,
213
+ } = options;
214
+
215
+ return async (context: Context, next: () => Promise<Response>) => {
216
+ const response = await next();
217
+
218
+ // XSS Protection
219
+ if (xssProtection) {
220
+ response.headers.set("X-XSS-Protection", "1; mode=block");
221
+ }
222
+
223
+ // Frame Guard
224
+ if (frameGuard) {
225
+ response.headers.set("X-Frame-Options", frameGuard);
226
+ }
227
+
228
+ // HSTS
229
+ if (hsts) {
230
+ const maxAge =
231
+ typeof hsts === "object" ? (hsts.maxAge ?? 31536000) : 31536000;
232
+ const includeSubDomains =
233
+ typeof hsts === "object" ? (hsts.includeSubDomains ?? true) : true;
234
+
235
+ let hstsValue = `max-age=${maxAge}`;
236
+ if (includeSubDomains) {
237
+ hstsValue += "; includeSubDomains";
238
+ }
239
+
240
+ response.headers.set("Strict-Transport-Security", hstsValue);
241
+ }
242
+
243
+ // Prevent MIME type sniffing
244
+ response.headers.set("X-Content-Type-Options", "nosniff");
245
+
246
+ return response;
247
+ };
248
+ }
249
+
250
+ // ============= Rate Limiter Middleware =============
251
+
252
+ export interface RateLimitOptions {
253
+ windowMs?: number;
254
+ max?: number;
255
+ keyGenerator?: (context: Context) => string;
256
+ handler?: (context: Context) => Response;
257
+ }
258
+
259
+ export function rateLimit(options: RateLimitOptions = {}): Middleware {
260
+ const {
261
+ windowMs = 60000, // 1 minute
262
+ max = 100,
263
+ keyGenerator = (ctx) => ctx.ip ?? "unknown",
264
+ handler = (ctx) =>
265
+ ctx.error("Too many requests, please try again later.", 429),
266
+ } = options;
267
+
268
+ const hits = new Map<string, { count: number; resetTime: number }>();
269
+
270
+ // Cleanup old entries periodically
271
+ setInterval(() => {
272
+ const now = Date.now();
273
+ for (const [key, value] of hits.entries()) {
274
+ if (value.resetTime < now) {
275
+ hits.delete(key);
276
+ }
277
+ }
278
+ }, windowMs);
279
+
280
+ return async (context: Context, next: () => Promise<Response>) => {
281
+ const key = keyGenerator(context);
282
+ const now = Date.now();
283
+
284
+ let record = hits.get(key);
285
+
286
+ if (!record || record.resetTime < now) {
287
+ record = { count: 0, resetTime: now + windowMs };
288
+ }
289
+
290
+ record.count++;
291
+ hits.set(key, record);
292
+
293
+ // Set rate limit headers
294
+ context.setHeader("X-RateLimit-Limit", String(max));
295
+ context.setHeader(
296
+ "X-RateLimit-Remaining",
297
+ String(Math.max(0, max - record.count)),
298
+ );
299
+ context.setHeader("X-RateLimit-Reset", String(record.resetTime));
300
+
301
+ if (record.count > max) {
302
+ return handler(context);
303
+ }
304
+
305
+ return next();
306
+ };
307
+ }
308
+
309
+ // ============= Compression Middleware =============
310
+
311
+ export interface CompressionOptions {
312
+ threshold?: number;
313
+ types?: string[];
314
+ }
315
+
316
+ export function compression(options: CompressionOptions = {}): Middleware {
317
+ const { threshold = 1024 } = options;
318
+
319
+ return async (context: Context, next: () => Promise<Response>) => {
320
+ const response = await next();
321
+
322
+ // Check if client accepts gzip
323
+ const acceptEncoding = context.getHeader("accept-encoding") ?? "";
324
+ if (!acceptEncoding.includes("gzip")) {
325
+ return response;
326
+ }
327
+
328
+ // Check content type
329
+ const contentType = response.headers.get("content-type") ?? "";
330
+ if (
331
+ !contentType.includes("text") &&
332
+ !contentType.includes("json") &&
333
+ !contentType.includes("javascript")
334
+ ) {
335
+ return response;
336
+ }
337
+
338
+ // Check size
339
+ const contentLength = Number.parseInt(
340
+ response.headers.get("content-length") ?? "0",
341
+ 10,
342
+ );
343
+ if (contentLength > 0 && contentLength < threshold) {
344
+ return response;
345
+ }
346
+
347
+ // Compress using Bun's built-in gzip
348
+ const buffer = await response.arrayBuffer();
349
+ const compressed = Bun.gzipSync(buffer);
350
+
351
+ return new Response(compressed, {
352
+ status: response.status,
353
+ headers: {
354
+ ...Object.fromEntries(response.headers.entries()),
355
+ "Content-Encoding": "gzip",
356
+ "Content-Length": String(compressed.byteLength),
357
+ },
358
+ });
359
+ };
360
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Middleware System
3
+ *
4
+ * Provides a composable middleware pipeline with async/await support
5
+ * and context mutation capabilities.
6
+ */
7
+
8
+ import type { Context } from "../context";
9
+
10
+ // ============= Types =============
11
+
12
+ /**
13
+ * Middleware function type
14
+ */
15
+ export type Middleware = (
16
+ context: Context,
17
+ next: () => Promise<Response>,
18
+ ) => Promise<Response> | Response;
19
+
20
+ /**
21
+ * Route handler type
22
+ */
23
+ export type Handler = (context: Context) => Promise<Response> | Response;
24
+
25
+ // ============= Compose =============
26
+
27
+ /**
28
+ * Compose multiple middleware into a single function
29
+ * Following Koa-style middleware pattern
30
+ */
31
+ export function compose(
32
+ middleware: Middleware[],
33
+ ): (context: Context, handler: Handler) => Promise<Response> {
34
+ return async (context: Context, handler: Handler): Promise<Response> => {
35
+ let index = -1;
36
+
37
+ const dispatch = async (i: number): Promise<Response> => {
38
+ if (i <= index) {
39
+ throw new Error("next() called multiple times");
40
+ }
41
+
42
+ index = i;
43
+
44
+ // If we've run all middleware, call the handler
45
+ if (i >= middleware.length) {
46
+ return handler(context);
47
+ }
48
+
49
+ const fn = middleware[i];
50
+
51
+ return fn(context, async () => {
52
+ return dispatch(i + 1);
53
+ });
54
+ };
55
+
56
+ return dispatch(0);
57
+ };
58
+ }
59
+
60
+ // ============= Pipeline Class =============
61
+
62
+ export class Pipeline {
63
+ private middleware: Middleware[] = [];
64
+
65
+ /**
66
+ * Add middleware to the pipeline
67
+ */
68
+ use(middleware: Middleware): this {
69
+ this.middleware.push(middleware);
70
+ return this;
71
+ }
72
+
73
+ /**
74
+ * Execute the pipeline with a handler
75
+ */
76
+ async execute(context: Context, handler: Handler): Promise<Response> {
77
+ const fn = compose(this.middleware);
78
+ return fn(context, handler);
79
+ }
80
+
81
+ /**
82
+ * Get middleware count
83
+ */
84
+ get length(): number {
85
+ return this.middleware.length;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Create a new middleware pipeline
91
+ */
92
+ export function createPipeline(): Pipeline {
93
+ return new Pipeline();
94
+ }