meocord 4.2.1 → 4.2.3

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.
Files changed (137) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
  3. package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
  4. package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
  5. package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
  6. package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +4 -104
  8. package/dist/cjs/core/index.cjs +435 -182
  9. package/dist/cjs/decorator/index.cjs +116 -121
  10. package/dist/cjs/testing/index.cjs +1923 -808
  11. package/dist/cli.json +1 -1
  12. package/dist/esm/bin/app-template/package.json.template +9 -9
  13. package/dist/esm/bin/app-template/src/main.ts.template +3 -2
  14. package/dist/esm/bin/generator.js +6 -4
  15. package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
  16. package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
  17. package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
  18. package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
  19. package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
  20. package/dist/esm/bin/helper/service-generator.helper.js +6 -4
  21. package/dist/esm/bin/meocord.js +106 -48
  22. package/dist/esm/build/native-addons.js +75 -19
  23. package/dist/esm/build/rsbuild-config.js +11 -3
  24. package/dist/esm/build/typed-externals.js +15 -2
  25. package/dist/esm/common/cooldown-store.js +38 -14
  26. package/dist/esm/common/deprecation.js +4 -1
  27. package/dist/esm/common/execution-context.js +3 -1
  28. package/dist/esm/common/explained-error.js +7 -3
  29. package/dist/esm/common/log-level.js +13 -9
  30. package/dist/esm/common/logger.js +4 -2
  31. package/dist/esm/common/meocord-text.js +11 -6
  32. package/dist/esm/common/redis-cooldown-store.js +3 -7
  33. package/dist/esm/common/response/flags.js +17 -1
  34. package/dist/esm/common/response/install-context.js +27 -16
  35. package/dist/esm/common/response/presenter.js +7 -3
  36. package/dist/esm/common/response/response-state.js +32 -10
  37. package/dist/esm/common/sharded-cooldown-store.js +11 -9
  38. package/dist/esm/common/theme.js +3 -1
  39. package/dist/esm/common/translator.js +27 -8
  40. package/dist/esm/core/autocomplete-routes.js +26 -0
  41. package/dist/esm/core/command-conflicts.js +15 -7
  42. package/dist/esm/core/command-registration.js +15 -1
  43. package/dist/esm/core/cooldown-runner.js +7 -9
  44. package/dist/esm/core/defer.js +2 -5
  45. package/dist/esm/core/dispatcher.js +92 -66
  46. package/dist/esm/core/event-requirements.js +67 -19
  47. package/dist/esm/core/event-sources.js +100 -0
  48. package/dist/esm/core/fallback.js +3 -1
  49. package/dist/esm/core/filter-runner.js +6 -6
  50. package/dist/esm/core/guard-runner.js +173 -52
  51. package/dist/esm/core/handler-input.js +24 -3
  52. package/dist/esm/core/handler-pipeline.js +54 -32
  53. package/dist/esm/core/handler-registry.js +51 -21
  54. package/dist/esm/core/input-runner.js +7 -8
  55. package/dist/esm/core/interceptor-runner.js +4 -5
  56. package/dist/esm/core/lifecycle-hooks.js +3 -8
  57. package/dist/esm/core/lifecycle-order.js +1 -15
  58. package/dist/esm/core/meocord-classes.js +14 -1
  59. package/dist/esm/core/meocord-factory.js +83 -54
  60. package/dist/esm/core/meocord.app.js +174 -89
  61. package/dist/esm/core/message-help.js +37 -15
  62. package/dist/esm/core/message-params.js +126 -46
  63. package/dist/esm/core/message-routes.js +225 -45
  64. package/dist/esm/core/message-words.js +30 -2
  65. package/dist/esm/core/observer-runner.js +4 -2
  66. package/dist/esm/core/process-state.js +19 -0
  67. package/dist/esm/core/providers.js +39 -4
  68. package/dist/esm/core/scalar-types.js +57 -16
  69. package/dist/esm/core/shard-context.js +11 -10
  70. package/dist/esm/core/shard-exit.js +3 -3
  71. package/dist/esm/core/shard-manager.js +16 -6
  72. package/dist/esm/core/stage-scope.js +2 -1
  73. package/dist/esm/core/startup-roots.js +28 -8
  74. package/dist/esm/core/theme-resolvers.js +26 -11
  75. package/dist/esm/core/theme-runtime.js +23 -13
  76. package/dist/esm/core/theme-scope.js +31 -30
  77. package/dist/esm/core/theme-validation.js +1 -1
  78. package/dist/esm/decorator/app.decorator.js +37 -1
  79. package/dist/esm/decorator/command-builder.decorator.js +14 -0
  80. package/dist/esm/decorator/controller-class.decorator.js +1 -1
  81. package/dist/esm/decorator/controller.decorator.js +48 -19
  82. package/dist/esm/decorator/defer.decorator.js +2 -2
  83. package/dist/esm/decorator/event.decorator.js +4 -5
  84. package/dist/esm/decorator/filter.decorator.js +4 -4
  85. package/dist/esm/decorator/guard.decorator.js +4 -61
  86. package/dist/esm/decorator/inject.decorator.js +4 -1
  87. package/dist/esm/decorator/interceptor.decorator.js +2 -2
  88. package/dist/esm/decorator/theme.decorator.js +4 -3
  89. package/dist/esm/decorator/validation.decorator.js +5 -5
  90. package/dist/esm/package.json.js +1 -1
  91. package/dist/esm/testing/catalog.js +32 -25
  92. package/dist/esm/testing/cooldown-store-suite.js +3 -3
  93. package/dist/esm/testing/discord-defaults.js +449 -335
  94. package/dist/esm/testing/inspect-handler.js +91 -10
  95. package/dist/esm/testing/meocord-testing-module.js +217 -91
  96. package/dist/esm/testing/mock-fn.js +8 -2
  97. package/dist/esm/testing/mock-interaction.js +864 -124
  98. package/dist/esm/testing/response-log.js +8 -0
  99. package/dist/esm/testing/response.js +7 -5
  100. package/dist/esm/testing/routing.js +48 -6
  101. package/dist/esm/testing/strict-mocks.js +8 -4
  102. package/dist/esm/testing/theme.js +3 -2
  103. package/dist/esm/util/common.util.js +30 -9
  104. package/dist/esm/util/generator-cli.util.js +6 -4
  105. package/dist/esm/util/inherited-env.util.js +72 -13
  106. package/dist/esm/util/json.util.js +10 -3
  107. package/dist/esm/util/meocord-cli.util.js +3 -2
  108. package/dist/esm/util/meocord-config-loader.util.js +14 -1
  109. package/dist/esm/util/meocord-config-validation.util.js +13 -3
  110. package/dist/esm/util/meocord-source-config.util.js +13 -3
  111. package/dist/esm/util/platform.util.js +3 -2
  112. package/dist/esm/util/refusal.util.js +29 -11
  113. package/dist/esm/util/registration-mode.util.js +14 -1
  114. package/dist/esm/util/timer-limit.util.js +4 -1
  115. package/dist/esm/util/tsconfig-paths.util.js +38 -7
  116. package/dist/esm/util/tsconfig.util.js +16 -58
  117. package/dist/esm/util/user-text.util.js +8 -4
  118. package/dist/esm/util/value.util.js +33 -5
  119. package/dist/types/common/index.d.cts +17 -10
  120. package/dist/types/common/index.d.ts +17 -10
  121. package/dist/types/core/index.d.cts +21 -11
  122. package/dist/types/core/index.d.ts +21 -11
  123. package/dist/types/decorator/index.d.cts +63 -63
  124. package/dist/types/decorator/index.d.ts +63 -63
  125. package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
  126. package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
  127. package/dist/types/interface/index.d.cts +1 -1
  128. package/dist/types/interface/index.d.ts +1 -1
  129. package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
  130. package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
  131. package/dist/types/testing/index.d.cts +158 -61
  132. package/dist/types/testing/index.d.ts +158 -61
  133. package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
  134. package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
  135. package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
  136. package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
  137. package/package.json +5 -2
