meocord 4.2.2 → 4.2.3

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 (136) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/dist/cjs/_shared/{controller.decorator-U2NBWcoU.cjs → controller.decorator-hp5xdJU4.cjs} +512 -183
  3. package/dist/cjs/_shared/{startup-roots-lA5xkBFq.cjs → event-sources-DzqTODXg.cjs} +265 -80
  4. package/dist/cjs/_shared/{providers-B4MOFBrW.cjs → providers-BBvbnmLP.cjs} +780 -281
  5. package/dist/cjs/_shared/{sharded-cooldown-store-HTUb3Ii_.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
  6. package/dist/cjs/_shared/{translate-error-CbBHUu5P.cjs → translate-error-wsgfsUET.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +4 -104
  8. package/dist/cjs/core/index.cjs +432 -181
  9. package/dist/cjs/decorator/index.cjs +85 -120
  10. package/dist/cjs/testing/index.cjs +1664 -784
  11. package/dist/cli.json +1 -1
  12. package/dist/esm/bin/app-template/package.json.template +9 -9
  13. package/dist/esm/bin/app-template/src/main.ts.template +3 -2
  14. package/dist/esm/bin/generator.js +6 -4
  15. package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
  16. package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
  17. package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
  18. package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
  19. package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
  20. package/dist/esm/bin/helper/service-generator.helper.js +6 -4
  21. package/dist/esm/bin/meocord.js +106 -48
  22. package/dist/esm/build/native-addons.js +75 -19
  23. package/dist/esm/build/rsbuild-config.js +11 -3
  24. package/dist/esm/build/typed-externals.js +15 -2
  25. package/dist/esm/common/cooldown-store.js +38 -14
  26. package/dist/esm/common/deprecation.js +4 -1
  27. package/dist/esm/common/execution-context.js +3 -1
  28. package/dist/esm/common/explained-error.js +7 -3
  29. package/dist/esm/common/log-level.js +13 -9
  30. package/dist/esm/common/logger.js +4 -2
  31. package/dist/esm/common/meocord-text.js +11 -6
  32. package/dist/esm/common/redis-cooldown-store.js +3 -7
  33. package/dist/esm/common/response/flags.js +17 -1
  34. package/dist/esm/common/response/install-context.js +27 -16
  35. package/dist/esm/common/response/presenter.js +7 -3
  36. package/dist/esm/common/response/response-state.js +27 -5
  37. package/dist/esm/common/sharded-cooldown-store.js +11 -9
  38. package/dist/esm/common/theme.js +3 -1
  39. package/dist/esm/common/translator.js +17 -8
  40. package/dist/esm/core/autocomplete-routes.js +26 -0
  41. package/dist/esm/core/command-conflicts.js +15 -7
  42. package/dist/esm/core/command-registration.js +15 -1
  43. package/dist/esm/core/cooldown-runner.js +7 -9
  44. package/dist/esm/core/defer.js +2 -5
  45. package/dist/esm/core/dispatcher.js +83 -58
  46. package/dist/esm/core/event-requirements.js +67 -19
  47. package/dist/esm/core/event-sources.js +100 -0
  48. package/dist/esm/core/fallback.js +3 -1
  49. package/dist/esm/core/filter-runner.js +4 -4
  50. package/dist/esm/core/guard-runner.js +173 -52
  51. package/dist/esm/core/handler-input.js +24 -3
  52. package/dist/esm/core/handler-pipeline.js +44 -28
  53. package/dist/esm/core/handler-registry.js +51 -21
  54. package/dist/esm/core/input-runner.js +7 -8
  55. package/dist/esm/core/interceptor-runner.js +4 -5
  56. package/dist/esm/core/lifecycle-hooks.js +3 -8
  57. package/dist/esm/core/lifecycle-order.js +1 -15
  58. package/dist/esm/core/meocord-classes.js +14 -1
  59. package/dist/esm/core/meocord-factory.js +80 -53
  60. package/dist/esm/core/meocord.app.js +174 -89
  61. package/dist/esm/core/message-help.js +5 -9
  62. package/dist/esm/core/message-params.js +126 -46
  63. package/dist/esm/core/message-routes.js +225 -45
  64. package/dist/esm/core/message-words.js +30 -2
  65. package/dist/esm/core/observer-runner.js +2 -1
  66. package/dist/esm/core/process-state.js +19 -0
  67. package/dist/esm/core/providers.js +28 -3
  68. package/dist/esm/core/scalar-types.js +57 -16
  69. package/dist/esm/core/shard-context.js +11 -10
  70. package/dist/esm/core/shard-exit.js +3 -3
  71. package/dist/esm/core/shard-manager.js +16 -6
  72. package/dist/esm/core/startup-roots.js +28 -8
  73. package/dist/esm/core/theme-resolvers.js +26 -11
  74. package/dist/esm/core/theme-runtime.js +23 -13
  75. package/dist/esm/core/theme-scope.js +27 -29
  76. package/dist/esm/core/theme-validation.js +1 -1
  77. package/dist/esm/decorator/app.decorator.js +6 -1
  78. package/dist/esm/decorator/command-builder.decorator.js +14 -0
  79. package/dist/esm/decorator/controller-class.decorator.js +1 -1
  80. package/dist/esm/decorator/controller.decorator.js +48 -19
  81. package/dist/esm/decorator/defer.decorator.js +2 -2
  82. package/dist/esm/decorator/event.decorator.js +4 -5
  83. package/dist/esm/decorator/filter.decorator.js +2 -2
  84. package/dist/esm/decorator/guard.decorator.js +4 -61
  85. package/dist/esm/decorator/inject.decorator.js +4 -1
  86. package/dist/esm/decorator/interceptor.decorator.js +2 -2
  87. package/dist/esm/decorator/theme.decorator.js +4 -3
  88. package/dist/esm/decorator/validation.decorator.js +5 -5
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +29 -13
  91. package/dist/esm/testing/cooldown-store-suite.js +3 -3
  92. package/dist/esm/testing/discord-defaults.js +449 -355
  93. package/dist/esm/testing/inspect-handler.js +91 -10
  94. package/dist/esm/testing/meocord-testing-module.js +195 -77
  95. package/dist/esm/testing/mock-fn.js +8 -2
  96. package/dist/esm/testing/mock-interaction.js +639 -113
  97. package/dist/esm/testing/response-log.js +8 -0
  98. package/dist/esm/testing/response.js +7 -5
  99. package/dist/esm/testing/routing.js +48 -6
  100. package/dist/esm/testing/strict-mocks.js +1 -0
  101. package/dist/esm/testing/theme.js +3 -2
  102. package/dist/esm/util/common.util.js +30 -9
  103. package/dist/esm/util/generator-cli.util.js +6 -4
  104. package/dist/esm/util/inherited-env.util.js +72 -13
  105. package/dist/esm/util/json.util.js +10 -3
  106. package/dist/esm/util/meocord-cli.util.js +3 -2
  107. package/dist/esm/util/meocord-config-loader.util.js +14 -1
  108. package/dist/esm/util/meocord-config-validation.util.js +13 -3
  109. package/dist/esm/util/meocord-source-config.util.js +13 -3
  110. package/dist/esm/util/platform.util.js +3 -2
  111. package/dist/esm/util/refusal.util.js +29 -11
  112. package/dist/esm/util/registration-mode.util.js +14 -1
  113. package/dist/esm/util/timer-limit.util.js +4 -1
  114. package/dist/esm/util/tsconfig-paths.util.js +38 -7
  115. package/dist/esm/util/tsconfig.util.js +16 -58
  116. package/dist/esm/util/user-text.util.js +8 -4
  117. package/dist/esm/util/value.util.js +15 -4
  118. package/dist/types/common/index.d.cts +13 -9
  119. package/dist/types/common/index.d.ts +13 -9
  120. package/dist/types/core/index.d.cts +21 -11
  121. package/dist/types/core/index.d.ts +21 -11
  122. package/dist/types/decorator/index.d.cts +63 -63
  123. package/dist/types/decorator/index.d.ts +63 -63
  124. package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
  125. package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
  126. package/dist/types/interface/index.d.cts +1 -1
  127. package/dist/types/interface/index.d.ts +1 -1
  128. package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
  129. package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
  130. package/dist/types/testing/index.d.cts +137 -54
  131. package/dist/types/testing/index.d.ts +137 -54
  132. package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
  133. package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
  134. package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
  135. package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
  136. package/package.json +5 -2
package/dist/cli.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "meocordVersion": "4.2.2",
3
+ "meocordVersion": "4.2.3",
4
4
  "name": "meocord",
5
5
  "description": "CLI for managing the MeoCord application",
6
6
  "usage": "[options] [command]",
@@ -18,26 +18,26 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "discord.js": "^14.27.0",
21
- "dotenv": "^18.0.5",
21
+ "dotenv": "^18.0.7",
22
22
  "meocord": "^{{version}}"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@eslint/js": "^10.0.1",
26
- "@types/node": "^26.6.3",
27
- "@typescript-eslint/parser": "^8.71.0",
28
- "@vitest/coverage-istanbul": "^5.0.3",
29
- "eslint": "^10.11.0",
26
+ "@types/node": "^26.6.5",
27
+ "@typescript-eslint/parser": "^8.71.2",
28
+ "@vitest/coverage-istanbul": "^5.0.4",
29
+ "eslint": "^10.12.0",
30
30
  "eslint-config-prettier": "^10.1.8",
31
31
  "eslint-import-resolver-typescript": "^4.4.5",
32
32
  "eslint-plugin-import-x": "^4.17.1",
33
- "eslint-plugin-prettier": "^5.5.6",
33
+ "eslint-plugin-prettier": "^5.5.7",
34
34
  "eslint-plugin-unused-imports": "^4.4.1",
35
35
  "globals": "^17.13.0",
36
- "prettier": "^3.9.9",
36
+ "prettier": "^3.9.10",
37
37
  "reflect-metadata": "^0.2.2",
38
38
  "typescript": "^6.0.3",
39
- "typescript-eslint": "^8.71.0",
39
+ "typescript-eslint": "^8.71.2",
40
40
  "unplugin-swc": "^2.0.0",
41
- "vitest": "^5.0.3"
41
+ "vitest": "^5.0.4"
42
42
  }
43
43
  }
