@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,552 @@
1
+ /**
2
+ * Island Architecture Implementation
3
+ *
4
+ * Provides partial hydration capabilities:
5
+ * - Mark components as interactive islands
6
+ * - Framework-agnostic island definitions
7
+ * - Lazy/eager/visible hydration strategies
8
+ * - State serialization for islands
9
+ */
10
+
11
+ import { createLogger, type Logger } from "../logger/index.js";
12
+ import type {
13
+ IslandConfig,
14
+ IslandDefinition,
15
+ IslandHydrationStrategy,
16
+ IslandRegistry,
17
+ IslandRenderResult,
18
+ IslandState,
19
+ IslandHydrationScript,
20
+ FrontendFramework,
21
+ SSRElement,
22
+ } from "./types.js";
23
+
24
+ // ============= Constants =============
25
+
26
+ const ISLAND_MARKER = "data-island";
27
+ const ISLAND_ID = "data-island-id";
28
+ const ISLAND_COMPONENT = "data-island-component";
29
+ const ISLAND_PROPS = "data-island-props";
30
+ const ISLAND_STRATEGY = "data-island-strategy";
31
+
32
+ // ============= Island Manager Class =============
33
+
34
+ /**
35
+ * Island Manager handles partial hydration of components
36
+ *
37
+ * Features:
38
+ * - Register interactive components as islands
39
+ * - Multiple hydration strategies (lazy, eager, visible, idle)
40
+ * - Framework-agnostic island definitions
41
+ * - State serialization for client hydration
42
+ */
43
+ export class IslandManager {
44
+ private registry: IslandRegistry = new Map();
45
+ private logger: Logger;
46
+ private islandCounter = 0;
47
+ private framework: FrontendFramework;
48
+
49
+ constructor(framework: FrontendFramework) {
50
+ this.framework = framework;
51
+ this.logger = createLogger({
52
+ level: "debug",
53
+ pretty: true,
54
+ context: { component: "IslandManager" },
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Register an island component
60
+ */
61
+ register(definition: IslandDefinition): string {
62
+ const id = definition.id || `island-${++this.islandCounter}`;
63
+
64
+ this.registry.set(id, {
65
+ ...definition,
66
+ id,
67
+ });
68
+
69
+ this.logger.debug(`Registered island: ${id} (${definition.component})`);
70
+ return id;
71
+ }
72
+
73
+ /**
74
+ * Register multiple islands
75
+ */
76
+ registerAll(definitions: IslandDefinition[]): string[] {
77
+ return definitions.map(def => this.register(def));
78
+ }
79
+
80
+ /**
81
+ * Unregister an island
82
+ */
83
+ unregister(id: string): boolean {
84
+ return this.registry.delete(id);
85
+ }
86
+
87
+ /**
88
+ * Get island by ID
89
+ */
90
+ getIsland(id: string): IslandDefinition | undefined {
91
+ return this.registry.get(id);
92
+ }
93
+
94
+ /**
95
+ * Get all registered islands
96
+ */
97
+ getAllIslands(): IslandDefinition[] {
98
+ return Array.from(this.registry.values());
99
+ }
100
+
101
+ /**
102
+ * Render an island to HTML
103
+ */
104
+ renderIsland(
105
+ componentName: string,
106
+ props: Record<string, unknown> = {},
107
+ options: Partial<IslandConfig> = {}
108
+ ): IslandRenderResult {
109
+ const id = options.id || `island-${++this.islandCounter}`;
110
+ const strategy = options.strategy || "lazy";
111
+
112
+ // Find the island definition
113
+ const definition = this.findIslandByComponent(componentName);
114
+
115
+ if (!definition) {
116
+ this.logger.warn(`Island not found: ${componentName}`);
117
+ return {
118
+ html: `<!-- Island not found: ${componentName} -->`,
119
+ id,
120
+ component: componentName,
121
+ hydrated: false,
122
+ };
123
+ }
124
+
125
+ // Generate the island HTML wrapper
126
+ const propsJson = JSON.stringify(props);
127
+ const escapedProps = this.escapeHtml(propsJson);
128
+
129
+ const wrapperAttrs = {
130
+ [ISLAND_MARKER]: "true",
131
+ [ISLAND_ID]: id,
132
+ [ISLAND_COMPONENT]: componentName,
133
+ [ISLAND_PROPS]: escapedProps,
134
+ [ISLAND_STRATEGY]: strategy,
135
+ };
136
+
137
+ const attrString = Object.entries(wrapperAttrs)
138
+ .map(([key, value]) => `${key}="${value}"`)
139
+ .join(" ");
140
+
141
+ // Generate placeholder or SSR content
142
+ const placeholder = options.placeholder || this.generatePlaceholder(componentName, props);
143
+
144
+ const html = `<div ${attrString}>${placeholder}</div>`;
145
+
146
+ return {
147
+ html,
148
+ id,
149
+ component: componentName,
150
+ hydrated: false,
151
+ props,
152
+ strategy,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Render island with SSR content
158
+ */
159
+ renderIslandSSR(
160
+ componentName: string,
161
+ ssrContent: string,
162
+ props: Record<string, unknown> = {},
163
+ options: Partial<IslandConfig> = {}
164
+ ): IslandRenderResult {
165
+ const id = options.id || `island-${++this.islandCounter}`;
166
+ const strategy = options.strategy || "lazy";
167
+
168
+ const propsJson = JSON.stringify(props);
169
+ const escapedProps = this.escapeHtml(propsJson);
170
+
171
+ const wrapperAttrs = {
172
+ [ISLAND_MARKER]: "true",
173
+ [ISLAND_ID]: id,
174
+ [ISLAND_COMPONENT]: componentName,
175
+ [ISLAND_PROPS]: escapedProps,
176
+ [ISLAND_STRATEGY]: strategy,
177
+ };
178
+
179
+ const attrString = Object.entries(wrapperAttrs)
180
+ .map(([key, value]) => `${key}="${value}"`)
181
+ .join(" ");
182
+
183
+ const html = `<div ${attrString}>${ssrContent}</div>`;
184
+
185
+ return {
186
+ html,
187
+ id,
188
+ component: componentName,
189
+ hydrated: false,
190
+ props,
191
+ strategy,
192
+ ssrContent,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Find island by component name
198
+ */
199
+ private findIslandByComponent(componentName: string): IslandDefinition | undefined {
200
+ for (const island of this.registry.values()) {
201
+ if (island.component === componentName) {
202
+ return island;
203
+ }
204
+ }
205
+ return undefined;
206
+ }
207
+
208
+ /**
209
+ * Generate placeholder content
210
+ */
211
+ private generatePlaceholder(
212
+ componentName: string,
213
+ props: Record<string, unknown>
214
+ ): string {
215
+ // Generate a simple placeholder based on component type
216
+ if (props.children && typeof props.children === "string") {
217
+ return props.children;
218
+ }
219
+
220
+ // Return empty placeholder - will be filled on hydration
221
+ return "";
222
+ }
223
+
224
+ /**
225
+ * Escape HTML in JSON string
226
+ */
227
+ private escapeHtml(str: string): string {
228
+ return str
229
+ .replace(/&/g, "&")
230
+ .replace(/</g, "<")
231
+ .replace(/>/g, ">")
232
+ .replace(/"/g, "\"")
233
+ .replace(/'/g, "'");
234
+ }
235
+
236
+ /**
237
+ * Get hydration script for all islands
238
+ */
239
+ getHydrationScript(): string {
240
+ const islands = this.getAllIslands();
241
+ const framework = this.framework;
242
+
243
+ const islandData = islands.map(island => ({
244
+ id: island.id,
245
+ component: island.component,
246
+ entry: island.entry,
247
+ strategy: island.strategy,
248
+ }));
249
+
250
+ return `
251
+ (function() {
252
+ const islands = ${JSON.stringify(islandData)};
253
+ const framework = "${framework}";
254
+
255
+ // Island hydration manager
256
+ window.__ISLANDS__ = {
257
+ pending: new Map(),
258
+ hydrated: new Set(),
259
+ registry: new Map(islands.map(i => [i.id, i])),
260
+
261
+ // Hydrate a single island
262
+ async hydrate(islandId) {
263
+ if (this.hydrated.has(islandId)) return;
264
+
265
+ const island = this.registry.get(islandId);
266
+ if (!island) return;
267
+
268
+ const element = document.querySelector('[data-island-id="' + islandId + '"]');
269
+ if (!element) return;
270
+
271
+ try {
272
+ const props = JSON.parse(element.getAttribute('data-island-props') || '{}');
273
+ const module = await import(island.entry);
274
+
275
+ if (framework === 'react') {
276
+ const { hydrate } = await import('react-dom/client');
277
+ hydrate(module.default(element, props), element);
278
+ } else if (framework === 'vue') {
279
+ const { createApp } = await import('vue');
280
+ createApp(module.default, props).mount(element, true);
281
+ } else if (framework === 'svelte') {
282
+ module.mount(element, { props, hydrate: true });
283
+ } else if (framework === 'solid') {
284
+ const { hydrate } = await import('solid-js/web');
285
+ hydrate(() => module.default(props), element);
286
+ }
287
+
288
+ this.hydrated.add(islandId);
289
+ console.log('[Islands] Hydrated:', islandId);
290
+ } catch (error) {
291
+ console.error('[Islands] Hydration failed:', islandId, error);
292
+ }
293
+ },
294
+
295
+ // Hydrate all islands with eager strategy
296
+ async hydrateEager() {
297
+ for (const [id, island] of this.registry) {
298
+ if (island.strategy === 'eager') {
299
+ await this.hydrate(id);
300
+ }
301
+ }
302
+ },
303
+
304
+ // Hydrate islands when visible
305
+ hydrateVisible() {
306
+ const observer = new IntersectionObserver((entries) => {
307
+ for (const entry of entries) {
308
+ if (entry.isIntersecting) {
309
+ const id = entry.target.getAttribute('data-island-id');
310
+ if (id) this.hydrate(id);
311
+ observer.unobserve(entry.target);
312
+ }
313
+ }
314
+ }, { rootMargin: '50px' });
315
+
316
+ for (const [id, island] of this.registry) {
317
+ if (island.strategy === 'visible') {
318
+ const element = document.querySelector('[data-island-id="' + id + '"]');
319
+ if (element) observer.observe(element);
320
+ }
321
+ }
322
+ },
323
+
324
+ // Hydrate islands when idle
325
+ hydrateIdle() {
326
+ for (const [id, island] of this.registry) {
327
+ if (island.strategy === 'idle') {
328
+ if ('requestIdleCallback' in window) {
329
+ requestIdleCallback(() => this.hydrate(id));
330
+ } else {
331
+ setTimeout(() => this.hydrate(id), 1);
332
+ }
333
+ }
334
+ }
335
+ },
336
+
337
+ // Hydrate lazy islands on interaction
338
+ hydrateLazy() {
339
+ for (const [id, island] of this.registry) {
340
+ if (island.strategy === 'lazy') {
341
+ const element = document.querySelector('[data-island-id="' + id + '"]');
342
+ if (element) {
343
+ const events = ['mouseenter', 'focus', 'touchstart', 'click'];
344
+ const handler = () => {
345
+ this.hydrate(id);
346
+ events.forEach(e => element.removeEventListener(e, handler));
347
+ };
348
+ events.forEach(e => element.addEventListener(e, handler, { once: true }));
349
+ }
350
+ }
351
+ }
352
+ },
353
+
354
+ // Initialize all islands
355
+ init() {
356
+ this.hydrateEager();
357
+ this.hydrateVisible();
358
+ this.hydrateIdle();
359
+ this.hydrateLazy();
360
+ }
361
+ };
362
+
363
+ // Initialize on DOM ready
364
+ if (document.readyState === 'loading') {
365
+ document.addEventListener('DOMContentLoaded', () => window.__ISLANDS__.init());
366
+ } else {
367
+ window.__ISLANDS__.init();
368
+ }
369
+ })();
370
+ `;
371
+ }
372
+
373
+ /**
374
+ * Get client island script (minimal)
375
+ */
376
+ getClientScript(): string {
377
+ return `
378
+ import { createIslandHydrator } from 'bueno/frontend/islands-client';
379
+
380
+ const hydrator = createIslandHydrator('${this.framework}');
381
+ hydrator.init();
382
+ `;
383
+ }
384
+
385
+ /**
386
+ * Get island count
387
+ */
388
+ getIslandCount(): number {
389
+ return this.registry.size;
390
+ }
391
+
392
+ /**
393
+ * Clear all islands
394
+ */
395
+ clear(): void {
396
+ this.registry.clear();
397
+ this.islandCounter = 0;
398
+ }
399
+
400
+ /**
401
+ * Get framework
402
+ */
403
+ getFramework(): FrontendFramework {
404
+ return this.framework;
405
+ }
406
+
407
+ /**
408
+ * Set framework
409
+ */
410
+ setFramework(framework: FrontendFramework): void {
411
+ this.framework = framework;
412
+ }
413
+ }
414
+
415
+ // ============= Factory Function =============
416
+
417
+ /**
418
+ * Create an island manager
419
+ */
420
+ export function createIslandManager(framework: FrontendFramework): IslandManager {
421
+ return new IslandManager(framework);
422
+ }
423
+
424
+ // ============= Utility Functions =============
425
+
426
+ /**
427
+ * Create an island definition
428
+ */
429
+ export function defineIsland(
430
+ component: string,
431
+ entry: string,
432
+ options: Partial<IslandDefinition> = {}
433
+ ): IslandDefinition {
434
+ return {
435
+ id: options.id || `island-${component}`,
436
+ component,
437
+ entry,
438
+ strategy: options.strategy || "lazy",
439
+ ...options,
440
+ };
441
+ }
442
+
443
+ /**
444
+ * Element-like interface for DOM elements (for type checking without DOM lib)
445
+ */
446
+ interface ElementLike {
447
+ hasAttribute(name: string): boolean;
448
+ getAttribute(name: string): string | null;
449
+ }
450
+
451
+ /**
452
+ * Check if an element is an island
453
+ */
454
+ export function isIslandElement(element: ElementLike): boolean {
455
+ return element.hasAttribute(ISLAND_MARKER);
456
+ }
457
+
458
+ /**
459
+ * Get island data from element
460
+ */
461
+ export function getIslandData(element: ElementLike): IslandState | null {
462
+ if (!isIslandElement(element)) {
463
+ return null;
464
+ }
465
+
466
+ return {
467
+ id: element.getAttribute(ISLAND_ID) || "",
468
+ component: element.getAttribute(ISLAND_COMPONENT) || "",
469
+ props: JSON.parse(element.getAttribute(ISLAND_PROPS) || "{}"),
470
+ strategy: (element.getAttribute(ISLAND_STRATEGY) as IslandHydrationStrategy) || "lazy",
471
+ hydrated: false,
472
+ };
473
+ }
474
+
475
+ /**
476
+ * Generate island wrapper attributes
477
+ */
478
+ export function getIslandAttributes(
479
+ id: string,
480
+ component: string,
481
+ props: Record<string, unknown>,
482
+ strategy: IslandHydrationStrategy = "lazy"
483
+ ): Record<string, string> {
484
+ return {
485
+ [ISLAND_MARKER]: "true",
486
+ [ISLAND_ID]: id,
487
+ [ISLAND_COMPONENT]: component,
488
+ [ISLAND_PROPS]: JSON.stringify(props),
489
+ [ISLAND_STRATEGY]: strategy,
490
+ };
491
+ }
492
+
493
+ /**
494
+ * Create island SSR element
495
+ */
496
+ export function createIslandElement(
497
+ id: string,
498
+ component: string,
499
+ props: Record<string, unknown>,
500
+ strategy: IslandHydrationStrategy = "lazy",
501
+ children?: SSRElement[]
502
+ ): SSRElement {
503
+ return {
504
+ tag: "div",
505
+ attrs: getIslandAttributes(id, component, props, strategy),
506
+ children,
507
+ };
508
+ }
509
+
510
+ /**
511
+ * Parse islands from HTML string
512
+ */
513
+ export function parseIslandsFromHTML(html: string): IslandState[] {
514
+ const islands: IslandState[] = [];
515
+ const regex = /data-island-id="([^"]+)"[^>]*data-island-component="([^"]+)"[^>]*data-island-props="([^"]+)"[^>]*data-island-strategy="([^"]+)"/g;
516
+
517
+ let match;
518
+ while ((match = regex.exec(html)) !== null) {
519
+ islands.push({
520
+ id: match[1],
521
+ component: match[2],
522
+ props: JSON.parse(match[3].replace(/"/g, '"').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')),
523
+ strategy: match[4] as IslandHydrationStrategy,
524
+ hydrated: false,
525
+ });
526
+ }
527
+
528
+ return islands;
529
+ }
530
+
531
+ /**
532
+ * Get hydration priority
533
+ */
534
+ export function getHydrationPriority(strategy: IslandHydrationStrategy): number {
535
+ const priorities: Record<IslandHydrationStrategy, number> = {
536
+ eager: 1,
537
+ visible: 2,
538
+ idle: 3,
539
+ lazy: 4,
540
+ };
541
+
542
+ return priorities[strategy] || 4;
543
+ }
544
+
545
+ /**
546
+ * Sort islands by hydration priority
547
+ */
548
+ export function sortIslandsByPriority(islands: IslandDefinition[]): IslandDefinition[] {
549
+ return [...islands].sort(
550
+ (a, b) => getHydrationPriority(a.strategy) - getHydrationPriority(b.strategy)
551
+ );
552
+ }