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
@@ -27,6 +27,7 @@ var chalk__default = /*#__PURE__*/_interopDefaultCompat(chalk);
27
27
 
28
28
  /** Where the built bundle's pre-entry records the bundle's own path. */ const BUNDLE_ENTRY_KEY = Symbol.for('meocord.bundleEntry');
29
29
  /** Where the built bundle's pre-entry records the mode the bundle was built in. */ const BUILD_MODE_KEY = Symbol.for('meocord.buildMode');
30
+ /** Where the built bot's pre-entry records `startupErrors` from its config, before the application's modules load. */ const STARTUP_ERRORS_KEY = Symbol.for('meocord.startupErrorsMode');
30
31
  /**
31
32
  * The file a shard process runs: the built bundle, as its pre-entry recorded it, else the script this
32
33
  * process was started with; undefined when there is none, as for code run with `node -e` outside a built bundle.
@@ -79,12 +80,20 @@ let problem;
79
80
  const reason = (problem.error instanceof Error ? problem.error.message : String(problem.error)).replace(/\.$/, '');
80
81
  const missing = missingPackage(problem.error);
81
82
  const neededBy = missing?.importer ? `, which ${missing.importer} imports,` : '';
82
- const fix = missing ? `Install ${missing.name}${neededBy} in the project` : 'Fix meocord.config.ts';
83
- return `MeoCord config at ${problem.path} failed to load: ${reason}. ${fix}, then run \`meocord build\`.`;
83
+ const fix = missing ? `Install ${missing.name}${neededBy} in the project, then run \`meocord build\`.` : fixedInConfig(problem.error) ? 'Fix meocord.config.ts, then run `meocord build`.' : 'It threw as it loaded: check the environment it reads, such as its .env files.';
84
+ return `MeoCord config at ${problem.path} failed to load: ${reason}. ${fix}`;
84
85
  }
85
86
  const where = problem?.path ?? 'meocord.config.mjs';
86
87
  return `MeoCord config not found at ${where} (working directory ${process.cwd()}). Run \`meocord build\`, and start the bot from the dist it writes.`;
87
88
  }
89
+ /**
90
+ * Whether the config's file or imports are at fault, which a rebuild after fixing it mends: it could not be read as a
91
+ * module, or imports a file that isn't there. Otherwise its code threw as it ran.
92
+ */ function fixedInConfig(error) {
93
+ const code = error?.code;
94
+ if (error instanceof SyntaxError || code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;
95
+ return typeof code === 'string' && code.startsWith('ERR_REQUIRE_');
96
+ }
88
97
  /**
89
98
  * The package a module-not-found error names, when it is a package rather than a file: `dotenv` for `dotenv/config`,
90
99
  * `@scope/name` for `@scope/name/sub`, with the installed package that imports it, if one does. Undefined for any other
@@ -119,7 +128,7 @@ let problem;
119
128
  try {
120
129
  // Called through a variable so a bundler does not try to resolve the path while building the
121
130
  // application; the file is only there once the build is done.
122
- const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/controller.decorator-BywwkU9O.cjs', document.baseURI).href)));
131
+ const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/controller.decorator-XKMlcUZz.cjs', document.baseURI).href)));
123
132
  const loaded = load(compiledPath);
124
133
  return loaded.default ?? loaded;
125
134
  } catch (error) {
@@ -352,7 +361,7 @@ const warned = new Set();
352
361
  */ function warnPlaceholder(logger, value, outcome, how) {
353
362
  warnOnce(logger, `${value} reads a placeholder here; in the next major version (5.0) ${outcome}. ${how}`);
354
363
  }
355
- function warnOnce(logger, text) {
364
+ /** Logs `text` once per run, however often it is asked for. */ function warnOnce(logger, text) {
356
365
  if (warned.has(text)) return;
357
366
  warned.add(text);
358
367
  logger.warn(text);
@@ -394,7 +403,30 @@ function warnOnce(logger, text) {
394
403
  }
395
404
  });
396
405
 
397
- const scope = new node_async_hooks.AsyncLocalStorage();
406
+ var version = "4.2.1";
407
+ var packageJson = {
408
+ version: version};
409
+
410
+ /**
411
+ * The name of a key the ES module and CommonJS builds of this meocord version share, and no other version has, for
412
+ * `Symbol.for`: a process that loads both builds of one install keeps one state under it, and two installed versions
413
+ * keep theirs apart. `Symbol.for(sharedName(...))` written out types as a `unique symbol`.
414
+ */ const sharedName = (name)=>`meocord@${packageJson.version}:${name}`;
415
+ /** The key named {@link sharedName}. */ function sharedKey(name) {
416
+ return Symbol.for(sharedName(name));
417
+ }
418
+ /** The state both builds of this version keep on `globalThis` under `name`, made by whichever asks first. */ function shared(name, make) {
419
+ const key = sharedKey(name);
420
+ const store = globalThis;
421
+ if (!Object.hasOwn(store, key)) Object.defineProperty(store, key, {
422
+ value: make(),
423
+ configurable: true
424
+ });
425
+ return store[key];
426
+ }
427
+
428
+ // One per process for this meocord version, so a call's theme reads the same from its ES module and CommonJS builds
429
+ const scope = shared('themeScope', ()=>new node_async_hooks.AsyncLocalStorage());
398
430
  const isPlainObject$1 = (value)=>{
399
431
  if (value === null || typeof value !== 'object') return false;
400
432
  const prototype = Object.getPrototypeOf(value);
@@ -692,10 +724,10 @@ const REFUSAL = Symbol.for('meocord.refusal');
692
724
  // A stack frame's function and file: `at fn (file:line:col)` or `at file:line:col`, the file possibly a file:// URL
693
725
  const FRAME = /^\s*at (?:(.*?) \()?(.+?):\d+:\d+\)?$/;
694
726
  /**
695
- * A refusal as the application reports it: the message, and the first file of the application's own source the stack
696
- * passes through, other than its entry, which only loads the rest. The file alone: a line from a development build's
697
- * source map can be off, and a compiler's decorate helper can be placed in any file, so those frames are skipped.
698
- */ function describeRefusal(error, root, windows = process.platform === 'win32') {
727
+ * The first file of the application's own source an error's stack passes through, other than its entry, which only
728
+ * loads the rest, relative to `root`. The file alone: a line from a development build's source map can be off, and a
729
+ * compiler's decorate helper can be placed in any file, so those frames are skipped.
730
+ */ function sourceFileOf(error, root, windows = process.platform === 'win32') {
699
731
  const paths = windows ? path__default.default.win32 : path__default.default.posix;
700
732
  const source = comparablePath(paths.join(root, 'src') + paths.sep, windows);
701
733
  const entry = comparablePath(paths.join(root, 'src', 'main.ts'), windows);
@@ -706,9 +738,149 @@ const FRAME = /^\s*at (?:(.*?) \()?(.+?):\d+:\d+\)?$/;
706
738
  const file = paths.normalize(framePath(name, windows));
707
739
  const compared = comparablePath(file, windows);
708
740
  if (!compared.startsWith(source) || compared === entry || compared.includes(dependencies)) continue;
709
- return `${error.message}\n in ${paths.relative(root, file).split(paths.sep).join('/')}`;
741
+ return paths.relative(root, file).split(paths.sep).join('/');
710
742
  }
711
- return error.message;
743
+ return undefined;
744
+ }
745
+ /**
746
+ * A refusal as the application reports it: the message, led by the declaration it is about where the message does not
747
+ * name it, and the file of the application's source it comes from; see {@link sourceFileOf}.
748
+ */ function describeRefusal(error, root, windows = process.platform === 'win32') {
749
+ const declaration = declarationOf(error);
750
+ const message = declaration && !error.message.startsWith(declaration) ? `${declaration}: ${error.message}` : error.message;
751
+ const file = sourceFileOf(error, root, windows);
752
+ return file ? `${message}\n in ${file}` : message;
753
+ }
754
+ // ---------------------------------------------------------------------------
755
+ // Startup errors: those create() and a testing module's compile() find, and those decorators find at import
756
+ // ---------------------------------------------------------------------------
757
+ const STARTUP_ERRORS = Symbol.for('meocord.startupErrors');
758
+ const DECLARED = Symbol.for('meocord.declaredStartupErrors');
759
+ const ALL_DECLARED = Symbol.for('meocord.allDeclaredStartupErrors');
760
+ // The startup errors kept while checks run under collectStartupErrors; undefined outside them
761
+ let kept;
762
+ /**
763
+ * Reports an error a startup check finds. Outside {@link collectStartupErrors} it is thrown, as a refusal; inside, it
764
+ * is kept, and the check goes on to find the rest.
765
+ */ function startupError(error) {
766
+ refuse(error);
767
+ if (!kept) throw error;
768
+ // Two checks of one mistake, such as a class needing a translator, name it once
769
+ if (!kept.some((other)=>other.message === error.message)) kept.push(error);
770
+ }
771
+ /**
772
+ * Runs startup checks, and once they are done throws the first error they found, unchanged, with every one found on it
773
+ * for {@link startupErrorsOf}. An error that ends the checks after one was found is left out, as one the found error
774
+ * caused; one that ends them before any was found is thrown as it is.
775
+ */ function collectStartupErrors(checks) {
776
+ const outer = kept;
777
+ const found = [];
778
+ kept = found;
779
+ let result;
780
+ try {
781
+ result = checks();
782
+ } catch (error) {
783
+ if (found.length === 0) throw error;
784
+ if (isRefusal(error) && !found.includes(error)) found.push(error);
785
+ throw firstOf(found);
786
+ } finally{
787
+ kept = outer;
788
+ }
789
+ if (found.length > 0) throw firstOf(found);
790
+ return result;
791
+ }
792
+ /**
793
+ * Ends the checks under way with the errors found so far, before a step that is no check, such as binding or a warning:
794
+ * a run with one error stops where it stopped when the first error was thrown, and the errors of one pass of the
795
+ * checks are still reported together.
796
+ */ function stopOnStartupErrors() {
797
+ if (kept && kept.length > 0) throw kept[0];
798
+ }
799
+ function firstOf(errors) {
800
+ const [first] = errors;
801
+ Object.defineProperty(first, STARTUP_ERRORS, {
802
+ value: errors,
803
+ configurable: true
804
+ });
805
+ return first;
806
+ }
807
+ /** Every startup error found together with `error`, the first of them; `error` alone when it was the only one. */ function startupErrorsOf(error) {
808
+ return error[STARTUP_ERRORS] ?? [
809
+ error
810
+ ];
811
+ }
812
+ /**
813
+ * Logs each startup error found with `error` as the application reports a refusal, and how many there were, when there
814
+ * was more than one; a lone error is left to whoever reports it now.
815
+ */ function logStartupErrors(error, log, ending) {
816
+ const errors = startupErrorsOf(error);
817
+ if (errors.length < 2) return false;
818
+ for (const each of errors)log(describeRefusal(each, process.cwd()));
819
+ log(`MeoCord found ${errors.length} startup errors; ${ending}.`);
820
+ return true;
821
+ }
822
+ /** Sets how decorators report startup errors from here on, before the application's modules load. */ function setStartupErrorsMode(mode) {
823
+ globalThis[STARTUP_ERRORS_KEY] = mode;
824
+ }
825
+ /** Whether decorators keep their startup errors for create(), as `startupErrors: 'all'` has them. */ const keepsDeclaredErrors = ()=>globalThis[STARTUP_ERRORS_KEY] === 'all';
826
+ /** The declaration a decorator's startup error is about, `Class.method` or `Class`. */ function declarationOf(error) {
827
+ return error.declaration;
828
+ }
829
+ /**
830
+ * A decorator that names, on each startup error it throws, the declaration it was applied to, `Class.method` or
831
+ * `Class`, as `declaration`, and the source file it is declared in, as `file`: the same error, thrown at the same
832
+ * moment, with its message unchanged. With `startupErrors: 'all'` the error is kept on the class instead, for create()
833
+ * to report with the rest, and the decorator applies nothing more.
834
+ */ function declaring(decorate) {
835
+ return function(target, propertyKey, ...rest) {
836
+ try {
837
+ return decorate.call(this, target, propertyKey, ...rest);
838
+ } catch (error) {
839
+ if (!isRefusal(error)) throw error;
840
+ keepOrThrow(error, target, propertyKey);
841
+ return undefined;
842
+ }
843
+ };
844
+ }
845
+ function keepOrThrow(error, target, propertyKey) {
846
+ var _globalThis, _ALL_DECLARED;
847
+ // A decorator applied through another, such as applyDecorators, names the declaration once, where it was first thrown
848
+ if (declarationOf(error) === undefined) {
849
+ Object.defineProperty(error, 'declaration', {
850
+ value: decoratedName(target, propertyKey),
851
+ configurable: true
852
+ });
853
+ Object.defineProperty(error, 'file', {
854
+ get: ()=>sourceFileOf(error, process.cwd()),
855
+ configurable: true
856
+ });
857
+ }
858
+ if (!keepsDeclaredErrors()) throw error;
859
+ const owner = typeof target === 'function' ? target : target.constructor;
860
+ if (!Object.prototype.hasOwnProperty.call(owner, DECLARED)) Object.defineProperty(owner, DECLARED, {
861
+ value: []
862
+ });
863
+ const declared = owner[DECLARED];
864
+ if (!declared.includes(error)) declared.push(error);
865
+ const all = (_globalThis = globalThis)[_ALL_DECLARED = ALL_DECLARED] ?? (_globalThis[_ALL_DECLARED] = []);
866
+ if (!all.includes(error)) all.push(error);
867
+ }
868
+ /** The startup errors decorators kept on `classes` and the classes they extend, each once, in the order they were found. */ function declaredErrorsOf(classes) {
869
+ const found = new Set();
870
+ for (const cls of classes){
871
+ for(let current = cls; typeof current === 'function' && current !== Function.prototype; current = Object.getPrototypeOf(current)){
872
+ if (Object.prototype.hasOwnProperty.call(current, DECLARED)) for (const error of current[DECLARED])found.add(error);
873
+ }
874
+ }
875
+ const order = allDeclaredErrors();
876
+ return [
877
+ ...found
878
+ ].sort((a, b)=>order.indexOf(a) - order.indexOf(b));
879
+ }
880
+ /** Every startup error decorators kept in this process, in the order they were found. */ function allDeclaredErrors() {
881
+ return [
882
+ ...globalThis[ALL_DECLARED] ?? []
883
+ ];
712
884
  }
713
885
  /**
714
886
  * Reports a refusal nothing catches as one line, with its source file, and exits 1; other errors keep the runtime's
@@ -736,6 +908,38 @@ const FRAME = /^\s*at (?:(.*?) \()?(.+?):\d+:\d+\)?$/;
736
908
  process.on('unhandledRejection', onRejection);
737
909
  }
738
910
 
911
+ /** Characters a log line shows escaped: C0 and C1 controls, DEL, line and paragraph separators, and bidi controls. */ const UNSAFE_IN_LOG = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
912
+ /** The same, with the quote and the backslash, for text shown between quotes. */ const UNSAFE_IN_QUOTES = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069"\\]/g;
913
+ const SHORT_ESCAPES = {
914
+ '\n': '\\n',
915
+ '\r': '\\r',
916
+ '\t': '\\t',
917
+ '"': '\\"',
918
+ '\\': '\\\\'
919
+ };
920
+ const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`;
921
+ /** Text for a log line, on one line: its control characters escaped, so it can't start a line of its own. */ function escapeForLog(text) {
922
+ return text.replace(UNSAFE_IN_LOG, escapeChar);
923
+ }
924
+ /** How many characters of a user's text a log line quotes before cutting it short. */ const LOGGED_CHARACTERS = 200;
925
+ /**
926
+ * A user's text as a log line quotes it: escaped as {@link escapeForLog} does, with its quotes and backslashes too,
927
+ * and cut short after 200 characters, with its length.
928
+ */ function quoteForLog(text) {
929
+ const characters = [
930
+ ...text
931
+ ];
932
+ const quoted = (shown)=>`"${shown.replace(UNSAFE_IN_QUOTES, escapeChar)}"`;
933
+ if (characters.length <= LOGGED_CHARACTERS) return quoted(text);
934
+ return `${quoted(`${characters.slice(0, LOGGED_CHARACTERS).join('')}…`)} (${characters.length} characters)`;
935
+ }
936
+ /** Words a user wrote, as a reply quotes them: on one line, with their markdown, masked links included, shown as typed. */ function userWords(text) {
937
+ return discord_js.escapeMarkdown(text.replace(/\s+/g, ' ').trim(), {
938
+ maskedLink: true
939
+ });
940
+ }
941
+ /** Items as a sentence lists them: `a`, `a and b`, `a, b and c`. */ const listed = (items)=>items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`;
942
+
739
943
  /**
740
944
  * The role names MeoCord keeps for roles it may add to `colors`, `emojis` and `buttons`: the runtime copy of
741
945
  * `ReservedThemeRole`, which refuses them in TypeScript at the root theme, so that a JavaScript app is told too. A type
@@ -760,9 +964,10 @@ const EMOJI = 'a unicode emoji, or a custom one written <:name:id> or <a:name:id
760
964
  const BUTTON = 'ButtonStyle.Primary, Secondary, Success or Danger';
761
965
  /** A custom emoji as Discord writes one in text: its name, 2 to 32 word characters, and its id. */ const CUSTOM_EMOJI$1 = /^<a?:\w{2,32}:\d{17,20}>$/;
762
966
  /**
763
- * One unicode emoji: a flag of two regional indicators, a keycap, or a pictograph with an optional presentation
764
- * selector or skin tone, joined to others by zero-width joiners into one sequence.
765
- */ const UNICODE_EMOJI = /^(?:\p{Regional_Indicator}{2}|[#*0-9]️?⃣|\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})?(?:‍\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})?)*)$/u;
967
+ * One unicode emoji: a flag of two regional indicators, a subdivision flag (a black flag, tag characters and a cancel
968
+ * tag, as England's), a keycap, or a pictograph with an optional presentation selector or skin tone, joined to others by
969
+ * zero-width joiners into one sequence.
970
+ */ const UNICODE_EMOJI = /^(?:\p{Regional_Indicator}{2}|\u{1F3F4}[\u{E0020}-\u{E007E}]+\u{E007F}|[#*0-9]️?⃣|\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})?(?:‍\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})?)*)$/u;
766
971
  const byte = (value)=>Number.isInteger(value) && value >= 0 && value <= 255;
767
972
  /** Whether discord.js resolves the value to a colour Discord takes. Stricter on tuples, whose channels it only sums. */ function isColour(value) {
768
973
  if (typeof value === 'string') return /^#?[\da-f]{6}$/i.test(value) || value === 'Random' || value === 'Default' || Object.hasOwn(discord_js.Colors, value);
@@ -777,8 +982,9 @@ const THEMED_BUTTON_STYLES = [
777
982
  discord_js.ButtonStyle.Danger
778
983
  ];
779
984
  const isButtonStyle = (value)=>THEMED_BUTTON_STYLES.includes(value);
780
- const describe = (value)=>typeof value === 'string' ? `'${value}'` : Array.isArray(value) ? `[${value.map((item)=>typeof item === 'string' ? `'${item}'` : String(item)).join(', ')}]` : value === null ? 'null' : typeof value === 'object' ? 'an object' : String(value);
781
- const describeGroup = (value)=>Array.isArray(value) ? 'an array' : describe(value);
985
+ const asWritten = (text)=>`'${text}'`;
986
+ const describe = (value, quote = asWritten)=>typeof value === 'string' ? quote(value) : Array.isArray(value) ? `[${value.map((item)=>typeof item === 'string' ? quote(item) : String(item)).join(', ')}]` : value === null ? 'null' : typeof value === 'object' ? 'an object' : String(value);
987
+ const describeGroup = (value, quote = asWritten)=>Array.isArray(value) ? 'an array' : describe(value, quote);
782
988
  /** Whether a value is a plain object, as an object literal or `JSON.parse` makes: one a theme merges key by key. */ const isPlainObject = (value)=>{
783
989
  const prototype = Object.getPrototypeOf(value);
784
990
  return prototype === Object.prototype || prototype === null;
@@ -810,11 +1016,15 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
810
1016
  * roles an app added, a colour discord.js cannot resolve, an emoji Discord would refuse, a button style other than
811
1017
  * Discord's four coloured ones, or a reserved role name. A group of the app's own is its to check.
812
1018
  * @param where - Where it was set, such as `themeFor.guild for guild 123`, to begin each line with.
813
- */ function themeProblems(theme, where) {
1019
+ * @param options.forLog - Shows values and role names as a log line shows text from outside the code, for a theme a
1020
+ * `themeFor` resolver returned: escaped, and a value quoted and cut short.
1021
+ */ function themeProblems(theme, where, { forLog = false } = {}) {
814
1022
  const at = where ? `${where}: ` : '';
1023
+ const quote = forLog ? quoteForLog : asWritten;
1024
+ const name = forLog ? escapeForLog : (role)=>role;
815
1025
  if (typeof theme !== 'object' || theme === null || Array.isArray(theme)) {
816
1026
  return [
817
- `${at}theme must be an object of groups (got ${describeGroup(theme)})`
1027
+ `${at}theme must be an object of groups (got ${describeGroup(theme, quote)})`
818
1028
  ];
819
1029
  }
820
1030
  if (!isPlainObject(theme)) return [
@@ -826,7 +1036,7 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
826
1036
  const roles = theme[group];
827
1037
  if (roles === undefined) continue;
828
1038
  if (typeof roles !== 'object' || roles === null || Array.isArray(roles)) {
829
- problems.push(`${at}theme.${group} must be an object of roles (got ${describeGroup(roles)})`);
1039
+ problems.push(`${at}theme.${group} must be an object of roles (got ${describeGroup(roles, quote)})`);
830
1040
  continue;
831
1041
  }
832
1042
  if (!isPlainObject(roles)) {
@@ -834,8 +1044,8 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
834
1044
  continue;
835
1045
  }
836
1046
  for (const [role, value] of Object.entries(roles)){
837
- if (reserved.has(role)) problems.push(`${at}theme.${group}.${role}: MeoCord reserves the role name ${role} for a role it may add; rename yours`);
838
- else if (value !== undefined && !check(value)) problems.push(`${at}theme.${group}.${role}: ${describe(value)} is not ${what}: give ${instead}`);
1047
+ if (reserved.has(role)) problems.push(`${at}theme.${group}.${name(role)}: MeoCord reserves the role name ${name(role)} for a role it may add; rename yours`);
1048
+ else if (value !== undefined && !check(value)) problems.push(`${at}theme.${group}.${name(role)}: ${describe(value, quote)} is not ${what}: give ${instead}`);
839
1049
  }
840
1050
  }
841
1051
  return problems;
@@ -902,9 +1112,9 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
902
1112
  *
903
1113
  * @group Utilities
904
1114
  * @see {@link createMetadata}
905
- * @see {@link https://meocord.dev/docs/4.1/custom-decorators | Custom decorators}
1115
+ * @see {@link https://meocord.dev/docs/4.2/custom-decorators | Custom decorators}
906
1116
  */ function applyDecorators(...decorators) {
907
- return function(target, propertyKey, descriptor) {
1117
+ return declaring(function(target, propertyKey, descriptor) {
908
1118
  // Last first, as TypeScript applies decorators stacked, each given what the one before it returned
909
1119
  const ordered = [
910
1120
  ...decorators
@@ -917,7 +1127,7 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
917
1127
  let current = target;
918
1128
  for (const decorator of ordered)current = decorator(current) ?? current;
919
1129
  return current;
920
- };
1130
+ });
921
1131
  }
922
1132
  const logger$5 = new Logger('SetMetadata');
923
1133
  /** The prefix of every key MeoCord keeps its own metadata under. */ const MEOCORD_PREFIX = 'meocord:';
@@ -956,7 +1166,7 @@ const logger$5 = new Logger('SetMetadata');
956
1166
  * @see {@link createMetadata}
957
1167
  */ function SetMetadata(metadataKey, metadataValue) {
958
1168
  warnDeprecated(logger$5, 'SetMetadata', 'createMetadata');
959
- return function(target, propertyKey) {
1169
+ return declaring(function(target, propertyKey) {
960
1170
  // Checked where it applies, so the refusal names the handler or controller
961
1171
  const owner = metadataKey.startsWith(MEOCORD_PREFIX) ? `MeoCord keeps its own metadata under keys beginning "${MEOCORD_PREFIX}"` : INJECTION_KEYS.has(metadataKey) ? 'dependency injection reads how to make the class from it' : undefined;
962
1172
  if (owner) {
@@ -967,7 +1177,7 @@ const logger$5 = new Logger('SetMetadata');
967
1177
  } else {
968
1178
  Reflect.defineMetadata(metadataKey, metadataValue, target);
969
1179
  }
970
- };
1180
+ });
971
1181
  }
972
1182
 
973
1183
  /**
@@ -1029,8 +1239,8 @@ const logger$5 = new Logger('SetMetadata');
1029
1239
  /**
1030
1240
  * Where a translator made by `createTranslator` keeps its catalogs, for MeoCord's texts, `@MeoCord` and
1031
1241
  * `expectCompleteCatalog`.
1032
- */ const CATALOGS = Symbol('catalogs');
1033
- /** How MeoCord's own texts read a translator made by `createTranslator`: the catalogs that serve a locale, in order. */ const LOCALE_CHAIN = Symbol('localeChain');
1242
+ */ const CATALOGS = Symbol.for(sharedName('translator:catalogs'));
1243
+ /** How MeoCord's own texts read a translator made by `createTranslator`: the catalogs that serve a locale, in order. */ const LOCALE_CHAIN = Symbol.for(sharedName('translator:localeChain'));
1034
1244
  /** For a class that injects `Translator` in an app that configured none. */ function missingTranslatorError(cls) {
1035
1245
  return new Error(`${cls.name}: it injects Translator, but @MeoCord has no i18n. Pass @MeoCord({ i18n: t }), where t comes from createTranslator.`);
1036
1246
  }
@@ -1077,6 +1287,22 @@ function interpolate(message, params) {
1077
1287
  }
1078
1288
  return typeof current === 'object' && current !== null;
1079
1289
  }
1290
+ /** The locale Discord shows a locale's users when that locale has no localization of its own, by its locale fallbacks. */ const FALLBACK_PARTNERS = {
1291
+ [discord_js.Locale.EnglishUS]: discord_js.Locale.EnglishGB,
1292
+ [discord_js.Locale.EnglishGB]: discord_js.Locale.EnglishUS,
1293
+ [discord_js.Locale.SpanishLATAM]: discord_js.Locale.SpanishES
1294
+ };
1295
+ /** Each key with a `.` in it, which `lookup` splits as a path and so never reaches, with the group it sits in. */ function dottedKeys(catalog, group = '') {
1296
+ return Object.entries(catalog).flatMap(([name, value])=>[
1297
+ ...name.includes('.') ? [
1298
+ {
1299
+ name,
1300
+ group
1301
+ }
1302
+ ] : [],
1303
+ ...typeof value === 'object' && value !== null ? dottedKeys(value, group ? `${group}.${name}` : name) : []
1304
+ ]);
1305
+ }
1080
1306
  class CatalogTranslator extends Translator {
1081
1307
  /** The catalogs that serve a locale, most specific first, ending with the default. */ [LOCALE_CHAIN](requested) {
1082
1308
  const chain = [];
@@ -1095,10 +1321,13 @@ class CatalogTranslator extends Translator {
1095
1321
  translate(requested, key, params = {}) {
1096
1322
  for (const locale of this[LOCALE_CHAIN](requested)){
1097
1323
  const message = lookup(this.catalogs[locale], key);
1098
- if (message !== undefined) return interpolate(pluralForm({
1324
+ if (message === undefined) continue;
1325
+ // An empty translation, as tools export an untranslated string, counts as missing: Discord refuses an empty reply
1326
+ const text = pluralForm({
1099
1327
  message,
1100
1328
  locale
1101
- }, params.count), params);
1329
+ }, params.count);
1330
+ if (text !== '') return interpolate(text, params);
1102
1331
  }
1103
1332
  this.warnUnknown(key);
1104
1333
  return key;
@@ -1128,11 +1357,16 @@ class CatalogTranslator extends Translator {
1128
1357
  for (const locale of this.locales){
1129
1358
  if (locale === this.defaultLocale) continue;
1130
1359
  const message = lookup(this.catalogs[locale], key);
1131
- // A translation with a {param} is left out: Discord would show it as written
1132
- if (typeof message === 'string' && placeholderNames(message).length === 0) {
1360
+ // A translation with a {param} is left out, as Discord would show it as written, and so is an empty one
1361
+ if (typeof message === 'string' && message !== '' && placeholderNames(message).length === 0) {
1133
1362
  localized[locale] = fillPlaceholders(message, (_name, whole)=>whole);
1134
1363
  }
1135
1364
  }
1365
+ // Discord gives a locale it falls back from its partner's value, so the default keeps its own wording beside one
1366
+ const partner = FALLBACK_PARTNERS[this.defaultLocale];
1367
+ if (partner && localized[partner] !== undefined && typeof original === 'string' && original !== '') {
1368
+ localized[this.defaultLocale] = fillPlaceholders(original, (_name, whole)=>whole);
1369
+ }
1136
1370
  return localized;
1137
1371
  }
1138
1372
  for(interaction, options = {}) {
@@ -1211,6 +1445,12 @@ class CatalogTranslator extends Translator {
1211
1445
  }
1212
1446
  }
1213
1447
  if (!locales[defaultLocale]) throw refuse(new Error(`The default locale "${defaultLocale}" has no catalog in locales.`));
1448
+ const logger = new Logger('Translator');
1449
+ for (const [locale, catalog] of Object.entries(locales)){
1450
+ for (const { name, group } of dottedKeys(catalog)){
1451
+ logger.warn(`The ${locale} catalog's key "${name}"${group ? ` in ${group}` : ''} has a ".", which a lookup reads as a path, so its ` + 'message is never found. Nest it as a group instead.');
1452
+ }
1453
+ }
1214
1454
  // Typed by the default catalog; at runtime every catalog is read the same way.
1215
1455
  return new CatalogTranslator(defaultLocale, locales);
1216
1456
  }
@@ -1315,16 +1555,17 @@ const ENGLISH = 'en-US';
1315
1555
  return english();
1316
1556
  }
1317
1557
  const lists = new Map();
1318
- /** A list in a locale's words: "a, b, or c" in en-US, or, for a unit, "a, b, c". */ function listIn(locale, style, items) {
1319
- let format = lists.get(`${locale} ${style}`);
1320
- if (!format) lists.set(`${locale} ${style}`, format = new Intl.ListFormat(locale, {
1321
- type: style === 'or' ? 'disjunction' : 'unit'
1558
+ /** An `or` list in a locale's words, such as "a, b, or c" in en-US. */ function orListIn(locale, items) {
1559
+ let format = lists.get(locale);
1560
+ if (!format) lists.set(locale, format = new Intl.ListFormat(locale, {
1561
+ type: 'disjunction'
1322
1562
  }));
1323
1563
  return format.format(items);
1324
1564
  }
1325
1565
  /**
1326
- * A text in `locale`, as {@link findText} finds it. A list in MeoCord's English is joined with its `joiner`, a comma
1327
- * unless given; in an app's catalog, in the words of the locale that has it.
1566
+ * A text in `locale`, as {@link findText} finds it. An `or` list in an app's catalog reads in the words of the locale
1567
+ * that has it; any other list is joined with its `joiner`, a comma unless given, as Intl's `unit` lists join with
1568
+ * nothing in zh-CN and with a space in ja.
1328
1569
  */ function renderText(translator, locale, text) {
1329
1570
  const found = findText(translator, locale, text.key);
1330
1571
  if (!found) return text.fallback ?? text.key;
@@ -1333,14 +1574,15 @@ const lists = new Map();
1333
1574
  if (!Object.hasOwn(params, name)) return whole;
1334
1575
  const value = params[name];
1335
1576
  if (typeof value !== 'object') return String(value);
1336
- if ('list' in value) return found.own ? listIn(found.locale, value.style, value.list) : value.list.join(value.joiner ?? ', ');
1577
+ if ('list' in value) return found.own && value.style === 'or' ? orListIn(found.locale, value.list) : value.list.join(value.joiner ?? ', ');
1337
1578
  return renderText(translator, locale, value);
1338
1579
  });
1339
1580
  }
1340
1581
  /** Renders texts in one locale through one translator: a word or number as it is, a text in the locale's words. */ function textRenderer(translator, locale) {
1341
1582
  return (text)=>typeof text !== 'object' ? String(text) : 'list' in text ? text.list.join(text.joiner ?? ', ') : renderText(translator, locale, text);
1342
1583
  }
1343
- const translators = new WeakMap();
1584
+ // Shared by both builds of this version, so either answers in the app's language
1585
+ const translators = shared('clientTranslators', ()=>new WeakMap());
1344
1586
  /** Records the translator of the app a client's interactions and messages come to. */ function registerClientTranslator(client, translator) {
1345
1587
  if (translator) translators.set(client, translator);
1346
1588
  else translators.delete(client);
@@ -1456,7 +1698,7 @@ const issueTexts = new WeakMap();
1456
1698
  * @group Responses
1457
1699
  * @category Errors
1458
1700
  * @see {@link Catch}
1459
- * @see {@link https://meocord.dev/docs/4.1/exception-filters | Exception filters}
1701
+ * @see {@link https://meocord.dev/docs/4.2/exception-filters | Exception filters}
1460
1702
  */ class CommandNotFoundError extends Error {
1461
1703
  /** @param message - What the error says, `No handler matched the interaction.` unless given. */ constructor(message = 'No handler matched the interaction.'){
1462
1704
  super(message);
@@ -1529,7 +1771,7 @@ const issueTexts = new WeakMap();
1529
1771
  * @category Errors
1530
1772
  * @see {@link MessageHandler}
1531
1773
  * @see {@link translateError}
1532
- * @see {@link https://meocord.dev/docs/4.1/message-commands | Message commands}
1774
+ * @see {@link https://meocord.dev/docs/4.2/message-commands | Message commands}
1533
1775
  */ class MessageUsageError extends Error {
1534
1776
  /**
1535
1777
  * @param usage - The command as the user should type it, such as `!ban <target> [reason…]`, or one line per
@@ -1696,6 +1938,30 @@ const issueTexts = new WeakMap();
1696
1938
  this.name = 'CooldownStoreError';
1697
1939
  }
1698
1940
  }
1941
+ /**
1942
+ * Makes `instanceof` hold for an error from either build of this meocord version, ES module or CommonJS: the class's
1943
+ * prototype carries a brand both builds share, which `instanceof` on the class itself checks. A subclass, such as an
1944
+ * app's own `UserError`, keeps the usual check, so it still matches only its own instances.
1945
+ */ function brand(error, name) {
1946
+ const key = sharedKey(`error:${name}`);
1947
+ Object.defineProperty(error.prototype, key, {
1948
+ value: true
1949
+ });
1950
+ Object.defineProperty(error, Symbol.hasInstance, {
1951
+ value (value) {
1952
+ if (this !== error) return Function.prototype[Symbol.hasInstance].call(this, value);
1953
+ return (typeof value === 'object' || typeof value === 'function') && value !== null && key in value;
1954
+ }
1955
+ });
1956
+ }
1957
+ // Named as written, since a minifier may rename the classes themselves
1958
+ brand(GuardDeniedError, 'GuardDeniedError');
1959
+ brand(UserError, 'UserError');
1960
+ brand(CommandNotFoundError, 'CommandNotFoundError');
1961
+ brand(ValidationError, 'ValidationError');
1962
+ brand(MessageUsageError, 'MessageUsageError');
1963
+ brand(CooldownError, 'CooldownError');
1964
+ brand(CooldownStoreError, 'CooldownStoreError');
1699
1965
 
1700
1966
  /** Discord's codes for a body it could not read: invalid form body or JSON, or an empty message. */ const MALFORMED_BODY = new Set([
1701
1967
  discord_js.RESTJSONErrorCodes.InvalidFormBodyOrContentType,
@@ -1712,38 +1978,6 @@ const issueTexts = new WeakMap();
1712
1978
  else logger.error(`Could not ${what}:`, error);
1713
1979
  }
1714
1980
 
1715
- /** Characters a log line shows escaped: C0 and C1 controls, DEL, line and paragraph separators, and bidi controls. */ const UNSAFE_IN_LOG = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
1716
- /** The same, with the quote and the backslash, for text shown between quotes. */ const UNSAFE_IN_QUOTES = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069"\\]/g;
1717
- const SHORT_ESCAPES = {
1718
- '\n': '\\n',
1719
- '\r': '\\r',
1720
- '\t': '\\t',
1721
- '"': '\\"',
1722
- '\\': '\\\\'
1723
- };
1724
- const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`;
1725
- /** Text for a log line, on one line: its control characters escaped, so it can't start a line of its own. */ function escapeForLog(text) {
1726
- return text.replace(UNSAFE_IN_LOG, escapeChar);
1727
- }
1728
- /** How many characters of a user's text a log line quotes before cutting it short. */ const LOGGED_CHARACTERS = 200;
1729
- /**
1730
- * A user's text as a log line quotes it: escaped as {@link escapeForLog} does, with its quotes and backslashes too,
1731
- * and cut short after 200 characters, with its length.
1732
- */ function quoteForLog(text) {
1733
- const characters = [
1734
- ...text
1735
- ];
1736
- const quoted = (shown)=>`"${shown.replace(UNSAFE_IN_QUOTES, escapeChar)}"`;
1737
- if (characters.length <= LOGGED_CHARACTERS) return quoted(text);
1738
- return `${quoted(`${characters.slice(0, LOGGED_CHARACTERS).join('')}…`)} (${characters.length} characters)`;
1739
- }
1740
- /** Words a user wrote, as a reply quotes them: on one line, with their markdown, masked links included, shown as typed. */ function userWords(text) {
1741
- return discord_js.escapeMarkdown(text.replace(/\s+/g, ' ').trim(), {
1742
- maskedLink: true
1743
- });
1744
- }
1745
- /** Items as a sentence lists them: `a`, `a and b`, `a, b and c`. */ const listed = (items)=>items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`;
1746
-
1747
1981
  /** Whether a value is a class, which a stage entry or a `@Catch` type must be: what `new` and `instanceof` take. */ const isConstructor = (value)=>typeof value === 'function';
1748
1982
  /** A value as a refusal or a warning names it: a string quoted, `null` and `undefined` as written, else its kind. */ function describeValue(value) {
1749
1983
  if (value === null || value === undefined) return String(value);
@@ -1752,6 +1986,17 @@ const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toStr
1752
1986
  return typeof value === 'object' ? 'an object' : `a ${typeof value}`;
1753
1987
  }
1754
1988
  /** A class's name after its article, such as `an AutocompleteInteraction`; a `U` name reads as "you", so takes `a`. */ const withArticle = (name)=>`${/^[AEIO]/i.test(name) ? 'an' : 'a'} ${name}`;
1989
+ /**
1990
+ * Sets `key` on `target` as an own, enumerable data property: unlike `target[key] = value`, a key named `__proto__`
1991
+ * is a key like any other rather than the object's prototype.
1992
+ */ function setOwn(target, key, value) {
1993
+ Object.defineProperty(target, key, {
1994
+ value,
1995
+ enumerable: true,
1996
+ writable: true,
1997
+ configurable: true
1998
+ });
1999
+ }
1755
2000
 
1756
2001
  /**
1757
2002
  * The discord.js interaction class each command type handles, shared by `@Command`, the dispatcher and the testing
@@ -1849,7 +2094,7 @@ const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toStr
1849
2094
  walk(option.options ?? []);
1850
2095
  continue;
1851
2096
  }
1852
- params[option.name] = resolveOptionValue(option);
2097
+ setOwn(params, option.name, resolveOptionValue(option));
1853
2098
  }
1854
2099
  };
1855
2100
  walk(data);
@@ -2127,16 +2372,41 @@ function rewriteComponent(component, kept) {
2127
2372
  })
2128
2373
  };
2129
2374
  /** The id MeoCord gives the containers it renders, so a view left behind can be found again. */ const RENDERED_CONTAINER_ID = 0x4d43;
2130
- const presenters = new WeakMap();
2375
+ // Shared by both builds of this version, so either answers with the app's presenter
2376
+ const presenters = shared('clientPresenters', ()=>new WeakMap());
2131
2377
  /** Sets the presenter for the interactions a client receives. */ function setPresenter(client, presenter) {
2132
2378
  presenters.set(client, presenter);
2133
2379
  }
2134
2380
  /** The presenter for an interaction's client, or the default one. */ function presenterFor(client) {
2135
2381
  return client && presenters.get(client) || defaultPresenter;
2136
2382
  }
2137
- function textOf(view) {
2138
- return view.emoji ? `${view.emoji} ${view.text}` : view.text;
2383
+ /** Discord's limits on a message's text, counted by `String.length` as discord.js's builders count them. */ const TEXT_LIMITS = {
2384
+ embed: 4096,
2385
+ v2: 4000,
2386
+ content: 2000
2387
+ };
2388
+ /** `text` cut to `limit` characters ending in `…`, on a code point boundary so no surrogate pair is split. */ function fitText(text, limit) {
2389
+ if (text.length <= limit) return text;
2390
+ let cut = text.slice(0, Math.max(0, limit - 1));
2391
+ const last = cut.charCodeAt(cut.length - 1);
2392
+ if (last >= 0xd800 && last <= 0xdbff) cut = cut.slice(0, -1);
2393
+ return `${cut}…`;
2139
2394
  }
2395
+ /**
2396
+ * An error's message as a presenter is given it: fitted to `limit`, and, when empty or only whitespace, which Discord
2397
+ * refuses as a text, MeoCord's generic error text, so a presenter that writes it as its text always renders.
2398
+ */ function presentedMessage(message, limit, generic) {
2399
+ return fitText(message.trim() === '' ? generic() : message, limit);
2400
+ }
2401
+ /**
2402
+ * The view's text after what MeoCord puts before it, its emoji and a Text Display's title line. When only those take
2403
+ * the two past `limit`, the text is cut so they fit; a text past `limit` by itself is left, a view MeoCord can't render.
2404
+ */ function textAfter(prefix, view, limit) {
2405
+ const total = prefix.length + view.text.length;
2406
+ if (total <= limit || view.text.length > limit || prefix.length >= limit) return prefix + view.text;
2407
+ return prefix + fitText(view.text, limit - prefix.length);
2408
+ }
2409
+ const emojiOf = (view)=>view.emoji ? `${view.emoji} ` : '';
2140
2410
  /** Discord's limit of attachments on one message. */ const ATTACHMENT_LIMIT = 10;
2141
2411
  /** The most items Discord shows in one media gallery. */ const MEDIA_GALLERY_LIMIT = 10;
2142
2412
  /**
@@ -2220,7 +2490,7 @@ const IMAGE = /\.(png|jpe?g|gif|webp)$/i;
2220
2490
  return (view.files ?? []).filter((file)=>!shown.has(`attachment://${nameOf(file)}`));
2221
2491
  }
2222
2492
  /** A view as an embed: its image, or else its first image file not its thumbnail, as the embed's image. */ function renderEmbed(view) {
2223
- const embed = new discord_js.EmbedBuilder().setDescription(textOf(view));
2493
+ const embed = new discord_js.EmbedBuilder().setDescription(textAfter(emojiOf(view), view, TEXT_LIMITS.embed));
2224
2494
  if (view.title) embed.setTitle(view.title);
2225
2495
  if (view.color !== undefined) embed.setColor(view.color);
2226
2496
  const image = view.image ?? unshownFiles(view).map(nameOf).find((name)=>IMAGE.test(name));
@@ -2231,7 +2501,8 @@ const IMAGE = /\.(png|jpe?g|gif|webp)$/i;
2231
2501
  /** A view as a Components V2 container, carrying MeoCord's id. */ function renderContainer(view) {
2232
2502
  const container = new discord_js.ContainerBuilder().setId(RENDERED_CONTAINER_ID);
2233
2503
  if (view.color !== undefined) container.setAccentColor(discord_js.resolveColor(view.color));
2234
- const text = new discord_js.TextDisplayBuilder().setContent(view.title ? `### ${view.title}\n${textOf(view)}` : textOf(view));
2504
+ const titleLine = view.title ? `### ${view.title}\n` : '';
2505
+ const text = new discord_js.TextDisplayBuilder().setContent(textAfter(titleLine + emojiOf(view), view, TEXT_LIMITS.v2));
2235
2506
  if (view.thumbnail) {
2236
2507
  container.addSectionComponents(new discord_js.SectionBuilder().addTextDisplayComponents(text).setThumbnailAccessory(new discord_js.ThumbnailBuilder().setURL(urlOf(view, view.thumbnail))));
2237
2508
  } else container.addTextDisplayComponents(text);
@@ -2362,13 +2633,16 @@ const EMBED_CONTENT = [
2362
2633
  return sameJson(content(a), content(b));
2363
2634
  }
2364
2635
 
2365
- let last = 0;
2366
- const order = new WeakMap();
2636
+ // One count for both builds of this version, so calls either makes merge in the order made
2637
+ const calls = shared('callOrder', ()=>({
2638
+ last: 0,
2639
+ order: new WeakMap()
2640
+ }));
2367
2641
  /** Marks a Discord call as made now, so calls recorded in different places can be merged in the order made. */ function stampCall(call) {
2368
- order.set(call, ++last);
2642
+ calls.order.set(call, ++calls.last);
2369
2643
  }
2370
2644
  /** When a call stamped by {@link stampCall} was made, relative to the others. */ function callOrder(call) {
2371
- return order.get(call) ?? 0;
2645
+ return calls.order.get(call) ?? 0;
2372
2646
  }
2373
2647
 
2374
2648
  /**
@@ -2501,7 +2775,10 @@ const logger$4 = new Logger('Theme');
2501
2775
  */ accept(id, result, report) {
2502
2776
  if (result === undefined || result === null) return undefined;
2503
2777
  const layer = copyLayer(result);
2504
- const problems = themeProblems(layer, `themeFor.${this.kind} for ${this.kind} ${id}`);
2778
+ // A value from the app's data, such as a user's saved choice, shown as a log line shows outside text
2779
+ const problems = themeProblems(layer, `themeFor.${this.kind} for ${this.kind} ${id}`, {
2780
+ forLog: true
2781
+ });
2505
2782
  if (problems.length === 0) {
2506
2783
  if (report) this.warned.delete(id);
2507
2784
  return layer;
@@ -2748,7 +3025,8 @@ function toBody(payload) {
2748
3025
  */ function editedSince(stamp, ours) {
2749
3026
  return typeof stamp === 'number' && ours !== undefined && stamp > ours;
2750
3027
  }
2751
- const messageLocks = new Map();
3028
+ // Both builds of this meocord version keep one, so a message one locks, the other sees locked
3029
+ const messageLocks = shared('messageLocks', ()=>new Map());
2752
3030
  /** How long a locked message is remembered once no call holds it. */ const LOCK_MEMORY_MS = 60000;
2753
3031
  /**
2754
3032
  * A body with the theme's primary colour where the app left one unset: an embed with no `color`, and a
@@ -2810,9 +3088,12 @@ function toJson(value) {
2810
3088
  return this.phase;
2811
3089
  }
2812
3090
  get message() {
3091
+ // A deleted answer, or a follow-up in its place, leaves the component's own message behind
3092
+ if (this.answer.kind !== 'original') return this.lastMessage;
2813
3093
  return this.lastMessage ?? ('message' in this.interaction ? this.interaction.message ?? undefined : undefined);
2814
3094
  }
2815
3095
  get original() {
3096
+ // Each component is a top-level component's API JSON, as `toJSON()` gave it; the lock reads them as plain JSON
2816
3097
  return this.snapshot;
2817
3098
  }
2818
3099
  /** The Discord calls made through this state, for tests. */ get history() {
@@ -3144,9 +3425,11 @@ function toJson(value) {
3144
3425
  this.sync();
3145
3426
  const body = await this.themed(payload, options);
3146
3427
  if (this.phase === 'unanswered') return this.reply(body);
3147
- if (this.phase === 'deferred' && answersWithOwnMessage(this.interaction)) {
3428
+ // Once the answer is deleted, the next message sent is the answer
3429
+ if (this.answer.kind === 'deleted') return this.answerWith(body);
3430
+ if (this.phase === 'deferred' && answersWithOwnMessage(this.interaction) && this.answer.kind === 'original') {
3148
3431
  // Discord makes a follow-up to a deferred, unsent reply that reply, ignoring its flags: a private one
3149
- // would be shown to everyone on a public deferral, so that deferral is deleted first.
3432
+ // would be shown to everyone on a public deferral, so that deferral is deleted, and the follow-up is the answer.
3150
3433
  const requested = Number(discord_js.MessageFlagsBitField.resolve(body.flags ?? 0));
3151
3434
  if (!hasEphemeral(requested)) return this.editMessage(body);
3152
3435
  // A private deferral is already what the flag asks for, which an edit cannot take
@@ -3156,7 +3439,11 @@ function toJson(value) {
3156
3439
  });
3157
3440
  await this.call('deleteReply', undefined, ()=>this.interaction.deleteReply());
3158
3441
  this.phase = 'replied';
3442
+ return this.answerWith(body);
3159
3443
  }
3444
+ return (await this.sendFollowUp(body)).message;
3445
+ }
3446
+ /** Sends a follow-up, with the flags it was sent with. */ async sendFollowUp(body) {
3160
3447
  const flags = this.withSuppression(this.flagsFor('followUp', body.flags, false));
3161
3448
  const sent = forMode(body, hasComponentsV2(flags));
3162
3449
  const message = await this.call('followUp', {
@@ -3166,6 +3453,20 @@ function toJson(value) {
3166
3453
  ...sent,
3167
3454
  flags
3168
3455
  }));
3456
+ return {
3457
+ message: message,
3458
+ flags
3459
+ };
3460
+ }
3461
+ /** Sends a follow-up that becomes the answer, which later edits and `delete()` act on by its id. */ async answerWith(body) {
3462
+ const { message, flags } = await this.sendFollowUp(body);
3463
+ // Its privacy as sent, not as read back from the message
3464
+ this.answer = {
3465
+ kind: 'followUp',
3466
+ id: message.id,
3467
+ ephemeral: hasEphemeral(flags)
3468
+ };
3469
+ this.lastMessage = message;
3169
3470
  return message;
3170
3471
  }
3171
3472
  delete() {
@@ -3173,7 +3474,22 @@ function toJson(value) {
3173
3474
  this.sync();
3174
3475
  if (this.phase === 'unanswered') throw new Error('There is no answer to delete: the interaction has not been answered.');
3175
3476
  this.refuseAfterModal();
3176
- await this.call('deleteReply', undefined, ()=>this.interaction.deleteReply());
3477
+ const { answer } = this;
3478
+ if (answer.kind === 'deleted') throw new Error('There is no answer to delete: it was deleted.');
3479
+ // A follow-up is as private as it was sent, a command's own reply as the interaction's answer, a component's
3480
+ // message as its flags
3481
+ const shown = this.message?.flags?.has(discord_js.MessageFlags.Ephemeral) ?? false;
3482
+ const ephemeral = answer.kind === 'followUp' ? answer.ephemeral : answersWithOwnMessage(this.interaction) ? Boolean(this.interaction.ephemeral) || shown : shown;
3483
+ if (answer.kind === 'followUp') await this.call('deleteReply', answer.id, ()=>this.interaction.deleteReply(answer.id));
3484
+ else await this.call('deleteReply', undefined, ()=>this.interaction.deleteReply());
3485
+ this.answer = {
3486
+ kind: 'deleted',
3487
+ ephemeral
3488
+ };
3489
+ this.lastMessage = undefined;
3490
+ // The message is gone: nothing is left to put back as it was before a lock
3491
+ this.settled = true;
3492
+ this.leave();
3177
3493
  });
3178
3494
  }
3179
3495
  /** A modal is the interaction's whole answer: what the user enters arrives as a submit interaction of its own. */ refuseAfterModal() {
@@ -3283,7 +3599,20 @@ function toJson(value) {
3283
3599
  withAttachments(body) {
3284
3600
  return rewriteAttachmentUrls(body, keptAttachmentNames(body, this.message?.attachments?.values() ?? []));
3285
3601
  }
3286
- /** Edits the answer through the interaction, and through the channel only once its token has expired. */ async editMessage(body, { restoring = false } = {}) {
3602
+ /**
3603
+ * Edits the answer through the interaction, and through the channel only once its token has expired. Once the answer
3604
+ * is deleted, the edit is sent as a follow-up that becomes the answer, as private as the deleted one unless it sets
3605
+ * flags of its own.
3606
+ */ async editMessage(body, { restoring = false } = {}) {
3607
+ const { answer } = this;
3608
+ if (answer.kind === 'deleted') {
3609
+ if (restoring) return undefined;
3610
+ const { attachments: _attachments, ...rest } = body;
3611
+ return this.answerWith({
3612
+ ...rest,
3613
+ flags: body.flags ?? (answer.ephemeral ? discord_js.MessageFlags.Ephemeral : 0)
3614
+ });
3615
+ }
3287
3616
  if (!restoring) {
3288
3617
  this.settled = true;
3289
3618
  this.leave();
@@ -3305,8 +3634,13 @@ function toJson(value) {
3305
3634
  ...this.withAttachments(forMode(body, this.v2)),
3306
3635
  flags
3307
3636
  };
3637
+ // A follow-up that is the answer is edited by its id
3638
+ const edit = answer.kind === 'followUp' ? {
3639
+ ...sent,
3640
+ message: answer.id
3641
+ } : sent;
3308
3642
  try {
3309
- this.lastMessage = await this.call('editReply', sent, ()=>this.interaction.editReply(sent));
3643
+ this.lastMessage = await this.call('editReply', edit, ()=>this.interaction.editReply(edit));
3310
3644
  } catch (error) {
3311
3645
  const message = this.message;
3312
3646
  const expired = Date.now() - this.interaction.createdTimestamp >= TOKEN_EXPIRED_AFTER_MS;
@@ -3425,12 +3759,16 @@ function toJson(value) {
3425
3759
  * place, and the failure is returned for the caller to report once the user is answered.
3426
3760
  */ async drawnView(error, message, theme) {
3427
3761
  const tone = isUserOutcome(error, this.interaction) ? 'warning' : 'danger';
3762
+ const context = this.presenterContext(this.v2, theme);
3763
+ // Fitted to the mode's limit first, so a presenter that writes the message as its text, MeoCord's own included, renders
3764
+ const generic = ()=>textFor(this.interaction, {
3765
+ key: 'meocord.fallback.error'
3766
+ });
3428
3767
  const presented = {
3429
- message,
3768
+ message: presentedMessage(message, TEXT_LIMITS[context.mode], generic),
3430
3769
  error,
3431
3770
  tone
3432
3771
  };
3433
- const context = this.presenterContext(this.v2, theme);
3434
3772
  const ready = (view)=>{
3435
3773
  const sendable = this.sendable(themedView(view, theme), this.keptBeside());
3436
3774
  // Rendered once here, so a view MeoCord cannot render fails before anything is sent
@@ -3573,6 +3911,11 @@ function toJson(value) {
3573
3911
  await this.followUpNow(this.privateError(view));
3574
3912
  return;
3575
3913
  }
3914
+ // A deleted message has nothing to add the error to or put back: the error follows up, and is the answer
3915
+ if (this.answer.kind === 'deleted') {
3916
+ await this.followUpNow(this.privateError(view));
3917
+ return;
3918
+ }
3576
3919
  // The message the component is on: whether it is private does not change with edits.
3577
3920
  const current = 'message' in this.interaction ? this.interaction.message ?? undefined : this.message;
3578
3921
  if (current?.flags?.has(discord_js.MessageFlags.Ephemeral)) {
@@ -3618,6 +3961,12 @@ function toJson(value) {
3618
3961
  /** The answer steps asked for, run one after another; `pending` counts those not yet settled. */ this.queue = Promise.resolve();
3619
3962
  this.pending = 0;
3620
3963
  /** Whether the answer was a modal, which has no message to edit or delete. */ this.modalShown = false;
3964
+ /**
3965
+ * Which message is the answer: the interaction's own reply, a follow-up that took its place, or none since `delete()`,
3966
+ * with whether the deleted one was private. `sync()` leaves it alone, so every edit follows it.
3967
+ */ this.answer = {
3968
+ kind: 'original'
3969
+ };
3621
3970
  this.calls = [];
3622
3971
  /** Whether the interaction was acknowledged with a private deferred reply for a drawn error view, which that view replaces. */ this.privateDeferral = false;
3623
3972
  /** Whether the locked message was answered, so nothing restores it again. */ this.settled = false;
@@ -3629,7 +3978,8 @@ function toJson(value) {
3629
3978
  this.v2 = Boolean(message?.flags?.has(discord_js.MessageFlags.IsComponentsV2));
3630
3979
  }
3631
3980
  }
3632
- const states = new WeakMap();
3981
+ // One state per interaction for both builds of this version, so either sees where the other left its answer
3982
+ const states = shared('responseStates', ()=>new WeakMap());
3633
3983
  /** The response state of a repliable interaction, created on first use. */ function responseOf(interaction) {
3634
3984
  let state = states.get(interaction);
3635
3985
  if (!state) states.set(interaction, state = new InteractionResponse(interaction));
@@ -3700,7 +4050,7 @@ const states = new WeakMap();
3700
4050
  * @group Utilities
3701
4051
  * @see {@link createMetadata}
3702
4052
  * @see {@link StageParams}
3703
- * @see {@link https://meocord.dev/docs/4.1/guards | Guards}
4053
+ * @see {@link https://meocord.dev/docs/4.2/guards | Guards}
3704
4054
  */ class ExecutionContext {
3705
4055
  /**
3706
4056
  * The response state of the interaction being handled, the same one `respond(interaction)` returns,
@@ -4099,9 +4449,17 @@ const logger$2 = new Logger('ExecutionContext');
4099
4449
  };
4100
4450
  });
4101
4451
  }
4102
- /** Drops the keys whose calls have all left their window, once a minute from the store's first call. */ startSweeping() {
4452
+ /**
4453
+ * Drops the keys whose calls have all left their window, once a minute from a call, until a sweep leaves none: an
4454
+ * idle store then holds no timer, so one nothing references is collected, and its next call starts the sweep again.
4455
+ */ startSweeping() {
4103
4456
  if (this.sweeper) return;
4104
- this.sweeper = setInterval(()=>sweep(this.calls, Date.now()), SWEEP_INTERVAL_MS);
4457
+ this.sweeper = setInterval(()=>{
4458
+ sweep(this.calls, Date.now());
4459
+ if (this.calls.size > 0) return;
4460
+ clearInterval(this.sweeper);
4461
+ this.sweeper = undefined;
4462
+ }, SWEEP_INTERVAL_MS);
4105
4463
  // A cooldown must never be what keeps a stopping bot alive.
4106
4464
  this.sweeper.unref?.();
4107
4465
  }
@@ -4399,10 +4757,25 @@ const messageOf = (error)=>String(error?.message ?? error);
4399
4757
  }
4400
4758
 
4401
4759
  /**
4402
- * How specific a route pattern is, for ranking the patterns that can match one input: more literal
4403
- * text first, then a fixed length before one taking the rest, then no optional param before one, then
4404
- * fewer params, then narrower typed params, whose segments take fewer values. Component patterns count literal
4405
- * characters; message patterns count literal words.
4760
+ * Gives a decorator's `wrapper` the `length` and `name` of the method it replaces, so whatever reads the handler, such
4761
+ * as `invoke` counting the arguments it takes, sees the method as it was declared.
4762
+ */ function wrapsMethod(wrapper, original) {
4763
+ Object.defineProperty(wrapper, 'length', {
4764
+ value: original.length,
4765
+ configurable: true
4766
+ });
4767
+ Object.defineProperty(wrapper, 'name', {
4768
+ value: original.name,
4769
+ configurable: true
4770
+ });
4771
+ return wrapper;
4772
+ }
4773
+
4774
+ /**
4775
+ * How specific a route pattern is, for ranking the message patterns that can match one message: more literal
4776
+ * words first, then a fixed length before one taking the rest, then no optional param before one, then
4777
+ * fewer params, then narrower typed params, whose words take fewer values. Component patterns count literal
4778
+ * characters, for CommandMeta.specificity alone.
4406
4779
  */ function routeSpecificity({ literals, params, rest = false, optional = false, typed = 0 }) {
4407
4780
  return literals * 1000 - (rest ? 500 : 0) - (optional ? 250 : 0) - params + typed / 1000;
4408
4781
  }
@@ -4443,14 +4816,26 @@ const UNIT_MS = lookupTable({
4443
4816
  return total + Number(amount) * UNIT_MS[unit];
4444
4817
  }, 0);
4445
4818
  }
4819
+ /** The largest snowflake: Discord's IDs are unsigned 64-bit integers. */ const MAX_SNOWFLAKE = 2n ** 64n - 1n;
4820
+ /**
4821
+ * A Discord ID, kept as its text: 17 to 20 digits, no leading zero, up to the largest 64-bit value. An ID's top 42 bits
4822
+ * are milliseconds since 2015-01-01, so every ID made from 2015-01-28 on has 17 digits or more, and none is a safe
4823
+ * integer.
4824
+ */ const snowflake = (word)=>/^[1-9]\d{16,19}$/.test(word) && BigInt(word) <= MAX_SNOWFLAKE ? word : undefined;
4825
+ /** A UUID in its canonical 8-4-4-4-12 form, in either case, kept as written. */ const uuid = (word)=>/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test(word) ? word : undefined;
4446
4826
  /** The words a type such as `on|off` chooses from, or `undefined` for any other type. */ const choicesOf = (type)=>type.includes('|') ? type.split('|') : undefined;
4447
4827
  /** The types a component's customId segment can take: each parsed from the segment alone, with no request. */ const SEGMENT_TYPES = lookupTable({
4448
4828
  string: (segment)=>segment,
4449
4829
  int: (segment)=>number(segment, true),
4450
4830
  number: (segment)=>number(segment, false),
4451
- bool
4831
+ bool,
4832
+ snowflake,
4833
+ uuid
4452
4834
  });
4453
- /** Whether `type` can type a customId segment: `string`, `int`, `number`, `bool`, or words to choose from. */ const isSegmentType = (type)=>type in SEGMENT_TYPES || choicesOf(type) !== undefined;
4835
+ /**
4836
+ * Whether `type` can type a customId segment: `string`, `int`, `number`, `bool`, `snowflake`, `uuid`, or words to
4837
+ * choose from.
4838
+ */ const isSegmentType = (type)=>type in SEGMENT_TYPES || choicesOf(type) !== undefined;
4454
4839
  /** A customId segment as its type's value, words to choose from compared as written: `undefined` when it is not one. */ function parseSegment(type, segment) {
4455
4840
  const choices = choicesOf(type);
4456
4841
  if (choices) return choices.includes(segment) ? segment : undefined;
@@ -4481,6 +4866,7 @@ const UNIT_MS = lookupTable({
4481
4866
  /** How many guard wrappers `@UseGuard` and `@Controller` put around one method. */ guardWrappers: 'meocord:guard-wrappers',
4482
4867
  /** The prototype whose method a class-level `@UseGuard` re-declared on a subclass. */ inheritedFrom: 'meocord:inherited-from',
4483
4868
  /** `false` when `@Controller({ inheritStages: false })` stops class stages at this class. */ inheritStages: 'meocord:inherit-stages',
4869
+ /** `true` when `@Controller({ inheritedRoutes: 'replace' })` drops the routes this class's re-decorated methods inherit. */ replacesInheritedRoutes: 'meocord:replaces-inherited-routes',
4484
4870
  /** A controller's class-level `@Cooldown`s. */ classCooldowns: 'meocord:class-cooldowns',
4485
4871
  /** A method's `@Cooldown`s, in declaration order. */ methodCooldowns: 'meocord:method-cooldowns',
4486
4872
  /** The error types a filter's `@Catch` names; empty to catch everything. */ catchTypes: 'meocord:catch-types',
@@ -4555,19 +4941,60 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
4555
4941
  else handlers[index] = entry;
4556
4942
  Reflect.defineMetadata(key, handlers, target);
4557
4943
  }
4558
- /** The class's own command map, started from a copy of the inherited one, for the same reason. */ function ownCommandMap(target) {
4944
+ /**
4945
+ * Leaves each method the class's own decorators declare only those routes, of every kind, dropping the ones it inherits
4946
+ * for it: what `@Controller({ inheritedRoutes: 'replace' })` does once the class's method decorators have run.
4947
+ */ function dropInheritedRoutes(prototype) {
4948
+ const redeclared = new Set(getDeclaredRoutes(prototype).map((route)=>route.method));
4949
+ if (redeclared.size === 0) return;
4950
+ const base = Object.getPrototypeOf(prototype);
4951
+ // An entry the class takes from its base, for a method it re-declares
4952
+ const inheritedFor = (inherited, method)=>(entry)=>inherited.includes(entry) && redeclared.has(method(entry));
4953
+ const commands = ownCommandMap(prototype);
4954
+ const baseCommands = Reflect.getMetadata(META.commands, base) ?? Object.create(null);
4955
+ for (const [name, metas] of Object.entries(commands)){
4956
+ const kept = metas.filter((meta)=>!inheritedFor(commandsNamed(baseCommands, name), (entry)=>entry.methodName)(meta));
4957
+ if (kept.length > 0) commands[name] = kept;
4958
+ else delete commands[name];
4959
+ }
4960
+ Reflect.defineMetadata(META.commands, commands, prototype);
4961
+ const lists = [
4962
+ [
4963
+ META.messageHandlers,
4964
+ (entry)=>entry.method
4965
+ ],
4966
+ [
4967
+ META.reactionHandlers,
4968
+ (entry)=>entry.method
4969
+ ],
4970
+ [
4971
+ META.autocompleteHandlers,
4972
+ (entry)=>entry.methodName
4973
+ ]
4974
+ ];
4975
+ for (const [key, method] of lists){
4976
+ const inherited = Reflect.getMetadata(key, base) ?? [];
4977
+ Reflect.defineMetadata(key, ownHandlerList(key, prototype).filter((entry)=>!inheritedFor(inherited, method)(entry)), prototype);
4978
+ }
4979
+ }
4980
+ /**
4981
+ * The class's own command map, started from a copy of the inherited one, for the same reason. Keyed by command name
4982
+ * without a prototype, so a command named like an inherited key, such as `constructor`, is a key like any other.
4983
+ */ function ownCommandMap(target) {
4559
4984
  const own = Reflect.getOwnMetadata(META.commands, target);
4560
4985
  if (own) return own;
4561
4986
  const inherited = Reflect.getMetadata(META.commands, target) ?? {};
4562
- return Object.fromEntries(Object.entries(inherited).map(([name, metas])=>[
4563
- name,
4564
- [
4565
- ...metas
4566
- ]
4567
- ]));
4987
+ const copy = Object.create(null);
4988
+ for (const [name, metas] of Object.entries(inherited))copy[name] = [
4989
+ ...metas
4990
+ ];
4991
+ return copy;
4992
+ }
4993
+ /** The handlers a command map holds for `name`: its own entry only, never one `Object.prototype` holds. */ function commandsNamed(map, name) {
4994
+ return map && Object.hasOwn(map, name) ? map[name] : [];
4568
4995
  }
4569
4996
  function MessageHandler(pattern, options = {}) {
4570
- return function(target, propertyKey) {
4997
+ return declaring(function(target, propertyKey) {
4571
4998
  refuseOnClass('@MessageHandler', target, propertyKey);
4572
4999
  // An empty pattern means every message, as no pattern does
4573
5000
  if (pattern === '') {
@@ -4581,7 +5008,7 @@ function MessageHandler(pattern, options = {}) {
4581
5008
  };
4582
5009
  addOwnHandler(META.messageHandlers, target, declared, (other)=>other.method === method && other.pattern === declared.pattern);
4583
5010
  declareRoute(target, messageRoute(method, declared.pattern));
4584
- };
5011
+ });
4585
5012
  }
4586
5013
  function ReactionHandler(emojiOrSettings, settings = {}) {
4587
5014
  const [emoji, own] = typeof emojiOrSettings === 'object' ? [
@@ -4591,7 +5018,7 @@ function ReactionHandler(emojiOrSettings, settings = {}) {
4591
5018
  emojiOrSettings,
4592
5019
  settings
4593
5020
  ];
4594
- return function(target, propertyKey) {
5021
+ return declaring(function(target, propertyKey) {
4595
5022
  refuseOnClass('@ReactionHandler', target, propertyKey);
4596
5023
  const method = propertyKey.toString();
4597
5024
  addOwnHandler(META.reactionHandlers, target, {
@@ -4600,7 +5027,7 @@ function ReactionHandler(emojiOrSettings, settings = {}) {
4600
5027
  settings: own
4601
5028
  }, (other)=>other.method === method && other.emoji === emoji);
4602
5029
  declareRoute(target, reactionRoute(method, emoji));
4603
- };
5030
+ });
4604
5031
  }
4605
5032
  /** A custom emoji as Discord writes it in a message: `<:name:id>`, or `<a:name:id>` for an animated one. */ const CUSTOM_EMOJI = /^<a?:\w+:(\d+)>$/;
4606
5033
  /**
@@ -4629,12 +5056,24 @@ function ReactionHandler(emojiOrSettings, settings = {}) {
4629
5056
  }
4630
5057
  // `{name}`, or `{name:type}` with the type read as far as the brace, so a type no segment can hold is named
4631
5058
  const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
5059
+ // The whole of a brace pair that is a param, as PLACEHOLDER_PATTERN reads one
5060
+ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5061
+ /**
5062
+ * Why a pattern's brace pairs that aren't params are matched as literal text, or undefined when it has none: a
5063
+ * param's name is ASCII letters, digits and _, so `{café}` is text.
5064
+ */ function literalBraces(pattern) {
5065
+ const braces = (pattern.match(/\{[^{}]*}/g) ?? []).filter((brace)=>!WHOLE_PLACEHOLDER.test(brace));
5066
+ if (braces.length === 0) return undefined;
5067
+ const one = braces.length === 1;
5068
+ return `${braces.join(', ')} ${one ? "isn't a param" : "aren't params"}, since a param's name is ASCII letters, digits and _; ` + `${one ? 'it is' : 'they are'} matched as literal text.`;
5069
+ }
4632
5070
  /** The character a parameter will not cross, so one pattern segment maps to one value. */ const PARAM_SEPARATOR = '/';
5071
+ /** The longest customId Discord accepts; the shortest is one character. */ const MAX_CUSTOM_ID_LENGTH = 100;
4633
5072
  /** Escapes a literal stretch of a pattern so only placeholders stay meaningful. */ const escapeLiteral = (literal)=>literal.replace(/[/\\^$*+?.()|[\]{}]/g, '\\$&');
4634
5073
  /**
4635
- * Compiles a pattern into a regex, its parameter names, their types and its specificity. A `{name}` takes a
4636
- * whole `/`-separated segment, so a uuid is captured whole and `profile/{uuid}` never overlaps
4637
- * `profile/{uuid}/{id}`; a param that shares a segment with literal text is refused.
5074
+ * Compiles a pattern into a regex, its parameter names, their types, its 4.1 specificity and the length of the
5075
+ * shortest customId it matches. A `{name}` takes a whole `/`-separated segment, so a uuid is captured whole and
5076
+ * `profile/{uuid}` never overlaps `profile/{uuid}/{id}`; a param that shares a segment with literal text is refused.
4638
5077
  */ function createRegexFromPattern(pattern) {
4639
5078
  const params = [];
4640
5079
  // By the param's own name, so one named like an inherited key, such as `__proto__`, keeps its type
@@ -4655,6 +5094,13 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4655
5094
  if (literal !== '' && !literal.endsWith(PARAM_SEPARATOR) || after !== undefined && after !== PARAM_SEPARATOR) {
4656
5095
  throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} must occupy a whole segment, so it has to be ` + `preceded and followed by "${PARAM_SEPARATOR}" or by the ends of the pattern. ` + `Write "a${PARAM_SEPARATOR}{${param}}" rather than "a-{${param}}".`));
4657
5096
  }
5097
+ // A regex group's name, which the engine would refuse with the compiled regex in place of the pattern
5098
+ if (params.includes(param)) {
5099
+ throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} appears twice; give each param its own name.`));
5100
+ }
5101
+ if (/^\d/.test(param)) {
5102
+ throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} starts with a digit; start a param's name with a letter or _.`));
5103
+ }
4658
5104
  if (type !== undefined) {
4659
5105
  if (!isSegmentType(type)) throw refuse(new Error(`Invalid pattern "${pattern}": ${segmentTypeProblem(param, type)}`));
4660
5106
  if (choicesOf(type)?.includes('')) {
@@ -4672,25 +5118,23 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4672
5118
  literalLength += trailing.length;
4673
5119
  regexPattern += escapeLiteral(trailing);
4674
5120
  const regex = new RegExp(`^${regexPattern}$`);
4675
- // Literal text is the signal: a pattern spelling out more of the id describes it
4676
- // more exactly than one leaving it to a parameter. Fewer parameters breaks a tie
4677
- // between equal-length patterns, then narrower typed params; the order the controllers
4678
- // and handlers are listed in settles what is left, and the startup warning names the pair.
5121
+ // What 4.1 ranked by, kept for code that reads CommandMeta.specificity; routing ranks segment by segment
4679
5122
  const specificity = routeSpecificity({
4680
5123
  literals: literalLength,
4681
5124
  params: params.length,
4682
- typed: Object.values(types).reduce((sum, type)=>sum + narrowness(type), 0)
5125
+ typed: Object.values(types).reduce((sum, type)=>sum + (choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1), 0)
4683
5126
  });
5127
+ // Each param takes at least one character
4684
5128
  return {
4685
5129
  regex,
4686
5130
  params,
4687
5131
  types: lookupTable(types),
4688
- specificity
5132
+ specificity,
5133
+ shortest: literalLength + params.length
4689
5134
  };
4690
5135
  }
4691
- /** How few values a segment type takes, so a narrower type ranks first: words to choose from, then bool, int, number. */ const narrowness = (type)=>choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1;
4692
5136
  /** Why a `{name:type}` cannot type a customId segment, which holds text the bot wrote, with no message to read. */ function segmentTypeProblem(param, type) {
4693
- const kinds = 'string, int, number, bool, or words to choose from such as {mode:on|off}';
5137
+ const kinds = 'string, int, number, bool, snowflake, uuid, or words to choose from such as {mode:on|off}';
4694
5138
  if ([
4695
5139
  'member',
4696
5140
  'user',
@@ -4714,10 +5158,11 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4714
5158
  * the keys the handler's params require are checked against the pattern or route, a typed segment's value against
4715
5159
  * its type, and a select menu's choices, such as `values: string[]`, against what discord.js gives; a key a pipe
4716
5160
  * produces is declared `Piped<T>`, and left to the pipe. Two component handlers of one type whose patterns match
4717
- * exactly the same ids stop the bot at startup; patterns that only overlap are warned about, naming the one that
4718
- * runs: the more specific, or between equally specific ones, the one listed first. In the next major version (5.0),
4719
- * the one that spells out the first segment where two equally specific patterns differ runs instead, and the warning
4720
- * names the pairs that changes. A context menu handler receives the kind its builder's `setType()` names, and one
5161
+ * exactly the same ids stop the bot at startup. Patterns that both match an id rank segment by segment, left to
5162
+ * right: at the first segment one spells out as literal text and the other leaves to a param, the literal one runs,
5163
+ * so `profile/me/{section}` takes `profile/me/edit` from `profile/{userId}/edit`; between patterns that leaves tied,
5164
+ * the narrower type at the first param where they differ. Two still tied are warned about as the bot starts, and the
5165
+ * one listed first runs. A context menu handler receives the kind its builder's `setType()` names, and one
4721
5166
  * declaring the other kind fails to compile; when the compiler cannot tell the kind, `@Command` checks the parameter
4722
5167
  * type it emits as it applies. A subclass that re-declares an inherited handler on the same name or pattern takes its
4723
5168
  * own builder and options; on another it still answers the inherited one too, which the bot warns about as it
@@ -4750,12 +5195,11 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4750
5195
  * @category Handlers
4751
5196
  * @see {@link CommandBuilder}
4752
5197
  * @see {@link route}
4753
- * @see {@link https://meocord.dev/docs/4.1/slash-commands | Slash commands}
4754
- * @see {@link https://meocord.dev/docs/4.1/components | Buttons, selects and modals}
5198
+ * @see {@link https://meocord.dev/docs/4.2/slash-commands | Slash commands}
5199
+ * @see {@link https://meocord.dev/docs/4.2/components | Buttons, selects and modals}
4755
5200
  */ function Command(name, builderOrType) {
4756
5201
  const commandName = typeof name === 'string' ? name : name.pattern;
4757
- return function(target, propertyKey, _descriptor) {
4758
- var _commands, _commandName;
5202
+ return declaring(function(target, propertyKey, _descriptor) {
4759
5203
  refuseOnClass('@Command', target, propertyKey);
4760
5204
  const originalMethod = _descriptor.value;
4761
5205
  if (!originalMethod) {
@@ -4763,7 +5207,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4763
5207
  }
4764
5208
  // Wrap original method for interaction type validation
4765
5209
  const declaredAs = typeof builderOrType === 'function' ? builderOrType.name : `CommandType.${builderOrType}`;
4766
- _descriptor.value = function(interaction, params) {
5210
+ const checked = function(interaction, params) {
4767
5211
  if (!matchesCommandType(commandType, interaction)) {
4768
5212
  throw new Error(`${target.constructor.name}.${propertyKey}: @Command('${commandName}', ${declaredAs}) takes ` + `${interactionClassName(commandType)}${givenInstead(interaction)}.`);
4769
5213
  }
@@ -4772,6 +5216,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4772
5216
  params
4773
5217
  ]);
4774
5218
  };
5219
+ _descriptor.value = wrapsMethod(checked, originalMethod);
4775
5220
  // This class's own map, inherited routes included
4776
5221
  const commands = ownCommandMap(target);
4777
5222
  let builderInstance;
@@ -4807,7 +5252,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4807
5252
  }));
4808
5253
  }
4809
5254
  // discord.js builders validate as they are set, and their errors name neither the command nor the field.
4810
- throw refuse(new Error(`${where}: ${builderOrType.name} could not build "${commandName}": ${detail}. Check its names, descriptions and ` + `localizations, which Discord limits to 32 and 100 characters.`, {
5255
+ throw refuse(new Error(`${where}: ${builderOrType.name} could not build "${commandName}": ${detail}. Check its names, descriptions and ` + 'localizations, which Discord limits to 32 characters for a name and 100 for a description. A name is letters, ' + 'numbers, `-` and `_`, lowercase where the script has case, and without spaces, in every locale.', {
4811
5256
  cause: error
4812
5257
  }));
4813
5258
  }
@@ -4827,6 +5272,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4827
5272
  commandType = builderOrType;
4828
5273
  }
4829
5274
  if (isCustomIdRouted(commandType)) {
5275
+ // A route warned as it was made
5276
+ const braces = typeof name === 'string' ? literalBraces(commandName) : undefined;
5277
+ if (braces) logger.warn(`${target.constructor.name}.${propertyKey}: in the pattern "${commandName}", ${braces}`);
4830
5278
  let pattern;
4831
5279
  try {
4832
5280
  pattern = createRegexFromPattern(commandName);
@@ -4835,10 +5283,14 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4835
5283
  cause: error
4836
5284
  }));
4837
5285
  }
4838
- const { regex: generatedRegex, params, specificity: patternSpecificity } = pattern;
4839
- regex = generatedRegex;
4840
- dynamicParams = params;
4841
- specificity = patternSpecificity;
5286
+ regex = pattern.regex;
5287
+ dynamicParams = pattern.params;
5288
+ specificity = pattern.specificity;
5289
+ // Warned rather than refused, so a bot that carries such a handler still starts
5290
+ if (pattern.shortest === 0 || pattern.shortest > MAX_CUSTOM_ID_LENGTH) {
5291
+ const length = pattern.shortest === 0 ? 'the pattern is empty' : `its shortest is ${pattern.shortest} characters`;
5292
+ logger.warn(`${target.constructor.name}.${propertyKey}: the pattern "${commandName}" matches no customId: Discord's customIds ` + `are 1 to ${MAX_CUSTOM_ID_LENGTH} characters, and ${length}, so the handler never runs.`);
5293
+ }
4842
5294
  }
4843
5295
  const declared = {
4844
5296
  methodName: propertyKey,
@@ -4855,14 +5307,15 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4855
5307
  }
4856
5308
  };
4857
5309
  // One the class inherits for this method and route is replaced where it stands, so this one's options apply
4858
- const metas = (_commands = commands)[_commandName = commandName] ?? (_commands[_commandName] = []);
4859
- const inherited = getCommandMap(Object.getPrototypeOf(target))?.[commandName] ?? [];
5310
+ if (!Object.hasOwn(commands, commandName)) commands[commandName] = [];
5311
+ const metas = commands[commandName];
5312
+ const inherited = commandsNamed(getCommandMap(Object.getPrototypeOf(target)), commandName);
4860
5313
  const replaced = metas.findIndex((meta)=>inherited.includes(meta) && meta.methodName === propertyKey && meta.type === commandType);
4861
5314
  if (replaced === -1) metas.push(declared);
4862
5315
  else metas[replaced] = declared;
4863
5316
  Reflect.defineMetadata(META.commands, commands, target);
4864
5317
  declareRoute(target, commandRoute(propertyKey, commandType, commandName));
4865
- };
5318
+ });
4866
5319
  }
4867
5320
  /** The interaction class Discord sends for each context menu kind. */ const CONTEXT_MENU_INTERACTIONS = new Map([
4868
5321
  [
@@ -4934,9 +5387,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4934
5387
  * @group Decorators
4935
5388
  * @category Handlers
4936
5389
  * @see {@link Command}
4937
- * @see {@link https://meocord.dev/docs/4.1/autocomplete | Autocomplete}
5390
+ * @see {@link https://meocord.dev/docs/4.2/autocomplete | Autocomplete}
4938
5391
  */ function Autocomplete(commandPath, optionName) {
4939
- return function(target, propertyKey, _descriptor) {
5392
+ return declaring(function(target, propertyKey, _descriptor) {
4940
5393
  refuseOnClass('@Autocomplete', target, propertyKey);
4941
5394
  const methodName = propertyKey.toString();
4942
5395
  addOwnHandler(META.autocompleteHandlers, target, {
@@ -4945,7 +5398,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4945
5398
  methodName
4946
5399
  }, (other)=>other.methodName === methodName && other.commandPath === commandPath && other.optionName === optionName);
4947
5400
  declareRoute(target, autocompleteRoute(methodName, commandPath, optionName));
4948
- };
5401
+ });
4949
5402
  }
4950
5403
  /**
4951
5404
  * Returns a controller's autocomplete handlers, option-specific ones first.
@@ -4967,52 +5420,63 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
4967
5420
  const match = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`).exec(segment);
4968
5421
  return match ? typeKey(match[2]) : undefined;
4969
5422
  }
4970
- /** Whether a segment's two readings can both take one value: a literal, text, or a typed param. */ function segmentsOverlap(left, right) {
5423
+ /**
5424
+ * How few values a segment type takes, so a narrower type ranks first: words to choose from, then bool, uuid,
5425
+ * snowflake, int, number, and text last. A snowflake is narrower than a number, which takes all its values.
5426
+ */ const NARROWNESS = lookupTable({
5427
+ bool: 5,
5428
+ uuid: 4,
5429
+ snowflake: 3,
5430
+ int: 2,
5431
+ number: 1,
5432
+ '': 0
5433
+ });
5434
+ const narrowness = (type)=>choicesOf(type) ? 6 : NARROWNESS[type];
5435
+ /**
5436
+ * How narrow a pattern segment that is a param, `{name}` or `{name:type}`, is: from `0` for text to `4` for words to
5437
+ * choose from. `undefined` for a segment of literal text.
5438
+ */ function paramNarrowness(segment) {
5439
+ const type = segmentParamType(segment);
5440
+ return type === undefined ? undefined : narrowness(type);
5441
+ }
5442
+ /** A value of each scalar segment type: an int, a bool, a snowflake and a uuid; text takes any. */ const SAMPLE_VALUES = [
5443
+ '1',
5444
+ 'true',
5445
+ '10000000000000000',
5446
+ '00000000-0000-0000-0000-000000000000'
5447
+ ];
5448
+ /** A value both readings of a segment take: a literal, text, or a typed param; `undefined` when they share none. */ function sharedValue(left, right) {
4971
5449
  const [a, b] = [
4972
5450
  segmentParamType(left),
4973
5451
  segmentParamType(right)
4974
5452
  ];
4975
- if (a === undefined && b === undefined) return left === right;
4976
- if (a === undefined || b === undefined) {
4977
- const [type, literal] = a === undefined ? [
4978
- b,
5453
+ const takes = (type, segment, value)=>type === undefined ? segment === value : type === '' || parseSegment(type, value) !== undefined;
5454
+ // Every type takes one of these when it takes anything another reading takes: a literal, a listed word, or a value
5455
+ // of each scalar type
5456
+ const candidates = [
5457
+ ...a === undefined ? [
4979
5458
  left
4980
- ] : [
4981
- a,
5459
+ ] : [],
5460
+ ...b === undefined ? [
4982
5461
  right
4983
- ];
4984
- return type === '' || parseSegment(type, literal) !== undefined;
4985
- }
4986
- if (a === '' || b === '' || a === b) return true;
4987
- // Words to choose from overlap another type when one of them is a value of it
4988
- const [wordsA, wordsB] = [
4989
- choicesOf(a),
4990
- choicesOf(b)
5462
+ ] : [],
5463
+ ...choicesOf(a ?? '') ?? [],
5464
+ ...choicesOf(b ?? '') ?? [],
5465
+ ...SAMPLE_VALUES
4991
5466
  ];
4992
- if (wordsA) return wordsA.some((word)=>parseSegment(b, word) !== undefined);
4993
- if (wordsB) return wordsB.some((word)=>parseSegment(a, word) !== undefined);
4994
- // Of the scalar types, only a whole number and a number share values
4995
- return a === 'int' && b === 'number' || a === 'number' && b === 'int';
4996
- }
4997
- /**
4998
- * Finds pairs of customId patterns that can both match one id, such as `a/{x}/c` and `a/b/{y}`.
4999
- * @returns Each ambiguous pair once, in the order the patterns were given.
5000
- */ function findAmbiguousRoutes(patterns) {
5001
- const segmentsOf = (pattern)=>pattern.split(PARAM_SEPARATOR);
5002
- const collisions = [];
5003
- for(let i = 0; i < patterns.length; i++){
5004
- for(let j = i + 1; j < patterns.length; j++){
5005
- const left = segmentsOf(patterns[i]);
5006
- const right = segmentsOf(patterns[j]);
5007
- if (left.length !== right.length) continue;
5008
- const disjoint = left.some((segment, index)=>!segmentsOverlap(segment, right[index]));
5009
- if (!disjoint) collisions.push([
5010
- patterns[i],
5011
- patterns[j]
5012
- ]);
5013
- }
5014
- }
5015
- return collisions;
5467
+ return candidates.find((value)=>takes(a, left, value) && takes(b, right, value));
5468
+ }
5469
+ /**
5470
+ * A customId both patterns match, such as `ticket/1/close` for `ticket/{id}/close` and `ticket/{n:int}/close`;
5471
+ * `undefined` when no id matches both.
5472
+ */ function sharedCustomId(left, right) {
5473
+ const [a, b] = [
5474
+ left.split(PARAM_SEPARATOR),
5475
+ right.split(PARAM_SEPARATOR)
5476
+ ];
5477
+ if (a.length !== b.length) return undefined;
5478
+ const values = a.map((segment, index)=>sharedValue(segment, b[index]));
5479
+ return values.every((value)=>value !== undefined) ? values.join(PARAM_SEPARATOR) : undefined;
5016
5480
  }
5017
5481
 
5018
5482
  exports.Autocomplete = Autocomplete;
@@ -5027,6 +5491,7 @@ exports.ExecutionContext = ExecutionContext;
5027
5491
  exports.GuardDeniedError = GuardDeniedError;
5028
5492
  exports.HandlerExecutionContext = HandlerExecutionContext;
5029
5493
  exports.Logger = Logger;
5494
+ exports.MAX_CUSTOM_ID_LENGTH = MAX_CUSTOM_ID_LENGTH;
5030
5495
  exports.MEOCORD_MESSAGES = MEOCORD_MESSAGES;
5031
5496
  exports.META = META;
5032
5497
  exports.MemoryCooldownStore = MemoryCooldownStore;
@@ -5036,11 +5501,13 @@ exports.REFUSED_AS_TOO_LARGE = REFUSED_AS_TOO_LARGE;
5036
5501
  exports.ReactionHandler = ReactionHandler;
5037
5502
  exports.RedisCooldownStore = RedisCooldownStore;
5038
5503
  exports.SetMetadata = SetMetadata;
5504
+ exports.TEXT_LIMITS = TEXT_LIMITS;
5039
5505
  exports.ThemeCache = ThemeCache;
5040
5506
  exports.Translator = Translator;
5041
5507
  exports.UnroutedExecutionContext = UnroutedExecutionContext;
5042
5508
  exports.UserError = UserError;
5043
5509
  exports.ValidationError = ValidationError;
5510
+ exports.allDeclaredErrors = allDeclaredErrors;
5044
5511
  exports.ambientThemeVersion = ambientThemeVersion;
5045
5512
  exports.applyDecorators = applyDecorators;
5046
5513
  exports.assertValidTheme = assertValidTheme;
@@ -5053,6 +5520,8 @@ exports.bundleEntry = bundleEntry;
5053
5520
  exports.callOrder = callOrder;
5054
5521
  exports.choicesOf = choicesOf;
5055
5522
  exports.claimAmbientTheme = claimAmbientTheme;
5523
+ exports.collectStartupErrors = collectStartupErrors;
5524
+ exports.commandsNamed = commandsNamed;
5056
5525
  exports.compiledConfigMessage = compiledConfigMessage;
5057
5526
  exports.cooldownMessage = cooldownMessage;
5058
5527
  exports.cooldownStoreMessage = cooldownStoreMessage;
@@ -5060,6 +5529,8 @@ exports.cooldownText = cooldownText;
5060
5529
  exports.copyLayer = copyLayer;
5061
5530
  exports.createRegexFromPattern = createRegexFromPattern;
5062
5531
  exports.createTranslator = createTranslator;
5532
+ exports.declaredErrorsOf = declaredErrorsOf;
5533
+ exports.declaring = declaring;
5063
5534
  exports.decoratedName = decoratedName;
5064
5535
  exports.defaultTheme = defaultTheme;
5065
5536
  exports.defineCatalog = defineCatalog;
@@ -5067,10 +5538,11 @@ exports.deprecatedOnMethod = deprecatedOnMethod;
5067
5538
  exports.describeInteraction = describeInteraction;
5068
5539
  exports.describeRefusal = describeRefusal;
5069
5540
  exports.describeValue = describeValue;
5541
+ exports.dropInheritedRoutes = dropInheritedRoutes;
5070
5542
  exports.duration = duration;
5071
5543
  exports.escapeForLog = escapeForLog;
5072
5544
  exports.existingResponse = existingResponse;
5073
- exports.findAmbiguousRoutes = findAmbiguousRoutes;
5545
+ exports.fitText = fitText;
5074
5546
  exports.focusedOptionName = focusedOptionName;
5075
5547
  exports.forgetRecorded = forgetRecorded;
5076
5548
  exports.getAutocompleteHandlers = getAutocompleteHandlers;
@@ -5094,8 +5566,10 @@ exports.isRefusal = isRefusal;
5094
5566
  exports.isTooLarge = isTooLarge;
5095
5567
  exports.isUserOutcome = isUserOutcome;
5096
5568
  exports.listed = listed;
5569
+ exports.literalBraces = literalBraces;
5097
5570
  exports.loadMeoCordConfig = loadMeoCordConfig;
5098
5571
  exports.logFailedSend = logFailedSend;
5572
+ exports.logStartupErrors = logStartupErrors;
5099
5573
  exports.lookup = lookup;
5100
5574
  exports.lookupLayers = lookupLayers;
5101
5575
  exports.lookupTable = lookupTable;
@@ -5109,6 +5583,7 @@ exports.missingTranslatorError = missingTranslatorError;
5109
5583
  exports.number = number;
5110
5584
  exports.ownHandlerList = ownHandlerList;
5111
5585
  exports.ownsAmbientTheme = ownsAmbientTheme;
5586
+ exports.paramNarrowness = paramNarrowness;
5112
5587
  exports.parseSegment = parseSegment;
5113
5588
  exports.patternShape = patternShape;
5114
5589
  exports.placeholderNames = placeholderNames;
@@ -5131,9 +5606,16 @@ exports.routeSpecificity = routeSpecificity;
5131
5606
  exports.runInThemeScope = runInThemeScope;
5132
5607
  exports.runWithTheme = runWithTheme;
5133
5608
  exports.setLegacyThemeLayer = setLegacyThemeLayer;
5609
+ exports.setOwn = setOwn;
5134
5610
  exports.setPresenter = setPresenter;
5611
+ exports.setStartupErrorsMode = setStartupErrorsMode;
5135
5612
  exports.setThemeOutsideCalls = setThemeOutsideCalls;
5613
+ exports.shared = shared;
5614
+ exports.sharedCustomId = sharedCustomId;
5615
+ exports.sharedName = sharedName;
5136
5616
  exports.stampCall = stampCall;
5617
+ exports.startupError = startupError;
5618
+ exports.stopOnStartupErrors = stopOnStartupErrors;
5137
5619
  exports.textOfIssue = textOfIssue;
5138
5620
  exports.textRenderer = textRenderer;
5139
5621
  exports.themeForInteraction = themeForInteraction;
@@ -5148,7 +5630,9 @@ exports.useTheme = useTheme;
5148
5630
  exports.userWords = userWords;
5149
5631
  exports.warnDeprecated = warnDeprecated;
5150
5632
  exports.warnDeprecatedBehaviour = warnDeprecatedBehaviour;
5633
+ exports.warnOnce = warnOnce;
5151
5634
  exports.warnPlaceholder = warnPlaceholder;
5152
5635
  exports.withRelease = withRelease;
5153
5636
  exports.withSendableFiles = withSendableFiles;
5154
5637
  exports.withoutFiles = withoutFiles;
5638
+ exports.wrapsMethod = wrapsMethod;