@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
@@ -9,7 +9,7 @@
9
9
  */
10
10
  export function camelCase(str: string): string {
11
11
  return str
12
- .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
12
+ .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
13
13
  .replace(/^(.)/, (c) => c.toLowerCase());
14
14
  }
15
15
 
@@ -18,7 +18,7 @@ export function camelCase(str: string): string {
18
18
  */
19
19
  export function pascalCase(str: string): string {
20
20
  return str
21
- .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
21
+ .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
22
22
  .replace(/^(.)/, (c) => c.toUpperCase());
23
23
  }
24
24
 
@@ -27,8 +27,8 @@ export function pascalCase(str: string): string {
27
27
  */
28
28
  export function kebabCase(str: string): string {
29
29
  return str
30
- .replace(/([a-z])([A-Z])/g, '$1-$2')
31
- .replace(/[-_\s]+/g, '-')
30
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
31
+ .replace(/[-_\s]+/g, "-")
32
32
  .toLowerCase();
33
33
  }
34
34
 
@@ -37,8 +37,8 @@ export function kebabCase(str: string): string {
37
37
  */
38
38
  export function snakeCase(str: string): string {
39
39
  return str
40
- .replace(/([a-z])([A-Z])/g, '$1_$2')
41
- .replace(/[-\s]+/g, '_')
40
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
41
+ .replace(/[-\s]+/g, "_")
42
42
  .toLowerCase();
43
43
  }
44
44
 
@@ -67,31 +67,45 @@ export function capitalize(str: string): string {
67
67
  * Pluralize a word (simple implementation)
68
68
  */
69
69
  export function pluralize(word: string): string {
70
- if (word.endsWith('y') && !['ay', 'ey', 'iy', 'oy', 'uy'].some((e) => word.endsWith(e))) {
71
- return word.slice(0, -1) + 'ies';
70
+ if (
71
+ word.endsWith("y") &&
72
+ !["ay", "ey", "iy", "oy", "uy"].some((e) => word.endsWith(e))
73
+ ) {
74
+ return word.slice(0, -1) + "ies";
72
75
  }
73
- if (word.endsWith('s') || word.endsWith('x') || word.endsWith('z') || word.endsWith('ch') || word.endsWith('sh')) {
74
- return word + 'es';
76
+ if (
77
+ word.endsWith("s") ||
78
+ word.endsWith("x") ||
79
+ word.endsWith("z") ||
80
+ word.endsWith("ch") ||
81
+ word.endsWith("sh")
82
+ ) {
83
+ return word + "es";
75
84
  }
76
- return word + 's';
85
+ return word + "s";
77
86
  }
78
87
 
79
88
  /**
80
89
  * Singularize a word (simple implementation)
81
90
  */
82
91
  export function singularize(word: string): string {
83
- if (word.endsWith('ies')) {
84
- return word.slice(0, -3) + 'y';
92
+ if (word.endsWith("ies")) {
93
+ return word.slice(0, -3) + "y";
85
94
  }
86
- if (word.endsWith('es')) {
95
+ if (word.endsWith("es")) {
87
96
  // Check for s, x, z, ch, sh endings
88
97
  const withoutEs = word.slice(0, -2);
89
- if (withoutEs.endsWith('s') || withoutEs.endsWith('x') || withoutEs.endsWith('z') ||
90
- withoutEs.endsWith('ch') || withoutEs.endsWith('sh')) {
98
+ if (
99
+ withoutEs.endsWith("s") ||
100
+ withoutEs.endsWith("x") ||
101
+ withoutEs.endsWith("z") ||
102
+ withoutEs.endsWith("ch") ||
103
+ withoutEs.endsWith("sh")
104
+ ) {
91
105
  return withoutEs;
92
106
  }
93
107
  }
94
- if (word.endsWith('s') && !word.endsWith('ss')) {
108
+ if (word.endsWith("s") && !word.endsWith("ss")) {
95
109
  return word.slice(0, -1);
96
110
  }
97
111
  return word;
@@ -116,13 +130,13 @@ export function isValidFileName(str: string): boolean {
116
130
  */
117
131
  export function truncate(str: string, maxLength: number): string {
118
132
  if (str.length <= maxLength) return str;
119
- return str.slice(0, maxLength - 3) + '...';
133
+ return str.slice(0, maxLength - 3) + "...";
120
134
  }
121
135
 
122
136
  /**
123
137
  * Pad string to center
124
138
  */
125
- export function padCenter(str: string, length: number, char = ' '): string {
139
+ export function padCenter(str: string, length: number, char = " "): string {
126
140
  const padding = length - str.length;
127
141
  if (padding <= 0) return str;
128
142
  const left = Math.floor(padding / 2);
@@ -134,7 +148,7 @@ export function padCenter(str: string, length: number, char = ' '): string {
134
148
  * Remove file extension
135
149
  */
136
150
  export function removeExtension(filename: string): string {
137
- const lastDot = filename.lastIndexOf('.');
151
+ const lastDot = filename.lastIndexOf(".");
138
152
  if (lastDot === -1 || lastDot === 0) return filename;
139
153
  return filename.slice(0, lastDot);
140
154
  }
@@ -143,8 +157,8 @@ export function removeExtension(filename: string): string {
143
157
  * Get file extension
144
158
  */
145
159
  export function getExtension(filename: string): string {
146
- const lastDot = filename.lastIndexOf('.');
147
- if (lastDot === -1 || lastDot === 0) return '';
160
+ const lastDot = filename.lastIndexOf(".");
161
+ if (lastDot === -1 || lastDot === 0) return "";
148
162
  return filename.slice(lastDot + 1);
149
163
  }
150
164
 
@@ -152,8 +166,8 @@ export function getExtension(filename: string): string {
152
166
  * Generate a unique ID
153
167
  */
154
168
  export function generateId(length = 8): string {
155
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
156
- let result = '';
169
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
170
+ let result = "";
157
171
  for (let i = 0; i < length; i++) {
158
172
  result += chars.charAt(Math.floor(Math.random() * chars.length));
159
173
  }
@@ -164,25 +178,25 @@ export function generateId(length = 8): string {
164
178
  * Escape string for use in template literals
165
179
  */
166
180
  export function escapeTemplateString(str: string): string {
167
- return str.replace(/[`\\$]/g, '\\$&');
181
+ return str.replace(/[`\\$]/g, "\\$&");
168
182
  }
169
183
 
170
184
  /**
171
185
  * Escape string for use in regular expressions
172
186
  */
173
187
  export function escapeRegExp(str: string): string {
174
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
188
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
175
189
  }
176
190
 
177
191
  /**
178
192
  * Indent a multiline string
179
193
  */
180
194
  export function indent(str: string, spaces = 2): string {
181
- const indentation = ' '.repeat(spaces);
195
+ const indentation = " ".repeat(spaces);
182
196
  return str
183
- .split('\n')
197
+ .split("\n")
184
198
  .map((line) => (line.trim() ? indentation + line : line))
185
- .join('\n');
199
+ .join("\n");
186
200
  }
187
201
 
188
202
  /**
@@ -190,8 +204,8 @@ export function indent(str: string, spaces = 2): string {
190
204
  */
191
205
  export function stripLines(str: string): string {
192
206
  return str
193
- .split('\n')
207
+ .split("\n")
194
208
  .map((line) => line.trim())
195
- .join('\n')
196
- .replace(/\n{3,}/g, '\n\n');
197
- }
209
+ .join("\n")
210
+ .replace(/\n{3,}/g, "\n\n");
211
+ }
@@ -4,8 +4,8 @@
4
4
  * Gets the current version of @buenojs/bueno from package.json
5
5
  */
6
6
 
7
- import { readFileSync } from 'fs';
8
- import { join } from 'path';
7
+ import { readFileSync } from "fs";
8
+ import { join } from "path";
9
9
 
10
10
  let cachedVersion: string | null = null;
11
11
 
@@ -20,14 +20,20 @@ export function getBuenoVersion(): string {
20
20
 
21
21
  try {
22
22
  // Try to read from the package.json in the bueno package
23
- const packageJsonPath = join(import.meta.dir, '..', '..', '..', 'package.json');
24
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
23
+ const packageJsonPath = join(
24
+ import.meta.dir,
25
+ "..",
26
+ "..",
27
+ "..",
28
+ "package.json",
29
+ );
30
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
25
31
  cachedVersion = `^${packageJson.version}`;
26
32
  return cachedVersion;
27
33
  } catch {
28
34
  // Fallback to a default version if package.json can't be read
29
- console.warn('Could not read version from package.json, using default');
30
- return '^0.8.0';
35
+ console.warn("Could not read version from package.json, using default");
36
+ return "^0.8.0";
31
37
  }
32
38
  }
33
39
 
@@ -36,6 +42,6 @@ export function getBuenoVersion(): string {
36
42
  */
37
43
  export function getBuenoDependency(): Record<string, string> {
38
44
  return {
39
- '@buenojs/bueno': getBuenoVersion(),
45
+ "@buenojs/bueno": getBuenoVersion(),
40
46
  };
41
- }
47
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Environment variable validation for Bueno Framework
3
+ */
4
+
5
+ import type { ValidationResult } from "../validation";
6
+ import { validateEnvSync } from "../validation";
7
+ import { envSchema } from "../validation/schemas";
8
+
9
+ /**
10
+ * Validate environment variables against the Bueno schema
11
+ *
12
+ * @param envVars - Environment variables to validate
13
+ * @returns Validation result with transformed values or error details
14
+ */
15
+ export function validateEnvVars(
16
+ envVars: Record<string, string>,
17
+ ): ValidationResult {
18
+ return validateEnvSync(envSchema, envVars);
19
+ }
20
+
21
+ /**
22
+ * Validate and load environment variables from .env files
23
+ *
24
+ * @param options - Options for loading and validating environment variables
25
+ * @returns Validation result with transformed values or error details
26
+ */
27
+ export async function validateAndLoadEnv(options?: {
28
+ /** Custom list of env files to load */
29
+ files?: string[];
30
+ /** Whether to also load NODE_ENV-specific file */
31
+ loadNodeEnv?: boolean;
32
+ /** Base directory for env files */
33
+ cwd?: string;
34
+ /** Whether to merge with existing Bun.env */
35
+ mergeWithProcess?: boolean;
36
+ }): Promise<ValidationResult> {
37
+ try {
38
+ // Load environment variables from files
39
+ const { loadEnvFiles } = await import("./env");
40
+ const rawEnvVars = await loadEnvFiles(options);
41
+
42
+ // Validate the environment variables
43
+ return validateEnvVars(rawEnvVars);
44
+ } catch (error) {
45
+ return {
46
+ success: false,
47
+ issues: [
48
+ {
49
+ message: `Failed to load and validate environment variables: ${error instanceof Error ? error.message : "Unknown error"}`,
50
+ },
51
+ ],
52
+ };
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Get validation error messages as a single string
58
+ *
59
+ * @param result - Validation result
60
+ * @returns Formatted error message or null if valid
61
+ */
62
+ export function getValidationErrorMessage(
63
+ result: ValidationResult,
64
+ ): string | null {
65
+ if (result.success) {
66
+ return null;
67
+ }
68
+
69
+ return result.issues.map((issue) => issue.message).join("\n");
70
+ }
71
+
72
+ /**
73
+ * Check if required environment variables are set
74
+ *
75
+ * @param envVars - Environment variables to check
76
+ * @returns True if all required variables are present and valid
77
+ */
78
+ export function hasRequiredEnvVars(envVars: Record<string, string>): boolean {
79
+ const result = validateEnvVars(envVars);
80
+ return result.success;
81
+ }
82
+
83
+ /**
84
+ * Get missing required environment variables
85
+ *
86
+ * @param envVars - Environment variables to check
87
+ * @returns Array of missing required variable names
88
+ */
89
+ export function getMissingEnvVars(envVars: Record<string, string>): string[] {
90
+ const result = validateEnvVars(envVars);
91
+ if (result.success) {
92
+ return [];
93
+ }
94
+
95
+ return result.issues
96
+ .filter((issue) =>
97
+ issue.message.includes("This environment variable is required"),
98
+ )
99
+ .map((issue) => issue.path?.[0] || "unknown");
100
+ }
package/src/config/env.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  * Environment variable handling for Bueno Framework
3
3
  */
4
4
 
5
+ import { setNestedValue } from "./merge";
5
6
  import type { BuenoConfig, DeepPartial, EnvMapping } from "./types";
6
7
  import { ENV_MAPPINGS } from "./types";
7
- import { setNestedValue } from "./merge";
8
8
 
9
9
  /**
10
10
  * Environment variable source information
@@ -168,6 +168,113 @@ export async function loadEnvFiles(options?: {
168
168
  return result;
169
169
  }
170
170
 
171
+ // ============= Environment Variable Validation =============
172
+
173
+ /**
174
+ * Validate environment variables before loading
175
+ *
176
+ * @param envVars - Environment variables to validate
177
+ * @returns Validation result with transformed values or error details
178
+ */
179
+ export async function validateEnvVars(
180
+ envVars: Record<string, string>,
181
+ ): Promise<ValidationResult> {
182
+ try {
183
+ const { validateEnvVars } = await import("./env-validation");
184
+ return validateEnvVars(envVars);
185
+ } catch (error) {
186
+ return {
187
+ success: false,
188
+ issues: [
189
+ {
190
+ message: `Failed to validate environment variables: ${error instanceof Error ? error.message : "Unknown error"}`,
191
+ },
192
+ ],
193
+ };
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Load and validate environment variables with detailed error reporting
199
+ *
200
+ * @param options - Options for loading and validating environment variables
201
+ * @returns Loaded environment data with validation result
202
+ */
203
+ export async function loadAndValidateEnv(options?: {
204
+ /** Custom list of env files to load */
205
+ files?: string[];
206
+ /** Whether to also load NODE_ENV-specific file */
207
+ loadNodeEnv?: boolean;
208
+ /** Base directory for env files */
209
+ cwd?: string;
210
+ /** Whether to merge with existing Bun.env */
211
+ mergeWithProcess?: boolean;
212
+ /** Custom environment variable mappings */
213
+ mappings?: EnvMapping[];
214
+ }): Promise<{ loaded: LoadedEnv; valid: boolean; errors?: string }> {
215
+ try {
216
+ // Load environment variables from files
217
+ const fileVars = await loadEnvFiles(options);
218
+
219
+ // Validate the environment variables
220
+ const validationResult = await validateEnvVars(fileVars);
221
+
222
+ // Merge with Bun.env if requested
223
+ const raw: Record<string, string> =
224
+ options?.mergeWithProcess !== false
225
+ ? {
226
+ ...fileVars,
227
+ ...Object.fromEntries(
228
+ Object.entries(Bun.env).filter(([, v]) => v !== undefined) as [
229
+ string,
230
+ string,
231
+ ][],
232
+ ),
233
+ }
234
+ : fileVars;
235
+
236
+ // Transform to config
237
+ const config = envToConfig(raw, options?.mappings);
238
+
239
+ // Track sources
240
+ const sources = new Map<string, EnvSourceInfo>();
241
+ for (const [name, value] of Object.entries(raw)) {
242
+ sources.set(name, {
243
+ name,
244
+ value,
245
+ source: fileVars[name] !== undefined ? ".env file" : "process",
246
+ });
247
+ }
248
+
249
+ // Set loaded vars to Bun.env
250
+ for (const [key, value] of Object.entries(fileVars)) {
251
+ if (Bun.env[key] === undefined) {
252
+ Bun.env[key] = value;
253
+ }
254
+ }
255
+
256
+ const loaded: LoadedEnv = { raw, config, sources };
257
+
258
+ if (!validationResult.success) {
259
+ return {
260
+ loaded,
261
+ valid: false,
262
+ errors: validationResult.issues
263
+ .map((issue) => issue.message)
264
+ .join("\n"),
265
+ };
266
+ }
267
+
268
+ return { loaded, valid: true };
269
+ } catch (error) {
270
+ return {
271
+ loaded: { raw: {}, config: {}, sources: new Map() },
272
+ valid: false,
273
+ errors: `Failed to load and validate environment variables: ${error instanceof Error ? error.message : "Unknown error"}`,
274
+ };
275
+ }
276
+ }
277
+
171
278
  /**
172
279
  * Get environment variable value from Bun.env
173
280
  */
@@ -233,36 +340,18 @@ export async function loadEnv(options?: {
233
340
  /** Custom environment variable mappings */
234
341
  mappings?: EnvMapping[];
235
342
  }): Promise<LoadedEnv> {
236
- // Load from .env files
237
- const fileVars = await loadEnvFiles(options);
238
-
239
- // Merge with Bun.env if requested
240
- const raw: Record<string, string> =
241
- options?.mergeWithProcess !== false
242
- ? { ...fileVars, ...Object.fromEntries(Object.entries(Bun.env).filter(([, v]) => v !== undefined) as [string, string][]) }
243
- : fileVars;
244
-
245
- // Transform to config
246
- const config = envToConfig(raw, options?.mappings);
247
-
248
- // Track sources
249
- const sources = new Map<string, EnvSourceInfo>();
250
- for (const [name, value] of Object.entries(raw)) {
251
- sources.set(name, {
252
- name,
253
- value,
254
- source: fileVars[name] !== undefined ? ".env file" : "process",
255
- });
343
+ // Load and validate environment variables
344
+ const result = await loadAndValidateEnv(options);
345
+
346
+ if (!result.valid) {
347
+ console.error("Environment variable validation failed:");
348
+ console.error(result.errors);
349
+ throw new Error(
350
+ "Environment variable validation failed. Check the error messages above.",
351
+ );
256
352
  }
257
353
 
258
- // Set loaded vars to Bun.env
259
- for (const [key, value] of Object.entries(fileVars)) {
260
- if (Bun.env[key] === undefined) {
261
- Bun.env[key] = value;
262
- }
263
- }
264
-
265
- return { raw, config, sources };
354
+ return result.loaded;
266
355
  }
267
356
 
268
357
  /**
@@ -324,7 +413,7 @@ export function parseEnvBoolean(value: string): boolean {
324
413
  * Parse a number environment variable
325
414
  */
326
415
  export function parseEnvNumber(value: string): number {
327
- const num = parseInt(value, 10);
416
+ const num = Number.parseInt(value, 10);
328
417
  if (isNaN(num)) {
329
418
  throw new Error(`Invalid number: ${value}`);
330
419
  }
@@ -351,22 +440,58 @@ export function parseEnvArray(value: string): string[] {
351
440
  export const envConfigMapping: EnvConfigMapping[] = [
352
441
  { envVar: "BUENO_PORT", configKey: "server.port", transform: parseEnvNumber },
353
442
  { envVar: "BUENO_HOST", configKey: "server.host" },
354
- { envVar: "BUENO_DEV", configKey: "server.development", transform: parseEnvBoolean },
443
+ {
444
+ envVar: "BUENO_DEV",
445
+ configKey: "server.development",
446
+ transform: parseEnvBoolean,
447
+ },
355
448
  { envVar: "DATABASE_URL", configKey: "database.url" },
356
- { envVar: "DATABASE_POOL_SIZE", configKey: "database.poolSize", transform: parseEnvNumber },
449
+ {
450
+ envVar: "DATABASE_POOL_SIZE",
451
+ configKey: "database.poolSize",
452
+ transform: parseEnvNumber,
453
+ },
357
454
  { envVar: "REDIS_URL", configKey: "cache.url" },
358
455
  { envVar: "CACHE_DRIVER", configKey: "cache.driver" },
359
456
  { envVar: "CACHE_TTL", configKey: "cache.ttl", transform: parseEnvNumber },
360
457
  { envVar: "LOG_LEVEL", configKey: "logger.level" },
361
- { envVar: "LOG_PRETTY", configKey: "logger.pretty", transform: parseEnvBoolean },
362
- { envVar: "HEALTH_ENABLED", configKey: "health.enabled", transform: parseEnvBoolean },
363
- { envVar: "METRICS_ENABLED", configKey: "metrics.enabled", transform: parseEnvBoolean },
364
- { envVar: "TELEMETRY_ENABLED", configKey: "telemetry.enabled", transform: parseEnvBoolean },
458
+ {
459
+ envVar: "LOG_PRETTY",
460
+ configKey: "logger.pretty",
461
+ transform: parseEnvBoolean,
462
+ },
463
+ {
464
+ envVar: "HEALTH_ENABLED",
465
+ configKey: "health.enabled",
466
+ transform: parseEnvBoolean,
467
+ },
468
+ {
469
+ envVar: "METRICS_ENABLED",
470
+ configKey: "metrics.enabled",
471
+ transform: parseEnvBoolean,
472
+ },
473
+ {
474
+ envVar: "TELEMETRY_ENABLED",
475
+ configKey: "telemetry.enabled",
476
+ transform: parseEnvBoolean,
477
+ },
365
478
  { envVar: "TELEMETRY_SERVICE_NAME", configKey: "telemetry.serviceName" },
366
479
  { envVar: "TELEMETRY_ENDPOINT", configKey: "telemetry.endpoint" },
367
- { envVar: "FRONTEND_DEV_SERVER", configKey: "frontend.devServer", transform: parseEnvBoolean },
368
- { envVar: "FRONTEND_HMR", configKey: "frontend.hmr", transform: parseEnvBoolean },
369
- { envVar: "FRONTEND_PORT", configKey: "frontend.port", transform: parseEnvNumber },
480
+ {
481
+ envVar: "FRONTEND_DEV_SERVER",
482
+ configKey: "frontend.devServer",
483
+ transform: parseEnvBoolean,
484
+ },
485
+ {
486
+ envVar: "FRONTEND_HMR",
487
+ configKey: "frontend.hmr",
488
+ transform: parseEnvBoolean,
489
+ },
490
+ {
491
+ envVar: "FRONTEND_PORT",
492
+ configKey: "frontend.port",
493
+ transform: parseEnvNumber,
494
+ },
370
495
  ];
371
496
 
372
497
  /**
@@ -396,7 +521,10 @@ export function getEnvConfig(
396
521
  /**
397
522
  * Get an environment variable value
398
523
  */
399
- export function getEnvValue(key: string, defaultValue?: string): string | undefined {
524
+ export function getEnvValue(
525
+ key: string,
526
+ defaultValue?: string,
527
+ ): string | undefined {
400
528
  return Bun.env[key] ?? defaultValue;
401
529
  }
402
530
 
@@ -405,4 +533,4 @@ export function getEnvValue(key: string, defaultValue?: string): string | undefi
405
533
  */
406
534
  export function setEnvValue(key: string, value: string): void {
407
535
  Bun.env[key] = value;
408
- }
536
+ }