diff --git a/docs/tooling/cli/04-templates.md b/docs/tooling/cli/04-templates.md index 2ffb49c343..c32fcd8278 100644 --- a/docs/tooling/cli/04-templates.md +++ b/docs/tooling/cli/04-templates.md @@ -64,14 +64,14 @@ Custom templates can be installed from local directories. To install a template "backstage": { "cli": { "new": { - "templates": ["./templates/custom-plugin.yaml"] + "templates": ["./templates/custom-plugin"] } } } } ``` -Each entry in the `templates` array should be relative path that points either directly to a template YAML file or a directory containing a `template.yaml` file. If the path starts with `./` it will be use as is, otherwise it will be resolved as a module within `node_modules`. +Each entry in the `templates` array should be relative path that points to a directory containing a `template.yaml` file. If the path starts with `./` it will be use as is, otherwise it will be resolved as a module within `node_modules`. When defining the `templates` array it will override the default set of templates. If you want to keep using and of the build-in templates in the Backstage CLI you can reference them directly within the CLI package. This following is the full list of built-in templates: @@ -100,9 +100,11 @@ When defining the `templates` array it will override the default set of template ## Creating your own CLI templates -Your first step in creating your own CLI template is composing your yaml file: +Each template lives in its own directory and must have a `template.yaml` file that describes the template. The template directory can also contain any files that should be templated or copied to the generated package. -```yaml title="in templates/custom-plugin.yaml" +Start by creating `template.yaml` in a new directory somewhere in your project, in this example we're using `./templates/custom-plugin/template.yaml`: + +```yaml title="in templates/custom-plugin/template.yaml" name: custom-plugin role: frontend-plugin description: Description of my CLI template # optional @@ -117,25 +119,18 @@ The following properties are supported: - `description` - A description of the type of package that this template produces. - `values` - A map of additional values that will be present during templating. The values are themselves templated and can reference other values. If the key matches any of the user prompts, such as `pluginId`, the value will be used directly instead of prompting the user. -Once you have your composed template yaml file, [add your new template](#installing-custom-templates) to the CLI config in your root `package.json`: +Next, add any other files you want to be part of the template to the same directory. All files will be copied as is, except any files with a `.hbs` extension. They will be treated as [Handlebars](https://handlebarsjs.com/) templates and will be rendered with the values from the `template.yaml` file as well as additional prompts such as `pluginId`. For example, you could create a `src/index.ts` file with the following content: -```diff -{ - // ... - "backstage": { - "cli": { - "new": { -+ "templates": [ -+ "./templates/custom-plugin.yaml" -+ ] - } - } - } +```typescript title="in templates/custom-plugin/src/index.ts.hbs" +export function getPluginId() { + return '{{ pluginId }}'; } ``` If you'd like to see more examples, you can find all the default templates and its yaml files [here](https://github.com/backstage/backstage/tree/master/packages/cli/templates). +Once your template is ready, [add it to your config](#installing-custom-templates), and you should now be able to select it when running `yarn new`. + ### Template Roles The `role` property in the template yaml file is used to determine what input will be gathered for the template, as well as what actions will be taken after the new package has been created. The following roles are supported: diff --git a/packages/cli/src/lib/new/preparation/loadPortableTemplate.test.ts b/packages/cli/src/lib/new/preparation/loadPortableTemplate.test.ts index 326c8203ec..21734f3da9 100644 --- a/packages/cli/src/lib/new/preparation/loadPortableTemplate.test.ts +++ b/packages/cli/src/lib/new/preparation/loadPortableTemplate.test.ts @@ -16,6 +16,7 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; import { loadPortableTemplate } from './loadPortableTemplate'; +import { TEMPLATE_FILE_NAME } from '../types'; describe('loadTemplate', () => { const mockDir = createMockDirectory(); @@ -26,19 +27,21 @@ describe('loadTemplate', () => { it('should load a valid template', async () => { mockDir.setContent({ - 'path/to/template.yaml': ` + 'path/to': { + [TEMPLATE_FILE_NAME]: ` name: template1 role: frontend-plugin values: foo: bar `, + }, 'path/to/hello.txt': 'hello world', }); await expect( loadPortableTemplate({ name: 'template1', - target: mockDir.resolve('path/to/template.yaml'), + target: mockDir.resolve('path/to', TEMPLATE_FILE_NAME), }), ).resolves.toEqual({ name: 'template1', diff --git a/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.test.ts b/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.test.ts index 3ce3cbd76b..ca71dfc668 100644 --- a/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.test.ts +++ b/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.test.ts @@ -18,6 +18,7 @@ import { realpathSync } from 'node:fs'; import { loadPortableTemplateConfig } from './loadPortableTemplateConfig'; import { defaultTemplates } from '../defaultTemplates'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { TEMPLATE_FILE_NAME } from '../types'; describe('loadPortableTemplateConfig', () => { const mockDir = createMockDirectory(); @@ -32,7 +33,7 @@ describe('loadPortableTemplateConfig', () => { backstage: { cli: { new: { - templates: ['./path/to/template1', './path/to/template2.yaml'], + templates: ['./path/to/template1'], globals: { license: 'MIT', private: true, @@ -43,9 +44,9 @@ describe('loadPortableTemplateConfig', () => { }, }, }), - 'path/to/template1/template.yaml': - 'name: template1\nrole: frontend-plugin\n', - 'path/to/template2.yaml': 'name: template2\nrole: frontend-plugin\n', + 'path/to/template1': { + [TEMPLATE_FILE_NAME]: 'name: template1\nrole: frontend-plugin\n', + }, }); await expect( @@ -57,11 +58,7 @@ describe('loadPortableTemplateConfig', () => { templatePointers: [ { name: 'template1', - target: mockDir.resolve('path/to/template1/template.yaml'), - }, - { - name: 'template2', - target: mockDir.resolve('path/to/template2.yaml'), + target: mockDir.resolve('path/to/template1', TEMPLATE_FILE_NAME), }, ], license: 'MIT', @@ -84,11 +81,7 @@ describe('loadPortableTemplateConfig', () => { templatePointers: [ { name: 'template1', - target: mockDir.resolve('path/to/template1/template.yaml'), - }, - { - name: 'template2', - target: mockDir.resolve('path/to/template2.yaml'), + target: mockDir.resolve('path/to/template1', TEMPLATE_FILE_NAME), }, ], license: 'nope', @@ -121,14 +114,14 @@ describe('loadPortableTemplateConfig', () => { package: { templates: { plugin: { - 'template.yaml': + [TEMPLATE_FILE_NAME]: 'name: frontend-plugin\nrole: frontend-plugin\n', }, }, }, }, 'my-package': { - 'template.yaml': 'name: backend-plugin\nrole: backend-plugin\n', + [TEMPLATE_FILE_NAME]: 'name: backend-plugin\nrole: backend-plugin\n', }, }, }); @@ -144,14 +137,15 @@ describe('loadPortableTemplateConfig', () => { name: 'frontend-plugin', target: realpathSync( mockDir.resolve( - 'node_modules/@my/package/templates/plugin/template.yaml', + 'node_modules/@my/package/templates/plugin', + TEMPLATE_FILE_NAME, ), ), }, { name: 'backend-plugin', target: realpathSync( - mockDir.resolve('node_modules/my-package/template.yaml'), + mockDir.resolve('node_modules/my-package', TEMPLATE_FILE_NAME), ), }, ], @@ -180,7 +174,7 @@ describe('loadPortableTemplateConfig', () => { node_modules: Object.fromEntries( defaultTemplates.map(t => [ t, - { 'template.yaml': `name: x\nrole: web-library\n` }, + { [TEMPLATE_FILE_NAME]: `name: x\nrole: web-library\n` }, ]), ), }); @@ -194,7 +188,7 @@ describe('loadPortableTemplateConfig', () => { templatePointers: defaultTemplates.map(t => ({ name: 'x', target: realpathSync( - mockDir.resolve(`node_modules/${t}/template.yaml`), + mockDir.resolve(`node_modules/${t}`, TEMPLATE_FILE_NAME), ), })), license: 'MIT', @@ -255,7 +249,7 @@ describe('loadPortableTemplateConfig', () => { node_modules: Object.fromEntries( defaultTemplates.map(t => [ t, - { 'template.yaml': `name: x\nrole: web-library\n` }, + { [TEMPLATE_FILE_NAME]: `name: x\nrole: web-library\n` }, ]), ), }); diff --git a/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.ts b/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.ts index 35f24799a7..7cca6ba836 100644 --- a/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.ts +++ b/packages/cli/src/lib/new/preparation/loadPortableTemplateConfig.ts @@ -15,10 +15,14 @@ */ import fs from 'fs-extra'; -import { resolve as resolvePath, dirname, extname } from 'node:path'; +import { resolve as resolvePath, dirname } from 'node:path'; import { paths } from '../../paths'; import { defaultTemplates } from '../defaultTemplates'; -import { PortableTemplateConfig, PortableTemplatePointer } from '../types'; +import { + PortableTemplateConfig, + PortableTemplatePointer, + TEMPLATE_FILE_NAME, +} from '../types'; import { parse as parseYaml } from 'yaml'; import { z } from 'zod'; import { fromZodError } from 'zod-validation-error'; @@ -122,19 +126,13 @@ export async function loadPortableTemplateConfig( } function resolveLocalTemplatePath(pointer: string, basePath: string): string { - const isDirectoryPointer = extname(pointer) === ''; - if (pointer.startsWith('.')) { - if (isDirectoryPointer) { - return resolvePath(basePath, pointer, 'template.yaml'); - } - return resolvePath(basePath, pointer); + return resolvePath(basePath, pointer, TEMPLATE_FILE_NAME); } - if (isDirectoryPointer) { - return require.resolve(`${pointer}/template.yaml`, { paths: [basePath] }); - } - return require.resolve(pointer, { paths: [basePath] }); + return require.resolve(`${pointer}/${TEMPLATE_FILE_NAME}`, { + paths: [basePath], + }); } const partialTemplateDefinitionSchema = z.object({ diff --git a/packages/cli/src/lib/new/types.ts b/packages/cli/src/lib/new/types.ts index 61fd6110fd..ebf28ee69c 100644 --- a/packages/cli/src/lib/new/types.ts +++ b/packages/cli/src/lib/new/types.ts @@ -38,6 +38,8 @@ export type PortableTemplateConfig = { packageNamePluginInfix: string; }; +export const TEMPLATE_FILE_NAME = 'template.yaml'; + export type PortableTemplatePointer = { name: string; description?: string;