@@ -1,53 +1,11 @@
1
- import { GuildChannel, MessageReaction, MessageMentions, Message, GuildMember, BaseGuild, Guild, ModalSubmitInteraction, TextChannel, ThreadChannel, BaseGuildVoiceChannel, GuildPremiumTier, GuildVerificationLevel, GuildNSFWLevel, Locale, PresenceManager, ClientUser, UserFlagsBitField, User, GuildMemberFlagsBitField, Role, Collection, MessageType, ReactionManager, ChatInputCommandInteraction, CommandInteraction, AutocompleteInteraction, ReactionUserManager, BaseInteraction, ChannelType, NewsChannel, VoiceChannel, ForumChannel, MediaChannel, DMChannel, BaseChannel, Client, PermissionsBitField, PermissionFlagsBits } from 'discord.js';
2
- import { createMockChannel, createMockGuild, createMockMessage, ownedManager, memberRoles, createMockUser, createChatInputOptions, readPlaceAs, botMemberOf, heldChannelOf, isRawMember, isUnsetEveryone, NEW_SERVER_EVERYONE_PERMISSIONS } from './mock-interaction.js';
1
+ import { GuildChannel, MessageReaction, MessageMentions, Message, GuildMember, BaseGuild, Guild, ModalSubmitInteraction, TextChannel, ThreadChannel, BaseGuildVoiceChannel, GuildPremiumTier, GuildVerificationLevel, GuildNSFWLevel, Locale, PresenceManager, ClientUser, UserFlagsBitField, User, GuildMemberFlagsBitField, GuildEmoji, Sticker, GuildScheduledEvent, AutoModerationRule, Invite, Role, Collection, MessageType, ReactionManager, ChatInputCommandInteraction, CommandInteraction, AutocompleteInteraction, ReactionUserManager, MessageComponentInteraction, PermissionsBitField, BaseInteraction, ChannelType, NewsChannel, VoiceChannel, ForumChannel, MediaChannel, StageChannel, CategoryChannel, DirectoryChannel, DMChannel, PartialGroupDMChannel, BaseChannel, Client, GuildMemberRoleManager, ComponentType, ButtonStyle, PermissionFlagsBits } from 'discord.js';
2
+ import { createModalFields } from './modal-fields.js';
3
3
  import { nextSnowflake } from './snowflake.js';
4
4
  import { strictMocks } from './strict-mocks.js';
5
5
  import { Logger } from '../common/logger.js';
6
6
  import { warnOnce } from '../common/deprecation.js';
7
7
 
8
8
  const mockLogger = new Logger('Mocks');
