meocord 3.0.0 → 3.2.0

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 (76) hide show
  1. package/AUTHOR.md +2 -3
  2. package/README.md +361 -55
  3. package/dist/cjs/_shared/controller.decorator-MUHA_A3z.cjs +529 -0
  4. package/dist/cjs/core/index.cjs +246 -86
  5. package/dist/cjs/decorator/index.cjs +4 -2
  6. package/dist/cjs/enum/index.cjs +17 -4
  7. package/dist/cjs/testing/index.cjs +88 -4
  8. package/dist/esm/bin/app-template/README.md.template +61 -0
  9. package/dist/esm/bin/app-template/_env.example.template +2 -0
  10. package/dist/esm/bin/app-template/_gitignore.template +22 -0
  11. package/dist/esm/bin/app-template/_prettierrc.mjs.template +10 -0
  12. package/dist/esm/bin/app-template/eslint.config.ts.template +25 -0
  13. package/dist/esm/bin/app-template/meocord.config.ts.template +25 -0
  14. package/dist/esm/bin/app-template/package.json.template +38 -0
  15. package/dist/esm/bin/app-template/src/app.ts.template +44 -0
  16. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +17 -0
  17. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +19 -0
  18. package/dist/esm/bin/app-template/src/controllers/context-menu/builders/sample.builder.ts.template +10 -0
  19. package/dist/esm/bin/app-template/src/controllers/context-menu/sample.context-menu.controller.spec.ts.template +17 -0
  20. package/dist/esm/bin/app-template/src/controllers/context-menu/sample.context-menu.controller.ts.template +13 -0
  21. package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.spec.ts.template +17 -0
  22. package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.ts.template +28 -0
  23. package/dist/esm/bin/app-template/src/controllers/modal-submit/sample.modal-submit.controller.spec.ts.template +17 -0
  24. package/dist/esm/bin/app-template/src/controllers/modal-submit/sample.modal-submit.controller.ts.template +13 -0
  25. package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.spec.ts.template +17 -0
  26. package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.ts.template +29 -0
  27. package/dist/esm/bin/app-template/src/controllers/select-menu/sample.select-menu.controller.spec.ts.template +17 -0
  28. package/dist/esm/bin/app-template/src/controllers/select-menu/sample.select-menu.controller.ts.template +11 -0
  29. package/dist/esm/bin/app-template/src/controllers/slash/builders/sample.builder.ts.template +10 -0
  30. package/dist/esm/bin/app-template/src/controllers/slash/sample.slash.controller.spec.ts.template +17 -0
  31. package/dist/esm/bin/app-template/src/controllers/slash/sample.slash.controller.ts.template +19 -0
  32. package/dist/esm/bin/app-template/src/guards/rate-limit.guard.spec.ts.template +13 -0
  33. package/dist/esm/bin/app-template/src/guards/rate-limit.guard.ts.template +52 -0
  34. package/dist/esm/bin/app-template/src/main.ts.template +14 -0
  35. package/dist/esm/bin/app-template/src/services/sample.service.spec.ts.template +17 -0
  36. package/dist/esm/bin/app-template/src/services/sample.service.ts.template +9 -0
  37. package/dist/esm/bin/app-template/tsconfig.eslint.json.template +5 -0
  38. package/dist/esm/bin/app-template/tsconfig.json.template +30 -0
  39. package/dist/esm/bin/app-template/tsconfig.test.json.template +8 -0
  40. package/dist/esm/bin/app-template/vitest.config.ts.template +35 -0
  41. package/dist/esm/bin/builder-template/builder/primary-entry-point.builder.template +26 -0
  42. package/dist/esm/bin/builder-template/controller/autocomplete.controller.template +16 -0
  43. package/dist/esm/bin/builder-template/controller/button.controller.template +1 -1
  44. package/dist/esm/bin/builder-template/controller/channel-select-menu.controller.template +12 -0
  45. package/dist/esm/bin/builder-template/controller/context-menu.controller.template +2 -2
  46. package/dist/esm/bin/builder-template/controller/mentionable-select-menu.controller.template +12 -0
  47. package/dist/esm/bin/builder-template/controller/modal-submit.controller.template +1 -1
  48. package/dist/esm/bin/builder-template/controller/primary-entry-point.controller.template +12 -0
  49. package/dist/esm/bin/builder-template/controller/role-select-menu.controller.template +12 -0
  50. package/dist/esm/bin/builder-template/controller/slash.controller.template +1 -1
  51. package/dist/esm/bin/builder-template/controller/user-select-menu.controller.template +12 -0
  52. package/dist/esm/bin/generator.js +4 -9
  53. package/dist/esm/bin/helper/app-generator.helper.js +78 -0
  54. package/dist/esm/bin/helper/controller-generator.helper.js +24 -7
  55. package/dist/esm/bin/meocord.js +156 -75
  56. package/dist/esm/core/meocord.app.js +247 -86
  57. package/dist/esm/decorator/controller.decorator.js +73 -10
  58. package/dist/esm/decorator/guard.decorator.js +1 -1
  59. package/dist/esm/decorator/index.js +1 -1
  60. package/dist/esm/enum/controller.enum.js +23 -4
  61. package/dist/esm/testing/mock-interaction.js +89 -5
  62. package/dist/esm/util/common.util.js +11 -3
  63. package/dist/esm/util/generator-cli.util.js +21 -4
  64. package/dist/esm/util/interaction.util.js +174 -0
  65. package/dist/esm/util/package-manager.util.js +9 -2
  66. package/dist/esm/util/package-version.util.js +32 -0
  67. package/dist/esm/util/runtime.util.js +72 -0
  68. package/dist/types/controller.enum-DYfhYaat.d.ts +36 -0
  69. package/dist/types/core/index.d.ts +75 -2
  70. package/dist/types/decorator/index.d.ts +61 -49
  71. package/dist/types/enum/index.d.ts +1 -1
  72. package/dist/types/interface/index.d.ts +89 -3
  73. package/dist/types/testing/index.d.ts +3 -21
  74. package/package.json +11 -12
  75. package/dist/cjs/_shared/controller.decorator-CC6BjHkS.cjs +0 -288
  76. package/dist/types/controller.enum-QA-IuReF.d.ts +0 -18
