meocord 4.2.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +264 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-XKMlcUZz.cjs} +376 -100
  4. package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-BZgJyfZS.cjs} +145 -36
  5. package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-CgfPQrNl.cjs} +219 -63
  7. package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-BUAR1WYG.cjs} +10 -5
  8. package/dist/cjs/common/index.cjs +3 -3
  9. package/dist/cjs/core/index.cjs +185 -168
  10. package/dist/cjs/decorator/index.cjs +21 -6
  11. package/dist/cjs/testing/index.cjs +1594 -286
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/generator.js +4 -5
  14. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  15. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  16. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  17. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  18. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  19. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  20. package/dist/esm/bin/meocord.js +84 -25
  21. package/dist/esm/build/load-config.pre-entry.js +13 -3
  22. package/dist/esm/build/rsbuild-config.js +16 -12
  23. package/dist/esm/build/typed-externals.js +36 -0
  24. package/dist/esm/common/cooldown-store.js +10 -2
  25. package/dist/esm/common/errors.js +25 -0
  26. package/dist/esm/common/meocord-text.js +11 -8
  27. package/dist/esm/common/response/call-order.js +9 -4
  28. package/dist/esm/common/response/presenter.js +33 -6
  29. package/dist/esm/common/response/response-state.js +84 -10
  30. package/dist/esm/common/route.js +11 -4
  31. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  32. package/dist/esm/common/translator.js +37 -6
  33. package/dist/esm/core/command-registration.js +93 -24
  34. package/dist/esm/core/command-routes.js +2 -2
  35. package/dist/esm/core/cooldown-runner.js +29 -8
  36. package/dist/esm/core/fallback.js +20 -11
  37. package/dist/esm/core/guard-runner.js +16 -3
  38. package/dist/esm/core/handler-input.js +4 -3
  39. package/dist/esm/core/lifecycle-hooks.js +134 -47
  40. package/dist/esm/core/meocord-factory.js +55 -32
  41. package/dist/esm/core/meocord.app.js +116 -75
  42. package/dist/esm/core/providers.js +7 -2
  43. package/dist/esm/core/running-calls.js +43 -0
  44. package/dist/esm/core/shard-context.js +5 -6
  45. package/dist/esm/core/shard-manager.js +16 -10
  46. package/dist/esm/core/startup-roots.js +21 -1
  47. package/dist/esm/core/theme-resolvers.js +4 -1
  48. package/dist/esm/core/theme-scope.js +3 -1
  49. package/dist/esm/core/theme-validation.js +17 -10
  50. package/dist/esm/decorator/controller.decorator.js +56 -20
  51. package/dist/esm/decorator/cooldown.decorator.js +17 -0
  52. package/dist/esm/decorator/guard.decorator.js +3 -2
  53. package/dist/esm/package.json.js +1 -1
  54. package/dist/esm/testing/catalog.js +26 -10
  55. package/dist/esm/testing/cooldown-store-suite.js +67 -5
  56. package/dist/esm/testing/discord-defaults.js +205 -30
  57. package/dist/esm/testing/meocord-testing-module.js +134 -28
  58. package/dist/esm/testing/mock-fn.js +107 -26
  59. package/dist/esm/testing/mock-interaction.js +1026 -172
  60. package/dist/esm/testing/snowflake.js +9 -2
  61. package/dist/esm/testing/strict-mocks.js +37 -17
  62. package/dist/esm/util/common.util.js +47 -19
  63. package/dist/esm/util/dev-runner.util.js +8 -8
  64. package/dist/esm/util/generator-cli.util.js +4 -5
  65. package/dist/esm/util/interaction.util.js +2 -2
  66. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  67. package/dist/esm/util/meocord-config-validation.util.js +3 -5
  68. package/dist/esm/util/meocord-source-config.util.js +65 -20
  69. package/dist/esm/util/method-wrapper.util.js +16 -0
  70. package/dist/esm/util/platform.util.js +10 -10
  71. package/dist/esm/util/shard-process.util.js +4 -1
  72. package/dist/esm/util/sharding-mode.util.js +5 -7
  73. package/dist/esm/util/shared-state.util.js +21 -0
  74. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  75. package/dist/esm/util/tsconfig.util.js +10 -10
  76. package/dist/esm/util/value.util.js +12 -1
  77. package/dist/types/common/index.d.cts +6 -6
  78. package/dist/types/common/index.d.ts +6 -6
  79. package/dist/types/core/index.d.cts +3 -2
  80. package/dist/types/core/index.d.ts +3 -2
  81. package/dist/types/decorator/index.d.cts +1 -0
  82. package/dist/types/decorator/index.d.ts +1 -0
  83. package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
  84. package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
  85. package/dist/types/interface/index.d.cts +1 -1
  86. package/dist/types/interface/index.d.ts +1 -1
  87. package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
  88. package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
  89. package/dist/types/testing/index.d.cts +175 -53
  90. package/dist/types/testing/index.d.ts +175 -53
  91. package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  92. package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  93. package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
  94. package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
  95. package/package.json +2 -2