@@ -11,8 +11,9 @@ async function bootstrap() {
11
11
  logger.log('Application started')
12
12
  }
13
13
 
14
- // An error MeoCord explained, such as an app it refuses or a token Discord refused, is already logged
14
+ // An error MeoCord explained, such as an app it refuses or a token Discord refused, is already logged, with the exit code set
15
15
  bootstrap().catch(error => {
16
- if (!isExplainedError(error)) logger.error('Error during startup:', error)
16
+ if (isExplainedError(error)) return
17
+ logger.error('Error during startup:', error)
17
18
  process.exitCode = 1
18
19
  })
@@ -4,20 +4,22 @@ import { Argument } from 'commander';
4
4
  import { ControllerType } from '../enum/controller.enum.js';
5
5
  import { ControllerGeneratorHelper } from './helper/controller-generator.helper.js';
6
6
  import { Logger } from '../common/logger.js';
7
- import '../core/theme-scope.js';
8
- import '../core/theme-validation.js';
7
+ import '../common/theme.js';
9
8
  import '../common/decorator.js';
10
9
  import 'reflect-metadata';
11
10
  import 'discord.js';
11
+ import '../common/deprecation.js';
12
12
  import '../common/response/response-state.js';
13
+ import '../core/theme-scope.js';
13
14
  import '../common/errors.js';
