meocord 4.1.2 → 4.2.1

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 (140) hide show
  1. package/CHANGELOG.md +362 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
  4. package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
  5. package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
  7. package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
  8. package/dist/cjs/common/index.cjs +4 -4
  9. package/dist/cjs/core/index.cjs +365 -310
  10. package/dist/cjs/decorator/index.cjs +85 -61
  11. package/dist/cjs/testing/index.cjs +2236 -496
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/app-template/README.md.template +3 -1
  14. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
  15. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
  16. package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
  17. package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
  18. package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
  19. package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
  20. package/dist/esm/bin/generator.js +4 -5
  21. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  22. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  23. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  24. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  25. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  26. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  27. package/dist/esm/bin/meocord.js +84 -25
  28. package/dist/esm/build/load-config.pre-entry.js +17 -4
  29. package/dist/esm/build/rsbuild-config.js +16 -12
  30. package/dist/esm/build/typed-externals.js +36 -0
  31. package/dist/esm/common/cooldown-store.js +10 -2
  32. package/dist/esm/common/decorator.js +6 -6
  33. package/dist/esm/common/deprecation.js +2 -2
  34. package/dist/esm/common/errors.js +27 -2
  35. package/dist/esm/common/execution-context.js +1 -1
  36. package/dist/esm/common/meocord-text.js +11 -8
  37. package/dist/esm/common/metadata.js +1 -1
  38. package/dist/esm/common/response/call-order.js +9 -4
  39. package/dist/esm/common/response/presenter.js +33 -6
  40. package/dist/esm/common/response/response-state.js +84 -10
  41. package/dist/esm/common/route.js +21 -5
  42. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  43. package/dist/esm/common/translate-error.js +1 -1
  44. package/dist/esm/common/translator.js +37 -6
  45. package/dist/esm/core/command-conflicts.js +34 -34
  46. package/dist/esm/core/command-registration.js +93 -24
  47. package/dist/esm/core/command-routes.js +2 -2
  48. package/dist/esm/core/component-routes.js +74 -31
  49. package/dist/esm/core/cooldown-runner.js +29 -8
  50. package/dist/esm/core/dispatcher.js +51 -7
  51. package/dist/esm/core/fallback.js +20 -11
  52. package/dist/esm/core/filter-runner.js +2 -2
  53. package/dist/esm/core/guard-runner.js +16 -3
  54. package/dist/esm/core/handler-input.js +4 -3
  55. package/dist/esm/core/handler-pipeline.js +27 -14
  56. package/dist/esm/core/handler-registry.js +1 -1
  57. package/dist/esm/core/interceptor-runner.js +2 -2
  58. package/dist/esm/core/lifecycle-hooks.js +134 -47
  59. package/dist/esm/core/meocord-factory.js +241 -179
  60. package/dist/esm/core/meocord.app.js +116 -75
  61. package/dist/esm/core/message-routes.js +4 -3
  62. package/dist/esm/core/observer-runner.js +4 -3
  63. package/dist/esm/core/providers.js +45 -23
  64. package/dist/esm/core/route-specificity.js +4 -4
  65. package/dist/esm/core/running-calls.js +43 -0
  66. package/dist/esm/core/scalar-types.js +15 -3
  67. package/dist/esm/core/shard-context.js +6 -7
  68. package/dist/esm/core/shard-manager.js +16 -10
  69. package/dist/esm/core/startup-roots.js +23 -4
  70. package/dist/esm/core/theme-resolvers.js +4 -1
  71. package/dist/esm/core/theme-runtime.js +2 -2
  72. package/dist/esm/core/theme-scope.js +3 -1
  73. package/dist/esm/core/theme-validation.js +17 -10
  74. package/dist/esm/decorator/app.decorator.js +12 -7
  75. package/dist/esm/decorator/command-builder.decorator.js +4 -4
  76. package/dist/esm/decorator/controller-class.decorator.js +9 -4
  77. package/dist/esm/decorator/controller.decorator.js +162 -83
  78. package/dist/esm/decorator/cooldown.decorator.js +20 -3
  79. package/dist/esm/decorator/defer.decorator.js +4 -4
  80. package/dist/esm/decorator/event.decorator.js +5 -5
  81. package/dist/esm/decorator/filter.decorator.js +7 -7
  82. package/dist/esm/decorator/guard.decorator.js +11 -10
  83. package/dist/esm/decorator/inject.decorator.js +4 -4
  84. package/dist/esm/decorator/interceptor.decorator.js +7 -7
  85. package/dist/esm/decorator/observer.decorator.js +4 -4
  86. package/dist/esm/decorator/service.decorator.js +4 -4
  87. package/dist/esm/decorator/theme.decorator.js +3 -3
  88. package/dist/esm/decorator/validation.decorator.js +10 -10
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +27 -11
  91. package/dist/esm/testing/cooldown-store-suite.js +85 -15
  92. package/dist/esm/testing/discord-defaults.js +249 -21
  93. package/dist/esm/testing/index.js +4 -2
  94. package/dist/esm/testing/inspect-handler.js +20 -3
  95. package/dist/esm/testing/meocord-testing-module.js +352 -205
  96. package/dist/esm/testing/mock-fn.js +173 -22
  97. package/dist/esm/testing/mock-interaction.js +1214 -209
  98. package/dist/esm/testing/response.js +1 -1
  99. package/dist/esm/testing/routing.js +39 -14
  100. package/dist/esm/testing/snowflake.js +9 -2
  101. package/dist/esm/testing/startup-errors.js +26 -0
  102. package/dist/esm/testing/strict-mocks.js +65 -0
  103. package/dist/esm/testing/theme.js +1 -1
  104. package/dist/esm/util/bundle-entry.util.js +2 -1
  105. package/dist/esm/util/common.util.js +47 -19
  106. package/dist/esm/util/dev-runner.util.js +8 -8
  107. package/dist/esm/util/generator-cli.util.js +4 -5
  108. package/dist/esm/util/interaction.util.js +3 -3
  109. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  110. package/dist/esm/util/meocord-config-validation.util.js +4 -5
  111. package/dist/esm/util/meocord-source-config.util.js +65 -20
  112. package/dist/esm/util/metadata-keys.js +1 -0
  113. package/dist/esm/util/method-wrapper.util.js +16 -0
  114. package/dist/esm/util/platform.util.js +10 -10
  115. package/dist/esm/util/refusal.util.js +148 -8
  116. package/dist/esm/util/shard-process.util.js +4 -1
  117. package/dist/esm/util/sharding-mode.util.js +5 -7
  118. package/dist/esm/util/shared-state.util.js +21 -0
  119. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  120. package/dist/esm/util/tsconfig.util.js +10 -10
  121. package/dist/esm/util/value.util.js +12 -1
  122. package/dist/types/common/index.d.cts +8 -8
  123. package/dist/types/common/index.d.ts +8 -8
  124. package/dist/types/core/index.d.cts +10 -7
  125. package/dist/types/core/index.d.ts +10 -7
  126. package/dist/types/decorator/index.d.cts +31 -29
  127. package/dist/types/decorator/index.d.ts +31 -29
  128. package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
  129. package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
  130. package/dist/types/interface/index.d.cts +1 -1
  131. package/dist/types/interface/index.d.ts +1 -1
  132. package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
  133. package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
  134. package/dist/types/testing/index.d.cts +406 -70
  135. package/dist/types/testing/index.d.ts +406 -70
  136. package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  137. package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  138. package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
  139. package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
  140. package/package.json +4 -3