package/dist/cli.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "meocordVersion": "4.2.0",
3
+ "meocordVersion": "4.2.1",
4
4
  "name": "meocord",
5
5
  "description": "CLI for managing the MeoCord application",
6
6
  "usage": "[options] [command]",
@@ -9,16 +9,15 @@ import '../core/theme-validation.js';
9
9
  import '../common/decorator.js';
10
10
  import 'reflect-metadata';
11
11
  import 'discord.js';
12
- import '../common/translator.js';
13
- import '../common/response/send-failure.js';
14
- import '../util/interaction.util.js';
15
- import '../common/response/flags.js';
16
- import '../common/response/components.js';
12
+ import '../common/response/response-state.js';
13
+ import '../common/errors.js';
17
14
  import 'node:crypto';
18
15
  import '../decorator/controller.decorator.js';
19
16
  import '../core/scalar-types.js';
20
17
  import 'node:fs';
21
18
  import 'node:url';
19
+ import '../common/translator.js';
20
+ import '../common/meocord-text.js';
22
21
  import { ServiceGeneratorHelper } from './helper/service-generator.helper.js';
23
22
  import { GuardGeneratorHelper } from './helper/guard-generator.helper.js';
24
23
  import { InterceptorGeneratorHelper } from './helper/interceptor-generator.helper.js';
@@ -5,16 +5,15 @@ import '../../core/theme-validation.js';
5
5
  import '../../common/decorator.js';
6
6
  import 'reflect-metadata';
7
7
  import 'discord.js';
8
- import '../../common/translator.js';
9
- import '../../common/response/send-failure.js';
10
- import '../../util/interaction.util.js';
11
- import '../../common/response/flags.js';
12
- import '../../common/response/components.js';
8
+ import '../../common/response/response-state.js';
9
+ import '../../common/errors.js';
13
10
  import 'node:crypto';
14
11
  import '../../decorator/controller.decorator.js';
15
12
  import '../../core/scalar-types.js';
16
13
  import 'node:fs';
17
14
  import 'node:url';
15
+ import '../../common/translator.js';
16
+ import '../../common/meocord-text.js';
18
17
  import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, writeFiles } from '../../util/generator-cli.util.js';
19
18
 
20
19
  class FilterGeneratorHelper {
@@ -5,16 +5,15 @@ import '../../core/theme-validation.js';
5
5
  import '../../common/decorator.js';
6
6
  import 'reflect-metadata';
7
7
  import 'discord.js';
8
- import '../../common/translator.js';
9
- import '../../common/response/send-failure.js';
10
- import '../../util/interaction.util.js';
11
- import '../../common/response/flags.js';
12
- import '../../common/response/components.js';
8
+ import '../../common/response/response-state.js';
9
+ import '../../common/errors.js';
13
10
  import 'node:crypto';
14
11
  import '../../decorator/controller.decorator.js';
15
12
  import '../../core/scalar-types.js';
16
13
  import 'node:fs';
17
14
  import 'node:url';
15
+ import '../../common/translator.js';
16
+ import '../../common/meocord-text.js';
18
17
  import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, writeFiles } from '../../util/generator-cli.util.js';
19
18
 
20
19
  class GuardGeneratorHelper {
@@ -5,16 +5,15 @@ import '../../core/theme-validation.js';
5
5
  import '../../common/decorator.js';
6
6
  import 'reflect-metadata';
7
7
  import 'discord.js';
8
- import '../../common/translator.js';
9
- import '../../common/response/send-failure.js';
10
- import '../../util/interaction.util.js';
11
- import '../../common/response/flags.js';
12
- import '../../common/response/components.js';
8
+ import '../../common/response/response-state.js';
9
+ import '../../common/errors.js';
13
10
  import 'node:crypto';
14
11
  import '../../decorator/controller.decorator.js';
15
12
  import '../../core/scalar-types.js';
16
13
  import 'node:fs';
17
14
  import 'node:url';
15
+ import '../../common/translator.js';
16
+ import '../../common/meocord-text.js';
18
17
  import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, writeFiles } from '../../util/generator-cli.util.js';
19
18
 
20
19
  class InterceptorGeneratorHelper {
@@ -5,16 +5,15 @@ import '../../core/theme-validation.js';
5
5
  import '../../common/decorator.js';
6
6
  import 'reflect-metadata';
7
7
  import 'discord.js';
8
- import '../../common/translator.js';
9
- import '../../common/response/send-failure.js';
10
- import '../../util/interaction.util.js';
11
- import '../../common/response/flags.js';
12
- import '../../common/response/components.js';
8
+ import '../../common/response/response-state.js';
9
+ import '../../common/errors.js';
13
10
  import 'node:crypto';
14
11
  import '../../decorator/controller.decorator.js';
15
12
  import '../../core/scalar-types.js';
16
13
  import 'node:fs';
17
14
  import 'node:url';
15
+ import '../../common/translator.js';
16
+ import '../../common/meocord-text.js';
18
17
  import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, writeFiles } from '../../util/generator-cli.util.js';