@@ -7,8 +7,9 @@ import { spawn } from 'node:child_process';
7
7
  import { capitalize } from 'lodash-es';
8
8
  import wait from '../util/wait.util.js';
9
9
  import { GeneratorCLI } from './generator.js';
10
+ import { runtimePrefixFor, AppGeneratorHelper } from './helper/app-generator.helper.js';
10
11
  import * as fs from 'node:fs';
11
- import { setEnvironment, compileAndValidateConfig } from '../util/common.util.js';
12
+ import { setEnvironment, compileAndValidateConfig, validateDiscordToken } from '../util/common.util.js';
12
13
  import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
13
14
  import { Command } from 'commander';
14
15
  import { simpleGit } from 'simple-git';
@@ -16,6 +17,8 @@ import { execSync } from 'child_process';
16
17
  import * as p from '@clack/prompts';
17
18
  import { detectInstalledPMs, getInstallCommand } from '../util/package-manager.util.js';
18
19
  import { ensureReady, configureCommandHelp } from '../util/meocord-cli.util.js';
20
+ import { resolveOwnVersion } from '../util/package-version.util.js';
21
+ import { buildAppCommand, resolveRuntime } from '../util/runtime.util.js';
19
22
  import packageJson from '../package.json.js';
20
23
  import { fileURLToPath } from 'url';
21
24
  import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
@@ -25,7 +28,35 @@ const __filename$1 = fileURLToPath(import.meta.url);
25
28
  const __dirname$1 = path.dirname(__filename$1);
