meocord 1.8.4 → 2.0.0-beta.2

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/README.md CHANGED
@@ -41,7 +41,7 @@
41
41
 
42
42
  ### Prerequisites
43
43
 
44
- - **Runtime**: Node.js (latest LTS) or Bun 1.x+
44
+ - **Runtime**: Node.js 22 or newer, or Bun 1.x+
45
45
  - **TypeScript**: 5.0+
46
46
  - **Package manager**: npm, yarn, pnpm, or bun
47
47
 
@@ -410,6 +410,33 @@ interaction.guildId = 'guild-123'
410
410
 
411
411
  Works for any discord.js class — interactions, `Message`, `MessageReaction`, and anything else. No per-type maintenance.
412
412
 
413
+ **Assignable to the real class** — the returned mock can be passed straight to code that expects the discord.js type. No `as unknown as ButtonInteraction` at the call site.
414
+
415
+ ```typescript
416
+ const interaction = createMockInteraction(ButtonInteraction)
417
+
418
+ await controller.handleButton(interaction) // takes a real ButtonInteraction
419
+ ```
420
+
421
+ **Property overrides at construction** — pass a second argument to set properties as the mock is built. This is required for anything discord.js declares `readonly` (`ModalSubmitInteraction#customId` and `#fields`, `MessageComponentInteraction#message`, `client`, `guildId` on some classes), since those cannot be assigned afterwards. It is also how you set a property backed by a getter-only prototype accessor, such as `targetUser` or `targetMessage` on a context menu.
422
+
423
+ ```typescript
424
+ import { createMockInteraction, createMockUser } from 'meocord/testing'
425
+ import { ModalSubmitInteraction, UserContextMenuCommandInteraction } from 'discord.js'
426
+
427
+ const modal = createMockInteraction(ModalSubmitInteraction, {
428
+ customId: 'wish-import-800000000',
429
+ fields: { getTextInputValue: () => '{"pulls":[]}' } as unknown as ModalSubmitInteraction['fields'],
430
+ })
431
+
432
+ const contextMenu = createMockInteraction(UserContextMenuCommandInteraction, {
433
+ commandName: 'profile',
434
+ targetUser: createMockUser(),
435
+ })
436
+ ```
437
+
438
+ The override record is typed as `MockProps<T>`, exported from `meocord/testing`. Every key is optional, and a misspelled property name is a compile error.
439
+
413
440
  ### `createChatInputOptions`
414
441
 
415
442
  Builds a typed options resolver from a plain record. Type routing mirrors the real `CommandInteractionOptionResolver`: wrong-type access returns `null`, `required=true` throws if the option is absent.
@@ -502,6 +529,19 @@ const module = MeoCordTestingModule.create({
502
529
 
503
530
  `canActivate: () => true` allows the method to run. `() => false` blocks it. Multiple guards chain fluently.
504
531
 
532
+ ### `overrideProvider`
533
+
534
+ Replaces a provider already registered on the module. The value is typed as `Partial<T>`, so a double only has to cover the methods the test exercises — a class with a private member could never be satisfied by a full object literal anyway. A misspelled method name is still a compile error.
535
+
536
+ ```typescript
537
+ const module = MeoCordTestingModule.create({
538
+ controllers: [GreetingSlashController],
539
+ providers: [{ provide: GreetingService, useValue: realGreetingService }],
540
+ })
541
+ .overrideProvider(GreetingService).useValue({ buildGreeting: createMockFn() })
542
+ .compile()
543
+ ```
544
+
505
545
  ### Full example
506
546
 
507
547
  ```typescript
@@ -22,7 +22,13 @@ function isValueProvider(p) {
22
22
  * Builder returned by `MeoCordTestingModule.create()`.
23
23
  * Call `.compile()` to get the resolved `TestingModule`.
24
24
  */ class TestingModuleBuilder {
25
- overrideProvider(token) {
25
+ /**
26
+ * A double covers the methods under test, not the whole class — and a class
27
+ * with any private member (a service holding a logger, say) can never be
28
+ * satisfied by an object literal at all. `Partial<T>` still rejects a
29
+ * misspelled method name, which is the check that actually earns its keep.
30
+ * Matches `overrideGuard`, which has always taken `Partial<GuardInterface>`.
31
+ */ overrideProvider(token) {
26
32
  return {
27
33
  useValue: (value)=>{
28
34
  this.overrides.set(token, {
@@ -355,8 +361,17 @@ function stubDeep(instance, externalStubs) {
355
361
  stubs.set(key, stub);
356
362
  return stub;
357
363
  },
364
+ // defineProperty rather than assignment: many discord.js properties are
365
+ // prototype getters with no setter (targetUser, targetMessage, createdAt),
366
+ // and a plain write against one of those is a silent no-op. Defining an own
367
+ // data property shadows the accessor, which is what test setup means.
358
368
  set (target, prop, value) {
359
- Reflect.set(target, prop, value, target);
369
+ Object.defineProperty(target, prop, {
370
+ value,
371
+ writable: true,
372
+ enumerable: true,
373
+ configurable: true
374
+ });
360
375
  return true;
361
376
  }
362
377
  });
@@ -481,7 +496,7 @@ function findPrototypeMethod(instance, name) {
481
496
  * interaction.replied // → true
482
497
  * await interaction.reply({}) // throws — already replied
483
498
  * ```
484
- */ function createMockInteraction(Class) {
499
+ */ function createMockInteraction(Class, props) {
485
500
  const instance = Object.create(Class.prototype);
486
501
  const stubs = new Map();
487
502
  // Set type fields so all prototype type-guard methods compute the right value
@@ -551,6 +566,19 @@ function findPrototypeMethod(instance, name) {
551
566
  }));
552
567
  }
553
568
  }