@@ -1,7 +1,7 @@
1
+ import { shared } from '../util/shared-state.util.js';
2
+
1
3
  // Framework-agnostic mock function, so `meocord/testing` depends on neither jest nor vitest.
2
4
  // Both detect it through `_isMockFunction` and read `fn.mock.calls` in their assertions.
3
- // Each call signature of `T`, up to four, as a union: `Parameters` and `ReturnType` read only an overloaded function's
4
- // last signature, such as the list form of a manager's `fetch`
5
5
  /**
6
6
  * Tells whether a value is a mock function: one from `meocord/testing`, `jest.fn()` or `vi.fn()`.
7
7
  *
@@ -24,15 +24,90 @@
24
24
  */ function isMockFunction(fn) {
25
25
  return typeof fn === 'function' && '_isMockFunction' in fn && fn._isMockFunction === true;
26
26
  }
27
- // Every mock createMockFn made, held weakly so the mocks of a finished test can still be collected
28
- const created = new Set();
29
- const collected = new FinalizationRegistry((ref)=>created.delete(ref));
27
+ /**
28
+ * What `useMockFn` set and the mocks made, kept for both builds of this meocord version, so a setup file in one module
29
+ * format sets them for specs in the other:
30
+ * - `invocationOrder`: when each call of meocord's own mocks came, across all of them, as Vitest numbers its own;
31
+ * - `created`: every mock createMockFn made, held weakly so the mocks of a finished test can still be collected;
32
+ * - `runnerFactory` and `restoreImpl`: the runner's factory useMockFn registered, and for each of its mocks how to
33
+ * give back the implementation it was created with, through the runner's own setter, which a mock's behaviour may wrap.
34
+ */ const mocks = shared('mocks', ()=>({
35
+ invocationOrder: 0,
36
+ created: new Set(),
37
+ runnerFactory: undefined,
38
+ restoreImpl: new WeakMap()
39
+ }));
40
+ const collected = new FinalizationRegistry((ref)=>mocks.created.delete(ref));
41
+ /**
42
+ * Makes every mock `meocord/testing` creates with the test runner's own mock function, such as Vitest's `vi.fn`.
43
+ *
44
+ * Call it once in the runner's setup file. The runner then treats meocord's mocks as its own: its `clearMocks` and
45
+ * `mockReset` config and `vi.clearAllMocks()` reach them, its matchers read them, and its whole mock API is there.
46
+ * Without it, mocks are {@link createMockFn}'s own, which jest's and Vitest's `expect` read too.
47
+ *
48
+ * @remarks
49
+ * Each mock starts with the runner's mock holding meocord's behaviour, such as an interaction's replies, as its own
50
+ * implementation. {@link resetAllMocks} puts that behaviour back where the runner's `mockReset` drops it, as jest's and
51
+ * bun's do; a runner's own reset-all, such as jest's `resetMocks`, can't, so under jest and bun reset with
52
+ * {@link resetAllMocks}. node:test's `mock.fn` records calls in a shape of its own, so under node:test leave meocord's
53
+ * own function in place. Calling it again with the same factory does nothing; another, once a mock exists, throws.
54
+ *
55
+ * @param factory - The runner's mock function factory: `vi.fn`, `jest.fn` or bun's `mock`.
56
+ * @throws Error when a mock already exists from another factory, or when `factory` doesn't make a mock with jest's and
57
+ * Vitest's API.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // vitest.setup.ts
62
+ * import { vi } from 'vitest'
63
+ *
64
+ * useMockFn(vi.fn)
65
+ * ```
66
+ *
67
+ * @group Testing
68
+ * @category Mocks
69
+ * @see {@link createMockFn}
70
+ */ function useMockFn(factory) {
71
+ if (factory === mocks.runnerFactory) return;
72
+ if ([
73
+ ...mocks.created
74
+ ].some((ref)=>ref.deref() !== undefined)) {
75
+ throw new Error("useMockFn() goes before any mock is made: a mock made before it keeps meocord's own function, and the two kinds " + "would mix. Call it once, in the test runner's setup file.");
76
+ }
77
+ const refusal = (cause)=>new Error("useMockFn() takes a function that makes mocks with jest's and Vitest's API, such as vi.fn, jest.fn or bun's " + "mock. node:test's mock.fn records calls in its own shape, so under node:test leave meocord's own mock function " + 'in place.', cause === undefined ? undefined : {
78
+ cause
79
+ });
80
+ let probe;
81
+ try {
82
+ probe = factory();
83
+ } catch (error) {
84
+ // node:test's mock.fn, given on its own, throws for want of its tracker
85
+ throw refusal(error);
86
+ }
87
+ const api = [
88
+ 'mockImplementation',
89
+ 'mockClear',
90
+ 'mockReset'
91
+ ];
92
+ if (!isMockFunction(probe) || !Array.isArray(probe.mock?.calls) || api.some((name)=>typeof probe[name] !== 'function')) {
93
+ throw refusal();
94
+ }
95
+ mocks.runnerFactory = factory;
96
+ }
97
+ /** Whether mocks are made with a runner's function, which {@link useMockFn} registered. */ const usesRunnerMockFn = ()=>mocks.runnerFactory !== undefined;
98
+ /** Tracks a mock for clearAllMocks and resetAllMocks. */ function track(mockFn) {
99
+ const ref = new WeakRef(mockFn);
100
+ mocks.created.add(ref);
101
+ collected.register(mockFn, ref);
102
+ return mockFn;
103
+ }
30
104
  /**
31
105
  * Clears the calls every mock from `meocord/testing` has recorded, keeping what each was told to do.
32
106
  *
33
- * Vitest's `clearMocks` and jest's `clearAllMocks()` reach only their own `vi.fn()` and `jest.fn()` mocks. This one
34
- * covers `createMockFn` and everything built on it, such as `createMockInteraction` and `createMockClient`. To also
35
- * undo what a test set, use {@link resetAllMocks}.
107
+ * Vitest's `clearMocks` and jest's `clearAllMocks()` reach only their own `vi.fn()` and `jest.fn()` mocks, and
108
+ * meocord's too once {@link useMockFn} makes them with the runner's function. This one covers `createMockFn` and
109
+ * everything built on it, such as `createMockInteraction` and `createMockClient`, either way. To also undo what a test
110
+ * set, use {@link resetAllMocks}.
36
111
  *
37
112
  * @example
38
113
  * ```ts
@@ -45,7 +120,7 @@ const collected = new FinalizationRegistry((ref)=>created.delete(ref));
45
120
  * @category Mocks
46
121
  * @see {@link resetAllMocks}
47
122
  */ function clearAllMocks() {
48
- for (const ref of created)ref.deref()?.mockClear();
123
+ for (const ref of mocks.created)ref.deref()?.mockClear();
49
124
  }
