ame') }} ``` - **Input**: `{ kind: 'Group', namespace: 'default', name: 'techdocs' }` - **Output**: `techdocs` ### projectSlug The `projectSlug` filter generates a project slug from a repository URL. **Usage Example** ```yaml - id: log name: Project Slug action: debug:log input: message: ${{ parameters.repoUrl | projectSlug }} ``` - **Input**: `github.com?repo=backstage&owner=backstage` - **Output**: `backstage/backstage` # Template Globals In addition to its powerful filtering functionality, the Nunjucks engine allows access from the template expression context to specified globally-accessible references. Backstage propagates this capability via the scaffolder backend plugin, which we shall soon see in action. # Customizing the templating environment Custom plugins make it possible to install your own template extensions, which may be any combination of filters, global functions and global values. With the new backend you would use a scaffolder plugin module for this; later we will demonstrate the analogous approach with the old backend. ## Streamlining Template Extension Module Creation with the Backstage CLI The creation of a "template environment customization" module in Backstage can be accelerated using the Backstage CLI. Start by using the `yarn backstage-cli new` command to generate a scaffolder module. This command sets up the necessary boilerplate code, providing a smooth start: ``` $ yarn backstage-cli new ? What do you want to create? > backend-module - A new backend module that extends an existing backend plugin with additional features backend-plugin - A new backend plugin plugin - A new frontend plugin node-library - A new node-library package, exporting shared functionality for backend plugins and modules plugin-common - A new isomorphic common plugin package plugin-node - A new Node.js library plugin package plugin-react - A new web library plugin package scaffolder-module - An module exporting custom actions for @backstage/plugin-scaffolder-backend ``` When prompted, select the option to generate a backend module. Since we want to extend the Scaffolder backend, enter `scaffolder` when prompted for the plugin to extend. Next, enter a name for your module (relative to the generated `scaffolder-backend-module-` prefix), and the CLI will generate the required files and directory structure. ## Writing your Module Once the CLI has generated the essential structure for your new scaffolder module, it's time to implement our template extensions. Here we'll demonstrate how to create each of the supported extension types. `src/module.ts` is where the magic happens. First we prepare to utilize the associated (_**alpha** phase_) API extension point by adding: ```ts import { scaffolderTemplatingExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; ``` Considering the generated code, you may observe that everything rests on the `createBackendModule` call, which after providing some minimal metadata to establish context, specifies a `register` callback whose sole responsibility here is to call, in turn, `registerInit` against the `BackendModuleRegistrationPoints` argument it receives. Modify this call to make the `scaffolderTemplatingExtensionPoint` available to the specified `init` function: ```ts register(reg) { reg.registerInit({ deps: { ..., templating: scaffolderTemplatingExtensionPoint, }, async init({ ..., templating }) { ... }; }); }; ``` Now we're ready to extend the scaffolder templating engine. For our purposes here we'll drop everything in `module.ts`; use your own judgment as to the organization of your real-world plugin modules. ### Custom Filter In this contrived example we add a filter to test whether the incoming string value contains (at least) a specified number of occurrences of a given substring. We can easily define this by adding code to our `init` callback: ```ts async init({ ..., templating, }) { ... templating.addTemplateFilters({ containsOccurrences: (arg: string, substring: string, times: number) => { let pos = 0; let count = 0; while (pos < arg.length) { pos = arg.indexOf(substring, pos); if (pos < 0) { break; } count++; } return count === times; }, }); }, ``` This demonstrates the bare minimum: a TypeScript `Record` of named template filter implementations to register. However, by adopting an alternate structure we can document our filter with additional metadata; to utilize this capability we begin by adding a new import: ```ts import { createTemplateFilter } from '@backstage/plugin-scaffolder-node/alpha'; ``` Then, update your `init` implementation to specify an array rather than an object/record: ```ts async init({ ..., templating, }) { ... templating.addTemplateFilters([ createTemplateFilter({ id: 'containsOccurrences', description: 'determine whether filter input contains a substring N times', filter: (arg: string, substring: string, times: number) => { let pos = 0; let count = 0; while (pos < arg.length) { pos = arg.indexOf(substring, pos); if (pos < 0) { break; } count++; } return count === times; }, }), ]); }, ``` With this we have added a `description` to our filter, which helps a template author to understand the filter's purpose. #### Schema To enhance our filter documentation further, we will specify its `schema` using a callback against the [Zod][] schema declaration library: ```ts createTemplateFilter({ id: 'containsOccurrences', description: 'determine whether filter input contains a substring N times', schema: z => z.function( z.tuple([ z.string().describe('input'), z.string().describe('substring whose occurrences to find'), z.number().describe('number of occurrences to check for'), ]), z.boolean(), ), ..., }), ``` Because a filter is, in fact, a function, its schema is defined by generating a [Zod function schema][zod-fn] against the parameter supplied to our schema callback. A filter function is required to have at least one argument; in this example, we have two additional arguments. But what if we modify our filter's implementation function to make `times` optional? Code: ```ts createTemplateFilter({ id: 'containsOccurrences', ..., filter: (arg: string, substring: string, times?: number) => { if (times === undefined) { // note that, in real life, simply calling this function directly with Nunjucks would suffice rather than implementing a filter: return arg.includes(substring); } // original implementation follows ... }, }), ``` In this case we should modify our `schema`: ```ts createTemplateFilter({ ..., schema: z => z.function( z.tuple([ z.string().describe('input'), z.string().describe('substring whose occurrences to find'), z .number() .describe('number of occurrences to check for') .optional(), ]), z.boolean(), ), ..., }), ``` #### Filter Example Documentation Our filter documentation may benefit from examples which we specify thus: ```ts createTemplateFilter({ ..., examples: [ { description: 'Basic Usage', example: `\ - name: Contains Occurrences action: debug:log input: message: \${{ parameters.projectName | containsOccurrences('-', 2) }} `, notes: `\ - **Input**: \`foo-bar-baz\` - **Output**: \`true\` `, }, { description: 'Omitting Optional Parameter', example: `\ - name: Contains baz action: debug:log input: message: \${{ parameters.projectName | containsOccurrences('baz') | dump }} `, notes: `\ - **Input**: \`foo-bar\` - **Output**: \`false\` `, }, ], }), ``` ### Custom Global Function In case your template needs access to a value generated from a function not appropriately modeled as a filter, Nunjucks supports the direct invocation of [global functions][global-fn]. We might, for example, add to `init`: ```ts async init({ ..., templating, }) { ... templating.addTemplateGlobals({ now: () => new Date().toISOString(), }); }, ``` Here we have implemented a simple mechanism to obtain a timestamp (note that because we can only pass JSON-compatible--or `undefined`--values we have chosen to model a date/time as an ISO string) using a globally available function. Again we have the option to make our global function self-documenting. Import: ```ts import { ..., createTemplateGlobalFunction, } from '@backstage/plugin-scaffolder-node/alpha'; ``` Then modify: ```ts ... templating.addTemplateGlobals([ createTemplateGlobalFunction({ id: 'now', description: 'obtain an ISO representation of the current date and time', fn: () => new Date().toISOString(), }), ]); ``` #### Schema Declaring a global function schema is quite like the schema declaration for a template filter: ```ts createTemplateGlobal({ ..., schema: z => z.function().args().returns(z.string()), ..., }), ``` #### Template Global Function Example Documentation Again, this works in the same way as filter examples: ```ts createTemplateGlobal({ ..., examples: [ { description: 'Obtain the current date/time', example: `\ - name: Log Timestamp action: debug:log input: message: Current date/time: \${{ now() }} `, // optional `notes` omitted from this example }, ], ..., }), ``` ### Custom Global Value Alternatively, your template may need access to a simple JSON value, which can be registered in this manner: ```ts async init({ ..., templating, }) { ... templating.addTemplateGlobals({ ..., preferredMetasyntacticIdentifier: 'foo', }); }, ``` Or the documenting form: ```ts async init({ ..., templating, }) { ... templating.addTemplateGlobals([ ..., createTemplateGlobalValue({ id: 'preferredMetasyntacticVariable', value: 'foo', description: 'This description is as contrived as the global value it documents', }), ]); }, ``` ## Register Template Extensions with the Legacy Backend System Users of the original Backstage backend can register template extensions by specifying options to the scaffolder backend plugin's `createRouter` function (customarily called in `packages/backend/src/plugins/scaffolder.ts`): - `additionalTemplateFilters` - either of: - object mapping filter name to implementation function, or - array of documented template filters as returned by the utility function `createTemplateFilter` - `additionalTemplateGlobals` - either of: - object mapping global name to value or function, or - array of documented global functions and values as returned by the utility functions `createTemplateGlobalFunction` and `createTemplateGlobalValue` [nunjucks]: https://mozilla.github.io/nunjucks [filter]: https://mozilla.github.io/nunjucks/templating.html#filters [global-fn]: https://mozilla.github.io/nunjucks/templating.html#global-functions [parseRepoUrl]: https://backstage.io/docs/reference/plugin-scaffolder-node.parserepourl [CompoundEntityRef]: https://backstage.io/docs/reference/catalog-model.compoundentityref [Zod]: https://zod.dev/ [zod-fn]: https://zod.dev/?id=functions [piped]: https://en.wikipedia.org/wiki/Pipeline_(Unix)#Pipelines_in_command_line_interfaces ‚m)nâ