26
29
  /**
27
30
  * A Command Line Interface (CLI) for managing the MeoCord application.
28
- */ class MeoCordCLI {
31
+ */ /**
32
+ * Oldest Node the framework supports, mirroring `engines.node`.
33
+ *
34
+ * A test holds the two together, since a floor that drifted from the manifest would warn
35
+ * about versions npm is happy to install on.
36
+ */ const MINIMUM_NODE_MAJOR = 22;
37
+ /**
38
+ * Warns when the running Node is older than the framework supports.
39
+ *
40
+ * Advisory only. Comparing against the newest LTS release meant warning about being a
41
+ * patch behind, and reaching the network to find out meant a failed request could stop
42
+ * the command outright — for a check whose only outcome is a warning.
43
+ */ function warnIfNodeIsBelowSupported() {
44
+ const major = Number.parseInt(process.versions.node.split('.')[0], 10);
45
+ if (Number.isFinite(major) && major < MINIMUM_NODE_MAJOR) {
46
+ p.log.warn(`Node ${process.versions.node} is older than the supported minimum (v${MINIMUM_NODE_MAJOR}). ` + `The app will be created, but may not run.`);
47
+ }
48
+ }
49
+ /**
50
+ * The package manager's own complaint, which `execSync` buries on the thrown object.
51
+ *
52
+ * Its `message` says only that a command exited non-zero, which is never the part worth
53
+ * reading.
54
+ */ function installFailure(error) {
55
+ const { stderr, message } = error ?? {};
56
+ const reported = stderr?.toString().trim();
57
+ return new Error(reported || message || String(error));
58
+ }
59
+ class MeoCordCLI {
29
60
  /**
30
61
  * Configures and runs the MeoCord CLI.
31
62
  */ async run() {
@@ -81,6 +112,9 @@ copies or substantial portions of the Software.
81
112
  if (options.build || options.dev) {
82
113
  await compileAndValidateConfig();
83
114
  }
115
+ // Checked for every start, including one that skips the build: this is the point
116
+ // where the application actually needs to log in.
117
+ await validateDiscordToken();
84
118
  if (options.build) {
85
119
  await this.build(mode);
86
120
  await this.compileConfig();
@@ -94,7 +128,6 @@ copies or substantial portions of the Software.
94
128
  async createApp(appName, options) {
95
129
  const kebabCaseAppName = appName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
96
130
  const appPath = path.resolve(process.cwd(), kebabCaseAppName);
97
- const gitRepo = 'https://github.com/l7aromeo/meocord-template.git';
98
131
  p.intro(`meocord v${this.version}`);
99
132
  // Validate directory
100
133
  if (fs.existsSync(appPath)) {
@@ -102,24 +135,7 @@ copies or substantial portions of the Software.
102
135
  await wait(100);
103
136
  process.exit(1);
104
137
  }
105
- let latestLTS = null;
106
- try {
107
- const res = await fetch('https://nodejs.org/dist/index.json');
108
- const releases = await res.json();
109
- const ltsRelease = releases.find((r)=>r.lts !== false);
110
- if (ltsRelease) latestLTS = ltsRelease.version.slice(1);
111
- } catch {
112
- p.cancel('No internet connection. Creating a MeoCord app requires network access.');
113
- await wait(100);
114
- process.exit(1);
115
- }
116
- if (latestLTS) {
117
- const [major, minor, patch] = process.version.slice(1).split('.').map(Number);
118
- const [minMajor, minMinor, minPatch] = latestLTS.split('.').map(Number);
119
- if (major < minMajor || major === minMajor && minor < minMinor || major === minMajor && minor === minMinor && patch < minPatch) {
120
- p.log.warn(`Your Node.js (${process.version}) is behind the latest LTS (v${latestLTS}). Consider upgrading for best compatibility.`);
121
- }
122
- }
138
+ warnIfNodeIsBelowSupported();
123
139
  // Determine package manager
124
140
  const installedPMs = detectInstalledPMs();
125
141
  let pm;
@@ -155,47 +171,42 @@ copies or substantial portions of the Software.
155
171
  pm = selected;
156
172
  }
157
173
  const s = p.spinner();
158
- // Clone template
159
174
  s.start(`Creating a new MeoCord app: ${kebabCaseAppName}`);
160
175
  try {
161
- await simpleGit().clone(gitRepo, appPath);
176
+ this.appGeneratorHelper.generateApp(appPath, {
177
+ appName: kebabCaseAppName,
178
+ displayName: appName,
179
+ version: this.version,
180
+ packageManager: pm,
181
+ runtimePrefix: runtimePrefixFor(pm)
182
+ });
162
183
  } catch (error) {
163
- s.stop('Failed to fetch template.');
164
- p.cancel(error instanceof Error ? error.message : String(error));
165
- await wait(100);
166
- process.exit(1);
184
+ s.stop('Failed to create the app.');
185
+ await this.abortCreate(appPath, error);
167
186
  }
168
187
  s.stop(`App created at: ${appPath}`);
169
- // Initialize git
170
188
  s.start('Initializing Git repository...');
171
189
  try {
172
- fs.rmSync(path.join(appPath, '.git'), {
173
- recursive: true,
174
- force: true
175
- });
176
190
  const git = simpleGit(appPath);
177
191
  await git.init();
178
192
  await git.add('./*');
179
193
  await git.commit('Initial commit');
180
194
  } catch (error) {
181
195
  s.stop('Failed to initialize Git.');
182
- p.cancel(error instanceof Error ? error.message : String(error));
183
- await wait(100);
184
- process.exit(1);
196
+ await this.abortCreate(appPath, error);
185
197
  }
186
198
  s.stop('Git repository initialized.');
187
- // Install dependencies
188
199
  s.start(`Installing dependencies with ${pm}...`);
189
200
  try {
201
+ // Output is captured rather than discarded: a failed install is only actionable
202
+ // if the resolver's complaint survives to the error message.
190
203
  execSync(getInstallCommand(pm), {
191
204
  cwd: appPath,
192
- stdio: 'ignore'
205
+ stdio: 'pipe'
193
206
  });
194
207
  } catch (error) {
195
208
  s.stop('Failed to install dependencies.');
196
- p.cancel(error instanceof Error ? error.message : String(error));
197
- await wait(100);
198
- process.exit(1);
209
+ await this.abortCreate(appPath, installFailure(error));
199
210
  }
200
211
  s.stop('Dependencies installed.');
201
212
  p.outro(`MeoCord app "${kebabCaseAppName}" is ready!`);
@@ -341,6 +352,40 @@ copies or substantial portions of the Software.
341
352
  }
342
353
  }
343
354
  /**
355
+ * Replaces the running application with one built from the current sources.
356
+ *
357
+ * The replacement is spawned only once the previous process has exited. Both would
358
+ * hold the same gateway session, and claiming it before the first lets go produces a
359
+ * login conflict rather than a reload.
360
+ */ restartApp() {
361
+ const previous = this.appProcess;
362
+ this.appProcess = null;
363
+ if (!previous || previous.killed) {
364
+ this.appProcess = this.spawnApp();
365
+ return;
366
+ }
367
+ previous.removeAllListeners('exit');
368
+ previous.once('exit', ()=>{
369
+ this.appProcess = this.spawnApp();
370
+ });
371
+ previous.kill();
372
+ }
373
+ /**
374
+ * Runs the built application.
375
+ *
376
+ * Shared by both start modes so watching and production launch the bundle the same
377
+ * way; a watcher that spawned it differently could pass in development and fail in
378
+ * production on the difference alone.
379
+ *
380
+ * @returns The application process.
381
+ */ spawnApp() {
382
+ const { command, args } = buildAppCommand(this.runtime, this.mainJSPath);
383
+ return spawn(command, args, {
384
+ cwd: this.projectRoot,
385
+ stdio: 'inherit'
386
+ });
387
+ }
388
+ /**
344
389
  * Starts the MeoCord application in development mode with live updates.
345
390
  */ async startDev() {
346
391
  try {
@@ -357,7 +402,6 @@ copies or substantial portions of the Software.
357
402
  await wait(100);
358
403
  process.exit(1);
359
404
  }
360
- let nodemonProcess = null;
361
405
  let isRunning = false;
362
406
  const watch = ()=>compiler.watch({}, (err, stats)=>{
363
407
  if (err) {
@@ -367,18 +411,7 @@ copies or substantial portions of the Software.
367
411
  if (stats?.hasErrors()) {
368
412
  this.logger.error('Build failed due to errors in the compilation process:', stats.compilation.errors);
369
413
  } else {
370
- if (nodemonProcess) {
371
- nodemonProcess.kill();
372
- nodemonProcess = null;
373
- }
374
- nodemonProcess = spawn('npx -y nodemon', [
375
- '-q',
376
- this.mainJSPath
377
- ], {
378
- shell: true,
379
- cwd: this.projectRoot,
380
- stdio: 'inherit'
381
- });
414
+ this.restartApp();
382
415
  isRunning = true;
383
416
  }
384
417
  });
@@ -387,12 +420,12 @@ copies or substantial portions of the Software.
387
420
  const fsWatcher = fs.watch(path.resolve(process.cwd(), 'meocord.config.ts'), ()=>{
388
421
  clearTimeout(debounceWatcher);
389
422
  debounceWatcher = setTimeout(async ()=>{
390
- if (isRunning && nodemonProcess) {
423
+ if (isRunning && this.appProcess) {
391
424
  isRunning = false;
392
425
  this.logger.log('MeoCord config change detected, reloading config...');
393
- if (nodemonProcess && !nodemonProcess.killed) {
394
- nodemonProcess.kill();
395
- nodemonProcess = null;
426
+ if (this.appProcess && !this.appProcess.killed) {
427
+ this.appProcess.kill();
428
+ this.appProcess = null;
396
429
  }
397
430
  await new Promise((resolve)=>compiler.close(resolve));
398
431
  watch();
@@ -403,15 +436,15 @@ copies or substantial portions of the Software.
403
436
  process.on('SIGINT', async ()=>{
404
437
  if (sigintReceived) {
405
438
  // Second Ctrl+C — force kill and exit immediately
406
- if (nodemonProcess && !nodemonProcess.killed) nodemonProcess.kill('SIGKILL');
439
+ if (this.appProcess && !this.appProcess.killed) this.appProcess.kill('SIGKILL');
407
440
  process.exit(1);
408
441
  }
409
442
  sigintReceived = true;
410
- // Nodemon and the bot already received SIGINT from the process group.
411
- // Clean up parent-owned resources and wait for nodemon to exit.
443
+ // The application already received SIGINT from the process group. Clean up
444
+ // parent-owned resources and wait for it to exit on its own terms.
412
445
  fsWatcher.close();
413
- if (nodemonProcess && !nodemonProcess.killed) {
414
- nodemonProcess.on('exit', async ()=>{
446
+ if (this.appProcess && !this.appProcess.killed) {
447
+ this.appProcess.on('exit', async ()=>{
415
448
  await new Promise((resolve)=>compiler.close(resolve));
416
449
  process.exit(0);
417
450
  });
@@ -436,11 +469,7 @@ copies or substantial portions of the Software.
436
469
  }
437
470
  this.clearConsole();
438
471
  this.logger.log('Starting...');
439
- const start = spawn(`node ${this.mainJSPath}`, {
440
- shell: true,
441
- cwd: this.projectRoot,
442
- stdio: 'inherit'
443
- }).on('spawn', this.clearConsole);
472
+ const start = this.spawnApp().on('spawn', this.clearConsole);
444
473
  start.on('exit', (code)=>{
445
474
  process.exit(code ?? 0);
446
475
  });
@@ -462,6 +491,20 @@ copies or substantial portions of the Software.
462
491
  }
463
492
  }
464
493
  /**
494
+ * Reports why creation failed and removes what was written.
495
+ *
496
+ * A directory left behind from a failed attempt is indistinguishable from one the user
497
+ * meant to keep, and the next attempt refuses to start because the name is taken.
498
+ */ async abortCreate(appPath, error) {
499
+ fs.rmSync(appPath, {
500
+ recursive: true,
501
+ force: true
502
+ });
503
+ p.cancel(error instanceof Error ? error.message : String(error));
504
+ await wait(100);
505
+ process.exit(1);
506
+ }
507
+ /**
465
508
  * Clears the console on all platforms.
466
509
  */ clearConsole() {
467
510
  process.stdout.write('\u001b[3J\u001b[2J\u001b[H');
@@ -473,13 +516,51 @@ copies or substantial portions of the Software.
473
516
  this.mainJSPath = path.join(this.projectRoot, 'dist', 'main.js');
474
517
  this.webpackConfigPath = path.resolve(__dirname$1, '..', '..', '..', 'webpack.config.js');
475
518
  this.generatorCLI = new GeneratorCLI(this.appName);
476
- this.version = packageJson.version;
519
+ this.appGeneratorHelper = new AppGeneratorHelper();
520
+ this.version = resolveOwnVersion(__dirname$1, packageJson.version);
521
+ /**
522
+ * Binary the application is spawned with, so it runs on the same runtime as the CLI
523
+ * rather than on whichever one happens to be named in the source.
524
+ */ this.runtime = resolveRuntime(process.env, process.execPath);
525
+ /** The running application, while a watch session owns one. */ this.appProcess = null;
526
+ }
527
+ }
528
+ /**
529
+ * Whether this file is what the process was started with.
530
+ *
531
+ * The bin ships behind a `node_modules/.bin` symlink, so `argv[1]` is the link while
532
+ * `import.meta.url` is its target; the two only agree once the link is resolved.
533
+ * Anything undecidable counts as a launch, so the published CLI still starts in cases
534
+ * this cannot classify.
535
+ */ function isProcessEntry() {
536
+ const invoked = process.argv[1];
537
+ if (invoked === undefined) return true;
538
+ try {
539
+ return samePath(fs.realpathSync(invoked), __filename$1);
540
+ } catch {
541
+ return true;
477
542
  }
478
543
  }
479
- // Create an instance of the CLI and run it
480
- const cli = new MeoCordCLI();
481
- cli.run().catch(async (error)=>{
482
- cli.logger.error('Failed to initialize CLI:', error?.message || error);
483
- await wait(100);
484
- process.exit(1);
485
- });
544
+ /**
545
+ * Whether two resolved paths name the same file.
546
+ *
547
+ * Windows paths differ in case and separator without naming different files — npm's
548
+ * shim passes the script path in whatever form it recorded — and a comparison that
549
+ * missed would leave the CLI exiting without a word, since it would decide it was
550
+ * merely being imported.
551
+ */ function samePath(left, right) {
552
+ const normalise = (value)=>process.platform === 'win32' ? path.resolve(value).toLowerCase() : value;
553
+ return normalise(left) === normalise(right);
554
+ }
555
+ // Importing this module must not launch the command parser: its own tests do exactly
556
+ // that to inspect how the application is spawned.
557
+ if (isProcessEntry()) {
558
+ const cli = new MeoCordCLI();
559
+ cli.run().catch(async (error)=>{
560
+ cli.logger.error('Failed to initialize CLI:', error?.message || error);
561
+ await wait(100);
562
+ process.exit(1);
563
+ });
564
+ }
565
+
566
+ export { MINIMUM_NODE_MAJOR, MeoCordCLI };