50
125
  /**
51
126
  * Clears every mock from `meocord/testing`, and puts each back to the implementation it was created with.
@@ -68,18 +143,33 @@ const collected = new FinalizationRegistry((ref)=>created.delete(ref));
68
143
  * @category Mocks
69
144
  * @see {@link clearAllMocks}
70
145
  */ function resetAllMocks() {
71
- for (const ref of created)ref.deref()?.mockReset();
146
+ for (const ref of mocks.created){
147
+ const mockFn = ref.deref();
148
+ if (!mockFn) continue;
149
+ mockFn.mockReset();
150
+ // A runner whose reset drops the implementation the mock was created with, as jest's and bun's do, gets it back
151
+ mocks.restoreImpl.get(mockFn)?.();
152
+ }
72
153
  }
73
154
  /**
74
155
  * Creates a mock function that jest's and Vitest's `expect` both read.
75
156
  *
76
157
  * Use it for a function double in code that must not depend on one test runner, such as a shared test helper; in a
77
- * Vitest test, `vi.fn()` works as well. {@link clearAllMocks} and {@link resetAllMocks} reach it.
158
+ * Vitest test, `vi.fn()` works as well. {@link clearAllMocks} and {@link resetAllMocks} reach it. Once
159
+ * {@link useMockFn} registers a runner's function, it returns that runner's mock.
78
160
  *
79
161
  * @remarks
80
162
  * `mockReturnValue`, `mockResolvedValue`, `mockRejectedValue` and `mockImplementation` share one implementation, so
81
163
  * the last one set wins. The `*Once` variants queue, and run first, in the order they were set.
82
164
  *
165
+ * It records what Vitest's `toHaveResolved` and `toHaveBeenCalledBefore` read as well. Its call order is counted
166
+ * across meocord's own mocks, apart from the runner's, so compare the order of two of meocord's mocks, or of two of the
167
+ * runner's, not one of each.
168
+ *
169
+ * A promise the implementation returns is handed back as one that settles the same way, not as the same object: test
170
+ * it with `toEqual`, `toHaveReturnedWith` or by awaiting it, not with `toBe`. A property set on the given promise, such
171
+ * as a `cancel` method, isn't carried over.
172
+ *
83
173
  * @param impl - The implementation to run until another is set.
84
174
  *
85
175
  * @example
@@ -97,6 +187,15 @@ const collected = new FinalizationRegistry((ref)=>created.delete(ref));
97
187
  * @category Mocks
98
188
  * @see {@link MockInstance}
99
189
  */ function createMockFn(impl) {
190
+ const { runnerFactory } = mocks;
191
+ if (runnerFactory) {
192
+ const runnerMock = runnerFactory(impl);
193
+ if (impl) {
194
+ const setImpl = runnerMock.mockImplementation.bind(runnerMock);
195
+ mocks.restoreImpl.set(runnerMock, ()=>setImpl(impl));
196
+ }
197
+ return track(runnerMock);
198
+ }
100
199
  // One persistent implementation and one queue of single-use ones, as in jest and vitest: the
101
200
  // four setters share the slot, so the last call wins, and the `*Once` variants share the queue,
102
201
  // so they run in declaration order.
@@ -106,9 +205,20 @@ const collected = new FinalizationRegistry((ref)=>created.delete(ref));
106
205
  const calls = [];
107
206
  const results = [];
108
207
  const instances = [];
208
+ const settledResults = [];
209
+ const invocationCallOrder = [];
210
+ const contexts = [];
109
211
  const mockFn = function(...args) {
110
212
  calls.push(args);
111
213
  instances.push(this);
214
+ contexts.push(this);
215
+ invocationCallOrder.push(++mocks.invocationOrder);
216
+ // Updated in place as the call settles, so a promise's outcome shows once it resolves or rejects
217
+ const settled = {
218
+ type: 'incomplete',
219
+ value: undefined
220
+ };
221
+ settledResults.push(settled);
112
222
  let type = 'return';
113
223
  let value;
114
224
  try {
@@ -119,10 +229,36 @@ const collected = new FinalizationRegistry((ref)=>created.delete(ref));
119
229
  } else {
120
230
  value = undefined;
121
231
  }
232
+ if (value instanceof Promise) {
233
+ // Returned in its place, so a rejection nobody awaits stays unhandled in the test, as it is on a bot: a handler
234
+ // on the implementation's own promise would mark it handled
235
+ value = value.then((resolved)=>{
236
+ Object.assign(settled, {
237
+ type: 'fulfilled',
238
+ value: resolved
239
+ });
240
+ return resolved;
241
+ }, (rejected)=>{
242
+ Object.assign(settled, {
243
+ type: 'rejected',
244
+ value: rejected
245
+ });
246
+ throw rejected;
247
+ });
248
+ } else {
249
+ Object.assign(settled, {
250
+ type: 'fulfilled',
251
+ value
252
+ });
253
+ }
122
254
  return value;
123
255
  } catch (err) {
124
256
  type = 'throw';
125
257
  value = err;
258
+ Object.assign(settled, {
259
+ type: 'rejected',
260
+ value: err
261
+ });
126
262
  throw err;
127
263
  } finally{
128
264
  results.push({
@@ -150,6 +286,18 @@ const collected = new FinalizationRegistry((ref)=>created.delete(ref));
150
286
  return calls.length > 0 ? calls[calls.length - 1] : undefined;
151
287
  }
152
288
  };
289
+ // Out of the mock's keys, so a test comparing `mock` as a whole sees the shape it always had
290
+ Object.defineProperties(mock, {
291
+ settledResults: {
292
+ get: ()=>settledResults
293
+ },
294
+ invocationCallOrder: {
295
+ get: ()=>invocationCallOrder
296
+ },
297
+ contexts: {
298
+ get: ()=>contexts
299
+ }
300
+ });
153
301
  Object.defineProperty(mockFn, 'mock', {
154
302
  value: mock,
155
303
  enumerable: false
@@ -186,16 +334,22 @@ const collected = new FinalizationRegistry((ref)=>created.delete(ref));
186
334
  onceQueue.push(fn);
187
335
  return mockFn;
188
336
  };
337
+ const forgetCalls = ()=>{
338
+ for (const record of [
339
+ calls,
340
+ results,
341
+ instances,
342
+ settledResults,
343
+ invocationCallOrder,
344
+ contexts
345
+ ])record.length = 0;
346
+ };
189
347
  mockFn.mockClear = ()=>{
190
- calls.length = 0;
191
- results.length = 0;
192
- instances.length = 0;
348
+ forgetCalls();
193
349
  return mockFn;
194
350
  };
195
351
  mockFn.mockReset = ()=>{
196
- calls.length = 0;
197
- results.length = 0;
198
- instances.length = 0;
352
+ forgetCalls();
199
353
  onceQueue = [];
200
354
  currentImpl = impl;
201
355
  return mockFn;
@@ -209,10 +363,7 @@ const collected = new FinalizationRegistry((ref)=>created.delete(ref));
209
363
  name = n;
210
364
  return mockFn;
211
365
  };
212
- const ref = new WeakRef(mockFn);
213
- created.add(ref);
214
- collected.register(mockFn, ref);
215
- return mockFn;
366
+ return track(mockFn);
216
367
  }
217
368
 
218
- export { clearAllMocks, createMockFn, isMockFunction, resetAllMocks };
369
+ export { clearAllMocks, createMockFn, isMockFunction, resetAllMocks, useMockFn, usesRunnerMockFn };