569
+ // Applied last so an explicit prop wins over the type fields and the reply
570
+ // state machine. defineProperty rather than assignment for the same reason the
571
+ // Proxy uses it: several of these shadow a getter-only prototype accessor.
572
+ if (props !== undefined) {
573
+ for (const [key, value] of Object.entries(props)){
574
+ Object.defineProperty(instance, key, {
575
+ value,
576
+ writable: true,
577
+ enumerable: true,
578
+ configurable: true
579
+ });
580
+ }
581
+ }
554
582
  return stubDeep(instance, stubs);
555
583
  }
556
584
  // ---------------------------------------------------------------------------
@@ -719,7 +747,14 @@ function createMockMessage() {
719
747
  * interaction.options.getSubcommand() // → 'notes'
720
748
  * interaction.options.getNumber('uid') // → 12345678
721
749
  * ```
722
- */ function createChatInputOptions(opts = {}) {
750
+ */ // `any` is the default rather than `CacheType` because TypeScript types a generic
751
+ // class's `prototype` with `any` for its parameters, and createMockInteraction infers
752
+ // T from exactly that — `createMockInteraction(ChatInputCommandInteraction)` produces
753
+ // an interaction whose `options` is `CommandInteractionOptionResolver<any>`. Defaulting
754
+ // to `CacheType` instead makes CacheTypeReducer widen getChannel's return with a `null`
755
+ // the target rejects, and the resolver stops being assignable to the property it exists
756
+ // to fill. Pass Cached explicitly when the interaction under test is pinned.
757
+ function createChatInputOptions(opts = {}) {
723
758
  const { subcommandGroup = null, subcommand = null, ...values } = opts;
724
759
  function resolveOrThrow(name, value, required) {
725
760
  if (value === null) {
@@ -1,4 +1,4 @@
1
- var version = "1.2.6";
1
+ var version = "0.0.0-development";
2
2
  var packageJson = {
3
3
  version: version};
4
4
 
@@ -19,7 +19,13 @@ function isValueProvider(p) {
19
19
  * Builder returned by `MeoCordTestingModule.create()`.
20
20
  * Call `.compile()` to get the resolved `TestingModule`.
21
21
  */ class TestingModuleBuilder {
22
- overrideProvider(token) {
22
+ /**
23
+ * A double covers the methods under test, not the whole class — and a class
24
+ * with any private member (a service holding a logger, say) can never be
25
+ * satisfied by an object literal at all. `Partial<T>` still rejects a
26
+ * misspelled method name, which is the check that actually earns its keep.
27
+ * Matches `overrideGuard`, which has always taken `Partial<GuardInterface>`.
28
+ */ overrideProvider(token) {
23
29
  return {
24
30
  useValue: (value)=>{
25
31
  this.overrides.set(token, {
@@ -48,8 +48,17 @@ function stubDeep(instance, externalStubs) {
48
48
  stubs.set(key, stub);
49
49
  return stub;
50
50
  },
51
+ // defineProperty rather than assignment: many discord.js properties are
52
+ // prototype getters with no setter (targetUser, targetMessage, createdAt),
53
+ // and a plain write against one of those is a silent no-op. Defining an own
54
+ // data property shadows the accessor, which is what test setup means.
51
55
  set (target, prop, value) {
52
- Reflect.set(target, prop, value, target);
56
+ Object.defineProperty(target, prop, {
57
+ value,
58
+ writable: true,
59
+ enumerable: true,
60
+ configurable: true
61
+ });
53
62
  return true;
54
63
  }
55
64
  });
@@ -174,7 +183,7 @@ function findPrototypeMethod(instance, name) {
174
183
  * interaction.replied // → true
175
184
  * await interaction.reply({}) // throws — already replied
176
185
  * ```
177
- */ function createMockInteraction(Class) {
186
+ */ function createMockInteraction(Class, props) {
178
187
  const instance = Object.create(Class.prototype);
179
188
  const stubs = new Map();
180
189
  // Set type fields so all prototype type-guard methods compute the right value
@@ -244,6 +253,19 @@ function findPrototypeMethod(instance, name) {
244
253
  }));
245
254
  }
