@grupodiariodaregiao/bunstone 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.ts CHANGED
@@ -8,22 +8,464 @@ import {
8
8
  } from "node:fs/promises";
9
9
  import { join } from "node:path";
10
10
 
11
+ // ── Tiny ANSI helpers (no external dep) ──────────────────────────────────────
12
+ const R = "\x1b[0m";
13
+ const bold = (s: string) => `\x1b[1m${s}${R}`;
14
+ const red = (s: string) => `\x1b[31m${s}${R}`;
15
+ const yellow = (s: string) => `\x1b[33m${s}${R}`;
16
+ const green = (s: string) => `\x1b[32m${s}${R}`;
17
+ const cyan = (s: string) => `\x1b[36m${s}${R}`;
18
+ const gray = (s: string) => `\x1b[90m${s}${R}`;
19
+ const BORDER = "━".repeat(64);
20
+ const THIN = "─".repeat(64);
21
+
22
+ // ── Arg parsing ───────────────────────────────────────────────────────────────
11
23
  const args = Bun.argv.slice(2);
12
- let command = args[0];
13
- let projectName = args[1];
24
+ const command = args[0];
14
25
 
15
- // Default to "new" command if only project name is provided
16
- if (command && command !== "new" && !projectName) {
17
- projectName = command;
18
- command = "new";
26
+ // ── Command dispatch ──────────────────────────────────────────────────────────
27
+ if (command === "run") {
28
+ await runCommand(args.slice(1));
29
+ } else if (command === "exports") {
30
+ await exportsCommand();
31
+ } else if (command === "new" || (command && !args[1])) {
32
+ await scaffold(command === "new" ? args[1] : command);
33
+ } else {
34
+ printHelp();
35
+ process.exit(1);
19
36
  }
20
37
 
21
- // Default project name if none provided
22
- if (!projectName && command === "new") {
23
- projectName = "my-bunstone-app";
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+ // bunstone run [bun-flags...] <entrypoint>
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Regexp that matches Bun's raw import error line.
44
+ * e.g. SyntaxError: Export named 'RabbitMessage' not found in module '…/bunstone/dist/index.js'.
45
+ */
46
+ const BUN_EXPORT_RE = /Export named '(.+?)' not found in module '(.+?)'/;
47
+
48
+ async function runCommand(runArgs: string[]) {
49
+ if (runArgs.length === 0) {
50
+ console.error(red("✖ Usage: bunstone run [bun-flags...] <entrypoint.ts>"));
51
+ process.exit(1);
52
+ }
53
+
54
+ // Separate bun flags (start with -) from the entrypoint
55
+ const bunFlags: string[] = [];
56
+ let entrypoint = "";
57
+ for (const arg of runArgs) {
58
+ if (arg.startsWith("-") && !entrypoint) bunFlags.push(arg);
59
+ else entrypoint = arg;
60
+ }
61
+
62
+ if (!entrypoint) {
63
+ console.error(red("✖ No entrypoint file specified."));
64
+ console.error(gray(" Usage: bunstone run [--watch] src/main.ts"));
65
+ process.exit(1);
66
+ }
67
+
68
+ const cmd = ["bun", ...bunFlags, entrypoint];
69
+
70
+ const proc = Bun.spawn(cmd, {
71
+ stdin: "inherit",
72
+ stdout: "inherit",
73
+ stderr: "pipe",
74
+ });
75
+
76
+ // Accumulate stderr so we can inspect it for the import error pattern
77
+ const stderrChunks: Uint8Array[] = [];
78
+ const stderrReader = proc.stderr.getReader();
79
+
80
+ (async () => {
81
+ const decoder = new TextDecoder();
82
+ while (true) {
83
+ const { done, value } = await stderrReader.read();
84
+ if (done) break;
85
+ stderrChunks.push(value);
86
+
87
+ // Write to real stderr immediately so the developer still sees everything
88
+ process.stderr.write(decoder.decode(value));
89
+ }
90
+ })();
91
+
92
+ const exitCode = await proc.exited;
93
+
94
+ if (exitCode !== 0) {
95
+ const decoder = new TextDecoder();
96
+ const fullStderr = stderrChunks.map((c) => decoder.decode(c)).join("");
97
+
98
+ const match = BUN_EXPORT_RE.exec(fullStderr);
99
+ if (match) {
100
+ const [, name, modulePath] = match;
101
+ printImportError(name, modulePath);
102
+ }
103
+ }
104
+
105
+ process.exit(exitCode);
24
106
  }
25
107
 
26
- const projectPath = join(process.cwd(), projectName || "");
108
+ function printImportError(name: string, modulePath: string) {
109
+ // Lazy-load the utils so the CLI does not depend on the built dist
110
+ const pkg = modulePath.includes("node_modules/")
111
+ ? modulePath.replace(/.*node_modules\//, "").replace(/\/dist.*/, "")
112
+ : modulePath;
113
+
114
+ // These are inlined here (not imported from dist) so the CLI works even
115
+ // before the user runs `bun build`.
116
+ const TYPE_ONLY: ReadonlySet<string> = new Set([
117
+ "RabbitMessage",
118
+ "RabbitPublishOptions",
119
+ "RabbitSubscribeOptions",
120
+ "DeadLetterMessage",
121
+ "DeadLetterDeathInfo",
122
+ "RequeueOptions",
123
+ "RabbitMQExchangeConfig",
124
+ "RabbitMQQueueBinding",
125
+ "RabbitMQQueueConfig",
126
+ "RabbitMQReconnectConfig",
127
+ "RabbitMQModuleOptions",
128
+ "HttpRequest",
129
+ "ModuleConfig",
130
+ "Options",
131
+ "GuardContract",
132
+ ]);
133
+
134
+ const ALL_VALUES = [
135
+ "CacheAdapter",
136
+ "FormDataAdapter",
137
+ "UploadAdapter",
138
+ "EmailModule",
139
+ "EmailService",
140
+ "EmailLayout",
141
+ "AppStartup",
142
+ "Layout",
143
+ "Controller",
144
+ "Get",
145
+ "Post",
146
+ "Put",
147
+ "Patch",
148
+ "Delete",
149
+ "Head",
150
+ "RateLimit",
151
+ "RateLimitGuard",
152
+ "RedisStorage",
153
+ "CommandBus",
154
+ "CqrsModule",
155
+ "CommandHandler",
156
+ "EventHandler",
157
+ "QueryHandler",
158
+ "Saga",
159
+ "EventBus",
160
+ "QueryBus",
161
+ "SqlModule",
162
+ "SqlService",
163
+ "BullMqModule",
164
+ "QueueService",
165
+ "Processor",
166
+ "Process",
167
+ "RabbitMQModule",
168
+ "RabbitMQService",
169
+ "RabbitMQDeadLetterService",
170
+ "RabbitMQConnection",
171
+ "RabbitConsumer",
172
+ "RabbitSubscribe",
173
+ "BunstoneError",
174
+ "DependencyResolutionError",
175
+ "ModuleInitializationError",
176
+ "ConfigurationError",
177
+ "CqrsError",
178
+ "DatabaseError",
179
+ "BullMQError",
180
+ "RabbitMQError",
181
+ "ScheduleError",
182
+ "TestingError",
183
+ "RateLimitError",
184
+ "UploadError",
185
+ "EmailError",
186
+ "HttpParamError",
187
+ "GuardError",
188
+ "AdapterError",
189
+ "ImportError",
190
+ "ErrorFormatter",
191
+ "Guard",
192
+ "UseGuards",
193
+ "HttpException",
194
+ "BadRequestException",
195
+ "UnauthorizedException",
196
+ "ForbiddenException",
197
+ "NotFoundException",
198
+ "ConflictException",
199
+ "UnprocessableEntityException",
200
+ "InternalServerErrorException",
201
+ "OkResponse",
202
+ "CreatedResponse",
203
+ "NoContentResponse",
204
+ "HttpMethod",
205
+ "Body",
206
+ "Param",
207
+ "Query",
208
+ "Headers",
209
+ "Req",
210
+ "Res",
211
+ "FormData",
212
+ "Injectable",
213
+ "Jwt",
214
+ "JwtModule",
215
+ "JwtService",
216
+ "UseJwt",
217
+ "Module",
218
+ "OnModuleInit",
219
+ "OnModuleDestroy",
220
+ "ApiTags",
221
+ "ApiOperation",
222
+ "ApiResponse",
223
+ "ApiBody",
224
+ "Render",
225
+ "Cron",
226
+ "Timeout",
227
+ "Test",
228
+ "TestingModuleBuilder",
229
+ "TestApp",
230
+ "Logger",
231
+ ];
232
+
233
+ const isTypeOnly = TYPE_ONLY.has(name);
234
+ const suggestions = closestMatchesCli(
235
+ name,
236
+ isTypeOnly ? ALL_VALUES : [...ALL_VALUES, ...TYPE_ONLY],
237
+ );
238
+
239
+ const code = isTypeOnly ? "BNS-IMP-001" : "BNS-IMP-002";
240
+
241
+ console.error(`\n${red(BORDER)}`);
242
+ console.error(red(bold(" 💥 Bunstone — Import Error")));
243
+ console.error(`${red(BORDER)}\n`);
244
+
245
+ console.error(
246
+ ` ${yellow(bold("Code :"))} ${bold(code)} ${gray(`(ImportError)`)}`,
247
+ );
248
+
249
+ if (isTypeOnly) {
250
+ console.error(
251
+ ` ${yellow(bold("Message :"))} '${name}' is a ${bold("type-only")} export of '${pkg}' — it does not exist at runtime.`,
252
+ );
253
+ console.error(`\n ${green(bold("💡 How to fix:"))}`);
254
+ console.error(
255
+ green(
256
+ [
257
+ ` Replace the import with 'import type':`,
258
+ ``,
259
+ ` ${red("✗")} import { ${name} } from '${pkg}'`,
260
+ ` ${green("✓")} import type { ${name} } from '${pkg}'`,
261
+ ].join("\n"),
262
+ ),
263
+ );
264
+ if (suggestions.length > 0) {
265
+ console.error(
266
+ `\n ${cyan("If you were looking for a runtime value with a similar name:")}`,
267
+ );
268
+ for (const s of suggestions) {
269
+ console.error(` ${gray("→")} ${s}`);
270
+ }
271
+ }
272
+ } else {
273
+ console.error(
274
+ ` ${yellow(bold("Message :"))} '${name}' is not exported by '${pkg}'.`,
275
+ );
276
+ console.error(`\n ${green(bold("💡 How to fix:"))}`);
277
+ console.error(green(` Check the spelling of the imported name.`));
278
+ console.error(
279
+ green(
280
+ ` Run ${bold("bunstone exports")} to see all available exports.`,
281
+ ),
282
+ );
283
+ if (suggestions.length > 0) {
284
+ console.error(`\n ${cyan("Did you mean one of these?")}`);
285
+ for (const s of suggestions) {
286
+ console.error(` ${gray("→")} ${s}`);
287
+ }
288
+ }
289
+ }
290
+
291
+ console.error(`\n${red(THIN)}`);
292
+ console.error(red(bold(" ✖ Fix the import above and restart.")));
293
+ console.error(`${red(BORDER)}\n`);
294
+ }
295
+
296
+ /** Minimal inline Levenshtein for the CLI (avoids importing from dist). */
297
+ function closestMatchesCli(
298
+ name: string,
299
+ candidates: Iterable<string>,
300
+ limit = 5,
301
+ maxDist = 4,
302
+ ): string[] {
303
+ function dist(a: string, b: string) {
304
+ const s1 = a.toLowerCase();
305
+ const s2 = b.toLowerCase();
306
+ const [m, n] = [s1.length, s2.length];
307
+ const dp = Array.from({ length: m + 1 }, (_, i) =>
308
+ Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
309
+ );
310
+ for (let i = 1; i <= m; i++)
311
+ for (let j = 1; j <= n; j++)
312
+ dp[i][j] =
313
+ s1[i - 1] === s2[j - 1]
314
+ ? dp[i - 1][j - 1]
315
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
316
+ return dp[m][n];
317
+ }
318
+
319
+ const results: { name: string; d: number }[] = [];
320
+ for (const c of candidates) {
321
+ const d = dist(name, c);
322
+ if (d <= maxDist) results.push({ name: c, d });
323
+ }
324
+ return results
325
+ .sort((a, b) => a.d - b.d)
326
+ .slice(0, limit)
327
+ .map((r) => r.name);
328
+ }
329
+
330
+ // ─────────────────────────────────────────────────────────────────────────────
331
+ // bunstone exports
332
+ // ─────────────────────────────────────────────────────────────────────────────
333
+
334
+ async function exportsCommand() {
335
+ const VALUE_EXPORTS = [
336
+ "CacheAdapter",
337
+ "FormDataAdapter",
338
+ "UploadAdapter",
339
+ "EmailModule",
340
+ "EmailService",
341
+ "EmailLayout",
342
+ "AppStartup",
343
+ "Layout",
344
+ "Controller",
345
+ "Get",
346
+ "Post",
347
+ "Put",
348
+ "Patch",
349
+ "Delete",
350
+ "Head",
351
+ "RateLimit",
352
+ "RateLimitGuard",
353
+ "RedisStorage",
354
+ "CommandBus",
355
+ "CqrsModule",
356
+ "CommandHandler",
357
+ "EventHandler",
358
+ "QueryHandler",
359
+ "Saga",
360
+ "EventBus",
361
+ "QueryBus",
362
+ "SqlModule",
363
+ "SqlService",
364
+ "BullMqModule",
365
+ "QueueService",
366
+ "Processor",
367
+ "Process",
368
+ "RabbitMQModule",
369
+ "RabbitMQService",
370
+ "RabbitMQDeadLetterService",
371
+ "RabbitMQConnection",
372
+ "RabbitConsumer",
373
+ "RabbitSubscribe",
374
+ "BunstoneError",
375
+ "DependencyResolutionError",
376
+ "ModuleInitializationError",
377
+ "ConfigurationError",
378
+ "CqrsError",
379
+ "DatabaseError",
380
+ "BullMQError",
381
+ "RabbitMQError",
382
+ "ScheduleError",
383
+ "TestingError",
384
+ "RateLimitError",
385
+ "UploadError",
386
+ "EmailError",
387
+ "HttpParamError",
388
+ "GuardError",
389
+ "AdapterError",
390
+ "ImportError",
391
+ "ErrorFormatter",
392
+ "Guard",
393
+ "UseGuards",
394
+ "HttpException",
395
+ "BadRequestException",
396
+ "UnauthorizedException",
397
+ "ForbiddenException",
398
+ "NotFoundException",
399
+ "ConflictException",
400
+ "UnprocessableEntityException",
401
+ "InternalServerErrorException",
402
+ "OkResponse",
403
+ "CreatedResponse",
404
+ "NoContentResponse",
405
+ "HttpMethod",
406
+ "Body",
407
+ "Param",
408
+ "Query",
409
+ "Headers",
410
+ "Req",
411
+ "Res",
412
+ "FormData",
413
+ "Injectable",
414
+ "Jwt",
415
+ "JwtModule",
416
+ "JwtService",
417
+ "UseJwt",
418
+ "Module",
419
+ "OnModuleInit",
420
+ "OnModuleDestroy",
421
+ "ApiTags",
422
+ "ApiOperation",
423
+ "ApiResponse",
424
+ "ApiBody",
425
+ "Render",
426
+ "Cron",
427
+ "Timeout",
428
+ "Test",
429
+ "TestingModuleBuilder",
430
+ "TestApp",
431
+ "Logger",
432
+ ];
433
+ const TYPE_ONLY_EXPORTS = [
434
+ "RabbitMessage",
435
+ "RabbitPublishOptions",
436
+ "RabbitSubscribeOptions",
437
+ "DeadLetterMessage",
438
+ "DeadLetterDeathInfo",
439
+ "RequeueOptions",
440
+ "RabbitMQExchangeConfig",
441
+ "RabbitMQQueueBinding",
442
+ "RabbitMQQueueConfig",
443
+ "RabbitMQReconnectConfig",
444
+ "RabbitMQModuleOptions",
445
+ "HttpRequest",
446
+ "ModuleConfig",
447
+ "Options",
448
+ "GuardContract",
449
+ ];
450
+
451
+ console.log(`\n${bold("@grupodiariodaregiao/bunstone")} — Public Exports\n`);
452
+ console.log(cyan(bold(" Value exports ")) + gray("(import { ... })"));
453
+ for (const name of VALUE_EXPORTS.sort())
454
+ console.log(` ${gray("·")} ${name}`);
455
+
456
+ console.log();
457
+ console.log(
458
+ yellow(bold(" Type-only exports ")) + gray("(import type { ... })"),
459
+ );
460
+ for (const name of TYPE_ONLY_EXPORTS.sort())
461
+ console.log(` ${gray("·")} ${name}`);
462
+ console.log();
463
+ }
464
+
465
+ // ─────────────────────────────────────────────────────────────────────────────
466
+ // bunstone new <project-name>
467
+ // ─────────────────────────────────────────────────────────────────────────────
468
+
27
469
  const starterPath = join(import.meta.dir, "..", "starter");
28
470
 
29
471
  async function copyDir(src: string, dest: string) {
@@ -34,28 +476,20 @@ async function copyDir(src: string, dest: string) {
34
476
  const srcPath = join(src, entry.name);
35
477
  const destPath = join(dest, entry.name);
36
478
 
37
- if (entry.isDirectory()) {
38
- await copyDir(srcPath, destPath);
39
- } else {
40
- await copyFile(srcPath, destPath);
41
- }
479
+ if (entry.isDirectory()) await copyDir(srcPath, destPath);
480
+ else await copyFile(srcPath, destPath);
42
481
  }
43
482
  }
44
483
 
45
- async function scaffold() {
46
- if (command !== "new" || !projectName) {
47
- console.log("Usage: bunstone new <project-name>");
48
- console.log(" or: bunstone <project-name>");
49
- process.exit(1);
50
- }
484
+ async function scaffold(projectName_?: string) {
485
+ const projectName = projectName_ ?? "my-bunstone-app";
486
+ const projectPath = join(process.cwd(), projectName);
51
487
 
52
488
  console.log(`🚀 Scaffolding new Bunstone project in ${projectPath}...`);
53
489
 
54
490
  try {
55
- // Copy the entire starter directory
56
491
  await copyDir(starterPath, projectPath);
57
492
 
58
- // Update package.json name
59
493
  const pkgPath = join(projectPath, "package.json");
60
494
  const pkgContent = await readFile(pkgPath, "utf-8");
61
495
  const pkg = JSON.parse(pkgContent);
@@ -82,4 +516,22 @@ async function scaffold() {
82
516
  }
83
517
  }
84
518
 
85
- scaffold();
519
+ // ─────────────────────────────────────────────────────────────────────────────
520
+ // Help
521
+ // ─────────────────────────────────────────────────────────────────────────────
522
+
523
+ function printHelp() {
524
+ console.log(`
525
+ ${bold("bunstone")} — CLI for the Bunstone framework
526
+
527
+ ${cyan("Usage:")}
528
+ bunstone new <project-name> Scaffold a new project
529
+ bunstone run [bun-flags] <entry> Run your app with enhanced error messages
530
+ bunstone exports List all public exports
531
+
532
+ ${cyan("Examples:")}
533
+ bunstone new my-api
534
+ bunstone run src/main.ts
535
+ bunstone run --watch src/main.ts
536
+ `);
537
+ }
package/dist/index.d.ts CHANGED
@@ -46,6 +46,7 @@ export * from "./lib/rabbitmq/decorators/subscribe.decorator";
46
46
  export type * from "./lib/rabbitmq/interfaces/rabbitmq-options.interface";
47
47
  export type * from "./lib/rabbitmq/interfaces/rabbitmq-message.interface";
48
48
  export * from "./lib/errors";
49
+ export { ErrorFormatter } from "./lib/utils/error-formatter";
49
50
  export * from "./lib/guard";
50
51
  export * from "./lib/http-exceptions";
51
52
  export * from "./lib/http-methods";