15
+ import '../common/cooldown-store.js';
14
16
  import 'node:crypto';
15
17
  import '../decorator/controller.decorator.js';
16
18
  import '../core/scalar-types.js';
17
- import 'node:fs';
18
- import 'node:url';
19
+ import '../util/refusal.util.js';
19
20
  import '../common/translator.js';
20
21
  import '../common/meocord-text.js';
22
+ import '../core/theme-resolvers.js';
21
23
  import { ServiceGeneratorHelper } from './helper/service-generator.helper.js';
22
24
  import { GuardGeneratorHelper } from './helper/guard-generator.helper.js';
23
25
  import { InterceptorGeneratorHelper } from './helper/interceptor-generator.helper.js';
@@ -1,19 +1,21 @@
1
1
  import path from 'path';
2
2
  import { Logger } from '../../common/logger.js';
3
- import '../../core/theme-scope.js';
4
- import '../../core/theme-validation.js';
3
+ import '../../common/theme.js';
5
4
  import '../../common/decorator.js';
6
5
  import 'reflect-metadata';
7
6
  import 'discord.js';
7
+ import '../../common/deprecation.js';
8
8
  import '../../common/response/response-state.js';
9
+ import '../../core/theme-scope.js';
9
10
  import '../../common/errors.js';
11
+ import '../../common/cooldown-store.js';
10
12
  import 'node:crypto';
11
13
  import '../../decorator/controller.decorator.js';
12
14
  import '../../core/scalar-types.js';
13
- import 'node:fs';
14
- import 'node:url';
15
+ import '../../util/refusal.util.js';
15
16
  import '../../common/translator.js';
16
17
  import '../../common/meocord-text.js';
18
+ import '../../core/theme-resolvers.js';
17
19
  import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, writeFiles } from '../../util/generator-cli.util.js';
18
20
 
