meocord 4.1.2 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +362 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
  4. package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
  5. package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
  7. package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
  8. package/dist/cjs/common/index.cjs +4 -4
  9. package/dist/cjs/core/index.cjs +365 -310
  10. package/dist/cjs/decorator/index.cjs +85 -61
  11. package/dist/cjs/testing/index.cjs +2236 -496
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/app-template/README.md.template +3 -1
  14. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
  15. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
  16. package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
  17. package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
  18. package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
  19. package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
  20. package/dist/esm/bin/generator.js +4 -5
  21. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  22. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  23. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  24. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  25. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  26. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  27. package/dist/esm/bin/meocord.js +84 -25
  28. package/dist/esm/build/load-config.pre-entry.js +17 -4
  29. package/dist/esm/build/rsbuild-config.js +16 -12
  30. package/dist/esm/build/typed-externals.js +36 -0
  31. package/dist/esm/common/cooldown-store.js +10 -2
  32. package/dist/esm/common/decorator.js +6 -6
  33. package/dist/esm/common/deprecation.js +2 -2
  34. package/dist/esm/common/errors.js +27 -2
  35. package/dist/esm/common/execution-context.js +1 -1
  36. package/dist/esm/common/meocord-text.js +11 -8
  37. package/dist/esm/common/metadata.js +1 -1
  38. package/dist/esm/common/response/call-order.js +9 -4
  39. package/dist/esm/common/response/presenter.js +33 -6
  40. package/dist/esm/common/response/response-state.js +84 -10
  41. package/dist/esm/common/route.js +21 -5
  42. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  43. package/dist/esm/common/translate-error.js +1 -1
  44. package/dist/esm/common/translator.js +37 -6
  45. package/dist/esm/core/command-conflicts.js +34 -34
  46. package/dist/esm/core/command-registration.js +93 -24
  47. package/dist/esm/core/command-routes.js +2 -2
  48. package/dist/esm/core/component-routes.js +74 -31
  49. package/dist/esm/core/cooldown-runner.js +29 -8
  50. package/dist/esm/core/dispatcher.js +51 -7
  51. package/dist/esm/core/fallback.js +20 -11
  52. package/dist/esm/core/filter-runner.js +2 -2
  53. package/dist/esm/core/guard-runner.js +16 -3
  54. package/dist/esm/core/handler-input.js +4 -3
  55. package/dist/esm/core/handler-pipeline.js +27 -14
  56. package/dist/esm/core/handler-registry.js +1 -1
  57. package/dist/esm/core/interceptor-runner.js +2 -2
  58. package/dist/esm/core/lifecycle-hooks.js +134 -47
  59. package/dist/esm/core/meocord-factory.js +241 -179
  60. package/dist/esm/core/meocord.app.js +116 -75
  61. package/dist/esm/core/message-routes.js +4 -3
  62. package/dist/esm/core/observer-runner.js +4 -3
  63. package/dist/esm/core/providers.js +45 -23
  64. package/dist/esm/core/route-specificity.js +4 -4
  65. package/dist/esm/core/running-calls.js +43 -0
  66. package/dist/esm/core/scalar-types.js +15 -3
  67. package/dist/esm/core/shard-context.js +6 -7
  68. package/dist/esm/core/shard-manager.js +16 -10
  69. package/dist/esm/core/startup-roots.js +23 -4
  70. package/dist/esm/core/theme-resolvers.js +4 -1
  71. package/dist/esm/core/theme-runtime.js +2 -2
  72. package/dist/esm/core/theme-scope.js +3 -1
  73. package/dist/esm/core/theme-validation.js +17 -10
  74. package/dist/esm/decorator/app.decorator.js +12 -7
  75. package/dist/esm/decorator/command-builder.decorator.js +4 -4
  76. package/dist/esm/decorator/controller-class.decorator.js +9 -4
  77. package/dist/esm/decorator/controller.decorator.js +162 -83
  78. package/dist/esm/decorator/cooldown.decorator.js +20 -3
  79. package/dist/esm/decorator/defer.decorator.js +4 -4
  80. package/dist/esm/decorator/event.decorator.js +5 -5
  81. package/dist/esm/decorator/filter.decorator.js +7 -7
  82. package/dist/esm/decorator/guard.decorator.js +11 -10
  83. package/dist/esm/decorator/inject.decorator.js +4 -4
  84. package/dist/esm/decorator/interceptor.decorator.js +7 -7
  85. package/dist/esm/decorator/observer.decorator.js +4 -4
  86. package/dist/esm/decorator/service.decorator.js +4 -4
  87. package/dist/esm/decorator/theme.decorator.js +3 -3
  88. package/dist/esm/decorator/validation.decorator.js +10 -10
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +27 -11
  91. package/dist/esm/testing/cooldown-store-suite.js +85 -15
  92. package/dist/esm/testing/discord-defaults.js +249 -21
  93. package/dist/esm/testing/index.js +4 -2
  94. package/dist/esm/testing/inspect-handler.js +20 -3
  95. package/dist/esm/testing/meocord-testing-module.js +352 -205
  96. package/dist/esm/testing/mock-fn.js +173 -22
  97. package/dist/esm/testing/mock-interaction.js +1214 -209
  98. package/dist/esm/testing/response.js +1 -1
  99. package/dist/esm/testing/routing.js +39 -14
  100. package/dist/esm/testing/snowflake.js +9 -2
  101. package/dist/esm/testing/startup-errors.js +26 -0
  102. package/dist/esm/testing/strict-mocks.js +65 -0
  103. package/dist/esm/testing/theme.js +1 -1
  104. package/dist/esm/util/bundle-entry.util.js +2 -1
  105. package/dist/esm/util/common.util.js +47 -19
  106. package/dist/esm/util/dev-runner.util.js +8 -8
  107. package/dist/esm/util/generator-cli.util.js +4 -5
  108. package/dist/esm/util/interaction.util.js +3 -3
  109. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  110. package/dist/esm/util/meocord-config-validation.util.js +4 -5
  111. package/dist/esm/util/meocord-source-config.util.js +65 -20
  112. package/dist/esm/util/metadata-keys.js +1 -0
  113. package/dist/esm/util/method-wrapper.util.js +16 -0
  114. package/dist/esm/util/platform.util.js +10 -10
  115. package/dist/esm/util/refusal.util.js +148 -8
  116. package/dist/esm/util/shard-process.util.js +4 -1
  117. package/dist/esm/util/sharding-mode.util.js +5 -7
  118. package/dist/esm/util/shared-state.util.js +21 -0
  119. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  120. package/dist/esm/util/tsconfig.util.js +10 -10
  121. package/dist/esm/util/value.util.js +12 -1
  122. package/dist/types/common/index.d.cts +8 -8
  123. package/dist/types/common/index.d.ts +8 -8
  124. package/dist/types/core/index.d.cts +10 -7
  125. package/dist/types/core/index.d.ts +10 -7
  126. package/dist/types/decorator/index.d.cts +31 -29
  127. package/dist/types/decorator/index.d.ts +31 -29
  128. package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
  129. package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
  130. package/dist/types/interface/index.d.cts +1 -1
  131. package/dist/types/interface/index.d.ts +1 -1
  132. package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
  133. package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
  134. package/dist/types/testing/index.d.cts +406 -70
  135. package/dist/types/testing/index.d.ts +406 -70
  136. package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  137. package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  138. package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
  139. package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
  140. package/package.json +4 -3
