cli: Add pluginPackage support to backend-plugin-module template

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2026-02-02 15:28:42 +01:00
committed by Patrik Oldsberg
parent ef910b05b9
commit 971cc9457a
7 changed files with 127 additions and 4 deletions
@@ -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',
});
});
});
});
@@ -28,6 +28,13 @@ import {
import { PortableTemplate } from '../types';
import { resolvePackageParams } from './resolvePackageParams';
const knownPluginPackages: Record<string, string> = {
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<DistinctQuestion> {
@@ -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);
}
+7 -1
View File
@@ -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 = {