@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.
Files changed (234) hide show
  1. package/README.md +264 -17
  2. package/dist/cli/{index.js → bin.js} +413 -332
  3. package/dist/container/index.js +273 -0
  4. package/dist/context/index.js +219 -0
  5. package/dist/database/index.js +493 -0
  6. package/dist/frontend/index.js +7697 -0
  7. package/dist/graphql/index.js +2156 -0
  8. package/dist/health/index.js +364 -0
  9. package/dist/i18n/index.js +345 -0
  10. package/dist/index.js +9694 -5047
  11. package/dist/jobs/index.js +819 -0
  12. package/dist/lock/index.js +367 -0
  13. package/dist/logger/index.js +281 -0
  14. package/dist/metrics/index.js +289 -0
  15. package/dist/middleware/index.js +77 -0
  16. package/dist/migrations/index.js +571 -0
  17. package/dist/modules/index.js +3411 -0
  18. package/dist/notification/index.js +484 -0
  19. package/dist/observability/index.js +331 -0
  20. package/dist/openapi/index.js +795 -0
  21. package/dist/orm/index.js +1356 -0
  22. package/dist/router/index.js +886 -0
  23. package/dist/rpc/index.js +691 -0
  24. package/dist/schema/index.js +400 -0
  25. package/dist/telemetry/index.js +595 -0
  26. package/dist/template/index.js +640 -0
  27. package/dist/templates/index.js +640 -0
  28. package/dist/testing/index.js +1111 -0
  29. package/dist/types/index.js +60 -0
  30. package/llms.txt +231 -0
  31. package/package.json +125 -27
  32. package/src/cache/index.ts +2 -1
  33. package/src/cli/ARCHITECTURE.md +3 -3
  34. package/src/cli/bin.ts +2 -2
  35. package/src/cli/commands/build.ts +183 -165
  36. package/src/cli/commands/dev.ts +96 -89
  37. package/src/cli/commands/generate.ts +142 -111
  38. package/src/cli/commands/help.ts +20 -16
  39. package/src/cli/commands/index.ts +3 -6
  40. package/src/cli/commands/migration.ts +124 -105
  41. package/src/cli/commands/new.ts +294 -232
  42. package/src/cli/commands/start.ts +81 -79
  43. package/src/cli/core/args.ts +68 -50
  44. package/src/cli/core/console.ts +89 -95
  45. package/src/cli/core/index.ts +4 -4
  46. package/src/cli/core/prompt.ts +65 -62
  47. package/src/cli/core/spinner.ts +23 -20
  48. package/src/cli/index.ts +46 -38
  49. package/src/cli/templates/database/index.ts +37 -18
  50. package/src/cli/templates/database/mysql.ts +3 -3
  51. package/src/cli/templates/database/none.ts +2 -2
  52. package/src/cli/templates/database/postgresql.ts +3 -3
  53. package/src/cli/templates/database/sqlite.ts +3 -3
  54. package/src/cli/templates/deploy.ts +29 -26
  55. package/src/cli/templates/docker.ts +41 -30
  56. package/src/cli/templates/frontend/index.ts +33 -15
  57. package/src/cli/templates/frontend/none.ts +2 -2
  58. package/src/cli/templates/frontend/react.ts +18 -18
  59. package/src/cli/templates/frontend/solid.ts +15 -15
  60. package/src/cli/templates/frontend/svelte.ts +17 -17
  61. package/src/cli/templates/frontend/vue.ts +15 -15
  62. package/src/cli/templates/generators/index.ts +29 -29
  63. package/src/cli/templates/generators/types.ts +21 -21
  64. package/src/cli/templates/index.ts +6 -6
  65. package/src/cli/templates/project/api.ts +37 -36
  66. package/src/cli/templates/project/default.ts +25 -25
  67. package/src/cli/templates/project/fullstack.ts +28 -26
  68. package/src/cli/templates/project/index.ts +55 -16
  69. package/src/cli/templates/project/minimal.ts +17 -12
  70. package/src/cli/templates/project/types.ts +10 -5
  71. package/src/cli/templates/project/website.ts +15 -15
  72. package/src/cli/utils/fs.ts +55 -41
  73. package/src/cli/utils/index.ts +3 -3
  74. package/src/cli/utils/strings.ts +47 -33
  75. package/src/cli/utils/version.ts +14 -8
  76. package/src/config/env-validation.ts +100 -0
  77. package/src/config/env.ts +169 -41
  78. package/src/config/index.ts +28 -20
  79. package/src/config/loader.ts +25 -16
  80. package/src/config/merge.ts +21 -10
  81. package/src/config/types.ts +566 -25
  82. package/src/config/validation.ts +215 -7
  83. package/src/container/forward-ref.ts +22 -22
  84. package/src/container/index.ts +34 -12
  85. package/src/context/index.ts +11 -1
  86. package/src/database/index.ts +7 -190
  87. package/src/database/orm/builder.ts +457 -0
  88. package/src/database/orm/casts/index.ts +130 -0
  89. package/src/database/orm/casts/types.ts +25 -0
  90. package/src/database/orm/compiler.ts +304 -0
  91. package/src/database/orm/hooks/index.ts +114 -0
  92. package/src/database/orm/index.ts +61 -0
  93. package/src/database/orm/model-registry.ts +59 -0
  94. package/src/database/orm/model.ts +821 -0
  95. package/src/database/orm/relationships/base.ts +146 -0
  96. package/src/database/orm/relationships/belongs-to-many.ts +179 -0
  97. package/src/database/orm/relationships/belongs-to.ts +56 -0
  98. package/src/database/orm/relationships/has-many.ts +45 -0
  99. package/src/database/orm/relationships/has-one.ts +41 -0
  100. package/src/database/orm/relationships/index.ts +11 -0
  101. package/src/database/orm/scopes/index.ts +55 -0
  102. package/src/events/__tests__/event-system.test.ts +235 -0
  103. package/src/events/config.ts +238 -0
  104. package/src/events/example-usage.ts +185 -0
  105. package/src/events/index.ts +278 -0
  106. package/src/events/manager.ts +385 -0
  107. package/src/events/registry.ts +182 -0
  108. package/src/events/types.ts +124 -0
  109. package/src/frontend/api-routes.ts +65 -23
  110. package/src/frontend/bundler.ts +76 -34
  111. package/src/frontend/console-client.ts +2 -2
  112. package/src/frontend/console-stream.ts +94 -38
  113. package/src/frontend/dev-server.ts +94 -46
  114. package/src/frontend/file-router.ts +61 -19
  115. package/src/frontend/frameworks/index.ts +37 -10
  116. package/src/frontend/frameworks/react.ts +10 -8
  117. package/src/frontend/frameworks/solid.ts +11 -9
  118. package/src/frontend/frameworks/svelte.ts +15 -9
  119. package/src/frontend/frameworks/vue.ts +13 -11
  120. package/src/frontend/hmr-client.ts +12 -10
  121. package/src/frontend/hmr.ts +146 -103
  122. package/src/frontend/index.ts +14 -5
  123. package/src/frontend/islands.ts +41 -22
  124. package/src/frontend/isr.ts +59 -37
  125. package/src/frontend/layout.ts +36 -21
  126. package/src/frontend/ssr/react.ts +74 -27
  127. package/src/frontend/ssr/solid.ts +54 -20
  128. package/src/frontend/ssr/svelte.ts +48 -14
  129. package/src/frontend/ssr/vue.ts +50 -18
  130. package/src/frontend/ssr.ts +83 -39
  131. package/src/frontend/types.ts +91 -56
  132. package/src/graphql/built-in-engine.ts +598 -0
  133. package/src/graphql/context-builder.ts +110 -0
  134. package/src/graphql/decorators.ts +358 -0
  135. package/src/graphql/execution-pipeline.ts +227 -0
  136. package/src/graphql/graphql-module.ts +563 -0
  137. package/src/graphql/index.ts +101 -0
  138. package/src/graphql/metadata.ts +237 -0
  139. package/src/graphql/schema-builder.ts +319 -0
  140. package/src/graphql/subscription-handler.ts +283 -0
  141. package/src/graphql/types.ts +324 -0
  142. package/src/health/index.ts +21 -9
  143. package/src/i18n/engine.ts +305 -0
  144. package/src/i18n/index.ts +38 -0
  145. package/src/i18n/loader.ts +218 -0
  146. package/src/i18n/middleware.ts +164 -0
  147. package/src/i18n/negotiator.ts +162 -0
  148. package/src/i18n/types.ts +158 -0
  149. package/src/index.ts +182 -27
  150. package/src/jobs/drivers/memory.ts +315 -0
  151. package/src/jobs/drivers/redis.ts +459 -0
  152. package/src/jobs/index.ts +30 -0
  153. package/src/jobs/queue.ts +281 -0
  154. package/src/jobs/types.ts +295 -0
  155. package/src/jobs/worker.ts +380 -0
  156. package/src/logger/index.ts +1 -3
  157. package/src/logger/transports/index.ts +62 -22
  158. package/src/metrics/index.ts +25 -16
  159. package/src/migrations/index.ts +9 -0
  160. package/src/modules/filters.ts +13 -17
  161. package/src/modules/guards.ts +49 -26
  162. package/src/modules/index.ts +457 -299
  163. package/src/modules/interceptors.ts +58 -20
  164. package/src/modules/lazy.ts +11 -19
  165. package/src/modules/lifecycle.ts +15 -7
  166. package/src/modules/metadata.ts +15 -5
  167. package/src/modules/pipes.ts +94 -72
  168. package/src/notification/channels/base.ts +68 -0
  169. package/src/notification/channels/email.ts +105 -0
  170. package/src/notification/channels/push.ts +104 -0
  171. package/src/notification/channels/sms.ts +105 -0
  172. package/src/notification/channels/whatsapp.ts +104 -0
  173. package/src/notification/index.ts +48 -0
  174. package/src/notification/service.ts +354 -0
  175. package/src/notification/types.ts +344 -0
  176. package/src/observability/__tests__/observability.test.ts +483 -0
  177. package/src/observability/breadcrumbs.ts +114 -0
  178. package/src/observability/index.ts +136 -0
  179. package/src/observability/interceptor.ts +85 -0
  180. package/src/observability/service.ts +303 -0
  181. package/src/observability/trace.ts +37 -0
  182. package/src/observability/types.ts +196 -0
  183. package/src/openapi/__tests__/decorators.test.ts +335 -0
  184. package/src/openapi/__tests__/document-builder.test.ts +285 -0
  185. package/src/openapi/__tests__/route-scanner.test.ts +334 -0
  186. package/src/openapi/__tests__/schema-generator.test.ts +275 -0
  187. package/src/openapi/decorators.ts +328 -0
  188. package/src/openapi/document-builder.ts +274 -0
  189. package/src/openapi/index.ts +112 -0
  190. package/src/openapi/metadata.ts +112 -0
  191. package/src/openapi/route-scanner.ts +289 -0
  192. package/src/openapi/schema-generator.ts +256 -0
  193. package/src/openapi/swagger-module.ts +166 -0
  194. package/src/openapi/types.ts +398 -0
  195. package/src/orm/index.ts +10 -0
  196. package/src/rpc/index.ts +3 -1
  197. package/src/schema/index.ts +9 -0
  198. package/src/security/index.ts +15 -6
  199. package/src/ssg/index.ts +9 -8
  200. package/src/telemetry/index.ts +76 -22
  201. package/src/template/index.ts +7 -0
  202. package/src/templates/engine.ts +224 -0
  203. package/src/templates/index.ts +9 -0
  204. package/src/templates/loader.ts +331 -0
  205. package/src/templates/renderers/markdown.ts +212 -0
  206. package/src/templates/renderers/simple.ts +269 -0
  207. package/src/templates/types.ts +154 -0
  208. package/src/testing/index.ts +100 -27
  209. package/src/types/optional-deps.d.ts +347 -187
  210. package/src/validation/index.ts +92 -2
  211. package/src/validation/schemas.ts +536 -0
  212. package/tests/integration/cli.test.ts +19 -19
  213. package/tests/integration/fullstack.test.ts +4 -4
  214. package/tests/unit/cli.test.ts +1 -1
  215. package/tests/unit/database.test.ts +2 -72
  216. package/tests/unit/env-validation.test.ts +166 -0
  217. package/tests/unit/events.test.ts +910 -0
  218. package/tests/unit/graphql.test.ts +991 -0
  219. package/tests/unit/i18n.test.ts +455 -0
  220. package/tests/unit/jobs.test.ts +493 -0
  221. package/tests/unit/notification.test.ts +988 -0
  222. package/tests/unit/observability.test.ts +453 -0
  223. package/tests/unit/orm/builder.test.ts +323 -0
  224. package/tests/unit/orm/casts.test.ts +179 -0
  225. package/tests/unit/orm/compiler.test.ts +220 -0
  226. package/tests/unit/orm/eager-loading.test.ts +285 -0
  227. package/tests/unit/orm/hooks.test.ts +191 -0
  228. package/tests/unit/orm/model.test.ts +373 -0
  229. package/tests/unit/orm/relationships.test.ts +303 -0
  230. package/tests/unit/orm/scopes.test.ts +74 -0
  231. package/tests/unit/templates-simple.test.ts +53 -0
  232. package/tests/unit/templates.test.ts +454 -0
  233. package/tests/unit/validation.test.ts +18 -24
  234. package/tsconfig.json +11 -3