19
18
 
20
19
  class ObserverGeneratorHelper {
@@ -5,16 +5,15 @@ import '../../core/theme-validation.js';
5
5
  import '../../common/decorator.js';
6
6
  import 'reflect-metadata';
7
7
  import 'discord.js';
8
- import '../../common/translator.js';
9
- import '../../common/response/send-failure.js';
10
- import '../../util/interaction.util.js';
11
- import '../../common/response/flags.js';
12
- import '../../common/response/components.js';
8
+ import '../../common/response/response-state.js';
9
+ import '../../common/errors.js';
13
10
  import 'node:crypto';
14
11
  import '../../decorator/controller.decorator.js';
15
12
  import '../../core/scalar-types.js';
16
13
  import 'node:fs';
17
14
  import 'node:url';
15
+ import '../../common/translator.js';
16
+ import '../../common/meocord-text.js';
18
17
  import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, writeFiles } from '../../util/generator-cli.util.js';
19
18
 
20
19
  class PipeGeneratorHelper {
@@ -5,16 +5,15 @@ import '../../core/theme-validation.js';
5
5
  import '../../common/decorator.js';
6
6
  import 'reflect-metadata';
7
7
  import 'discord.js';
8
- import '../../common/translator.js';
9
- import '../../common/response/send-failure.js';
10
- import '../../util/interaction.util.js';
11
- import '../../common/response/flags.js';
12
- import '../../common/response/components.js';
8
+ import '../../common/response/response-state.js';
9
+ import '../../common/errors.js';
13
10
  import 'node:crypto';
14
11
  import '../../decorator/controller.decorator.js';
15
12
  import '../../core/scalar-types.js';
16
13
  import 'node:fs';
17
14
  import 'node:url';
15
+ import '../../common/translator.js';
16
+ import '../../common/meocord-text.js';
18
17
  import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, writeFiles } from '../../util/generator-cli.util.js';
19
18
 
20
19
  class ServiceGeneratorHelper {
@@ -8,22 +8,21 @@ import '../core/theme-validation.js';
8
8
  import '../common/decorator.js';
9
9
  import 'reflect-metadata';
10
10
  import 'discord.js';
11
- import '../common/translator.js';
12
- import '../common/response/send-failure.js';
13
- import '../util/interaction.util.js';
14
- import '../common/response/flags.js';
15
- import '../common/response/components.js';
11
+ import '../common/response/response-state.js';
12
+ import '../common/errors.js';
16
13
  import '../decorator/controller.decorator.js';
17
14
  import '../core/scalar-types.js';
18
15
  import * as fs from 'node:fs';
19
16
  import 'node:url';
17
+ import '../common/translator.js';
18
+ import '../common/meocord-text.js';
20
19
  import { spawn } from 'node:child_process';
21
20
  import { capitalize } from 'lodash-es';
22
21
  import wait from '../util/wait.util.js';
23
22
  import { GeneratorCLI } from './generator.js';
24
23
  import { runtimePrefixFor, AppGeneratorHelper } from './helper/app-generator.helper.js';
25
24
  import { makeInitialCommit } from './helper/initial-commit.helper.js';
26
- import { setEnvironment, compileAndValidateConfig, validateRunConfig, validateDiscordToken } from '../util/common.util.js';
25
+ import { setEnvironment, compileAndValidateConfig, validateRunConfig, validateDiscordToken, checkSourceConfig, reportConfigProblem } from '../util/common.util.js';
27
26
  import { Command } from 'commander';
28
27
  import { execSync } from 'child_process';
29
28
  import * as p from '@clack/prompts';
@@ -38,7 +37,7 @@ import { isDevRunnerMessage, DEV_RUNNER_ENV } from '../util/dev-runner.util.js';
38
37
  import packageJson from '../package.json.js';
39
38
  import { fileURLToPath } from 'url';
40
39
  import { assertNoWebpackHook, createRsbuildConfig, optionalExternalConflicts, optionalExternalNames } from '../build/rsbuild-config.js';
41
- import { createNativeExternals, installedPackages, copyPackagesInto, bundledModuleFiles, assertNoBundledNativeAddons, findBundledNativeAddons } from '../build/native-addons.js';
40
+ import { createNativeExternals, packageNameOfRequest, installedPackages, copyPackagesInto, bundledModuleFiles, assertNoBundledNativeAddons, findBundledNativeAddons } from '../build/native-addons.js';
42
41
  import { writePlatformManifest, PLATFORM_MANIFEST } from '../util/platform.util.js';
43
42
  import { loadMeoCordSourceConfig } from '../util/meocord-source-config.util.js';
44
43
  import { loadMeoCordConfig } from '../util/meocord-config-loader.util.js';
@@ -94,6 +93,29 @@ const __dirname$1 = path.dirname(__filename$1);
94
93
  }
95
94
  return digest.digest('hex');
96
95
  }
