@buenojs/bueno 0.8.4 → 0.8.6
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 +264 -17
- package/dist/cli/{index.js → bin.js} +413 -332
- package/dist/container/index.js +273 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/graphql/index.js +2156 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +9694 -5047
- 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 +3411 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +795 -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/llms.txt +231 -0
- package/package.json +125 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/ARCHITECTURE.md +3 -3
- 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 +294 -232
- 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 +37 -18
- package/src/cli/templates/database/mysql.ts +3 -3
- package/src/cli/templates/database/none.ts +2 -2
- package/src/cli/templates/database/postgresql.ts +3 -3
- package/src/cli/templates/database/sqlite.ts +3 -3
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +33 -15
- package/src/cli/templates/frontend/none.ts +2 -2
- package/src/cli/templates/frontend/react.ts +18 -18
- package/src/cli/templates/frontend/solid.ts +15 -15
- package/src/cli/templates/frontend/svelte.ts +17 -17
- package/src/cli/templates/frontend/vue.ts +15 -15
- package/src/cli/templates/generators/index.ts +29 -29
- package/src/cli/templates/generators/types.ts +21 -21
- package/src/cli/templates/index.ts +6 -6
- package/src/cli/templates/project/api.ts +37 -36
- package/src/cli/templates/project/default.ts +25 -25
- package/src/cli/templates/project/fullstack.ts +28 -26
- package/src/cli/templates/project/index.ts +55 -16
- package/src/cli/templates/project/minimal.ts +17 -12
- package/src/cli/templates/project/types.ts +10 -5
- package/src/cli/templates/project/website.ts +15 -15
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -3
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +14 -8
- 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 +566 -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/graphql/built-in-engine.ts +598 -0
- package/src/graphql/context-builder.ts +110 -0
- package/src/graphql/decorators.ts +358 -0
- package/src/graphql/execution-pipeline.ts +227 -0
- package/src/graphql/graphql-module.ts +563 -0
- package/src/graphql/index.ts +101 -0
- package/src/graphql/metadata.ts +237 -0
- package/src/graphql/schema-builder.ts +319 -0
- package/src/graphql/subscription-handler.ts +283 -0
- package/src/graphql/types.ts +324 -0
- 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 +182 -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 +457 -299
- 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/cli.test.ts +19 -19
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/cli.test.ts +1 -1
- 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/graphql.test.ts +991 -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/telemetry/index.ts
CHANGED
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
/**
|
|
11
11
|
* Span kind enumeration
|
|
12
12
|
*/
|
|
13
|
-
export type SpanKind =
|
|
13
|
+
export type SpanKind =
|
|
14
|
+
| "server"
|
|
15
|
+
| "client"
|
|
16
|
+
| "producer"
|
|
17
|
+
| "consumer"
|
|
18
|
+
| "internal";
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* Span status code
|
|
@@ -80,7 +85,11 @@ export interface SpanOptions {
|
|
|
80
85
|
/** Initial attributes */
|
|
81
86
|
attributes?: Record<string, string | number | boolean>;
|
|
82
87
|
/** Links to other spans */
|
|
83
|
-
links?: Array<{
|
|
88
|
+
links?: Array<{
|
|
89
|
+
traceId: string;
|
|
90
|
+
spanId: string;
|
|
91
|
+
attributes?: Record<string, string | number | boolean>;
|
|
92
|
+
}>;
|
|
84
93
|
/** Start time in nanoseconds (defaults to current time) */
|
|
85
94
|
startTime?: number;
|
|
86
95
|
}
|
|
@@ -215,7 +224,7 @@ export function generateSpanId(): string {
|
|
|
215
224
|
function hexToBase64(hex: string): string {
|
|
216
225
|
const bytes = new Uint8Array(hex.length / 2);
|
|
217
226
|
for (let i = 0; i < hex.length; i += 2) {
|
|
218
|
-
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
227
|
+
bytes[i / 2] = Number.parseInt(hex.substring(i, i + 2), 16);
|
|
219
228
|
}
|
|
220
229
|
return btoa(String.fromCharCode(...bytes));
|
|
221
230
|
}
|
|
@@ -230,7 +239,10 @@ export function nowNanoseconds(): number {
|
|
|
230
239
|
/**
|
|
231
240
|
* Convert an attribute value to OTLP format
|
|
232
241
|
*/
|
|
233
|
-
function toOTLPAttribute(
|
|
242
|
+
function toOTLPAttribute(
|
|
243
|
+
key: string,
|
|
244
|
+
value: string | number | boolean,
|
|
245
|
+
): OTLPAttribute {
|
|
234
246
|
if (typeof value === "string") {
|
|
235
247
|
return { key, value: { stringValue: value } };
|
|
236
248
|
} else if (typeof value === "number") {
|
|
@@ -289,7 +301,7 @@ export class OTLPExporter {
|
|
|
289
301
|
private pendingSpans: Span[] = [];
|
|
290
302
|
private exportTimer: Timer | null = null;
|
|
291
303
|
private isShuttingDown = false;
|
|
292
|
-
private serviceName
|
|
304
|
+
private serviceName = "unknown-service";
|
|
293
305
|
private resourceAttributes: Record<string, string | number | boolean> = {};
|
|
294
306
|
|
|
295
307
|
constructor(options: OTLPExporterOptions) {
|
|
@@ -315,7 +327,9 @@ export class OTLPExporter {
|
|
|
315
327
|
/**
|
|
316
328
|
* Set resource attributes
|
|
317
329
|
*/
|
|
318
|
-
setResourceAttributes(
|
|
330
|
+
setResourceAttributes(
|
|
331
|
+
attributes: Record<string, string | number | boolean>,
|
|
332
|
+
): void {
|
|
319
333
|
this.resourceAttributes = { ...attributes };
|
|
320
334
|
}
|
|
321
335
|
|
|
@@ -444,17 +458,23 @@ export class OTLPExporter {
|
|
|
444
458
|
const otlpSpans: OTLPSpan[] = spans.map((span) => ({
|
|
445
459
|
traceId: hexToBase64(span.traceId),
|
|
446
460
|
spanId: hexToBase64(span.spanId),
|
|
447
|
-
parentSpanId: span.parentSpanId
|
|
461
|
+
parentSpanId: span.parentSpanId
|
|
462
|
+
? hexToBase64(span.parentSpanId)
|
|
463
|
+
: undefined,
|
|
448
464
|
name: span.name,
|
|
449
465
|
kind: spanKindToOTLP(span.kind),
|
|
450
466
|
startTimeUnixNano: span.startTime,
|
|
451
467
|
endTimeUnixNano: span.endTime ?? span.startTime,
|
|
452
|
-
attributes: Object.entries(span.attributes).map(([k, v]) =>
|
|
468
|
+
attributes: Object.entries(span.attributes).map(([k, v]) =>
|
|
469
|
+
toOTLPAttribute(k, v),
|
|
470
|
+
),
|
|
453
471
|
events: span.events.map((event) => ({
|
|
454
472
|
timeUnixNano: event.timestamp,
|
|
455
473
|
name: event.name,
|
|
456
474
|
attributes: event.attributes
|
|
457
|
-
? Object.entries(event.attributes).map(([k, v]) =>
|
|
475
|
+
? Object.entries(event.attributes).map(([k, v]) =>
|
|
476
|
+
toOTLPAttribute(k, v),
|
|
477
|
+
)
|
|
458
478
|
: [],
|
|
459
479
|
})),
|
|
460
480
|
status: {
|
|
@@ -612,14 +632,19 @@ export class Tracer {
|
|
|
612
632
|
|
|
613
633
|
// Pop from stack
|
|
614
634
|
this.spanStack.pop();
|
|
615
|
-
this.currentSpan =
|
|
635
|
+
this.currentSpan =
|
|
636
|
+
previousSpan ?? this.spanStack[this.spanStack.length - 1] ?? null;
|
|
616
637
|
}
|
|
617
638
|
}
|
|
618
639
|
|
|
619
640
|
/**
|
|
620
641
|
* Add an event to a span
|
|
621
642
|
*/
|
|
622
|
-
addEvent(
|
|
643
|
+
addEvent(
|
|
644
|
+
span: Span,
|
|
645
|
+
name: string,
|
|
646
|
+
attributes?: Record<string, string | number | boolean>,
|
|
647
|
+
): void {
|
|
623
648
|
if (span.ended) return;
|
|
624
649
|
|
|
625
650
|
span.events.push({
|
|
@@ -632,7 +657,11 @@ export class Tracer {
|
|
|
632
657
|
/**
|
|
633
658
|
* Set an attribute on a span
|
|
634
659
|
*/
|
|
635
|
-
setAttribute(
|
|
660
|
+
setAttribute(
|
|
661
|
+
span: Span,
|
|
662
|
+
key: string,
|
|
663
|
+
value: string | number | boolean,
|
|
664
|
+
): void {
|
|
636
665
|
if (span.ended) return;
|
|
637
666
|
span.attributes[key] = value;
|
|
638
667
|
}
|
|
@@ -640,7 +669,10 @@ export class Tracer {
|
|
|
640
669
|
/**
|
|
641
670
|
* Set multiple attributes on a span
|
|
642
671
|
*/
|
|
643
|
-
setAttributes(
|
|
672
|
+
setAttributes(
|
|
673
|
+
span: Span,
|
|
674
|
+
attributes: Record<string, string | number | boolean>,
|
|
675
|
+
): void {
|
|
644
676
|
if (span.ended) return;
|
|
645
677
|
Object.assign(span.attributes, attributes);
|
|
646
678
|
}
|
|
@@ -725,7 +757,7 @@ export class Tracer {
|
|
|
725
757
|
return {
|
|
726
758
|
traceId,
|
|
727
759
|
spanId,
|
|
728
|
-
traceFlags: parseInt(flags, 16),
|
|
760
|
+
traceFlags: Number.parseInt(flags, 16),
|
|
729
761
|
traceState: carrier["tracestate"] ?? carrier["Tracestate"],
|
|
730
762
|
};
|
|
731
763
|
}
|
|
@@ -789,7 +821,10 @@ export class Tracer {
|
|
|
789
821
|
/**
|
|
790
822
|
* Create a configured tracer
|
|
791
823
|
*/
|
|
792
|
-
export function createTracer(
|
|
824
|
+
export function createTracer(
|
|
825
|
+
serviceName: string,
|
|
826
|
+
options: Omit<TracerOptions, "serviceName"> = {},
|
|
827
|
+
): Tracer {
|
|
793
828
|
return new Tracer({
|
|
794
829
|
...options,
|
|
795
830
|
serviceName,
|
|
@@ -847,7 +882,11 @@ export function traceMiddleware(tracer: Tracer) {
|
|
|
847
882
|
|
|
848
883
|
let span: Span;
|
|
849
884
|
if (parentContext) {
|
|
850
|
-
span = tracer.startSpanFromContext(
|
|
885
|
+
span = tracer.startSpanFromContext(
|
|
886
|
+
`${ctx.method} ${ctx.path}`,
|
|
887
|
+
parentContext,
|
|
888
|
+
spanOptions,
|
|
889
|
+
);
|
|
851
890
|
} else {
|
|
852
891
|
span = tracer.startSpan(`${ctx.method} ${ctx.path}`, spanOptions);
|
|
853
892
|
}
|
|
@@ -895,7 +934,11 @@ interface TracedDatabase {
|
|
|
895
934
|
/**
|
|
896
935
|
* Wrap database with tracing
|
|
897
936
|
*/
|
|
898
|
-
export function traceDatabase(
|
|
937
|
+
export function traceDatabase(
|
|
938
|
+
tracer: Tracer,
|
|
939
|
+
db: TracedDatabase,
|
|
940
|
+
system = "unknown",
|
|
941
|
+
): TracedDatabase {
|
|
899
942
|
const tracedDb: TracedDatabase = { ...db };
|
|
900
943
|
|
|
901
944
|
// Wrap query method
|
|
@@ -958,7 +1001,9 @@ export function traceDatabase(tracer: Tracer, db: TracedDatabase, system: string
|
|
|
958
1001
|
*/
|
|
959
1002
|
function extractOperation(sql: string): string {
|
|
960
1003
|
const normalized = sql.trim().toUpperCase();
|
|
961
|
-
const match = normalized.match(
|
|
1004
|
+
const match = normalized.match(
|
|
1005
|
+
/^(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TRUNCATE)/,
|
|
1006
|
+
);
|
|
962
1007
|
return match ? match[1] : "UNKNOWN";
|
|
963
1008
|
}
|
|
964
1009
|
|
|
@@ -977,7 +1022,9 @@ interface TracedFetchOptions extends RequestInit {
|
|
|
977
1022
|
/**
|
|
978
1023
|
* Create a traced fetch function
|
|
979
1024
|
*/
|
|
980
|
-
export function createTracedFetch(
|
|
1025
|
+
export function createTracedFetch(
|
|
1026
|
+
tracer: Tracer,
|
|
1027
|
+
): (url: string | URL, options?: TracedFetchOptions) => Promise<Response> {
|
|
981
1028
|
return async (url: string | URL, options: TracedFetchOptions = {}) => {
|
|
982
1029
|
const { parentSpan, attributes = {}, ...fetchOptions } = options;
|
|
983
1030
|
const urlStr = url.toString();
|
|
@@ -1052,7 +1099,10 @@ export class SpanBuilder {
|
|
|
1052
1099
|
/**
|
|
1053
1100
|
* Add an event
|
|
1054
1101
|
*/
|
|
1055
|
-
addEvent(
|
|
1102
|
+
addEvent(
|
|
1103
|
+
name: string,
|
|
1104
|
+
attributes?: Record<string, string | number | boolean>,
|
|
1105
|
+
): this {
|
|
1056
1106
|
this.tracer.addEvent(this.span, name, attributes);
|
|
1057
1107
|
return this;
|
|
1058
1108
|
}
|
|
@@ -1092,6 +1142,10 @@ export class SpanBuilder {
|
|
|
1092
1142
|
/**
|
|
1093
1143
|
* Create a span builder
|
|
1094
1144
|
*/
|
|
1095
|
-
export function span(
|
|
1145
|
+
export function span(
|
|
1146
|
+
tracer: Tracer,
|
|
1147
|
+
name: string,
|
|
1148
|
+
options: SpanOptions = {},
|
|
1149
|
+
): SpanBuilder {
|
|
1096
1150
|
return new SpanBuilder(tracer, name, options);
|
|
1097
|
-
}
|
|
1151
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Engine
|
|
3
|
+
*
|
|
4
|
+
* Main orchestration class for:
|
|
5
|
+
* - Loading templates (with caching)
|
|
6
|
+
* - Rendering with variable interpolation
|
|
7
|
+
* - Converting Markdown to HTML/text
|
|
8
|
+
* - Channel-specific variant selection
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { TemplateLoader } from "./loader";
|
|
12
|
+
import { MarkdownRenderer } from "./renderers/markdown";
|
|
13
|
+
import { SimpleRenderer } from "./renderers/simple";
|
|
14
|
+
import type {
|
|
15
|
+
RenderOptions,
|
|
16
|
+
Template,
|
|
17
|
+
TemplateData,
|
|
18
|
+
TemplateEngineConfig,
|
|
19
|
+
TemplateEngineMetrics,
|
|
20
|
+
} from "./types";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default channel-to-variant mapping
|
|
24
|
+
* When no variant is specified, auto-detect based on channel
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_CHANNEL_VARIANT_MAP: Record<string, string> = {
|
|
27
|
+
email: "email",
|
|
28
|
+
sms: "sms",
|
|
29
|
+
push: "push",
|
|
30
|
+
whatsapp: "whatsapp",
|
|
31
|
+
web: "web",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Template engine
|
|
36
|
+
*/
|
|
37
|
+
export class TemplateEngine {
|
|
38
|
+
private loader: TemplateLoader;
|
|
39
|
+
private simpleRenderer: SimpleRenderer;
|
|
40
|
+
private metrics: TemplateEngineMetrics = {
|
|
41
|
+
loaded: 0,
|
|
42
|
+
cacheHits: 0,
|
|
43
|
+
cacheMisses: 0,
|
|
44
|
+
avgRenderTime: 0,
|
|
45
|
+
totalRenders: 0,
|
|
46
|
+
};
|
|
47
|
+
private renderTimes: number[] = [];
|
|
48
|
+
private channelVariantMap: Record<string, string>;
|
|
49
|
+
|
|
50
|
+
constructor(private config: TemplateEngineConfig) {
|
|
51
|
+
this.loader = new TemplateLoader({
|
|
52
|
+
basePath: config.basePath,
|
|
53
|
+
cacheEnabled: config.cache?.enabled ?? true,
|
|
54
|
+
cacheTtl: config.cache?.ttl ?? 3600,
|
|
55
|
+
maxCacheSize: config.cache?.maxSize ?? 100,
|
|
56
|
+
watch: config.watch ?? false,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.simpleRenderer = new SimpleRenderer();
|
|
60
|
+
this.channelVariantMap =
|
|
61
|
+
config.channelVariantMap || DEFAULT_CHANNEL_VARIANT_MAP;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Render a template with data
|
|
66
|
+
*/
|
|
67
|
+
async render(
|
|
68
|
+
templateId: string,
|
|
69
|
+
data: TemplateData,
|
|
70
|
+
options?: RenderOptions,
|
|
71
|
+
): Promise<string> {
|
|
72
|
+
const startTime = Date.now();
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Load template
|
|
76
|
+
const template = this.loader.load(templateId);
|
|
77
|
+
|
|
78
|
+
// Determine variant to use
|
|
79
|
+
const variant = this._getVariant(template, options?.variant);
|
|
80
|
+
|
|
81
|
+
// Get variant content
|
|
82
|
+
let content = template.variants[variant];
|
|
83
|
+
if (!content) {
|
|
84
|
+
// Fallback to first available variant
|
|
85
|
+
content = Object.values(template.variants)[0] || "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Render with simple renderer (variables + filters + conditionals)
|
|
89
|
+
const interpolated = this.simpleRenderer.render(content, data);
|
|
90
|
+
|
|
91
|
+
// Determine output format
|
|
92
|
+
const outputFormat = options?.outputFormat || "html";
|
|
93
|
+
|
|
94
|
+
// Convert based on template format
|
|
95
|
+
let result: string;
|
|
96
|
+
if (template.format === "markdown") {
|
|
97
|
+
if (outputFormat === "text") {
|
|
98
|
+
result = MarkdownRenderer.toText(interpolated);
|
|
99
|
+
} else {
|
|
100
|
+
result = MarkdownRenderer.toHtml(interpolated);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
// Plain text or HTML template - return as-is
|
|
104
|
+
result = interpolated;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Update metrics
|
|
108
|
+
const duration = Date.now() - startTime;
|
|
109
|
+
this._updateMetrics(duration);
|
|
110
|
+
|
|
111
|
+
return result;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// If loading fails, throw with context
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Failed to render template "${templateId}": ${
|
|
116
|
+
error instanceof Error ? error.message : String(error)
|
|
117
|
+
}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Render template to HTML (convenience method)
|
|
124
|
+
*/
|
|
125
|
+
async renderToHtml(
|
|
126
|
+
templateId: string,
|
|
127
|
+
data: TemplateData,
|
|
128
|
+
variant?: string,
|
|
129
|
+
): Promise<string> {
|
|
130
|
+
return this.render(templateId, data, {
|
|
131
|
+
variant,
|
|
132
|
+
outputFormat: "html",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Render template to plain text (convenience method)
|
|
138
|
+
*/
|
|
139
|
+
async renderToText(
|
|
140
|
+
templateId: string,
|
|
141
|
+
data: TemplateData,
|
|
142
|
+
variant?: string,
|
|
143
|
+
): Promise<string> {
|
|
144
|
+
return this.render(templateId, data, {
|
|
145
|
+
variant,
|
|
146
|
+
outputFormat: "text",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get variant for rendering
|
|
152
|
+
*/
|
|
153
|
+
private _getVariant(template: Template, explicitVariant?: string): string {
|
|
154
|
+
// Explicit variant takes priority
|
|
155
|
+
if (explicitVariant && template.variants[explicitVariant]) {
|
|
156
|
+
return explicitVariant;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Fall back to template default
|
|
160
|
+
if (template.metadata.default) {
|
|
161
|
+
const defaultVariant = String(template.metadata.default);
|
|
162
|
+
if (template.variants[defaultVariant]) {
|
|
163
|
+
return defaultVariant;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Use first available variant
|
|
168
|
+
return Object.keys(template.variants)[0] || "default";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get variant for a specific channel
|
|
173
|
+
* Useful for NotificationService to auto-detect correct variant
|
|
174
|
+
*/
|
|
175
|
+
getVariantForChannel(channel: string): string {
|
|
176
|
+
return this.channelVariantMap[channel.toLowerCase()] || channel;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Register custom filter
|
|
181
|
+
*/
|
|
182
|
+
registerFilter(
|
|
183
|
+
name: string,
|
|
184
|
+
fn: (value: unknown, ...args: unknown[]) => unknown,
|
|
185
|
+
): void {
|
|
186
|
+
this.simpleRenderer.registerFilter(name, fn);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Update metrics
|
|
191
|
+
*/
|
|
192
|
+
private _updateMetrics(duration: number): void {
|
|
193
|
+
this.metrics.totalRenders++;
|
|
194
|
+
this.renderTimes.push(duration);
|
|
195
|
+
|
|
196
|
+
// Keep only recent 100 measurements for average
|
|
197
|
+
if (this.renderTimes.length > 100) {
|
|
198
|
+
this.renderTimes.shift();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.metrics.avgRenderTime =
|
|
202
|
+
this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length;
|
|
203
|
+
|
|
204
|
+
// Update loader metrics
|
|
205
|
+
const loaderMetrics = this.loader.getMetrics();
|
|
206
|
+
this.metrics.loaded = loaderMetrics.loads;
|
|
207
|
+
this.metrics.cacheHits = loaderMetrics.cacheHits;
|
|
208
|
+
this.metrics.cacheMisses = loaderMetrics.cacheMisses;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get engine metrics
|
|
213
|
+
*/
|
|
214
|
+
getMetrics(): TemplateEngineMetrics {
|
|
215
|
+
return { ...this.metrics };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Clear caches
|
|
220
|
+
*/
|
|
221
|
+
clear(): void {
|
|
222
|
+
this.loader.clear();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template System Public API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from "./types";
|
|
6
|
+
export { TemplateEngine } from "./engine";
|
|
7
|
+
export { TemplateLoader } from "./loader";
|
|
8
|
+
export { SimpleRenderer } from "./renderers/simple";
|
|
9
|
+
export { MarkdownRenderer } from "./renderers/markdown";
|