@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
package/src/frontend/islands.ts
CHANGED
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
* - State serialization for islands
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { type Logger, createLogger } from "../logger/index.js";
|
|
12
12
|
import type {
|
|
13
|
+
FrontendFramework,
|
|
13
14
|
IslandConfig,
|
|
14
15
|
IslandDefinition,
|
|
16
|
+
IslandHydrationScript,
|
|
15
17
|
IslandHydrationStrategy,
|
|
16
18
|
IslandRegistry,
|
|
17
19
|
IslandRenderResult,
|
|
18
20
|
IslandState,
|
|
19
|
-
IslandHydrationScript,
|
|
20
|
-
FrontendFramework,
|
|
21
21
|
SSRElement,
|
|
22
22
|
} from "./types.js";
|
|
23
23
|
|
|
@@ -74,7 +74,7 @@ export class IslandManager {
|
|
|
74
74
|
* Register multiple islands
|
|
75
75
|
*/
|
|
76
76
|
registerAll(definitions: IslandDefinition[]): string[] {
|
|
77
|
-
return definitions.map(def => this.register(def));
|
|
77
|
+
return definitions.map((def) => this.register(def));
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
@@ -104,7 +104,7 @@ export class IslandManager {
|
|
|
104
104
|
renderIsland(
|
|
105
105
|
componentName: string,
|
|
106
106
|
props: Record<string, unknown> = {},
|
|
107
|
-
options: Partial<IslandConfig> = {}
|
|
107
|
+
options: Partial<IslandConfig> = {},
|
|
108
108
|
): IslandRenderResult {
|
|
109
109
|
const id = options.id || `island-${++this.islandCounter}`;
|
|
110
110
|
const strategy = options.strategy || "lazy";
|
|
@@ -139,7 +139,8 @@ export class IslandManager {
|
|
|
139
139
|
.join(" ");
|
|
140
140
|
|
|
141
141
|
// Generate placeholder or SSR content
|
|
142
|
-
const placeholder =
|
|
142
|
+
const placeholder =
|
|
143
|
+
options.placeholder || this.generatePlaceholder(componentName, props);
|
|
143
144
|
|
|
144
145
|
const html = `<div ${attrString}>${placeholder}</div>`;
|
|
145
146
|
|
|
@@ -160,7 +161,7 @@ export class IslandManager {
|
|
|
160
161
|
componentName: string,
|
|
161
162
|
ssrContent: string,
|
|
162
163
|
props: Record<string, unknown> = {},
|
|
163
|
-
options: Partial<IslandConfig> = {}
|
|
164
|
+
options: Partial<IslandConfig> = {},
|
|
164
165
|
): IslandRenderResult {
|
|
165
166
|
const id = options.id || `island-${++this.islandCounter}`;
|
|
166
167
|
const strategy = options.strategy || "lazy";
|
|
@@ -196,7 +197,9 @@ export class IslandManager {
|
|
|
196
197
|
/**
|
|
197
198
|
* Find island by component name
|
|
198
199
|
*/
|
|
199
|
-
private findIslandByComponent(
|
|
200
|
+
private findIslandByComponent(
|
|
201
|
+
componentName: string,
|
|
202
|
+
): IslandDefinition | undefined {
|
|
200
203
|
for (const island of this.registry.values()) {
|
|
201
204
|
if (island.component === componentName) {
|
|
202
205
|
return island;
|
|
@@ -210,7 +213,7 @@ export class IslandManager {
|
|
|
210
213
|
*/
|
|
211
214
|
private generatePlaceholder(
|
|
212
215
|
componentName: string,
|
|
213
|
-
props: Record<string, unknown
|
|
216
|
+
props: Record<string, unknown>,
|
|
214
217
|
): string {
|
|
215
218
|
// Generate a simple placeholder based on component type
|
|
216
219
|
if (props.children && typeof props.children === "string") {
|
|
@@ -229,7 +232,7 @@ export class IslandManager {
|
|
|
229
232
|
.replace(/&/g, "&")
|
|
230
233
|
.replace(/</g, "<")
|
|
231
234
|
.replace(/>/g, ">")
|
|
232
|
-
.replace(/"/g, "
|
|
235
|
+
.replace(/"/g, '"')
|
|
233
236
|
.replace(/'/g, "'");
|
|
234
237
|
}
|
|
235
238
|
|
|
@@ -240,7 +243,7 @@ export class IslandManager {
|
|
|
240
243
|
const islands = this.getAllIslands();
|
|
241
244
|
const framework = this.framework;
|
|
242
245
|
|
|
243
|
-
const islandData = islands.map(island => ({
|
|
246
|
+
const islandData = islands.map((island) => ({
|
|
244
247
|
id: island.id,
|
|
245
248
|
component: island.component,
|
|
246
249
|
entry: island.entry,
|
|
@@ -417,7 +420,9 @@ hydrator.init();
|
|
|
417
420
|
/**
|
|
418
421
|
* Create an island manager
|
|
419
422
|
*/
|
|
420
|
-
export function createIslandManager(
|
|
423
|
+
export function createIslandManager(
|
|
424
|
+
framework: FrontendFramework,
|
|
425
|
+
): IslandManager {
|
|
421
426
|
return new IslandManager(framework);
|
|
422
427
|
}
|
|
423
428
|
|
|
@@ -429,7 +434,7 @@ export function createIslandManager(framework: FrontendFramework): IslandManager
|
|
|
429
434
|
export function defineIsland(
|
|
430
435
|
component: string,
|
|
431
436
|
entry: string,
|
|
432
|
-
options: Partial<IslandDefinition> = {}
|
|
437
|
+
options: Partial<IslandDefinition> = {},
|
|
433
438
|
): IslandDefinition {
|
|
434
439
|
return {
|
|
435
440
|
id: options.id || `island-${component}`,
|
|
@@ -467,7 +472,9 @@ export function getIslandData(element: ElementLike): IslandState | null {
|
|
|
467
472
|
id: element.getAttribute(ISLAND_ID) || "",
|
|
468
473
|
component: element.getAttribute(ISLAND_COMPONENT) || "",
|
|
469
474
|
props: JSON.parse(element.getAttribute(ISLAND_PROPS) || "{}"),
|
|
470
|
-
strategy:
|
|
475
|
+
strategy:
|
|
476
|
+
(element.getAttribute(ISLAND_STRATEGY) as IslandHydrationStrategy) ||
|
|
477
|
+
"lazy",
|
|
471
478
|
hydrated: false,
|
|
472
479
|
};
|
|
473
480
|
}
|
|
@@ -479,7 +486,7 @@ export function getIslandAttributes(
|
|
|
479
486
|
id: string,
|
|
480
487
|
component: string,
|
|
481
488
|
props: Record<string, unknown>,
|
|
482
|
-
strategy: IslandHydrationStrategy = "lazy"
|
|
489
|
+
strategy: IslandHydrationStrategy = "lazy",
|
|
483
490
|
): Record<string, string> {
|
|
484
491
|
return {
|
|
485
492
|
[ISLAND_MARKER]: "true",
|
|
@@ -498,7 +505,7 @@ export function createIslandElement(
|
|
|
498
505
|
component: string,
|
|
499
506
|
props: Record<string, unknown>,
|
|
500
507
|
strategy: IslandHydrationStrategy = "lazy",
|
|
501
|
-
children?: SSRElement[]
|
|
508
|
+
children?: SSRElement[],
|
|
502
509
|
): SSRElement {
|
|
503
510
|
return {
|
|
504
511
|
tag: "div",
|
|
@@ -512,14 +519,21 @@ export function createIslandElement(
|
|
|
512
519
|
*/
|
|
513
520
|
export function parseIslandsFromHTML(html: string): IslandState[] {
|
|
514
521
|
const islands: IslandState[] = [];
|
|
515
|
-
const regex =
|
|
522
|
+
const regex =
|
|
523
|
+
/data-island-id="([^"]+)"[^>]*data-island-component="([^"]+)"[^>]*data-island-props="([^"]+)"[^>]*data-island-strategy="([^"]+)"/g;
|
|
516
524
|
|
|
517
525
|
let match;
|
|
518
526
|
while ((match = regex.exec(html)) !== null) {
|
|
519
527
|
islands.push({
|
|
520
528
|
id: match[1],
|
|
521
529
|
component: match[2],
|
|
522
|
-
props: JSON.parse(
|
|
530
|
+
props: JSON.parse(
|
|
531
|
+
match[3]
|
|
532
|
+
.replace(/"/g, '"')
|
|
533
|
+
.replace(/&/g, "&")
|
|
534
|
+
.replace(/</g, "<")
|
|
535
|
+
.replace(/>/g, ">"),
|
|
536
|
+
),
|
|
523
537
|
strategy: match[4] as IslandHydrationStrategy,
|
|
524
538
|
hydrated: false,
|
|
525
539
|
});
|
|
@@ -531,7 +545,9 @@ export function parseIslandsFromHTML(html: string): IslandState[] {
|
|
|
531
545
|
/**
|
|
532
546
|
* Get hydration priority
|
|
533
547
|
*/
|
|
534
|
-
export function getHydrationPriority(
|
|
548
|
+
export function getHydrationPriority(
|
|
549
|
+
strategy: IslandHydrationStrategy,
|
|
550
|
+
): number {
|
|
535
551
|
const priorities: Record<IslandHydrationStrategy, number> = {
|
|
536
552
|
eager: 1,
|
|
537
553
|
visible: 2,
|
|
@@ -545,8 +561,11 @@ export function getHydrationPriority(strategy: IslandHydrationStrategy): number
|
|
|
545
561
|
/**
|
|
546
562
|
* Sort islands by hydration priority
|
|
547
563
|
*/
|
|
548
|
-
export function sortIslandsByPriority(
|
|
564
|
+
export function sortIslandsByPriority(
|
|
565
|
+
islands: IslandDefinition[],
|
|
566
|
+
): IslandDefinition[] {
|
|
549
567
|
return [...islands].sort(
|
|
550
|
-
(a, b) =>
|
|
568
|
+
(a, b) =>
|
|
569
|
+
getHydrationPriority(a.strategy) - getHydrationPriority(b.strategy),
|
|
551
570
|
);
|
|
552
|
-
}
|
|
571
|
+
}
|
package/src/frontend/isr.ts
CHANGED
|
@@ -8,18 +8,18 @@
|
|
|
8
8
|
* - Distributed cache support via Bun.redis
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { type Logger, createLogger } from "../logger/index.js";
|
|
12
|
+
import { type SSRRenderer, createSSRContext } from "./ssr.js";
|
|
12
13
|
import type {
|
|
13
|
-
ISRConfig,
|
|
14
|
-
PartialISRConfig,
|
|
15
14
|
ISRCacheEntry,
|
|
15
|
+
ISRConfig,
|
|
16
16
|
ISRPageConfig,
|
|
17
17
|
ISRRevalidationResult,
|
|
18
18
|
ISRStats,
|
|
19
|
+
PartialISRConfig,
|
|
19
20
|
SSRRenderOptions,
|
|
20
21
|
} from "./types.js";
|
|
21
|
-
import {
|
|
22
|
-
import type { SSRContext, RenderResult } from "./types.js";
|
|
22
|
+
import type { RenderResult, SSRContext } from "./types.js";
|
|
23
23
|
|
|
24
24
|
// ============= Constants =============
|
|
25
25
|
|
|
@@ -31,7 +31,7 @@ const DEFAULT_STALE_WHILE_REVALIDATE = 60; // 1 minute
|
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* ISR Manager handles incremental static regeneration
|
|
34
|
-
*
|
|
34
|
+
*
|
|
35
35
|
* Features:
|
|
36
36
|
* - Time-based revalidation with configurable TTL
|
|
37
37
|
* - Stale-while-revalidate for instant responses
|
|
@@ -42,7 +42,8 @@ export class ISRManager {
|
|
|
42
42
|
private config: ISRConfig;
|
|
43
43
|
private logger: Logger;
|
|
44
44
|
private cache: Map<string, ISRCacheEntry> = new Map();
|
|
45
|
-
private pendingRegenerations: Map<string, Promise<ISRRevalidationResult>> =
|
|
45
|
+
private pendingRegenerations: Map<string, Promise<ISRRevalidationResult>> =
|
|
46
|
+
new Map();
|
|
46
47
|
private ssrRenderer: SSRRenderer | null = null;
|
|
47
48
|
private stats = {
|
|
48
49
|
hits: 0,
|
|
@@ -67,7 +68,8 @@ export class ISRManager {
|
|
|
67
68
|
return {
|
|
68
69
|
cacheDir: config.cacheDir ?? DEFAULT_CACHE_DIR,
|
|
69
70
|
defaultRevalidate: config.defaultRevalidate ?? DEFAULT_REVALIDATE,
|
|
70
|
-
staleWhileRevalidate:
|
|
71
|
+
staleWhileRevalidate:
|
|
72
|
+
config.staleWhileRevalidate ?? DEFAULT_STALE_WHILE_REVALIDATE,
|
|
71
73
|
maxCacheSize: config.maxCacheSize ?? 1000,
|
|
72
74
|
redis: config.redis,
|
|
73
75
|
redisKeyPrefix: config.redisKeyPrefix ?? "bueno:isr:",
|
|
@@ -88,7 +90,7 @@ export class ISRManager {
|
|
|
88
90
|
async getPage(
|
|
89
91
|
url: string,
|
|
90
92
|
request: Request,
|
|
91
|
-
pageConfig?: ISRPageConfig
|
|
93
|
+
pageConfig?: ISRPageConfig,
|
|
92
94
|
): Promise<RenderResult> {
|
|
93
95
|
if (!this.config.enabled) {
|
|
94
96
|
return this.renderPage(url, request);
|
|
@@ -100,8 +102,10 @@ export class ISRManager {
|
|
|
100
102
|
if (entry) {
|
|
101
103
|
const now = Date.now();
|
|
102
104
|
const age = (now - entry.timestamp) / 1000;
|
|
103
|
-
const revalidate =
|
|
104
|
-
|
|
105
|
+
const revalidate =
|
|
106
|
+
pageConfig?.revalidate ?? this.config.defaultRevalidate;
|
|
107
|
+
const staleWhileRevalidate =
|
|
108
|
+
pageConfig?.staleWhileRevalidate ?? this.config.staleWhileRevalidate;
|
|
105
109
|
|
|
106
110
|
// Cache hit - check if stale
|
|
107
111
|
if (age < revalidate) {
|
|
@@ -114,7 +118,9 @@ export class ISRManager {
|
|
|
114
118
|
// Stale but within stale-while-revalidate window
|
|
115
119
|
if (age < revalidate + staleWhileRevalidate) {
|
|
116
120
|
this.stats.staleHits++;
|
|
117
|
-
this.logger.debug(
|
|
121
|
+
this.logger.debug(
|
|
122
|
+
`Cache hit (stale): ${url}, revalidating in background`,
|
|
123
|
+
);
|
|
118
124
|
|
|
119
125
|
// Trigger background revalidation
|
|
120
126
|
this.triggerBackgroundRevalidation(url, request, pageConfig);
|
|
@@ -137,7 +143,10 @@ export class ISRManager {
|
|
|
137
143
|
/**
|
|
138
144
|
* Render a page using SSR
|
|
139
145
|
*/
|
|
140
|
-
private async renderPage(
|
|
146
|
+
private async renderPage(
|
|
147
|
+
url: string,
|
|
148
|
+
request: Request,
|
|
149
|
+
): Promise<RenderResult> {
|
|
141
150
|
if (!this.ssrRenderer) {
|
|
142
151
|
throw new Error("SSR renderer not configured");
|
|
143
152
|
}
|
|
@@ -159,7 +168,7 @@ export class ISRManager {
|
|
|
159
168
|
private triggerBackgroundRevalidation(
|
|
160
169
|
url: string,
|
|
161
170
|
request: Request,
|
|
162
|
-
pageConfig?: ISRPageConfig
|
|
171
|
+
pageConfig?: ISRPageConfig,
|
|
163
172
|
): void {
|
|
164
173
|
const cacheKey = this.getCacheKey(url);
|
|
165
174
|
|
|
@@ -168,10 +177,11 @@ export class ISRManager {
|
|
|
168
177
|
return;
|
|
169
178
|
}
|
|
170
179
|
|
|
171
|
-
const promise = this.revalidatePage(url, request, pageConfig)
|
|
172
|
-
|
|
180
|
+
const promise = this.revalidatePage(url, request, pageConfig).finally(
|
|
181
|
+
() => {
|
|
173
182
|
this.pendingRegenerations.delete(cacheKey);
|
|
174
|
-
}
|
|
183
|
+
},
|
|
184
|
+
);
|
|
175
185
|
|
|
176
186
|
this.pendingRegenerations.set(cacheKey, promise);
|
|
177
187
|
}
|
|
@@ -182,7 +192,7 @@ export class ISRManager {
|
|
|
182
192
|
async revalidatePage(
|
|
183
193
|
url: string,
|
|
184
194
|
request: Request,
|
|
185
|
-
pageConfig?: ISRPageConfig
|
|
195
|
+
pageConfig?: ISRPageConfig,
|
|
186
196
|
): Promise<ISRRevalidationResult> {
|
|
187
197
|
const cacheKey = this.getCacheKey(url);
|
|
188
198
|
const startTime = Date.now();
|
|
@@ -205,7 +215,8 @@ export class ISRManager {
|
|
|
205
215
|
};
|
|
206
216
|
} catch (error) {
|
|
207
217
|
const duration = Date.now() - startTime;
|
|
208
|
-
const errorMessage =
|
|
218
|
+
const errorMessage =
|
|
219
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
209
220
|
this.logger.error(`Revalidation failed: ${url}`, error);
|
|
210
221
|
|
|
211
222
|
return {
|
|
@@ -258,7 +269,9 @@ export class ISRManager {
|
|
|
258
269
|
// Invalidate Redis cache if available
|
|
259
270
|
if (this.config.redis) {
|
|
260
271
|
try {
|
|
261
|
-
const keys = await this.config.redis.keys(
|
|
272
|
+
const keys = await this.config.redis.keys(
|
|
273
|
+
`${this.config.redisKeyPrefix}*`,
|
|
274
|
+
);
|
|
262
275
|
for (const key of keys) {
|
|
263
276
|
const cacheKey = key.replace(this.config.redisKeyPrefix, "");
|
|
264
277
|
if (regex.test(cacheKey)) {
|
|
@@ -284,7 +297,9 @@ export class ISRManager {
|
|
|
284
297
|
|
|
285
298
|
if (this.config.redis) {
|
|
286
299
|
try {
|
|
287
|
-
const keys = await this.config.redis.keys(
|
|
300
|
+
const keys = await this.config.redis.keys(
|
|
301
|
+
`${this.config.redisKeyPrefix}*`,
|
|
302
|
+
);
|
|
288
303
|
if (keys.length > 0) {
|
|
289
304
|
await this.config.redis.del(...keys);
|
|
290
305
|
}
|
|
@@ -309,7 +324,9 @@ export class ISRManager {
|
|
|
309
324
|
// Check Redis if available
|
|
310
325
|
if (this.config.redis) {
|
|
311
326
|
try {
|
|
312
|
-
const data = await this.config.redis.get(
|
|
327
|
+
const data = await this.config.redis.get(
|
|
328
|
+
`${this.config.redisKeyPrefix}${key}`,
|
|
329
|
+
);
|
|
313
330
|
if (data) {
|
|
314
331
|
const entry = JSON.parse(data) as ISRCacheEntry;
|
|
315
332
|
// Cache locally for faster access
|
|
@@ -330,7 +347,7 @@ export class ISRManager {
|
|
|
330
347
|
private async setCacheEntry(
|
|
331
348
|
key: string,
|
|
332
349
|
result: RenderResult,
|
|
333
|
-
pageConfig?: ISRPageConfig
|
|
350
|
+
pageConfig?: ISRPageConfig,
|
|
334
351
|
): Promise<void> {
|
|
335
352
|
const entry: ISRCacheEntry = {
|
|
336
353
|
result,
|
|
@@ -354,7 +371,7 @@ export class ISRManager {
|
|
|
354
371
|
await this.config.redis.set(
|
|
355
372
|
`${this.config.redisKeyPrefix}${key}`,
|
|
356
373
|
JSON.stringify(entry),
|
|
357
|
-
{ EX: ttl }
|
|
374
|
+
{ EX: ttl },
|
|
358
375
|
);
|
|
359
376
|
} catch (error) {
|
|
360
377
|
this.logger.error("Failed to set Redis cache entry", error);
|
|
@@ -366,10 +383,14 @@ export class ISRManager {
|
|
|
366
383
|
* Evict oldest entries when cache is full
|
|
367
384
|
*/
|
|
368
385
|
private evictOldestEntries(): void {
|
|
369
|
-
const entries = Array.from(this.cache.entries())
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
386
|
+
const entries = Array.from(this.cache.entries()).sort(
|
|
387
|
+
(a, b) => a[1].timestamp - b[1].timestamp,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const toEvict = entries.slice(
|
|
391
|
+
0,
|
|
392
|
+
Math.floor(this.config.maxCacheSize * 0.1),
|
|
393
|
+
);
|
|
373
394
|
for (const [key] of toEvict) {
|
|
374
395
|
this.cache.delete(key);
|
|
375
396
|
}
|
|
@@ -398,9 +419,10 @@ export class ISRManager {
|
|
|
398
419
|
...this.stats,
|
|
399
420
|
cacheSize: this.cache.size,
|
|
400
421
|
pendingRevalidations: this.pendingRegenerations.size,
|
|
401
|
-
hitRate:
|
|
402
|
-
|
|
403
|
-
|
|
422
|
+
hitRate:
|
|
423
|
+
this.stats.hits + this.stats.misses > 0
|
|
424
|
+
? this.stats.hits / (this.stats.hits + this.stats.misses)
|
|
425
|
+
: 0,
|
|
404
426
|
};
|
|
405
427
|
}
|
|
406
428
|
|
|
@@ -517,15 +539,15 @@ export function parseRevalidateHeader(header: string): {
|
|
|
517
539
|
revalidate: number;
|
|
518
540
|
staleWhileRevalidate: number;
|
|
519
541
|
} {
|
|
520
|
-
const parts = header.split(",").map(p => p.trim());
|
|
542
|
+
const parts = header.split(",").map((p) => p.trim());
|
|
521
543
|
let revalidate = DEFAULT_REVALIDATE;
|
|
522
544
|
let staleWhileRevalidate = DEFAULT_STALE_WHILE_REVALIDATE;
|
|
523
545
|
|
|
524
546
|
for (const part of parts) {
|
|
525
547
|
if (part.includes("stale-while-revalidate=")) {
|
|
526
|
-
staleWhileRevalidate = parseInt(part.split("=")[1], 10);
|
|
548
|
+
staleWhileRevalidate = Number.parseInt(part.split("=")[1], 10);
|
|
527
549
|
} else {
|
|
528
|
-
revalidate = parseInt(part, 10);
|
|
550
|
+
revalidate = Number.parseInt(part, 10);
|
|
529
551
|
}
|
|
530
552
|
}
|
|
531
553
|
|
|
@@ -537,7 +559,7 @@ export function parseRevalidateHeader(header: string): {
|
|
|
537
559
|
*/
|
|
538
560
|
export function generateCacheControlHeader(
|
|
539
561
|
revalidate: number,
|
|
540
|
-
staleWhileRevalidate: number
|
|
562
|
+
staleWhileRevalidate: number,
|
|
541
563
|
): string {
|
|
542
564
|
return `public, max-age=0, s-maxage=${revalidate}, stale-while-revalidate=${staleWhileRevalidate}`;
|
|
543
565
|
}
|
|
@@ -548,8 +570,8 @@ export function generateCacheControlHeader(
|
|
|
548
570
|
export function shouldRegenerate(
|
|
549
571
|
entry: ISRCacheEntry,
|
|
550
572
|
revalidate: number,
|
|
551
|
-
staleWhileRevalidate: number
|
|
573
|
+
staleWhileRevalidate: number,
|
|
552
574
|
): boolean {
|
|
553
575
|
const age = (Date.now() - entry.timestamp) / 1000;
|
|
554
576
|
return age > revalidate && age <= revalidate + staleWhileRevalidate;
|
|
555
|
-
}
|
|
577
|
+
}
|
package/src/frontend/layout.ts
CHANGED
|
@@ -8,20 +8,20 @@
|
|
|
8
8
|
* - Per-segment layouts
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { type Logger, createLogger } from "../logger/index.js";
|
|
12
12
|
import type {
|
|
13
|
+
LayoutConfig,
|
|
13
14
|
LayoutDefinition,
|
|
15
|
+
LayoutMiddleware,
|
|
14
16
|
LayoutNode,
|
|
15
|
-
LayoutTree,
|
|
16
17
|
LayoutProps,
|
|
17
|
-
LayoutRenderer,
|
|
18
|
-
LayoutMiddleware,
|
|
19
|
-
LayoutConfig,
|
|
20
|
-
PartialLayoutConfig,
|
|
21
18
|
LayoutRenderResult,
|
|
19
|
+
LayoutRenderer,
|
|
22
20
|
LayoutSegment,
|
|
21
|
+
LayoutTree,
|
|
22
|
+
PartialLayoutConfig,
|
|
23
23
|
} from "./types.js";
|
|
24
|
-
import type { SSRContext, SSRElement
|
|
24
|
+
import type { RenderResult, SSRContext, SSRElement } from "./types.js";
|
|
25
25
|
|
|
26
26
|
// ============= Constants =============
|
|
27
27
|
|
|
@@ -70,7 +70,9 @@ export class LayoutManager {
|
|
|
70
70
|
* Initialize the layout manager by scanning for layout files
|
|
71
71
|
*/
|
|
72
72
|
async init(): Promise<void> {
|
|
73
|
-
this.logger.info(
|
|
73
|
+
this.logger.info(
|
|
74
|
+
`Initializing layout manager from: ${this.config.pagesDir}`,
|
|
75
|
+
);
|
|
74
76
|
await this.scanLayoutFiles();
|
|
75
77
|
this.buildLayoutTree();
|
|
76
78
|
this.logger.info(`Loaded ${this.layouts.size} layouts`);
|
|
@@ -81,7 +83,9 @@ export class LayoutManager {
|
|
|
81
83
|
*/
|
|
82
84
|
private async scanLayoutFiles(): Promise<void> {
|
|
83
85
|
const pagesPath = this.config.pagesDir;
|
|
84
|
-
const glob = new Bun.Glob(
|
|
86
|
+
const glob = new Bun.Glob(
|
|
87
|
+
`**/${LAYOUT_FILE}{${this.config.extensions.join(",")}}`,
|
|
88
|
+
);
|
|
85
89
|
|
|
86
90
|
try {
|
|
87
91
|
for await (const file of glob.scan(pagesPath)) {
|
|
@@ -95,7 +99,10 @@ export class LayoutManager {
|
|
|
95
99
|
/**
|
|
96
100
|
* Process a single layout file
|
|
97
101
|
*/
|
|
98
|
-
private async processLayoutFile(
|
|
102
|
+
private async processLayoutFile(
|
|
103
|
+
filePath: string,
|
|
104
|
+
basePath: string,
|
|
105
|
+
): Promise<void> {
|
|
99
106
|
const fullPath = `${basePath}/${filePath}`;
|
|
100
107
|
const segment = this.getLayoutSegment(filePath);
|
|
101
108
|
|
|
@@ -115,7 +122,10 @@ export class LayoutManager {
|
|
|
115
122
|
*/
|
|
116
123
|
private getLayoutSegment(filePath: string): string {
|
|
117
124
|
// Remove _layout.tsx from path
|
|
118
|
-
const segment = filePath.replace(
|
|
125
|
+
const segment = filePath.replace(
|
|
126
|
+
new RegExp(`/${LAYOUT_FILE}\\.(tsx?|jsx?)$`),
|
|
127
|
+
"",
|
|
128
|
+
);
|
|
119
129
|
return segment === "" ? "/" : `/${segment}`;
|
|
120
130
|
}
|
|
121
131
|
|
|
@@ -153,7 +163,10 @@ export class LayoutManager {
|
|
|
153
163
|
/**
|
|
154
164
|
* Build a layout tree node
|
|
155
165
|
*/
|
|
156
|
-
private buildTreeNode(
|
|
166
|
+
private buildTreeNode(
|
|
167
|
+
layout: LayoutDefinition,
|
|
168
|
+
parent: LayoutNode | null,
|
|
169
|
+
): LayoutNode {
|
|
157
170
|
const node: LayoutNode = {
|
|
158
171
|
layout,
|
|
159
172
|
parent,
|
|
@@ -224,7 +237,9 @@ export class LayoutManager {
|
|
|
224
237
|
/**
|
|
225
238
|
* Load layout module
|
|
226
239
|
*/
|
|
227
|
-
private async loadLayoutModule(
|
|
240
|
+
private async loadLayoutModule(
|
|
241
|
+
filePath: string,
|
|
242
|
+
): Promise<LayoutRenderer | null> {
|
|
228
243
|
try {
|
|
229
244
|
const module = await import(filePath);
|
|
230
245
|
return module.default || module;
|
|
@@ -240,7 +255,7 @@ export class LayoutManager {
|
|
|
240
255
|
async renderLayouts(
|
|
241
256
|
routePath: string,
|
|
242
257
|
content: string,
|
|
243
|
-
context: SSRContext
|
|
258
|
+
context: SSRContext,
|
|
244
259
|
): Promise<LayoutRenderResult> {
|
|
245
260
|
const chain = this.getLayoutChain(routePath);
|
|
246
261
|
|
|
@@ -361,7 +376,9 @@ export class LayoutManager {
|
|
|
361
376
|
/**
|
|
362
377
|
* Create a layout manager
|
|
363
378
|
*/
|
|
364
|
-
export function createLayoutManager(
|
|
379
|
+
export function createLayoutManager(
|
|
380
|
+
config: PartialLayoutConfig = {},
|
|
381
|
+
): LayoutManager {
|
|
365
382
|
return new LayoutManager(config);
|
|
366
383
|
}
|
|
367
384
|
|
|
@@ -394,7 +411,7 @@ export function getLayoutSegmentFromPath(filePath: string): string {
|
|
|
394
411
|
*/
|
|
395
412
|
export function buildLayoutProps(
|
|
396
413
|
children: string,
|
|
397
|
-
context: SSRContext
|
|
414
|
+
context: SSRContext,
|
|
398
415
|
): LayoutProps {
|
|
399
416
|
return {
|
|
400
417
|
children,
|
|
@@ -409,7 +426,7 @@ export function buildLayoutProps(
|
|
|
409
426
|
*/
|
|
410
427
|
export function createLayoutSegment(
|
|
411
428
|
path: string,
|
|
412
|
-
params: Record<string, string> = {}
|
|
429
|
+
params: Record<string, string> = {},
|
|
413
430
|
): LayoutSegment {
|
|
414
431
|
return {
|
|
415
432
|
path,
|
|
@@ -421,9 +438,7 @@ export function createLayoutSegment(
|
|
|
421
438
|
/**
|
|
422
439
|
* Merge layout head elements
|
|
423
440
|
*/
|
|
424
|
-
export function mergeLayoutHead(
|
|
425
|
-
...heads: SSRElement[][]
|
|
426
|
-
): SSRElement[] {
|
|
441
|
+
export function mergeLayoutHead(...heads: SSRElement[][]): SSRElement[] {
|
|
427
442
|
const merged: SSRElement[] = [];
|
|
428
443
|
const seen = new Set<string>();
|
|
429
444
|
|
|
@@ -472,4 +487,4 @@ export function layoutTreeToString(node: LayoutNode, indent = 0): string {
|
|
|
472
487
|
}
|
|
473
488
|
|
|
474
489
|
return result;
|
|
475
|
-
}
|
|
490
|
+
}
|