96
+ // Latin letters that do not decompose into a base letter and a mark
97
+ const UNDECOMPOSED = {
98
+ ß: 'ss',
99
+ ẞ: 'SS',
100
+ æ: 'ae',
101
+ Æ: 'Ae',
102
+ œ: 'oe',
103
+ Œ: 'Oe',
104
+ ø: 'o',
105
+ Ø: 'O',
106
+ ł: 'l',
107
+ Ł: 'L',
108
+ đ: 'd',
109
+ Đ: 'D',
110
+ þ: 'th',
111
+ Þ: 'Th'
112
+ };
113
+ /**
114
+ * The folder `meocord create` writes an app named `appName` into, which is also its package name: kebab case, ASCII.
115
+ * Accents fold to their base letters first, so "CaféBot" splits as "CafeBot" does, into `cafe-bot`.
116
+ */ function appDirectoryName(appName) {
117
+ return appName.normalize('NFKD').replace(/\p{M}+/gu, '').replace(/[ßẞæÆœŒøØłŁđĐþÞ]/g, (letter)=>UNDECOMPOSED[letter]).replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
118
+ }
97
119
  /** A Command Line Interface (CLI) for managing the MeoCord application. */ class MeoCordCLI {
98
120
  /**
99
121
  * Configures and runs the MeoCord CLI.
@@ -191,12 +213,12 @@ copies or substantial portions of the Software.
191
213
  return program;
192
214
  }
193
215
  async createApp(appName, options) {
194
- const kebabCaseAppName = appName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
216
+ const kebabCaseAppName = appDirectoryName(appName);
195
217
  const appPath = path.resolve(process.cwd(), kebabCaseAppName);
196
218
  p.intro(`meocord v${this.version}`);
197
219
  // A name of only symbols would resolve to the current directory itself.
198
220
  if (!kebabCaseAppName) {
199
- p.cancel(`"${appName}" needs a name with letters or digits, such as my-bot: it names the app's directory.`);
221
+ p.cancel(`"${appName}" needs a name with Latin letters or digits, such as my-bot: it names the app's directory.`);
200
222
  await wait(100);
201
223
  process.exit(1);
202
224
  }
@@ -307,7 +329,7 @@ copies or substantial portions of the Software.
307
329
  optionalExternals: meocordConfig?.optionalExternals
308
330
  });
309
331
  for (const name of optionalExternalConflicts(meocordConfig?.optionalExternals, meocordConfig?.externals)){
310
- this.logger.warn(`"${name}" is in both optionalExternals and externals. In externals it becomes an import that runs ` + `before the bot's code and fails when the package is missing; remove it from externals.`);
332
+ this.logger.warn(`"${name}" is in both optionalExternals and externals. In externals, an ESM import of it runs as its importer ` + `loads and fails when the package is missing, where a require stays optional; remove it from externals.`);
311
333
  }
312
334
  const config = meocordConfig?.rsbuild?.(base) ?? base;
313
335
  // A native addon cannot be inlined into JavaScript, so when bundling, each one is kept out of
@@ -349,10 +371,13 @@ copies or substantial portions of the Software.
349
371
  dir
350
372
  ]));
351
373
  const nativeNames = new Set(natives.keys());
352
- const listed = (meocordConfig.externals ?? []).filter((item)=>typeof item === 'string');
374
+ // The package a string external is in, so one naming a file inside a package, such as 'lodash/fp.js', packs it
375
+ const listed = (meocordConfig.externals ?? []).flatMap((item)=>typeof item === 'string' ? packageNameOfRequest(item) ?? [] : []);
353
376
  const wanted = [
354
- ...listed,
355
- ...optionalExternalNames(meocordConfig.optionalExternals)
377
+ ...new Set([
378
+ ...listed,
379
+ ...optionalExternalNames(meocordConfig.optionalExternals)
380
+ ])
356
381
  ].filter((name)=>!packages.has(name));
357
382
  for (const { name, dir, native } of installedPackages(wanted, this.projectRoot)){
358
383
  packages.set(name, dir);
@@ -382,6 +407,22 @@ copies or substantial portions of the Software.
382
407
  this.logger.info(`dist/node_modules holds ${count}; nothing else to install.`);
383
408
  }
384
409
  }
410
+ /**
411
+ * Removes what {@link packDependencies} wrote, after a build without `bundleDependencies`, so its bundle resolves
412
+ * packages from the project rather than from a previous self-contained build's copy. Nothing else in dist is touched.
413
+ */ removePackedOutputs() {
414
+ const dist = path.join(this.projectRoot, 'dist');
415
+ for (const output of [
416
+ 'node_modules',
417
+ 'package.json',
418
+ PLATFORM_MANIFEST
419
+ ]){
420
+ fs.rmSync(path.join(dist, output), {
421
+ recursive: true,
422
+ force: true
423
+ });
424
+ }
425
+ }
385
426
  /** Builds the application in the given mode. */ async build(mode) {
386
427
  try {
387
428
  this.logger.info(`Building ${mode} version...`);
@@ -398,6 +439,8 @@ copies or substantial portions of the Software.
398
439
  // bundle would run on this machine -- through its node_modules -- and fail in production.
399
440
  assertNoBundledNativeAddons(findBundledNativeAddons(bundledFiles, this.projectRoot));
400
441
  this.packDependencies(meocordConfig, natives.found);
442
+ } else if (!meocordConfig?.bundleDependencies) {
443
+ this.removePackedOutputs();
401
444
  }
402
445
  this.logger.info(`${capitalize(mode)} build completed successfully.`);
403
446
  } catch (error) {
@@ -431,7 +474,8 @@ copies or substantial portions of the Software.
431
474
  mode,
432
475
  entry: configPath,
433
476
  bundleDependencies,
434
- externals: meocordConfig?.externals
477
+ externals: meocordConfig?.externals,
478
+ requireable: true
435
479
  });