@@ -10,7 +10,12 @@
10
10
  /**
11
11
  * Span kind enumeration
12
12
  */
13
- export type SpanKind = "server" | "client" | "producer" | "consumer" | "internal";
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<{ traceId: string; spanId: string; attributes?: Record<string, string | number | boolean> }>;
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(key: string, value: string | number | boolean): OTLPAttribute {
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: string = "unknown-service";
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(attributes: Record<string, string | number | boolean>): void {
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 ? hexToBase64(span.parentSpanId) : undefined,
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]) => toOTLPAttribute(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]) => toOTLPAttribute(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 = previousSpan ?? this.spanStack[this.spanStack.length - 1] ?? null;
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(span: Span, name: string, attributes?: Record<string, string | number | boolean>): void {
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(span: Span, key: string, value: string | number | boolean): void {
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(span: Span, attributes: Record<string, string | number | boolean>): void {
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(serviceName: string, options: Omit<TracerOptions, "serviceName"> = {}): Tracer {
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(`${ctx.method} ${ctx.path}`, parentContext, spanOptions);
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(tracer: Tracer, db: TracedDatabase, system: string = "unknown"): TracedDatabase {
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(/^(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TRUNCATE)/);
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(tracer: Tracer): (url: string | URL, options?: TracedFetchOptions) => Promise<Response> {
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(name: string, attributes?: Record<string, string | number | boolean>): this {
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(tracer: Tracer, name: string, options: SpanOptions = {}): SpanBuilder {
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,7 @@
1
+ /**
2
+ * Template System - Subpath Export
3
+ *
4
+ * Usage: import { TemplateEngine } from "@buenojs/bueno/template"
5
+ */
6
+
7
+ export * from "../templates/index";
@@ -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";