19
21
  class FilterGeneratorHelper {
@@ -1,19 +1,21 @@
1
1
  import path from 'path';
2
2
  import { Logger } from '../../common/logger.js';
3
- import '../../core/theme-scope.js';
4
- import '../../core/theme-validation.js';
3
+ import '../../common/theme.js';
5
4
  import '../../common/decorator.js';
6
5
  import 'reflect-metadata';
7
6
  import 'discord.js';
7
+ import '../../common/deprecation.js';
8
8
  import '../../common/response/response-state.js';
9
+ import '../../core/theme-scope.js';
9
10
  import '../../common/errors.js';
11
+ import '../../common/cooldown-store.js';
10
12
  import 'node:crypto';
11
13
  import '../../decorator/controller.decorator.js';
12
14
  import '../../core/scalar-types.js';
13
- import 'node:fs';
14
- import 'node:url';
15
+ import '../../util/refusal.util.js';
15
16
  import '../../common/translator.js';
16
17
  import '../../common/meocord-text.js';
18
+ import '../../core/theme-resolvers.js';
17
19
  import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, writeFiles } from '../../util/generator-cli.util.js';
18
20
 
19
21
  class GuardGeneratorHelper {
@@ -1,19 +1,21 @@
1
1
  import path from 'path';
2
2
  import { Logger } from '../../common/logger.js';
3
- import '../../core/theme-scope.js';
4
- import '../../core/theme-validation.js';
3
+ import '../../common/theme.js';
5
4
  import '../../common/decorator.js';
6
5
  import 'reflect-metadata';
7
6
  import 'discord.js';
7
+ import '../../common/deprecation.js';
8
8
  import '../../common/response/response-state.js';
9
+ import '../../core/theme-scope.js';
9
10
  import '../../common/errors.js';
11
+ import '../../common/cooldown-store.js';
10
12
  import 'node:crypto';
11
13
  import '../../decorator/controller.decorator.js';
12
14
  import '../../core/scalar-types.js';
13
- import 'node:fs';
14
- import 'node:url';
15
+ import '../../util/refusal.util.js';
15
16
  import '../../common/translator.js';
16
17
  import '../../common/meocord-text.js';
18
+ import '../../core/theme-resolvers.js';
17
19
  import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, writeFiles } from '../../util/generator-cli.util.js';
18
20
 
19
21
  class InterceptorGeneratorHelper {
@@ -1,19 +1,21 @@
1
1
  import path from 'path';
2
2
  import { Logger } from '../../common/logger.js';
3
- import '../../core/theme-scope.js';
4
- import '../../core/theme-validation.js';
3
+ import '../../common/theme.js';
5
4
  import '../../common/decorator.js';
6
5
  import 'reflect-metadata';
7
6
  import 'discord.js';
7
+ import '../../common/deprecation.js';
8
8
  import '../../common/response/response-state.js';
9
+ import '../../core/theme-scope.js';
9
10
  import '../../common/errors.js';
11
+ import '../../common/cooldown-store.js';
10
12
  import 'node:crypto';
11
13
  import '../../decorator/controller.decorator.js';
12
14
  import '../../core/scalar-types.js';
13
- import 'node:fs';
14
- import 'node:url';
15
+ import '../../util/refusal.util.js';
15
16
  import '../../common/translator.js';
16
17
  import '../../common/meocord-text.js';
18
+ import '../../core/theme-resolvers.js';
17
19
  import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, writeFiles } from '../../util/generator-cli.util.js';
18
20
 
19
21
  class ObserverGeneratorHelper {
@@ -1,19 +1,21 @@
1
1
  import path from 'path';
2
2
  import { Logger } from '../../common/logger.js';
3
- import '../../core/theme-scope.js';
4
- import '../../core/theme-validation.js';
3
+ import '../../common/theme.js';
5
4
  import '../../common/decorator.js';
6
5
  import 'reflect-metadata';
7
6
  import 'discord.js';
7
+ import '../../common/deprecation.js';
8
8
  import '../../common/response/response-state.js';
9
+ import '../../core/theme-scope.js';
9
10
  import '../../common/errors.js';
11
+ import '../../common/cooldown-store.js';
10
12
  import 'node:crypto';
11
13
  import '../../decorator/controller.decorator.js';
12
14
  import '../../core/scalar-types.js';
13
- import 'node:fs';
14
- import 'node:url';
15
+ import '../../util/refusal.util.js';
15
16
  import '../../common/translator.js';
16
17
  import '../../common/meocord-text.js';
18
+ import '../../core/theme-resolvers.js';
17
19
  import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, writeFiles } from '../../util/generator-cli.util.js';
18
20
 
