diff --git a/.changeset/ninety-suits-drum.md b/.changeset/ninety-suits-drum.md new file mode 100644 index 0000000000..1eb0a7f1db --- /dev/null +++ b/.changeset/ninety-suits-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The `new` command now prompts for the plugin package name when creating a backend plugin module, in order to properly populate the `package.json` file. diff --git a/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.test.ts b/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.test.ts index 905ec0b0c5..ee45f7a708 100644 --- a/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.test.ts +++ b/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.test.ts @@ -149,4 +149,70 @@ describe('collectTemplateParams', () => { ], }); }); + + describe('backend-plugin-module with pluginPackage', () => { + const backendModuleOptions = { + ...baseOptions, + template: { + name: 'test-module', + role: 'backend-plugin-module' as const, + files: [], + values: {}, + }, + }; + + it('should auto-fill pluginPackage for catalog plugin without prompting', async () => { + await expect( + collectPortableTemplateInput({ + ...backendModuleOptions, + prefilledParams: { + pluginId: 'catalog', + moduleId: 'my-module', + }, + }), + ).resolves.toEqual({ + roleParams: { + role: 'backend-plugin-module', + pluginId: 'catalog', + moduleId: 'my-module', + pluginPackage: '@backstage/plugin-catalog-backend', + }, + owner: undefined, + version: '0.1.0', + license: 'Apache-2.0', + private: true, + packageName: '@internal/plugin-catalog-backend-module-my-module', + packagePath: 'plugins/catalog-backend-module-my-module', + }); + }); + + it('should prompt for pluginPackage for unknown plugins', async () => { + jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ + pluginPackage: '@mycompany/plugin-custom-backend', + }); + + await expect( + collectPortableTemplateInput({ + ...backendModuleOptions, + prefilledParams: { + pluginId: 'custom', + moduleId: 'my-extension', + }, + }), + ).resolves.toEqual({ + roleParams: { + role: 'backend-plugin-module', + pluginId: 'custom', + moduleId: 'my-extension', + pluginPackage: '@mycompany/plugin-custom-backend', + }, + owner: undefined, + version: '0.1.0', + license: 'Apache-2.0', + private: true, + packageName: '@internal/plugin-custom-backend-module-my-extension', + packagePath: 'plugins/custom-backend-module-my-extension', + }); + }); + }); }); diff --git a/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.ts b/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.ts index 63e3791a49..d7521407dc 100644 --- a/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.ts +++ b/packages/cli-module-new/src/lib/preparation/collectPortableTemplateInput.ts @@ -28,6 +28,13 @@ import { import { PortableTemplate } from '../types'; import { resolvePackageParams } from './resolvePackageParams'; +const knownPluginPackages: Record = { + auth: '@backstage/plugin-auth-backend', + catalog: '@backstage/plugin-catalog-backend', + events: '@backstage/plugin-events-backend', + scaffolder: '@backstage/plugin-scaffolder-backend', +}; + type CollectTemplateParamsOptions = { config: PortableTemplateConfig; template: PortableTemplate; @@ -80,11 +87,16 @@ export async function collectPortableTemplateInput( ...promptAnswers, }; + const pluginPackage = + knownPluginPackages[answers.pluginId as string] ?? + (answers.pluginPackage as string); + const roleParams = { role: template.role, name: answers.name, pluginId: answers.pluginId, moduleId: answers.moduleId, + pluginPackage, } as PortableTemplateInputRoleParams; const packageParams = resolvePackageParams({ @@ -153,6 +165,26 @@ export function moduleIdIdPrompt(): DistinctQuestion { }; } +export function pluginPackagePrompt(): DistinctQuestion { + return { + type: 'input', + name: 'pluginPackage', + message: + 'Enter the package name of the plugin this module extends (e.g. @backstage/plugin-catalog-backend) [required]', + validate: (value: string) => { + if (!value) { + return 'Please enter the package name of the plugin'; + } + if (!isValidNpmPackageName(value)) { + return 'Please enter a valid npm package name (e.g. @backstage/plugin-catalog-backend or my-plugin-backend)'; + } + return true; + }, + when: (answers: PortableTemplateParams) => + !knownPluginPackages[answers.pluginId as string], + }; +} + export function getPromptsForRole( role: PortableTemplateRole, ): Array { @@ -169,8 +201,9 @@ export function getPromptsForRole( case 'backend-plugin': return [pluginIdPrompt()]; case 'frontend-plugin-module': - case 'backend-plugin-module': return [pluginIdPrompt(), moduleIdIdPrompt()]; + case 'backend-plugin-module': + return [pluginIdPrompt(), moduleIdIdPrompt(), pluginPackagePrompt()]; default: return []; } @@ -195,3 +228,13 @@ export function ownerPrompt(): DistinctQuestion { }, }; } + +// Reuses the same pattern as namePrompt/pluginIdPrompt but extended to support npm scopes +// Matches: @scope/package-name, @scope/package, package-name, package +const packageNamePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/; +const scopedPackageNamePattern = + /^@[a-z0-9]+(-[a-z0-9]+)*\/[a-z0-9]+(-[a-z0-9]+)*$/; + +function isValidNpmPackageName(name: string) { + return packageNamePattern.test(name) || scopedPackageNamePattern.test(name); +} diff --git a/packages/cli-module-new/src/lib/types.ts b/packages/cli-module-new/src/lib/types.ts index 83a195a43a..913c74b672 100644 --- a/packages/cli-module-new/src/lib/types.ts +++ b/packages/cli-module-new/src/lib/types.ts @@ -94,9 +94,15 @@ export type PortableTemplateInputRoleParams = pluginId: string; } | { - role: 'frontend-plugin-module' | 'backend-plugin-module'; + role: 'frontend-plugin-module'; pluginId: string; moduleId: string; + } + | { + role: 'backend-plugin-module'; + pluginId: string; + moduleId: string; + pluginPackage: string; }; export type PortableTemplateInput = { diff --git a/packages/cli-module-new/templates/backend-plugin-module/package.json.hbs b/packages/cli-module-new/templates/backend-plugin-module/package.json.hbs index c34f7e0646..5e21e5c2eb 100644 --- a/packages/cli-module-new/templates/backend-plugin-module/package.json.hbs +++ b/packages/cli-module-new/templates/backend-plugin-module/package.json.hbs @@ -10,7 +10,8 @@ }, "backstage": { "role": "backend-plugin-module", - "pluginId": "{{pluginId}}" + "pluginId": "{{pluginId}}", + "pluginPackage": "{{pluginPackage}}" }, "scripts": { "start": "backstage-cli package start", diff --git a/packages/cli-module-new/templates/scaffolder-backend-module/package.json.hbs b/packages/cli-module-new/templates/scaffolder-backend-module/package.json.hbs index 72cb370443..bb7704964d 100644 --- a/packages/cli-module-new/templates/scaffolder-backend-module/package.json.hbs +++ b/packages/cli-module-new/templates/scaffolder-backend-module/package.json.hbs @@ -10,7 +10,8 @@ }, "backstage": { "role": "backend-plugin-module", - "pluginId": "scaffolder" + "pluginId": "scaffolder", + "pluginPackage": "@backstage/plugin-scaffolder-backend" }, "scripts": { "start": "backstage-cli package start", diff --git a/packages/cli-module-new/templates/scaffolder-backend-module/portable-template.yaml b/packages/cli-module-new/templates/scaffolder-backend-module/portable-template.yaml index a131c14062..fc7669cef3 100644 --- a/packages/cli-module-new/templates/scaffolder-backend-module/portable-template.yaml +++ b/packages/cli-module-new/templates/scaffolder-backend-module/portable-template.yaml @@ -3,4 +3,5 @@ role: backend-plugin-module description: A module exporting custom actions for @backstage/plugin-scaffolder-backend values: pluginId: scaffolder + pluginPackage: '@backstage/plugin-scaffolder-backend' moduleVar: '{{ camelCase pluginId }}Module{{ upperFirst ( camelCase moduleId ) }}'