meocord 4.2.1 → 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 (137) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
  3. package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
  4. package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
  5. package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
  6. package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +4 -104
  8. package/dist/cjs/core/index.cjs +435 -182
  9. package/dist/cjs/decorator/index.cjs +116 -121
  10. package/dist/cjs/testing/index.cjs +1923 -808
  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 +32 -10
  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 +27 -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 +92 -66
  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 +6 -6
  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 +54 -32
  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 +83 -54
  60. package/dist/esm/core/meocord.app.js +174 -89
  61. package/dist/esm/core/message-help.js +37 -15
  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 +4 -2
  66. package/dist/esm/core/process-state.js +19 -0
  67. package/dist/esm/core/providers.js +39 -4
  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/stage-scope.js +2 -1
  73. package/dist/esm/core/startup-roots.js +28 -8
  74. package/dist/esm/core/theme-resolvers.js +26 -11
  75. package/dist/esm/core/theme-runtime.js +23 -13
  76. package/dist/esm/core/theme-scope.js +31 -30
  77. package/dist/esm/core/theme-validation.js +1 -1
  78. package/dist/esm/decorator/app.decorator.js +37 -1
  79. package/dist/esm/decorator/command-builder.decorator.js +14 -0
  80. package/dist/esm/decorator/controller-class.decorator.js +1 -1
  81. package/dist/esm/decorator/controller.decorator.js +48 -19
  82. package/dist/esm/decorator/defer.decorator.js +2 -2
  83. package/dist/esm/decorator/event.decorator.js +4 -5
  84. package/dist/esm/decorator/filter.decorator.js +4 -4
  85. package/dist/esm/decorator/guard.decorator.js +4 -61
  86. package/dist/esm/decorator/inject.decorator.js +4 -1
  87. package/dist/esm/decorator/interceptor.decorator.js +2 -2
  88. package/dist/esm/decorator/theme.decorator.js +4 -3
  89. package/dist/esm/decorator/validation.decorator.js +5 -5
  90. package/dist/esm/package.json.js +1 -1
  91. package/dist/esm/testing/catalog.js +32 -25
  92. package/dist/esm/testing/cooldown-store-suite.js +3 -3
  93. package/dist/esm/testing/discord-defaults.js +449 -335
  94. package/dist/esm/testing/inspect-handler.js +91 -10
  95. package/dist/esm/testing/meocord-testing-module.js +217 -91
  96. package/dist/esm/testing/mock-fn.js +8 -2
  97. package/dist/esm/testing/mock-interaction.js +864 -124
  98. package/dist/esm/testing/response-log.js +8 -0
  99. package/dist/esm/testing/response.js +7 -5
  100. package/dist/esm/testing/routing.js +48 -6
  101. package/dist/esm/testing/strict-mocks.js +8 -4
  102. package/dist/esm/testing/theme.js +3 -2
  103. package/dist/esm/util/common.util.js +30 -9
  104. package/dist/esm/util/generator-cli.util.js +6 -4
  105. package/dist/esm/util/inherited-env.util.js +72 -13
  106. package/dist/esm/util/json.util.js +10 -3
  107. package/dist/esm/util/meocord-cli.util.js +3 -2
  108. package/dist/esm/util/meocord-config-loader.util.js +14 -1
  109. package/dist/esm/util/meocord-config-validation.util.js +13 -3
  110. package/dist/esm/util/meocord-source-config.util.js +13 -3
  111. package/dist/esm/util/platform.util.js +3 -2
  112. package/dist/esm/util/refusal.util.js +29 -11
  113. package/dist/esm/util/registration-mode.util.js +14 -1
  114. package/dist/esm/util/timer-limit.util.js +4 -1
  115. package/dist/esm/util/tsconfig-paths.util.js +38 -7
  116. package/dist/esm/util/tsconfig.util.js +16 -58
  117. package/dist/esm/util/user-text.util.js +8 -4
  118. package/dist/esm/util/value.util.js +33 -5
  119. package/dist/types/common/index.d.cts +17 -10
  120. package/dist/types/common/index.d.ts +17 -10
  121. package/dist/types/core/index.d.cts +21 -11
  122. package/dist/types/core/index.d.ts +21 -11
  123. package/dist/types/decorator/index.d.cts +63 -63
  124. package/dist/types/decorator/index.d.ts +63 -63
  125. package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
  126. package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
  127. package/dist/types/interface/index.d.cts +1 -1
  128. package/dist/types/interface/index.d.ts +1 -1
  129. package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
  130. package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
  131. package/dist/types/testing/index.d.cts +158 -61
  132. package/dist/types/testing/index.d.ts +158 -61
  133. package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
  134. package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
  135. package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
  136. package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
  137. package/package.json +5 -2
@@ -0,0 +1,8 @@
1
+ import { sharedName } from '../util/shared-state.util.js';
2
+
3
+ /**
4
+ * Where a mock interaction keeps every answer it got, through respond() or discord.js directly, in order, under a key
5
+ * both builds of this meocord version read.
6
+ */ const RESPONSE_LOG = Symbol.for(sharedName('responseLog'));
7
+
8
+ export { RESPONSE_LOG };
@@ -1,7 +1,7 @@
1
1
  import { DiscordAPIError } from 'discord.js';
2
2
  import { existingResponse } from '../common/response/response-state.js';
3
3
  import { callOrder } from '../common/response/call-order.js';
4
- import { RESPONSE_LOG } from './mock-interaction.js';
4
+ import { RESPONSE_LOG } from './response-log.js';
5
5
 
