@grupodiariodaregiao/bunstone 0.4.5 → 0.4.7
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 +476 -24
- package/dist/index.js +220 -3
- package/dist/lib/errors/index.d.ts +23 -0
- package/dist/lib/utils/error-formatter.d.ts +8 -0
- package/dist/lib/utils/fuzzy-match.d.ts +15 -0
- package/dist/lib/utils/known-exports.d.ts +15 -0
- package/lib/app-startup.ts +31 -2
- package/lib/errors/index.ts +89 -0
- package/lib/utils/error-formatter.ts +39 -2
- package/lib/utils/fuzzy-match.ts +62 -0
- package/lib/utils/known-exports.ts +162 -0
- package/package.json +1 -1
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
|
-
|
|
13
|
-
let projectName = args[1];
|
|
24
|
+
const command = args[0];
|
|
14
25
|
|
|
15
|
-
//
|
|
16
|
-
if (command
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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.js
CHANGED
|
@@ -32499,6 +32499,41 @@ class AdapterError extends BunstoneError {
|
|
|
32499
32499
|
}
|
|
32500
32500
|
}
|
|
32501
32501
|
|
|
32502
|
+
class ImportError extends BunstoneError {
|
|
32503
|
+
constructor(message, code = "BNS-IMP-002", suggestion, context, cause) {
|
|
32504
|
+
super(message, code, suggestion, context, cause);
|
|
32505
|
+
}
|
|
32506
|
+
static typeOnlyImport(name, pkg, valueAlternatives) {
|
|
32507
|
+
const altSection = valueAlternatives.length > 0 ? [
|
|
32508
|
+
"",
|
|
32509
|
+
"If you were looking for a runtime value with a similar name, did you mean one of these?",
|
|
32510
|
+
...valueAlternatives.map((s) => ` - ${s}`)
|
|
32511
|
+
].join(`
|
|
32512
|
+
`) : "";
|
|
32513
|
+
return new ImportError(`'${name}' is a type-only export of '${pkg}' \u2014 it does not exist at runtime.`, "BNS-IMP-001", [
|
|
32514
|
+
`Replace the import with 'import type':`,
|
|
32515
|
+
` \u2717 import { ${name} } from '${pkg}'`,
|
|
32516
|
+
` \u2713 import type { ${name} } from '${pkg}'`,
|
|
32517
|
+
altSection
|
|
32518
|
+
].filter(Boolean).join(`
|
|
32519
|
+
`), { name, package: pkg });
|
|
32520
|
+
}
|
|
32521
|
+
static unknownExport(name, pkg, suggestions) {
|
|
32522
|
+
const didYouMean = suggestions.length > 0 ? [
|
|
32523
|
+
"",
|
|
32524
|
+
"Did you mean one of these?",
|
|
32525
|
+
...suggestions.map((s) => ` - ${s}`)
|
|
32526
|
+
].join(`
|
|
32527
|
+
`) : "";
|
|
32528
|
+
return new ImportError(`'${name}' is not exported by '${pkg}'.`, "BNS-IMP-002", [
|
|
32529
|
+
"Check the spelling of the imported name.",
|
|
32530
|
+
"Run 'bunstone exports' to see all available exports.",
|
|
32531
|
+
didYouMean
|
|
32532
|
+
].filter(Boolean).join(`
|
|
32533
|
+
`), { name, package: pkg });
|
|
32534
|
+
}
|
|
32535
|
+
}
|
|
32536
|
+
|
|
32502
32537
|
// lib/injectable.ts
|
|
32503
32538
|
var import_reflect_metadata = __toESM(require_Reflect(), 1);
|
|
32504
32539
|
function Injectable() {
|
|
@@ -43269,7 +43304,156 @@ var colors = {
|
|
|
43269
43304
|
strikethrough: "\x1B[9m"
|
|
43270
43305
|
};
|
|
43271
43306
|
|
|
43307
|
+
// lib/utils/fuzzy-match.ts
|
|
43308
|
+
function editDistance(a, b3) {
|
|
43309
|
+
const s1 = a.toLowerCase();
|
|
43310
|
+
const s2 = b3.toLowerCase();
|
|
43311
|
+
const m = s1.length;
|
|
43312
|
+
const n2 = s2.length;
|
|
43313
|
+
let prev = Array.from({ length: n2 + 1 }, (_3, j3) => j3);
|
|
43314
|
+
let curr = new Array(n2 + 1).fill(0);
|
|
43315
|
+
for (let i = 1;i <= m; i++) {
|
|
43316
|
+
curr[0] = i;
|
|
43317
|
+
for (let j3 = 1;j3 <= n2; j3++) {
|
|
43318
|
+
const cost = s1[i - 1] === s2[j3 - 1] ? 0 : 1;
|
|
43319
|
+
curr[j3] = cost === 0 ? prev[j3 - 1] : 1 + Math.min(prev[j3], curr[j3 - 1], prev[j3 - 1]);
|
|
43320
|
+
}
|
|
43321
|
+
[prev, curr] = [curr, prev];
|
|
43322
|
+
}
|
|
43323
|
+
return prev[n2];
|
|
43324
|
+
}
|
|
43325
|
+
function closestMatches(name2, candidates, limit = 5, maxDistance = 4) {
|
|
43326
|
+
const results = [];
|
|
43327
|
+
for (const candidate of candidates) {
|
|
43328
|
+
const dist = editDistance(name2, candidate);
|
|
43329
|
+
if (dist <= maxDistance) {
|
|
43330
|
+
results.push({ name: candidate, dist });
|
|
43331
|
+
}
|
|
43332
|
+
}
|
|
43333
|
+
results.sort((a, b3) => a.dist - b3.dist);
|
|
43334
|
+
return results.slice(0, limit).map((r2) => r2.name);
|
|
43335
|
+
}
|
|
43336
|
+
|
|
43337
|
+
// lib/utils/known-exports.ts
|
|
43338
|
+
var TYPE_ONLY_EXPORTS = new Set([
|
|
43339
|
+
"RabbitMessage",
|
|
43340
|
+
"RabbitPublishOptions",
|
|
43341
|
+
"RabbitSubscribeOptions",
|
|
43342
|
+
"DeadLetterMessage",
|
|
43343
|
+
"DeadLetterDeathInfo",
|
|
43344
|
+
"RequeueOptions",
|
|
43345
|
+
"RabbitMQExchangeConfig",
|
|
43346
|
+
"RabbitMQQueueBinding",
|
|
43347
|
+
"RabbitMQQueueConfig",
|
|
43348
|
+
"RabbitMQReconnectConfig",
|
|
43349
|
+
"RabbitMQModuleOptions",
|
|
43350
|
+
"HttpRequest",
|
|
43351
|
+
"ModuleConfig",
|
|
43352
|
+
"Options",
|
|
43353
|
+
"GuardContract"
|
|
43354
|
+
]);
|
|
43355
|
+
var VALUE_EXPORTS = [
|
|
43356
|
+
"CacheAdapter",
|
|
43357
|
+
"FormDataAdapter",
|
|
43358
|
+
"UploadAdapter",
|
|
43359
|
+
"EmailModule",
|
|
43360
|
+
"EmailService",
|
|
43361
|
+
"EmailLayout",
|
|
43362
|
+
"AppStartup",
|
|
43363
|
+
"Layout",
|
|
43364
|
+
"Controller",
|
|
43365
|
+
"Get",
|
|
43366
|
+
"Post",
|
|
43367
|
+
"Put",
|
|
43368
|
+
"Patch",
|
|
43369
|
+
"Delete",
|
|
43370
|
+
"Head",
|
|
43371
|
+
"Options",
|
|
43372
|
+
"RateLimit",
|
|
43373
|
+
"RateLimitGuard",
|
|
43374
|
+
"RedisStorage",
|
|
43375
|
+
"CommandBus",
|
|
43376
|
+
"CqrsModule",
|
|
43377
|
+
"CommandHandler",
|
|
43378
|
+
"EventHandler",
|
|
43379
|
+
"QueryHandler",
|
|
43380
|
+
"Saga",
|
|
43381
|
+
"EventBus",
|
|
43382
|
+
"QueryBus",
|
|
43383
|
+
"SqlModule",
|
|
43384
|
+
"SqlService",
|
|
43385
|
+
"BullMqModule",
|
|
43386
|
+
"QueueService",
|
|
43387
|
+
"Processor",
|
|
43388
|
+
"Process",
|
|
43389
|
+
"RabbitMQModule",
|
|
43390
|
+
"RabbitMQService",
|
|
43391
|
+
"RabbitMQDeadLetterService",
|
|
43392
|
+
"RabbitMQConnection",
|
|
43393
|
+
"RabbitConsumer",
|
|
43394
|
+
"RabbitSubscribe",
|
|
43395
|
+
"BunstoneError",
|
|
43396
|
+
"DependencyResolutionError",
|
|
43397
|
+
"ModuleInitializationError",
|
|
43398
|
+
"ConfigurationError",
|
|
43399
|
+
"CqrsError",
|
|
43400
|
+
"DatabaseError",
|
|
43401
|
+
"BullMQError",
|
|
43402
|
+
"RabbitMQError",
|
|
43403
|
+
"ScheduleError",
|
|
43404
|
+
"TestingError",
|
|
43405
|
+
"RateLimitError",
|
|
43406
|
+
"UploadError",
|
|
43407
|
+
"EmailError",
|
|
43408
|
+
"HttpParamError",
|
|
43409
|
+
"GuardError",
|
|
43410
|
+
"AdapterError",
|
|
43411
|
+
"ImportError",
|
|
43412
|
+
"ErrorFormatter",
|
|
43413
|
+
"Guard",
|
|
43414
|
+
"UseGuards",
|
|
43415
|
+
"HttpException",
|
|
43416
|
+
"BadRequestException",
|
|
43417
|
+
"UnauthorizedException",
|
|
43418
|
+
"ForbiddenException",
|
|
43419
|
+
"NotFoundException",
|
|
43420
|
+
"ConflictException",
|
|
43421
|
+
"UnprocessableEntityException",
|
|
43422
|
+
"InternalServerErrorException",
|
|
43423
|
+
"OkResponse",
|
|
43424
|
+
"CreatedResponse",
|
|
43425
|
+
"NoContentResponse",
|
|
43426
|
+
"HttpMethod",
|
|
43427
|
+
"Body",
|
|
43428
|
+
"Param",
|
|
43429
|
+
"Query",
|
|
43430
|
+
"Headers",
|
|
43431
|
+
"Req",
|
|
43432
|
+
"Res",
|
|
43433
|
+
"FormData",
|
|
43434
|
+
"Injectable",
|
|
43435
|
+
"Jwt",
|
|
43436
|
+
"JwtModule",
|
|
43437
|
+
"JwtService",
|
|
43438
|
+
"UseJwt",
|
|
43439
|
+
"Module",
|
|
43440
|
+
"OnModuleInit",
|
|
43441
|
+
"OnModuleDestroy",
|
|
43442
|
+
"ApiTags",
|
|
43443
|
+
"ApiOperation",
|
|
43444
|
+
"ApiResponse",
|
|
43445
|
+
"ApiBody",
|
|
43446
|
+
"Render",
|
|
43447
|
+
"Cron",
|
|
43448
|
+
"Timeout",
|
|
43449
|
+
"Test",
|
|
43450
|
+
"TestingModuleBuilder",
|
|
43451
|
+
"TestApp",
|
|
43452
|
+
"Logger"
|
|
43453
|
+
];
|
|
43454
|
+
|
|
43272
43455
|
// lib/utils/error-formatter.ts
|
|
43456
|
+
var BUN_EXPORT_NOT_FOUND = /Export named '(.+?)' not found in module '(.+?)'/;
|
|
43273
43457
|
var BORDER = "\u2501".repeat(64);
|
|
43274
43458
|
var THIN = "\u2500".repeat(64);
|
|
43275
43459
|
function label(text, color) {
|
|
@@ -43282,8 +43466,26 @@ function indent(text, spaces = 4) {
|
|
|
43282
43466
|
}
|
|
43283
43467
|
|
|
43284
43468
|
class ErrorFormatter {
|
|
43469
|
+
static fromBunSyntaxError(error) {
|
|
43470
|
+
if (!(error instanceof SyntaxError))
|
|
43471
|
+
return null;
|
|
43472
|
+
const match = BUN_EXPORT_NOT_FOUND.exec(error.message);
|
|
43473
|
+
if (!match)
|
|
43474
|
+
return null;
|
|
43475
|
+
const name2 = match[1] ?? "";
|
|
43476
|
+
const modulePath = match[2] ?? "";
|
|
43477
|
+
const pkg = modulePath.includes("node_modules/") ? modulePath.replace(/.*node_modules\//, "").replace(/\/dist.*/, "") : modulePath;
|
|
43478
|
+
if (TYPE_ONLY_EXPORTS.has(name2)) {
|
|
43479
|
+
const valueAlternatives = closestMatches(name2, VALUE_EXPORTS);
|
|
43480
|
+
return ImportError.typeOnlyImport(name2, pkg, valueAlternatives);
|
|
43481
|
+
}
|
|
43482
|
+
const allNames = [...VALUE_EXPORTS, ...TYPE_ONLY_EXPORTS];
|
|
43483
|
+
const suggestions = closestMatches(name2, allNames);
|
|
43484
|
+
return ImportError.unknownExport(name2, pkg, suggestions);
|
|
43485
|
+
}
|
|
43285
43486
|
static format(error, exit = false) {
|
|
43286
|
-
const
|
|
43487
|
+
const upgraded = ErrorFormatter.fromBunSyntaxError(error);
|
|
43488
|
+
const e2 = upgraded ?? error;
|
|
43287
43489
|
console.error(`
|
|
43288
43490
|
${colors.red}${BORDER}${colors.reset}`);
|
|
43289
43491
|
console.error(`${colors.red}${colors.bold} \uD83D\uDCA5 Bunstone \u2014 Error Report${colors.reset}`);
|
|
@@ -117981,6 +118183,13 @@ if (document.readyState === 'loading') {
|
|
|
117981
118183
|
return raw.content.toString();
|
|
117982
118184
|
}
|
|
117983
118185
|
})();
|
|
118186
|
+
const xDeath = raw.properties.headers?.["x-death"];
|
|
118187
|
+
const isDeadLetter = Array.isArray(xDeath) && xDeath.length > 0;
|
|
118188
|
+
const isDlqQueue = queueName.toLowerCase().includes(".dlq");
|
|
118189
|
+
if ((isDlqQueue || descriptor.options.queue?.toLowerCase().includes(".dlq")) && !isDeadLetter) {
|
|
118190
|
+
channel.ack(raw);
|
|
118191
|
+
return;
|
|
118192
|
+
}
|
|
117984
118193
|
const msg = {
|
|
117985
118194
|
data,
|
|
117986
118195
|
raw,
|
|
@@ -117993,7 +118202,7 @@ if (document.readyState === 'loading') {
|
|
|
117993
118202
|
} catch (err) {
|
|
117994
118203
|
AppStartup.logger.error(`Unhandled error in RabbitMQ handler ${providerClass.name}.${descriptor.methodName}() on exchange "${exchange}" routingKey "${routingKey}": ${err.message}`);
|
|
117995
118204
|
if (!noAck) {
|
|
117996
|
-
channel.nack(raw, false,
|
|
118205
|
+
channel.nack(raw, false, false);
|
|
117997
118206
|
}
|
|
117998
118207
|
}
|
|
117999
118208
|
}, { noAck });
|
|
@@ -118036,6 +118245,13 @@ if (document.readyState === 'loading') {
|
|
|
118036
118245
|
return raw.content.toString();
|
|
118037
118246
|
}
|
|
118038
118247
|
})();
|
|
118248
|
+
const xDeath = raw.properties.headers?.["x-death"];
|
|
118249
|
+
const isDeadLetter = Array.isArray(xDeath) && xDeath.length > 0;
|
|
118250
|
+
const isDlqQueue = queue2.toLowerCase().includes(".dlq");
|
|
118251
|
+
if (isDlqQueue && !isDeadLetter) {
|
|
118252
|
+
channel.ack(raw);
|
|
118253
|
+
return;
|
|
118254
|
+
}
|
|
118039
118255
|
let settled = false;
|
|
118040
118256
|
const settle = (fn3) => {
|
|
118041
118257
|
if (!settled) {
|
|
@@ -118065,7 +118281,7 @@ if (document.readyState === 'loading') {
|
|
|
118065
118281
|
} catch (err) {
|
|
118066
118282
|
AppStartup.logger.error(`Unhandled error in RabbitMQ handler ${providerName}.${descriptor.methodName}() on queue "${queue2}": ${err.message}`);
|
|
118067
118283
|
if (!handlerNoAck && !settled) {
|
|
118068
|
-
settle(() => channel.nack(raw, false,
|
|
118284
|
+
settle(() => channel.nack(raw, false, false));
|
|
118069
118285
|
}
|
|
118070
118286
|
}
|
|
118071
118287
|
}
|
|
@@ -120335,6 +120551,7 @@ export {
|
|
|
120335
120551
|
Jwt,
|
|
120336
120552
|
InternalServerErrorException,
|
|
120337
120553
|
Injectable,
|
|
120554
|
+
ImportError,
|
|
120338
120555
|
HttpParamError,
|
|
120339
120556
|
HttpException,
|
|
120340
120557
|
Header,
|
|
@@ -196,3 +196,26 @@ export declare class GuardError extends BunstoneError {
|
|
|
196
196
|
export declare class AdapterError extends BunstoneError {
|
|
197
197
|
constructor(message: string, code?: "BNS-ADP-001" | "BNS-ADP-002", suggestion?: string, context?: Record<string, unknown>, cause?: Error);
|
|
198
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Represents an import-time error detected by the `bunstone run` CLI.
|
|
201
|
+
*
|
|
202
|
+
* Bun raises a raw `SyntaxError: Export named 'X' not found` that gives no
|
|
203
|
+
* guidance on the problem. This error carries structured context so the CLI
|
|
204
|
+
* can print a rich, actionable crash report.
|
|
205
|
+
*
|
|
206
|
+
* Codes:
|
|
207
|
+
* - `BNS-IMP-001` – name is a type-only export (must use `import type`)
|
|
208
|
+
* - `BNS-IMP-002` – name does not exist at all in the package (typo / wrong name)
|
|
209
|
+
*/
|
|
210
|
+
export declare class ImportError extends BunstoneError {
|
|
211
|
+
constructor(message: string, code?: "BNS-IMP-001" | "BNS-IMP-002", suggestion?: string, context?: Record<string, unknown>, cause?: Error);
|
|
212
|
+
/**
|
|
213
|
+
* The user imported a type-only name as a value.
|
|
214
|
+
* e.g. `import { RabbitMessage }` instead of `import type { RabbitMessage }`.
|
|
215
|
+
*/
|
|
216
|
+
static typeOnlyImport(name: string, pkg: string, valueAlternatives: string[]): ImportError;
|
|
217
|
+
/**
|
|
218
|
+
* The user imported a name that does not exist in the package at all.
|
|
219
|
+
*/
|
|
220
|
+
static unknownExport(name: string, pkg: string, suggestions: string[]): ImportError;
|
|
221
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ImportError } from "../errors";
|
|
1
2
|
/**
|
|
2
3
|
* Formats and prints a structured "Crash Report" whenever a `BunstoneError`
|
|
3
4
|
* (or any unhandled error) aborts initialisation.
|
|
@@ -22,6 +23,13 @@
|
|
|
22
23
|
* ────────────────────────────────────────────────────────────────
|
|
23
24
|
*/
|
|
24
25
|
export declare class ErrorFormatter {
|
|
26
|
+
/**
|
|
27
|
+
* Attempts to convert a raw Bun `SyntaxError: Export named 'X' not found`
|
|
28
|
+
* into a structured `ImportError` with actionable hints.
|
|
29
|
+
*
|
|
30
|
+
* Returns `null` when the error does not match the pattern.
|
|
31
|
+
*/
|
|
32
|
+
static fromBunSyntaxError(error: unknown): ImportError | null;
|
|
25
33
|
/**
|
|
26
34
|
* Prints a full crash report and (optionally) exits the process.
|
|
27
35
|
*
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight fuzzy-matching utilities used by the `bunstone run` CLI command
|
|
3
|
+
* to produce "did you mean?" suggestions when an export name is not found.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Levenshtein edit distance between two strings (case-insensitive).
|
|
7
|
+
*/
|
|
8
|
+
export declare function editDistance(a: string, b: string): number;
|
|
9
|
+
/**
|
|
10
|
+
* Returns up to `limit` closest matches for `name` from `candidates`,
|
|
11
|
+
* ordered by edit distance (closest first).
|
|
12
|
+
*
|
|
13
|
+
* Only returns candidates whose distance is ≤ `maxDistance`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function closestMatches(name: string, candidates: Iterable<string>, limit?: number, maxDistance?: number): string[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalogue of every public name exported from the bunstone package,
|
|
3
|
+
* split by whether it is a **value** (present at runtime) or a **type**
|
|
4
|
+
* (erased at compile-time, absent from the JS bundle).
|
|
5
|
+
*
|
|
6
|
+
* Used by the `bunstone run` CLI command to produce "did you mean?" hints
|
|
7
|
+
* when Bun throws `SyntaxError: Export named 'X' not found`.
|
|
8
|
+
*/
|
|
9
|
+
/** Names that are exported with `export type` – they do NOT exist in the JS bundle. */
|
|
10
|
+
export declare const TYPE_ONLY_EXPORTS: ReadonlySet<string>;
|
|
11
|
+
/**
|
|
12
|
+
* All value names shipped in the bundle.
|
|
13
|
+
* Keep in sync with `index.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const VALUE_EXPORTS: ReadonlyArray<string>;
|
package/lib/app-startup.ts
CHANGED
|
@@ -1080,6 +1080,24 @@ if (document.readyState === 'loading') {
|
|
|
1080
1080
|
}
|
|
1081
1081
|
})();
|
|
1082
1082
|
|
|
1083
|
+
const xDeath = raw.properties.headers?.["x-death"];
|
|
1084
|
+
const isDeadLetter =
|
|
1085
|
+
Array.isArray(xDeath) && xDeath.length > 0;
|
|
1086
|
+
const isDlqQueue = queueName.toLowerCase().includes(".dlq");
|
|
1087
|
+
|
|
1088
|
+
// If the handler is on a DLQ or uses DeadLetterMessage type,
|
|
1089
|
+
// only process messages that have x-death headers.
|
|
1090
|
+
if (
|
|
1091
|
+
(isDlqQueue ||
|
|
1092
|
+
descriptor.options.queue
|
|
1093
|
+
?.toLowerCase()
|
|
1094
|
+
.includes(".dlq")) &&
|
|
1095
|
+
!isDeadLetter
|
|
1096
|
+
) {
|
|
1097
|
+
channel.ack(raw);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1083
1101
|
const msg: RabbitMessage = {
|
|
1084
1102
|
data,
|
|
1085
1103
|
raw,
|
|
@@ -1095,7 +1113,7 @@ if (document.readyState === 'loading') {
|
|
|
1095
1113
|
`Unhandled error in RabbitMQ handler ${providerClass.name}.${descriptor.methodName}() on exchange "${exchange}" routingKey "${routingKey}": ${err.message}`,
|
|
1096
1114
|
);
|
|
1097
1115
|
if (!noAck) {
|
|
1098
|
-
channel.nack(raw, false,
|
|
1116
|
+
channel.nack(raw, false, false);
|
|
1099
1117
|
}
|
|
1100
1118
|
}
|
|
1101
1119
|
},
|
|
@@ -1161,6 +1179,17 @@ if (document.readyState === 'loading') {
|
|
|
1161
1179
|
}
|
|
1162
1180
|
})();
|
|
1163
1181
|
|
|
1182
|
+
const xDeath = raw.properties.headers?.["x-death"];
|
|
1183
|
+
const isDeadLetter = Array.isArray(xDeath) && xDeath.length > 0;
|
|
1184
|
+
const isDlqQueue = queue.toLowerCase().includes(".dlq");
|
|
1185
|
+
|
|
1186
|
+
// If this is a DLQ, skip messages that don't have x-death headers
|
|
1187
|
+
// (i.e. someone published directly to the DLQ instead of it being a failed message)
|
|
1188
|
+
if (isDlqQueue && !isDeadLetter) {
|
|
1189
|
+
channel.ack(raw);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1164
1193
|
// Settle guard: ack/nack/reject may only be called once per
|
|
1165
1194
|
// delivery tag regardless of how many handlers invoke it.
|
|
1166
1195
|
let settled = false;
|
|
@@ -1204,7 +1233,7 @@ if (document.readyState === 'loading') {
|
|
|
1204
1233
|
`Unhandled error in RabbitMQ handler ${providerName}.${descriptor.methodName}() on queue "${queue}": ${err.message}`,
|
|
1205
1234
|
);
|
|
1206
1235
|
if (!handlerNoAck && !settled) {
|
|
1207
|
-
settle(() => channel.nack(raw, false,
|
|
1236
|
+
settle(() => channel.nack(raw, false, false));
|
|
1208
1237
|
}
|
|
1209
1238
|
}
|
|
1210
1239
|
}
|
package/lib/errors/index.ts
CHANGED
|
@@ -679,3 +679,92 @@ export class AdapterError extends BunstoneError {
|
|
|
679
679
|
super(message, code, suggestion, context, cause);
|
|
680
680
|
}
|
|
681
681
|
}
|
|
682
|
+
|
|
683
|
+
// ─── Import ───────────────────────────────────────────────────────────────────
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Represents an import-time error detected by the `bunstone run` CLI.
|
|
687
|
+
*
|
|
688
|
+
* Bun raises a raw `SyntaxError: Export named 'X' not found` that gives no
|
|
689
|
+
* guidance on the problem. This error carries structured context so the CLI
|
|
690
|
+
* can print a rich, actionable crash report.
|
|
691
|
+
*
|
|
692
|
+
* Codes:
|
|
693
|
+
* - `BNS-IMP-001` – name is a type-only export (must use `import type`)
|
|
694
|
+
* - `BNS-IMP-002` – name does not exist at all in the package (typo / wrong name)
|
|
695
|
+
*/
|
|
696
|
+
export class ImportError extends BunstoneError {
|
|
697
|
+
constructor(
|
|
698
|
+
message: string,
|
|
699
|
+
code: "BNS-IMP-001" | "BNS-IMP-002" = "BNS-IMP-002",
|
|
700
|
+
suggestion?: string,
|
|
701
|
+
context?: Record<string, unknown>,
|
|
702
|
+
cause?: Error,
|
|
703
|
+
) {
|
|
704
|
+
super(message, code, suggestion, context, cause);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* The user imported a type-only name as a value.
|
|
709
|
+
* e.g. `import { RabbitMessage }` instead of `import type { RabbitMessage }`.
|
|
710
|
+
*/
|
|
711
|
+
static typeOnlyImport(
|
|
712
|
+
name: string,
|
|
713
|
+
pkg: string,
|
|
714
|
+
valueAlternatives: string[],
|
|
715
|
+
): ImportError {
|
|
716
|
+
const altSection =
|
|
717
|
+
valueAlternatives.length > 0
|
|
718
|
+
? [
|
|
719
|
+
"",
|
|
720
|
+
"If you were looking for a runtime value with a similar name, did you mean one of these?",
|
|
721
|
+
...valueAlternatives.map((s) => ` - ${s}`),
|
|
722
|
+
].join("\n ")
|
|
723
|
+
: "";
|
|
724
|
+
|
|
725
|
+
return new ImportError(
|
|
726
|
+
`'${name}' is a type-only export of '${pkg}' — it does not exist at runtime.`,
|
|
727
|
+
"BNS-IMP-001",
|
|
728
|
+
[
|
|
729
|
+
`Replace the import with 'import type':`,
|
|
730
|
+
` ✗ import { ${name} } from '${pkg}'`,
|
|
731
|
+
` ✓ import type { ${name} } from '${pkg}'`,
|
|
732
|
+
altSection,
|
|
733
|
+
]
|
|
734
|
+
.filter(Boolean)
|
|
735
|
+
.join("\n "),
|
|
736
|
+
{ name, package: pkg },
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* The user imported a name that does not exist in the package at all.
|
|
742
|
+
*/
|
|
743
|
+
static unknownExport(
|
|
744
|
+
name: string,
|
|
745
|
+
pkg: string,
|
|
746
|
+
suggestions: string[],
|
|
747
|
+
): ImportError {
|
|
748
|
+
const didYouMean =
|
|
749
|
+
suggestions.length > 0
|
|
750
|
+
? [
|
|
751
|
+
"",
|
|
752
|
+
"Did you mean one of these?",
|
|
753
|
+
...suggestions.map((s) => ` - ${s}`),
|
|
754
|
+
].join("\n ")
|
|
755
|
+
: "";
|
|
756
|
+
|
|
757
|
+
return new ImportError(
|
|
758
|
+
`'${name}' is not exported by '${pkg}'.`,
|
|
759
|
+
"BNS-IMP-002",
|
|
760
|
+
[
|
|
761
|
+
"Check the spelling of the imported name.",
|
|
762
|
+
"Run 'bunstone exports' to see all available exports.",
|
|
763
|
+
didYouMean,
|
|
764
|
+
]
|
|
765
|
+
.filter(Boolean)
|
|
766
|
+
.join("\n "),
|
|
767
|
+
{ name, package: pkg },
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { BunstoneError } from "../errors";
|
|
1
|
+
import { BunstoneError, ImportError } from "../errors";
|
|
2
2
|
import { colors } from "./colors";
|
|
3
|
+
import { closestMatches } from "./fuzzy-match";
|
|
4
|
+
import { TYPE_ONLY_EXPORTS, VALUE_EXPORTS } from "./known-exports";
|
|
5
|
+
|
|
6
|
+
/** Regexp that matches Bun's "Export named 'X' not found in module 'Y'." message. */
|
|
7
|
+
const BUN_EXPORT_NOT_FOUND = /Export named '(.+?)' not found in module '(.+?)'/;
|
|
3
8
|
|
|
4
9
|
const BORDER = "━".repeat(64);
|
|
5
10
|
const THIN = "─".repeat(64);
|
|
@@ -39,6 +44,36 @@ function indent(text: string, spaces = 4): string {
|
|
|
39
44
|
* ────────────────────────────────────────────────────────────────
|
|
40
45
|
*/
|
|
41
46
|
export class ErrorFormatter {
|
|
47
|
+
/**
|
|
48
|
+
* Attempts to convert a raw Bun `SyntaxError: Export named 'X' not found`
|
|
49
|
+
* into a structured `ImportError` with actionable hints.
|
|
50
|
+
*
|
|
51
|
+
* Returns `null` when the error does not match the pattern.
|
|
52
|
+
*/
|
|
53
|
+
static fromBunSyntaxError(error: unknown): ImportError | null {
|
|
54
|
+
if (!(error instanceof SyntaxError)) return null;
|
|
55
|
+
|
|
56
|
+
const match = BUN_EXPORT_NOT_FOUND.exec(error.message);
|
|
57
|
+
if (!match) return null;
|
|
58
|
+
|
|
59
|
+
const name = match[1] ?? "";
|
|
60
|
+
const modulePath = match[2] ?? "";
|
|
61
|
+
const pkg = modulePath.includes("node_modules/")
|
|
62
|
+
? modulePath.replace(/.*node_modules\//, "").replace(/\/dist.*/, "")
|
|
63
|
+
: modulePath;
|
|
64
|
+
|
|
65
|
+
if (TYPE_ONLY_EXPORTS.has(name)) {
|
|
66
|
+
// Name exists but is type-only
|
|
67
|
+
const valueAlternatives = closestMatches(name, VALUE_EXPORTS);
|
|
68
|
+
return ImportError.typeOnlyImport(name, pkg, valueAlternatives);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Name does not exist at all – fuzzy match against all known names
|
|
72
|
+
const allNames = [...VALUE_EXPORTS, ...TYPE_ONLY_EXPORTS];
|
|
73
|
+
const suggestions = closestMatches(name, allNames);
|
|
74
|
+
return ImportError.unknownExport(name, pkg, suggestions);
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
/**
|
|
43
78
|
* Prints a full crash report and (optionally) exits the process.
|
|
44
79
|
*
|
|
@@ -46,7 +81,9 @@ export class ErrorFormatter {
|
|
|
46
81
|
* @param exit When `true` (default), calls `process.exit(1)` after printing.
|
|
47
82
|
*/
|
|
48
83
|
static format(error: unknown, exit = false): void {
|
|
49
|
-
|
|
84
|
+
// Upgrade raw Bun import errors before formatting
|
|
85
|
+
const upgraded = ErrorFormatter.fromBunSyntaxError(error);
|
|
86
|
+
const e = (upgraded ?? error) as any;
|
|
50
87
|
|
|
51
88
|
console.error(`\n${colors.red}${BORDER}${colors.reset}`);
|
|
52
89
|
console.error(
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight fuzzy-matching utilities used by the `bunstone run` CLI command
|
|
3
|
+
* to produce "did you mean?" suggestions when an export name is not found.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Levenshtein edit distance between two strings (case-insensitive).
|
|
8
|
+
*/
|
|
9
|
+
export function editDistance(a: string, b: string): number {
|
|
10
|
+
const s1 = a.toLowerCase();
|
|
11
|
+
const s2 = b.toLowerCase();
|
|
12
|
+
const m = s1.length;
|
|
13
|
+
const n = s2.length;
|
|
14
|
+
|
|
15
|
+
// Two-row rolling DP – avoids 2D-array index safety issues.
|
|
16
|
+
let prev: number[] = Array.from({ length: n + 1 }, (_, j) => j);
|
|
17
|
+
let curr: number[] = new Array<number>(n + 1).fill(0);
|
|
18
|
+
|
|
19
|
+
for (let i = 1; i <= m; i++) {
|
|
20
|
+
curr[0] = i;
|
|
21
|
+
for (let j = 1; j <= n; j++) {
|
|
22
|
+
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
|
23
|
+
curr[j] =
|
|
24
|
+
cost === 0
|
|
25
|
+
? (prev[j - 1] as number)
|
|
26
|
+
: 1 +
|
|
27
|
+
Math.min(
|
|
28
|
+
prev[j] as number,
|
|
29
|
+
curr[j - 1] as number,
|
|
30
|
+
prev[j - 1] as number,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
[prev, curr] = [curr, prev];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return prev[n] as number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns up to `limit` closest matches for `name` from `candidates`,
|
|
41
|
+
* ordered by edit distance (closest first).
|
|
42
|
+
*
|
|
43
|
+
* Only returns candidates whose distance is ≤ `maxDistance`.
|
|
44
|
+
*/
|
|
45
|
+
export function closestMatches(
|
|
46
|
+
name: string,
|
|
47
|
+
candidates: Iterable<string>,
|
|
48
|
+
limit = 5,
|
|
49
|
+
maxDistance = 4,
|
|
50
|
+
): string[] {
|
|
51
|
+
const results: { name: string; dist: number }[] = [];
|
|
52
|
+
|
|
53
|
+
for (const candidate of candidates) {
|
|
54
|
+
const dist = editDistance(name, candidate);
|
|
55
|
+
if (dist <= maxDistance) {
|
|
56
|
+
results.push({ name: candidate, dist });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
results.sort((a, b) => a.dist - b.dist);
|
|
61
|
+
return results.slice(0, limit).map((r) => r.name);
|
|
62
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalogue of every public name exported from the bunstone package,
|
|
3
|
+
* split by whether it is a **value** (present at runtime) or a **type**
|
|
4
|
+
* (erased at compile-time, absent from the JS bundle).
|
|
5
|
+
*
|
|
6
|
+
* Used by the `bunstone run` CLI command to produce "did you mean?" hints
|
|
7
|
+
* when Bun throws `SyntaxError: Export named 'X' not found`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ── Type-only exports (erased at runtime) ─────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** Names that are exported with `export type` – they do NOT exist in the JS bundle. */
|
|
13
|
+
export const TYPE_ONLY_EXPORTS: ReadonlySet<string> = new Set([
|
|
14
|
+
// rabbitmq-message.interface
|
|
15
|
+
"RabbitMessage",
|
|
16
|
+
"RabbitPublishOptions",
|
|
17
|
+
"RabbitSubscribeOptions",
|
|
18
|
+
"DeadLetterMessage",
|
|
19
|
+
"DeadLetterDeathInfo",
|
|
20
|
+
"RequeueOptions",
|
|
21
|
+
// rabbitmq-options.interface
|
|
22
|
+
"RabbitMQExchangeConfig",
|
|
23
|
+
"RabbitMQQueueBinding",
|
|
24
|
+
"RabbitMQQueueConfig",
|
|
25
|
+
"RabbitMQReconnectConfig",
|
|
26
|
+
"RabbitMQModuleOptions",
|
|
27
|
+
// general types
|
|
28
|
+
"HttpRequest",
|
|
29
|
+
"ModuleConfig",
|
|
30
|
+
"Options",
|
|
31
|
+
"GuardContract",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
// ── Value exports (present at runtime) ────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* All value names shipped in the bundle.
|
|
38
|
+
* Keep in sync with `index.ts`.
|
|
39
|
+
*/
|
|
40
|
+
export const VALUE_EXPORTS: ReadonlyArray<string> = [
|
|
41
|
+
// adapters
|
|
42
|
+
"CacheAdapter",
|
|
43
|
+
"FormDataAdapter",
|
|
44
|
+
"UploadAdapter",
|
|
45
|
+
// email
|
|
46
|
+
"EmailModule",
|
|
47
|
+
"EmailService",
|
|
48
|
+
"EmailLayout",
|
|
49
|
+
// app
|
|
50
|
+
"AppStartup",
|
|
51
|
+
// components
|
|
52
|
+
"Layout",
|
|
53
|
+
// controller
|
|
54
|
+
"Controller",
|
|
55
|
+
"Get",
|
|
56
|
+
"Post",
|
|
57
|
+
"Put",
|
|
58
|
+
"Patch",
|
|
59
|
+
"Delete",
|
|
60
|
+
"Head",
|
|
61
|
+
"Options",
|
|
62
|
+
// rate limit
|
|
63
|
+
"RateLimit",
|
|
64
|
+
"RateLimitGuard",
|
|
65
|
+
"RedisStorage",
|
|
66
|
+
// cqrs
|
|
67
|
+
"CommandBus",
|
|
68
|
+
"CqrsModule",
|
|
69
|
+
"CommandHandler",
|
|
70
|
+
"EventHandler",
|
|
71
|
+
"QueryHandler",
|
|
72
|
+
"Saga",
|
|
73
|
+
"EventBus",
|
|
74
|
+
"QueryBus",
|
|
75
|
+
// database
|
|
76
|
+
"SqlModule",
|
|
77
|
+
"SqlService",
|
|
78
|
+
// bullmq
|
|
79
|
+
"BullMqModule",
|
|
80
|
+
"QueueService",
|
|
81
|
+
"Processor",
|
|
82
|
+
"Process",
|
|
83
|
+
// rabbitmq
|
|
84
|
+
"RabbitMQModule",
|
|
85
|
+
"RabbitMQService",
|
|
86
|
+
"RabbitMQDeadLetterService",
|
|
87
|
+
"RabbitMQConnection",
|
|
88
|
+
"RabbitConsumer",
|
|
89
|
+
"RabbitSubscribe",
|
|
90
|
+
// errors
|
|
91
|
+
"BunstoneError",
|
|
92
|
+
"DependencyResolutionError",
|
|
93
|
+
"ModuleInitializationError",
|
|
94
|
+
"ConfigurationError",
|
|
95
|
+
"CqrsError",
|
|
96
|
+
"DatabaseError",
|
|
97
|
+
"BullMQError",
|
|
98
|
+
"RabbitMQError",
|
|
99
|
+
"ScheduleError",
|
|
100
|
+
"TestingError",
|
|
101
|
+
"RateLimitError",
|
|
102
|
+
"UploadError",
|
|
103
|
+
"EmailError",
|
|
104
|
+
"HttpParamError",
|
|
105
|
+
"GuardError",
|
|
106
|
+
"AdapterError",
|
|
107
|
+
"ImportError",
|
|
108
|
+
"ErrorFormatter",
|
|
109
|
+
// guard
|
|
110
|
+
"Guard",
|
|
111
|
+
"UseGuards",
|
|
112
|
+
// http-exceptions
|
|
113
|
+
"HttpException",
|
|
114
|
+
"BadRequestException",
|
|
115
|
+
"UnauthorizedException",
|
|
116
|
+
"ForbiddenException",
|
|
117
|
+
"NotFoundException",
|
|
118
|
+
"ConflictException",
|
|
119
|
+
"UnprocessableEntityException",
|
|
120
|
+
"InternalServerErrorException",
|
|
121
|
+
"OkResponse",
|
|
122
|
+
"CreatedResponse",
|
|
123
|
+
"NoContentResponse",
|
|
124
|
+
// http-methods
|
|
125
|
+
"HttpMethod",
|
|
126
|
+
// http-params
|
|
127
|
+
"Body",
|
|
128
|
+
"Param",
|
|
129
|
+
"Query",
|
|
130
|
+
"Headers",
|
|
131
|
+
"Req",
|
|
132
|
+
"Res",
|
|
133
|
+
"FormData",
|
|
134
|
+
// injectable
|
|
135
|
+
"Injectable",
|
|
136
|
+
// jwt
|
|
137
|
+
"Jwt",
|
|
138
|
+
"JwtModule",
|
|
139
|
+
"JwtService",
|
|
140
|
+
"UseJwt",
|
|
141
|
+
// module
|
|
142
|
+
"Module",
|
|
143
|
+
// on-module
|
|
144
|
+
"OnModuleInit",
|
|
145
|
+
"OnModuleDestroy",
|
|
146
|
+
// openapi
|
|
147
|
+
"ApiTags",
|
|
148
|
+
"ApiOperation",
|
|
149
|
+
"ApiResponse",
|
|
150
|
+
"ApiBody",
|
|
151
|
+
// render
|
|
152
|
+
"Render",
|
|
153
|
+
// schedule
|
|
154
|
+
"Cron",
|
|
155
|
+
"Timeout",
|
|
156
|
+
// testing
|
|
157
|
+
"Test",
|
|
158
|
+
"TestingModuleBuilder",
|
|
159
|
+
"TestApp",
|
|
160
|
+
// logger
|
|
161
|
+
"Logger",
|
|
162
|
+
];
|