@nectar-js/nectar 0.1.0 → 0.3.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-Cv0mpiRy.js} +191 -131
- package/dist/cli-Cv0mpiRy.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/{index-DGMxBsub.d.ts → index-IqoTGJFM.d.ts} +54 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -3
- package/dist/index.js.map +1 -1
- package/dist/{plugins-CGvM19v9.js → plugins-C2uwN3-D.js} +106 -30
- package/dist/plugins-C2uwN3-D.js.map +1 -0
- package/dist/{registration-CaE0QBT6.js → registration-mUZJidnF.js} +66 -7
- package/dist/registration-mUZJidnF.js.map +1 -0
- package/dist/{runtime-CZJeZvSL.js → runtime-B7mCDO5Q.js} +97 -16
- package/dist/runtime-B7mCDO5Q.js.map +1 -0
- package/dist/start.js +1 -1
- package/dist/testing.d.ts +14 -6
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- 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,4 +1,4 @@
|
|
|
1
|
-
import { ButtonInteraction, ChannelSelectMenuInteraction, ChatInputCommandInteraction, Client, ClientEvents, ClientOptions, GatewayIntentBits, Interaction, MentionableSelectMenuInteraction, MessageContextMenuCommandInteraction, ModalSubmitInteraction, PermissionResolvable, RoleSelectMenuInteraction, StringSelectMenuInteraction, UserContextMenuCommandInteraction, UserSelectMenuInteraction } from "discord.js";
|
|
1
|
+
import { Attachment, ButtonInteraction, ChannelSelectMenuInteraction, ChatInputCommandInteraction, Client, ClientEvents, ClientOptions, CommandInteractionOption, GatewayIntentBits, Interaction, MentionableSelectMenuInteraction, MessageContextMenuCommandInteraction, ModalSubmitInteraction, PermissionResolvable, RoleSelectMenuInteraction, StringSelectMenuInteraction, User, UserContextMenuCommandInteraction, UserSelectMenuInteraction } from "discord.js";
|
|
2
2
|
import { ApplicationCommandType, ApplicationIntegrationType, ChannelType, InteractionContextType, LocalizationMap, RESTPostAPIApplicationCommandsJSONBody } from "discord-api-types/v10";
|
|
3
3
|
//#region src/version.d.ts
|
|
4
4
|
/** From package.json, which sits one level up from both `src/` and `dist/`. */
|
|
@@ -69,7 +69,15 @@ interface CommandMeta extends TopLevelMeta {
|
|
|
69
69
|
options?: CommandOption[];
|
|
70
70
|
nameLocalizations?: LocalizationMap;
|
|
71
71
|
descriptionLocalizations?: LocalizationMap;
|
|
72
|
+
/**
|
|
73
|
+
* Defer the reply before the handler runs, so a slow handler doesn't miss Discord's three
|
|
74
|
+
* second window. `"ephemeral"` defers with an ephemeral reply. The handler then answers with
|
|
75
|
+
* `editReply()`.
|
|
76
|
+
*/
|
|
77
|
+
defer?: boolean | "ephemeral";
|
|
72
78
|
}
|
|
79
|
+
/** How a command route defers, as the manifest records it. */
|
|
80
|
+
type DeferMode = "reply" | "ephemeral";
|
|
73
81
|
/** `export const meta` in a `route.ts` under `commands/`. Describes a parent command or subcommand group. */
|
|
74
82
|
interface CommandRouteMeta extends TopLevelMeta {
|
|
75
83
|
name?: string;
|
|
@@ -276,11 +284,18 @@ interface Trace {
|
|
|
276
284
|
elapsed(): number;
|
|
277
285
|
}
|
|
278
286
|
type Params = Record<string, string | string[]>;
|
|
279
|
-
|
|
287
|
+
/** Command option values by name. Untyped until the route is known. */
|
|
288
|
+
type Options = Record<string, unknown>;
|
|
289
|
+
interface InteractionContext<I = Interaction, P = Params, O = Options> {
|
|
280
290
|
interaction: I;
|
|
281
291
|
client: Client;
|
|
282
292
|
route: RouteInfo;
|
|
283
293
|
params: P;
|
|
294
|
+
/**
|
|
295
|
+
* The command's options by name, resolved through discord.js. Options the user left out
|
|
296
|
+
* are `null`. Empty for components and autocomplete.
|
|
297
|
+
*/
|
|
298
|
+
options: O;
|
|
284
299
|
env: Env;
|
|
285
300
|
trace: Trace;
|
|
286
301
|
/** What plugins provide. Empty without plugins. */
|
|
@@ -380,6 +395,8 @@ interface ManifestRouteBase {
|
|
|
380
395
|
}
|
|
381
396
|
interface ManifestCommandRoute extends ManifestRouteBase {
|
|
382
397
|
kind: "command";
|
|
398
|
+
/** The reply to defer before the handler runs, from `meta.defer`. */
|
|
399
|
+
defer: DeferMode | null;
|
|
383
400
|
}
|
|
384
401
|
interface ManifestAutocompleteRoute extends ManifestRouteBase {
|
|
385
402
|
kind: "autocomplete";
|
|
@@ -575,6 +592,11 @@ type Empty = Record<never, never>;
|
|
|
575
592
|
type Declared<K extends string> = NectarRoutes extends Record<K, infer V> ? V : never;
|
|
576
593
|
type Fallback<T, F> = [T] extends [never] ? F : T;
|
|
577
594
|
type ComponentKindName = "button" | "modal" | `select:${SelectKind}`;
|
|
595
|
+
/** One command option as the generated types describe it. */
|
|
596
|
+
interface OptionSpecType {
|
|
597
|
+
type: OptionType;
|
|
598
|
+
required: boolean;
|
|
599
|
+
}
|
|
578
600
|
/** Component routes by path. Until types are generated, any string is accepted. */
|
|
579
601
|
type ComponentRoutes = Fallback<Declared<"components">, Record<string, {
|
|
580
602
|
kind: ComponentKindName;
|
|
@@ -584,7 +606,7 @@ type ComponentRoutes = Fallback<Declared<"components">, Record<string, {
|
|
|
584
606
|
/** Command routes by path. Until types are generated, any string is accepted. */
|
|
585
607
|
type CommandRoutes = Fallback<Declared<"commands">, Record<string, {
|
|
586
608
|
type: CommandType;
|
|
587
|
-
options: Record<string,
|
|
609
|
+
options: Record<string, OptionSpecType>;
|
|
588
610
|
context: Empty;
|
|
589
611
|
}>>;
|
|
590
612
|
type ComponentPath = keyof ComponentRoutes & string;
|
|
@@ -593,9 +615,16 @@ type Route<Routes, P extends string> = P extends keyof Routes ? Routes[P] : neve
|
|
|
593
615
|
type ComponentInteraction<K> = K extends "button" ? ButtonInteraction : K extends "modal" ? ModalSubmitInteraction : K extends "select:string" ? StringSelectMenuInteraction : K extends "select:user" ? UserSelectMenuInteraction : K extends "select:role" ? RoleSelectMenuInteraction : K extends "select:channel" ? ChannelSelectMenuInteraction : K extends "select:mentionable" ? MentionableSelectMenuInteraction : never;
|
|
594
616
|
type CommandInteraction<T> = T extends "chatInput" ? ChatInputCommandInteraction : T extends "user" ? UserContextMenuCommandInteraction : T extends "message" ? MessageContextMenuCommandInteraction : never;
|
|
595
617
|
type Field<R, K extends string, F> = R extends Record<K, infer V> ? V : F;
|
|
618
|
+
/** What `ctx.options` holds for each option type, as discord.js resolves it. */
|
|
619
|
+
type OptionValue<T> = T extends "string" ? string : T extends "integer" | "number" ? number : T extends "boolean" ? boolean : T extends "user" ? User : T extends "channel" ? NonNullable<CommandInteractionOption["channel"]> : T extends "role" ? NonNullable<CommandInteractionOption["role"]> : T extends "mentionable" ? NonNullable<CommandInteractionOption["member" | "role" | "user"]> : T extends "attachment" ? Attachment : never;
|
|
620
|
+
/** `ctx.options` for a command route: required options as their value, the rest `| null`. */
|
|
621
|
+
type OptionValues<P extends CommandPath> = { [K in keyof Field<Route<CommandRoutes, P>, "options", Empty>]: Field<Route<CommandRoutes, P>, "options", Empty>[K] extends {
|
|
622
|
+
type: infer T;
|
|
623
|
+
required: infer R;
|
|
624
|
+
} ? R extends true ? OptionValue<T> : OptionValue<T> | null : never; };
|
|
596
625
|
type ComponentParams<P extends ComponentPath> = Field<Route<ComponentRoutes, P>, "params", Params>;
|
|
597
|
-
type ComponentContext<P extends ComponentPath> = InteractionContext<ComponentInteraction<Field<Route<ComponentRoutes, P>, "kind", ComponentKindName>>, ComponentParams<P
|
|
598
|
-
type CommandContext<P extends CommandPath> = InteractionContext<CommandInteraction<Field<Route<CommandRoutes, P>, "type", CommandType>>, Empty
|
|
626
|
+
type ComponentContext<P extends ComponentPath> = InteractionContext<ComponentInteraction<Field<Route<ComponentRoutes, P>, "kind", ComponentKindName>>, ComponentParams<P>, Empty> & Field<Route<ComponentRoutes, P>, "context", Empty>;
|
|
627
|
+
type CommandContext<P extends CommandPath> = InteractionContext<CommandInteraction<Field<Route<CommandRoutes, P>, "type", CommandType>>, Empty, OptionValues<P>> & Field<Route<CommandRoutes, P>, "context", Empty>;
|
|
599
628
|
/** Every field is optional when the route has no parameters, so `customId("confirm")` works. */
|
|
600
629
|
type ParamsArg<P extends ComponentPath> = Empty extends ComponentParams<P> ? [params?: ComponentParams<P>] : [params: ComponentParams<P>];
|
|
601
630
|
/**
|
|
@@ -661,6 +690,21 @@ interface RoleOptions extends PolicyOptions {
|
|
|
661
690
|
}
|
|
662
691
|
/** Passes only when the invoking member holds the listed role IDs. */
|
|
663
692
|
declare function requireRoles(roles: string | readonly string[], options?: RoleOptions): Middleware;
|
|
693
|
+
interface CooldownOptions {
|
|
694
|
+
/**
|
|
695
|
+
* Who shares a cooldown: each user (default), everyone in a guild, or everyone. In a DM,
|
|
696
|
+
* `"guild"` falls back to the user.
|
|
697
|
+
*/
|
|
698
|
+
scope?: "user" | "guild" | "global";
|
|
699
|
+
/** What the user sees while waiting, given the seconds left. */
|
|
700
|
+
message?: string | ((seconds: number) => string);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Passes once per `seconds` for each route and subject. The cooldown starts when the
|
|
704
|
+
* interaction passes, so a handler that throws still counts. Autocomplete is never held back.
|
|
705
|
+
* Cooldowns live in memory, per process, and reset on restart.
|
|
706
|
+
*/
|
|
707
|
+
declare function cooldown(seconds: number, options?: CooldownOptions): Middleware;
|
|
664
708
|
//#endregion
|
|
665
709
|
//#region src/registration/diff.d.ts
|
|
666
710
|
interface CommandDiff {
|
|
@@ -675,7 +719,9 @@ interface CommandDiff {
|
|
|
675
719
|
//#region src/registration/remote.d.ts
|
|
676
720
|
/** The two calls registration needs. discord.js's `REST` satisfies this. */
|
|
677
721
|
interface CommandRest {
|
|
678
|
-
get(route: `/${string}
|
|
722
|
+
get(route: `/${string}`, options?: {
|
|
723
|
+
query: URLSearchParams;
|
|
724
|
+
}): Promise<unknown>;
|
|
679
725
|
put(route: `/${string}`, options: {
|
|
680
726
|
body: unknown;
|
|
681
727
|
}): Promise<unknown>;
|
|
@@ -753,5 +799,5 @@ interface NectarRoutes {}
|
|
|
753
799
|
*/
|
|
754
800
|
interface NectarServices {}
|
|
755
801
|
//#endregion
|
|
756
|
-
export {
|
|
757
|
-
//# sourceMappingURL=index-
|
|
802
|
+
export { Env as $, OptionValues as A, CommandOption as At, defineConfig as B, version as Bt, ComponentContext as C, ParamValidator as Ct, ComponentPath as D, SelectKind as Dt, ComponentParams as E, MAX_CUSTOM_ID_LENGTH as Et, defineError as F, OptionChoice as Ft, PluginCommand as G, NectarPlugin as H, defineEvent as I, OptionType as It, PluginGraph as J, PluginCommandContext as K, defineMiddleware as L, SimpleOption as Lt, customId as M, CommandType as Mt, defineCommand as N, IntegerOption as Nt, ComponentRoutes as O, ChannelOption as Ot, defineComponent as P, NumberOption as Pt, ContextExtension as Q, ConfigError as R, StringOption as Rt, CommandRoutes as S, LoggerOptions as St, ComponentOptions as T, CustomIdTooLongError as Tt, PluginApp as U, validateConfig as V, PluginChange as W, EventMeta as X, definePlugin as Y, EventMode as Z, requirePermissions as _, SignalType as _t, SyncResult as a, Middleware as at, CommandContext as b, LogRecord as bt, RegistrationError as c, Options as ct, CommandDiff as d, Trace as dt, ErrorHandler as et, CooldownOptions as f, InteractionMeta as ft, guildOnly as g, SignalEmitter as gt, cooldown as h, SignalData as ht, SyncOptions as i, Logger as it, Routed as j, CommandRouteMeta as jt, OptionSpecType as k, CommandMeta as kt, RegistrationProblem as l, Params as lt, RoleOptions as m, Signal as mt, NectarServices as n, Extended as nt, UnsafeSyncError as o, MiddlewareExtension as ot, PolicyOptions as p, RejectReason as pt, PluginError as q, ScopeSync as r, InteractionContext as rt, syncCommands as s, Next as st, NectarRoutes as t, EventContext as tt, Scope as u, RouteInfo as ut, requireRoles as v, LogFields as vt, ComponentKindName as w, StandardSchemaLike as wt, CommandPath as x, LogSink as xt, requiredIntents as y, LogLevel as yt, NectarConfig as z, TopLevelMeta as zt };
|
|
803
|
+
//# sourceMappingURL=index-IqoTGJFM.d.ts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
export { type ChannelOption, type CommandContext, type CommandDiff, type CommandMeta, type CommandOption, type CommandPath, type CommandRouteMeta, type CommandRoutes, type CommandType, type ComponentContext, type ComponentKindName, type ComponentOptions, type ComponentParams, type ComponentPath, type ComponentRoutes, ConfigError, type ContextExtension, CustomIdTooLongError, type Env, type ErrorHandler, type EventContext, type EventMeta, type EventMode, type Extended, type IntegerOption, type InteractionContext, type InteractionMeta, type LogFields, type LogLevel, type LogRecord, type LogSink, type Logger, type LoggerOptions, MAX_CUSTOM_ID_LENGTH, type Middleware, type MiddlewareExtension, type NectarConfig, type NectarPlugin, NectarRoutes, NectarServices, type Next, type NumberOption, type OptionChoice, type OptionType, type ParamValidator, type Params, type PluginApp, type PluginChange, type PluginCommand, type PluginCommandContext, PluginError, type PluginGraph, type PolicyOptions, RegistrationError, type RegistrationProblem, type RejectReason, type RoleOptions, type RouteInfo, type Routed, type Scope, type ScopeSync, type SelectKind, type Signal, type SignalData, type SignalEmitter, type SignalType, type SimpleOption, type StandardSchemaLike, type StringOption, type SyncOptions, type SyncResult, type TopLevelMeta, type Trace, UnsafeSyncError, customId, defineCommand, defineComponent, defineConfig, defineError, defineEvent, defineMiddleware, definePlugin, guildOnly, requirePermissions, requireRoles, requiredIntents, syncCommands, validateConfig, version };
|
|
1
|
+
import { $ as Env, A as OptionValues, At as CommandOption, B as defineConfig, Bt as version, C as ComponentContext, Ct as ParamValidator, D as ComponentPath, Dt as SelectKind, E as ComponentParams, Et as MAX_CUSTOM_ID_LENGTH, F as defineError, Ft as OptionChoice, G as PluginCommand, H as NectarPlugin, I as defineEvent, It as OptionType, J as PluginGraph, K as PluginCommandContext, L as defineMiddleware, Lt as SimpleOption, M as customId, Mt as CommandType, N as defineCommand, Nt as IntegerOption, O as ComponentRoutes, Ot as ChannelOption, P as defineComponent, Pt as NumberOption, Q as ContextExtension, R as ConfigError, Rt as StringOption, S as CommandRoutes, St as LoggerOptions, T as ComponentOptions, Tt as CustomIdTooLongError, U as PluginApp, V as validateConfig, W as PluginChange, X as EventMeta, Y as definePlugin, Z as EventMode, _ as requirePermissions, _t as SignalType, a as SyncResult, at as Middleware, b as CommandContext, bt as LogRecord, c as RegistrationError, ct as Options, d as CommandDiff, dt as Trace, et as ErrorHandler, f as CooldownOptions, ft as InteractionMeta, g as guildOnly, gt as SignalEmitter, h as cooldown, ht as SignalData, i as SyncOptions, it as Logger, j as Routed, jt as CommandRouteMeta, k as OptionSpecType, kt as CommandMeta, l as RegistrationProblem, lt as Params, m as RoleOptions, mt as Signal, n as NectarServices, nt as Extended, o as UnsafeSyncError, ot as MiddlewareExtension, p as PolicyOptions, pt as RejectReason, q as PluginError, r as ScopeSync, rt as InteractionContext, s as syncCommands, st as Next, t as NectarRoutes, tt as EventContext, u as Scope, ut as RouteInfo, v as requireRoles, vt as LogFields, w as ComponentKindName, wt as StandardSchemaLike, x as CommandPath, xt as LogSink, y as requiredIntents, yt as LogLevel, z as NectarConfig, zt as TopLevelMeta } from "./index-IqoTGJFM.js";
|
|
2
|
+
export { type ChannelOption, type CommandContext, type CommandDiff, type CommandMeta, type CommandOption, type CommandPath, type CommandRouteMeta, type CommandRoutes, type CommandType, type ComponentContext, type ComponentKindName, type ComponentOptions, type ComponentParams, type ComponentPath, type ComponentRoutes, ConfigError, type ContextExtension, type CooldownOptions, CustomIdTooLongError, type Env, type ErrorHandler, type EventContext, type EventMeta, type EventMode, type Extended, type IntegerOption, type InteractionContext, type InteractionMeta, type LogFields, type LogLevel, type LogRecord, type LogSink, type Logger, type LoggerOptions, MAX_CUSTOM_ID_LENGTH, type Middleware, type MiddlewareExtension, type NectarConfig, type NectarPlugin, NectarRoutes, NectarServices, type Next, type NumberOption, type OptionChoice, type OptionSpecType, type OptionType, type OptionValues, type Options, type ParamValidator, type Params, type PluginApp, type PluginChange, type PluginCommand, type PluginCommandContext, PluginError, type PluginGraph, type PolicyOptions, RegistrationError, type RegistrationProblem, type RejectReason, type RoleOptions, type RouteInfo, type Routed, type Scope, type ScopeSync, type SelectKind, type Signal, type SignalData, type SignalEmitter, type SignalType, type SimpleOption, type StandardSchemaLike, type StringOption, type SyncOptions, type SyncResult, type TopLevelMeta, type Trace, UnsafeSyncError, cooldown, customId, defineCommand, defineComponent, defineConfig, defineError, defineEvent, defineMiddleware, definePlugin, guildOnly, requirePermissions, requireRoles, requiredIntents, syncCommands, validateConfig, version };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { l as defineConfig, n as syncCommands, o as requiredIntents, r as RegistrationError, s as ConfigError, t as UnsafeSyncError, u as validateConfig } from "./registration-
|
|
1
|
+
import { D as version, T as MAX_CUSTOM_ID_LENGTH, l as encodeComponentRoute, n as definePlugin, t as PluginError, w as CustomIdTooLongError } from "./plugins-C2uwN3-D.js";
|
|
2
|
+
import { l as defineConfig, n as syncCommands, o as requiredIntents, r as RegistrationError, s as ConfigError, t as UnsafeSyncError, u as validateConfig } from "./registration-mUZJidnF.js";
|
|
3
3
|
import { MessageFlags } from "discord-api-types/v10";
|
|
4
4
|
//#region src/define.ts
|
|
5
5
|
/**
|
|
@@ -72,6 +72,40 @@ function requireRoles(roles, options = {}) {
|
|
|
72
72
|
return next();
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Passes once per `seconds` for each route and subject. The cooldown starts when the
|
|
77
|
+
* interaction passes, so a handler that throws still counts. Autocomplete is never held back.
|
|
78
|
+
* Cooldowns live in memory, per process, and reset on restart.
|
|
79
|
+
*/
|
|
80
|
+
function cooldown(seconds, options = {}) {
|
|
81
|
+
if (!(seconds > 0)) throw new RangeError(`cooldown() needs a positive number of seconds.`);
|
|
82
|
+
const scope = options.scope ?? "user";
|
|
83
|
+
const message = options.message ?? ((left) => `Try again in ${left}s.`);
|
|
84
|
+
const until = /* @__PURE__ */ new Map();
|
|
85
|
+
return async (ctx, next) => {
|
|
86
|
+
const i = ctx.interaction;
|
|
87
|
+
if (typeof i.respond === "function") return next();
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const key = `${ctx.route.id}\n${subject(i, scope)}`;
|
|
90
|
+
const expires = until.get(key);
|
|
91
|
+
if (expires !== void 0 && expires > now) {
|
|
92
|
+
const left = Math.ceil((expires - now) / 1e3);
|
|
93
|
+
return deny(ctx, typeof message === "string" ? message : message(left));
|
|
94
|
+
}
|
|
95
|
+
if (until.size >= SWEEP_AT) {
|
|
96
|
+
for (const [k, t] of until) if (t <= now) until.delete(k);
|
|
97
|
+
}
|
|
98
|
+
until.set(key, now + seconds * 1e3);
|
|
99
|
+
return next();
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Expired entries are dropped once the map reaches this size, so it stays bounded. */
|
|
103
|
+
const SWEEP_AT = 1e3;
|
|
104
|
+
function subject(i, scope) {
|
|
105
|
+
if (scope === "global") return "";
|
|
106
|
+
if (scope === "guild" && typeof i.guildId === "string") return `g:${i.guildId}`;
|
|
107
|
+
return `u:${i.user?.id ?? ""}`;
|
|
108
|
+
}
|
|
75
109
|
function inGuild(ctx) {
|
|
76
110
|
const i = ctx.interaction;
|
|
77
111
|
return typeof i.inGuild === "function" ? i.inGuild() : typeof i.guildId === "string";
|
|
@@ -100,6 +134,6 @@ async function deny(ctx, message) {
|
|
|
100
134
|
});
|
|
101
135
|
}
|
|
102
136
|
//#endregion
|
|
103
|
-
export { ConfigError, CustomIdTooLongError, MAX_CUSTOM_ID_LENGTH, PluginError, RegistrationError, UnsafeSyncError, customId, defineCommand, defineComponent, defineConfig, defineError, defineEvent, defineMiddleware, definePlugin, guildOnly, requirePermissions, requireRoles, requiredIntents, syncCommands, validateConfig, version };
|
|
137
|
+
export { ConfigError, CustomIdTooLongError, MAX_CUSTOM_ID_LENGTH, PluginError, RegistrationError, UnsafeSyncError, cooldown, customId, defineCommand, defineComponent, defineConfig, defineError, defineEvent, defineMiddleware, definePlugin, guildOnly, requirePermissions, requireRoles, requiredIntents, syncCommands, validateConfig, version };
|
|
104
138
|
|
|
105
139
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/define.ts","../src/policy.ts"],"sourcesContent":["import type {\n ButtonInteraction,\n ChannelSelectMenuInteraction,\n ChatInputCommandInteraction,\n ClientEvents,\n MentionableSelectMenuInteraction,\n MessageContextMenuCommandInteraction,\n ModalSubmitInteraction,\n RoleSelectMenuInteraction,\n StringSelectMenuInteraction,\n UserContextMenuCommandInteraction,\n UserSelectMenuInteraction,\n} from \"discord.js\";\nimport type { CommandType, OptionType } from \"./commands/meta.js\";\nimport type { SelectKind } from \"./components/compile.js\";\nimport type { ParamValidator } from \"./components/params.js\";\nimport { encodeComponentRoute } from \"./components/registry.js\";\nimport type { NectarRoutes } from \"./index.js\";\nimport type {\n ContextExtension,\n ErrorHandler,\n EventContext,\n Extended,\n InteractionContext,\n Middleware,\n Next,\n Params,\n} from \"./runtime/types.js\";\n\ntype Empty = Record<never, never>;\n\n/** `NectarRoutes[K]` when the generated types declare it, otherwise `never`. */\ntype Declared<K extends string> = NectarRoutes extends Record<K, infer V> ? V : never;\ntype Fallback<T, F> = [T] extends [never] ? F : T;\n\nexport type ComponentKindName = \"button\" | \"modal\" | `select:${SelectKind}`;\n\nexport interface ComponentRouteType {\n kind: ComponentKindName;\n params: Params;\n context: object;\n}\n\nexport interface CommandRouteType {\n type: CommandType;\n options: Record<string, OptionType>;\n context: object;\n}\n\n/** Component routes by path. Until types are generated, any string is accepted. */\nexport type ComponentRoutes = Fallback<\n Declared<\"components\">,\n Record<string, { kind: ComponentKindName; params: Params; context: Empty }>\n>;\n\n/** Command routes by path. Until types are generated, any string is accepted. */\nexport type CommandRoutes = Fallback<\n Declared<\"commands\">,\n Record<string, { type: CommandType; options: Record<string, OptionType>; context: Empty }>\n>;\n\nexport type ComponentPath = keyof ComponentRoutes & string;\nexport type CommandPath = keyof CommandRoutes & string;\n\ntype Route<Routes, P extends string> = P extends keyof Routes ? Routes[P] : never;\n\ntype ComponentInteraction<K> = K extends \"button\"\n ? ButtonInteraction\n : K extends \"modal\"\n ? ModalSubmitInteraction\n : K extends \"select:string\"\n ? StringSelectMenuInteraction\n : K extends \"select:user\"\n ? UserSelectMenuInteraction\n : K extends \"select:role\"\n ? RoleSelectMenuInteraction\n : K extends \"select:channel\"\n ? ChannelSelectMenuInteraction\n : K extends \"select:mentionable\"\n ? MentionableSelectMenuInteraction\n : never;\n\ntype CommandInteraction<T> = T extends \"chatInput\"\n ? ChatInputCommandInteraction\n : T extends \"user\"\n ? UserContextMenuCommandInteraction\n : T extends \"message\"\n ? MessageContextMenuCommandInteraction\n : never;\n\ntype Field<R, K extends string, F> = R extends Record<K, infer V> ? V : F;\n\nexport type ComponentParams<P extends ComponentPath> = Field<\n Route<ComponentRoutes, P>,\n \"params\",\n Params\n>;\n\nexport type ComponentContext<P extends ComponentPath> = InteractionContext<\n ComponentInteraction<Field<Route<ComponentRoutes, P>, \"kind\", ComponentKindName>>,\n ComponentParams<P>\n> &\n Field<Route<ComponentRoutes, P>, \"context\", Empty>;\n\nexport type CommandContext<P extends CommandPath> = InteractionContext<\n CommandInteraction<Field<Route<CommandRoutes, P>, \"type\", CommandType>>,\n Empty\n> &\n Field<Route<CommandRoutes, P>, \"context\", Empty>;\n\n/** Every field is optional when the route has no parameters, so `customId(\"confirm\")` works. */\ntype ParamsArg<P extends ComponentPath> =\n Empty extends ComponentParams<P> ? [params?: ComponentParams<P>] : [params: ComponentParams<P>];\n\n/**\n * The custom ID for a component route, ready for a discord.js builder.\n *\n * Typed against the generated route map: the path must exist and the parameters must match\n * the dynamic segments. Throws when a value is missing or the ID would exceed Discord's limit.\n */\nexport function customId<P extends ComponentPath>(route: P, ...args: ParamsArg<P>): string {\n return encodeComponentRoute(route, (args[0] ?? {}) as Params);\n}\n\n/** A handler that remembers which route it was written for, so the compiler can check the file location. */\nexport type Routed<F> = F & { route: string };\n\nexport function defineCommand<P extends CommandPath>(\n route: P,\n handler: (ctx: CommandContext<P>) => unknown,\n): Routed<typeof handler> {\n return routed(\"defineCommand\", route, handler);\n}\n\nexport interface ComponentOptions<P extends ComponentPath> {\n /**\n * Validators for the route's parameters, run on every incoming custom ID before middleware.\n * A parameter without one accepts any string. When a validator fails the interaction is\n * dropped and reported; the handler never sees it.\n */\n params?: { [K in keyof ComponentParams<P>]?: ParamValidator<ComponentParams<P>[K]> };\n}\n\nexport function defineComponent<P extends ComponentPath>(\n route: P,\n handler: (ctx: ComponentContext<P>) => unknown,\n options: ComponentOptions<P> = {},\n): Routed<typeof handler> {\n const defined = routed(\"defineComponent\", route, handler);\n if (options.params !== undefined) Object.assign(defined, { params: options.params });\n return defined;\n}\n\nexport function defineEvent<Name extends keyof ClientEvents>(\n event: Name,\n handler: (...args: [...ClientEvents[Name], EventContext]) => unknown,\n): Routed<typeof handler> {\n return routed(\"defineEvent\", event, handler);\n}\n\n/**\n * `return next({ member })` types `member` onto every downstream context. Middleware that\n * calls `next()` without returning it adds nothing; pass the extension type explicitly if\n * you need both.\n */\nexport function defineMiddleware<E extends ContextExtension = Empty>(\n middleware: (\n ctx: InteractionContext,\n next: Next,\n // biome-ignore lint/suspicious/noConfusingVoidType: handlers that only call next() return void\n ) => Promise<Extended<E> | undefined | void> | Extended<E> | undefined | void,\n): Middleware<E> {\n if (typeof middleware !== \"function\") {\n throw new TypeError(`defineMiddleware() expects a function, got ${typeof middleware}.`);\n }\n return middleware as Middleware<E>;\n}\n\nexport function defineError(handler: ErrorHandler): ErrorHandler {\n if (typeof handler !== \"function\") {\n throw new TypeError(`defineError() expects a function, got ${typeof handler}.`);\n }\n return handler;\n}\n\nfunction routed<F>(name: string, route: string, handler: F): Routed<F> {\n if (typeof route !== \"string\" || route === \"\") {\n throw new TypeError(`${name}() expects the route path as its first argument.`);\n }\n if (typeof handler !== \"function\") {\n throw new TypeError(`${name}(\"${route}\") expects a handler function, got ${typeof handler}.`);\n }\n return Object.assign(handler, { route });\n}\n","import type { PermissionResolvable } from \"discord.js\";\nimport { MessageFlags } from \"discord-api-types/v10\";\nimport type { InteractionContext, Middleware } from \"./runtime/types.js\";\n\n/**\n * Opt-in policy middleware. Each returns a middleware that stops the chain and answers the\n * user with a short ephemeral message when the check fails. Registration-time permissions\n * (`meta.defaultMemberPermissions`) are a separate concept: Discord enforces those before the\n * interaction reaches the bot, and server admins can override them. These checks run in the\n * bot and cannot be overridden.\n *\n * Use them from a `middleware.ts`:\n *\n * export default requirePermissions(\"BanMembers\");\n *\n * Or compose them with your own middleware by calling them inside it.\n */\n\nexport interface PolicyOptions {\n /** What the user sees when the check fails. */\n message?: string;\n}\n\n/** Passes only interactions that come from a guild. */\nexport function guildOnly(options: PolicyOptions = {}): Middleware {\n const message = options.message ?? \"This only works in a server.\";\n return async (ctx, next) => {\n if (!inGuild(ctx)) return deny(ctx, message);\n return next();\n };\n}\n\n/** Passes only when the invoking member has every listed permission in the current channel. */\nexport function requirePermissions(\n permissions: PermissionResolvable,\n options: PolicyOptions = {},\n): Middleware {\n const message = options.message ?? \"You do not have permission to do that.\";\n return async (ctx, next) => {\n if (!inGuild(ctx)) return deny(ctx, message);\n const held = (ctx.interaction as GuildInteractionLike).memberPermissions;\n if (held === null || held === undefined || !held.has(permissions)) return deny(ctx, message);\n return next();\n };\n}\n\nexport interface RoleOptions extends PolicyOptions {\n /** `\"any\"` (default) passes with one matching role; `\"all\"` needs every listed role. */\n mode?: \"any\" | \"all\";\n}\n\n/** Passes only when the invoking member holds the listed role IDs. */\nexport function requireRoles(\n roles: string | readonly string[],\n options: RoleOptions = {},\n): Middleware {\n const wanted = typeof roles === \"string\" ? [roles] : [...roles];\n const message = options.message ?? \"You do not have the role for that.\";\n const mode = options.mode ?? \"any\";\n return async (ctx, next) => {\n if (!inGuild(ctx)) return deny(ctx, message);\n const held = memberRoles(ctx.interaction as GuildInteractionLike);\n const ok = mode === \"all\" ? wanted.every((r) => held.has(r)) : wanted.some((r) => held.has(r));\n if (!ok) return deny(ctx, message);\n return next();\n };\n}\n\nfunction inGuild(ctx: InteractionContext): boolean {\n const i = ctx.interaction as GuildInteractionLike;\n return typeof i.inGuild === \"function\" ? i.inGuild() : typeof i.guildId === \"string\";\n}\n\n/**\n * Role IDs of the invoking member. discord.js gives a `GuildMember` with a role cache when the\n * guild is cached and a raw API member with an ID array otherwise.\n */\nfunction memberRoles(interaction: GuildInteractionLike): Set<string> {\n const roles = interaction.member?.roles;\n if (roles === undefined || roles === null) return new Set();\n if (Array.isArray(roles)) return new Set(roles);\n return new Set((roles as { cache: Map<string, unknown> }).cache.keys());\n}\n\n/** Answers the user, when the interaction can still be answered, and ends the chain. */\nasync function deny(ctx: InteractionContext, message: string): Promise<void> {\n const i = ctx.interaction as RepliableLike;\n if (typeof i.respond === \"function\") {\n // Autocomplete cannot show a message. An empty list is the only quiet answer.\n if (!i.responded) await i.respond([]);\n return;\n }\n if (typeof i.reply !== \"function\" || i.replied || i.deferred) return;\n await i.reply({ content: message, flags: MessageFlags.Ephemeral });\n}\n\ninterface GuildInteractionLike {\n inGuild?: () => boolean;\n guildId?: string | null;\n memberPermissions?: { has(permission: PermissionResolvable): boolean } | null;\n member?: { roles: readonly string[] | { cache: Map<string, unknown> } } | null;\n}\n\ninterface RepliableLike {\n replied?: boolean;\n deferred?: boolean;\n responded?: boolean;\n reply?: (options: { content: string; flags: MessageFlags }) => Promise<unknown>;\n respond?: (choices: never[]) => Promise<unknown>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/define.ts","../src/policy.ts"],"sourcesContent":["import type {\n Attachment,\n ButtonInteraction,\n ChannelSelectMenuInteraction,\n ChatInputCommandInteraction,\n ClientEvents,\n CommandInteractionOption,\n MentionableSelectMenuInteraction,\n MessageContextMenuCommandInteraction,\n ModalSubmitInteraction,\n RoleSelectMenuInteraction,\n StringSelectMenuInteraction,\n User,\n UserContextMenuCommandInteraction,\n UserSelectMenuInteraction,\n} from \"discord.js\";\nimport type { CommandType, OptionType } from \"./commands/meta.js\";\nimport type { SelectKind } from \"./components/compile.js\";\nimport type { ParamValidator } from \"./components/params.js\";\nimport { encodeComponentRoute } from \"./components/registry.js\";\nimport type { NectarRoutes } from \"./index.js\";\nimport type {\n ContextExtension,\n ErrorHandler,\n EventContext,\n Extended,\n InteractionContext,\n Middleware,\n Next,\n Params,\n} from \"./runtime/types.js\";\n\ntype Empty = Record<never, never>;\n\n/** `NectarRoutes[K]` when the generated types declare it, otherwise `never`. */\ntype Declared<K extends string> = NectarRoutes extends Record<K, infer V> ? V : never;\ntype Fallback<T, F> = [T] extends [never] ? F : T;\n\nexport type ComponentKindName = \"button\" | \"modal\" | `select:${SelectKind}`;\n\nexport interface ComponentRouteType {\n kind: ComponentKindName;\n params: Params;\n context: object;\n}\n\n/** One command option as the generated types describe it. */\nexport interface OptionSpecType {\n type: OptionType;\n required: boolean;\n}\n\nexport interface CommandRouteType {\n type: CommandType;\n options: Record<string, OptionSpecType>;\n context: object;\n}\n\n/** Component routes by path. Until types are generated, any string is accepted. */\nexport type ComponentRoutes = Fallback<\n Declared<\"components\">,\n Record<string, { kind: ComponentKindName; params: Params; context: Empty }>\n>;\n\n/** Command routes by path. Until types are generated, any string is accepted. */\nexport type CommandRoutes = Fallback<\n Declared<\"commands\">,\n Record<string, { type: CommandType; options: Record<string, OptionSpecType>; context: Empty }>\n>;\n\nexport type ComponentPath = keyof ComponentRoutes & string;\nexport type CommandPath = keyof CommandRoutes & string;\n\ntype Route<Routes, P extends string> = P extends keyof Routes ? Routes[P] : never;\n\ntype ComponentInteraction<K> = K extends \"button\"\n ? ButtonInteraction\n : K extends \"modal\"\n ? ModalSubmitInteraction\n : K extends \"select:string\"\n ? StringSelectMenuInteraction\n : K extends \"select:user\"\n ? UserSelectMenuInteraction\n : K extends \"select:role\"\n ? RoleSelectMenuInteraction\n : K extends \"select:channel\"\n ? ChannelSelectMenuInteraction\n : K extends \"select:mentionable\"\n ? MentionableSelectMenuInteraction\n : never;\n\ntype CommandInteraction<T> = T extends \"chatInput\"\n ? ChatInputCommandInteraction\n : T extends \"user\"\n ? UserContextMenuCommandInteraction\n : T extends \"message\"\n ? MessageContextMenuCommandInteraction\n : never;\n\ntype Field<R, K extends string, F> = R extends Record<K, infer V> ? V : F;\n\n/** What `ctx.options` holds for each option type, as discord.js resolves it. */\ntype OptionValue<T> = T extends \"string\"\n ? string\n : T extends \"integer\" | \"number\"\n ? number\n : T extends \"boolean\"\n ? boolean\n : T extends \"user\"\n ? User\n : T extends \"channel\"\n ? NonNullable<CommandInteractionOption[\"channel\"]>\n : T extends \"role\"\n ? NonNullable<CommandInteractionOption[\"role\"]>\n : T extends \"mentionable\"\n ? NonNullable<CommandInteractionOption[\"member\" | \"role\" | \"user\"]>\n : T extends \"attachment\"\n ? Attachment\n : never;\n\n/** `ctx.options` for a command route: required options as their value, the rest `| null`. */\nexport type OptionValues<P extends CommandPath> = {\n [K in keyof Field<Route<CommandRoutes, P>, \"options\", Empty>]: Field<\n Route<CommandRoutes, P>,\n \"options\",\n Empty\n >[K] extends { type: infer T; required: infer R }\n ? R extends true\n ? OptionValue<T>\n : OptionValue<T> | null\n : never;\n};\n\nexport type ComponentParams<P extends ComponentPath> = Field<\n Route<ComponentRoutes, P>,\n \"params\",\n Params\n>;\n\nexport type ComponentContext<P extends ComponentPath> = InteractionContext<\n ComponentInteraction<Field<Route<ComponentRoutes, P>, \"kind\", ComponentKindName>>,\n ComponentParams<P>,\n Empty\n> &\n Field<Route<ComponentRoutes, P>, \"context\", Empty>;\n\nexport type CommandContext<P extends CommandPath> = InteractionContext<\n CommandInteraction<Field<Route<CommandRoutes, P>, \"type\", CommandType>>,\n Empty,\n OptionValues<P>\n> &\n Field<Route<CommandRoutes, P>, \"context\", Empty>;\n\n/** Every field is optional when the route has no parameters, so `customId(\"confirm\")` works. */\ntype ParamsArg<P extends ComponentPath> =\n Empty extends ComponentParams<P> ? [params?: ComponentParams<P>] : [params: ComponentParams<P>];\n\n/**\n * The custom ID for a component route, ready for a discord.js builder.\n *\n * Typed against the generated route map: the path must exist and the parameters must match\n * the dynamic segments. Throws when a value is missing or the ID would exceed Discord's limit.\n */\nexport function customId<P extends ComponentPath>(route: P, ...args: ParamsArg<P>): string {\n return encodeComponentRoute(route, (args[0] ?? {}) as Params);\n}\n\n/** A handler that remembers which route it was written for, so the compiler can check the file location. */\nexport type Routed<F> = F & { route: string };\n\nexport function defineCommand<P extends CommandPath>(\n route: P,\n handler: (ctx: CommandContext<P>) => unknown,\n): Routed<typeof handler> {\n return routed(\"defineCommand\", route, handler);\n}\n\nexport interface ComponentOptions<P extends ComponentPath> {\n /**\n * Validators for the route's parameters, run on every incoming custom ID before middleware.\n * A parameter without one accepts any string. When a validator fails the interaction is\n * dropped and reported; the handler never sees it.\n */\n params?: { [K in keyof ComponentParams<P>]?: ParamValidator<ComponentParams<P>[K]> };\n}\n\nexport function defineComponent<P extends ComponentPath>(\n route: P,\n handler: (ctx: ComponentContext<P>) => unknown,\n options: ComponentOptions<P> = {},\n): Routed<typeof handler> {\n const defined = routed(\"defineComponent\", route, handler);\n if (options.params !== undefined) Object.assign(defined, { params: options.params });\n return defined;\n}\n\nexport function defineEvent<Name extends keyof ClientEvents>(\n event: Name,\n handler: (...args: [...ClientEvents[Name], EventContext]) => unknown,\n): Routed<typeof handler> {\n return routed(\"defineEvent\", event, handler);\n}\n\n/**\n * `return next({ member })` types `member` onto every downstream context. Middleware that\n * calls `next()` without returning it adds nothing; pass the extension type explicitly if\n * you need both.\n */\nexport function defineMiddleware<E extends ContextExtension = Empty>(\n middleware: (\n ctx: InteractionContext,\n next: Next,\n // biome-ignore lint/suspicious/noConfusingVoidType: handlers that only call next() return void\n ) => Promise<Extended<E> | undefined | void> | Extended<E> | undefined | void,\n): Middleware<E> {\n if (typeof middleware !== \"function\") {\n throw new TypeError(`defineMiddleware() expects a function, got ${typeof middleware}.`);\n }\n return middleware as Middleware<E>;\n}\n\nexport function defineError(handler: ErrorHandler): ErrorHandler {\n if (typeof handler !== \"function\") {\n throw new TypeError(`defineError() expects a function, got ${typeof handler}.`);\n }\n return handler;\n}\n\nfunction routed<F>(name: string, route: string, handler: F): Routed<F> {\n if (typeof route !== \"string\" || route === \"\") {\n throw new TypeError(`${name}() expects the route path as its first argument.`);\n }\n if (typeof handler !== \"function\") {\n throw new TypeError(`${name}(\"${route}\") expects a handler function, got ${typeof handler}.`);\n }\n return Object.assign(handler, { route });\n}\n","import type { PermissionResolvable } from \"discord.js\";\nimport { MessageFlags } from \"discord-api-types/v10\";\nimport type { InteractionContext, Middleware } from \"./runtime/types.js\";\n\n/**\n * Opt-in policy middleware. Each returns a middleware that stops the chain and answers the\n * user with a short ephemeral message when the check fails. Registration-time permissions\n * (`meta.defaultMemberPermissions`) are a separate concept: Discord enforces those before the\n * interaction reaches the bot, and server admins can override them. These checks run in the\n * bot and cannot be overridden.\n *\n * Use them from a `middleware.ts`:\n *\n * export default requirePermissions(\"BanMembers\");\n *\n * Or compose them with your own middleware by calling them inside it.\n */\n\nexport interface PolicyOptions {\n /** What the user sees when the check fails. */\n message?: string;\n}\n\n/** Passes only interactions that come from a guild. */\nexport function guildOnly(options: PolicyOptions = {}): Middleware {\n const message = options.message ?? \"This only works in a server.\";\n return async (ctx, next) => {\n if (!inGuild(ctx)) return deny(ctx, message);\n return next();\n };\n}\n\n/** Passes only when the invoking member has every listed permission in the current channel. */\nexport function requirePermissions(\n permissions: PermissionResolvable,\n options: PolicyOptions = {},\n): Middleware {\n const message = options.message ?? \"You do not have permission to do that.\";\n return async (ctx, next) => {\n if (!inGuild(ctx)) return deny(ctx, message);\n const held = (ctx.interaction as GuildInteractionLike).memberPermissions;\n if (held === null || held === undefined || !held.has(permissions)) return deny(ctx, message);\n return next();\n };\n}\n\nexport interface RoleOptions extends PolicyOptions {\n /** `\"any\"` (default) passes with one matching role; `\"all\"` needs every listed role. */\n mode?: \"any\" | \"all\";\n}\n\n/** Passes only when the invoking member holds the listed role IDs. */\nexport function requireRoles(\n roles: string | readonly string[],\n options: RoleOptions = {},\n): Middleware {\n const wanted = typeof roles === \"string\" ? [roles] : [...roles];\n const message = options.message ?? \"You do not have the role for that.\";\n const mode = options.mode ?? \"any\";\n return async (ctx, next) => {\n if (!inGuild(ctx)) return deny(ctx, message);\n const held = memberRoles(ctx.interaction as GuildInteractionLike);\n const ok = mode === \"all\" ? wanted.every((r) => held.has(r)) : wanted.some((r) => held.has(r));\n if (!ok) return deny(ctx, message);\n return next();\n };\n}\n\nexport interface CooldownOptions {\n /**\n * Who shares a cooldown: each user (default), everyone in a guild, or everyone. In a DM,\n * `\"guild\"` falls back to the user.\n */\n scope?: \"user\" | \"guild\" | \"global\";\n /** What the user sees while waiting, given the seconds left. */\n message?: string | ((seconds: number) => string);\n}\n\n/**\n * Passes once per `seconds` for each route and subject. The cooldown starts when the\n * interaction passes, so a handler that throws still counts. Autocomplete is never held back.\n * Cooldowns live in memory, per process, and reset on restart.\n */\nexport function cooldown(seconds: number, options: CooldownOptions = {}): Middleware {\n if (!(seconds > 0)) throw new RangeError(`cooldown() needs a positive number of seconds.`);\n const scope = options.scope ?? \"user\";\n const message = options.message ?? ((left: number) => `Try again in ${left}s.`);\n const until = new Map<string, number>();\n return async (ctx, next) => {\n const i = ctx.interaction as SubjectLike;\n if (typeof i.respond === \"function\") return next();\n const now = Date.now();\n const key = `${ctx.route.id}\\n${subject(i, scope)}`;\n const expires = until.get(key);\n if (expires !== undefined && expires > now) {\n const left = Math.ceil((expires - now) / 1000);\n return deny(ctx, typeof message === \"string\" ? message : message(left));\n }\n if (until.size >= SWEEP_AT) {\n for (const [k, t] of until) if (t <= now) until.delete(k);\n }\n until.set(key, now + seconds * 1000);\n return next();\n };\n}\n\n/** Expired entries are dropped once the map reaches this size, so it stays bounded. */\nconst SWEEP_AT = 1000;\n\nfunction subject(i: SubjectLike, scope: NonNullable<CooldownOptions[\"scope\"]>): string {\n if (scope === \"global\") return \"\";\n if (scope === \"guild\" && typeof i.guildId === \"string\") return `g:${i.guildId}`;\n return `u:${i.user?.id ?? \"\"}`;\n}\n\ninterface SubjectLike {\n guildId?: string | null;\n user?: { id: string } | null;\n respond?: unknown;\n}\n\nfunction inGuild(ctx: InteractionContext): boolean {\n const i = ctx.interaction as GuildInteractionLike;\n return typeof i.inGuild === \"function\" ? i.inGuild() : typeof i.guildId === \"string\";\n}\n\n/**\n * Role IDs of the invoking member. discord.js gives a `GuildMember` with a role cache when the\n * guild is cached and a raw API member with an ID array otherwise.\n */\nfunction memberRoles(interaction: GuildInteractionLike): Set<string> {\n const roles = interaction.member?.roles;\n if (roles === undefined || roles === null) return new Set();\n if (Array.isArray(roles)) return new Set(roles);\n return new Set((roles as { cache: Map<string, unknown> }).cache.keys());\n}\n\n/** Answers the user, when the interaction can still be answered, and ends the chain. */\nasync function deny(ctx: InteractionContext, message: string): Promise<void> {\n const i = ctx.interaction as RepliableLike;\n if (typeof i.respond === \"function\") {\n // Autocomplete cannot show a message. An empty list is the only quiet answer.\n if (!i.responded) await i.respond([]);\n return;\n }\n if (typeof i.reply !== \"function\" || i.replied || i.deferred) return;\n await i.reply({ content: message, flags: MessageFlags.Ephemeral });\n}\n\ninterface GuildInteractionLike {\n inGuild?: () => boolean;\n guildId?: string | null;\n memberPermissions?: { has(permission: PermissionResolvable): boolean } | null;\n member?: { roles: readonly string[] | { cache: Map<string, unknown> } } | null;\n}\n\ninterface RepliableLike {\n replied?: boolean;\n deferred?: boolean;\n responded?: boolean;\n reply?: (options: { content: string; flags: MessageFlags }) => Promise<unknown>;\n respond?: (choices: never[]) => Promise<unknown>;\n}\n"],"mappings":"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"}
|
|
@@ -91,6 +91,69 @@ function escapeValue(value) {
|
|
|
91
91
|
}
|
|
92
92
|
//#endregion
|
|
93
93
|
//#region src/compiler/diagnostics.ts
|
|
94
|
+
/** Every code the compiler reports. Each has an entry in the diagnostics reference. */
|
|
95
|
+
const DIAGNOSTIC_CODES = [
|
|
96
|
+
"file-outside-category",
|
|
97
|
+
"unknown-category",
|
|
98
|
+
"file-in-wrong-category",
|
|
99
|
+
"invalid-segment",
|
|
100
|
+
"route-without-path",
|
|
101
|
+
"dynamic-segment-not-allowed",
|
|
102
|
+
"duplicate-param",
|
|
103
|
+
"catch-all-not-last",
|
|
104
|
+
"duplicate-route",
|
|
105
|
+
"module-load-failed",
|
|
106
|
+
"missing-handler",
|
|
107
|
+
"route-mismatch",
|
|
108
|
+
"missing-meta",
|
|
109
|
+
"invalid-meta",
|
|
110
|
+
"invalid-name",
|
|
111
|
+
"invalid-description",
|
|
112
|
+
"invalid-option",
|
|
113
|
+
"missing-route-meta",
|
|
114
|
+
"route-meta-without-path",
|
|
115
|
+
"unused-route-meta",
|
|
116
|
+
"mixed-command-and-subcommands",
|
|
117
|
+
"mixed-subcommand-and-group",
|
|
118
|
+
"command-too-deep",
|
|
119
|
+
"too-many-subcommands",
|
|
120
|
+
"context-menu-nested",
|
|
121
|
+
"top-level-field-on-group",
|
|
122
|
+
"top-level-field-on-subcommand",
|
|
123
|
+
"duplicate-command-name",
|
|
124
|
+
"too-many-commands",
|
|
125
|
+
"autocomplete-without-command",
|
|
126
|
+
"autocomplete-export-not-function",
|
|
127
|
+
"autocomplete-unknown-option",
|
|
128
|
+
"autocomplete-missing-handler",
|
|
129
|
+
"autocomplete-missing-file",
|
|
130
|
+
"missing-select-kind",
|
|
131
|
+
"invalid-select-kind",
|
|
132
|
+
"invalid-param-validator",
|
|
133
|
+
"catch-all-route",
|
|
134
|
+
"short-id-collision",
|
|
135
|
+
"duplicate-component-pattern",
|
|
136
|
+
"unknown-event",
|
|
137
|
+
"event-nested-path",
|
|
138
|
+
"event-mode-conflict",
|
|
139
|
+
"missing-intent",
|
|
140
|
+
"plugin-failed",
|
|
141
|
+
"plugin-invalid-change",
|
|
142
|
+
"plugin-unknown-route",
|
|
143
|
+
"plugin-missing-file"
|
|
144
|
+
];
|
|
145
|
+
const REFERENCE = "https://nectar-js.github.io/nectar/reference/diagnostics";
|
|
146
|
+
/** The reference entry for a compiler code. Codes from plugins have none. */
|
|
147
|
+
function docsUrl(code) {
|
|
148
|
+
return DIAGNOSTIC_CODES.includes(code) ? `${REFERENCE}#${code}` : void 0;
|
|
149
|
+
}
|
|
150
|
+
/** A value's type as a message puts it: `missing`, `a number`, `an array`. */
|
|
151
|
+
function typeOf(value) {
|
|
152
|
+
if (value === void 0) return "missing";
|
|
153
|
+
if (value === null) return "null";
|
|
154
|
+
if (Array.isArray(value)) return "an array";
|
|
155
|
+
return typeof value === "object" ? "an object" : `a ${typeof value}`;
|
|
156
|
+
}
|
|
94
157
|
var Diagnostics = class {
|
|
95
158
|
items = [];
|
|
96
159
|
error(code, message, location = {}) {
|
|
@@ -184,11 +247,11 @@ const PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
184
247
|
*/
|
|
185
248
|
function parseSegment(dirName) {
|
|
186
249
|
if (dirName.startsWith("[") || dirName.endsWith("]")) {
|
|
187
|
-
if (!dirName.startsWith("[") || !dirName.endsWith("]")) return fail(`"${dirName}" has an unmatched bracket.
|
|
250
|
+
if (!dirName.startsWith("[") || !dirName.endsWith("]")) return fail(`"${dirName}" has an unmatched bracket. Parameters look like [name].`);
|
|
188
251
|
const inner = dirName.slice(1, -1);
|
|
189
252
|
const isCatchAll = inner.startsWith("...");
|
|
190
253
|
const name = isCatchAll ? inner.slice(3) : inner;
|
|
191
|
-
if (!PARAM_NAME.test(name)) return fail(`"${dirName}"
|
|
254
|
+
if (!PARAM_NAME.test(name)) return fail(`"${dirName}" has an invalid parameter name. Parameters become keys of ctx.params, so use letters, digits, and underscores, and don't start with a digit.`);
|
|
192
255
|
return ok({
|
|
193
256
|
type: isCatchAll ? "catchAll" : "dynamic",
|
|
194
257
|
name
|
|
@@ -197,13 +260,13 @@ function parseSegment(dirName) {
|
|
|
197
260
|
if (dirName.startsWith("(") || dirName.endsWith(")")) {
|
|
198
261
|
if (!dirName.startsWith("(") || !dirName.endsWith(")")) return fail(`"${dirName}" has an unmatched parenthesis. Route groups look like (name).`);
|
|
199
262
|
const name = dirName.slice(1, -1);
|
|
200
|
-
if (!STATIC_NAME.test(name)) return fail(`"${dirName}"
|
|
263
|
+
if (!STATIC_NAME.test(name)) return fail(`"${dirName}" isn't a valid group name. Use letters, digits, hyphens, and underscores.`);
|
|
201
264
|
return ok({
|
|
202
265
|
type: "group",
|
|
203
266
|
name
|
|
204
267
|
});
|
|
205
268
|
}
|
|
206
|
-
if (!STATIC_NAME.test(dirName)) return fail(`"${dirName}"
|
|
269
|
+
if (!STATIC_NAME.test(dirName)) return fail(`"${dirName}" can't be part of a route path. Use letters, digits, hyphens, and underscores, and start with a letter or digit.`);
|
|
207
270
|
return ok({
|
|
208
271
|
type: "static",
|
|
209
272
|
name: dirName
|
|
@@ -240,11 +303,11 @@ function fail(reason) {
|
|
|
240
303
|
function paramValidatorsOf(handler, route) {
|
|
241
304
|
const declared = handler.params;
|
|
242
305
|
if (declared === void 0) return {};
|
|
243
|
-
if (typeof declared !== "object" || declared === null || Array.isArray(declared)) throw new Error(
|
|
306
|
+
if (typeof declared !== "object" || declared === null || Array.isArray(declared)) throw new Error(`params is ${typeOf(declared)}, not an object of validators.`);
|
|
244
307
|
const validators = {};
|
|
245
308
|
for (const [name, validator] of Object.entries(declared)) {
|
|
246
|
-
if (!route.params.includes(name)) throw new Error(
|
|
247
|
-
if (!isValidator(validator)) throw new Error(
|
|
309
|
+
if (!route.params.includes(name)) throw new Error(`params validates "${name}", which is not a parameter of this route. ${route.params.length === 0 ? "It has none." : `It has: ${route.params.join(", ")}.`}`);
|
|
310
|
+
if (!isValidator(validator)) throw new Error(`params.${name} is ${typeOf(validator)}. A validator is a function or a Standard Schema.`);
|
|
248
311
|
validators[name] = validator;
|
|
249
312
|
}
|
|
250
313
|
return validators;
|
|
@@ -337,7 +400,7 @@ async function compileRoute(route, diagnostics) {
|
|
|
337
400
|
const last = route.segments.at(-1);
|
|
338
401
|
const catchAll = last?.type === "catchAll" ? last.name : null;
|
|
339
402
|
const overhead = 8 + route.params.length;
|
|
340
|
-
if (catchAll !== null) diagnostics.warn("catch-all-route", `${formatSegment(last)}
|
|
403
|
+
if (catchAll !== null) diagnostics.warn("catch-all-route", `${formatSegment(last)} takes any number of values. They all count toward Discord's 100 character limit on custom IDs, and customId() throws when an ID goes over.`, {
|
|
341
404
|
file: route.file,
|
|
342
405
|
route: route.id
|
|
343
406
|
});
|
|
@@ -345,17 +408,17 @@ async function compileRoute(route, diagnostics) {
|
|
|
345
408
|
try {
|
|
346
409
|
module = await loadModule(route.file);
|
|
347
410
|
} catch (error) {
|
|
348
|
-
diagnostics.error("module-load-failed", `
|
|
411
|
+
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)}`, {
|
|
349
412
|
file: route.file,
|
|
350
413
|
route: route.id
|
|
351
414
|
});
|
|
352
415
|
return null;
|
|
353
416
|
}
|
|
354
|
-
if (!
|
|
417
|
+
if (!checkHandler(module, route, diagnostics)) return null;
|
|
355
418
|
try {
|
|
356
419
|
paramValidatorsOf(module.default, route);
|
|
357
420
|
} catch (error) {
|
|
358
|
-
diagnostics.error("invalid-param-validator", `${error instanceof Error ? error.message : String(error)}
|
|
421
|
+
diagnostics.error("invalid-param-validator", `${error instanceof Error ? error.message : String(error)} Validators go in defineComponent's third argument, like { params: { id: (value) => ... } }.`, {
|
|
359
422
|
file: route.file,
|
|
360
423
|
route: route.id
|
|
361
424
|
});
|
|
@@ -375,13 +438,23 @@ async function compileRoute(route, diagnostics) {
|
|
|
375
438
|
overhead
|
|
376
439
|
};
|
|
377
440
|
}
|
|
378
|
-
/**
|
|
379
|
-
|
|
441
|
+
/**
|
|
442
|
+
* The default export is the handler Nectar calls, and one made with `defineComponent(path, ...)`
|
|
443
|
+
* or the like must name the route its file sits in.
|
|
444
|
+
*/
|
|
445
|
+
function checkHandler(module, route, diagnostics, expected = route.path) {
|
|
380
446
|
const handler = module.default;
|
|
381
|
-
if (typeof handler !== "function")
|
|
447
|
+
if (typeof handler !== "function") {
|
|
448
|
+
const define = route.kind === "command" ? "defineCommand" : route.kind === "event" ? "defineEvent" : "defineComponent";
|
|
449
|
+
diagnostics.error("missing-handler", `This ${path.basename(route.file)} ${handler === void 0 ? "has no default export" : `exports ${typeOf(handler)} as its default`}. Nectar calls the default export when the route runs, so export the handler, like export default ${define}("${expected}", handler).`, {
|
|
450
|
+
file: route.file,
|
|
451
|
+
route: route.id
|
|
452
|
+
});
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
382
455
|
const declared = handler.route;
|
|
383
456
|
if (declared === void 0 || declared === expected) return true;
|
|
384
|
-
diagnostics.error("route-mismatch", `This file is
|
|
457
|
+
diagnostics.error("route-mismatch", `This file's route is "${expected}", but its handler says "${String(declared)}". The string types the handler, so it has to match where the file is. Change it to "${expected}", or move the file.`, {
|
|
385
458
|
file: route.file,
|
|
386
459
|
route: route.id
|
|
387
460
|
});
|
|
@@ -390,14 +463,14 @@ function checkDeclaredRoute(module, route, diagnostics, expected = route.path) {
|
|
|
390
463
|
function validateSelectKind(module, route, diagnostics) {
|
|
391
464
|
const kind = module.kind;
|
|
392
465
|
if (kind === void 0) {
|
|
393
|
-
diagnostics.error("missing-select-kind", "select.ts
|
|
466
|
+
diagnostics.error("missing-select-kind", "This select.ts doesn't export kind, which says what the select menu picks from: \"string\", \"user\", \"role\", \"channel\", or \"mentionable\". Add one, like export const kind = \"string\".", {
|
|
394
467
|
file: route.file,
|
|
395
468
|
route: route.id
|
|
396
469
|
});
|
|
397
470
|
return null;
|
|
398
471
|
}
|
|
399
472
|
if (typeof kind !== "string" || !SELECT_KINDS.has(kind)) {
|
|
400
|
-
diagnostics.error("invalid-select-kind",
|
|
473
|
+
diagnostics.error("invalid-select-kind", `kind is ${describe$1(kind)}. Use "string", "user", "role", "channel", or "mentionable".`, {
|
|
401
474
|
file: route.file,
|
|
402
475
|
route: route.id
|
|
403
476
|
});
|
|
@@ -413,7 +486,7 @@ function detectShortIdCollisions(routes, diagnostics) {
|
|
|
413
486
|
seen.set(route.shortId, route);
|
|
414
487
|
continue;
|
|
415
488
|
}
|
|
416
|
-
diagnostics.error("short-id-collision", `${route.id} and ${existing.id} hash to the same short ID "${route.shortId}", so their custom IDs
|
|
489
|
+
diagnostics.error("short-id-collision", `${route.id} and ${existing.id} hash to the same short ID, "${route.shortId}", so Nectar can't tell their custom IDs apart. Rename a directory in one of them.`, {
|
|
417
490
|
file: route.file,
|
|
418
491
|
route: route.id
|
|
419
492
|
});
|
|
@@ -421,7 +494,8 @@ function detectShortIdCollisions(routes, diagnostics) {
|
|
|
421
494
|
}
|
|
422
495
|
/**
|
|
423
496
|
* Two routes of the same kind whose paths differ only in parameter names, like
|
|
424
|
-
* `tickets/[id]/close` and `tickets/[ticketId]/close
|
|
497
|
+
* `tickets/[id]/close` and `tickets/[ticketId]/close`. Their hashes differ, but they take the
|
|
498
|
+
* same values in the same places, so they are one route split in two.
|
|
425
499
|
*/
|
|
426
500
|
function detectDuplicatePatterns(routes, diagnostics) {
|
|
427
501
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -434,7 +508,7 @@ function detectDuplicatePatterns(routes, diagnostics) {
|
|
|
434
508
|
continue;
|
|
435
509
|
}
|
|
436
510
|
if (existing.id === route.id) continue;
|
|
437
|
-
diagnostics.error("duplicate-component-pattern", `${route.id}
|
|
511
|
+
diagnostics.error("duplicate-component-pattern", `${route.id} is the same path as ${existing.id} in ${relative(existing.file)}, with a different parameter name. Parameter names don't make routes distinct. Merge the two and keep one name.`, {
|
|
438
512
|
file: route.file,
|
|
439
513
|
route: route.id
|
|
440
514
|
});
|
|
@@ -484,9 +558,10 @@ function toManifest(graph, outDir) {
|
|
|
484
558
|
};
|
|
485
559
|
};
|
|
486
560
|
const routes = [];
|
|
487
|
-
for (const command of graph.commands) for (const route of Object.
|
|
561
|
+
for (const command of graph.commands) for (const [key, route] of Object.entries(command.handlers)) routes.push({
|
|
488
562
|
...base(route),
|
|
489
|
-
kind: "command"
|
|
563
|
+
kind: "command",
|
|
564
|
+
defer: command.defer[key] ?? null
|
|
490
565
|
});
|
|
491
566
|
for (const entry of graph.autocomplete) routes.push({
|
|
492
567
|
...base(entry.route),
|
|
@@ -591,23 +666,24 @@ async function applyPlugins(graph, plugins) {
|
|
|
591
666
|
function apply(graph, plugin, change) {
|
|
592
667
|
const type = isRecord(change) ? change.type : void 0;
|
|
593
668
|
if (type !== "middleware" && type !== "diagnostic") {
|
|
594
|
-
graph.diagnostics.error("plugin-invalid-change", `Plugin "${plugin}" returned a change
|
|
669
|
+
graph.diagnostics.error("plugin-invalid-change", `Plugin "${plugin}" returned a change with type ${JSON.stringify(type)}. A change's type is "middleware" or "diagnostic".`);
|
|
595
670
|
return;
|
|
596
671
|
}
|
|
597
672
|
if (change.type === "diagnostic") {
|
|
598
673
|
const { severity, code, message, file, route } = change;
|
|
599
|
-
|
|
674
|
+
graph.diagnostics.items.push({
|
|
675
|
+
code,
|
|
676
|
+
severity: severity === "error" ? "error" : "warning",
|
|
677
|
+
message,
|
|
600
678
|
...file === void 0 ? {} : { file },
|
|
601
679
|
...route === void 0 ? {} : { route }
|
|
602
|
-
};
|
|
603
|
-
if (severity === "error") graph.diagnostics.error(code, message, where);
|
|
604
|
-
else graph.diagnostics.warn(code, message, where);
|
|
680
|
+
});
|
|
605
681
|
return;
|
|
606
682
|
}
|
|
607
683
|
const routes = graph.routes.filter((r) => r.id === change.route && (change.kind === void 0 || r.kind === change.kind));
|
|
608
684
|
const target = change.kind === void 0 ? change.route : `${change.route} (${change.kind})`;
|
|
609
685
|
if (routes.length === 0) {
|
|
610
|
-
graph.diagnostics.error("plugin-unknown-route", `Plugin "${plugin}" adds middleware to route "${target}", which does not exist.`);
|
|
686
|
+
graph.diagnostics.error("plugin-unknown-route", `Plugin "${plugin}" adds middleware to route "${target}", which does not exist. Route IDs look like "command:moderation/ban".`);
|
|
611
687
|
return;
|
|
612
688
|
}
|
|
613
689
|
if (routes.some((r) => r.category === "event")) {
|
|
@@ -658,6 +734,6 @@ var PluginError = class extends Error {
|
|
|
658
734
|
}
|
|
659
735
|
};
|
|
660
736
|
//#endregion
|
|
661
|
-
export {
|
|
737
|
+
export { typeOf as C, version as D, decodeCustomId as E, docsUrl as S, MAX_CUSTOM_ID_LENGTH as T, parseSegment as _, MANIFEST_FILE as a, loadModule as b, writeManifest as c, checkHandler as d, compileComponents as f, formatSegment as g, paramValidatorsOf as h, pluginGraph as i, encodeComponentRoute as l, findInvalidParam as m, definePlugin as n, stableStringify as o, customIdFor as p, applyPlugins as r, toManifest as s, PluginError as t, registerComponentRoutes as u, enableModuleReloading as v, CustomIdTooLongError as w, Diagnostics as x, invalidateModuleGraph as y };
|
|
662
738
|
|
|
663
|
-
//# sourceMappingURL=plugins-
|
|
739
|
+
//# sourceMappingURL=plugins-C2uwN3-D.js.map
|