meocord 3.2.2 → 4.0.0-beta.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 (52) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +160 -28
  3. package/THIRD_PARTY_NOTICES.md +40 -0
  4. package/dist/cjs/_shared/{controller.decorator-eT0oHb8D.cjs → controller.decorator-9nIxOp9y.cjs} +1 -1
  5. package/dist/cjs/_shared/{metadata-key.enum-DITxfZlw.cjs → metadata-key.enum-aABGdodb.cjs} +0 -4
  6. package/dist/cjs/_shared/{theme-EmG1ZEwV.cjs → theme-BXdSoifk.cjs} +4 -15
  7. package/dist/cjs/common/index.cjs +1 -5
  8. package/dist/cjs/core/index.cjs +67 -5
  9. package/dist/cjs/decorator/index.cjs +2 -2
  10. package/dist/cjs/enum/index.cjs +1 -1
  11. package/dist/cjs/testing/index.cjs +2 -6
  12. package/dist/esm/bin/app-template/meocord.config.ts.template +8 -13
  13. package/dist/esm/bin/app-template/package.json.template +12 -12
  14. package/dist/esm/bin/app-template/vitest.config.ts.template +6 -2
  15. package/dist/esm/bin/builder-template/builder/context-menu.builder.template +2 -2
  16. package/dist/esm/bin/builder-template/builder/primary-entry-point.builder.template +2 -2
  17. package/dist/esm/bin/builder-template/builder/slash.builder.template +2 -2
  18. package/dist/esm/bin/builder-template/controller/autocomplete.controller.template +4 -3
  19. package/dist/esm/bin/builder-template/controller/context-menu.controller.template +2 -2
  20. package/dist/esm/bin/builder-template/controller/primary-entry-point.controller.template +2 -2
  21. package/dist/esm/bin/builder-template/controller/slash.controller.template +2 -2
  22. package/dist/esm/bin/helper/controller-generator.helper.js +43 -26
  23. package/dist/esm/bin/helper/guard-generator.helper.js +8 -2
  24. package/dist/esm/bin/helper/service-generator.helper.js +8 -2
  25. package/dist/esm/bin/meocord.js +160 -140
  26. package/dist/esm/build/native-addons.js +235 -0
  27. package/dist/esm/build/rsbuild-config.js +169 -0
  28. package/dist/esm/common/decorator.js +0 -4
  29. package/dist/esm/common/theme.js +1 -5
  30. package/dist/esm/core/meocord-factory.js +4 -0
  31. package/dist/esm/enum/controller.enum.js +1 -5
  32. package/dist/esm/enum/metadata-key.enum.js +0 -4
  33. package/dist/esm/package.json.js +1 -1
  34. package/dist/esm/testing/mock-fn.js +1 -5
  35. package/dist/esm/util/generator-cli.util.js +54 -10
  36. package/dist/esm/util/json.util.js +0 -4
  37. package/dist/esm/util/meocord-config-loader.util.js +14 -2
  38. package/dist/esm/util/platform.util.js +66 -0
  39. package/dist/esm/util/runtime.util.js +20 -7
  40. package/dist/esm/util/tsconfig.util.js +1 -1
  41. package/dist/esm/util/wait.util.js +1 -5
  42. package/dist/types/common/index.d.ts +0 -16
  43. package/dist/types/core/index.d.ts +0 -12
  44. package/dist/types/decorator/index.d.ts +1 -31
  45. package/dist/types/enum/index.d.ts +0 -5
  46. package/dist/types/interface/index.d.ts +46 -57
  47. package/dist/types/testing/index.d.ts +4 -19
  48. package/meocord.eslint.cjs +0 -6
  49. package/meocord.eslint.d.ts +0 -6
  50. package/meocord.eslint.mjs +0 -6
  51. package/package.json +19 -23
  52. package/webpack.config.js +0 -147
@@ -3,18 +3,18 @@
3
3
  require('reflect-metadata');
4
4
  var inversify = require('inversify');
