@nectar-js/nectar 0.1.0 → 0.2.0
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/dist/{cli-Ce-ZUj6M.js → cli-DJg1-t94.js} +173 -104
- package/dist/cli-DJg1-t94.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/{index-DGMxBsub.d.ts → index-DNXcZ4Ff.d.ts} +4 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/{plugins-CGvM19v9.js → plugins-BCYwtuo-.js} +103 -28
- package/dist/plugins-BCYwtuo-.js.map +1 -0
- package/dist/{registration-CaE0QBT6.js → registration-BN3P3MDg.js} +66 -7
- package/dist/registration-BN3P3MDg.js.map +1 -0
- package/dist/{runtime-CZJeZvSL.js → runtime-B_bjrg1W.js} +19 -7
- package/dist/runtime-B_bjrg1W.js.map +1 -0
- package/dist/start.js +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -2
- package/package.json +1 -1
- package/dist/cli-Ce-ZUj6M.js.map +0 -1
- package/dist/plugins-CGvM19v9.js.map +0 -1
- package/dist/registration-CaE0QBT6.js.map +0 -1
- package/dist/runtime-CZJeZvSL.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as checkIntents, c as configFor, i as scopeKey, n as syncCommands, r as RegistrationError, s as ConfigError, t as UnsafeSyncError, u as validateConfig } from "./registration-
|
|
3
|
-
import { i as createLogger, l as createSignals, n as createRuntime, r as manifestFiles, s as HandlerLoadError, t as LoginError, u as loadManifest } from "./runtime-
|
|
1
|
+
import { C as typeOf, D as version, S as docsUrl, _ as parseSegment, a as MANIFEST_FILE, b as loadModule, c as writeManifest, d as checkHandler, f as compileComponents, g as formatSegment, i as pluginGraph, o as stableStringify, r as applyPlugins, s as toManifest, t as PluginError, v as enableModuleReloading, x as Diagnostics, y as invalidateModuleGraph } from "./plugins-BCYwtuo-.js";
|
|
2
|
+
import { a as checkIntents, c as configFor, i as scopeKey, n as syncCommands, r as RegistrationError, s as ConfigError, t as UnsafeSyncError, u as validateConfig } from "./registration-BN3P3MDg.js";
|
|
3
|
+
import { i as createLogger, l as createSignals, n as createRuntime, r as manifestFiles, s as HandlerLoadError, t as LoginError, u as loadManifest } from "./runtime-B_bjrg1W.js";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { existsSync, mkdirSync, readdirSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
@@ -162,7 +162,8 @@ async function compileAutocomplete(table, commands) {
|
|
|
162
162
|
const results = await Promise.all(routes.map(async (route) => {
|
|
163
163
|
const target = targets.get(route.id);
|
|
164
164
|
if (target === void 0) {
|
|
165
|
-
|
|
165
|
+
if (table.routes.some((r) => r.id === route.id && r.kind === "command")) return null;
|
|
166
|
+
diagnostics.error("autocomplete-without-command", "There's no command.ts next to this autocomplete.ts. Autocomplete answers the options of the command in the same directory, so move it next to that command.ts.", {
|
|
166
167
|
file: route.file,
|
|
167
168
|
route: route.id
|
|
168
169
|
});
|
|
@@ -172,7 +173,7 @@ async function compileAutocomplete(table, commands) {
|
|
|
172
173
|
try {
|
|
173
174
|
module = await loadModule(route.file);
|
|
174
175
|
} catch (error) {
|
|
175
|
-
diagnostics.error("module-load-failed", `
|
|
176
|
+
diagnostics.error("module-load-failed", `The compiler imports every route file to read its exports, and this one threw: ${error instanceof Error ? error.message : String(error)}`, {
|
|
176
177
|
file: route.file,
|
|
177
178
|
route: route.id
|
|
178
179
|
});
|
|
@@ -182,7 +183,7 @@ async function compileAutocomplete(table, commands) {
|
|
|
182
183
|
let ok = true;
|
|
183
184
|
for (const name of exported) {
|
|
184
185
|
if (typeof module[name] !== "function") {
|
|
185
|
-
diagnostics.error("autocomplete-export-not-function", `Export "${name}"
|
|
186
|
+
diagnostics.error("autocomplete-export-not-function", `Export "${name}" isn't a function. Each export of autocomplete.ts is a function that answers the option with the same name.`, {
|
|
186
187
|
file: route.file,
|
|
187
188
|
route: route.id
|
|
188
189
|
});
|
|
@@ -190,7 +191,7 @@ async function compileAutocomplete(table, commands) {
|
|
|
190
191
|
continue;
|
|
191
192
|
}
|
|
192
193
|
if (!target.options.has(name)) {
|
|
193
|
-
diagnostics.error("autocomplete-unknown-option", `Export "${name}"
|
|
194
|
+
diagnostics.error("autocomplete-unknown-option", `Export "${name}" doesn't match an option with autocomplete: true in ${relative$3(target.command.file)}. ${expected(target.options)} Rename the export, or set autocomplete: true on the option.`, {
|
|
194
195
|
file: route.file,
|
|
195
196
|
route: route.id
|
|
196
197
|
});
|
|
@@ -199,7 +200,7 @@ async function compileAutocomplete(table, commands) {
|
|
|
199
200
|
}
|
|
200
201
|
for (const name of [...target.options].sort()) {
|
|
201
202
|
if (exported.includes(name)) continue;
|
|
202
|
-
diagnostics.error("autocomplete-missing-handler", `Option "${name}" has
|
|
203
|
+
diagnostics.error("autocomplete-missing-handler", `Option "${name}" has autocomplete: true, but this file doesn't export a function named "${name}" to answer it.`, {
|
|
203
204
|
file: route.file,
|
|
204
205
|
route: route.id
|
|
205
206
|
});
|
|
@@ -213,7 +214,8 @@ async function compileAutocomplete(table, commands) {
|
|
|
213
214
|
}));
|
|
214
215
|
for (const [id, target] of targets) {
|
|
215
216
|
if (target.options.size === 0 || routes.some((r) => r.id === id)) continue;
|
|
216
|
-
|
|
217
|
+
const names = [...target.options].map((o) => `"${o}"`);
|
|
218
|
+
diagnostics.error("autocomplete-missing-file", names.length === 1 ? `Option ${names[0]} has autocomplete: true, but there's no autocomplete.ts next to this command. Add one that exports a function named ${names[0]}.` : `Options ${new Intl.ListFormat("en").format(names)} have autocomplete: true, but there's no autocomplete.ts next to this command. Add one that exports a function named after each of them.`, {
|
|
217
219
|
file: target.command.file,
|
|
218
220
|
route: id
|
|
219
221
|
});
|
|
@@ -230,9 +232,11 @@ function autocompleteOptions(command, key) {
|
|
|
230
232
|
return new Set((options ?? []).filter((o) => o.autocomplete === true).map((o) => o.name));
|
|
231
233
|
}
|
|
232
234
|
function expected(options) {
|
|
233
|
-
|
|
235
|
+
const names = [...options].sort().map((o) => `"${o}"`);
|
|
236
|
+
if (names.length === 0) return "That command has no autocomplete options.";
|
|
237
|
+
return names.length === 1 ? `Its autocomplete option is ${names[0]}.` : `Its autocomplete options are ${new Intl.ListFormat("en").format(names)}.`;
|
|
234
238
|
}
|
|
235
|
-
function relative$
|
|
239
|
+
function relative$3(file) {
|
|
236
240
|
return path.relative(process.cwd(), file).split(path.sep).join("/");
|
|
237
241
|
}
|
|
238
242
|
//#endregion
|
|
@@ -270,23 +274,23 @@ function validateCommandMeta(value, file, diagnostics) {
|
|
|
270
274
|
file
|
|
271
275
|
};
|
|
272
276
|
if (value === void 0) {
|
|
273
|
-
diagnostics.error("missing-meta", "command.ts
|
|
277
|
+
diagnostics.error("missing-meta", "This command.ts doesn't export meta. Discord needs a description for every slash command, so add export const meta = { description: \"...\" }. A context menu command sets type instead, like { type: \"user\" }.", { file });
|
|
274
278
|
return null;
|
|
275
279
|
}
|
|
276
280
|
if (!isRecord(value)) {
|
|
277
|
-
fail$1(ctx, "invalid-meta",
|
|
281
|
+
fail$1(ctx, "invalid-meta", `meta is ${typeOf(value)}. Export an object, like { description: "..." }.`);
|
|
278
282
|
return null;
|
|
279
283
|
}
|
|
280
284
|
let ok = true;
|
|
281
285
|
const type = value.type ?? "chatInput";
|
|
282
|
-
if (typeof type !== "string" || !COMMAND_TYPES.has(type)) ok = fail$1(ctx, "invalid-meta",
|
|
286
|
+
if (typeof type !== "string" || !COMMAND_TYPES.has(type)) ok = fail$1(ctx, "invalid-meta", `meta.type is ${JSON.stringify(type)}. Use "chatInput" for a slash command, or "user" or "message" for a context menu command.`);
|
|
283
287
|
if (value.name !== void 0) ok = checkName(ctx, value.name, "meta.name", type === "chatInput") && ok;
|
|
284
288
|
if (type === "chatInput") {
|
|
285
289
|
ok = checkDescription(ctx, value.description, "meta.description") && ok;
|
|
286
290
|
if (value.options !== void 0) ok = checkOptions(ctx, value.options) && ok;
|
|
287
291
|
} else {
|
|
288
|
-
if (value.description !== void 0 && value.description !== "") ok = fail$1(ctx, "invalid-meta", "
|
|
289
|
-
if (value.options !== void 0) ok = fail$1(ctx, "invalid-meta", "
|
|
292
|
+
if (value.description !== void 0 && value.description !== "") ok = fail$1(ctx, "invalid-meta", "Discord doesn't allow a description on context menu commands. Remove meta.description.");
|
|
293
|
+
if (value.options !== void 0) ok = fail$1(ctx, "invalid-meta", "Discord doesn't allow options on context menu commands. Remove meta.options.");
|
|
290
294
|
}
|
|
291
295
|
ok = checkLocalizations(ctx, value.nameLocalizations, "meta.nameLocalizations") && ok;
|
|
292
296
|
ok = checkLocalizations(ctx, value.descriptionLocalizations, "meta.descriptionLocalizations") && ok;
|
|
@@ -300,11 +304,11 @@ function validateCommandRouteMeta(value, file, diagnostics) {
|
|
|
300
304
|
file
|
|
301
305
|
};
|
|
302
306
|
if (value === void 0) {
|
|
303
|
-
diagnostics.error("missing-meta", "route.ts
|
|
307
|
+
diagnostics.error("missing-meta", "This route.ts doesn't export meta. It holds the description Discord shows for the command or subcommand group, so add export const meta = { description: \"...\" }.", { file });
|
|
304
308
|
return null;
|
|
305
309
|
}
|
|
306
310
|
if (!isRecord(value)) {
|
|
307
|
-
fail$1(ctx, "invalid-meta",
|
|
311
|
+
fail$1(ctx, "invalid-meta", `meta is ${typeOf(value)}. Export an object, like { description: "..." }.`);
|
|
308
312
|
return null;
|
|
309
313
|
}
|
|
310
314
|
let ok = checkDescription(ctx, value.description, "meta.description");
|
|
@@ -322,91 +326,105 @@ function fail$1(ctx, code, message) {
|
|
|
322
326
|
ctx.diagnostics.error(code, message, { file: ctx.file });
|
|
323
327
|
return false;
|
|
324
328
|
}
|
|
329
|
+
/** What a text field holds, for length errors: `empty`, `140 characters`, or its type. */
|
|
330
|
+
function sizeOf(value) {
|
|
331
|
+
if (typeof value !== "string") return typeOf(value);
|
|
332
|
+
return value === "" ? "empty" : `${value.length} characters`;
|
|
333
|
+
}
|
|
325
334
|
function checkName(ctx, name, label, chatInput) {
|
|
326
|
-
if (typeof name !== "string" || name.length === 0 || name.length > 32) return fail$1(ctx, "invalid-name", `${label}
|
|
335
|
+
if (typeof name !== "string" || name.length === 0 || name.length > 32) return fail$1(ctx, "invalid-name", `${label} is ${sizeOf(name)}. Discord needs a name of 1 to 32 characters.`);
|
|
327
336
|
if (chatInput) {
|
|
328
|
-
if (!COMMAND_NAME.test(name) || name !== name.toLowerCase()) return fail$1(ctx, "invalid-name", `${label} "${name}"
|
|
337
|
+
if (!COMMAND_NAME.test(name) || name !== name.toLowerCase()) return fail$1(ctx, "invalid-name", `${label} is "${name}". Discord only allows lowercase letters, digits, hyphens, and underscores in slash command and option names.`);
|
|
329
338
|
}
|
|
330
339
|
return true;
|
|
331
340
|
}
|
|
332
341
|
function checkDescription(ctx, description, label) {
|
|
333
|
-
if (typeof description !== "string" || description.length === 0 || description.length > 100) return fail$1(ctx, "invalid-description", `${label}
|
|
342
|
+
if (typeof description !== "string" || description.length === 0 || description.length > 100) return fail$1(ctx, "invalid-description", `${label} is ${sizeOf(description)}. Discord needs a description of 1 to 100 characters.`);
|
|
334
343
|
return true;
|
|
335
344
|
}
|
|
336
345
|
function checkLocalizations(ctx, value, label) {
|
|
337
346
|
if (value === void 0) return true;
|
|
338
|
-
if (!isRecord(value)) return fail$1(ctx, "invalid-meta", `${label}
|
|
339
|
-
for (const [locale, text] of Object.entries(value)) if (typeof text !== "string") return fail$1(ctx, "invalid-meta", `${label}.${locale}
|
|
347
|
+
if (!isRecord(value)) return fail$1(ctx, "invalid-meta", `${label} is ${typeOf(value)}. Use an object of locale to text, like { fr: "..." }.`);
|
|
348
|
+
for (const [locale, text] of Object.entries(value)) if (typeof text !== "string") return fail$1(ctx, "invalid-meta", `${label}.${locale} is ${typeOf(text)}, not a string.`);
|
|
340
349
|
return true;
|
|
341
350
|
}
|
|
342
351
|
function checkTopLevel(ctx, value) {
|
|
343
352
|
let ok = true;
|
|
344
353
|
const perms = value.defaultMemberPermissions;
|
|
345
|
-
if (perms !== void 0 && perms !== null && typeof perms !== "bigint" && typeof perms !== "string" && typeof perms !== "number") ok = fail$1(ctx, "invalid-meta",
|
|
346
|
-
if (value.nsfw !== void 0 && typeof value.nsfw !== "boolean") ok = fail$1(ctx, "invalid-meta",
|
|
347
|
-
ok = checkEnumArray(ctx, value.contexts, "meta.contexts", [
|
|
354
|
+
if (perms !== void 0 && perms !== null && typeof perms !== "bigint" && typeof perms !== "string" && typeof perms !== "number") ok = fail$1(ctx, "invalid-meta", `meta.defaultMemberPermissions is ${typeOf(perms)}. Use a permission bitfield, like PermissionFlagsBits.BanMembers from discord.js, or null.`);
|
|
355
|
+
if (value.nsfw !== void 0 && typeof value.nsfw !== "boolean") ok = fail$1(ctx, "invalid-meta", `meta.nsfw is ${typeOf(value.nsfw)}. Use true or false.`);
|
|
356
|
+
ok = checkEnumArray(ctx, value.contexts, "meta.contexts", "InteractionContextType", [
|
|
348
357
|
0,
|
|
349
358
|
1,
|
|
350
359
|
2
|
|
351
360
|
]) && ok;
|
|
352
|
-
ok = checkEnumArray(ctx, value.integrationTypes, "meta.integrationTypes", [0, 1]) && ok;
|
|
361
|
+
ok = checkEnumArray(ctx, value.integrationTypes, "meta.integrationTypes", "ApplicationIntegrationType", [0, 1]) && ok;
|
|
353
362
|
return ok;
|
|
354
363
|
}
|
|
355
|
-
function checkEnumArray(ctx, value, label, allowed) {
|
|
364
|
+
function checkEnumArray(ctx, value, label, enumName, allowed) {
|
|
356
365
|
if (value === void 0) return true;
|
|
357
|
-
if (!Array.isArray(value) || value.some((v) => !allowed.includes(v))) return fail$1(ctx, "invalid-meta", `${label}
|
|
366
|
+
if (!Array.isArray(value) || value.some((v) => !allowed.includes(v))) return fail$1(ctx, "invalid-meta", `${label} has to be an array of ${enumName} values from discord.js.`);
|
|
358
367
|
return true;
|
|
359
368
|
}
|
|
360
369
|
function checkOptions(ctx, options) {
|
|
361
|
-
if (!Array.isArray(options)) return fail$1(ctx, "invalid-option",
|
|
362
|
-
if (options.length > 25) return fail$1(ctx, "invalid-option",
|
|
370
|
+
if (!Array.isArray(options)) return fail$1(ctx, "invalid-option", `meta.options is ${typeOf(options)}, not an array.`);
|
|
371
|
+
if (options.length > 25) return fail$1(ctx, "invalid-option", `meta.options has ${options.length} options. Discord allows 25 per command.`);
|
|
363
372
|
let ok = true;
|
|
364
373
|
const names = /* @__PURE__ */ new Set();
|
|
365
374
|
let seenOptional = false;
|
|
366
375
|
for (const [index, option] of options.entries()) {
|
|
367
376
|
const label = `meta.options[${index}]`;
|
|
368
377
|
if (!isRecord(option)) {
|
|
369
|
-
ok = fail$1(ctx, "invalid-option", `${label}
|
|
378
|
+
ok = fail$1(ctx, "invalid-option", `${label} is ${typeOf(option)}, not an object.`);
|
|
370
379
|
continue;
|
|
371
380
|
}
|
|
372
381
|
const typedOk = checkOption(ctx, option, label);
|
|
373
382
|
ok = typedOk && ok;
|
|
374
383
|
if (!typedOk) continue;
|
|
375
384
|
const typed = option;
|
|
376
|
-
if (names.has(typed.name)) ok = fail$1(ctx, "invalid-option", `${label}
|
|
385
|
+
if (names.has(typed.name)) ok = fail$1(ctx, "invalid-option", `${label} reuses the name "${typed.name}". Discord needs option names to be unique within a command.`);
|
|
377
386
|
names.add(typed.name);
|
|
378
387
|
if (typed.required) {
|
|
379
|
-
if (seenOptional) ok = fail$1(ctx, "invalid-option", `${label}
|
|
388
|
+
if (seenOptional) ok = fail$1(ctx, "invalid-option", `${label}, "${typed.name}", is required but comes after an optional option. Discord needs required options first, so move it up.`);
|
|
380
389
|
} else seenOptional = true;
|
|
381
390
|
}
|
|
382
391
|
return ok;
|
|
383
392
|
}
|
|
393
|
+
/** The option types each type-specific field works on. */
|
|
394
|
+
const FIELD_TYPES = {
|
|
395
|
+
choices: [
|
|
396
|
+
"string",
|
|
397
|
+
"integer",
|
|
398
|
+
"number"
|
|
399
|
+
],
|
|
400
|
+
autocomplete: [
|
|
401
|
+
"string",
|
|
402
|
+
"integer",
|
|
403
|
+
"number"
|
|
404
|
+
],
|
|
405
|
+
minLength: ["string"],
|
|
406
|
+
maxLength: ["string"],
|
|
407
|
+
minValue: ["integer", "number"],
|
|
408
|
+
maxValue: ["integer", "number"],
|
|
409
|
+
channelTypes: ["channel"]
|
|
410
|
+
};
|
|
411
|
+
const quoted = (items, type) => new Intl.ListFormat("en", { type }).format([...items].map((item) => `"${item}"`));
|
|
384
412
|
function checkOption(ctx, option, label) {
|
|
385
413
|
let ok = true;
|
|
386
|
-
if (typeof option.type !== "string" || !OPTION_TYPES.has(option.type)) return fail$1(ctx, "invalid-option", `${label}.type
|
|
414
|
+
if (typeof option.type !== "string" || !OPTION_TYPES.has(option.type)) return fail$1(ctx, "invalid-option", `${label}.type is ${option.type === void 0 ? "missing" : JSON.stringify(option.type)}. Use ${quoted(OPTION_TYPES, "disjunction")}.`);
|
|
387
415
|
ok = checkName(ctx, option.name, `${label}.name`, true) && ok;
|
|
388
416
|
ok = checkDescription(ctx, option.description, `${label}.description`) && ok;
|
|
389
|
-
if (option.required !== void 0 && typeof option.required !== "boolean") ok = fail$1(ctx, "invalid-option", `${label}.required
|
|
417
|
+
if (option.required !== void 0 && typeof option.required !== "boolean") ok = fail$1(ctx, "invalid-option", `${label}.required is ${typeOf(option.required)}. Use true or false.`);
|
|
390
418
|
ok = checkLocalizations(ctx, option.nameLocalizations, `${label}.nameLocalizations`) && ok;
|
|
391
419
|
ok = checkLocalizations(ctx, option.descriptionLocalizations, `${label}.descriptionLocalizations`) && ok;
|
|
392
420
|
const type = option.type;
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
"choices",
|
|
397
|
-
"autocomplete",
|
|
398
|
-
"minLength",
|
|
399
|
-
"maxLength",
|
|
400
|
-
"minValue",
|
|
401
|
-
"maxValue",
|
|
402
|
-
"channelTypes"
|
|
403
|
-
]) {
|
|
404
|
-
if (option[key] === void 0) continue;
|
|
405
|
-
if (!((key === "choices" || key === "autocomplete") && choosable ? true : (key === "minLength" || key === "maxLength") && type === "string" ? true : (key === "minValue" || key === "maxValue") && numeric ? true : key === "channelTypes" && type === "channel")) ok = fail$1(ctx, "invalid-option", `${label}.${key} is not valid for a "${type}" option.`);
|
|
421
|
+
for (const [key, types] of Object.entries(FIELD_TYPES)) {
|
|
422
|
+
if (option[key] === void 0 || types.includes(type)) continue;
|
|
423
|
+
ok = fail$1(ctx, "invalid-option", `${label}.${key} only works on ${quoted(types, "conjunction")} options, and this one is "${type}".`);
|
|
406
424
|
}
|
|
407
|
-
if (option.autocomplete !== void 0 && typeof option.autocomplete !== "boolean") ok = fail$1(ctx, "invalid-option", `${label}.autocomplete
|
|
425
|
+
if (option.autocomplete !== void 0 && typeof option.autocomplete !== "boolean") ok = fail$1(ctx, "invalid-option", `${label}.autocomplete is ${typeOf(option.autocomplete)}. Use true or false.`);
|
|
408
426
|
if (option.choices !== void 0) {
|
|
409
|
-
if (option.autocomplete === true) ok = fail$1(ctx, "invalid-option", `${label}
|
|
427
|
+
if (option.autocomplete === true) ok = fail$1(ctx, "invalid-option", `${label} has both choices and autocomplete. Discord allows one or the other.`);
|
|
410
428
|
ok = checkChoices(ctx, option.choices, `${label}.choices`, type === "string") && ok;
|
|
411
429
|
}
|
|
412
430
|
for (const key of [
|
|
@@ -416,30 +434,30 @@ function checkOption(ctx, option, label) {
|
|
|
416
434
|
"maxValue"
|
|
417
435
|
]) {
|
|
418
436
|
const v = option[key];
|
|
419
|
-
if (v !== void 0 && typeof v !== "number") ok = fail$1(ctx, "invalid-option", `${label}.${key}
|
|
437
|
+
if (v !== void 0 && typeof v !== "number") ok = fail$1(ctx, "invalid-option", `${label}.${key} is ${typeOf(v)}, not a number.`);
|
|
420
438
|
}
|
|
421
|
-
if (typeof option.minLength === "number" && (option.minLength < 0 || option.minLength > 6e3)) ok = fail$1(ctx, "invalid-option", `${label}.minLength
|
|
422
|
-
if (typeof option.maxLength === "number" && (option.maxLength < 1 || option.maxLength > 6e3)) ok = fail$1(ctx, "invalid-option", `${label}.maxLength
|
|
439
|
+
if (typeof option.minLength === "number" && (option.minLength < 0 || option.minLength > 6e3)) ok = fail$1(ctx, "invalid-option", `${label}.minLength is ${option.minLength}. Discord allows 0 to 6000.`);
|
|
440
|
+
if (typeof option.maxLength === "number" && (option.maxLength < 1 || option.maxLength > 6e3)) ok = fail$1(ctx, "invalid-option", `${label}.maxLength is ${option.maxLength}. Discord allows 1 to 6000.`);
|
|
423
441
|
if (option.channelTypes !== void 0) {
|
|
424
|
-
if (!Array.isArray(option.channelTypes) || option.channelTypes.some((c) => typeof c !== "number")) ok = fail$1(ctx, "invalid-option", `${label}.channelTypes
|
|
442
|
+
if (!Array.isArray(option.channelTypes) || option.channelTypes.some((c) => typeof c !== "number")) ok = fail$1(ctx, "invalid-option", `${label}.channelTypes has to be an array of ChannelType values from discord.js.`);
|
|
425
443
|
}
|
|
426
444
|
return ok;
|
|
427
445
|
}
|
|
428
446
|
function checkChoices(ctx, choices, label, isString) {
|
|
429
|
-
if (!Array.isArray(choices)) return fail$1(ctx, "invalid-option", `${label}
|
|
430
|
-
if (choices.length > 25) return fail$1(ctx, "invalid-option", `${label}
|
|
447
|
+
if (!Array.isArray(choices)) return fail$1(ctx, "invalid-option", `${label} is ${typeOf(choices)}, not an array.`);
|
|
448
|
+
if (choices.length > 25) return fail$1(ctx, "invalid-option", `${label} has ${choices.length} entries. Discord allows 25 per option.`);
|
|
431
449
|
let ok = true;
|
|
432
450
|
for (const [index, choice] of choices.entries()) {
|
|
433
451
|
const at = `${label}[${index}]`;
|
|
434
452
|
if (!isRecord(choice)) {
|
|
435
|
-
ok = fail$1(ctx, "invalid-option", `${at}
|
|
453
|
+
ok = fail$1(ctx, "invalid-option", `${at} is ${typeOf(choice)}. Each choice is an object like { name: "Red", value: "red" }.`);
|
|
436
454
|
continue;
|
|
437
455
|
}
|
|
438
456
|
const typed = choice;
|
|
439
|
-
if (typeof typed.name !== "string" || typed.name.length === 0 || typed.name.length > 100) ok = fail$1(ctx, "invalid-option", `${at}.name
|
|
457
|
+
if (typeof typed.name !== "string" || typed.name.length === 0 || typed.name.length > 100) ok = fail$1(ctx, "invalid-option", `${at}.name is ${sizeOf(typed.name)}. Discord needs a name of 1 to 100 characters.`);
|
|
440
458
|
if (isString) {
|
|
441
|
-
if (typeof typed.value !== "string" || typed.value.length === 0 || typed.value.length > 100) ok = fail$1(ctx, "invalid-option", `${at}.value
|
|
442
|
-
} else if (typeof typed.value !== "number") ok = fail$1(ctx, "invalid-option", `${at}.value
|
|
459
|
+
if (typeof typed.value !== "string" || typed.value.length === 0 || typed.value.length > 100) ok = fail$1(ctx, "invalid-option", `${at}.value is ${sizeOf(typed.value)}. A string option's choices need a value of 1 to 100 characters.`);
|
|
460
|
+
} else if (typeof typed.value !== "number") ok = fail$1(ctx, "invalid-option", `${at}.value is ${typeOf(typed.value)}. A numeric option's choices need a number value.`);
|
|
443
461
|
ok = checkLocalizations(ctx, typed.nameLocalizations, `${at}.nameLocalizations`) && ok;
|
|
444
462
|
}
|
|
445
463
|
return ok;
|
|
@@ -462,6 +480,17 @@ const COMMAND_TYPE = {
|
|
|
462
480
|
user: ApplicationCommandType.User,
|
|
463
481
|
message: ApplicationCommandType.Message
|
|
464
482
|
};
|
|
483
|
+
const TYPE_LABEL = {
|
|
484
|
+
[ApplicationCommandType.ChatInput]: "slash command",
|
|
485
|
+
[ApplicationCommandType.User]: "user context menu command",
|
|
486
|
+
[ApplicationCommandType.Message]: "message context menu command"
|
|
487
|
+
};
|
|
488
|
+
/** How many commands of each type Discord takes, globally and in each server. */
|
|
489
|
+
const COMMAND_LIMITS = {
|
|
490
|
+
[ApplicationCommandType.ChatInput]: 100,
|
|
491
|
+
[ApplicationCommandType.User]: 15,
|
|
492
|
+
[ApplicationCommandType.Message]: 15
|
|
493
|
+
};
|
|
465
494
|
/** Compiles the command routes of a route table into Discord command definitions. */
|
|
466
495
|
async function compileCommands(table) {
|
|
467
496
|
const diagnostics = new Diagnostics();
|
|
@@ -476,20 +505,37 @@ async function compileCommands(table) {
|
|
|
476
505
|
const command = compileTopLevel(top, entries, routeMetas, diagnostics);
|
|
477
506
|
if (command !== null) commands.push(command);
|
|
478
507
|
}
|
|
479
|
-
for (const entry of routeMetas.values()) if (!entry.used) diagnostics.warn("unused-route-meta", "This route.ts
|
|
508
|
+
for (const entry of routeMetas.values()) if (!entry.used) diagnostics.warn("unused-route-meta", "This route.ts has no effect because there are no subcommands below it. A plain command's meta goes in its command.ts.", { file: entry.boundary.file });
|
|
480
509
|
detectDuplicateNames(commands, diagnostics);
|
|
510
|
+
checkLimits(commands, diagnostics);
|
|
481
511
|
commands.sort((a, b) => a.type - b.type || a.name.localeCompare(b.name));
|
|
482
512
|
return {
|
|
483
513
|
commands,
|
|
484
514
|
diagnostics
|
|
485
515
|
};
|
|
486
516
|
}
|
|
517
|
+
function checkLimits(commands, diagnostics) {
|
|
518
|
+
for (const [type, limit] of Object.entries(COMMAND_LIMITS)) {
|
|
519
|
+
const ofType = commands.filter((c) => c.type === Number(type));
|
|
520
|
+
const first = ofType[0];
|
|
521
|
+
if (first === void 0 || ofType.length <= limit) continue;
|
|
522
|
+
const slash = Number(type) === ApplicationCommandType.ChatInput;
|
|
523
|
+
diagnostics.error("too-many-commands", `The app has ${ofType.length} ${TYPE_LABEL[Number(type)]}s, and Discord allows ${limit}, globally and in each server. ${slash ? "Subcommands don't count toward it, so group related commands under one." : "Remove some."}`, { file: commandsDir(first) });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/** The `commands/` directory, found by walking up from one of the command's handlers. */
|
|
527
|
+
function commandsDir(command) {
|
|
528
|
+
const route = Object.values(command.handlers)[0];
|
|
529
|
+
let dir = path.dirname(route.file);
|
|
530
|
+
for (let i = 0; i < route.segments.length; i++) dir = path.dirname(dir);
|
|
531
|
+
return dir;
|
|
532
|
+
}
|
|
487
533
|
async function loadRoutes(routes, diagnostics) {
|
|
488
534
|
const commandRoutes = routes.filter((r) => r.kind === "command");
|
|
489
535
|
return (await Promise.all(commandRoutes.map(async (route) => {
|
|
490
536
|
const module = await importOrReport(route.file, diagnostics);
|
|
491
537
|
if (module === null) return null;
|
|
492
|
-
if (!
|
|
538
|
+
if (!checkHandler(module, route, diagnostics)) return null;
|
|
493
539
|
const meta = validateCommandMeta(module.meta, route.file, diagnostics);
|
|
494
540
|
if (meta === null) return null;
|
|
495
541
|
return {
|
|
@@ -505,7 +551,7 @@ async function loadRouteMetas(boundaries, diagnostics) {
|
|
|
505
551
|
await Promise.all(relevant.map(async (boundary) => {
|
|
506
552
|
const key = boundary.segments.filter((s) => s.type !== "group").map(formatSegment).join("/");
|
|
507
553
|
if (key === "") {
|
|
508
|
-
diagnostics.error("route-meta-without-path", "route.ts
|
|
554
|
+
diagnostics.error("route-meta-without-path", "This route.ts isn't inside a command's directory, so it doesn't describe a command. Move it into the command's directory, like commands/moderation/route.ts.", { file: boundary.file });
|
|
509
555
|
return;
|
|
510
556
|
}
|
|
511
557
|
const module = await importOrReport(boundary.file, diagnostics);
|
|
@@ -524,15 +570,16 @@ async function importOrReport(file, diagnostics) {
|
|
|
524
570
|
try {
|
|
525
571
|
return await loadModule(file);
|
|
526
572
|
} catch (error) {
|
|
527
|
-
diagnostics.error("module-load-failed", `
|
|
573
|
+
diagnostics.error("module-load-failed", `The compiler imports every route file to read its exports, and this one threw: ${error instanceof Error ? error.message : String(error)}`, { file });
|
|
528
574
|
return null;
|
|
529
575
|
}
|
|
530
576
|
}
|
|
531
577
|
function compileTopLevel(top, entries, routeMetas, diagnostics) {
|
|
532
578
|
const direct = entries.filter((e) => e.parts.length === 1);
|
|
533
579
|
const nested = entries.filter((e) => e.parts.length > 1);
|
|
580
|
+
if (direct.length > 1) return null;
|
|
534
581
|
if (direct.length > 0 && nested.length > 0) {
|
|
535
|
-
for (const entry of nested) diagnostics.error("mixed-command-and-subcommands", `"${top}" has
|
|
582
|
+
for (const entry of nested) diagnostics.error("mixed-command-and-subcommands", `"${top}" has a command.ts and also subcommands, like this one. Discord doesn't let a command with subcommands run by itself. Replace ${relative$2(direct[0]?.route.file)} with a route.ts, or move this file out of ${top}/.`, {
|
|
536
583
|
file: entry.route.file,
|
|
537
584
|
route: entry.route.id
|
|
538
585
|
});
|
|
@@ -540,7 +587,7 @@ function compileTopLevel(top, entries, routeMetas, diagnostics) {
|
|
|
540
587
|
}
|
|
541
588
|
const tooDeep = nested.filter((e) => e.parts.length > 3);
|
|
542
589
|
if (tooDeep.length > 0) {
|
|
543
|
-
for (const entry of tooDeep) diagnostics.error("command-too-deep", `
|
|
590
|
+
for (const entry of tooDeep) diagnostics.error("command-too-deep", `"${entry.route.path}" is ${entry.parts.length} levels deep. Discord commands go three levels at most: command, subcommand group, and subcommand. Remove a level of directories.`, {
|
|
544
591
|
file: entry.route.file,
|
|
545
592
|
route: entry.route.id
|
|
546
593
|
});
|
|
@@ -593,14 +640,14 @@ function compileParentCommand(top, nested, routeMetas, diagnostics) {
|
|
|
593
640
|
bySecond.set(second, [...bySecond.get(second) ?? [], entry]);
|
|
594
641
|
}
|
|
595
642
|
if (bySecond.size > 25) {
|
|
596
|
-
diagnostics.error("too-many-subcommands", `"${top}" has ${bySecond.size} subcommands and groups. Discord allows
|
|
643
|
+
diagnostics.error("too-many-subcommands", `"${top}" has ${bySecond.size} subcommands and groups. Discord allows 25 per command. Move some into a subcommand group or another command.`, { file: parent.boundary.file });
|
|
597
644
|
ok = false;
|
|
598
645
|
}
|
|
599
646
|
for (const [second, entries] of [...bySecond].sort(([a], [b]) => a.localeCompare(b))) {
|
|
600
647
|
const subs = entries.filter((e) => e.parts.length === 2);
|
|
601
648
|
const grouped = entries.filter((e) => e.parts.length === 3);
|
|
602
649
|
if (subs.length > 0 && grouped.length > 0) {
|
|
603
|
-
for (const entry of grouped) diagnostics.error("mixed-subcommand-and-group",
|
|
650
|
+
for (const entry of grouped) diagnostics.error("mixed-subcommand-and-group", `${relative$2(subs[0]?.route.file)} makes "${top} ${second}" a subcommand, and this file makes it a subcommand group. Discord doesn't allow both. Move that command.ts into its own directory under ${second}/, or move this file out.`, {
|
|
604
651
|
file: entry.route.file,
|
|
605
652
|
route: entry.route.id
|
|
606
653
|
});
|
|
@@ -632,12 +679,12 @@ function compileParentCommand(top, nested, routeMetas, diagnostics) {
|
|
|
632
679
|
}
|
|
633
680
|
const extra = topLevelKeysUsed(group.meta);
|
|
634
681
|
if (extra.length > 0) {
|
|
635
|
-
diagnostics.error("top-level-field-on-group", `${extra
|
|
682
|
+
diagnostics.error("top-level-field-on-group", `${fieldList(extra)} can't be set on a subcommand group. Discord applies ${extra.length === 1 ? "it" : "them"} to the whole command, so move ${extra.length === 1 ? "it" : "them"} to ${relative$2(parent.boundary.file)}.`, { file: group.boundary.file });
|
|
636
683
|
ok = false;
|
|
637
684
|
continue;
|
|
638
685
|
}
|
|
639
686
|
if (grouped.length > 25) {
|
|
640
|
-
diagnostics.error("too-many-subcommands", `
|
|
687
|
+
diagnostics.error("too-many-subcommands", `The "${groupKey}" group has ${grouped.length} subcommands. Discord allows 25 per group. Move some into another group.`, { file: group.boundary.file });
|
|
641
688
|
ok = false;
|
|
642
689
|
continue;
|
|
643
690
|
}
|
|
@@ -681,7 +728,7 @@ function compileParentCommand(top, nested, routeMetas, diagnostics) {
|
|
|
681
728
|
function compileSubcommand(entry, diagnostics) {
|
|
682
729
|
const { route, meta } = entry;
|
|
683
730
|
if (meta.type !== void 0 && meta.type !== "chatInput") {
|
|
684
|
-
diagnostics.error("context-menu-nested", `
|
|
731
|
+
diagnostics.error("context-menu-nested", `This is a context menu command, but it's nested under ${entry.parts[0]}/ as a subcommand. Discord doesn't allow context menu subcommands. Move it to its own directory directly under commands/, like commands/${entry.parts.at(-1)}/command.ts.`, {
|
|
685
732
|
file: route.file,
|
|
686
733
|
route: route.id
|
|
687
734
|
});
|
|
@@ -689,7 +736,7 @@ function compileSubcommand(entry, diagnostics) {
|
|
|
689
736
|
}
|
|
690
737
|
const extra = topLevelKeysUsed(meta);
|
|
691
738
|
if (extra.length > 0) {
|
|
692
|
-
diagnostics.error("top-level-field-on-subcommand", `${extra
|
|
739
|
+
diagnostics.error("top-level-field-on-subcommand", `${fieldList(extra)} can't be set on a subcommand. Discord applies ${extra.length === 1 ? "it" : "them"} to the whole command, so move ${extra.length === 1 ? "it" : "them"} to the route.ts in ${entry.parts[0]}/.`, {
|
|
693
740
|
file: route.file,
|
|
694
741
|
route: route.id
|
|
695
742
|
});
|
|
@@ -719,7 +766,7 @@ function requireRouteMeta(key, child, routeMetas, diagnostics) {
|
|
|
719
766
|
return entry;
|
|
720
767
|
}
|
|
721
768
|
const dir = directoryForPath(child.route, key.split("/").length);
|
|
722
|
-
diagnostics.error("missing-route-meta", `"${key}" has subcommands but no route.ts. Discord needs a description for it. Add ${path.join(dir, "route.ts")}
|
|
769
|
+
diagnostics.error("missing-route-meta", `"${key}" has subcommands but no route.ts. Discord needs a description for it, and without a command.ts that goes in route.ts. Add ${relative$2(path.join(dir, "route.ts"))} with export const meta = { description: "..." }.`, {
|
|
723
770
|
file: child.route.file,
|
|
724
771
|
route: child.route.id
|
|
725
772
|
});
|
|
@@ -746,11 +793,15 @@ function isValidChatInputName(name) {
|
|
|
746
793
|
return /^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u.test(name) && name === name.toLowerCase();
|
|
747
794
|
}
|
|
748
795
|
function reportDirectoryName(route, name, diagnostics, file) {
|
|
749
|
-
diagnostics.error("invalid-name", `"${name}"
|
|
796
|
+
diagnostics.error("invalid-name", `"${name}" isn't a valid slash command name. Discord only allows lowercase letters, digits, hyphens, and underscores, up to 32 characters. Rename the directory, or set meta.name.`, {
|
|
750
797
|
file: file ?? route.file,
|
|
751
798
|
route: route.id
|
|
752
799
|
});
|
|
753
800
|
}
|
|
801
|
+
/** `meta.nsfw and meta.contexts` */
|
|
802
|
+
function fieldList(keys) {
|
|
803
|
+
return new Intl.ListFormat("en").format(keys.map((key) => `meta.${key}`));
|
|
804
|
+
}
|
|
754
805
|
function detectDuplicateNames(commands, diagnostics) {
|
|
755
806
|
const seen = /* @__PURE__ */ new Map();
|
|
756
807
|
for (const command of commands) {
|
|
@@ -760,7 +811,7 @@ function detectDuplicateNames(commands, diagnostics) {
|
|
|
760
811
|
seen.set(key, command);
|
|
761
812
|
continue;
|
|
762
813
|
}
|
|
763
|
-
diagnostics.error("duplicate-command-name",
|
|
814
|
+
diagnostics.error("duplicate-command-name", `${relative$2(existing.files[0])} and ${relative$2(command.files[0])} both register a ${TYPE_LABEL[command.type]} named "${command.name}". Discord needs the names to be unique, so change meta.name or the directory of one of them.`, { file: command.files[0] });
|
|
764
815
|
}
|
|
765
816
|
}
|
|
766
817
|
function optionPayload(option) {
|
|
@@ -816,7 +867,7 @@ function compact(object) {
|
|
|
816
867
|
for (const [key, value] of Object.entries(object)) if (value !== void 0) out[key] = value;
|
|
817
868
|
return out;
|
|
818
869
|
}
|
|
819
|
-
function relative$
|
|
870
|
+
function relative$2(file) {
|
|
820
871
|
return file === void 0 ? "?" : path.relative(process.cwd(), file).split(path.sep).join("/");
|
|
821
872
|
}
|
|
822
873
|
//#endregion
|
|
@@ -840,7 +891,7 @@ async function compileEvents(table) {
|
|
|
840
891
|
if (modes.size > 1) {
|
|
841
892
|
for (const handler of handlers) {
|
|
842
893
|
if (handler.mode === void 0) continue;
|
|
843
|
-
diagnostics.error("event-mode-conflict", `Handlers of "${name}"
|
|
894
|
+
diagnostics.error("event-mode-conflict", `Handlers of "${name}" set meta.mode to both "concurrent" and "sequential". The mode applies to all of an event's handlers, so set it in one file, or make them match.`, {
|
|
844
895
|
file: handler.route.file,
|
|
845
896
|
route: handler.route.id
|
|
846
897
|
});
|
|
@@ -870,13 +921,13 @@ async function loadHandler(route, diagnostics) {
|
|
|
870
921
|
try {
|
|
871
922
|
module = await loadModule(route.file);
|
|
872
923
|
} catch (error) {
|
|
873
|
-
diagnostics.error("module-load-failed", `
|
|
924
|
+
diagnostics.error("module-load-failed", `The compiler imports every route file to read its exports, and this one threw: ${error instanceof Error ? error.message : String(error)}`, {
|
|
874
925
|
file: route.file,
|
|
875
926
|
route: route.id
|
|
876
927
|
});
|
|
877
928
|
return null;
|
|
878
929
|
}
|
|
879
|
-
if (!
|
|
930
|
+
if (!checkHandler(module, route, diagnostics, name)) return null;
|
|
880
931
|
const meta = validateEventMeta(module.meta, route, diagnostics);
|
|
881
932
|
if (meta === null) return null;
|
|
882
933
|
return {
|
|
@@ -892,7 +943,7 @@ function eventName(route, diagnostics) {
|
|
|
892
943
|
const first = statics[0];
|
|
893
944
|
if (first === void 0 || first.type !== "static") return null;
|
|
894
945
|
if (statics.length > 1) {
|
|
895
|
-
diagnostics.error("event-nested-path", `
|
|
946
|
+
diagnostics.error("event-nested-path", `"${route.path}" has ${statics.slice(1).map(formatSegment).join("/")}/ below the event name, and event handlers go directly in events/<eventName>/. To split an event across files, use route groups, like events/${first.name}/(${statics[1]?.name})/event.ts.`, {
|
|
896
947
|
file: route.file,
|
|
897
948
|
route: route.id
|
|
898
949
|
});
|
|
@@ -901,8 +952,8 @@ function eventName(route, diagnostics) {
|
|
|
901
952
|
const name = first.name;
|
|
902
953
|
if (EVENT_NAMES.has(name)) return name;
|
|
903
954
|
const renamed = RENAMED[name];
|
|
904
|
-
const hint = renamed !== void 0 ? `discord.js renamed it to "${renamed}".` : closest(name) ? `Did you mean "${closest(name)}"?` : "Event
|
|
905
|
-
diagnostics.error("unknown-event", `"${name}"
|
|
955
|
+
const hint = renamed !== void 0 ? `discord.js renamed it to "${renamed}".` : closest(name) ? `Did you mean "${closest(name)}"?` : "Event directories are named after a value of discord.js's Events enum, like messageCreate.";
|
|
956
|
+
diagnostics.error("unknown-event", `"${name}" isn't a discord.js event. ${hint} Rename the directory.`, {
|
|
906
957
|
file: route.file,
|
|
907
958
|
route: route.id
|
|
908
959
|
});
|
|
@@ -917,11 +968,11 @@ function validateEventMeta(value, route, diagnostics) {
|
|
|
917
968
|
});
|
|
918
969
|
return null;
|
|
919
970
|
};
|
|
920
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return fail("
|
|
971
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return fail("meta has to be an object, like { order: 1 }.");
|
|
921
972
|
const meta = value;
|
|
922
|
-
if (meta.once !== void 0 && typeof meta.once !== "boolean") return fail("
|
|
923
|
-
if (meta.order !== void 0 && (typeof meta.order !== "number" || !Number.isFinite(meta.order))) return fail(
|
|
924
|
-
if (meta.mode !== void 0 && meta.mode !== "sequential" && meta.mode !== "concurrent") return fail(
|
|
973
|
+
if (meta.once !== void 0 && typeof meta.once !== "boolean") return fail("meta.once has to be true or false.");
|
|
974
|
+
if (meta.order !== void 0 && (typeof meta.order !== "number" || !Number.isFinite(meta.order))) return fail(`meta.order is ${String(meta.order)}. Use a finite number.`);
|
|
975
|
+
if (meta.mode !== void 0 && meta.mode !== "sequential" && meta.mode !== "concurrent") return fail(`meta.mode is ${JSON.stringify(meta.mode)}. Use "sequential" or "concurrent".`);
|
|
925
976
|
return meta;
|
|
926
977
|
}
|
|
927
978
|
/** Case-insensitive match against known events, to catch `GuildMemberAdd` or `guildmemberadd`. */
|
|
@@ -1040,6 +1091,12 @@ const BOUNDARY_KINDS = /* @__PURE__ */ new Set([
|
|
|
1040
1091
|
"error",
|
|
1041
1092
|
"route"
|
|
1042
1093
|
]);
|
|
1094
|
+
/** A directory name for the examples in messages. */
|
|
1095
|
+
const EXAMPLE_DIR = {
|
|
1096
|
+
command: "ping",
|
|
1097
|
+
component: "confirm",
|
|
1098
|
+
event: "messageCreate"
|
|
1099
|
+
};
|
|
1043
1100
|
/** Discovers the app directory and builds the route table. */
|
|
1044
1101
|
function buildRouteTable(appDir) {
|
|
1045
1102
|
return buildRouteTableFromFiles(discover(appDir));
|
|
@@ -1057,12 +1114,13 @@ function buildRouteTableFromFiles(files) {
|
|
|
1057
1114
|
segments: [],
|
|
1058
1115
|
file: source.file
|
|
1059
1116
|
});
|
|
1060
|
-
else diagnostics.error("file-outside-category", `${path.basename(source.file)}
|
|
1117
|
+
else diagnostics.error("file-outside-category", `${path.basename(source.file)} is directly in the app directory, where only middleware and error files go. Move it under commands/, components/, or events/.`, { file: source.file });
|
|
1061
1118
|
continue;
|
|
1062
1119
|
}
|
|
1063
1120
|
const category = CATEGORY_DIRS[categoryDir];
|
|
1064
1121
|
if (category === void 0) {
|
|
1065
|
-
|
|
1122
|
+
const name = path.basename(source.file);
|
|
1123
|
+
diagnostics.error("unknown-category", `${name} is in ${categoryDir}/, which isn't a route directory. Nectar treats every file named ${name} as a route file, so move it under commands/, components/, or events/, or rename it.`, { file: source.file });
|
|
1066
1124
|
continue;
|
|
1067
1125
|
}
|
|
1068
1126
|
const segments = parseSegments(rest, source, diagnostics);
|
|
@@ -1077,7 +1135,8 @@ function buildRouteTableFromFiles(files) {
|
|
|
1077
1135
|
continue;
|
|
1078
1136
|
}
|
|
1079
1137
|
if (!HANDLER_KINDS[category].has(source.kind)) {
|
|
1080
|
-
|
|
1138
|
+
const home = Object.entries(CATEGORY_DIRS).find(([, c]) => HANDLER_KINDS[c].has(source.kind));
|
|
1139
|
+
diagnostics.error("file-in-wrong-category", `${path.basename(source.file)} belongs under ${home?.[0]}/, not ${categoryDir}/.`, { file: source.file });
|
|
1081
1140
|
continue;
|
|
1082
1141
|
}
|
|
1083
1142
|
const route = makeRoute(category, source.kind, segments, source.file, diagnostics);
|
|
@@ -1103,31 +1162,35 @@ function parseSegments(dirs, source, diagnostics) {
|
|
|
1103
1162
|
return segments;
|
|
1104
1163
|
}
|
|
1105
1164
|
function makeRoute(category, kind, segments, file, diagnostics) {
|
|
1165
|
+
const name = path.basename(file);
|
|
1166
|
+
const dir = `${category}s`;
|
|
1106
1167
|
if (segments.length === 0) {
|
|
1107
|
-
diagnostics.error("route-without-path", `${
|
|
1168
|
+
diagnostics.error("route-without-path", `${name} is directly in ${dir}/, so it has no route path. Put it in a named directory, like ${dir}/${EXAMPLE_DIR[category]}/${name}.`, { file });
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1171
|
+
if (segments.every((segment) => segment.type === "group")) {
|
|
1172
|
+
const groups = segments.map(formatSegment).join("/");
|
|
1173
|
+
const example = category === "event" ? `${dir}/${EXAMPLE_DIR[category]}/${groups}/${name}` : `${dir}/${groups}/${EXAMPLE_DIR[category]}/${name}`;
|
|
1174
|
+
diagnostics.error("route-without-path", `${name} is only inside route groups, and groups aren't part of the route path. Put it in a named directory, like ${example}.`, { file });
|
|
1108
1175
|
return null;
|
|
1109
1176
|
}
|
|
1110
1177
|
const params = [];
|
|
1111
1178
|
for (const [index, segment] of segments.entries()) if (segment.type === "dynamic" || segment.type === "catchAll") {
|
|
1112
1179
|
if (category !== "component") {
|
|
1113
|
-
diagnostics.error("dynamic-segment-not-allowed", `${formatSegment(segment)} is a
|
|
1180
|
+
diagnostics.error("dynamic-segment-not-allowed", `${formatSegment(segment)} is a parameter, and only component routes can have parameters. ${category === "command" ? "Discord registers commands under fixed names, so take input with meta.options instead." : "The directory has to be named after a discord.js event, like events/messageCreate/."}`, { file });
|
|
1114
1181
|
return null;
|
|
1115
1182
|
}
|
|
1116
1183
|
if (params.includes(segment.name)) {
|
|
1117
|
-
diagnostics.error("duplicate-param", `
|
|
1184
|
+
diagnostics.error("duplicate-param", `The parameter "${segment.name}" appears twice in this route. Each parameter becomes a key of ctx.params, so the names have to differ. Rename one of the directories.`, { file });
|
|
1118
1185
|
return null;
|
|
1119
1186
|
}
|
|
1120
1187
|
if (segment.type === "catchAll" && index !== segments.length - 1) {
|
|
1121
|
-
diagnostics.error("catch-all-not-last", `${formatSegment(segment)}
|
|
1188
|
+
diagnostics.error("catch-all-not-last", `${formatSegment(segment)} has more directories after it. A catch-all takes all the remaining values, so it has to be the last segment. Use [${segment.name}] if it only needs one value.`, { file });
|
|
1122
1189
|
return null;
|
|
1123
1190
|
}
|
|
1124
1191
|
params.push(segment.name);
|
|
1125
1192
|
}
|
|
1126
1193
|
const routePath = segments.filter((segment) => segment.type !== "group" || category === "event").map(formatSegment).join("/");
|
|
1127
|
-
if (routePath === "") {
|
|
1128
|
-
diagnostics.error("route-without-path", `${path.basename(file)} sits only inside route groups. Groups do not contribute to the route, so this route has no path.`, { file });
|
|
1129
|
-
return null;
|
|
1130
|
-
}
|
|
1131
1194
|
const id = `${category}:${routePath}`;
|
|
1132
1195
|
return {
|
|
1133
1196
|
id,
|
|
@@ -1153,12 +1216,16 @@ function detectDuplicates(routes, diagnostics) {
|
|
|
1153
1216
|
seen.set(key, route);
|
|
1154
1217
|
continue;
|
|
1155
1218
|
}
|
|
1156
|
-
|
|
1219
|
+
const other = relative$1(existing.file);
|
|
1220
|
+
diagnostics.error("duplicate-route", existing.kind !== route.kind ? `${other} handles the same route, ${route.id}. customId() and the generated types identify a component by its path alone, so each path takes one button.ts, select.ts, or modal.ts. Move one of them into its own directory.` : path.dirname(existing.file) === path.dirname(route.file) ? `${other} is in the same directory and handles the same route, ${route.id}. Keep one of them.` : `${other} is the same route, ${route.id}. Route groups aren't part of the path, so they don't tell the two apart. Rename one of the directories.`, {
|
|
1157
1221
|
file: route.file,
|
|
1158
1222
|
route: route.id
|
|
1159
1223
|
});
|
|
1160
1224
|
}
|
|
1161
1225
|
}
|
|
1226
|
+
function relative$1(file) {
|
|
1227
|
+
return path.relative(process.cwd(), file).split(path.sep).join("/");
|
|
1228
|
+
}
|
|
1162
1229
|
//#endregion
|
|
1163
1230
|
//#region src/compiler/graph.ts
|
|
1164
1231
|
/** Runs the whole compiler pipeline on an app directory. */
|
|
@@ -1272,15 +1339,17 @@ async function compileProject(project, io) {
|
|
|
1272
1339
|
return graph;
|
|
1273
1340
|
}
|
|
1274
1341
|
/**
|
|
1275
|
-
* One diagnostic as a headline
|
|
1342
|
+
* One diagnostic as a headline, an indented message, and the code's reference entry:
|
|
1276
1343
|
*
|
|
1277
|
-
* ✖ error invalid-name app/commands/
|
|
1278
|
-
*
|
|
1344
|
+
* ✖ error invalid-name app/commands/Ping/command.ts
|
|
1345
|
+
* "Ping" isn't a valid slash command name. ...
|
|
1346
|
+
* https://nectar-js.github.io/nectar/reference/diagnostics#invalid-name
|
|
1279
1347
|
*/
|
|
1280
1348
|
function formatDiagnostic(diagnostic, root) {
|
|
1281
1349
|
const mark = diagnostic.severity === "error" ? fail(c.red("error")) : warn(c.yellow("warning"));
|
|
1282
1350
|
const where = diagnostic.file === void 0 ? "" : ` ${relative(root, diagnostic.file)}`;
|
|
1283
|
-
|
|
1351
|
+
const docs = docsUrl(diagnostic.code);
|
|
1352
|
+
return [`${mark} ${c.dim(diagnostic.code)}${where}`, ...indent([diagnostic.message, ...docs === void 0 ? [] : [link(docs)]])].join("\n");
|
|
1284
1353
|
}
|
|
1285
1354
|
function relative(root, file) {
|
|
1286
1355
|
return path.relative(root, file).split(path.sep).join("/");
|
|
@@ -2222,4 +2291,4 @@ function commandHelp(command) {
|
|
|
2222
2291
|
//#endregion
|
|
2223
2292
|
export { main as t };
|
|
2224
2293
|
|
|
2225
|
-
//# sourceMappingURL=cli-
|
|
2294
|
+
//# sourceMappingURL=cli-DJg1-t94.js.map
|