9
- /**
10
- * A member's permissions where an interaction was made, as Discord computes them: under strict mocks, every permission
11
- * for an Administrator or the owner, else the channel's overwrites on top of `member.permissions`, so a test that sets
12
- * those keeps the base; otherwise `member.permissions`, with a warning once where strict mocks would read otherwise.
13
- */ function inChannel(interaction, member) {
14
- const base = member.permissions;
15
- // Only a server the mock caches has the overwrites to apply
16
- const { guild } = interaction;
17
- if (!(guild instanceof Guild)) return base;
18
- const computed = base.has(PermissionFlagsBits.Administrator) || member.id === guild.ownerId ? new PermissionsBitField(PermissionsBitField.All).freeze() : withOverwrites(base, overwritingChannel(interaction, guild), member);
19
- if (strictMocks()) return computed;
20
- if (!computed.equals(base)) {
21
- const lost = new PermissionsBitField(base).remove(computed).toArray();
22
- const gained = new PermissionsBitField(computed).remove(base).toArray();
23
- const change = lost.length > 0 ? [
24
- `without ${lost.join(', ')}`
25
- ] : [];
26
- if (gained.length > 0) change.push(`with ${gained.join(', ')}`);
27
- warnOnce(mockLogger, `${interaction.constructor.name}.memberPermissions reads the member's own permissions here, without the ` + "channel's overwrites or every permission an Administrator or the owner has; in the next major version (5.0) " + `it reads them as Discord computes them in the channel, ${change.join(' and ')}. Set memberPermissions on the ` + 'mock, or call useStrictMocks() to compute them now.');
28
- }
29
- // Unless the test set them, a member's permissions come from its roles, @everyone's included
30
- if (!Object.prototype.hasOwnProperty.call(member, 'permissions')) warnOnUnsetEveryone(interaction, guild, 'memberPermissions', base);
31
- return base;
32
- }
33
- /**
34
- * Warns once, as `key`, where `permissions` read a server's @everyone role default mode made with none, and lack what
35
- * strict mocks give @everyone in a new server.
36
- */ function warnOnUnsetEveryone(interaction, guild, key, permissions) {
37
- const everyone = guild.roles.everyone;
38
- if (!everyone || !isUnsetEveryone(everyone) || permissions.has(NEW_SERVER_EVERYONE_PERMISSIONS)) return;
39
- warnOnce(mockLogger, `${interaction.constructor.name}.${key} reads the server's @everyone role here with no permissions; in the next major ` + 'version (5.0) it has the permissions Discord gives @everyone in a new server, as strict mocks read it. Set ' + 'guild.roles.everyone.permissions, or call useStrictMocks() to read it so now.');
40
- }
41
- /**
42
- * The channel whose overwrites apply where an interaction was made, read without making or caching one: a thread's
43
- * parent, or undefined where the channel, or a thread's parent, is one the mock would make, which has none.
44
- */ function overwritingChannel(interaction, guild) {
45
- const channel = heldChannelOf(interaction, guild);
46
- // A thread's parent by the id the test gave it or one already read, so strict mocks' parent isn't made for this read
47
- const parentId = (thread)=>Object.prototype.hasOwnProperty.call(thread, 'parentId') ? thread.parentId : madeParentIds.get(thread);
48
- const at = channel instanceof ThreadChannel ? guild.channels.cache.get(parentId(channel) ?? '') : channel;
49
- return at instanceof GuildChannel ? at : undefined;
50
- }
51
9
  // The parent id strict mocks made for each thread made without one, once read
52
10
  const madeParentIds = new WeakMap();
53
11
  // Private in discord.js's typings, though they are what its own permissionsFor computes with; called on the prototype
@@ -65,285 +23,7 @@ const channelPermissions = GuildChannel.prototype;
65
23
  Class.prototype,
66
24
  defaults
67
25
  ];
