@grupodiariodaregiao/bunstone 0.4.5 → 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 +476 -24
- package/dist/index.js +204 -1
- 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/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}`);
|
|
@@ -120335,6 +120537,7 @@ export {
|
|
|
120335
120537
|
Jwt,
|
|
120336
120538
|
InternalServerErrorException,
|
|
120337
120539
|
Injectable,
|
|
120540
|
+
ImportError,
|
|
120338
120541
|
HttpParamError,
|
|
120339
120542
|
HttpException,
|
|
120340
120543
|
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/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
|
+
];
|