246
255
  }
256
+ // Applied last so an explicit prop wins over the type fields and the reply
257
+ // state machine. defineProperty rather than assignment for the same reason the
258
+ // Proxy uses it: several of these shadow a getter-only prototype accessor.
259
+ if (props !== undefined) {
260
+ for (const [key, value] of Object.entries(props)){
261
+ Object.defineProperty(instance, key, {
262
+ value,
263
+ writable: true,
264
+ enumerable: true,
265
+ configurable: true
266
+ });
267
+ }
268
+ }
247
269
  return stubDeep(instance, stubs);
248
270
  }
249
271
  // ---------------------------------------------------------------------------
@@ -412,7 +434,14 @@ function createMockMessage() {
412
434
  * interaction.options.getSubcommand() // → 'notes'
413
435
  * interaction.options.getNumber('uid') // → 12345678
414
436
  * ```
415
- */ function createChatInputOptions(opts = {}) {
437
+ */ // `any` is the default rather than `CacheType` because TypeScript types a generic
438
+ // class's `prototype` with `any` for its parameters, and createMockInteraction infers
439
+ // T from exactly that — `createMockInteraction(ChatInputCommandInteraction)` produces
440
+ // an interaction whose `options` is `CommandInteractionOptionResolver<any>`. Defaulting
441
+ // to `CacheType` instead makes CacheTypeReducer widen getChannel's return with a `null`
442
+ // the target rejects, and the resolver stops being assignable to the property it exists
443
+ // to fill. Pass Cached explicitly when the interaction under test is pinned.
444
+ function createChatInputOptions(opts = {}) {
416
445
  const { subcommandGroup = null, subcommand = null, ...values } = opts;
417
446
  function resolveOrThrow(name, value, required) {
418
447
  if (value === null) {
@@ -1,4 +1,4 @@
1
- import { C as CommandType } from '../controller.enum-QA-IuReF.js';
1
+ import { CommandType } from '../enum/index.js';
2
2
  import { SlashCommandBuilder, SlashCommandSubcommandsOnlyBuilder, ContextMenuCommandBuilder, ButtonInteraction, StringSelectMenuInteraction, ChatInputCommandInteraction, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, ModalSubmitInteraction, OmitPartialGroupDMChannel, Message, MessageReaction, PartialMessageReaction, ClientOptions, ActivityOptions } from 'discord.js';
3
3
  import { ReactionHandlerOptions, GuardInterface } from '../interface/index.js';
4
4
  import { ServiceIdentifier } from 'inversify';
@@ -1,6 +1,6 @@
1
1
  import { User, PartialUser, BaseInteraction, Message, MessageReaction } from 'discord.js';
2
2
  import { Configuration } from 'webpack';
3
- import { R as ReactionHandlerAction } from '../controller.enum-QA-IuReF.js';
3
+ import { ReactionHandlerAction } from '../enum/index.js';
4
4
 
5
5
  /**
6
6
  * MeoCord Framework
@@ -1,6 +1,6 @@
1
1
  import { ServiceIdentifier, Container } from 'inversify';
2
2
  import { GuardInterface } from '../interface/index.js';
3
- import { CommandInteractionOptionResolver, Channel, Client, Guild, Message, User } from 'discord.js';
3
+ import { CacheType, CommandInteractionOptionResolver, Channel, Client, Guild, Message, User } from 'discord.js';
4
4
  import 'webpack';
5
5
  import '../controller.enum-QA-IuReF.js';
6
6
 
@@ -40,8 +40,15 @@ declare class TestingModuleBuilder {
40
40
  private readonly overrides;
41
41
  private readonly guardOverrides;
42
42
  constructor(options: TestingModuleOptions);
43
+ /**
44
+ * A double covers the methods under test, not the whole class — and a class
45
+ * with any private member (a service holding a logger, say) can never be
46
+ * satisfied by an object literal at all. `Partial<T>` still rejects a
47
+ * misspelled method name, which is the check that actually earns its keep.
48
+ * Matches `overrideGuard`, which has always taken `Partial<GuardInterface>`.
49
+ */
43
50
  overrideProvider<T>(token: ServiceIdentifier<T>): {
44
- useValue: (value: T) => TestingModuleBuilder;
51
+ useValue: (value: Partial<T>) => TestingModuleBuilder;
45
52
  };
46
53
  overrideGuard(guard: new (...args: any[]) => GuardInterface): {
47
54
  useValue: (stub: Partial<GuardInterface>) => TestingModuleBuilder;
@@ -143,10 +150,35 @@ declare function createMockFn<T extends (...args: any[]) => any = (...args: any[
143
150
  * Recursively transforms all methods of T into MockedFunction and all
144
151
  * nested objects into DeepMocked. Depth cap at 5 prevents infinite recursion
145
152
  * on circular discord.js types (e.g. Guild ↔ GuildMember).
146
- * -readonly removes readonly modifiers so test setup can write any property.
153
+ *
154
+ * Intersected with `T` so the mock is accepted wherever the real class is
155
+ * expected. A mapped type alone can never be: it iterates `keyof T`, and the
156
+ * private members classes use as brands — discord.js stamps
157
+ * `private readonly _cacheType` on `BaseInteraction` — are not in `keyof T`.
158
+ * Without the intersection every call site has to launder the mock through
159
+ * `as unknown as T`, which defeats the point of typing it at all.
160
+ *
161
+ * The intersection is honest rather than a convenient lie: the object really is
162
+ * `Object.create(Class.prototype)`, so its prototype chain is the real one, and
163
+ * TypeScript `private` is erased at runtime.
164
+ *
165
+ * `readonly` survives from the `T` side, so properties the class declares
166
+ * readonly cannot be assigned after construction. Pass those to the factory as
167
+ * {@link MockProps} instead.
147
168
  */
148
169
  type DeepMocked<T, Depth extends number[] = []> = Depth['length'] extends 5 ? T : {
149
170
  -readonly [K in keyof T]: T[K] extends (...args: infer A) => infer R ? MockedFunction<(...args: A) => R> : T[K] extends object ? DeepMocked<T[K], [...Depth, 0]> : T[K];
171
+ } & T;
172
+ /**
173
+ * Property overrides accepted at construction by the mock factories.
174
+ *
175
+ * Setup belongs here rather than in a post-construction assignment: the returned
176
+ * mock is assignable to the real discord.js class, so anything that class declares
177
+ * `readonly` — `ModalSubmitInteraction#customId`, `#fields`, `MessageComponentInteraction#message`
178
+ * — cannot be written afterwards without a cast.
179
+ */
180
+ type MockProps<T> = {
181
+ -readonly [K in keyof T]?: T[K] extends (...args: any[]) => any ? (...args: any[]) => any : T[K];
150
182
  };
151
183
  interface InteractionClass<T> {
152
184
  prototype: T;
@@ -179,7 +211,7 @@ interface InteractionClass<T> {
179
211
  * await interaction.reply({}) // throws — already replied
180
212
  * ```
181
213
  */
182
- declare function createMockInteraction<T extends object>(Class: InteractionClass<T>): DeepMocked<T>;
214
+ declare function createMockInteraction<T extends object>(Class: InteractionClass<T>, props?: MockProps<T>): DeepMocked<T>;
183
215
  /** Creates a mock {@link User}. All methods are auto-stubbed as a mock fn. */
184
216
  declare const createMockUser: () => DeepMocked<User>;
185
217
  /**
@@ -240,7 +272,7 @@ interface ChatInputOptions {
240
272
  * interaction.options.getNumber('uid') // → 12345678
241
273
  * ```
242
274
  */
243
- declare function createChatInputOptions(opts?: ChatInputOptions): DeepMocked<CommandInteractionOptionResolver>;
275
+ declare function createChatInputOptions<Cached extends CacheType = any>(opts?: ChatInputOptions): DeepMocked<CommandInteractionOptionResolver<Cached>>;
244
276
 
245
277
  export { MeoCordTestingModule, TestingModule, TestingModuleBuilder, createChatInputOptions, createMockChannel, createMockClient, createMockFn, createMockGuild, createMockInteraction, createMockMessage, createMockUser, isMockFunction };
246
- export type { ChatInputOptions, ClassProvider, DeepMocked, Mock, MockInstance, MockResult, MockState, MockedFunction, Provider, TestingModuleOptions, ValueProvider };
278
+ export type { ChatInputOptions, ClassProvider, DeepMocked, Mock, MockInstance, MockProps, MockResult, MockState, MockedFunction, Provider, TestingModuleOptions, ValueProvider };
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "meocord",
3
3
  "description": "Decorator-based Discord bot framework built on discord.js. Brings NestJS-style controllers, dependency injection, guards, and testing utilities to bot development — with a full CLI and TypeScript-first design.",
4
- "version": "1.8.4",
4
+ "version": "2.0.0-beta.2",
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
6
9
  "scripts": {
7
10
  "lint": "eslint --fix . && tsc --noEmit && tsc --noEmit --project tsconfig.test.json",
8
11
  "build": "rm -rf ./dist && rollup -c",
@@ -72,13 +75,13 @@
72
75
  ],
73
76
  "dependencies": {
74
77
  "@clack/prompts": "^1.7.0",
75
- "@swc/core": "1.15.43",
76
- "chalk": "^5.6.2",
78
+ "@swc/core": "1.15.47",
79
+ "chalk": "^6.0.0",
77
80
  "cli-table3": "^0.6.5",
78
81
  "commander": "^15.0.0",
79
82
  "dayjs": "^1.11.21",
80
83
  "dotenv": "^17.4.2",
81
- "inversify": "^8.1.1",
84
+ "inversify": "^8.2.3",
82
85
  "jiti": "^2.7.0",
83
86
  "lodash-es": "^4.18.1",
84
87
  "nodemon": "^3.1.14",
@@ -87,7 +90,7 @@
87
90
  "swc-loader": "^0.2.7",
88
91
  "terser-webpack-plugin": "^5.6.1",
89
92
  "tsconfig-paths-webpack-plugin": "^4.2.0",
90
- "webpack": "^5.108.3",
93
+ "webpack": "^5.109.2",
91
94
  "webpack-node-externals": "^3.0.0"
92
95
  },
93
96
  "devDependencies": {
@@ -102,27 +105,27 @@
102
105
  "@semantic-release/release-notes-generator": "^14.1.1",
103
106
  "@types/lodash-es": "^4.17.12",
104
107
  "@types/webpack-node-externals": "^3.0.4",
105
- "@typescript-eslint/parser": "^8.62.1",
106
- "discord.js": "^14.26.4",
107
- "eslint": "^10.6.0",
108
+ "@typescript-eslint/parser": "^8.67.0",
109
+ "discord.js": "^14.27.0",
110
+ "eslint": "^10.8.1",
108
111
  "eslint-config-prettier": "^10.1.8",
109
112
  "eslint-plugin-headers": "^1.3.4",
110
113
  "eslint-plugin-import-x": "^4.17.1",
111
114
  "eslint-plugin-prettier": "^5.5.6",
112
115
  "eslint-plugin-unused-imports": "^4.4.1",
113
- "globals": "^17.7.0",
116
+ "globals": "^17.11.0",
114
117
  "husky": "^9.1.7",
115
- "prettier": "^3.9.4",
116
- "rollup": "^4.62.2",
118
+ "prettier": "^3.9.6",
119
+ "rollup": "^4.62.4",
117
120
  "rollup-plugin-copy": "^3.5.0",
118
- "rollup-plugin-dts": "^6.4.1",
119
- "semantic-release": "^25.0.5",
121
+ "rollup-plugin-dts": "^6.5.1",
122
+ "semantic-release": "^25.0.9",
120
123
  "ts-node": "^10.9.2",
121
124
  "typescript": "^6.0.3",
122
- "typescript-eslint": "^8.62.1",
123
- "@vitest/coverage-istanbul": "^3.2.4",
124
- "unplugin-swc": "^1.5.5",
125
- "vitest": "^3.2.4"
125
+ "typescript-eslint": "^8.67.0",
126
+ "@vitest/coverage-istanbul": "^4.1.10",
127
+ "unplugin-swc": "^1.5.11",
128
+ "vitest": "^4.1.10"
126
129
  },
127
130
  "peerDependencies": {
128
131
  "discord.js": "^14.26.4",