436
480
  fs.mkdirSync(dist, {
437
481
  recursive: true
@@ -503,6 +547,13 @@ copies or substantial portions of the Software.
503
547
  * Runs the built application in register-only mode: it collects the commands from the bundle, sends
504
548
  * them over REST and exits, without logging in. Exits with the application's code.
505
549
  */ async register(guild) {
550
+ // An unset variable passed as the guild: registering where commands.* says instead could publish everywhere
551
+ if (guild !== undefined && !guild.trim()) {
552
+ this.logger.error('--guild was given no guild id, so nothing is registered. Pass a guild id, or leave --guild out to register where commands.* says.');
553
+ await wait(100);
554
+ process.exit(1);
555
+ return;
556
+ }
506
557
  if (!fs.existsSync(this.mainJSPath)) {
507
558
  this.logger.error('Main entry file (main.js) not found! Build first, or run `meocord register --build`.');
508
559
  await wait(100);
@@ -625,10 +676,8 @@ copies or substantial portions of the Software.
625
676
  cwd: this.projectRoot,
626
677
  env: {
627
678
  ...this.inheritedEnv,
628
- // Watch mode runs a development build, whose config reads the development files; Bun reads them for this too
629
- ...devRunner ? {
630
- NODE_ENV: 'development'
631
- } : process.env.NODE_ENV !== undefined && {
679
+ // The mode the command set, development for --dev, so the bot's config and Bun read the same .env files as the CLI
680
+ ...process.env.NODE_ENV !== undefined && {
632
681
  NODE_ENV: process.env.NODE_ENV
633
682
  },
634
683
  ...this.appEnv,
@@ -658,7 +707,7 @@ copies or substantial portions of the Software.
658
707
  // The new bundler is made, its plugins set up and its config resolved, before the running build is closed: a
659
708
  // config whose rsbuild hook or plugin throws leaves that build watching the sources, and the bot it started running
660
709
  const watch = async ()=>{
661
- const { rsbuild } = await this.createBundler('development');
710
+ const { rsbuild, meocordConfig } = await this.createBundler('development');
662
711
  await rsbuild.initConfigs({
663
712
  action: 'build'
664
713
  });
@@ -667,6 +716,7 @@ copies or substantial portions of the Software.
667
716
  rsbuild.onAfterBuild(({ stats })=>{
668
717
  isRunning = true;
669
718
  if (stats?.hasErrors()) return;
719
+ if (!meocordConfig?.bundleDependencies) this.removePackedOutputs();
670
720
  this.latestBuild = stats ? emittedDigest(stats) : undefined;
671
721
  this.restartApp(this.latestBuild);
672
722
  });
@@ -704,11 +754,20 @@ copies or substantial portions of the Software.
704
754
  changed = new Set();
705
755
  for (const file of files)this.logger.log(reloads[file]);
706
756
  if (!rebuilds(files)) return this.restartApp();
707
- // A config that doesn't compile, say mid-edit, leaves the running bot and its build as they are
708
- if (files.has('meocord.config.ts') && !await this.compileConfig({
709
- mode: 'development',
710
- exitOnFailure: false
711
- })) return;
757
+ // A config that is refused, as at startup, or doesn't compile, say mid-edit, leaves the running bot and its build
758
+ // as they are
759
+ if (files.has('meocord.config.ts')) {
760
+ const checked = checkSourceConfig();
761
+ if ('problem' in checked) {
762
+ // One that doesn't load, say mid-edit, is reported as a failed compile is, and watching goes on
763
+ if (checked.loadError !== undefined) return this.logger.error(`Failed to compile meocord.config.ts: ${checked.loadError}`);
764
+ return reportConfigProblem(checked.problem);
765
+ }
766
+ if (!await this.compileConfig({
767
+ mode: 'development',
768
+ exitOnFailure: false
769
+ })) return;
770
+ }
712
771
  isRunning = false;
713
772
  // The new build restarts the bot, whatever its output: the config it was launched with has changed
714
773
  this.launchedFrom = undefined;
@@ -836,4 +895,4 @@ if (isProcessEntry()) {
836
895
  });
837
896
  }
838
897
 
839
- export { MeoCordCLI, emittedDigest, stillRunning };
898
+ export { MeoCordCLI, appDirectoryName, emittedDigest, stillRunning };
@@ -1,12 +1,13 @@
1
- // Runs before the application's entry, so whatever environment meocord.config loads -- a dotenv
2
- // import, say -- is in place when the entry's decorators read process.env. Silent when the config
3
- // is missing or throws: MeoCordFactory.create reports that.
1
+ // Runs before the application's entry and every package it imports, so whatever environment meocord.config loads -- a
2
+ // dotenv import, say -- is in place when the entry's decorators and its packages read process.env. Silent when the
3
+ // config is missing or throws: MeoCordFactory.create reports that.
4
4
  import path from 'node:path'
5
5
  import { fileURLToPath } from 'node:url'
6
6
  import { installStackRemapper } from './stack-remap.js'
7
7
  import { listenForDevRunnerStop } from '../util/dev-runner.util.js'
8
8
  import { BUILD_MODE_KEY, BUNDLE_ENTRY_KEY, STARTUP_ERRORS_KEY } from '../util/bundle-entry.util.js'
9
9
  import { loadMeoCordConfig } from '../util/meocord-config-loader.util.js'
10
+ import { platformMismatch } from '../util/platform.util.js'
10
11
 
11
12
  // First, so a restart `meocord start --dev` asks for while the bundle still loads is heard
12
13
  listenForDevRunnerStop()
@@ -22,6 +23,15 @@ globalThis[BUILD_MODE_KEY] = process.env.NODE_ENV
22
23
  // rather than the directory the build ran in, which the bundle would otherwise carry
23
24
  __webpack_public_path__ = `${path.dirname(bundle).replace(/\\/g, '/')}/`
24
25
 
26
+ // Before any package loads: a native addon built for another platform would otherwise fail as it is imported, with its
27
+ // own error rather than this one, which names both platforms. In every process, a shard manager's and register's too
28
+ const mismatch = platformMismatch()
29
+ if (mismatch) {
30
+ // The shape of MeoCord's own error line, without its colours and timestamp, which need packages yet to load
31
+ process.stderr.write(`[ERROR] [MeoCord] ${mismatch}\n`)
32
+ process.exit(1)
33
+ }
34
+
25
35
  // Beside the bundle, wherever the bot was started from, by the loader the runtime uses: the same module when meocord is
26
36
  // bundled, else a copy of it, and require's cache evaluates the config once either way; the factory reports a failure
27
37
  const config = loadMeoCordConfig()
@@ -2,6 +2,7 @@ import path from 'path';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { OptionalProbesPlugin } from './optional-probes.js';
4
4
  import { RunnableBundlePlugin } from './runnable-bundle.js';
5
+ import { typeExternals } from './typed-externals.js';
5
6
  import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
6
7
 
7
8
  /**
@@ -11,8 +12,7 @@ import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
11
12
  /**
12
13
  * Native accelerators discord.js loads when present and works without. A bundler cannot tell
13
14
  * optional from missing, so they stay runtime imports. `node-commonjs` keeps each a `require` at its
14
- * call site, inside the library's try/catch, where a plain ESM external would hoist an import that
15
- * throws before the bot starts.
15
+ * call site, inside the library's try/catch, however the bundle asks for it.
16
16
  */ const DISCORD_OPTIONAL_NATIVES = [
17
17
  'zlib-sync',
18
18
  'bufferutil',
@@ -30,8 +30,8 @@ import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
30
30
  ];
31
31
  }
32
32
  /**
33
- * The `optionalExternals` also listed in `externals`, where each becomes a hoisted import that throws
34
- * at startup when the package is missing, defeating the point of listing it as optional.
33
+ * The `optionalExternals` also listed in `externals`, where an ESM import of each runs as its importer loads and
34
+ * throws when the package is missing, defeating the point of listing it as optional; a require stays one either way.
35
35
  */ function optionalExternalConflicts(optionalExternals = [], externals = []) {
36
36
  return optionalExternals.filter((name)=>externals.some((external)=>typeof external === 'string' ? external === name : external.test(name)));
37
37
  }
@@ -91,18 +91,16 @@ import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
91
91
  const wasmDir = environment.config.output.distPath.wasm;
92
92
  chain.output.webassemblyModuleFilename(path.posix.join(wasmDir, '[contenthash:10].module.wasm'));
93
93
  // `new URL('./file', import.meta.url)` stays a URL relative to the bundle. Through the asset prefix, a disk path,
94
- // it would parse as a URL of scheme `c:` on Windows rather than a file: URL
94
+ // it would parse as a URL of scheme `c:` on Windows rather than a file: URL. import.meta.url itself is left to the
95
+ // runtime, as import.meta.dirname is, so it names the running bundle rather than the source file on the machine
96
+ // that built it; the pre-entry records the bundle's own path from it, which a process manager's wrapper in
97
+ // process.argv[1] would hide
95
98
  chain.module.parser.merge({
96
99
  javascript: {
97
- url: 'new-url-relative'
100
+ url: 'new-url-relative',
101
+ importMeta: false
98
102
  }
99
103
  });
100
- // The pre-entry records the bundle's own path from import.meta.url, which the bundler would otherwise fix at
101
- // build time to the pre-entry's source file. In every mode: a process manager such as pm2 starts the bundle
102
- // from a wrapper of its own, so process.argv[1] names that wrapper, not the bundle
103
- chain.module.rule('meocord-pre-entry').test(CONFIG_PRE_ENTRY).parser({
104
- importMeta: false
105
- });
106
104
  // Hoisting modules into one scope renames a class whose name another module also declares, and
107
105
  // MeoCord keys cooldowns and names handlers by class name, so a build keeps each module's own scope
108
106
  chain.optimization.concatenateModules(false);
@@ -111,6 +109,12 @@ import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
111
109
  chain.optimization.emitOnErrors(false);
112
110
  // Keeps the bundle starting under Node and Bun alike, whatever devtool or dependencies it has.
113
111
  chain.plugin('meocord-runnable-bundle').use(RunnableBundlePlugin);
112
+ // A package kept out of the bundle loads where its importer runs, after the pre-entry, rather than hoisted above
113
+ // it; typed here, after autoExternal and the externals are merged
114
+ const externals = chain.get('externals');
115
+ if (externals && !options.requireable) chain.externals(typeExternals(Array.isArray(externals) ? externals : [
116
+ externals
117
+ ]));
114
118
  // Says a missing package a dependency only probes for is harmless, and how to silence it
115
119
  chain.plugin('meocord-optional-probes').use(OptionalProbesPlugin);
116
120
  },
@@ -0,0 +1,36 @@
1
+ /**
2
+ * How a package kept out of the bundle is loaded, by how the bundle asks for it: an ESM `import` becomes a dynamic
3
+ * `import()` where its importer runs, rather than one hoisted above the pre-entry, so the package loads after the
4
+ * platform check and after the config has loaded `.env`; a CommonJS `require` stays a `require` at its call site, which
5
+ * returns the module rather than the promise a dynamic import would.
6
+ */ function externalType(request, dependencyType) {
7
+ return `${dependencyType === 'commonjs' ? 'node-commonjs' : 'import'} ${request}`;
8
+ }
9
+ /** Whether an external's result names its own type, as `node-commonjs zlib-sync` does. */ const typed = (result)=>/\s/.test(result);
10
+ /**
11
+ * The externals with every untyped entry given its type by {@link externalType}: a string or pattern, as `autoExternal`
12
+ * and `externals` give them, and a function's untyped result. An object's entries are left as they are.
13
+ */ function typeExternals(items) {
14
+ return items.map((item)=>{
15
+ if (typeof item === 'string' || item instanceof RegExp) {
16
+ const matches = (request)=>typeof item === 'string' ? request === item : item.test(request);
17
+ return (data, callback)=>{
18
+ const { request, dependencyType } = data;
19
+ if (request && matches(request)) return callback(null, externalType(request, dependencyType));
20
+ callback();
21
+ };
22
+ }
23
+ if (typeof item !== 'function') return item;
24
+ const original = item;
25
+ return (data, callback)=>{
26
+ const finish = (result)=>callback(null, typeof result === 'string' && !typed(result) ? externalType(result, data.dependencyType) : result);
27
+ // Callback style, as MeoCord's own native externals are, or a promise
28
+ if (original.length >= 2) {
29
+ return original(data, (error, result)=>error ? callback(error) : finish(result));
30
+ }
31
+ Promise.resolve(original(data)).then(finish, callback);
32
+ };
33
+ });
34
+ }
35
+
36
+ export { externalType, typeExternals };
@@ -254,9 +254,17 @@
254
254
  };
255
255
  });
