@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,475 @@
1
+ /**
2
+ * Layout Nesting Implementation
3
+ *
4
+ * Provides Next.js-style layout nesting:
5
+ * - _layout.tsx convention for nested layouts
6
+ * - Layout inheritance down directory tree
7
+ * - Layout state preservation on navigation
8
+ * - Per-segment layouts
9
+ */
10
+
11
+ import { createLogger, type Logger } from "../logger/index.js";
12
+ import type {
13
+ LayoutDefinition,
14
+ LayoutNode,
15
+ LayoutTree,
16
+ LayoutProps,
17
+ LayoutRenderer,
18
+ LayoutMiddleware,
19
+ LayoutConfig,
20
+ PartialLayoutConfig,
21
+ LayoutRenderResult,
22
+ LayoutSegment,
23
+ } from "./types.js";
24
+ import type { SSRContext, SSRElement, RenderResult } from "./types.js";
25
+
26
+ // ============= Constants =============
27
+
28
+ const LAYOUT_FILE = "_layout";
29
+ const SUPPORTED_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
30
+
31
+ // ============= Layout Manager Class =============
32
+
33
+ /**
34
+ * Layout Manager handles nested layout resolution and rendering
35
+ *
36
+ * Features:
37
+ * - _layout.tsx convention
38
+ * - Layout inheritance down directory tree
39
+ * - Layout state preservation
40
+ * - Per-segment layouts
41
+ */
42
+ export class LayoutManager {
43
+ private config: LayoutConfig;
44
+ private logger: Logger;
45
+ private layouts: Map<string, LayoutDefinition> = new Map();
46
+ private layoutTree: LayoutTree | null = null;
47
+
48
+ constructor(config: PartialLayoutConfig = {}) {
49
+ this.config = this.normalizeConfig(config);
50
+ this.logger = createLogger({
51
+ level: "debug",
52
+ pretty: true,
53
+ context: { component: "LayoutManager" },
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Normalize partial config to full config with defaults
59
+ */
60
+ private normalizeConfig(config: PartialLayoutConfig): LayoutConfig {
61
+ return {
62
+ pagesDir: config.pagesDir ?? "pages",
63
+ rootDir: config.rootDir ?? process.cwd(),
64
+ extensions: config.extensions ?? SUPPORTED_EXTENSIONS,
65
+ preserveState: config.preserveState ?? true,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Initialize the layout manager by scanning for layout files
71
+ */
72
+ async init(): Promise<void> {
73
+ this.logger.info(`Initializing layout manager from: ${this.config.pagesDir}`);
74
+ await this.scanLayoutFiles();
75
+ this.buildLayoutTree();
76
+ this.logger.info(`Loaded ${this.layouts.size} layouts`);
77
+ }
78
+
79
+ /**
80
+ * Scan for layout files in the pages directory
81
+ */
82
+ private async scanLayoutFiles(): Promise<void> {
83
+ const pagesPath = this.config.pagesDir;
84
+ const glob = new Bun.Glob(`**/${LAYOUT_FILE}{${this.config.extensions.join(",")}}`);
85
+
86
+ try {
87
+ for await (const file of glob.scan(pagesPath)) {
88
+ await this.processLayoutFile(file, pagesPath);
89
+ }
90
+ } catch (error) {
91
+ this.logger.error(`Failed to scan layouts: ${pagesPath}`, error);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Process a single layout file
97
+ */
98
+ private async processLayoutFile(filePath: string, basePath: string): Promise<void> {
99
+ const fullPath = `${basePath}/${filePath}`;
100
+ const segment = this.getLayoutSegment(filePath);
101
+
102
+ const layout: LayoutDefinition = {
103
+ id: this.generateLayoutId(filePath),
104
+ filePath: fullPath,
105
+ segment,
106
+ depth: this.calculateDepth(segment),
107
+ };
108
+
109
+ this.layouts.set(segment, layout);
110
+ this.logger.debug(`Processed layout: ${segment}`);
111
+ }
112
+
113
+ /**
114
+ * Get layout segment from file path
115
+ */
116
+ private getLayoutSegment(filePath: string): string {
117
+ // Remove _layout.tsx from path
118
+ const segment = filePath.replace(new RegExp(`/${LAYOUT_FILE}\\.(tsx?|jsx?)$`), "");
119
+ return segment === "" ? "/" : `/${segment}`;
120
+ }
121
+
122
+ /**
123
+ * Calculate depth of layout segment
124
+ */
125
+ private calculateDepth(segment: string): number {
126
+ if (segment === "/") return 0;
127
+ return segment.split("/").length - 1;
128
+ }
129
+
130
+ /**
131
+ * Generate unique layout ID
132
+ */
133
+ private generateLayoutId(filePath: string): string {
134
+ return filePath.replace(/[\/\\.]/g, "-").replace(/^-/, "");
135
+ }
136
+
137
+ /**
138
+ * Build the layout tree from collected layouts
139
+ */
140
+ private buildLayoutTree(): void {
141
+ // Find root layout
142
+ const rootLayout = this.layouts.get("/");
143
+ if (!rootLayout) {
144
+ this.logger.warn("No root layout found");
145
+ this.layoutTree = null;
146
+ return;
147
+ }
148
+
149
+ // Build tree recursively
150
+ this.layoutTree = this.buildTreeNode(rootLayout, null);
151
+ }
152
+
153
+ /**
154
+ * Build a layout tree node
155
+ */
156
+ private buildTreeNode(layout: LayoutDefinition, parent: LayoutNode | null): LayoutNode {
157
+ const node: LayoutNode = {
158
+ layout,
159
+ parent,
160
+ children: [],
161
+ };
162
+
163
+ // Find child layouts
164
+ for (const [segment, childLayout] of this.layouts) {
165
+ if (segment === layout.segment) continue;
166
+
167
+ // Check if this is a direct child
168
+ if (this.isDirectChild(layout.segment, segment)) {
169
+ node.children.push(this.buildTreeNode(childLayout, node));
170
+ }
171
+ }
172
+
173
+ // Sort children by depth
174
+ node.children.sort((a, b) => a.layout.depth - b.layout.depth);
175
+
176
+ return node;
177
+ }
178
+
179
+ /**
180
+ * Check if a segment is a direct child of another
181
+ */
182
+ private isDirectChild(parentSegment: string, childSegment: string): boolean {
183
+ if (parentSegment === "/") {
184
+ // Root layout: direct children have no other parent
185
+ const parts = childSegment.split("/").filter(Boolean);
186
+ return parts.length === 1;
187
+ }
188
+
189
+ // Check if child starts with parent and has exactly one more segment
190
+ if (!childSegment.startsWith(parentSegment + "/")) return false;
191
+
192
+ const remaining = childSegment.slice(parentSegment.length + 1);
193
+ return !remaining.includes("/");
194
+ }
195
+
196
+ /**
197
+ * Get layout chain for a route
198
+ */
199
+ getLayoutChain(routePath: string): LayoutDefinition[] {
200
+ const chain: LayoutDefinition[] = [];
201
+
202
+ // Start from root and work down
203
+ const segments = this.getRouteSegments(routePath);
204
+
205
+ for (let i = 0; i <= segments.length; i++) {
206
+ const segment = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
207
+ const layout = this.layouts.get(segment);
208
+
209
+ if (layout) {
210
+ chain.push(layout);
211
+ }
212
+ }
213
+
214
+ return chain;
215
+ }
216
+
217
+ /**
218
+ * Get route segments from path
219
+ */
220
+ private getRouteSegments(path: string): string[] {
221
+ return path.split("/").filter(Boolean);
222
+ }
223
+
224
+ /**
225
+ * Load layout module
226
+ */
227
+ private async loadLayoutModule(filePath: string): Promise<LayoutRenderer | null> {
228
+ try {
229
+ const module = await import(filePath);
230
+ return module.default || module;
231
+ } catch (error) {
232
+ this.logger.error(`Failed to load layout module: ${filePath}`, error);
233
+ return null;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Render layouts with nested content
239
+ */
240
+ async renderLayouts(
241
+ routePath: string,
242
+ content: string,
243
+ context: SSRContext
244
+ ): Promise<LayoutRenderResult> {
245
+ const chain = this.getLayoutChain(routePath);
246
+
247
+ if (chain.length === 0) {
248
+ return {
249
+ html: content,
250
+ head: [],
251
+ body: [],
252
+ layouts: [],
253
+ };
254
+ }
255
+
256
+ let html = content;
257
+ const head: SSRElement[] = [];
258
+ const body: SSRElement[] = [];
259
+ const renderedLayouts: string[] = [];
260
+
261
+ // Render layouts from innermost to outermost
262
+ for (let i = chain.length - 1; i >= 0; i--) {
263
+ const layout = chain[i];
264
+ const renderer = await this.loadLayoutModule(layout.filePath);
265
+
266
+ if (renderer) {
267
+ const props: LayoutProps = {
268
+ children: html,
269
+ params: context.params,
270
+ query: context.query,
271
+ pathname: context.pathname,
272
+ };
273
+
274
+ // Render layout
275
+ const result = await renderer(props, context);
276
+
277
+ if (typeof result === "string") {
278
+ html = result;
279
+ } else {
280
+ html = result.html;
281
+ if (result.head) head.push(...result.head);
282
+ if (result.body) body.push(...result.body);
283
+ }
284
+
285
+ renderedLayouts.push(layout.segment);
286
+ }
287
+ }
288
+
289
+ return {
290
+ html,
291
+ head,
292
+ body,
293
+ layouts: renderedLayouts,
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Get layout for a segment
299
+ */
300
+ getLayout(segment: string): LayoutDefinition | undefined {
301
+ return this.layouts.get(segment);
302
+ }
303
+
304
+ /**
305
+ * Get all layouts
306
+ */
307
+ getAllLayouts(): LayoutDefinition[] {
308
+ return Array.from(this.layouts.values());
309
+ }
310
+
311
+ /**
312
+ * Get layout tree
313
+ */
314
+ getLayoutTree(): LayoutNode | null {
315
+ return this.layoutTree;
316
+ }
317
+
318
+ /**
319
+ * Check if a route has a layout
320
+ */
321
+ hasLayout(routePath: string): boolean {
322
+ const segments = this.getRouteSegments(routePath);
323
+
324
+ for (let i = 0; i <= segments.length; i++) {
325
+ const segment = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
326
+ if (this.layouts.has(segment)) {
327
+ return true;
328
+ }
329
+ }
330
+
331
+ return false;
332
+ }
333
+
334
+ /**
335
+ * Get layout depth for a route
336
+ */
337
+ getLayoutDepth(routePath: string): number {
338
+ return this.getLayoutChain(routePath).length;
339
+ }
340
+
341
+ /**
342
+ * Reload layouts (for hot reload)
343
+ */
344
+ async reload(): Promise<void> {
345
+ this.logger.info("Reloading layouts...");
346
+ this.layouts.clear();
347
+ this.layoutTree = null;
348
+ await this.init();
349
+ }
350
+
351
+ /**
352
+ * Get configuration
353
+ */
354
+ getConfig(): LayoutConfig {
355
+ return { ...this.config };
356
+ }
357
+ }
358
+
359
+ // ============= Factory Function =============
360
+
361
+ /**
362
+ * Create a layout manager
363
+ */
364
+ export function createLayoutManager(config: PartialLayoutConfig = {}): LayoutManager {
365
+ return new LayoutManager(config);
366
+ }
367
+
368
+ // ============= Utility Functions =============
369
+
370
+ /**
371
+ * Check if a file is a layout file
372
+ */
373
+ export function isLayoutFile(filename: string): boolean {
374
+ const baseName = filename.replace(/\.(tsx?|jsx?)$/, "");
375
+ return baseName === LAYOUT_FILE;
376
+ }
377
+
378
+ /**
379
+ * Get layout segment from file path
380
+ */
381
+ export function getLayoutSegmentFromPath(filePath: string): string {
382
+ const parts = filePath.split("/");
383
+ const dirParts = parts.slice(0, -1); // Remove filename
384
+
385
+ if (dirParts.length === 0) {
386
+ return "/";
387
+ }
388
+
389
+ return "/" + dirParts.join("/");
390
+ }
391
+
392
+ /**
393
+ * Build layout props for a page
394
+ */
395
+ export function buildLayoutProps(
396
+ children: string,
397
+ context: SSRContext
398
+ ): LayoutProps {
399
+ return {
400
+ children,
401
+ params: context.params,
402
+ query: context.query,
403
+ pathname: context.pathname,
404
+ };
405
+ }
406
+
407
+ /**
408
+ * Create a layout segment
409
+ */
410
+ export function createLayoutSegment(
411
+ path: string,
412
+ params: Record<string, string> = {}
413
+ ): LayoutSegment {
414
+ return {
415
+ path,
416
+ params,
417
+ component: null,
418
+ };
419
+ }
420
+
421
+ /**
422
+ * Merge layout head elements
423
+ */
424
+ export function mergeLayoutHead(
425
+ ...heads: SSRElement[][]
426
+ ): SSRElement[] {
427
+ const merged: SSRElement[] = [];
428
+ const seen = new Set<string>();
429
+
430
+ for (const head of heads) {
431
+ for (const element of head) {
432
+ const key = getHeadElementKey(element);
433
+ if (!seen.has(key)) {
434
+ seen.add(key);
435
+ merged.push(element);
436
+ }
437
+ }
438
+ }
439
+
440
+ return merged;
441
+ }
442
+
443
+ /**
444
+ * Get unique key for head element
445
+ */
446
+ function getHeadElementKey(element: SSRElement): string {
447
+ switch (element.tag) {
448
+ case "title":
449
+ return "title";
450
+ case "meta":
451
+ return `meta-${element.attrs.name || element.attrs.property || ""}`;
452
+ case "link":
453
+ return `link-${element.attrs.rel}-${element.attrs.href}`;
454
+ case "script":
455
+ return `script-${element.attrs.src || ""}`;
456
+ case "style":
457
+ return `style-${element.attrs.id || ""}`;
458
+ default:
459
+ return `${element.tag}-${JSON.stringify(element.attrs)}`;
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Render layout tree to string (for debugging)
465
+ */
466
+ export function layoutTreeToString(node: LayoutNode, indent = 0): string {
467
+ const prefix = " ".repeat(indent);
468
+ let result = `${prefix}${node.layout.segment}\n`;
469
+
470
+ for (const child of node.children) {
471
+ result += layoutTreeToString(child, indent + 1);
472
+ }
473
+
474
+ return result;
475
+ }