19
21
  class PipeGeneratorHelper {
@@ -1,19 +1,21 @@
1
1
  import path from 'path';
2
2
  import { Logger } from '../../common/logger.js';
3
- import '../../core/theme-scope.js';
4
- import '../../core/theme-validation.js';
3
+ import '../../common/theme.js';
5
4
  import '../../common/decorator.js';
6
5
  import 'reflect-metadata';
7
6
  import 'discord.js';
7
+ import '../../common/deprecation.js';
8
8
  import '../../common/response/response-state.js';
9
+ import '../../core/theme-scope.js';
9
10
  import '../../common/errors.js';
11
+ import '../../common/cooldown-store.js';
10
12
  import 'node:crypto';
11
13
  import '../../decorator/controller.decorator.js';
12
14
  import '../../core/scalar-types.js';
13
- import 'node:fs';
14
- import 'node:url';
15
+ import '../../util/refusal.util.js';
15
16
  import '../../common/translator.js';
16
17
  import '../../common/meocord-text.js';
18
+ import '../../core/theme-resolvers.js';
17
19
  import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, writeFiles } from '../../util/generator-cli.util.js';
18
20
 
19
21
  class ServiceGeneratorHelper {
@@ -3,25 +3,29 @@ import path from 'path';
3
3
  import { createRsbuild } from '@rsbuild/core';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { Logger } from '../common/logger.js';
6
- import '../core/theme-scope.js';
7
- import '../core/theme-validation.js';
6
+ import '../common/theme.js';
8
7
  import '../common/decorator.js';
9
8
  import 'reflect-metadata';
10
9
  import 'discord.js';
10
+ import '../common/deprecation.js';
11
11
  import '../common/response/response-state.js';
12
+ import '../core/theme-scope.js';
12
13
  import '../common/errors.js';
14
+ import '../common/cooldown-store.js';
13
15
  import '../decorator/controller.decorator.js';
14
16
  import '../core/scalar-types.js';
15
- import * as fs from 'node:fs';
16
- import 'node:url';
17
+ import '../util/refusal.util.js';
17
18
  import '../common/translator.js';
18
19
  import '../common/meocord-text.js';
20
+ import '../core/theme-resolvers.js';
19
21
  import { spawn } from 'node:child_process';
22
+ import { constants } from 'node:os';
20
23
  import { capitalize } from 'lodash-es';
21
24
  import wait from '../util/wait.util.js';
22
25
  import { GeneratorCLI } from './generator.js';
23
26
  import { runtimePrefixFor, AppGeneratorHelper } from './helper/app-generator.helper.js';
24
27
  import { makeInitialCommit } from './helper/initial-commit.helper.js';
28
+ import * as fs from 'node:fs';
25
29
  import { setEnvironment, compileAndValidateConfig, validateRunConfig, validateDiscordToken, checkSourceConfig, reportConfigProblem } from '../util/common.util.js';
26
30
  import { Command } from 'commander';
27
31
  import { execSync } from 'child_process';
@@ -37,11 +41,11 @@ import { isDevRunnerMessage, DEV_RUNNER_ENV } from '../util/dev-runner.util.js';
37
41
  import packageJson from '../package.json.js';
38
42
  import { fileURLToPath } from 'url';
39
43
  import { assertNoWebpackHook, createRsbuildConfig, optionalExternalConflicts, optionalExternalNames } from '../build/rsbuild-config.js';
40
- import { createNativeExternals, packageNameOfRequest, installedPackages, copyPackagesInto, bundledModuleFiles, assertNoBundledNativeAddons, findBundledNativeAddons } from '../build/native-addons.js';
44
+ import { createNativeExternals, packageNameOfRequest, installedPackages, copyPackagesInto, bundledModuleFiles, assertNoBundledNativeAddons, findBundledNativeAddons, otherCopiesWarning } from '../build/native-addons.js';
41
45
  import { writePlatformManifest, PLATFORM_MANIFEST } from '../util/platform.util.js';
42
46
  import { loadMeoCordSourceConfig } from '../util/meocord-source-config.util.js';
43
47
  import { loadMeoCordConfig } from '../util/meocord-config-loader.util.js';
44
- import { FORCE_REGISTER_ENV, REGISTER_ONLY_ENV, REGISTER_GUILD_ENV } from '../util/registration-mode.util.js';
48
+ import { FORCE_REGISTER_ENV, REGISTER_ONLY_ENV, REGISTER_GUILD_ENV, REGISTER_MODE_ENV } from '../util/registration-mode.util.js';
45
49
 
46
50
  const __filename$1 = fileURLToPath(import.meta.url);
47
51
  const __dirname$1 = path.dirname(__filename$1);
@@ -116,18 +120,22 @@ const UNDECOMPOSED = {
116
120
  */ function appDirectoryName(appName) {
117
121
  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
122
  }
123
+ /** How long watch mode waits after the last save to a file it reloads from before reloading. */ const RELOAD_DEBOUNCE_MS = 300;
124
+ /** The name watch mode taps the compiler's hooks under. */ const WATCH_HOOK_NAME = 'MeoCordWatch';
125
+ /** What a shell adds to a signal's number for the exit code of a process that signal ended. */ const SIGNAL_EXIT_BASE = 128;
119
126
  /** A Command Line Interface (CLI) for managing the MeoCord application. */ class MeoCordCLI {
120
127
  /**
121
128
  * Configures and runs the MeoCord CLI.
122
129
  */ async run() {
123
- this.program().showHelpAfterError().parse(process.argv);
130
+ this.program().parse(process.argv);
124
131
  }
125
132
  /**
126
133
  * The CLI's commands, options and arguments, configured but not parsed: what `run` parses, and what the build
127
134
  * describes in `dist/cli.json`, so the two cannot differ.
128
135
  */ program() {
129
136
  let program = new Command();
130
- program.name(this.appName.toLowerCase()).description(`CLI for managing the ${this.appName} application`).version(this.version);
137
+ // Before any command is added: commander copies it to each command as the command is made
138
+ program.name(this.appName.toLowerCase()).description(`CLI for managing the ${this.appName} application`).version(this.version).showHelpAfterError();
131
139
  program.command('show').description('Display information').option('-w, --warranty', 'Display warranty disclaimer').option('-c, --license', 'Display license').action((options)=>{
132
140
  if (!options.warranty && !options.license) {
133
141
  const show = `${program.name()} show`;
@@ -438,6 +446,9 @@ copies or substantial portions of the Software.
438
446
  // A safety net behind the externals function: a native addon that still reached the
439
447
  // bundle would run on this machine -- through its node_modules -- and fail in production.
440
448
  assertNoBundledNativeAddons(findBundledNativeAddons(bundledFiles, this.projectRoot));
449
+ for (const [name, others] of natives.otherCopies){
450
+ this.logger.warn(otherCopiesWarning(name, natives.found.get(name).dir, others, this.projectRoot));
451
+ }
441
452
  this.packDependencies(meocordConfig, natives.found);
442
453
  } else if (!meocordConfig?.bundleDependencies) {
443
454
  this.removePackedOutputs();
@@ -563,7 +574,7 @@ copies or substantial portions of the Software.
563
574
  this.appEnv[REGISTER_ONLY_ENV] = '1';
564
575
  this.appEnv[FORCE_REGISTER_ENV] = '1';
565
576
  if (guild) this.appEnv[REGISTER_GUILD_ENV] = guild;
566
- const child = this.spawnApp().on('exit', (code)=>process.exit(code ?? 1));
577
+ const child = this.spawnApp().on('exit', (code, signal)=>process.exit(this.exitCodeOf(code, signal, 1)));
567
578
  this.relayStopSignals(()=>child);
568
579
  }
569
580
  /**
@@ -581,6 +592,7 @@ copies or substantial portions of the Software.
581
592
  const kind = request();
582
593
  if (kind === 'duplicate') return;
583
594
  if (kind === 'first') stopping?.();
595
+ this.stopRelayed = true;
584
596
  const child = app();
585
597
  if (!stillRunning(child)) {
586
598
  if (kind === 'repeat') process.exit(1);
@@ -598,6 +610,15 @@ copies or substantial portions of the Software.
598
610
  }
599
611
  }
600
612
  /**
613
+ * The code the CLI exits with once the application has: its own, or for one a signal ended that the CLI didn't pass
614
+ * on, 128 plus the signal's number, as a shell gives, so a bot killed from outside doesn't read as a clean exit.
615
+ * @param fallback - For an application that a stop the CLI passed on ended without a code.
616
+ */ exitCodeOf(code, signal, fallback) {
617
+ if (code !== null) return code;
618
+ const number = signal ? constants.signals[signal] : undefined;
619
+ return number !== undefined && !this.stopRelayed ? SIGNAL_EXIT_BASE + number : fallback;
620
+ }
621
+ /**
601
622
  * Replaces the running application once it has exited, since two would contend for one gateway session. Builds that
602
623
  * finish meanwhile start nothing more: the replacement runs `dist/main.js` as it stands at launch, the latest build.
603
624
  * @param build - The digest of the build asking for it; the output the running bot was launched from leaves it running.
@@ -672,7 +693,7 @@ copies or substantial portions of the Software.
672
693
  const { command, args } = buildAppCommand(this.runtime, this.mainJSPath, {
673
694
  sourceMaps
674
695
  });
675
- return spawn(command, args, {
696
+ const child = spawn(command, args, {
676
697
  cwd: this.projectRoot,
677
698
  env: {
678
699
  ...this.inheritedEnv,
@@ -680,6 +701,8 @@ copies or substantial portions of the Software.
680
701
  ...process.env.NODE_ENV !== undefined && {
681
702
  NODE_ENV: process.env.NODE_ENV
682
703
  },
704
+ // And where it registers, which a bundle can't read from NODE_ENV: bundleDependencies writes in the build's mode
705
+ [REGISTER_MODE_ENV]: process.env.NODE_ENV === 'development' ? 'development' : 'production',
683
706
  ...this.appEnv,
684
707
  ...devRunner && {
685
708
  [DEV_RUNNER_ENV]: '1'
@@ -692,6 +715,16 @@ copies or substantial portions of the Software.
692
715
  'ipc'
693
716
  ] : 'inherit'
694
717
  });
718
+ // A runtime that can't be started, such as a path that isn't there, is reported as an event, past any caller's try.
719
+ // So is a signal or a message the running application couldn't be sent; its exit, when it comes, still decides the code
720
+ child.on('error', (error)=>{
721
+ if (child.pid !== undefined) {
722
+ return this.logger.error(`Could not signal or message the running application (process ${child.pid}): ${error.message}`);
723
+ }
724
+ this.logger.error(`Failed to start: ${command} could not be run: ${error.message}`);
725
+ process.exit(1);
726
+ });
727
+ return child;
695
728
  }
696
729
  /**
697
730
  * Starts the MeoCord application in development mode with live updates.
@@ -702,28 +735,41 @@ copies or substantial portions of the Software.
702
735
  await this.compileConfig({
703
736
  mode: 'development'
704
737
  });
705
- let isRunning = false;
706
738
  let watching;
739
+ // Settles once the running build has emitted, whatever its errors; a reload waits for it before replacing the build
740
+ let built = Promise.resolve();
707
741
  // The new bundler is made, its plugins set up and its config resolved, before the running build is closed: a
708
742
  // config whose rsbuild hook or plugin throws leaves that build watching the sources, and the bot it started running
709
743
  const watch = async ()=>{
710
- const { rsbuild, meocordConfig } = await this.createBundler('development');
744
+ const { rsbuild } = await this.createBundler('development');
711
745
  await rsbuild.initConfigs({
712
746
  action: 'build'
713
747
  });
714
748
  // Runs after every rebuild, which is where the application is restarted. A rebuild that fails reports its own
715
749
  // errors, emits nothing, and leaves the running bot alone rather than replacing it with a broken build.
750
+ let emitted;
751
+ const firstBuild = new Promise((resolve)=>emitted = resolve);
716
752
  rsbuild.onAfterBuild(({ stats })=>{
717
- isRunning = true;
753
+ emitted();
718
754
  if (stats?.hasErrors()) return;
719
- if (!meocordConfig?.bundleDependencies) this.removePackedOutputs();
755
+ // A watch build packs nothing, so a previous build's packed copies would shadow the project's packages
756
+ this.removePackedOutputs();
720
757
  this.latestBuild = stats ? emittedDigest(stats) : undefined;
721
758
  this.restartApp(this.latestBuild);
722
759
  });
760
+ // A compile that fails outright never reaches onAfterBuild, so it settles the wait too: a later save still reloads
761
+ rsbuild.onAfterCreateCompiler(({ compiler })=>{
762
+ for (const each of 'compilers' in compiler ? compiler.compilers : [
763
+ compiler
764
+ ]){
765
+ each.hooks.failed.tap(WATCH_HOOK_NAME, ()=>emitted());
766
+ }
767
+ });
723
768
  await watching?.close();
724
769
  watching = await rsbuild.build({
725
770
  watch: true
726
771
  });
772
+ built = firstBuild;
727
773
  };
728
774
  await watch();
729
775
  // Inputs the bundler does not see: the config, and tsconfig.json, which the build reads through a copy MeoCord
@@ -743,42 +789,53 @@ copies or substantial portions of the Software.
743
789
  ].some((file)=>!restarts.includes(file));
744
790
  let debounceWatcher;
745
791
  let changed = new Set();
792
+ // One reload at a time, each taking every file changed since the last began
793
+ const reload = async ()=>{
794
+ const files = changed;
795
+ changed = new Set();
796
+ if (this.stopping || files.size === 0) return;
797
+ for (const file of files)this.logger.log(reloads[file]);
798
+ if (!rebuilds(files)) return this.restartApp();
799
+ // A config that is refused, as at startup, or doesn't compile, say mid-edit, leaves the running bot and its build
800
+ // as they are
801
+ if (files.has('meocord.config.ts')) {
802
+ const checked = checkSourceConfig();
803
+ if ('problem' in checked) {
804
+ // One that doesn't load, say mid-edit, is reported as a failed compile is, and watching goes on
805
+ if (checked.loadError !== undefined) return this.logger.error(`Failed to compile meocord.config.ts: ${checked.loadError}`);
806
+ return reportConfigProblem(checked.problem);
807
+ }
808
+ if (!await this.compileConfig({
809
+ mode: 'development',
810
+ exitOnFailure: false
811
+ })) return;
812
+ }
813
+ // The new build restarts the bot, whatever its output: the config it was launched with has changed
814
+ this.launchedFrom = undefined;
815
+ try {
816
+ await watch();
817
+ } catch (error) {
818
+ // As with a failed login: the bot keeps running, and saving the file again tries again
819
+ this.logger.error(`Rebuilding failed: ${error instanceof Error ? error.message : String(error)}. The bot keeps running its last ` + 'build; save the file again to retry.');
820
+ }
821
+ };
822
+ let reloading = Promise.resolve();
823
+ // Whether a reload waits its turn: a later save joins it rather than queueing one of its own
824
+ let queued = false;
825
+ const queueReload = ()=>{
826
+ if (queued) return;
827
+ queued = true;
828
+ reloading = reloading.then(()=>built).then(()=>{
829
+ queued = false;
830
+ return reload();
831
+ }).catch((error)=>this.logger.error('Reloading failed:', error));
832
+ };
746
833
  // The folder rather than the files, so an editor that saves by replacing a file is still seen
747
834
  const fsWatcher = fs.watch(this.projectRoot, (_event, filename)=>{
748
835
  if (!filename || !Object.hasOwn(reloads, filename)) return;
749
836
  changed.add(filename);
750
837
  clearTimeout(debounceWatcher);
751
- debounceWatcher = setTimeout(async ()=>{
752
- if (!isRunning) return;
753
- const files = changed;
754
- changed = new Set();
755
- for (const file of files)this.logger.log(reloads[file]);
756
- if (!rebuilds(files)) return this.restartApp();
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
- }
771
- isRunning = false;
772
- // The new build restarts the bot, whatever its output: the config it was launched with has changed
773
- this.launchedFrom = undefined;
774
- try {
775
- await watch();
776
- } catch (error) {
777
- // As with a failed login: the bot keeps running, and saving the file again tries again
778
- isRunning = true;
779
- this.logger.error(`Rebuilding failed: ${error instanceof Error ? error.message : String(error)}. The bot keeps running its last ` + 'build; save the file again to retry.');
780
- }
781
- }, 300);
838
+ debounceWatcher = setTimeout(queueReload, RELOAD_DEBOUNCE_MS);
782
839
  });
783
840
  const stopWatching = ()=>{
784
841
  this.stopping = true;
@@ -818,8 +875,8 @@ copies or substantial portions of the Software.
818
875
  }
819
876
  this.logger.log('Starting...');
820
877
  const start = this.spawnApp();
821
- start.on('exit', (code)=>{
822
- process.exit(code ?? 0);
878
+ start.on('exit', (code, signal)=>{
879
+ process.exit(this.exitCodeOf(code, signal, 0));
823
880
  });
824
881
  this.relayStopSignals(()=>start);
825
882
  } catch (error) {
@@ -859,6 +916,7 @@ copies or substantial portions of the Software.
859
916
  /** Binary the application is spawned with: the runtime the user chose, as {@link resolveRuntime} finds it. */ this.runtime = resolveRuntime(process.env, process.execPath);
860
917
  /** What a bot this CLI starts inherits, taken before any config is loaded; see {@link inheritedEnvironment}. */ this.inheritedEnv = inheritedEnvironment(process.env, this.projectRoot);
861
918
  /** Environment added to the application's own when it is spawned. */ this.appEnv = {};
919
+ /** Whether the CLI has passed a stop signal on to the application, whose death by it is then the stop asked for. */ this.stopRelayed = false;
862
920
  /** The running application, while a watch session owns one, including while it exits to be replaced. */ this.appProcess = null;
863
921
  /** Whether the running application is exiting to be replaced; a build that finishes meanwhile joins that restart. */ this.restarting = false;
864
922
  /** Whether a watch session is stopping, when a rebuild must not start the application again. */ this.stopping = false;
@@ -895,4 +953,4 @@ if (isProcessEntry()) {
895
953
  });
896
954
  }
897
955
 
898
- export { MeoCordCLI, appDirectoryName, emittedDigest, stillRunning };
956
+ export { MeoCordCLI, RELOAD_DEBOUNCE_MS, appDirectoryName, emittedDigest, stillRunning };