68
- /**
69
- * What a mock reads for a data property discord.js always sets, where the test gave none: a value in Discord's shape,
70
- * such as `'en-US'` for a locale, `null` for what may be absent and `false` for a flag, or the property's own getter
71
- * for one discord.js computes. The classes a mock descends from are read nearest first.
72
- */ const DEFAULTS = new Map([
73
- defaultsOf(Guild, {
74
- name: ()=>'Guild',
75
- preferredLocale: ()=>Locale.EnglishUS,
76
- ownerId: nextSnowflake,
77
- memberCount: ()=>1,
78
- available: ()=>true,
79
- large: ()=>false,
80
- features: ()=>[],
81
- description: ()=>null,
82
- icon: ()=>null,
83
- banner: ()=>null,
84
- splash: ()=>null,
85
- systemChannelId: ()=>null,
86
- shardId: ()=>0,
87
- nsfwLevel: ()=>GuildNSFWLevel.Default,
88
- verificationLevel: ()=>GuildVerificationLevel.None,
89
- presences: (guild)=>ownedManager(PresenceManager.prototype, {
90
- guild
91
- }),
92
- premiumTier: ()=>GuildPremiumTier.None,
93
- createdTimestamp: REAL_GETTER,
94
- createdAt: REAL_GETTER
95
- }),
96
- defaultsOf(ClientUser, {
97
- username: ()=>'bot'
98
- }),
99
- defaultsOf(User, {
100
- username: ()=>'user',
101
- bot: ()=>false,
102
- globalName: ()=>null,
103
- discriminator: ()=>'0',
104
- avatar: ()=>null,
105
- system: ()=>false,
106
- flags: ()=>new UserFlagsBitField(),
107
- tag: REAL_GETTER,
108
- displayName: REAL_GETTER,
109
- defaultAvatarURL: REAL_GETTER,
110
- createdTimestamp: REAL_GETTER,
111
- createdAt: REAL_GETTER
112
- }),
113
- defaultsOf(GuildMember, {
114
- user: ()=>createMockUser(),
115
- id: REAL_GETTER,
116
- nickname: ()=>null,
117
- avatar: ()=>null,
118
- // In the server since the mock was made, as a member Discord sends always is
119
- joinedTimestamp: ()=>Date.now(),
120
- joinedAt: REAL_GETTER,
121
- partial: REAL_GETTER,
122
- premiumSince: REAL_GETTER,
123
- communicationDisabledUntil: REAL_GETTER,
124
- premiumSinceTimestamp: ()=>null,
125
- communicationDisabledUntilTimestamp: ()=>null,
126
- pending: ()=>false,
127
- flags: ()=>new GuildMemberFlagsBitField().freeze(),
128
- displayName: REAL_GETTER,
129
- roles: (member)=>memberRoles(member, []),
130
- permissions: REAL_GETTER
131
- }),
132
- defaultsOf(Role, {
133
- name: ()=>'role',
134
- // Above @everyone, which a server's role manager makes at position 0
135
- position: ()=>1,
136
- // Every role has a server; one made without is in one of its own, unless a guild given it takes it in
137
- guild: ()=>createMockGuild(),
138
- rawPosition: (role)=>role.position,
139
- permissions: noPermissions,
140
- hoist: ()=>false,
141
- managed: ()=>false,
142
- mentionable: ()=>false,
143
- icon: ()=>null,
144
- unicodeEmoji: ()=>null,
145
- tags: ()=>null,
146
- colors: ()=>({
147
- primaryColor: 0,
148
- secondaryColor: null,
149
- tertiaryColor: null
150
- }),
151
- hexColor: REAL_GETTER,
152
- createdTimestamp: REAL_GETTER,
153
- createdAt: REAL_GETTER
154
- }),
155
- // A message's text says which handler it is for, so it stays the test's to give, as a command's name does
156
- defaultsOf(Message, {
157
- pinned: ()=>false,
158
- tts: ()=>false,
159
- type: ()=>MessageType.Default,
160
- system: ()=>false,
161
- webhookId: ()=>null,
162
- applicationId: ()=>null,
163
- nonce: ()=>null,
164
- position: ()=>null,
165
- activity: ()=>null,
166
- reference: ()=>null,
167
- poll: ()=>null,
168
- interactionMetadata: ()=>null,
169
- stickers: ()=>new Collection(),
170
- messageSnapshots: ()=>new Collection(),
171
- reactions: (message)=>ownedManager(ReactionManager.prototype, {
172
- message
173
- }),
174
- url: REAL_GETTER
175
- }),
176
- // A command's name and a component's customId say which handler a call is for, so they stay the test's to give
177
- defaultsOf(ChatInputCommandInteraction, {
178
- options: ()=>createChatInputOptions()
179
- }),
180
- defaultsOf(CommandInteraction, {
181
- commandId: nextSnowflake,
182
- commandGuildId: ()=>null
183
- }),
184
- defaultsOf(AutocompleteInteraction, {
185
- commandId: nextSnowflake,
186
- commandGuildId: ()=>null,
187
- options: ()=>createChatInputOptions()
188
- }),
189
- defaultsOf(MessageReaction, {
190
- users: (reaction)=>ownedManager(ReactionUserManager.prototype, {
191
- reaction
192
- })
193
- }),
194
- defaultsOf(BaseInteraction, {
195
- applicationId: nextSnowflake,
196
- token: ()=>'mock-interaction-token',
197
- version: ()=>1,
198
- context: ()=>null,
199
- entitlements: ()=>new Collection(),
200
- // What Discord sends with an interaction in a server, the member's permissions there; typed for a cached one
201
- // Each reads where the interaction is as its own, and warns by its own name where strict mocks place it otherwise
202
- memberPermissions: (interaction)=>readPlaceAs(interaction, 'memberPermissions', ()=>{
203
- const { guildId } = interaction;
204
- const member = interaction.member;
205
- if (!guildId) return null;
206
- if (member instanceof GuildMember) return inChannel(interaction, member);
207
- // A raw member's permissions are the string Discord sends, read as discord.js reads them
208
- return isRawMember(member) && !interaction.guild ? new PermissionsBitField(BigInt(member.permissions)).freeze() : null;
209
- }),
210
- // The bot's permissions where the interaction was made: its member's in the channel, or in a thread its parent, as
211
- // discord.js computes them; none outside a server
212
- appPermissions: (interaction)=>readPlaceAs(interaction, 'appPermissions', ()=>{
213
- const { guild } = interaction;
214
- const me = guild instanceof Guild ? botMemberOf(guild) : null;
215
- if (!(me instanceof GuildMember) || heldChannelOf(interaction, guild) === null) return noPermissions();
216
- // discord.js's own computation, given its own overwritesFor over a test's stub on the channel; a channel the
217
- // mock would make has no overwrites
218
- const at = overwritingChannel(interaction, guild) ?? {
219
- guild,
220
- permissionOverwrites: {
221
- cache: new Collection()
222
- }
223
- };
224
- const channel = Object.create(at, {
225
- overwritesFor: {
226
- value: channelPermissions.overwritesFor
227
- }
228
- });
229
- const permissions = channelPermissions.memberPermissions.call(channel, me, true);
230
- // The bot's member has @everyone's too
231
- warnOnUnsetEveryone(interaction, guild, 'appPermissions', permissions);
232
- return permissions;
233
- }, // A server the bot isn't in has no guild in either mode, so no permissions in both
234
- (place)=>!place.raw)
235
- }),
236
- defaultsOf(TextChannel, {
237
- type: ()=>ChannelType.GuildText,
238
- nsfw: ()=>false,
239
- topic: ()=>null,
240
- rateLimitPerUser: ()=>0
241
- }),
242
- defaultsOf(NewsChannel, {
243
- type: ()=>ChannelType.GuildAnnouncement,
244
- nsfw: ()=>false,
245
- topic: ()=>null
246
- }),
247
- defaultsOf(VoiceChannel, {
248
- type: ()=>ChannelType.GuildVoice,
249
- nsfw: ()=>false
250
- }),
251
- defaultsOf(ForumChannel, {
252
- type: ()=>ChannelType.GuildForum,
253
- nsfw: ()=>false,
254
- topic: ()=>null
255
- }),
256
- defaultsOf(MediaChannel, {
257
- type: ()=>ChannelType.GuildMedia,
258
- nsfw: ()=>false,
259
- topic: ()=>null
260
- }),
261
- defaultsOf(ThreadChannel, {
262
- type: ()=>ChannelType.PublicThread,
263
- name: ()=>'thread'
264
- }),
265
- defaultsOf(DMChannel, {
266
- type: ()=>ChannelType.DM
267
- }),
268
- defaultsOf(GuildChannel, {
269
- name: ()=>'channel',
270
- guildId: (channel)=>typeof channel.guild?.id === 'string' ? channel.guild.id : nextSnowflake(),
271
- position: ()=>0,
272
- rawPosition: ()=>0,
273
- parentId: ()=>null
274
- }),
275
- defaultsOf(BaseChannel, {
276
- createdTimestamp: REAL_GETTER,
277
- createdAt: REAL_GETTER
278
- }),
279
- defaultsOf(Client, {
280
- readyTimestamp: ()=>Date.now(),
281
- readyAt: REAL_GETTER,
282
- uptime: REAL_GETTER,
283
- token: ()=>null
284
- }),
285
- defaultsOf(BaseGuild, {
286
- nameAcronym: REAL_GETTER
287
- })
288
- ]);
289
- /**
290
- * What strict mocks also read as Discord sends it, for discord.js's getters that read it: a server for a channel or
291
- * thread made without one, a thread's parent text channel in it, and data that is otherwise a placeholder.
292
- */ const STRICT_DEFAULTS = new Map([
293
- defaultsOf(Message, {
294
- content: ()=>'',
295
- editedTimestamp: ()=>null,
296
- editedAt: REAL_GETTER
297
- }),
298
- defaultsOf(MessageMentions, {
299
- repliedUser: ()=>null
300
- }),
301
- defaultsOf(Guild, {
302
- rulesChannelId: ()=>null,
303
- publicUpdatesChannelId: ()=>null,
304
- systemChannel: REAL_GETTER
305
- }),
306
- defaultsOf(BaseGuild, {
307
- verified: REAL_GETTER
308
- }),
309
- defaultsOf(GuildMember, {
310
- presence: REAL_GETTER
311
- }),
312
- defaultsOf(ModalSubmitInteraction, {
313
- message: ()=>null
314
- }),
315
- defaultsOf(ThreadChannel, {
316
- guild: ()=>createMockGuild(),
317
- guildId: (thread)=>thread.guild.id,
318
- parentId: (thread)=>{
319
- const parent = createMockChannel(TextChannel, {
320
- guild: thread.guild
321
- });
322
- thread.guild.channels.cache.set(parent.id, parent);
323
- madeParentIds.set(thread, parent.id);
324
- return parent.id;
325
- },
326
- parent: REAL_GETTER,
327
- ownerId: nextSnowflake,
328
- archived: ()=>false,
329
- locked: ()=>false
330
- }),
331
- defaultsOf(GuildChannel, {
332
- guild: ()=>createMockGuild(),
333
- parent: REAL_GETTER
334
- }),
335
- // Full once its members reach a user limit, which a voice channel has none of unless given one
336
- defaultsOf(BaseGuildVoiceChannel, {
337
- userLimit: ()=>0,
338
- full: REAL_GETTER
339
- }),
340
- // A reaction's message, as discord.js always has one, is a whole one
341
- defaultsOf(MessageReaction, {
342
- count: ()=>1,
343
- me: ()=>false,
344
- message: ()=>createMockMessage()
345
- })
346
- ]);
26
+ /** A channel's or thread's server id, its guild's, as discord.js sets it; one of its own for a guild the mock lacks. */ const guildIdOf = (channel)=>typeof channel.guild?.id === 'string' ? channel.guild.id : nextSnowflake();
347
27
  /**
348
28
  * What a mock reads as a placeholder unless strict mocks read it as discord.js gives it, by the value a test sets for
349
29
  * that, which the warning on reading one names.
@@ -405,19 +85,453 @@ const channelPermissions = GuildChannel.prototype;
405
85
  }
406
86
  return undefined;
407
87
  }
408
- /** The default a mock of `target`'s class has for `key`, read from the nearest class that has one, strict ones first. */ function discordDefault(target, key) {
409
- for (const defaults of strictMocks() ? [
410
- STRICT_DEFAULTS,
411
- DEFAULTS
412
- ] : [
413
- DEFAULTS
414
- ]){
415
- for(let proto = Object.getPrototypeOf(target); proto !== null; proto = Object.getPrototypeOf(proto)){
416
- const found = defaults.get(proto)?.[key];
417
- if (found !== undefined) return found;
88
+ /**
89
+ * The defaults a mock reads for a property the test gave none, made with `makers`: for `key` on a mock of `target`'s
90
+ * class, the default of the nearest class that has one, strict ones first.
91
+ */ function discordDefaults(makers) {
92
+ const { botMemberOf, createChatInputOptions, createMockChannel, createMockGuild, createMockMessage, createMockUser, heldChannelOf, isRawMember, isUnsetEveryone, madeAt, memberRoles, NEW_SERVER_EVERYONE_PERMISSIONS, ownedManager, readPlaceAs, readPlaceQuietly } = makers;
93
+ /**
94
+ * A member's permissions where an interaction was made, as Discord computes them: under strict mocks, every permission
95
+ * for an Administrator or the owner, else the channel's overwrites on top of `member.permissions`, so a test that sets
96
+ * those keeps the base; otherwise `member.permissions`, with a warning once where strict mocks would read otherwise.
97
+ */ function inChannel(interaction, member) {
98
+ const base = member.permissions;
99
+ // Only a server the mock caches has the overwrites to apply
100
+ const { guild } = interaction;
101
+ if (!(guild instanceof Guild)) return base;
102
+ const computed = base.has(PermissionFlagsBits.Administrator) || member.id === guild.ownerId ? new PermissionsBitField(PermissionsBitField.All).freeze() : withOverwrites(base, overwritingChannel(interaction, guild), member);
103
+ if (strictMocks()) return computed;
104
+ if (!computed.equals(base)) {
105
+ const lost = new PermissionsBitField(base).remove(computed).toArray();
106
+ const gained = new PermissionsBitField(computed).remove(base).toArray();
107
+ const change = lost.length > 0 ? [
108
+ `without ${lost.join(', ')}`
109
+ ] : [];
110
+ if (gained.length > 0) change.push(`with ${gained.join(', ')}`);
111
+ warnOnce(mockLogger, `${interaction.constructor.name}.memberPermissions reads the member's own permissions here, without the ` + "channel's overwrites or every permission an Administrator or the owner has; in the next major version (5.0) " + `it reads them as Discord computes them in the channel, ${change.join(' and ')}. Set memberPermissions on the ` + 'mock, or call useStrictMocks() to compute them now.');
418
112
  }
113
+ // Unless the test set them, a member's permissions come from its roles, @everyone's included
114
+ if (!Object.prototype.hasOwnProperty.call(member, 'permissions')) warnOnUnsetEveryone(interaction, guild, 'memberPermissions', base);
115
+ return base;
419
116
  }
420
- return undefined;
117
+ /**
118
+ * Warns once, as `key`, where `permissions` read a server's @everyone role default mode made with none, and lack what
119
+ * strict mocks give @everyone in a new server.
120
+ */ function warnOnUnsetEveryone(interaction, guild, key, permissions) {
121
+ const everyone = guild.roles.everyone;
122
+ if (!everyone || !isUnsetEveryone(everyone) || permissions.has(NEW_SERVER_EVERYONE_PERMISSIONS)) return;
123
+ warnOnce(mockLogger, `${interaction.constructor.name}.${key} reads the server's @everyone role here with no permissions; in the next major ` + 'version (5.0) it has the permissions Discord gives @everyone in a new server, as strict mocks read it. Set ' + 'guild.roles.everyone.permissions, or call useStrictMocks() to read it so now.');
124
+ }
125
+ /**
126
+ * The channel whose overwrites apply where an interaction was made, read without making or caching one: a thread's
127
+ * parent, or undefined where the channel, or a thread's parent, is one the mock would make, which has none.
128
+ */ function overwritingChannel(interaction, guild) {
129
+ const channel = heldChannelOf(interaction, guild);
130
+ // A thread's parent by the id the test gave it or one already read, so strict mocks' parent isn't made for this read
131
+ const parentId = (thread)=>Object.prototype.hasOwnProperty.call(thread, 'parentId') ? thread.parentId : madeParentIds.get(thread);
132
+ const at = channel instanceof ThreadChannel ? guild.channels.cache.get(parentId(channel) ?? '') : channel;
133
+ return at instanceof GuildChannel ? at : undefined;
134
+ }
135
+ /**
136
+ * A member's join or a client's ready time: under strict mocks when the mock was made, as Discord sends it, so time a
137
+ * test advances afterwards counts. Default mode stamps it at the first read and warns once.
138
+ */ function stampedAt(mock, name) {
139
+ if (strictMocks()) return madeAt(mock);
140
+ warnOnce(mockLogger, `${name} here is the time it was first read; under useStrictMocks() it is the time the mock was made, so time a ` + 'test advances afterwards counts.');
141
+ return Date.now();
142
+ }
143
+ /**
144
+ * The server of a channel or thread made without one: the one its own guildId names, else one of its own. Its own
145
+ * value is read, not its default, which reads the guild in turn.
146
+ */ function guildFromOwnId(channel) {
147
+ const id = Object.getOwnPropertyDescriptor(channel, 'guildId')?.value;
148
+ return createMockGuild(typeof id === 'string' ? {
149
+ id
150
+ } : {});
151
+ }
152
+ /** The bot's message a component interaction was used on, in the interaction's channel, holding that component. */ function clickedMessage(interaction) {
153
+ const channel = readPlaceQuietly(()=>interaction.channel);
154
+ const bot = interaction.client?.user;
155
+ const { componentType: type } = interaction;
156
+ // A placeholder customId, for an interaction made without one, has no place in the component's data
157
+ const customId = typeof interaction.customId === 'string' ? interaction.customId : undefined;
158
+ const component = type === ComponentType.Button ? {
159
+ type,
160
+ custom_id: customId,
161
+ style: ButtonStyle.Primary
162
+ } : type === ComponentType.StringSelect ? {
163
+ type,
164
+ custom_id: customId,
165
+ options: []
166
+ } : typeof type === 'number' ? {
167
+ type,
168
+ custom_id: customId
169
+ } : undefined;
170
+ return createMockMessage({
171
+ ...channel ? {
172
+ channel: channel
173
+ } : {},
174
+ ...bot instanceof User ? {
175
+ author: bot
176
+ } : {},
177
+ components: component ? [
178
+ {
179
+ type: ComponentType.ActionRow,
180
+ components: [
181
+ component
182
+ ]
183
+ }
184
+ ] : []
185
+ });
186
+ }
187
+ /**
188
+ * What a mock reads for a data property discord.js always sets, where the test gave none: a value in Discord's shape,
189
+ * such as `'en-US'` for a locale, `null` for what may be absent and `false` for a flag, or the property's own getter
190
+ * for one discord.js computes. The classes a mock descends from are read nearest first.
191
+ */ const DEFAULTS = new Map([
192
+ defaultsOf(Guild, {
193
+ name: ()=>'Guild',
194
+ preferredLocale: ()=>Locale.EnglishUS,
195
+ ownerId: nextSnowflake,
196
+ memberCount: ()=>1,
197
+ available: ()=>true,
198
+ large: ()=>false,
199
+ features: ()=>[],
200
+ description: ()=>null,
201
+ icon: ()=>null,
202
+ banner: ()=>null,
203
+ splash: ()=>null,
204
+ systemChannelId: ()=>null,
205
+ shardId: ()=>0,
206
+ nsfwLevel: ()=>GuildNSFWLevel.Default,
207
+ verificationLevel: ()=>GuildVerificationLevel.None,
208
+ presences: (guild)=>ownedManager(PresenceManager.prototype, {
209
+ guild
210
+ }),
211
+ premiumTier: ()=>GuildPremiumTier.None,
212
+ createdTimestamp: REAL_GETTER,
213
+ createdAt: REAL_GETTER
214
+ }),
215
+ defaultsOf(ClientUser, {
216
+ username: ()=>'bot'
217
+ }),
218
+ defaultsOf(User, {
219
+ username: ()=>'user',
220
+ bot: ()=>false,
221
+ globalName: ()=>null,
222
+ discriminator: ()=>'0',
223
+ avatar: ()=>null,
224
+ system: ()=>false,
225
+ flags: ()=>new UserFlagsBitField(),
226
+ tag: REAL_GETTER,
227
+ displayName: REAL_GETTER,
228
+ defaultAvatarURL: REAL_GETTER,
229
+ createdTimestamp: REAL_GETTER,
230
+ createdAt: REAL_GETTER
231
+ }),
232
+ defaultsOf(GuildMember, {
233
+ user: ()=>createMockUser(),
234
+ id: REAL_GETTER,
235
+ nickname: ()=>null,
236
+ avatar: ()=>null,
237
+ joinedTimestamp: (member)=>stampedAt(member, 'GuildMember.joinedTimestamp'),
238
+ joinedAt: REAL_GETTER,
239
+ partial: REAL_GETTER,
240
+ premiumSince: REAL_GETTER,
241
+ communicationDisabledUntil: REAL_GETTER,
242
+ premiumSinceTimestamp: ()=>null,
243
+ communicationDisabledUntilTimestamp: ()=>null,
244
+ pending: ()=>false,
245
+ flags: ()=>new GuildMemberFlagsBitField().freeze(),
246
+ displayName: REAL_GETTER,
247
+ // The colour of its highest coloured role, as discord.js computes it from the roles in its cache
248
+ displayColor: REAL_GETTER,
249
+ displayHexColor: REAL_GETTER,
250
+ roles: (member)=>memberRoles(member, []),
251
+ permissions: REAL_GETTER
252
+ }),
253
+ // A server's emoji, sticker, scheduled event and AutoMod rule are named, and what discord.js formats from them
254
+ // computes: an emoji's mention and identifier, an event's and an invite's link
255
+ defaultsOf(GuildEmoji, {
256
+ name: ()=>'emoji',
257
+ animated: ()=>false,
258
+ identifier: REAL_GETTER
259
+ }),
260
+ defaultsOf(Sticker, {
261
+ name: ()=>'sticker'
262
+ }),
263
+ defaultsOf(GuildScheduledEvent, {
264
+ name: ()=>'event',
265
+ url: REAL_GETTER
266
+ }),
267
+ defaultsOf(AutoModerationRule, {
268
+ name: ()=>'rule'
269
+ }),
270
+ defaultsOf(Invite, {
271
+ url: REAL_GETTER
272
+ }),
273
+ defaultsOf(Role, {
274
+ name: ()=>'role',
275
+ // Above @everyone, which a server's role manager makes at position 0
276
+ position: ()=>1,
277
+ // Every role has a server; one made without is in one of its own, unless a guild given it takes it in
278
+ guild: ()=>createMockGuild(),
279
+ rawPosition: (role)=>role.position,
280
+ permissions: noPermissions,
281
+ hoist: ()=>false,
282
+ managed: ()=>false,
283
+ mentionable: ()=>false,
284
+ icon: ()=>null,
285
+ unicodeEmoji: ()=>null,
286
+ tags: ()=>null,
287
+ colors: ()=>({
288
+ primaryColor: 0,
289
+ secondaryColor: null,
290
+ tertiaryColor: null
291
+ }),
292
+ hexColor: REAL_GETTER,
293
+ createdTimestamp: REAL_GETTER,
294
+ createdAt: REAL_GETTER
295
+ }),
296
+ // A message's text says which handler it is for, so it stays the test's to give, as a command's name does
297
+ defaultsOf(Message, {
298
+ pinned: ()=>false,
299
+ tts: ()=>false,
300
+ type: ()=>MessageType.Default,
301
+ system: ()=>false,
302
+ webhookId: ()=>null,
303
+ applicationId: ()=>null,
304
+ nonce: ()=>null,
305
+ position: ()=>null,
306
+ activity: ()=>null,
307
+ reference: ()=>null,
308
+ poll: ()=>null,
309
+ interactionMetadata: ()=>null,
310
+ stickers: ()=>new Collection(),
311
+ messageSnapshots: ()=>new Collection(),
312
+ reactions: (message)=>ownedManager(ReactionManager.prototype, {
313
+ message
314
+ }),
315
+ url: REAL_GETTER
316
+ }),
317
+ // A command's name and a component's customId say which handler a call is for, so they stay the test's to give
318
+ defaultsOf(ChatInputCommandInteraction, {
319
+ options: ()=>createChatInputOptions()
320
+ }),
321
+ defaultsOf(CommandInteraction, {
322
+ commandId: nextSnowflake,
323
+ commandGuildId: ()=>null
324
+ }),
325
+ defaultsOf(AutocompleteInteraction, {
326
+ commandId: nextSnowflake,
327
+ commandGuildId: ()=>null,
328
+ options: ()=>createChatInputOptions()
329
+ }),
330
+ defaultsOf(MessageReaction, {
331
+ users: (reaction)=>ownedManager(ReactionUserManager.prototype, {
332
+ reaction
333
+ })
334
+ }),
335
+ // The bot's message the component was used on, holding it, in the interaction's channel, as Discord always sends it;
336
+ // the component discord.js finds there by its customId
337
+ defaultsOf(MessageComponentInteraction, {
338
+ message: clickedMessage,
339
+ component: REAL_GETTER
340
+ }),
341
+ // A modal submitted with no fields given has none
342
+ defaultsOf(ModalSubmitInteraction, {
343
+ fields: ()=>createModalFields({}),
344
+ components: ()=>[]
345
+ }),
346
+ defaultsOf(BaseInteraction, {
347
+ applicationId: nextSnowflake,
348
+ token: ()=>'mock-interaction-token',
349
+ version: ()=>1,
350
+ context: ()=>null,
351
+ entitlements: ()=>new Collection(),
352
+ // What Discord sends with an interaction in a server, the member's permissions there; typed for a cached one
353
+ // Each reads where the interaction is as its own, and warns by its own name where strict mocks place it otherwise
354
+ memberPermissions: (interaction)=>readPlaceAs(interaction, 'memberPermissions', ()=>{
355
+ const { guildId } = interaction;
356
+ const member = interaction.member;
357
+ if (!guildId) return null;
358
+ if (member instanceof GuildMember) return inChannel(interaction, member);
359
+ // A raw member's permissions are the string Discord sends, read as discord.js reads them
360
+ return isRawMember(member) && !interaction.guild ? new PermissionsBitField(BigInt(member.permissions)).freeze() : null;
361
+ }),
362
+ // The bot's permissions where the interaction was made: its member's in the channel, or in a thread its parent, as
363
+ // discord.js computes them; none outside a server
364
+ appPermissions: (interaction)=>readPlaceAs(interaction, 'appPermissions', ()=>{
365
+ const { guild } = interaction;
366
+ const me = guild instanceof Guild ? botMemberOf(guild) : null;
367
+ if (!(me instanceof GuildMember) || heldChannelOf(interaction, guild) === null) return noPermissions();
368
+ // discord.js's own computation, given its own overwritesFor over a test's stub on the channel; a channel the
369
+ // mock would make has no overwrites
370
+ const at = overwritingChannel(interaction, guild) ?? {
371
+ guild,
372
+ permissionOverwrites: {
373
+ cache: new Collection()
374
+ }
375
+ };
376
+ const channel = Object.create(at, {
377
+ overwritesFor: {
378
+ value: channelPermissions.overwritesFor
379
+ }
380
+ });
381
+ const permissions = channelPermissions.memberPermissions.call(channel, me, true);
382
+ // The bot's member has @everyone's too
383
+ warnOnUnsetEveryone(interaction, guild, 'appPermissions', permissions);
384
+ return permissions;
385
+ }, // A server the bot isn't in has no guild in either mode, so no permissions in both
386
+ (place)=>!place.raw)
387
+ }),
388
+ defaultsOf(TextChannel, {
389
+ type: ()=>ChannelType.GuildText,
390
+ nsfw: ()=>false,
391
+ topic: ()=>null,
392
+ rateLimitPerUser: ()=>0
393
+ }),
394
+ defaultsOf(NewsChannel, {
395
+ type: ()=>ChannelType.GuildAnnouncement,
396
+ nsfw: ()=>false,
397
+ topic: ()=>null
398
+ }),
399
+ defaultsOf(VoiceChannel, {
400
+ type: ()=>ChannelType.GuildVoice,
401
+ nsfw: ()=>false
402
+ }),
403
+ defaultsOf(ForumChannel, {
404
+ type: ()=>ChannelType.GuildForum,
405
+ nsfw: ()=>false,
406
+ topic: ()=>null
407
+ }),
408
+ defaultsOf(MediaChannel, {
409
+ type: ()=>ChannelType.GuildMedia,
410
+ nsfw: ()=>false,
411
+ topic: ()=>null
412
+ }),
413
+ defaultsOf(StageChannel, {
414
+ type: ()=>ChannelType.GuildStageVoice,
415
+ nsfw: ()=>false,
416
+ topic: ()=>null
417
+ }),
418
+ defaultsOf(CategoryChannel, {
419
+ type: ()=>ChannelType.GuildCategory
420
+ }),
421
+ defaultsOf(DirectoryChannel, {
422
+ type: ()=>ChannelType.GuildDirectory
423
+ }),
424
+ // A thread's server is its guild's, as discord.js sets it from the thread's guild
425
+ defaultsOf(ThreadChannel, {
426
+ type: ()=>ChannelType.PublicThread,
427
+ name: ()=>'thread',
428
+ guildId: guildIdOf
429
+ }),
430
+ defaultsOf(DMChannel, {
431
+ type: ()=>ChannelType.DM
432
+ }),
433
+ defaultsOf(PartialGroupDMChannel, {
434
+ type: ()=>ChannelType.GroupDM
435
+ }),
436
+ defaultsOf(GuildChannel, {
437
+ name: ()=>'channel',
438
+ guildId: guildIdOf,
439
+ position: ()=>0,
440
+ rawPosition: ()=>0,
441
+ parentId: ()=>null
442
+ }),
443
+ defaultsOf(BaseChannel, {
444
+ createdTimestamp: REAL_GETTER,
445
+ createdAt: REAL_GETTER
446
+ }),
447
+ defaultsOf(Client, {
448
+ readyTimestamp: (client)=>stampedAt(client, 'Client.readyTimestamp'),
449
+ readyAt: REAL_GETTER,
450
+ uptime: REAL_GETTER,
451
+ token: ()=>null
452
+ }),
453
+ // What a member's roles show, computed from the roles in the cache as discord.js does
454
+ defaultsOf(GuildMemberRoleManager, {
455
+ color: REAL_GETTER,
456
+ hoist: REAL_GETTER,
457
+ icon: REAL_GETTER
458
+ }),
459
+ defaultsOf(BaseGuild, {
460
+ nameAcronym: REAL_GETTER
461
+ })
462
+ ]);
463
+ /**
464
+ * What strict mocks also read as Discord sends it, for discord.js's getters that read it: a server for a channel or
465
+ * thread made without one, a thread's parent text channel in it, and data that is otherwise a placeholder.
466
+ */ const STRICT_DEFAULTS = new Map([
467
+ defaultsOf(Message, {
468
+ content: ()=>'',
469
+ editedTimestamp: ()=>null,
470
+ editedAt: REAL_GETTER
471
+ }),
472
+ defaultsOf(MessageMentions, {
473
+ repliedUser: ()=>null
474
+ }),
475
+ defaultsOf(Guild, {
476
+ rulesChannelId: ()=>null,
477
+ publicUpdatesChannelId: ()=>null,
478
+ systemChannel: REAL_GETTER
479
+ }),
480
+ defaultsOf(BaseGuild, {
481
+ verified: REAL_GETTER
482
+ }),
483
+ defaultsOf(GuildMember, {
484
+ presence: REAL_GETTER
485
+ }),
486
+ defaultsOf(ModalSubmitInteraction, {
487
+ message: ()=>null
488
+ }),
489
+ defaultsOf(ThreadChannel, {
490
+ guild: guildFromOwnId,
491
+ guildId: (thread)=>thread.guild.id,
492
+ parentId: (thread)=>{
493
+ const parent = createMockChannel(TextChannel, {
494
+ guild: thread.guild
495
+ });
496
+ thread.guild.channels.cache.set(parent.id, parent);
497
+ madeParentIds.set(thread, parent.id);
498
+ return parent.id;
499
+ },
500
+ parent: REAL_GETTER,
501
+ ownerId: nextSnowflake,
502
+ archived: ()=>false,
503
+ locked: ()=>false
504
+ }),
505
+ defaultsOf(GuildChannel, {
506
+ guild: guildFromOwnId,
507
+ parent: REAL_GETTER
508
+ }),
509
+ // Full once its members reach a user limit, which a voice channel has none of unless given one
510
+ defaultsOf(BaseGuildVoiceChannel, {
511
+ userLimit: ()=>0,
512
+ full: REAL_GETTER
513
+ }),
514
+ // A reaction's message, as discord.js always has one, is a whole one
515
+ defaultsOf(MessageReaction, {
516
+ count: ()=>1,
517
+ me: ()=>false,
518
+ message: ()=>createMockMessage()
519
+ })
520
+ ]);
521
+ return (target, key)=>{
522
+ for (const defaults of strictMocks() ? [
523
+ STRICT_DEFAULTS,
524
+ DEFAULTS
525
+ ] : [
526
+ DEFAULTS
527
+ ]){
528
+ for(let proto = Object.getPrototypeOf(target); proto !== null; proto = Object.getPrototypeOf(proto)){
529
+ const found = defaults.get(proto)?.[key];
530
+ if (found !== undefined) return found;
531
+ }
532
+ }
533
+ return undefined;
534
+ };
421
535
  }
422
536
 
423
- export { REAL_GETTER, discordDefault, placeholderValue };
537
+ export { REAL_GETTER, discordDefaults, placeholderValue };