256
256
  }
257
- /** Drops the keys whose calls have all left their window, once a minute from the store's first call. */ startSweeping() {
257
+ /**
258
+ * Drops the keys whose calls have all left their window, once a minute from a call, until a sweep leaves none: an
259
+ * idle store then holds no timer, so one nothing references is collected, and its next call starts the sweep again.
260
+ */ startSweeping() {
258
261
  if (this.sweeper) return;
259
- this.sweeper = setInterval(()=>sweep(this.calls, Date.now()), SWEEP_INTERVAL_MS);
262
+ this.sweeper = setInterval(()=>{
263
+ sweep(this.calls, Date.now());
264
+ if (this.calls.size > 0) return;
265
+ clearInterval(this.sweeper);
266
+ this.sweeper = undefined;
267
+ }, SWEEP_INTERVAL_MS);
260
268
  // A cooldown must never be what keeps a stopping bot alive.
261
269
  this.sweeper.unref?.();
262
270
  }
@@ -1,4 +1,5 @@
1
1
  import { renderText } from './meocord-text.js';
2
+ import { sharedKey } from '../util/shared-state.util.js';
2
3
 
3
4
  /**
4
5
  * Thrown by a guard to deny a call and tell the user why.
@@ -330,5 +331,29 @@ import { renderText } from './meocord-text.js';
330
331
  this.name = 'CooldownStoreError';
331
332
  }
332
333
  }
334
+ /**
335
+ * Makes `instanceof` hold for an error from either build of this meocord version, ES module or CommonJS: the class's
336
+ * prototype carries a brand both builds share, which `instanceof` on the class itself checks. A subclass, such as an
337
+ * app's own `UserError`, keeps the usual check, so it still matches only its own instances.
338
+ */ function brand(error, name) {
339
+ const key = sharedKey(`error:${name}`);
340
+ Object.defineProperty(error.prototype, key, {
341
+ value: true
342
+ });
343
+ Object.defineProperty(error, Symbol.hasInstance, {
344
+ value (value) {
345
+ if (this !== error) return Function.prototype[Symbol.hasInstance].call(this, value);
346
+ return (typeof value === 'object' || typeof value === 'function') && value !== null && key in value;
347
+ }
348
+ });
349
+ }
350
+ // Named as written, since a minifier may rename the classes themselves
351
+ brand(GuardDeniedError, 'GuardDeniedError');
352
+ brand(UserError, 'UserError');
353
+ brand(CommandNotFoundError, 'CommandNotFoundError');
354
+ brand(ValidationError, 'ValidationError');
355
+ brand(MessageUsageError, 'MessageUsageError');
356
+ brand(CooldownError, 'CooldownError');
357
+ brand(CooldownStoreError, 'CooldownStoreError');
333
358
 
334
359
  export { CommandNotFoundError, CooldownError, CooldownStoreError, GuardDeniedError, MessageUsageError, UserError, ValidationError, cooldownMessage, cooldownStoreMessage, cooldownText, usageHeading };
@@ -1,5 +1,6 @@
1
1
  import { fillPlaceholders, pluralForm, lookup, languageOf, LOCALE_CHAIN, CATALOGS } from './translator.js';
2
2
  import { MEOCORD_MESSAGES } from './meocord-messages.js';
3
+ import { shared } from '../util/shared-state.util.js';
3
4
 
4
5
  const hasCatalogs = (translator)=>translator !== undefined && CATALOGS in translator && LOCALE_CHAIN in translator;
5
6
  const ENGLISH = 'en-US';
@@ -31,16 +32,17 @@ const ENGLISH = 'en-US';
31
32
  return english();
32
33
  }