package/dist/cli.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "meocordVersion": "4.1.2",
3
+ "meocordVersion": "4.2.1",
4
4
  "name": "meocord",
5
5
  "description": "CLI for managing the MeoCord application",
6
6
  "usage": "[options] [command]",
@@ -94,7 +94,9 @@ interceptors, filters })`. An observer hears about every call once listed in
94
94
  `meocord/testing` provides `MeoCordTestingModule` for building a controller with its
95
95
  dependencies, `invoke` for running a handler through its guards, interceptors and
96
96
  filters as the bot does, and `createMockInteraction` and friends for driving it without
97
- a Discord connection. `vitest.setup.ts` resets those mocks after every test, so set
97
+ a Discord connection. `vitest.setup.ts` has those mocks made with `vi.fn`, so Vitest
98
+ treats them as its own, and has them compute what discord.js computes, such as a
99
+ message's `editable`, rather than placeholders. It resets them after every test, so set
98
100
  what a mock returns in the test, or in `beforeEach`, that relies on it.
99
101
 
100
102
  ## Linting
@@ -9,6 +9,10 @@ import {
9
9
  } from 'meocord/testing'
10
10
  import { SampleButtonController } from '@src/controllers/button/sample.button.controller.js'
11
11
 
12
+ // Discord user ids, as the customId and the guard compare them
13
+ const owner = '200000000000000001'
14
+ const stranger = '200000000000000002'
15
+
12
16
  const clickBy = (userId: string, customId = 'button-click') => {
13
17
  const user = createMockUser()
14
18
  user.id = userId
@@ -19,7 +23,7 @@ describe('SampleButtonController', () => {
19
23
  const module = MeoCordTestingModule.create({ controllers: [SampleButtonController] }).compile()
20
24
 
21
25
  it('locks the message while it works, then answers', async () => {
22
- const interaction = clickBy('someone')
26
+ const interaction = clickBy(stranger)
23
27
 
24
28
  await module.invoke(SampleButtonController, 'handleButton', interaction)
25
29
 
@@ -29,16 +33,16 @@ describe('SampleButtonController', () => {
29
33
  })
30
34
 
31
35
  it('answers the owner of the button', async () => {
32
- const interaction = clickBy('owner', 'button-with/owner')
36
+ const interaction = clickBy(owner, `button-with/${owner}`)
33
37
 
34
38
  // The customId's route gives the handler its ownerId
35
39
  await module.invoke(SampleButtonController, 'handleButtonWithId', interaction)
36
40
 
37
- expect(getResponse(interaction).calls.at(-1)?.payload).toMatchObject({ content: 'Button of <@owner> clicked!' })
41
+ expect(getResponse(interaction).calls.at(-1)?.payload).toMatchObject({ content: `Button of <@${owner}> clicked!` })
38
42
  })
39
43
 
40
44
  it('denies anyone else before touching the message', async () => {
41
- const interaction = clickBy('stranger', 'button-with/owner')
45
+ const interaction = clickBy(stranger, `button-with/${owner}`)
42
46
 
43
47
  await expect(module.invoke(SampleButtonController, 'handleButtonWithId', interaction)).rejects.toThrow(
44
48
  GuardDeniedError,
@@ -15,8 +15,8 @@ export class SampleButtonController {
15
15
  await respond(interaction).send({ content: 'Button clicked!' })
16
16
  }
17
17
 
18
- // Only the user whose id the customId carries gets past OwnerGuard
19
- @Command('button-with/{ownerId}', CommandType.BUTTON)
18
+ // Only the user whose id the customId carries gets past OwnerGuard; `:snowflake` takes only a Discord id
19
+ @Command('button-with/{ownerId:snowflake}', CommandType.BUTTON)
20
20
  @UseGuard(OwnerGuard)
21
21
  @Cooldown({ uses: 5, seconds: 60 })
22
22
  @Defer()
@@ -7,30 +7,34 @@ import { OwnerGuard } from '@src/guards/owner.guard.js'
7
7
 
8
8
  @Controller()
9
9
  class CardController {
10
- @Command('card/{ownerId}', CommandType.BUTTON)
10
+ @Command('card/{ownerId:snowflake}', CommandType.BUTTON)
11
11
  @UseGuard(OwnerGuard)
12
12
  async open(interaction: ButtonInteraction) {
13
13
  await interaction.deferUpdate()
14
14
  }
15
15
  }
16
16
 
17
+ // Discord user ids, as the customId and the guard compare them
18
+ const owner = '200000000000000001'
19
+ const stranger = '200000000000000002'
20
+
17
21
  const clickBy = (userId: string) => {
18
22
  const user = createMockUser()
19
23
  user.id = userId
20
24
  // invoke reads ownerId from the customId, as dispatch does
21
- return createMockInteraction(ButtonInteraction, { customId: 'card/owner', user })
25
+ return createMockInteraction(ButtonInteraction, { customId: `card/${owner}`, user })
22
26
  }
23
27
 
24
28
  describe('OwnerGuard', () => {
25
29
  const module = MeoCordTestingModule.create({ controllers: [CardController] }).compile()
26
30
 
27
31
  it("lets the button's owner through", async () => {
28
- const { ran } = await module.invoke(CardController, 'open', clickBy('owner'))
32
+ const { ran } = await module.invoke(CardController, 'open', clickBy(owner))
29
33
 
30
34
  expect(ran).toBe(true)
31
35
  })
32
36
 
33
37
  it('denies anyone else with a GuardDeniedError', async () => {
34
- await expect(module.invoke(CardController, 'open', clickBy('stranger'))).rejects.toThrow(GuardDeniedError)
38
+ await expect(module.invoke(CardController, 'open', clickBy(stranger))).rejects.toThrow(GuardDeniedError)
35
39
  })
36
40
  })
@@ -3,7 +3,7 @@ import { Guard } from 'meocord/decorator'
3
3
  import { GuardDeniedError } from 'meocord/common'
4
4
  import { type GuardInterface } from 'meocord/interface'
5
5
 
6
- /** Lets only the user whose id a button carries use it: `@Command('card/{ownerId}', ...)` */
6
+ /** Lets only the user whose id a button carries use it: `@Command('card/{ownerId:snowflake}', ...)` */
7
7
  @Guard()
8
8
  export class OwnerGuard implements GuardInterface {
9
9
  canActivate(interaction: ButtonInteraction, { ownerId }: { ownerId: string }): boolean {
@@ -24,7 +24,7 @@ export default defineConfig({
24
24
  test: {
25
25
  globals: true,
26
26
  setupFiles: ['./vitest.setup.ts'],
27
- // vi.fn() mocks only; vitest.setup.ts resets the mocks from meocord/testing
27
+ // Every mock's calls, meocord/testing's included; vitest.setup.ts also resets meocord's to how each was created
28
28
  clearMocks: true,
29
29
  include: ['src/**/*.spec.ts'],
30
30
  coverage: {
@@ -1,6 +1,10 @@
1
1
  import 'reflect-metadata'
2
- import { resetAllMocks } from 'meocord/testing'
3
- import { afterEach } from 'vitest'
2
+ import { resetAllMocks, useMockFn, useStrictMocks } from 'meocord/testing'
3
+ import { afterEach, vi } from 'vitest'
4
4
 
5
- // Every test starts from mocks as they were created: clearMocks in vitest.config.ts reaches only vi.fn()
5
+ // meocord/testing makes its mocks with vi.fn, so Vitest's matchers, config and vi.mocked() treat them as its own
6
+ useMockFn(vi.fn)
7
+ // Mocks compute what discord.js computes, such as a message's editable or a member's kickable, rather than placeholders
8
+ useStrictMocks()
9
+ // Every test starts from meocord's mocks as they were created
6
10
  afterEach(() => resetAllMocks())
@@ -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
- import { BUILD_MODE_KEY, BUNDLE_ENTRY_KEY } from '../util/bundle-entry.util.js'
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,8 +23,20 @@ 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()
28
38
 
39
+ // Before the entry's decorators run, so with 'all' they keep their startup errors for create() to report together
40
+ if (config?.startupErrors === 'all') globalThis[STARTUP_ERRORS_KEY] = 'all'
41
+
29
42
  if (config?.sourceMappedStacks !== false) installStackRemapper(bundle)
@@ -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
  },