5
5
  var discord_js = require('discord.js');
6
- var theme = require('../_shared/theme-EmG1ZEwV.cjs');
7
- var controller_decorator = require('../_shared/controller.decorator-eT0oHb8D.cjs');
6
+ var theme = require('../_shared/theme-BXdSoifk.cjs');
7
+ var controller_decorator = require('../_shared/controller.decorator-9nIxOp9y.cjs');
8
8
  var lodashEs = require('lodash-es');
9
9
  var enum_index = require('../enum/index.cjs');
10
10
  var Table = require('cli-table3');
11
- var metadataKey_enum = require('../_shared/metadata-key.enum-DITxfZlw.cjs');
11
+ var fs = require('fs');
12
+ var path = require('path');
13
+ var metadataKey_enum = require('../_shared/metadata-key.enum-aABGdodb.cjs');
12
14
  require('node:util');
13
15
  require('dayjs');
14
16
  require('dayjs/plugin/utc.js');
15
17
  require('dayjs/plugin/timezone.js');
16
- require('path');
17
- require('fs');
18
18
  require('jiti');
19
19
  require('chalk');
20
20
 
@@ -507,6 +507,65 @@ class MeoCordApp {
507
507
  }
508
508
  }
509
509
 
510
+ /**
511
+ * File written beside a bundle that carries native addons, naming the platform it was built for.
512
+ */ const PLATFORM_MANIFEST = 'meocord.platform.json';
513
+ /** The C library this process runs against, when the runtime can say. */ function detectLibc() {
514
+ if (process.platform !== 'linux') return undefined;
515
+ try {
516
+ const report = process.report?.getReport?.();
517
+ if (!report?.header) return undefined;
518
+ return report.header.glibcVersionRuntime ? 'glibc' : 'musl';
519
+ } catch {
520
+ return undefined;
521
+ }
522
+ }
523
+ /** The platform this process is running on. */ function currentPlatform() {
524
+ const libc = detectLibc();
525
+ return {
526
+ platform: process.platform,
527
+ arch: process.arch,
528
+ ...libc && {
529
+ libc
530
+ }
531
+ };
532
+ }
533
+ /** `linux-x64 (glibc)`, `darwin-arm64`. */ function describePlatform({ platform, arch, libc }) {
534
+ return `${platform}-${arch}${libc ? ` (${libc})` : ''}`;
535
+ }
536
+ /**
537
+ * Whether native binaries built for one platform can load on another.
538
+ *
539
+ * The C library is compared only when both sides are known, so a runtime that cannot report it
540
+ * is not refused on a guess.
541
+ */ function isSamePlatform(built, running) {
542
+ if (built.platform !== running.platform || built.arch !== running.arch) return false;
543
+ return !built.libc || !running.libc || built.libc === running.libc;
544
+ }
545
+ /**
546
+ * Stops a bundle from starting on a platform its native addons were not built for.
547
+ *
548
+ * A binary for the wrong platform does not fail at startup. It fails when something first loads
549
+ * it -- for an image library, the first command that renders one -- with a dynamic linker error
550
+ * that names a file rather than the cause. Checking the manifest up front turns that into one
551
+ * clear message before the bot goes online.
552
+ *
553
+ * @param distDir - Directory holding the manifest. Defaults to the directory of the entry script.
554
+ */ function assertBuiltForThisPlatform(distDir = process.argv[1] ? path.dirname(process.argv[1]) : undefined) {
555
+ if (!distDir) return;
556
+ const manifest = path.join(distDir, PLATFORM_MANIFEST);
557
+ if (!fs.existsSync(manifest)) return;
558
+ let built;
559
+ try {
560
+ built = JSON.parse(fs.readFileSync(manifest, 'utf8'));
561
+ } catch {
562
+ return;
563
+ }
564
+ const running = currentPlatform();
565
+ if (isSamePlatform(built, running)) return;
566
+ throw new Error(`This build carries native addons compiled for ${describePlatform(built)}, but is running on ` + `${describePlatform(running)}. Compiled binaries only load on the platform they were built for. ` + 'Build on the same platform you deploy to -- for a container, run `meocord build` inside the image.');
567
+ }
568
+
510
569
  /**
511
570
  * Recursively binds a class and all its constructor dependencies to the container in singleton scope.
512
571
  */ function bindDependencies(container, cls) {
@@ -536,6 +595,9 @@ class MeoCordFactory {
536
595
  if (!meocordConfig) {
537
596
  throw new Error('MeoCord config not found. Ensure meocord.config.ts exists.');
538
597
  }
598
+ // Before anything is resolved: a controller or service is what first loads a native addon, and
599
+ // one built for another platform would otherwise fail there with a linker error.
600
+ assertBuiltForThisPlatform();
539
601
  const container = new inversify.Container();
540
602
  // Bind the Discord client as a constant value
541
603
  const discordClient = new discord_js.Client(options.clientOptions);
@@ -2,8 +2,8 @@
2
2
 
3
3
  require('reflect-metadata');
4
4
  var inversify = require('inversify');
5
- var metadataKey_enum = require('../_shared/metadata-key.enum-DITxfZlw.cjs');
6
- var controller_decorator = require('../_shared/controller.decorator-eT0oHb8D.cjs');
5
+ var metadataKey_enum = require('../_shared/metadata-key.enum-aABGdodb.cjs');
6
+ var controller_decorator = require('../_shared/controller.decorator-9nIxOp9y.cjs');
7
7
  var discord_js = require('discord.js');
8
8
  require('../enum/index.cjs');
9
9
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var metadataKey_enum = require('../_shared/metadata-key.enum-DITxfZlw.cjs');
3
+ var metadataKey_enum = require('../_shared/metadata-key.enum-aABGdodb.cjs');
4
4
 
5
5
  /**
6
6
  * The kinds of interaction a `@Command` method can be bound to.
@@ -2,7 +2,7 @@
2
2
 
3
3
  require('reflect-metadata');
4
4
  var inversify = require('inversify');
5
- var metadataKey_enum = require('../_shared/metadata-key.enum-DITxfZlw.cjs');
5
+ var metadataKey_enum = require('../_shared/metadata-key.enum-aABGdodb.cjs');
6
6
  var discord_js = require('discord.js');
7
7
 
8
8
  function isValueProvider(p) {
@@ -120,11 +120,7 @@ function isValueProvider(p) {
120
120
  }
121
121
  }
122
122
 
123
- /**
124
- * MeoCord Framework
125
- * Copyright (c) 2025-present Ukasyah Rahmatullah Zada
126
- * SPDX-License-Identifier: MIT
127
- */ // ---------------------------------------------------------------------------
123
+ // ---------------------------------------------------------------------------
128
124
  // Framework-agnostic mock function
129
125
  //
130
126
  // A minimal mock-fn implementation used internally by the testing utilities so
@@ -6,19 +6,14 @@ export default {
6
6
  // Read from the environment rather than written here: this file is committed, and a
7
7
  // token pasted into it is a token pushed to the remote. Copy .env.example to .env.
8
8
  discordToken: process.env.DISCORD_TOKEN!,
9
- webpack: config => {
10
- config.module.rules?.push({
11
- test: /\.(md|html)$/i,
12
- type: 'asset/source',
13
- })
14
- config.module.rules?.push({
15
- test: /\.(gif|jpg|jpeg|png|svg|woff|woff2|eot|ttf|otf)$/i,
16
- type: 'asset/resource',
17
- exclude: /node_modules/,
18
- generator: {
19
- filename: 'assets/[name][ext]',
20
- },
21
- })
9
+ rsbuild: config => {
10
+ // Images, fonts, svg and media are handled by the bundler already. Markdown and HTML are
11
+ // not, and `asset/source` is what makes `import readme from './readme.md'` give you the
12
+ // file's text rather than a path to it.
13
+ config.tools ??= {}
14
+ config.tools.rspack = (_rspackConfig, { addRules }) => {
15
+ addRules([{ test: /\.(md|html)$/i, type: 'asset/source' }])
16
+ }
22
17
 
23
18
  return config
24
19
  },
@@ -16,23 +16,23 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "discord.js": "^14.27.0",
19
- "dotenv": "^17.2.4",
19
+ "dotenv": "^18.0.3",
20
20
  "meocord": "^{{version}}"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@eslint/js": "^10.0.1",
24
- "@typescript-eslint/parser": "^8.58.1",
25
- "@vitest/coverage-istanbul": "^4.1.10",
26
- "eslint": "^10.2.0",
24
+ "@typescript-eslint/parser": "^8.70.1",
25
+ "@vitest/coverage-istanbul": "^5.0.1",
26
+ "eslint": "^10.11.0",
27
27
  "eslint-config-prettier": "^10.1.8",
28
- "eslint-plugin-import-x": "^4.16.2",
29
- "eslint-plugin-prettier": "^5.5.5",
28
+ "eslint-plugin-import-x": "^4.17.1",
29
+ "eslint-plugin-prettier": "^5.5.6",
30
30
  "eslint-plugin-unused-imports": "^4.4.1",
31
- "globals": "^17.5.0",
32
- "prettier": "^3.8.2",
33
- "typescript": "^6.0.2",
34
- "typescript-eslint": "^8.58.1",
35
- "unplugin-swc": "^1.5.7",
36
- "vitest": "^4.1.10"
31
+ "globals": "^17.12.0",
32
+ "prettier": "^3.9.8",
33
+ "typescript": "^6.0.3",
34
+ "typescript-eslint": "^8.70.1",
35
+ "unplugin-swc": "^2.0.0",
36
+ "vitest": "^5.0.1"
37
37
  }
38
38
  }
@@ -27,9 +27,13 @@ export default defineConfig({
27
27
  // istanbul rather than v8: v8 coverage needs Node's inspector API, which bun does
28
28
  // not implement, and this template is expected to run under either runtime.
29
29
  provider: 'istanbul',
30
- all: false,
31
30
  include: ['src/**/*.ts'],
32
- exclude: ['src/**/*.spec.ts'],
31
+ // Naming `include` reports files no test imported, which is the point -- a controller
32
+ // with no spec should show as zero rather than vanish from the percentage. Those files
33
+ // are parsed by istanbul rather than transformed by swc, and that parser does not know
34
+ // about decorators, so anything decorated but untested has to be excluded by hand.
35
+ // app.ts is wiring rather than logic, and main.ts only boots the app.
36
+ exclude: ['src/**/*.spec.ts', 'src/app.ts', 'src/main.ts'],
33
37
  },
34
38
  },
35
39
  })
