@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
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Simple Template Renderer
3
+ *
4
+ * Lightweight renderer for:
5
+ * - Variable interpolation: {{ variable }}
6
+ * - Filters: {{ value | uppercase | trim }}
7
+ * - Conditionals: {{ if condition }} ... {{ endif }}
8
+ *
9
+ * No dependencies, ~180 lines
10
+ */
11
+
12
+ import type { FilterRegistry, TemplateData } from "../types";
13
+
14
+ /**
15
+ * Built-in filters
16
+ */
17
+ const builtinFilters: FilterRegistry = {
18
+ uppercase: (value) => String(value).toUpperCase(),
19
+ lowercase: (value) => String(value).toLowerCase(),
20
+ capitalize: (value) => {
21
+ const str = String(value);
22
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
23
+ },
24
+ trim: (value) => String(value).trim(),
25
+ reverse: (value) => String(value).split("").reverse().join(""),
26
+ length: (value) => {
27
+ if (Array.isArray(value)) return value.length;
28
+ if (typeof value === "string") return value.length;
29
+ return 0;
30
+ },
31
+ join: (value, separator = ",") => {
32
+ if (Array.isArray(value)) return value.join(String(separator));
33
+ return String(value);
34
+ },
35
+ slice: (value, start = 0, end = undefined) => {
36
+ const str = String(value);
37
+ return str.slice(Number(start), end ? Number(end) : undefined);
38
+ },
39
+ default: (value, defaultVal) => {
40
+ if (value === null || value === undefined || value === "") {
41
+ return defaultVal;
42
+ }
43
+ return value;
44
+ },
45
+ isEmpty: (value) => {
46
+ if (Array.isArray(value)) return value.length === 0;
47
+ if (value === null || value === undefined || value === "") return true;
48
+ return false;
49
+ },
50
+ date: (value, format = "YYYY-MM-DD") => {
51
+ if (!(value instanceof Date)) {
52
+ value = new Date(value);
53
+ }
54
+ if (isNaN(value.getTime())) return String(value);
55
+
56
+ const year = value.getFullYear();
57
+ const month = String(value.getMonth() + 1).padStart(2, "0");
58
+ const date = String(value.getDate()).padStart(2, "0");
59
+ const hours = String(value.getHours()).padStart(2, "0");
60
+ const minutes = String(value.getMinutes()).padStart(2, "0");
61
+ const seconds = String(value.getSeconds()).padStart(2, "0");
62
+
63
+ return (format as string)
64
+ .replace("YYYY", String(year))
65
+ .replace("MM", month)
66
+ .replace("DD", date)
67
+ .replace("HH", hours)
68
+ .replace("mm", minutes)
69
+ .replace("ss", seconds);
70
+ },
71
+ };
72
+
73
+ /**
74
+ * Safely get nested property from object
75
+ */
76
+ function getNestedValue(obj: unknown, path: string): unknown {
77
+ const parts = path.split(".");
78
+ let current = obj;
79
+
80
+ for (const part of parts) {
81
+ if (current === null || current === undefined) {
82
+ return undefined;
83
+ }
84
+ current = (current as Record<string, unknown>)[part];
85
+ }
86
+
87
+ return current;
88
+ }
89
+
90
+ /**
91
+ * Evaluate simple conditions
92
+ * Supports: variable, !variable, var1 && var2, var1 || var2
93
+ */
94
+ function evaluateCondition(condition: string, data: TemplateData): boolean {
95
+ condition = condition.trim();
96
+
97
+ // Handle logical operators (simple parsing, left-to-right)
98
+ if (condition.includes("||")) {
99
+ return condition
100
+ .split("||")
101
+ .map((c) => evaluateCondition(c.trim(), data))
102
+ .some((result) => result);
103
+ }
104
+
105
+ if (condition.includes("&&")) {
106
+ return condition
107
+ .split("&&")
108
+ .map((c) => evaluateCondition(c.trim(), data))
109
+ .every((result) => result);
110
+ }
111
+
112
+ // Handle negation
113
+ if (condition.startsWith("!")) {
114
+ return !evaluateCondition(condition.slice(1).trim(), data);
115
+ }
116
+
117
+ // Get variable value
118
+ const value = getNestedValue(data, condition);
119
+
120
+ // Truthy check
121
+ return Boolean(value);
122
+ }
123
+
124
+ /**
125
+ * Simple template renderer
126
+ */
127
+ export class SimpleRenderer {
128
+ private filters: FilterRegistry;
129
+
130
+ constructor(customFilters?: FilterRegistry) {
131
+ this.filters = { ...builtinFilters, ...(customFilters || {}) };
132
+ }
133
+
134
+ /**
135
+ * Render template with data
136
+ */
137
+ render(template: string, data: TemplateData): string {
138
+ let result = template;
139
+
140
+ // First, handle conditionals ({{ if ... }} ... {{ endif }})
141
+ result = this._processConditionals(result, data);
142
+
143
+ // Then, handle variables and filters ({{ var | filter1 | filter2 }})
144
+ result = this._processVariables(result, data);
145
+
146
+ return result;
147
+ }
148
+
149
+ /**
150
+ * Process conditional blocks
151
+ */
152
+ private _processConditionals(template: string, data: TemplateData): string {
153
+ // Match: {{ if condition }} ... {{ else }} ... {{ endif }}
154
+ // Handle else blocks properly
155
+ let result = template;
156
+
157
+ // Process nested if blocks (inside-out)
158
+ const ifRegex = /\{\{\s*if\s+([^}]+)\s*\}\}([\s\S]*?)\{\{\s*endif\s*\}\}/;
159
+
160
+ while (ifRegex.test(result)) {
161
+ result = result.replace(ifRegex, (match, condition, content) => {
162
+ // Check for else block
163
+ const elseRegex = /\{\{\s*else\s*\}\}([\s\S]*)$/;
164
+ let thenBlock = content;
165
+ let elseBlock = "";
166
+
167
+ const elseMatch = content.match(elseRegex);
168
+ if (elseMatch) {
169
+ thenBlock = content.substring(0, elseMatch.index);
170
+ elseBlock = elseMatch[1];
171
+ }
172
+
173
+ // Evaluate condition
174
+ if (evaluateCondition(condition, data)) {
175
+ return thenBlock;
176
+ } else {
177
+ return elseBlock;
178
+ }
179
+ });
180
+ }
181
+
182
+ return result;
183
+ }
184
+
185
+ /**
186
+ * Process variable interpolation and filters
187
+ */
188
+ private _processVariables(template: string, data: TemplateData): string {
189
+ // Match: {{ var | filter1(arg) | filter2 }}
190
+ const varRegex = /\{\{\s*([^|}\s][^}]*?)\s*(?:\|([^}]*?))?\s*\}\}/g;
191
+
192
+ return template.replace(varRegex, (match, varPath, filterChain) => {
193
+ // Get variable value
194
+ let value = getNestedValue(data, varPath.trim());
195
+
196
+ // If undefined, return empty string
197
+ if (value === undefined || value === null) {
198
+ return "";
199
+ }
200
+
201
+ // Apply filters if present
202
+ if (filterChain) {
203
+ const filters = filterChain.split("|").map((f) => f.trim());
204
+
205
+ for (const filterStr of filters) {
206
+ // Parse filter and arguments: "uppercase" or "slice(1, 5)"
207
+ const filterMatch = filterStr.match(/^(\w+)(?:\(([^)]*)\))?$/);
208
+ if (!filterMatch) continue;
209
+
210
+ const filterName = filterMatch[1];
211
+ const argsStr = filterMatch[2];
212
+ const filterFn = this.filters[filterName];
213
+
214
+ if (!filterFn) {
215
+ console.warn(`Unknown filter: ${filterName}`);
216
+ continue;
217
+ }
218
+
219
+ // Parse arguments (simple string splitting)
220
+ const args: unknown[] = [];
221
+ if (argsStr) {
222
+ // Simple parsing: split by comma, handle quoted strings
223
+ let current = "";
224
+ let inQuotes = false;
225
+ let quoteChar = "";
226
+ for (const char of argsStr) {
227
+ if ((char === '"' || char === "'") && !inQuotes) {
228
+ inQuotes = true;
229
+ quoteChar = char;
230
+ } else if (char === quoteChar && inQuotes) {
231
+ inQuotes = false;
232
+ quoteChar = "";
233
+ } else if (char === "," && !inQuotes) {
234
+ const trimmed = current.trim();
235
+ // Remove quotes if present
236
+ const unquoted = trimmed.replace(/^["']|["']$/g, "");
237
+ args.push(unquoted);
238
+ current = "";
239
+ } else {
240
+ current += char;
241
+ }
242
+ }
243
+ if (current) {
244
+ const trimmed = current.trim();
245
+ const unquoted = trimmed.replace(/^["']|["']$/g, "");
246
+ args.push(unquoted);
247
+ }
248
+ }
249
+
250
+ // Apply filter
251
+ value = filterFn(value, ...args);
252
+ }
253
+ }
254
+
255
+ // Convert to string
256
+ return String(value);
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Register custom filter
262
+ */
263
+ registerFilter(
264
+ name: string,
265
+ fn: (value: unknown, ...args: unknown[]) => unknown,
266
+ ): void {
267
+ this.filters[name] = fn;
268
+ }
269
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Template System Types
3
+ *
4
+ * Core interfaces for the lightweight, multi-purpose template engine.
5
+ * Supports: Variables, filters, conditionals, Markdown rendering, and channel-specific variants.
6
+ */
7
+
8
+ /**
9
+ * Job status enumeration
10
+ */
11
+ export type JobStatus =
12
+ | "pending"
13
+ | "processing"
14
+ | "completed"
15
+ | "failed"
16
+ | "delayed";
17
+
18
+ /**
19
+ * Template metadata from front matter
20
+ */
21
+ export interface TemplateMetadata {
22
+ /** Channel variants supported by this template */
23
+ variants?: string[];
24
+ /** Default variant if not specified */
25
+ default?: string;
26
+ /** Template description */
27
+ description?: string;
28
+ /** Custom metadata */
29
+ [key: string]: unknown;
30
+ }
31
+
32
+ /**
33
+ * Parsed template with content and metadata
34
+ */
35
+ export interface Template {
36
+ /** Template identifier (path without extension) */
37
+ id: string;
38
+ /** Template format: "markdown", "text", or "html" */
39
+ format: "markdown" | "text" | "html";
40
+ /** Raw template content */
41
+ content: string;
42
+ /** Channel-specific variants: { "email": "...", "sms": "...", "push": "..." } */
43
+ variants: Record<string, string>;
44
+ /** Front matter metadata */
45
+ metadata: TemplateMetadata;
46
+ /** When template was loaded (for cache invalidation) */
47
+ loadedAt: number;
48
+ }
49
+
50
+ /**
51
+ * Options for rendering a template
52
+ */
53
+ export interface RenderOptions {
54
+ /** Specific variant to use (overrides auto-detection) */
55
+ variant?: string;
56
+ /** Output format: "html" for email, "text" for SMS/plain text */
57
+ outputFormat?: "html" | "text";
58
+ /** Timezone for date filters */
59
+ timezone?: string;
60
+ /** Locale for i18n (future) */
61
+ locale?: string;
62
+ }
63
+
64
+ /**
65
+ * Template engine data (variables passed to renderer)
66
+ */
67
+ export interface TemplateData {
68
+ [key: string]: unknown;
69
+ }
70
+
71
+ /**
72
+ * Renderer interface (implemented by SimpleRenderer, MarkdownRenderer, etc.)
73
+ */
74
+ export interface IRenderer {
75
+ /** Render template with data */
76
+ render(template: string, data: TemplateData): Promise<string>;
77
+ }
78
+
79
+ /**
80
+ * Filter function signature
81
+ */
82
+ export type FilterFn = (value: unknown, ...args: unknown[]) => unknown;
83
+
84
+ /**
85
+ * Built-in filters registry
86
+ */
87
+ export interface FilterRegistry {
88
+ [filterName: string]: FilterFn;
89
+ }
90
+
91
+ /**
92
+ * Template loader options
93
+ */
94
+ export interface TemplateLoaderOptions {
95
+ /** Base path to templates directory */
96
+ basePath: string;
97
+ /** Enable caching */
98
+ cacheEnabled?: boolean;
99
+ /** Cache time-to-live in seconds */
100
+ cacheTtl?: number;
101
+ /** Max number of templates in cache */
102
+ maxCacheSize?: number;
103
+ /** Watch for file changes in development */
104
+ watch?: boolean;
105
+ /** File extension to look for */
106
+ extension?: string;
107
+ }
108
+
109
+ /**
110
+ * Template engine configuration
111
+ */
112
+ export interface TemplateEngineConfig {
113
+ /** Base path for templates */
114
+ basePath: string;
115
+ /** Cache configuration */
116
+ cache?: {
117
+ enabled: boolean;
118
+ ttl: number;
119
+ maxSize: number;
120
+ };
121
+ /** Watch for changes in development */
122
+ watch?: boolean;
123
+ /** Default locale (future i18n) */
124
+ locale?: string;
125
+ /** Default output format */
126
+ defaultFormat?: "html" | "text";
127
+ /** Channel to variant mapping for auto-detection */
128
+ channelVariantMap?: Record<string, string>;
129
+ }
130
+
131
+ /**
132
+ * Template load event
133
+ */
134
+ export interface TemplateLoadedEvent {
135
+ templateId: string;
136
+ source: "memory" | "disk";
137
+ duration: number;
138
+ }
139
+
140
+ /**
141
+ * Template engine metrics
142
+ */
143
+ export interface TemplateEngineMetrics {
144
+ /** Total templates loaded */
145
+ loaded: number;
146
+ /** Number of cache hits */
147
+ cacheHits: number;
148
+ /** Number of cache misses */
149
+ cacheMisses: number;
150
+ /** Average render time in milliseconds */
151
+ avgRenderTime: number;
152
+ /** Total renders */
153
+ totalRenders: number;
154
+ }
@@ -598,7 +598,9 @@ export class TestCache {
598
598
  /**
599
599
  * Create a new TestCache instance, optionally with initial data
600
600
  */
601
- export async function createTestCache(initialData?: Record<string, unknown>): Promise<TestCache> {
601
+ export async function createTestCache(
602
+ initialData?: Record<string, unknown>,
603
+ ): Promise<TestCache> {
602
604
  const cache = new TestCache();
603
605
  if (initialData) {
604
606
  await cache.setMany(initialData);
@@ -748,7 +750,10 @@ export class TestDatabase {
748
750
  /**
749
751
  * Execute a SQL query and return results
750
752
  */
751
- async query<T = unknown>(sqlString: string, params: unknown[] = []): Promise<T[]> {
753
+ async query<T = unknown>(
754
+ sqlString: string,
755
+ params: unknown[] = [],
756
+ ): Promise<T[]> {
752
757
  this.ensureConnection();
753
758
 
754
759
  this._operations.push({
@@ -768,7 +773,10 @@ export class TestDatabase {
768
773
  /**
769
774
  * Execute a query and return a single row
770
775
  */
771
- async queryOne<T = unknown>(sqlString: string, params: unknown[] = []): Promise<T | null> {
776
+ async queryOne<T = unknown>(
777
+ sqlString: string,
778
+ params: unknown[] = [],
779
+ ): Promise<T | null> {
772
780
  const results = await this.query<T>(sqlString, params);
773
781
  return results.length > 0 ? results[0] : null;
774
782
  }
@@ -776,7 +784,14 @@ export class TestDatabase {
776
784
  /**
777
785
  * Execute a statement (INSERT, UPDATE, DELETE)
778
786
  */
779
- async execute(sqlString: string, params: unknown[] = []): Promise<{ rows: unknown[]; rowCount: number; insertId?: number | string }> {
787
+ async execute(
788
+ sqlString: string,
789
+ params: unknown[] = [],
790
+ ): Promise<{
791
+ rows: unknown[];
792
+ rowCount: number;
793
+ insertId?: number | string;
794
+ }> {
780
795
  this.ensureConnection();
781
796
 
782
797
  this._operations.push({
@@ -907,7 +922,10 @@ export class TestDatabase {
907
922
  /**
908
923
  * Create a table from column definitions
909
924
  */
910
- async createTable(name: string, columns: Record<string, string>): Promise<void> {
925
+ async createTable(
926
+ name: string,
927
+ columns: Record<string, string>,
928
+ ): Promise<void> {
911
929
  const columnDefs = Object.entries(columns)
912
930
  .map(([colName, def]) => `${colName} ${def}`)
913
931
  .join(", ");
@@ -941,14 +959,27 @@ export class TestDatabase {
941
959
  /**
942
960
  * Get table info
943
961
  */
944
- async getTableInfo(table: string): Promise<{ cid: number; name: string; type: string; notnull: number; dflt_value: unknown; pk: number }[]> {
962
+ async getTableInfo(table: string): Promise<
963
+ {
964
+ cid: number;
965
+ name: string;
966
+ type: string;
967
+ notnull: number;
968
+ dflt_value: unknown;
969
+ pk: number;
970
+ }[]
971
+ > {
945
972
  return this.query(`PRAGMA table_info(${table})`);
946
973
  }
947
974
 
948
975
  /**
949
976
  * Count rows in a table
950
977
  */
951
- async count(table: string, where?: string, params: unknown[] = []): Promise<number> {
978
+ async count(
979
+ table: string,
980
+ where?: string,
981
+ params: unknown[] = [],
982
+ ): Promise<number> {
952
983
  const sql = where
953
984
  ? `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`
954
985
  : `SELECT COUNT(*) as count FROM ${table}`;
@@ -960,7 +991,11 @@ export class TestDatabase {
960
991
  /**
961
992
  * Check if a row exists
962
993
  */
963
- async exists(table: string, where: string, params: unknown[] = []): Promise<boolean> {
994
+ async exists(
995
+ table: string,
996
+ where: string,
997
+ params: unknown[] = [],
998
+ ): Promise<boolean> {
964
999
  const count = await this.count(table, where, params);
965
1000
  return count > 0;
966
1001
  }
@@ -985,7 +1020,9 @@ export class TestDatabase {
985
1020
  /**
986
1021
  * Create a new TestDatabase instance, optionally with schema and seed data
987
1022
  */
988
- export async function createTestDatabase(options: TestDatabaseOptions = {}): Promise<TestDatabase> {
1023
+ export async function createTestDatabase(
1024
+ options: TestDatabaseOptions = {},
1025
+ ): Promise<TestDatabase> {
989
1026
  const db = new TestDatabase();
990
1027
  await db.connect();
991
1028
 
@@ -1059,7 +1096,10 @@ export async function assertTableNotHasRow(
1059
1096
  /**
1060
1097
  * Assert table exists in database
1061
1098
  */
1062
- export async function assertTableExists(db: TestDatabase, table: string): Promise<void> {
1099
+ export async function assertTableExists(
1100
+ db: TestDatabase,
1101
+ table: string,
1102
+ ): Promise<void> {
1063
1103
  const tables = await db.getTables();
1064
1104
  if (!tables.includes(table)) {
1065
1105
  throw new Error(
@@ -1071,7 +1111,10 @@ export async function assertTableExists(db: TestDatabase, table: string): Promis
1071
1111
  /**
1072
1112
  * Assert table does not exist in database
1073
1113
  */
1074
- export async function assertTableNotExists(db: TestDatabase, table: string): Promise<void> {
1114
+ export async function assertTableNotExists(
1115
+ db: TestDatabase,
1116
+ table: string,
1117
+ ): Promise<void> {
1075
1118
  const tables = await db.getTables();
1076
1119
  if (tables.includes(table)) {
1077
1120
  throw new Error(`Expected table "${table}" to NOT exist`);
@@ -1106,15 +1149,32 @@ export async function assertTableValue<T = unknown>(
1106
1149
  }
1107
1150
  // ============= Test Storage =============
1108
1151
 
1109
- import { mkdir, rm, readdir, stat as fsStat, copyFile, rename, unlink } from "node:fs/promises";
1110
- import { join, resolve, relative } from "node:path";
1152
+ import {
1153
+ copyFile,
1154
+ stat as fsStat,
1155
+ mkdir,
1156
+ readdir,
1157
+ rename,
1158
+ rm,
1159
+ unlink,
1160
+ } from "node:fs/promises";
1111
1161
  import { tmpdir } from "node:os";
1162
+ import { join, relative, resolve } from "node:path";
1112
1163
 
1113
1164
  /**
1114
1165
  * Storage operation record for testing
1115
1166
  */
1116
1167
  export interface StorageOperation {
1117
- type: "write" | "read" | "delete" | "exists" | "list" | "stat" | "copy" | "move" | "clear";
1168
+ type:
1169
+ | "write"
1170
+ | "read"
1171
+ | "delete"
1172
+ | "exists"
1173
+ | "list"
1174
+ | "stat"
1175
+ | "copy"
1176
+ | "move"
1177
+ | "clear";
1118
1178
  path?: string;
1119
1179
  src?: string;
1120
1180
  dest?: string;
@@ -1179,10 +1239,13 @@ export class TestStorage {
1179
1239
  * @param path - Relative path within storage
1180
1240
  * @param content - String or binary content
1181
1241
  */
1182
- async write(path: string, content: string | Uint8Array | ArrayBuffer): Promise<void> {
1242
+ async write(
1243
+ path: string,
1244
+ content: string | Uint8Array | ArrayBuffer,
1245
+ ): Promise<void> {
1183
1246
  await this.ensureInitialized();
1184
1247
  const fullPath = this.resolvePath(path);
1185
-
1248
+
1186
1249
  // Ensure parent directory exists
1187
1250
  const parentDir = fullPath.substring(0, fullPath.lastIndexOf("/"));
1188
1251
  if (parentDir) {
@@ -1192,7 +1255,7 @@ export class TestStorage {
1192
1255
  // Write content using Bun.file()
1193
1256
  const file = Bun.file(fullPath);
1194
1257
  const writer = file.writer();
1195
-
1258
+
1196
1259
  if (typeof content === "string") {
1197
1260
  writer.write(content);
1198
1261
  } else if (content instanceof Uint8Array) {
@@ -1200,12 +1263,13 @@ export class TestStorage {
1200
1263
  } else if (content instanceof ArrayBuffer) {
1201
1264
  writer.write(new Uint8Array(content));
1202
1265
  }
1203
-
1266
+
1204
1267
  await writer.end();
1205
1268
 
1206
- const size = typeof content === "string"
1207
- ? new TextEncoder().encode(content).length
1208
- : content.byteLength;
1269
+ const size =
1270
+ typeof content === "string"
1271
+ ? new TextEncoder().encode(content).length
1272
+ : content.byteLength;
1209
1273
 
1210
1274
  this._operations.push({
1211
1275
  type: "write",
@@ -1437,7 +1501,7 @@ export class TestStorage {
1437
1501
  */
1438
1502
  async clear(): Promise<void> {
1439
1503
  await this.ensureInitialized();
1440
-
1504
+
1441
1505
  try {
1442
1506
  const files = await this.list();
1443
1507
  for (const file of files) {
@@ -1505,8 +1569,11 @@ export class TestStorage {
1505
1569
  * Create a new TestStorage instance
1506
1570
  * @param options - Optional configuration
1507
1571
  */
1508
- export async function createTestStorage(options: TestStorageOptions = {}): Promise<TestStorage> {
1509
- const basePath = options.basePath ?? await createTempDir("bueno-test-storage-");
1572
+ export async function createTestStorage(
1573
+ options: TestStorageOptions = {},
1574
+ ): Promise<TestStorage> {
1575
+ const basePath =
1576
+ options.basePath ?? (await createTempDir("bueno-test-storage-"));
1510
1577
  const storage = new TestStorage(basePath);
1511
1578
  await storage.init();
1512
1579
  return storage;
@@ -1527,7 +1594,10 @@ async function createTempDir(prefix: string): Promise<string> {
1527
1594
  /**
1528
1595
  * Assert that a file exists in storage
1529
1596
  */
1530
- export async function assertFileExists(storage: TestStorage, path: string): Promise<void> {
1597
+ export async function assertFileExists(
1598
+ storage: TestStorage,
1599
+ path: string,
1600
+ ): Promise<void> {
1531
1601
  const exists = await storage.exists(path);
1532
1602
  if (!exists) {
1533
1603
  const files = await storage.list();
@@ -1540,7 +1610,10 @@ export async function assertFileExists(storage: TestStorage, path: string): Prom
1540
1610
  /**
1541
1611
  * Assert that a file does not exist in storage
1542
1612
  */
1543
- export async function assertFileNotExists(storage: TestStorage, path: string): Promise<void> {
1613
+ export async function assertFileNotExists(
1614
+ storage: TestStorage,
1615
+ path: string,
1616
+ ): Promise<void> {
1544
1617
  const exists = await storage.exists(path);
1545
1618
  if (exists) {
1546
1619
  throw new Error(`Expected file "${path}" to NOT exist`);
@@ -1583,4 +1656,4 @@ export async function assertFileSize(
1583
1656
  `Expected file "${path}" to have size ${expectedSize}, got ${stats.size}`,
1584
1657
  );
1585
1658
  }
1586
- }
1659
+ }