6
6
  const VISIBLE = new Set([
7
7
  'reply',
@@ -14,9 +14,10 @@ const VISIBLE = new Set([
14
14
  * Reports how an interaction was answered: where its answer stands, and every answer call it got, in order.
15
15
  *
16
16
  * Use it after `invoke` or `dispatch` to check what the member sees. A mock interaction records each `reply`,
17
- * `deferReply`, `editReply`, `followUp`, `deleteReply`, `update`, `deferUpdate` and `showModal`, whether the handler
18
- * made it through `respond()` or with discord.js directly, so both are reported, each once. For a message or a
19
- * reaction, read the mock's own methods.
17
+ * `deferReply`, `editReply`, `followUp`, `deleteReply`, `update`, `deferUpdate` and `showModal`, and under
18
+ * `useStrictMocks()` each `launchActivity`, `sendPremiumRequired` and autocomplete `respond` too, whether the handler made
19
+ * it through `respond()` or with discord.js directly, so both are reported, each once. For a message or a reaction, read
20
+ * the mock's own methods.
20
21
  *
21
22
  * @param interaction - The interaction a handler answered.
22
23
  * @returns The state, whether anything the member can see was sent, and the calls, in order.
@@ -61,7 +62,8 @@ const VISIBLE = new Set([
61
62
  sent,
62
63
  calls: calls
63
64
  };
64
- const answered = interaction.isRepliable() ? interaction.replied : false;
65
+ // An autocomplete's choices are its answer, as observers read it, where its mock records them, under strict mocks
66
+ const answered = interaction.isRepliable() ? interaction.replied : Boolean(log) && interaction.isAutocomplete() && interaction.responded;
65
67
  const deferred = interaction.isRepliable() ? interaction.deferred : false;
66
68
  return {
67
69
  state: answered ? 'replied' : deferred ? 'deferred' : 'unanswered',
@@ -1,10 +1,30 @@
1
1
  import { findComponentRouteConflicts, buildComponentRoutes, matchComponentRoute, readCustomId } from '../core/component-routes.js';
2
2
  import { isCustomIdRouted } from '../util/interaction.util.js';
3
- import { staticMessageStarts, matchMessageRoute, buildMessageRoutes, fitsScope } from '../core/message-routes.js';
4
- import { needsGuild } from '../core/message-params.js';
3
+ import { staticMessageStarts, buildMessageRoutes, matchMessageRoute, fitsScope } from '../core/message-routes.js';
4
+ import { takesGuildParam, needsGuild, isGuildType } from '../core/message-params.js';
5
+ import { warnOnce } from '../common/deprecation.js';
6
+ import { Logger } from '../common/logger.js';
7
+ import { strictMocks } from './strict-mocks.js';
5
8
  import { META } from '../util/metadata-keys.js';
9
+ import { describeValue } from '../util/value.util.js';
6
10
 
11
+ const logger = new Logger('resolveRoute');
12
+ /** Why an app import reads `undefined`, for its refusal. */ const CIRCULAR_IMPORT_HINT = ' An app import that reads undefined is often a circular import.';
13
+ /** Why an app is an object, for its refusal. */ const MODULE_HINT = ' An object here is often a module namespace, as `import * as App` gives: pass the class it exports.';
14
+ /** An app that isn't a class, as its refusal names it: the value, and its likely cause where there is one. */ function notAClass(app) {
15
+ const hint = app === undefined ? CIRCULAR_IMPORT_HINT : typeof app === 'object' && app !== null ? MODULE_HINT : '';
16
+ return {
17
+ name: describeValue(app),
18
+ hint
19
+ };
20
+ }
21
+ /** Whether a message gives a route's `member`, `role` or `channel` flag, which dispatch refuses in a DM. */ const givesGuildFlag = (route, params)=>route.flags.some((flag)=>flag.type !== undefined && isGuildType(flag.type) && Object.hasOwn(params, flag.flag));
7
22
  /** The options `@MeoCord` declares on an application class. */ function appOptionsOf(app) {
23
+ // An app that isn't a class at all, as a circular import gives, has no metadata to read
24
+ if (typeof app !== 'function') {
25
+ const { name, hint } = notAClass(app);
26
+ throw new TypeError(`${name} is not decorated with @MeoCord().${hint}`);
27
+ }
8
28
  const options = Reflect.getMetadata(META.appOptions, app);
9
29
  if (!options) throw new TypeError(`${app.name || 'The given class'} is not decorated with @MeoCord().`);
10
30
  return options;
@@ -52,11 +72,33 @@ import { META } from '../util/metadata-keys.js';
52
72
  if ('content' in input) {
53
73
  const { messages = {} } = appOptionsOf(app);
54
74
  const starts = staticMessageStarts(app, messages, input);
55
- const matched = matchMessageRoute(buildMessageRoutes(controllersOf(app), messages), input.content, starts);
75
+ const routes = buildMessageRoutes(controllersOf(app), messages);
76
+ let matched = matchMessageRoute(routes, input.content, starts);
77
+ const named = (route)=>`${route.controllerClass.name}.${String(route.method)}`;
78
+ // Without `dm`: under strict mocks, read as sent in a server, as dispatch routes such a message whatever the
79
+ // controllers' order; otherwise as from anywhere, warning where that answers differently
80
+ if (starts.inGuild === undefined) {
81
+ const inServer = matchMessageRoute(routes, input.content, {
82
+ ...starts,
83
+ inGuild: true
84
+ });
85
+ if (strictMocks()) matched = inServer && fitsScope(inServer.route.scope, true) ? inServer : undefined;
86
+ else if (matched && inServer && matched.route !== inServer.route && !fitsScope(matched.route.scope, true)) {
87
+ warnOnce(logger, `resolveRoute resolved a message without dm to ${named(matched.route)}, which works in a DM alone, where dispatch runs ` + `${named(inServer.route)} for a message sent in a server. Pass dm: true for a DM. Under useStrictMocks() it resolves to ` + `${named(inServer.route)}.`);
88
+ } else if (matched && !fitsScope(matched.route.scope, true)) {
89
+ // Only a handler that works in a DM alone takes it, which dispatch runs in a DM only
90
+ warnOnce(logger, `resolveRoute found only ${named(matched.route)}, which works in a DM alone, for a message without dm; dispatch runs it ` + 'only in a DM. Pass dm: true. Under useStrictMocks() this resolves to undefined.');
91
+ }
92
+ }
56
93
  if (!matched) return undefined;
57
94
  const { route, params } = matched;
58
- // Dispatch answers a route that does not work in a DM with its usage, and never runs it
59
- if (starts.inGuild === false && (!fitsScope(route.scope, false) || needsGuild(route))) return undefined;
95
+ // Dispatch never runs a route that does not work in a DM; one sent with a prefix or mention is told it works in a server only
96
+ if (starts.inGuild === false && (!fitsScope(route.scope, false) || takesGuildParam(route))) return undefined;
97
+ // Nor one with a required flag only a server has, or given one: refused under strict mocks, warned about otherwise
98
+ if (starts.inGuild === false && (needsGuild(route) || givesGuildFlag(route, params))) {
99
+ if (strictMocks()) return undefined;
100
+ warnOnce(logger, `resolveRoute resolved a DM to ${named(route)}, which dispatch answers "This command ` + 'works in a server only.": a member, role or channel flag is found only in a server. Under useStrictMocks() this ' + 'resolves to undefined.');
101
+ }
60
102
  return {
61
103
  controller: route.controllerClass,
62
104
  method: route.method,
@@ -140,4 +182,4 @@ import { META } from '../util/metadata-keys.js';
140
182
  return target;
141
183
  }
142
184
 
143
- export { findRouteConflicts, resolveRoute };
185
+ export { findRouteConflicts, notAClass, resolveRoute };
@@ -19,8 +19,9 @@ const settings = shared('strictMocks', ()=>({
19
19
  * and `message.thread` is `null` when the channel caches no thread under the message's id. So do a message's
20
20
  * `editedAt`, a member's `presence`, a server's `verified` and `systemChannel` and a channel's `parent`, each `null` or
21
21
  * `false` until the test gives what it is computed from. A reaction's `me` is `false`, a message's
22
- * `mentions.repliedUser` and a modal's `message` are `null`, and a reaction has a whole message of its own, so
23
- * MeoCord's dispatcher fetches neither. No placeholder warning is logged.
22
+ * `mentions.repliedUser` and a modal's `message` are `null`, an interaction's `ephemeral` is `null` until `reply()` or
23
+ * `deferReply()` decides it, and a reaction has a whole message of its own, so MeoCord's dispatcher fetches neither. No
24
+ * placeholder warning is logged.
24
25
  *
25
26
  * @remarks
26
27
  * Those getters read what a server holds, so strict mocks also have it as Discord sends it: the bot's member is in the
@@ -33,11 +34,14 @@ const settings = shared('strictMocks', ()=>({
33
34
  * A message the bot sends is its `client.user`'s, and one it sends in a DM is in that DM. Give the bot's member a role
34
35
  * to let it do more, such as kick a member its role ranks above. A value the test sets on a mock wins over the computed
35
36
  * one. An interaction's answer that discord.js would refuse, such as a second `reply()` after one a test set with
36
- * `mockResolvedValue`, is refused too, where without it the mock runs it and warns. The next major version (5.0)
37
- * computes these values, and refuses those answers, without the call.
37
+ * `mockResolvedValue`, is refused too, where without it the mock runs it and warns. An answer marks the interaction
38
+ * `replied`, `deferred` or `responded` once it resolves, as discord.js does, so code that doesn't await one fails as it
39
+ * would on a bot, within one run of the test's code. The next major version (5.0) computes these values, and refuses
40
+ * those answers, without the call.
38
41
  *
39
42
  * `invoke` also refuses a call that gives a handler fewer arguments than it declares where it builds none, such as a
40
43
  * reaction handler given its reaction without its `ReactionEvent`; without strict mocks, it warns once and calls it.
44
+ * It refuses an autocomplete that dispatch gives to another `@Autocomplete` handler, or to none, the same way.
41
45
  *
42
46
  * `compile()` also refuses an `overrideGuard`, `overrideInterceptor` or `overrideFilter` stub without the stage's
43
47
  * method, `canActivate`, `intercept` or `catch`; without strict mocks, it warns and builds the module.
@@ -1,8 +1,9 @@
1
1
  import { assertValidTheme } from '../core/theme-validation.js';
2
2
  import { defaultTheme, runWithTheme, mergeTheme, copyLayer } from '../core/theme-scope.js';
3
+ import { shared } from '../util/shared-state.util.js';
3
4
 
4
- // Themes createMockTheme made, which withTheme uses as they are
5
- const mocks = new WeakSet();
5
+ // Themes createMockTheme made through either build of this version, which withTheme uses as they are
6
+ const mocks = shared('mockThemes', ()=>new WeakSet());
6
7
  function mockTheme(overrides, where) {
7
8
  if (overrides === undefined) return defaultTheme();
8
9
  assertValidTheme(overrides, where);
@@ -23,21 +23,29 @@ import chalk from 'chalk';
23
23
  * Loads `meocord.config.ts` and checks its shape, as a command that builds and a watch-mode reload both do, printing
24
24
  * the warnings for options it does not know.
25
25
  *
26
+ * @param options.building - Whether the command builds, which refuses a `webpack` hook; one that runs without building
27
+ * only warns about it.
26
28
  * @returns The config, or the report of why it cannot be used: missing, failing to load, or with options of the wrong
27
29
  * type, each of them listed. A config that fails to load also gives the reason alone, as `loadError`.
28
- */ function checkSourceConfig() {
30
+ */ function checkSourceConfig({ building = true } = {}) {
29
31
  const meocordConfigPath = path.resolve(process.cwd(), 'meocord.config.ts');
30
32
  if (!fs.existsSync(meocordConfigPath)) return {
31
33
  problem: 'Configuration file "meocord.config.ts" is missing!'
32
34
  };
33
35
  const loaded = readMeoCordSourceConfig();
36
+ // Named as itself; with no loadError, watch mode reports it as a problem, not as the config failing to compile
37
+ if ('error' in loaded && loaded.file === 'tsconfig.json') {
38
+ return {
39
+ problem: `tsconfig.json could not be read, so nothing was built or started:\n ${loaded.error}`
40
+ };
41
+ }
34
42
  if ('error' in loaded) {
35
43
  return {
36
44
  problem: `meocord.config.ts could not be loaded, so nothing was built or started:\n ${loaded.error}`,
37
45
  loadError: loaded.error
38
46
  };
39
47
  }
40
- const problem = shapeProblem(loaded.config);
48
+ const problem = shapeProblem(loaded.config, building);
41
49
  return problem ? {
42
50
  problem
43
51
  } : {
@@ -51,9 +59,12 @@ import chalk from 'chalk';
51
59
  * Loads `meocord.config.ts` and checks its shape, exiting when it is missing, fails to load, or has
52
60
  * options of the wrong type; options it does not know are reported and left alone.
53
61
  *
62
+ * @param options.building - Whether the command builds, as {@link checkSourceConfig} takes it.
54
63
  * @returns The config, which a build compiles and the bot then runs with.
55
- */ async function compileAndValidateConfig() {
56
- const checked = checkSourceConfig();
64
+ */ async function compileAndValidateConfig({ building = true } = {}) {
65
+ const checked = checkSourceConfig({
66
+ building
67
+ });
57
68
  if ('problem' in checked) {
58
69
  reportConfigProblem(checked.problem);
59
70
  await wait(100);
@@ -80,17 +91,27 @@ import chalk from 'chalk';
80
91
  process.exit(1);
81
92
  return;
82
93
  }
83
- return compileAndValidateConfig();
94
+ return compileAndValidateConfig({
95
+ building: false
96
+ });
84
97
  }
85
- /** Exits with every problem when a configuration has options of the wrong type, and warns about unknown ones. */ async function assertConfigShape(config) {
86
- const problem = shapeProblem(config);
98
+ /**
99
+ * Exits with every problem when a configuration a command runs with, without building, has options of the wrong type,
100
+ * and warns about unknown ones.
101
+ */ async function assertConfigShape(config) {
102
+ const problem = shapeProblem(config, false);
87
103
  if (!problem) return;
88
104
  console.error(chalk.red(problem));
89
105
  await wait(100);
90
106
  process.exit(1);
91
107
  }
92
- /** Every option of the wrong type in a configuration, as one report, or undefined; options it does not know are warned about. */ function shapeProblem(config) {
93
- const { errors, warnings } = configProblems(config);
108
+ /**
109
+ * Every option of the wrong type in a configuration, as one report, or undefined; options it does not know are warned
110
+ * about. A `webpack` hook is refused only for a command that builds, as the build refuses it.
111
+ */ function shapeProblem(config, building = true) {
112
+ const { errors, warnings } = configProblems(config, {
113
+ building
114
+ });
94
115
  for (const warning of warnings)console.warn(chalk.yellow(`meocord.config.ts: ${warning}`));
95
116
  if (errors.length === 0) return undefined;
96
117
  return `meocord.config.ts has ${errors.length} problem(s):\n${errors.map((error)=>` - ${error}`).join('\n')}`;
@@ -2,20 +2,22 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { execFile } from 'child_process';
4
4
  import { Logger } from '../common/logger.js';
5
- import '../core/theme-scope.js';
6
- import '../core/theme-validation.js';
5
+ import '../common/theme.js';
7
6
  import '../common/decorator.js';
8
7
  import 'reflect-metadata';
9
8
  import 'discord.js';
9
+ import '../common/deprecation.js';
10
10
  import '../common/response/response-state.js';
11
+ import '../core/theme-scope.js';
11
12
  import '../common/errors.js';
13
+ import '../common/cooldown-store.js';
12
14
  import 'node:crypto';
13
15
  import '../decorator/controller.decorator.js';
14
16
  import '../core/scalar-types.js';
15
- import 'node:fs';
16
- import 'node:url';
17
+ import './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 { kebabCase, startCase, camelCase } from 'lodash-es';
20
22
  import { fileURLToPath } from 'url';
21
23
 
@@ -34,16 +34,11 @@ import { listed } from './user-text.util.js';
34
34
  const productionFiles = envFiles('production');
35
35
  // In Bun's order, a later file winning, as the config's dotenv also settles them
36
36
  const production = Object.assign({}, ...productionFiles.map((file)=>read(root, file)));
37
- const development = new Map();
38
- for (const file of envFiles(env.NODE_ENV).filter((file)=>!productionFiles.includes(file))){
39
- for (const [key, value] of Object.entries(read(root, file)))development.set(key, {
40
- value,
41
- file
42
- });
43
- }
37
+ const files = envFiles(env.NODE_ENV);
38
+ // Listed file by file, in Bun's order
44
39
  const loaded = [
45
- ...development
46
- ].filter(([key, { value }])=>env[key] === value && production[key] !== value);
40
+ ...bunLoadedValues(env, root, files)
41
+ ].filter(([key, { value, file }])=>!productionFiles.includes(file) && env[key] === value && production[key] !== value).sort(([, a], [, b])=>files.indexOf(a.file) - files.indexOf(b.file));
47
42
  if (loaded.length === 0) return none;
48
43
  return {
49
44
  files: [
@@ -61,13 +56,77 @@ import { listed } from './user-text.util.js';
61
56
  * The environment a bot started in `root` inherits: `env` without the values its .env files gave it. Neither dotenv nor
62
57
  * Bun replaces a variable already set, so an inherited copy would keep a value a file no longer holds; the bot reads the
63
58
  * files itself as it starts. A variable the shell sets to another value is kept.
64
- */ function inheritedEnvironment(env, root) {
59
+ */ function inheritedEnvironment(env, root, bun = process.versions.bun !== undefined) {
60
+ const files = envFiles(env.NODE_ENV);
65
61
  const fromFiles = new Map();
66
- for (const file of envFiles(env.NODE_ENV)){
67
- for (const [key, value] of Object.entries(read(root, file)))fromFiles.set(key, (fromFiles.get(key) ?? new Set()).add(value));
68
- }
62
+ const add = (key, value)=>fromFiles.set(key, (fromFiles.get(key) ?? new Set()).add(value));
63
+ // As dotenv sets them, and, when the CLI runs on Bun, as Bun set them before any code ran
64
+ for (const file of files)for (const [key, value] of Object.entries(read(root, file)))add(key, value);
65
+ if (bun) for (const [key, { value }] of bunLoadedValues(env, root, files))add(key, value);
69
66
  return Object.fromEntries(Object.entries(env).filter(([key, value])=>value === undefined || !fromFiles.get(key)?.has(value)));
70
67
  }
68
+ /**
69
+ * The value Bun gives each variable the .env files set, and the file it comes from: Bun loads them before any code
70
+ * runs, the file that wins in `files` first, sets only a variable not already set, and expands `$NAME`, `${NAME}` and
71
+ * `${NAME:-default}` line by line from what is set so far, a later line of the same file giving its text as written.
72
+ * A variable the shell set keeps the shell's value, which `env` holds, so it differs from the one given here.
73
+ */ function bunLoadedValues(env, root, files) {
74
+ // Each variable's first line in Bun's order: the one Bun loads it from
75
+ const lines = [];
76
+ const first = new Map();
77
+ for (const file of [
78
+ ...files
79
+ ].reverse()){
80
+ const own = read(root, file);
81
+ for (const [key, text] of Object.entries(own)){
82
+ if (!first.has(key)) first.set(key, lines.length);
83
+ lines.push({
84
+ key,
85
+ text,
86
+ file,
87
+ own
88
+ });
89
+ }
90
+ }
91
+ const values = new Map();
92
+ const fromShell = new Map();
93
+ // A variable the shell set holds a value its line doesn't give; one its own line refers to back counts as from it
94
+ const shellSet = (key)=>{
95
+ if (!fromShell.has(key)) {
96
+ fromShell.set(key, false);
97
+ fromShell.set(key, env[key] !== undefined && env[key] !== valueAt(first.get(key)));
98
+ }
99
+ return fromShell.get(key);
100
+ };
101
+ const valueAt = (index)=>{
102
+ if (!values.has(index)) {
103
+ const { text, file, own } = lines[index];
104
+ const lookup = (name)=>{
105
+ const at = first.get(name);
106
+ if (at === undefined || shellSet(name) || at < index) return env[name] ?? '';
107
+ // Not loaded yet: its text as written in this file, or unset until a later file loads it
108
+ return lines[at].file === file ? own[name] : '';
109
+ };
110
+ values.set(index, expandAsBun(text, lookup));
111
+ }
112
+ return values.get(index);
113
+ };
114
+ return new Map([
115
+ ...first
116
+ ].map(([key, index])=>[
117
+ key,
118
+ {
119
+ value: valueAt(index),
120
+ file: lines[index].file
121
+ }
122
+ ]));
123
+ }
124
+ /** A `$NAME` or `${NAME}` reference, with `${NAME:-default}`'s default, or a `\$` Bun reads as a dollar sign. */ const BUN_REFERENCE = /\\\$|\$\{([A-Za-z0-9_]+)(?::-([^}]*))?\}|\$([A-Za-z0-9_]+)/g;
125
+ /** A .env value as Bun expands it, each reference read through `lookup`; any other `$` stays as it is. */ const expandAsBun = (text, lookup)=>text.replace(BUN_REFERENCE, (match, braced, fallback, bare)=>{
126
+ if (match === '\\$') return '$';
127
+ const value = lookup(braced ?? bare);
128
+ return fallback !== undefined && value === '' ? fallback : value;
129
+ });
71
130
  /** A .env file's values, or none when it is not there to read. */ function read(root, file) {
72
131
  let text;
73
132
  try {
@@ -1,3 +1,7 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ /** The byte order mark an editor may write at the start of a UTF-8 file, which `JSON.parse` refuses. */ const BYTE_ORDER_MARK = '\uFEFF';
4
+ /** The text without a leading byte order mark, as Node and tsc read a JSON file. */ const withoutByteOrderMark = (text)=>text.startsWith(BYTE_ORDER_MARK) ? text.slice(BYTE_ORDER_MARK.length) : text;
1
5
  /**
2
6
  * Removes the comments and trailing commas `tsconfig.json` allows, leaving strings untouched, so an
3
7
  * `https://` URL or a `/*` glob inside one survives.
@@ -37,8 +41,11 @@
37
41
  }
38
42
  return result;
39
43
  }
40
- /** Parses JSON with the comments and trailing commas `tsconfig.json` allows. */ function parseJsonc(text) {
41
- return JSON.parse(stripJsonc(text));
44
+ /** Parses JSON with the comments and trailing commas `tsconfig.json` allows, and a leading byte order mark. */ function parseJsonc(text) {
45
+ return JSON.parse(stripJsonc(withoutByteOrderMark(text)));
46
+ }
47
+ /** Reads a JSON file, such as a `package.json`, as Node reads one: UTF-8, with a leading byte order mark ignored. */ function readJsonFile(file) {
48
+ return JSON.parse(withoutByteOrderMark(readFileSync(file, 'utf8')));
42
49
  }
43
50
 
44
- export { parseJsonc, stripJsonc };
51
+ export { parseJsonc, readJsonFile, stripJsonc };
@@ -3,6 +3,7 @@ import { findModulePackageDir } from './common.util.js';
3
3
  import path from 'node:path';
4
4
  import fs__default from 'node:fs';
5
5
  import wait from './wait.util.js';
6
+ import { readJsonFile } from './json.util.js';
6
7
  import chalk from 'chalk';
7
8
 
8
9
  function configureCommandHelp(command) {
@@ -120,10 +121,10 @@ function configureCommandHelp(command) {
120
121
  throw new Error('Cannot locate the "MeoCord" package directory.');
121
122
  }
122
123
  // Read and parse the root package.json
123
- const { dependencies } = JSON.parse(fs__default.readFileSync(packageJsonPath, 'utf-8'));
124
+ const { dependencies } = readJsonFile(packageJsonPath);
124
125
  // Read and parse the MeoCord package.json
125
126
  const internalPackageJsonPath = path.join(meocordPath, 'package.json');
126
- const { name: internalPackageName } = JSON.parse(fs__default.readFileSync(internalPackageJsonPath, 'utf-8'));
127
+ const { name: internalPackageName } = readJsonFile(internalPackageJsonPath);
127
128
  // Validate that MeoCord is listed as a dependency in the root package.json
128
129
  if (!dependencies?.[internalPackageName]) {
129
130
  throw new Error('The package.json does not list "MeoCord" as a dependency. Ensure you are in the root directory.');
@@ -49,9 +49,22 @@ let problem;
49
49
  * module, or imports a file that isn't there. Otherwise its code threw as it ran.
50
50
  */ function fixedInConfig(error) {
51
51
  const code = error?.code;
52
- if (error instanceof SyntaxError || code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;
52
+ if (unparsable(error) || code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;
53
53
  return typeof code === 'string' && code.startsWith('ERR_REQUIRE_');
54
54
  }
55
+ /** The name of Bun's error for a module it could not parse; several of them come together in an AggregateError. */ const BUN_PARSE_ERROR = 'BuildMessage';
56
+ /**
57
+ * Whether a module could not be parsed, so none of its code ran: on Node, a SyntaxError thrown from compiling it, whose
58
+ * first frame is Node's own; on Bun, its parse error. A SyntaxError the config's code throws as it runs, from
59
+ * `JSON.parse` or `new RegExp` on an environment value, is not one.
60
+ */ function unparsable(error) {
61
+ const bunParseError = (each)=>each?.name === BUN_PARSE_ERROR;
62
+ if (bunParseError(error)) return true;
63
+ if (error instanceof AggregateError) return error.errors.length > 0 && error.errors.every(bunParseError);
64
+ if (!(error instanceof SyntaxError)) return false;
65
+ const firstFrame = error.stack?.split('\n').find((line)=>line.trimStart().startsWith('at '));
66
+ return firstFrame === undefined || /[( ]node:/.test(firstFrame);
67
+ }
55
68
  /**
56
69
  * The package a module-not-found error names, when it is a package rather than a file: `dotenv` for `dotenv/config`,
57
70
  * `@scope/name` for `@scope/name/sub`, with the installed package that imports it, if one does. Undefined for any other
@@ -1,6 +1,9 @@
1
1
  import { shutdownTimeoutProblem } from './shutdown-timeout.util.js';
2
+ import { quoteForLog } from './user-text.util.js';
2
3
 
3
- const describe = (value)=>typeof value === 'string' ? `'${value}'` : typeof value === 'number' || typeof value === 'boolean' ? String(value) : Array.isArray(value) ? 'an array' : value === null ? 'null' : typeof value;
4
+ /** Why a config that still declares a `webpack` hook, which MeoCord does not run, is refused. */ const WEBPACK_HOOK_REFUSAL = 'meocord.config.ts still declares a `webpack` hook, which MeoCord no longer runs. ' + 'Rename it to `rsbuild` and reshape its body for Rsbuild: images, fonts, svg and media ' + 'need no rules any more, `output.filename` accepts a function for custom asset paths, ' + 'and raw rules go through `tools.rspack`. See the 4.0.0 release notes.';
5
+ // A string as other messages quote user text, so a line break in it can't start a line of its own
6
+ const describe = (value)=>typeof value === 'string' ? quoteForLog(value) : typeof value === 'number' || typeof value === 'boolean' ? String(value) : Array.isArray(value) ? 'an array' : value === null ? 'null' : typeof value;
4
7
  const optional = (check)=>(value, key)=>value === undefined ? undefined : check(value, key);
5
8
  const string = (value, key)=>typeof value === 'string' ? undefined : `${key} must be a string (got ${describe(value)})`;
6
9
  const boolean = (value, key)=>typeof value === 'boolean' ? undefined : `${key} must be true or false (got ${describe(value)})`;
@@ -60,8 +63,10 @@ const oneOf = (...allowed)=>(value, key)=>allowed.includes(value) ? undefined :
60
63
  * Checks a loaded configuration's shape, so a wrong type fails at once with what to fix instead of
61
64
  * surfacing later as an odd build or runtime error.
62
65
  *
66
+ * @param options.building - Whether the command builds, which refuses a `webpack` hook; one that only runs a build
67
+ * made before is warned that the hook has no effect.
63
68
  * @returns Every error and warning found; the command should stop when there are errors.
64
- */ function configProblems(config) {
69
+ */ function configProblems(config, { building = true } = {}) {
65
70
  const problems = {
66
71
  errors: [],
67
72
  warnings: []
@@ -72,6 +77,11 @@ const oneOf = (...allowed)=>(value, key)=>allowed.includes(value) ? undefined :
72
77
  }
73
78
  const shape = configShape(problems);
74
79
  for (const [name, value] of Object.entries(config)){
80
+ // A webpack hook: a build refuses it, so there it's that error, not an option with no effect
81
+ if (name === 'webpack' && building) {
82
+ problems.errors.push(WEBPACK_HOOK_REFUSAL);
83
+ continue;
84
+ }
75
85
  const check = shape[name];
76
86
  if (!check) {
77
87
  problems.warnings.push(`${name} is not a MeoCord option, so it has no effect.`);
@@ -83,4 +93,4 @@ const oneOf = (...allowed)=>(value, key)=>allowed.includes(value) ? undefined :
83
93
  return problems;
84
94
  }
85
95
 
86
- export { configProblems };
96
+ export { WEBPACK_HOOK_REFUSAL, configProblems };
@@ -10,7 +10,7 @@ import { projectPaths } from './tsconfig-paths.util.js';
10
10
  */ function loadMeoCordSourceConfig() {
11
11
  const result = readMeoCordSourceConfig();
12
12
  if ('error' in result) {
13
- console.error(`[MeoCord] Failed to load config: ${result.error}`);
13
+ console.error(result.file === 'tsconfig.json' ? `[MeoCord] Failed to read tsconfig.json: ${result.error}` : `[MeoCord] Failed to load config: ${result.error}`);
14
14
  return undefined;
15
15
  }
16
16
  return result.config;
@@ -19,14 +19,24 @@ import { projectPaths } from './tsconfig-paths.util.js';
19
19
  * Loads `meocord.config.ts` from source, reporting a failure instead of logging it, for the CLI to stop
20
20
  * on. jiti's message names the file, line and column of a syntax error.
21
21
  *
22
- * @returns The config (undefined when the file does not exist), or the reason it could not be loaded.
22
+ * @returns The config (undefined when the file does not exist), or the reason it could not be loaded, naming
23
+ * `tsconfig.json` as the file at fault when it was that file, whose `paths` the config is loaded with.
23
24
  */ function readMeoCordSourceConfig() {
24
25
  const configPath = path.resolve(process.cwd(), 'meocord.config.ts');
25
26
  if (!existsSync(configPath)) return {
26
27
  config: undefined
27
28
  };
29
+ // Read first, so a tsconfig.json that can't be read is reported as its own, not as the config failing to load
30
+ let aliases;
31
+ try {
32
+ aliases = jitiAliases(projectPaths() ?? {});
33
+ } catch (error) {
34
+ return {
35
+ error: error instanceof Error ? error.message : String(error),
36
+ file: 'tsconfig.json'
37
+ };
38
+ }
28
39
  try {
29
- const aliases = jitiAliases(projectPaths() ?? {});
30
40
  // The module as written, so an ES module with no default export is told apart from a CommonJS one
31
41
  const jiti = createJiti(import.meta.url, {
32
42
  interopDefault: false,
@@ -5,8 +5,9 @@ import { isBuiltApplication, bundleEntry } from './bundle-entry.util.js';
5
5
  /**
6
6
  * File written beside a bundle that carries native addons, naming the platform it was built for.
7
7
  */ const PLATFORM_MANIFEST = 'meocord.platform.json';
8
+ /** The one operating system whose binaries are built against a C library of its choosing, glibc or musl. */ const LIBC_PLATFORM = 'linux';
8
9
  /** The C library this process runs against, when the runtime can say. */ function detectLibc() {
9
- if (process.platform !== 'linux') return undefined;
10
+ if (process.platform !== LIBC_PLATFORM) return undefined;
10
11
  try {
11
12
  const report = process.report?.getReport?.();
12
13
  if (!report?.header) return undefined;
@@ -66,4 +67,4 @@ import { isBuiltApplication, bundleEntry } from './bundle-entry.util.js';
66
67
  return process.argv[1] ? path.dirname(process.argv[1]) : undefined;
67
68
  }
68
69
 
69
- export { PLATFORM_MANIFEST, currentPlatform, describePlatform, isSamePlatform, platformMismatch, writePlatformManifest };
70
+ export { LIBC_PLATFORM, PLATFORM_MANIFEST, currentPlatform, describePlatform, isSamePlatform, platformMismatch, writePlatformManifest };
@@ -1,15 +1,22 @@
1
1
  import path from 'path';
2
- import { warnDeprecatedBehaviour } from '../common/deprecation.js';
2
+ import { warnOnce, warnDeprecatedBehaviour } from '../common/deprecation.js';
3
3
  import { isExplainedError } from '../common/explained-error.js';
4
4
  import { Logger } from '../common/logger.js';
5
5
  import { isBuiltApplication, STARTUP_ERRORS_KEY } from './bundle-entry.util.js';
6
6
  import { comparablePath, framePath } from './source-path.util.js';
7
+ import { shared } from './shared-state.util.js';
7
8
 
8
9
  const REFUSAL = Symbol.for('meocord.refusal');
9
- /** Whether this process reports refusals yet; see `reportRefusals`. */ let reporting = false;
10
- /** Refusals someone else reports, and ends the process for, which `reportRefusals` leaves to them. */ const handedOff = new WeakSet();
10
+ /**
11
+ * One per process for this meocord version, so the two builds report a refusal once: whether this process reports
12
+ * refusals yet (see `reportRefusals`), and the refusals someone else reports, and ends the process for, which it leaves
13
+ * to them.
14
+ */ const refusals = shared('refusalReporting', ()=>({
15
+ reporting: false,
16
+ handedOff: new WeakSet()
17
+ }));
11
18
  /** Leaves a refusal to whoever reports it instead, such as a shard's manager: going uncaught, it is neither logged nor exits. */ function handOffRefusal(error) {
12
- handedOff.add(error);
19
+ refusals.handedOff.add(error);
13
20
  }
14
21
  /**
15
22
  * Marks an error MeoCord raises for code it refuses as the application loads, such as a decorator given what it
@@ -20,18 +27,29 @@ const REFUSAL = Symbol.for('meocord.refusal');
20
27
  enumerable: false
21
28
  });
22
29
  // Before the error is thrown, so a built application reports it however the throw ends
23
- if (!reporting && isBuiltApplication()) reportRefusals();
30
+ if (!refusals.reporting && isBuiltApplication()) reportRefusals();
24
31
  return error;
25
32
  }
26
- /** What a decorator is applied to, as a refusal names it: `Class.method` on a method, `Class` on a class. */ function decoratedName(target, propertyKey) {
33
+ /** The name of the class a member decorator is applied in: a static member's target is the class itself. */ function ownerName(target) {
34
+ return (typeof target === 'function' ? target : target.constructor).name;
35
+ }
36
+ /**
37
+ * What a decorator is applied to, as a refusal names it: `Class.method` on a method, a static one included, `Class` on
38
+ * a class.
39
+ */ function decoratedName(target, propertyKey) {
27
40
  if (propertyKey === undefined) return target.name || 'a class';
28
- return `${target.constructor.name}.${String(propertyKey)}`;
41
+ return `${ownerName(target)}.${String(propertyKey)}`;
29
42
  }
30
43
  /**
31
44
  * Refuses a decorator that goes only on a method, or on a member, where it is applied to a class, directly or through
32
- * `applyDecorators`, rather than store what nothing reads or fail on the method it lacks.
45
+ * `applyDecorators`, rather than store what nothing reads or fail on the method it lacks. On a static member, where
46
+ * nothing reads it either, it warns; the next major version (5.0) refuses it there too.
33
47
  */ function refuseOnClass(decorator, target, propertyKey, place = 'a method') {
34
48
  if (propertyKey === undefined) throw refuse(new Error(`${decoratedName(target)}: ${decorator} goes on ${place}, not on a class.`));
49
+ if (typeof target === 'function') {
50
+ const where = place === 'a method' ? 'an instance method, not a static one; it never runs' : 'an instance member, not a static one, where nothing reads it';
51
+ warnOnce(new Logger(decorator.slice(1)), `${decoratedName(target, propertyKey)}: ${decorator} goes on ${where}. The next major version (5.0) refuses it.`);
52
+ }
35
53
  }
36
54
  /** Refuses a decorator that goes only on a class where it is applied to a method, directly or through `applyDecorators`. */ function refuseOnMethod(decorator, target, propertyKey) {
37
55
  if (propertyKey !== undefined) throw refuse(new Error(`${decoratedName(target, propertyKey)}: ${decorator} goes on a class, not on a method.`));
@@ -214,9 +232,9 @@ function keepOrThrow(error, target, propertyKey) {
214
232
  * report. Installed by the first refusal in a built application. Bun has no monitor event for a rejection, so there a
215
233
  * listener reports a refused rejection and rejects any other again without itself, unless the app listens itself.
216
234
  */ function reportRefusals(log = (text)=>new Logger('MeoCord').error(text), exit = (code)=>process.exit(code), { bun = process.versions.bun !== undefined, reject = (reason)=>void Promise.reject(reason) } = {}) {
217
- reporting = true;
235
+ refusals.reporting = true;
218
236
  const report = (error)=>{
219
- if (handedOff.has(error)) return;
237
+ if (refusals.handedOff.has(error)) return;
220
238
  if (!isExplainedError(error)) log(describeRefusal(error, process.cwd()));
221
239
  exit(1);
222
240
  };
@@ -235,4 +253,4 @@ function keepOrThrow(error, target, propertyKey) {
235
253
  process.on('unhandledRejection', onRejection);
236
254
  }
237
255
 
238
- export { allDeclaredErrors, collectStartupErrors, declarationOf, declaredErrorsOf, declaring, decoratedName, deprecatedOnMethod, describeRefusal, handOffRefusal, isRefusal, keepsDeclaredErrors, logStartupErrors, refuse, refuseOnClass, refuseOnMethod, reportRefusals, setStartupErrorsMode, sourceFileOf, startupError, startupErrorsOf, stopOnStartupErrors };
256
+ export { allDeclaredErrors, collectStartupErrors, declarationOf, declaredErrorsOf, declaring, decoratedName, deprecatedOnMethod, describeRefusal, handOffRefusal, isRefusal, keepsDeclaredErrors, logStartupErrors, ownerName, refuse, refuseOnClass, refuseOnMethod, reportRefusals, setStartupErrorsMode, sourceFileOf, startupError, startupErrorsOf, stopOnStartupErrors };