@@ -3,10 +3,10 @@ import { CommandBuilder } from 'meocord/decorator'
3
3
  import { CommandType } from 'meocord/enum'
4
4
 
5
5
  @CommandBuilder(CommandType.CONTEXT_MENU)
6
- export class SampleCommandBuilder {
6
+ export class {{builderClassName}} {
7
7
  build() {
8
8
  return new ContextMenuCommandBuilder()
9
- .setName('sample-context-menu')
9
+ .setName('{{commandName}}')
10
10
  .setType(ApplicationCommandType.User)
11
11
  }
12
12
  }
@@ -12,11 +12,11 @@ import { CommandType } from 'meocord/enum'
12
12
  // Discord send the interaction here; `DiscordLaunchActivity` opens the activity without
13
13
  // ever reaching the bot.
14
14
  @CommandBuilder(CommandType.PRIMARY_ENTRY_POINT)
15
- export class SampleCommandBuilder {
15
+ export class {{builderClassName}} {
16
16
  build() {
17
17
  return {
18
18
  type: ApplicationCommandType.PrimaryEntryPoint as const,
19
- name: 'sample-entry-point',
19
+ name: '{{commandName}}',
20
20
  description: 'Launch the activity',
21
21
  handler: EntryPointCommandHandlerType.AppHandler,
22
22
  integration_types: [ApplicationIntegrationType.GuildInstall],
@@ -3,8 +3,8 @@ import { CommandBuilder } from 'meocord/decorator'
3
3
  import { CommandType } from 'meocord/enum'
4
4
 
5
5
  @CommandBuilder(CommandType.SLASH)
6
- export class SampleCommandBuilder {
6
+ export class {{builderClassName}} {
7
7
  build() {
8
- return new SlashCommandBuilder().setName('sample-slash').setDescription('This is a sample slash command')
8
+ return new SlashCommandBuilder().setName('{{commandName}}').setDescription('Describe what /{{commandName}} does')
9
9
  }
10
10
  }
@@ -4,9 +4,10 @@ import { Controller, Autocomplete } from 'meocord/decorator'
4
4
  @Controller()
5
5
  export class {{className}}AutocompleteController {
6
6
 
7
- // The option this completes must be declared with `.setAutocomplete(true)` on the
8
- // slash command builder, otherwise Discord never sends the interaction.
9
- @Autocomplete('sample-slash', 'query')
7
+ // Completes the `query` option of /{{commandName}} -- the command `meocord g co slash` generates
8
+ // for the same name. That option must be declared with `.setAutocomplete(true)` on the slash
9
+ // command's builder, otherwise Discord never sends the interaction.
10
+ @Autocomplete('{{commandName}}', 'query')
10
11
  async completeQuery(interaction: AutocompleteInteraction) {
11
12
  const { value } = interaction.options.getFocused(true)
12
13
  const choices = ['alpha', 'beta', 'gamma'].filter(choice => choice.startsWith(value))
@@ -1,11 +1,11 @@
1
1
  import { UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction } from 'discord.js'
2
2
  import { Controller, Command } from 'meocord/decorator'
3
- import { SampleCommandBuilder } from '{{builderImportPath}}'
3
+ import { {{builderClassName}} } from '{{builderImportPath}}'
4
4
 
5
5
  @Controller()
6
6
  export class {{className}}ContextMenuController {
7
7
 
8
- @Command('sample-context-menu', SampleCommandBuilder)
8
+ @Command('{{commandName}}', {{builderClassName}})
9
9
  async handleContextMenu(interaction: UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction) {
10
10
  await interaction.reply('This is sample reply of context menu command.')
11
11
  }
@@ -1,11 +1,11 @@
1
1
  import { PrimaryEntryPointCommandInteraction } from 'discord.js'
2
2
  import { Controller, Command } from 'meocord/decorator'
3
- import { SampleCommandBuilder } from '{{builderImportPath}}'
3
+ import { {{builderClassName}} } from '{{builderImportPath}}'
4
4
 
5
5
  @Controller()
6
6
  export class {{className}}PrimaryEntryPointController {
7
7
 
8
- @Command('sample-entry-point', SampleCommandBuilder)
8
+ @Command('{{commandName}}', {{builderClassName}})
9
9
  async handleEntryPoint(interaction: PrimaryEntryPointCommandInteraction) {
10
10
  await interaction.reply('Launching the activity.')
11
11
  }
@@ -1,11 +1,11 @@
1
1
  import { ChatInputCommandInteraction } from 'discord.js'
2
2
  import { Controller, Command } from 'meocord/decorator'
3
- import { SampleCommandBuilder } from '{{builderImportPath}}'
3
+ import { {{builderClassName}} } from '{{builderImportPath}}'
4
4
 
5
5
  @Controller()
6
6
  export class {{className}}SlashController {
7
7
 
8
- @Command('sample-slash', SampleCommandBuilder)
8
+ @Command('{{commandName}}', {{builderClassName}})
9
9
  async handleSampleSlash(interaction: ChatInputCommandInteraction) {
10
10
  await interaction.reply('This is sample reply of slash command.')
11
11
  }
@@ -1,6 +1,7 @@
1
1
  import path from 'path';
2
2
  import { ControllerType } from '../../enum/controller.enum.js';
3
- import { validateAndFormatName, populateTemplate, createDirectoryIfNotExists, generateFile, toClassName } from '../../util/generator-cli.util.js';
3
+ import { kebabCase } from 'lodash-es';
4
+ import { validateAndFormatName, assertFilesAbsent, populateTemplate, commandNameFor, createDirectoryIfNotExists, generateFile, toClassName } from '../../util/generator-cli.util.js';
4
5
  import { fileURLToPath } from 'url';
5
6
 
6
7
  const __filename$1 = fileURLToPath(import.meta.url);
@@ -14,8 +15,19 @@ class ControllerGeneratorHelper {
14
15
  */ generateController(args, type) {
15
16
  const { parts, kebabCaseName, className } = validateAndFormatName(args.controllerName);
16
17
  const controllerDir = path.join(process.cwd(), 'src', 'controllers', type, ...parts);
17
- const template = this.buildControllerTemplate(className, type, parts);
18
- this.generateControllerStructure(controllerDir, kebabCaseName, className, type, template);
18
+ // Every file this would write, checked before any is: a refusal leaves nothing half-made.
19
+ assertFilesAbsent([
20
+ ...this.getBuilderConfig(type, className, kebabCaseName, parts) ? [
21
+ this.builderFilePath(controllerDir, kebabCaseName)
22
+ ] : [],
23
+ path.join(controllerDir, `${kebabCaseName}.${type}.controller.ts`),
24
+ path.join(controllerDir, `${kebabCaseName}.${type}.controller.spec.ts`)
25
+ ]);
26
+ const template = this.buildControllerTemplate(className, type, parts, kebabCaseName);
27
+ this.generateControllerStructure(controllerDir, kebabCaseName, className, type, template, parts);
28
+ }
29
+ /** Where a controller's own builder is written: beside it, named after it. */ builderFilePath(controllerDir, kebabCaseName) {
30
+ return path.join(controllerDir, 'builders', `${kebabCaseName}.builder.ts`);
19
31
  }
20
32
  /**
21
33
  * Builds the controller template content by populating a template with variables.
@@ -23,8 +35,8 @@ class ControllerGeneratorHelper {
23
35
  * @param type - The type of the controller, defined in the `ControllerType` enum.
24
36
  * @returns The populated template string for the controller.
25
37
  * @throws Will throw an error if the controller type is unsupported.
26
- */ buildControllerTemplate(className, type, parts = []) {
27
- const templateConfig = this.getTemplateConfig(type, className, parts);
38
+ */ buildControllerTemplate(className, type, parts = [], kebabCaseName = kebabCase(className)) {
39
+ const templateConfig = this.getTemplateConfig(type, className, parts, kebabCaseName);
28
40
  if (!templateConfig) {
29
41
  throw new Error(`Unsupported controller type: ${type}`);
30
42
  }
@@ -35,7 +47,7 @@ class ControllerGeneratorHelper {
35
47
  * @param type - The type of the controller, defined in the `ControllerType` enum.
36
48
  * @param className - The name of the controller class.
37
49
  * @returns An object containing the template path and variables, or `undefined` if not found.
38
- */ getTemplateConfig(type, className, parts = []) {
50
+ */ getTemplateConfig(type, className, parts, kebabCaseName) {
39
51
  const baseDir = path.resolve(__dirname$1, '..', 'builder-template', 'controller');
40
52
  const templates = {
41
53
  [ControllerType.BUTTON]: 'button.controller.template',
@@ -53,21 +65,25 @@ class ControllerGeneratorHelper {
53
65
  [ControllerType.PRIMARY_ENTRY_POINT]: 'primary-entry-point.controller.template'
54
66
  };
55
67
  const template = templates[type] ? path.resolve(baseDir, templates[type]) : undefined;
56
- // The builder is written beside the controller, so a nested controller name moves
57
- // it too. Hard-coding `@src/controllers/<type>/builders/...` only ever pointed at
58
- // the top-level one, which for a nested name does not exist.
68
+ // The builder is written beside the controller and named after it, so each controller
69
+ // imports its own and generating one never touches another's. The path follows any
70
+ // nesting, since the builder moves with the controller.
59
71
  const builderImportPath = [
60
72
  '@src/controllers',
61
73
  type,
62
74
  ...parts,
63
- 'builders/sample.builder'
75
+ 'builders',
76
+ `${kebabCaseName}.builder`
64
77
  ].join('/');
78
+ const variables = {
79
+ className,
80
+ builderImportPath,
81
+ builderClassName: `${className}CommandBuilder`,
82
+ commandName: commandNameFor(parts, kebabCaseName)
83
+ };
65
84
  return template ? {
66
85
  template,
67
- variables: {
68
- className,
69
- builderImportPath
70
- }
86
+ variables
71
87
  } : undefined;
72
88
  }
73
89
  /**
@@ -77,8 +93,8 @@ class ControllerGeneratorHelper {
77
93
  * @param className - The name of the controller class.
78
94
  * @param type - The type of the controller, defined in the `ControllerType` enum.
79
95
  * @param controllerTemplate - The populated template string for the controller file.
80
- */ generateControllerStructure(controllerDir, kebabCaseName, className, type, controllerTemplate) {
81
- this.generateBuilderFile(className, type, controllerDir);
96
+ */ generateControllerStructure(controllerDir, kebabCaseName, className, type, controllerTemplate, parts) {
97
+ this.generateBuilderFile(className, kebabCaseName, type, controllerDir, parts);
82
98
  createDirectoryIfNotExists(controllerDir);
83
99
  const controllerFilePath = path.join(controllerDir, `${kebabCaseName}.${type}.controller.ts`);
84
100
  generateFile(controllerFilePath, controllerTemplate);
@@ -97,21 +113,19 @@ class ControllerGeneratorHelper {
97
113
  * @param className - The name of the controller class.
98
114
  * @param type - The type of the controller, defined in the `ControllerType` enum.
99
115
  * @param controllerDir - The absolute path to the controller directory.
100
- */ generateBuilderFile(className, type, controllerDir) {
101
- const builderConfig = this.getBuilderConfig(type, className);
116
+ */ generateBuilderFile(className, kebabCaseName, type, controllerDir, parts) {
117
+ const builderConfig = this.getBuilderConfig(type, className, kebabCaseName, parts);
102
118
  if (!builderConfig) return;
103
119
  const builderTemplate = populateTemplate(builderConfig.template, builderConfig.variables);
104
- const buildersDir = path.join(controllerDir, 'builders');
105
- createDirectoryIfNotExists(buildersDir);
106
- const builderFilePath = path.join(buildersDir, 'sample.builder.ts');
107
- generateFile(builderFilePath, builderTemplate);
120
+ createDirectoryIfNotExists(path.join(controllerDir, 'builders'));
121
+ generateFile(this.builderFilePath(controllerDir, kebabCaseName), builderTemplate);
108
122
  }
109
123
  /**
110
124
  * Retrieves the configuration for generating a builder file based on the controller type and class name.
111
125
  * @param type - The type of the controller, defined in the `ControllerType` enum.
112
126
  * @param className - The name of the controller class.
113
127
  * @returns An object containing the builder template path and variables, or `undefined` if not found.
114
- */ getBuilderConfig(type, className) {
128
+ */ getBuilderConfig(type, className, kebabCaseName, parts) {
115
129
  const baseDir = path.resolve(__dirname$1, '..', 'builder-template', 'builder');
116
130
  const templates = {
117
131
  [ControllerType.CONTEXT_MENU]: 'context-menu.builder.template',
@@ -119,11 +133,14 @@ class ControllerGeneratorHelper {
119
133
  [ControllerType.PRIMARY_ENTRY_POINT]: 'primary-entry-point.builder.template'
120
134
  };
121
135
  const template = templates[type] ? path.resolve(baseDir, templates[type]) : undefined;
136
+ const variables = {
137
+ className,
138
+ builderClassName: `${className}CommandBuilder`,
139
+ commandName: commandNameFor(parts, kebabCaseName)
140
+ };
122
141
  return template ? {
123
142
  template,
124
- variables: {
125
- className
126
- }
143
+ variables
127
144
  } : undefined;
128
145
  }
129
146
  }
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import { Logger } from '../../common/logger.js';
3
3
  import '../../common/theme.js';
4
- import { validateAndFormatName, buildTemplate, createDirectoryIfNotExists, generateFile } from '../../util/generator-cli.util.js';
4
+ import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, generateFile } from '../../util/generator-cli.util.js';
5
5
 
6
6
  class GuardGeneratorHelper {
7
7
  /**
@@ -20,13 +20,19 @@ class GuardGeneratorHelper {
20
20
  const { parts, kebabCaseName, className } = validateAndFormatName(guardName);
21
21
  const guardDir = path.join(process.cwd(), 'src', 'guards', ...parts);
22
22
  const guardFile = path.join(guardDir, `${kebabCaseName}.guard.ts`);
23
+ const specFile = path.join(guardDir, `${kebabCaseName}.guard.spec.ts`);
24
+ // Both files checked before either is written, so an existing guard is never replaced.
25
+ assertFilesAbsent([
26
+ guardFile,
27
+ specFile
28
+ ]);
23
29
  const guardTemplate = buildTemplate(className, 'guard.template');
24
30
  const specTemplate = buildTemplate(className, 'guard.spec.template', {
25
31
  kebabCaseName
26
32
  });
27
33
  createDirectoryIfNotExists(guardDir);
28
34
  generateFile(guardFile, guardTemplate);
29
- generateFile(path.join(guardDir, `${kebabCaseName}.guard.spec.ts`), specTemplate);
35
+ generateFile(specFile, specTemplate);
30
36
  }
31
37
  constructor(appName){
32
38
  this.appName = appName;
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import { Logger } from '../../common/logger.js';
3
3
  import '../../common/theme.js';
4
- import { validateAndFormatName, buildTemplate, createDirectoryIfNotExists, generateFile } from '../../util/generator-cli.util.js';
4
+ import { validateAndFormatName, assertFilesAbsent, buildTemplate, createDirectoryIfNotExists, generateFile } from '../../util/generator-cli.util.js';
5
5
 
6
6
  class ServiceGeneratorHelper {
7
7
  /**
@@ -20,13 +20,19 @@ class ServiceGeneratorHelper {
20
20
  const { parts, kebabCaseName, className } = validateAndFormatName(serviceName);
21
21
  const serviceDir = path.join(process.cwd(), 'src', 'services', ...parts);
22
22
  const serviceFile = path.join(serviceDir, `${kebabCaseName}.service.ts`);
23
+ const specFile = path.join(serviceDir, `${kebabCaseName}.service.spec.ts`);
24
+ // Both files checked before either is written, so an existing service is never replaced.
25
+ assertFilesAbsent([
26
+ serviceFile,
27
+ specFile
28
+ ]);
23
29
  const serviceTemplate = buildTemplate(className, 'service.template');
24
30
  const specTemplate = buildTemplate(className, 'service.spec.template', {
25
31
  kebabCaseName
26
32
  });
27
33
  createDirectoryIfNotExists(serviceDir);
28
34
  generateFile(serviceFile, serviceTemplate);
29
- generateFile(path.join(serviceDir, `${kebabCaseName}.service.spec.ts`), specTemplate);
35
+ generateFile(specFile, specTemplate);
30
36
  }
31
37
  constructor(appName){
32
38
  this.appName = appName;