33
34
  const lists = new Map();
34
- /** A list in a locale's words: "a, b, or c" in en-US, or, for a unit, "a, b, c". */ function listIn(locale, style, items) {
35
- let format = lists.get(`${locale} ${style}`);
36
- if (!format) lists.set(`${locale} ${style}`, format = new Intl.ListFormat(locale, {
37
- type: style === 'or' ? 'disjunction' : 'unit'
35
+ /** An `or` list in a locale's words, such as "a, b, or c" in en-US. */ function orListIn(locale, items) {
36
+ let format = lists.get(locale);
37
+ if (!format) lists.set(locale, format = new Intl.ListFormat(locale, {
38
+ type: 'disjunction'
38
39
  }));
39
40
  return format.format(items);
40
41
  }
41
42
  /**
42
- * A text in `locale`, as {@link findText} finds it. A list in MeoCord's English is joined with its `joiner`, a comma
43
- * unless given; in an app's catalog, in the words of the locale that has it.
43
+ * A text in `locale`, as {@link findText} finds it. An `or` list in an app's catalog reads in the words of the locale
44
+ * that has it; any other list is joined with its `joiner`, a comma unless given, as Intl's `unit` lists join with
45
+ * nothing in zh-CN and with a space in ja.
44
46
  */ function renderText(translator, locale, text) {
45
47
  const found = findText(translator, locale, text.key);
46
48
  if (!found) return text.fallback ?? text.key;
@@ -49,14 +51,15 @@ const lists = new Map();
49
51
  if (!Object.hasOwn(params, name)) return whole;
50
52
  const value = params[name];
51
53
  if (typeof value !== 'object') return String(value);
52
- if ('list' in value) return found.own ? listIn(found.locale, value.style, value.list) : value.list.join(value.joiner ?? ', ');
54
+ if ('list' in value) return found.own && value.style === 'or' ? orListIn(found.locale, value.list) : value.list.join(value.joiner ?? ', ');
53
55
  return renderText(translator, locale, value);
54
56
  });
55
57
  }
56
58
  /** 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) {
57
59
  return (text)=>typeof text !== 'object' ? String(text) : 'list' in text ? text.list.join(text.joiner ?? ', ') : renderText(translator, locale, text);
58
60
  }
59
- const translators = new WeakMap();
61
+ // Shared by both builds of this version, so either answers in the app's language
62
+ const translators = shared('clientTranslators', ()=>new WeakMap());
60
63
  /** Records the translator of the app a client's interactions and messages come to. */ function registerClientTranslator(client, translator) {
61
64
  if (translator) translators.set(client, translator);
62
65
  else translators.delete(client);