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
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "ESNext",
4
+ "target": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "strictNullChecks": true,
8
+ "strictBindCallApply": true,
9
+ "strictPropertyInitialization": false,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "emitDecoratorMetadata": true,
13
+ "experimentalDecorators": true,
14
+ "resolveJsonModule": true,
15
+ "verbatimModuleSyntax": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "skipLibCheck": true,
19
+ "noImplicitAny": false,
20
+ "noEmit": true,
21
+ "outDir": "./dist",
22
+ "rootDir": ".",
23
+ "paths": {
24
+ "@src/*": ["./src/*"]
25
+ },
26
+ "types": ["node"]
27
+ },
28
+ "include": ["src/**/*.ts"],
29
+ "exclude": ["meocord.config.ts", "dist", "vitest.config.ts", "node_modules", "src/**/*.spec.ts"]
30
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["node", "vitest/globals"]
5
+ },
6
+ "include": ["src/**/*.ts"],
7
+ "exclude": ["node_modules"]
8
+ }
@@ -0,0 +1,35 @@
1
+ import { defineConfig } from 'vitest/config'
2
+ import swc from 'unplugin-swc'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ export default defineConfig({
6
+ // SWC rather than vite's esbuild: esbuild cannot emit decorator metadata, which the
7
+ // dependency injection behind @Controller and @Service reads at runtime.
8
+ plugins: [
9
+ swc.vite({
10
+ jsc: {
11
+ parser: { syntax: 'typescript', decorators: true },
12
+ transform: { legacyDecorator: true, decoratorMetadata: true },
13
+ target: 'es2022',
14
+ },
15
+ module: { type: 'es6' },
16
+ }),
17
+ ],
18
+ resolve: {
19
+ alias: { '@src': fileURLToPath(new URL('./src', import.meta.url)) },
20
+ },
21
+ test: {
22
+ globals: true,
23
+ setupFiles: ['reflect-metadata'],
24
+ clearMocks: true,
25
+ include: ['src/**/*.spec.ts'],
26
+ coverage: {
27
+ // istanbul rather than v8: v8 coverage needs Node's inspector API, which bun does
28
+ // not implement, and this template is expected to run under either runtime.
29
+ provider: 'istanbul',
30
+ all: false,
31
+ include: ['src/**/*.ts'],
32
+ exclude: ['src/**/*.spec.ts'],
33
+ },
34
+ },
35
+ })
@@ -0,0 +1,26 @@
1
+ import {
2
+ ApplicationCommandType,
3
+ ApplicationIntegrationType,
4
+ EntryPointCommandHandlerType,
5
+ InteractionContextType,
6
+ } from 'discord.js'
7
+ import { CommandBuilder } from 'meocord/decorator'
8
+ import { CommandType } from 'meocord/enum'
9
+
10
+ // Entry point commands have no builder class in @discordjs/builders, so the raw REST
11
+ // body is returned instead — MeoCord registers it as-is. `AppHandler` is what makes
12
+ // Discord send the interaction here; `DiscordLaunchActivity` opens the activity without
13
+ // ever reaching the bot.
14
+ @CommandBuilder(CommandType.PRIMARY_ENTRY_POINT)
15
+ export class SampleCommandBuilder {
16
+ build() {
17
+ return {
18
+ type: ApplicationCommandType.PrimaryEntryPoint as const,
19
+ name: 'sample-entry-point',
20
+ description: 'Launch the activity',
21
+ handler: EntryPointCommandHandlerType.AppHandler,
22
+ integration_types: [ApplicationIntegrationType.GuildInstall],
23
+ contexts: [InteractionContextType.Guild],
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,16 @@
1
+ import { AutocompleteInteraction } from 'discord.js'
2
+ import { Controller, Autocomplete } from 'meocord/decorator'
3
+
4
+ @Controller()
5
+ export class {{className}}AutocompleteController {
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')
10
+ async completeQuery(interaction: AutocompleteInteraction) {
11
+ const { value } = interaction.options.getFocused(true)
12
+ const choices = ['alpha', 'beta', 'gamma'].filter(choice => choice.startsWith(value))
13
+
14
+ await interaction.respond(choices.map(choice => ({ name: choice, value: choice })))
15
+ }
16
+ }
@@ -10,7 +10,7 @@ export class {{className}}ButtonController {
10
10
  await interaction.reply('Button clicked!')
11
11
  }
12
12
 
13
- @Command('button-with-{id}', CommandType.BUTTON)
13
+ @Command('button-with/{id}', CommandType.BUTTON)
14
14
  async handleButtonWithId(interaction: ButtonInteraction, { id }) {
15
15
  await interaction.reply(`Button with id: ${id} clicked!`)
16
16
  }
@@ -0,0 +1,12 @@
1
+ import { ChannelSelectMenuInteraction } from 'discord.js'
2
+ import { Controller, Command } from 'meocord/decorator'
3
+ import { CommandType } from 'meocord/enum'
4
+
5
+ @Controller()
6
+ export class {{className}}ChannelSelectMenuController {
7
+
8
+ @Command('channel-select-menu', CommandType.CHANNEL_SELECT_MENU)
9
+ async handleChannelSelectMenu(interaction: ChannelSelectMenuInteraction) {
10
+ await interaction.reply(`Selected: ${interaction.channels.map(channel => channel.toString()).join(', ')}`)
11
+ }
12
+ }
@@ -1,6 +1,6 @@
1
1
  import { UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction } from 'discord.js'
2
- import { Controller, Command, CommandType } from 'meocord/decorator'
3
- import { SampleCommandBuilder } from '@src/controllers/context-menu/builders/sample.builder'
2
+ import { Controller, Command } from 'meocord/decorator'
3
+ import { SampleCommandBuilder } from '{{builderImportPath}}'
4
4
 
5
5
  @Controller()
6
6
  export class {{className}}ContextMenuController {
@@ -0,0 +1,12 @@
1
+ import { MentionableSelectMenuInteraction } from 'discord.js'
2
+ import { Controller, Command } from 'meocord/decorator'
3
+ import { CommandType } from 'meocord/enum'
4
+
5
+ @Controller()
6
+ export class {{className}}MentionableSelectMenuController {
7
+
8
+ @Command('mentionable-select-menu', CommandType.MENTIONABLE_SELECT_MENU)
9
+ async handleMentionableSelectMenu(interaction: MentionableSelectMenuInteraction) {
10
+ await interaction.reply(`Selected ${interaction.users.size} user(s) and ${interaction.roles.size} role(s).`)
11
+ }
12
+ }
@@ -3,7 +3,7 @@ import { Controller, Command } from 'meocord/decorator'
3
3
  import { CommandType } from 'meocord/enum'
4
4
 
5
5
  @Controller()
6
- export class {{className}}ModalController {
6
+ export class {{className}}ModalSubmitController {
7
7
 
8
8
  @Command('submit-modal', CommandType.MODAL_SUBMIT)
9
9
  async handleModal(interaction: ModalSubmitInteraction) {
@@ -0,0 +1,12 @@
1
+ import { PrimaryEntryPointCommandInteraction } from 'discord.js'
2
+ import { Controller, Command } from 'meocord/decorator'
3
+ import { SampleCommandBuilder } from '{{builderImportPath}}'
4
+
5
+ @Controller()
6
+ export class {{className}}PrimaryEntryPointController {
7
+
8
+ @Command('sample-entry-point', SampleCommandBuilder)
9
+ async handleEntryPoint(interaction: PrimaryEntryPointCommandInteraction) {
10
+ await interaction.reply('Launching the activity.')
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ import { RoleSelectMenuInteraction } from 'discord.js'
2
+ import { Controller, Command } from 'meocord/decorator'
3
+ import { CommandType } from 'meocord/enum'
4
+
5
+ @Controller()
6
+ export class {{className}}RoleSelectMenuController {
7
+
8
+ @Command('role-select-menu', CommandType.ROLE_SELECT_MENU)
9
+ async handleRoleSelectMenu(interaction: RoleSelectMenuInteraction) {
10
+ await interaction.reply(`Selected: ${interaction.roles.map(role => role.name).join(', ')}`)
11
+ }
12
+ }
@@ -1,6 +1,6 @@
1
1
  import { ChatInputCommandInteraction } from 'discord.js'
2
2
  import { Controller, Command } from 'meocord/decorator'
3
- import { SampleCommandBuilder } from '@src/controllers/slash/builders/sample.builder'
3
+ import { SampleCommandBuilder } from '{{builderImportPath}}'
4
4
 
5
5
  @Controller()
6
6
  export class {{className}}SlashController {
@@ -0,0 +1,12 @@
1
+ import { UserSelectMenuInteraction } from 'discord.js'
2
+ import { Controller, Command } from 'meocord/decorator'
3
+ import { CommandType } from 'meocord/enum'
4
+
5
+ @Controller()
6
+ export class {{className}}UserSelectMenuController {
7
+
8
+ @Command('user-select-menu', CommandType.USER_SELECT_MENU)
9
+ async handleUserSelectMenu(interaction: UserSelectMenuInteraction) {
10
+ await interaction.reply(`Selected: ${interaction.users.map(user => user.username).join(', ')}`)
11
+ }
12
+ }
@@ -1,4 +1,5 @@
1
1
  import { Argument } from 'commander';
2
+ import { ControllerType } from '../enum/controller.enum.js';
2
3
  import { ControllerGeneratorHelper } from './helper/controller-generator.helper.js';
3
4
  import { Logger } from '../common/logger.js';
4
5
  import '../common/theme.js';
@@ -9,15 +10,9 @@ import wait from '../util/wait.util.js';
9
10
  class GeneratorCLI {
10
11
  register(program) {
11
12
  const generatorCommand = program.command('generate').alias('g').description('Generate components');
12
- generatorCommand.command('controller').alias('co').description('Generate a controller component').addArgument(new Argument('<type>', 'Type of the controller (e.g., button, context-menu, etc.)').choices([
13
- 'button',
14
- 'context-menu',
15
- 'message',
16
- 'modal-submit',
17
- 'reaction',
18
- 'select-menu',
19
- 'slash'
20
- ])).addArgument(new Argument('<name>', 'Name of the controller')).action(async (type, name)=>{
13
+ generatorCommand.command('controller').alias('co').description('Generate a controller component').addArgument(// Derived from the enum rather than listed by hand, so a new controller kind
14
+ // cannot be added to the framework and stay unreachable from the CLI.
15
+ new Argument('<type>', 'Type of the controller (e.g., button, context-menu, etc.)').choices(Object.values(ControllerType))).addArgument(new Argument('<name>', 'Name of the controller')).action(async (type, name)=>{
21
16
  await this.handleGenerateComponent({
22
17
  component: 'controller',
23
18
  type: type,
@@ -0,0 +1,78 @@
1
+ import fs__default from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { populateTemplate } from '../../util/generator-cli.util.js';
5
+
6
+ const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
7
+ /** Marks a packaged template file, and is dropped from the name that gets written. */ const TEMPLATE_SUFFIX = '.template';
8
+ /**
9
+ * Stands in for a leading dot while the file is packaged.
10
+ *
11
+ * npm omits a `.gitignore` from a published tarball, and this repository's own tooling
12
+ * would treat a packaged `.ts` as one of its sources, so nothing here is stored under
13
+ * the name it is written as.
14
+ */ const DOT_PREFIX = '_';
15
+ /**
16
+ * The prefix each package manager needs for the framework to run on its runtime.
17
+ *
18
+ * The separating space belongs to the value, because the template writes
19
+ * `{{runtimePrefix}}meocord` with nothing between them — a package manager that needs
20
+ * no prefix has to render as `meocord …` rather than ` meocord …`. Trimming an entry
21
+ * here would join it to the command that follows.
22
+ */ const RUNTIME_PREFIXES = {
23
+ // Without `--bun`, bun honours the CLI's `#!/usr/bin/env node` line and hands it to
24
+ // node, which an image built on bun alone does not have.
25
+ bun: 'bun --bun '
26
+ };
27
+ /**
28
+ * The script prefix for a package manager.
29
+ *
30
+ * @param packageManager - Package manager the application was created with.
31
+ */ function runtimePrefixFor(packageManager) {
32
+ return RUNTIME_PREFIXES[packageManager] ?? '';
33
+ }
34
+ /** The name a packaged template is written under. */ function outputName(templateName) {
35
+ const name = templateName.endsWith(TEMPLATE_SUFFIX) ? templateName.slice(0, -TEMPLATE_SUFFIX.length) : templateName;
36
+ return name.startsWith(DOT_PREFIX) ? `.${name.slice(DOT_PREFIX.length)}` : name;
37
+ }
38
+ /**
39
+ * Writes a new application from the template packaged with this framework.
40
+ *
41
+ * The template ships inside the package rather than being fetched, so the application a
42
+ * given release scaffolds is always one that release can run. A template resolved at
43
+ * generation time drifts from the CLI asking for it, in whichever direction happens to
44
+ * be newer.
45
+ */ class AppGeneratorHelper {
46
+ /**
47
+ * Renders every packaged template file into the target directory.
48
+ *
49
+ * @param targetDir - Directory to write the application into.
50
+ * @param variables - Values substituted into the template.
51
+ * @returns The written paths, relative to the target directory.
52
+ */ generateApp(targetDir, variables) {
53
+ return this.templateFiles(this.templateDir).map((templatePath)=>{
54
+ const relative = path.relative(this.templateDir, templatePath).split(path.sep).map((segment)=>outputName(segment)).join(path.sep);
55
+ const destination = path.join(targetDir, relative);
56
+ fs__default.mkdirSync(path.dirname(destination), {
57
+ recursive: true
58
+ });
59
+ fs__default.writeFileSync(destination, populateTemplate(templatePath, variables));
60
+ return relative;
61
+ });
62
+ }
63
+ templateFiles(dir) {
64
+ return fs__default.readdirSync(dir, {
65
+ withFileTypes: true
66
+ }).flatMap((entry)=>{
67
+ const full = path.join(dir, entry.name);
68
+ return entry.isDirectory() ? this.templateFiles(full) : [
69
+ full
70
+ ];
71
+ });
72
+ }
73
+ constructor(){
74
+ this.templateDir = path.resolve(__dirname$1, '..', 'app-template');
75
+ }
76
+ }
77
+
78
+ export { AppGeneratorHelper, runtimePrefixFor };
@@ -14,7 +14,7 @@ class ControllerGeneratorHelper {
14
14
  */ generateController(args, type) {
15
15
  const { parts, kebabCaseName, className } = validateAndFormatName(args.controllerName);
16
16
  const controllerDir = path.join(process.cwd(), 'src', 'controllers', type, ...parts);
17
- const template = this.buildControllerTemplate(className, type);
17
+ const template = this.buildControllerTemplate(className, type, parts);
18
18
  this.generateControllerStructure(controllerDir, kebabCaseName, className, type, template);
19
19
  }
20
20
  /**
@@ -23,8 +23,8 @@ class ControllerGeneratorHelper {
23
23
  * @param type - The type of the controller, defined in the `ControllerType` enum.
24
24
  * @returns The populated template string for the controller.
25
25
  * @throws Will throw an error if the controller type is unsupported.
26
- */ buildControllerTemplate(className, type) {
27
- const templateConfig = this.getTemplateConfig(type, className);
26
+ */ buildControllerTemplate(className, type, parts = []) {
27
+ const templateConfig = this.getTemplateConfig(type, className, parts);
28
28
  if (!templateConfig) {
29
29
  throw new Error(`Unsupported controller type: ${type}`);
30
30
  }
@@ -35,22 +35,38 @@ class ControllerGeneratorHelper {
35
35
  * @param type - The type of the controller, defined in the `ControllerType` enum.
36
36
  * @param className - The name of the controller class.
37
37
  * @returns An object containing the template path and variables, or `undefined` if not found.
38
- */ getTemplateConfig(type, className) {
38
+ */ getTemplateConfig(type, className, parts = []) {
39
39
  const baseDir = path.resolve(__dirname$1, '..', 'builder-template', 'controller');
40
40
  const templates = {
41
41
  [ControllerType.BUTTON]: 'button.controller.template',
42
42
  [ControllerType.MODAL_SUBMIT]: 'modal-submit.controller.template',
43
43
  [ControllerType.SELECT_MENU]: 'select-menu.controller.template',
44
+ [ControllerType.USER_SELECT_MENU]: 'user-select-menu.controller.template',
45
+ [ControllerType.ROLE_SELECT_MENU]: 'role-select-menu.controller.template',
46
+ [ControllerType.MENTIONABLE_SELECT_MENU]: 'mentionable-select-menu.controller.template',
47
+ [ControllerType.CHANNEL_SELECT_MENU]: 'channel-select-menu.controller.template',
44
48
  [ControllerType.REACTION]: 'reaction.controller.template',
45
49
  [ControllerType.MESSAGE]: 'message.controller.template',
46
50
  [ControllerType.CONTEXT_MENU]: 'context-menu.controller.template',
47
- [ControllerType.SLASH]: 'slash.controller.template'
51
+ [ControllerType.SLASH]: 'slash.controller.template',
52
+ [ControllerType.AUTOCOMPLETE]: 'autocomplete.controller.template',
53
+ [ControllerType.PRIMARY_ENTRY_POINT]: 'primary-entry-point.controller.template'
48
54
  };
49
55
  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.
59
+ const builderImportPath = [
60
+ '@src/controllers',
61
+ type,
62
+ ...parts,
63
+ 'builders/sample.builder'
64
+ ].join('/');
50
65
  return template ? {
51
66
  template,
52
67
  variables: {
53
- className
68
+ className,
69
+ builderImportPath
54
70
  }
55
71
  } : undefined;
56
72
  }
@@ -99,7 +115,8 @@ class ControllerGeneratorHelper {
99
115
  const baseDir = path.resolve(__dirname$1, '..', 'builder-template', 'builder');
100
116
  const templates = {
101
117
  [ControllerType.CONTEXT_MENU]: 'context-menu.builder.template',
102
- [ControllerType.SLASH]: 'slash.builder.template'
118
+ [ControllerType.SLASH]: 'slash.builder.template',
119
+ [ControllerType.PRIMARY_ENTRY_POINT]: 'primary-entry-point.builder.template'
103
120
  };
104
121
  const template = templates[type] ? path.resolve(baseDir, templates[type]) : undefined;
105
122
  return template ? {