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.
- package/CHANGELOG.md +362 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
- package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
- package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
- package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
- package/dist/cjs/common/index.cjs +4 -4
- package/dist/cjs/core/index.cjs +365 -310
- package/dist/cjs/decorator/index.cjs +85 -61
- package/dist/cjs/testing/index.cjs +2236 -496
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/README.md.template +3 -1
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
- package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
- package/dist/esm/bin/generator.js +4 -5
- package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
- package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
- package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
- package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
- package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
- package/dist/esm/bin/helper/service-generator.helper.js +4 -5
- package/dist/esm/bin/meocord.js +84 -25
- package/dist/esm/build/load-config.pre-entry.js +17 -4
- package/dist/esm/build/rsbuild-config.js +16 -12
- package/dist/esm/build/typed-externals.js +36 -0
- package/dist/esm/common/cooldown-store.js +10 -2
- package/dist/esm/common/decorator.js +6 -6
- package/dist/esm/common/deprecation.js +2 -2
- package/dist/esm/common/errors.js +27 -2
- package/dist/esm/common/execution-context.js +1 -1
- package/dist/esm/common/meocord-text.js +11 -8
- package/dist/esm/common/metadata.js +1 -1
- package/dist/esm/common/response/call-order.js +9 -4
- package/dist/esm/common/response/presenter.js +33 -6
- package/dist/esm/common/response/response-state.js +84 -10
- package/dist/esm/common/route.js +21 -5
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translate-error.js +1 -1
- package/dist/esm/common/translator.js +37 -6
- package/dist/esm/core/command-conflicts.js +34 -34
- package/dist/esm/core/command-registration.js +93 -24
- package/dist/esm/core/command-routes.js +2 -2
- package/dist/esm/core/component-routes.js +74 -31
- package/dist/esm/core/cooldown-runner.js +29 -8
- package/dist/esm/core/dispatcher.js +51 -7
- package/dist/esm/core/fallback.js +20 -11
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/guard-runner.js +16 -3
- package/dist/esm/core/handler-input.js +4 -3
- package/dist/esm/core/handler-pipeline.js +27 -14
- package/dist/esm/core/handler-registry.js +1 -1
- package/dist/esm/core/interceptor-runner.js +2 -2
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +241 -179
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/message-routes.js +4 -3
- package/dist/esm/core/observer-runner.js +4 -3
- package/dist/esm/core/providers.js +45 -23
- package/dist/esm/core/route-specificity.js +4 -4
- package/dist/esm/core/running-calls.js +43 -0
- package/dist/esm/core/scalar-types.js +15 -3
- package/dist/esm/core/shard-context.js +6 -7
- package/dist/esm/core/shard-manager.js +16 -10
- package/dist/esm/core/startup-roots.js +23 -4
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-runtime.js +2 -2
- package/dist/esm/core/theme-scope.js +3 -1
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/app.decorator.js +12 -7
- package/dist/esm/decorator/command-builder.decorator.js +4 -4
- package/dist/esm/decorator/controller-class.decorator.js +9 -4
- package/dist/esm/decorator/controller.decorator.js +162 -83
- package/dist/esm/decorator/cooldown.decorator.js +20 -3
- package/dist/esm/decorator/defer.decorator.js +4 -4
- package/dist/esm/decorator/event.decorator.js +5 -5
- package/dist/esm/decorator/filter.decorator.js +7 -7
- package/dist/esm/decorator/guard.decorator.js +11 -10
- package/dist/esm/decorator/inject.decorator.js +4 -4
- package/dist/esm/decorator/interceptor.decorator.js +7 -7
- package/dist/esm/decorator/observer.decorator.js +4 -4
- package/dist/esm/decorator/service.decorator.js +4 -4
- package/dist/esm/decorator/theme.decorator.js +3 -3
- package/dist/esm/decorator/validation.decorator.js +10 -10
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +27 -11
- package/dist/esm/testing/cooldown-store-suite.js +85 -15
- package/dist/esm/testing/discord-defaults.js +249 -21
- package/dist/esm/testing/index.js +4 -2
- package/dist/esm/testing/inspect-handler.js +20 -3
- package/dist/esm/testing/meocord-testing-module.js +352 -205
- package/dist/esm/testing/mock-fn.js +173 -22
- package/dist/esm/testing/mock-interaction.js +1214 -209
- package/dist/esm/testing/response.js +1 -1
- package/dist/esm/testing/routing.js +39 -14
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/startup-errors.js +26 -0
- package/dist/esm/testing/strict-mocks.js +65 -0
- package/dist/esm/testing/theme.js +1 -1
- package/dist/esm/util/bundle-entry.util.js +2 -1
- package/dist/esm/util/common.util.js +47 -19
- package/dist/esm/util/dev-runner.util.js +8 -8
- package/dist/esm/util/generator-cli.util.js +4 -5
- package/dist/esm/util/interaction.util.js +3 -3
- package/dist/esm/util/meocord-config-loader.util.js +10 -2
- package/dist/esm/util/meocord-config-validation.util.js +4 -5
- package/dist/esm/util/meocord-source-config.util.js +65 -20
- package/dist/esm/util/metadata-keys.js +1 -0
- package/dist/esm/util/method-wrapper.util.js +16 -0
- package/dist/esm/util/platform.util.js +10 -10
- package/dist/esm/util/refusal.util.js +148 -8
- package/dist/esm/util/shard-process.util.js +4 -1
- package/dist/esm/util/sharding-mode.util.js +5 -7
- package/dist/esm/util/shared-state.util.js +21 -0
- package/dist/esm/util/tsconfig-paths.util.js +29 -0
- package/dist/esm/util/tsconfig.util.js +10 -10
- package/dist/esm/util/value.util.js +12 -1
- package/dist/types/common/index.d.cts +8 -8
- package/dist/types/common/index.d.ts +8 -8
- package/dist/types/core/index.d.cts +10 -7
- package/dist/types/core/index.d.ts +10 -7
- package/dist/types/decorator/index.d.cts +31 -29
- package/dist/types/decorator/index.d.ts +31 -29
- package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
- package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
- package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
- package/dist/types/testing/index.d.cts +406 -70
- package/dist/types/testing/index.d.ts +406 -70
- package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
- package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
- package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
- package/package.json +4 -3
package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs}
RENAMED
|
@@ -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
|
|
83
|
-
return `MeoCord config at ${problem.path} failed to load: ${reason}. ${fix}
|
|
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-
|
|
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
|
-
|
|
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
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*/ function
|
|
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
|
|
741
|
+
return paths.relative(root, file).split(paths.sep).join('/');
|
|
710
742
|
}
|
|
711
|
-
return
|
|
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
|
|
764
|
-
* selector or skin tone, joined to others by
|
|
765
|
-
|
|
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
|
|
781
|
-
const
|
|
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
|
-
|
|
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.
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
/**
|
|
1319
|
-
let format = lists.get(
|
|
1320
|
-
if (!format) lists.set(
|
|
1321
|
-
type:
|
|
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.
|
|
1327
|
-
*
|
|
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 ?
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2138
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
2366
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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',
|
|
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
|
-
|
|
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.
|
|
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
|
-
/**
|
|
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(()=>
|
|
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
|
-
*
|
|
4403
|
-
*
|
|
4404
|
-
|
|
4405
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
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
|
|
4636
|
-
* whole `/`-separated segment, so a uuid is captured whole and
|
|
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
|
-
//
|
|
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 +
|
|
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
|
|
4718
|
-
*
|
|
4719
|
-
*
|
|
4720
|
-
*
|
|
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.
|
|
4754
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
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
|
-
|
|
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 ` +
|
|
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
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
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
|
-
|
|
4859
|
-
const
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
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
|
-
|
|
5459
|
+
] : [],
|
|
5460
|
+
...b === undefined ? [
|
|
4982
5461
|
right
|
|
4983
|
-
]
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
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
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
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.
|
|
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;
|