add enhanced/documented filter/global types and creator functions

Signed-off-by: Matt Benson <gudnabrsam@gmail.com>
This commit is contained in:
Matt Benson
2025-02-07 11:55:49 -06:00
parent 846ed95c0e
commit dc8dd4bea7
19 changed files with 983 additions and 235 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-scaffolder-backend': minor
'@backstage/plugin-scaffolder-node': minor
---
New api functions createTemplateFilter, createTemplateGlobalFunction, createTemplateGlobalValue;
Core support for template extension documentation
@@ -328,6 +328,7 @@ preconfigured
prepack
Preprarer
productional
projectSlug
Protobuf
proxied
proxying
@@ -425,6 +426,7 @@ subheaders
subkey
subpath
subroutes
substring
subtree
superfences
Superfences
@@ -0,0 +1,502 @@
---
id: template-extensions
title: Template Extensions
description: Template extensions system
---
Backstage templating is powered by [Nunjucks][]. The basics:
# Template Filters
The [filter][] is a critical mechanism for the rendering of Nunjucks templates,
providing a means of transforming values in a familiar [piped][] fashion.
Template filters are functions that help you transform data, extract specific
information, and perform various operations in Scaffolder Templates.
## Built-in
Backstage provides out of the box the following set of "built-in" template
filters (to create your own custom filters, look to the section [Custom Filter](#custom-filter) hereafter):
### parseRepoUrl
The `parseRepoUrl` filter parses a repository URL into its constituent parts:
`owner`, repository name (`repo`), etc.
**Usage Example:**
```yaml
- id: log
name: Parse Repo URL
action: debug:log
input:
message: ${{ parameters.repoUrl | parseRepoUrl }}
```
- **Input**: `github.com?repo=backstage&owner=backstage`
- **Output**: "RepoSpec" (see [parseRepoUrl][])
### parseEntityRef
The `parseEntityRef` filter allows you to extract different parts of
an entity reference, such as the `kind`, `namespace`, and `name`.
**Usage example**
1. Without context
```yaml
- id: log
name: Parse Entity Reference
action: debug:log
input:
message: ${{ parameters.owner | parseEntityRef }}
```
- **Input**: `group:techdocs`
- **Output**: [CompoundEntityRef][]
1. With context
```yaml
- id: log
name: Parse Entity Reference
action: debug:log
input:
message: ${{ parameters.owner | parseEntityRef({ defaultKind:"group", defaultNamespace:"another-namespace" }) }}
```
- **Input**: `techdocs`
- **Output**: [CompoundEntityRef][]
### pick
The `pick` filter allows you to select a specific property (e.g. `kind`, `namespace`, `name`) from an object.
**Usage Example**
```yaml
- id: log
name: Pick
action: debug:log
input:
message: ${{ parameters.owner | parseEntityRef | pick('name') }}
```
- **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
@@ -631,7 +631,7 @@ output:
## The templating syntax
You might have noticed variables wrapped in `${{ }}` in the examples. These are
You might have noticed expressions wrapped in `${{ }}` in the examples. These are
template strings for linking and gluing the different parts of the template
together. All the form inputs from the `parameters` section will be available by
using this template syntax (for example, `${{ parameters.firstName }}` inserts
@@ -704,219 +704,16 @@ You can read more about all the `inputs` and `outputs` defined in the actions in
code part of the `JSONSchema`, or you can read more about our
[built in actions](./builtin-actions.md).
## Built in Filters
### More about expressions
Template filters are functions that help you transform data, extract specific information,
and perform various operations in Scaffolder Templates.
The `${{ }}` constructs in your template are evaluated using the
powerful [Nunjucks templating engine](https://mozilla.github.io/nunjucks/).
To learn more about basic Nunjucks templating please see
[templating documentation](https://mozilla.github.io/nunjucks/templating.html).
This section introduces the built-in filters provided by Backstage and offers examples of
how to use them in the Scaffolder templates. It's important to mention that Backstage also leverages the
native filters from the Nunjucks library. For a complete list of these native filters and their usage,
refer to the [Nunjucks documentation](https://mozilla.github.io/nunjucks/templating.html#builtin-filters).
To create your own custom filters, look to the section [Custom Filters and Globals](#custom-filters-and-globals) hereafter.
### parseRepoUrl
The `parseRepoUrl` filter parse a repository URL into
its components, such as `owner`, repository `name`, and more.
**Usage Example:**
```yaml
- id: log
name: Parse Repo URL
action: debug:log
input:
extra: ${{ parameters.repoUrl | parseRepoUrl }}
```
- **Input**: `github.com?repo=backstage&org=backstage`
- **Output**: [RepoSpec](https://github.com/backstage/backstage/blob/v1.17.2/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts#L39)
### parseEntityRef
The `parseEntityRef` filter allows you to extract different parts of
an entity reference, such as the `kind`, `namespace`, and `name`.
**Usage example**
1. Without context
```yaml
- id: log
name: Parse Entity Reference
action: debug:log
input:
extra: ${{ parameters.owner | parseEntityRef }}
```
- **Input**: `group:techdocs`
- **Output**: [CompoundEntityRef](https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/types.ts#L23)
2. With context
```yaml
- id: log
name: Parse Entity Reference
action: debug:log
input:
extra: ${{ parameters.owner | parseEntityRef({ defaultKind:"group", defaultNamespace:"another-namespace" }) }}
```
- **Input**: `techdocs`
- **Output**: [CompoundEntityRef](https://github.com/backstage/backstage/blob/v1.17.2/packages/catalog-model/src/types.ts#L23)
### pick
This `pick` filter allows you to select specific properties (`kind`, `namespace`, `name`) from an object.
**Usage Example**
```yaml
- id: log
name: Pick
action: debug:log
input:
extra: ${{ parameters.owner | parseEntityRef | pick('name') }}
```
- **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:
extra: ${{ parameters.repoUrl | projectSlug }}
```
- **Input**: `github.com?repo=backstage&org=backstage`
- **Output**: `backstage/backstage`
## Custom Filters and Globals
You may wish to extend the filters and globals with your own custom ones. For example `${{ myGlobal | myFilter | myOtherFilter }}` or `${{ myFunctionGlobal(1,2) | myFilter }}`.
This can be achieved using the `additionalTemplateFilters` and `additionalTemplateGlobals` properties respectively.
These properties accept a `Record`
```ts title="plugins/scaffolder-backend/src/service/router.ts"
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
```
where the first parameter is the identifier of the filter or global and the second is a `TemplateFilter` or a `TemplateGlobal` respectively.
A `TemplateFilter` is a function which will be called using the previous `JsonValue` objects and may return a `JsonValue` object.
A `TemplateGlobal` can either be a function which will be called using the passed `JsonValue` objects and may return a `JsonValue` object or it can be a `JsonValue` object itself.
```ts title="plugins/scaffolder-node/src/types.ts"
export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined;
export type TemplateGlobal =
| ((...args: JsonValue[]) => JsonValue | undefined)
| JsonValue;
```
**Usage Example**
Given you want to have the following filters and globals available in you template:
```yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: test
title: Test
spec:
owner: user:guest
type: service
steps:
- id: debug1
name: debug1
action: debug:log
input:
message: ${{ myGlobal | myFilter | myOtherFilter }}
- id: debug2
name: debug2
action: debug:log
input:
message: ${{ myFunctionGlobal(1,2) | myFilter }}
```
You will have to create a new [`BackendModule`](../../backend-system/architecture/06-modules.md) using the `scaffolderTemplatingExtensionPoint`.
Here is a very simplified example of how to do that:
```ts title="packages/backend-next/src/index.ts"
/* highlight-add-start */
import { scaffolderTemplatingExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
import { createBackendModule } from '@backstage/backend-plugin-api';
/* highlight-add-end */
/* highlight-add-start */
const scaffolderModuleCustomFilters = createBackendModule({
pluginId: 'scaffolder', // name of the plugin that the module is targeting
moduleId: 'custom-filters',
register(env) {
env.registerInit({
deps: {
scaffolder: scaffolderTemplatingExtensionPoint,
// ... and other dependencies as needed
},
async init({ scaffolder /* ..., other dependencies */ }) {
scaffolder.addTemplateGlobals({
myGlobal: () => 'myGlobal',
myFunctionGlobal: (...args: JsonValue[]) => args[0] + args[1],
});
scaffolder.addTemplateFilters({
myFilter: () => 'the value is this now',
myOtherFilter: (...args: JsonValue[]) => args.join(''),
});
},
});
},
});
/* highlight-add-end */
const backend = createBackend();
backend.add(import('@backstage/plugin-scaffolder-backend'));
/* highlight-add-next-line */
backend.add(scaffolderModuleCustomFilters);
```
If you still use the legacy backend system, then you will use the `createRouter()` function of the `Scaffolder plugin`
```ts title="packages/backend/src/plugins/scaffolder.ts"
export default async function createPlugin({
logger,
config,
}: PluginEnvironment): Promise<Router> {
...
return await createRouter({
logger,
config,
additionalTemplateFilters: {
<YOUR_FILTERS>
},
additionalTemplateGlobals: {
<YOUR_GLOBALS>
},
});
}
```
Note that additional template global functions are currently not supported in `fetch:template` (see #25445).
Information about Backstage's built-in Nunjucks extensions, as well as how to
create your own customizations, may be found at
[Template Extensions](./template-extensions.md).
## Template Editor
+1
View File
@@ -71,6 +71,7 @@ nav:
- Builtin Actions: 'features/software-templates/builtin-actions.md'
- Writing Custom Actions: 'features/software-templates/writing-custom-actions.md'
- Writing Custom Step Layouts: 'features/software-templates/writing-custom-step-layouts.md'
- Template Extensions: 'features/software-templates/template-extensions.md'
- Migrating from v1beta2 to v1beta3 templates: 'features/software-templates/migrating-from-v1beta2-to-v1beta3.md'
- Dry Run Testing: 'features/software-templates/dry-run-testing.md'
- Backstage Search:
+8 -2
View File
@@ -13,6 +13,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api';
import { BackstageCredentials } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { CreatedTemplateFilter } from '@backstage/plugin-scaffolder-node/alpha';
import { CreatedTemplateGlobal } from '@backstage/plugin-scaffolder-node/alpha';
import { createGithubActionsDispatchAction as createGithubActionsDispatchAction_2 } from '@backstage/plugin-scaffolder-backend-module-github';
import { createGithubDeployKeyAction as createGithubDeployKeyAction_2 } from '@backstage/plugin-scaffolder-backend-module-github';
import { createGithubEnvironmentAction as createGithubEnvironmentAction_2 } from '@backstage/plugin-scaffolder-backend-module-github';
@@ -549,9 +551,13 @@ export interface RouterOptions {
// (undocumented)
actions?: TemplateAction_2<any, any>[];
// (undocumented)
additionalTemplateFilters?: Record<string, TemplateFilter_2>;
additionalTemplateFilters?:
| Record<string, TemplateFilter_2>
| CreatedTemplateFilter[];
// (undocumented)
additionalTemplateGlobals?: Record<string, TemplateGlobal_2>;
additionalTemplateGlobals?:
| Record<string, TemplateGlobal_2>
| CreatedTemplateGlobal[];
// (undocumented)
additionalWorkspaceProviders?: Record<string, WorkspaceProvider>;
// (undocumented)
@@ -82,10 +82,27 @@ export const scaffolderPlugin = createBackendPlugin({
const additionalTemplateGlobals: Record<string, TemplateGlobal> = {};
env.registerExtensionPoint(scaffolderTemplatingExtensionPoint, {
addTemplateFilters(newFilters) {
Object.assign(additionalTemplateFilters, newFilters);
Object.assign(
additionalTemplateFilters,
Array.isArray(newFilters)
? Object.fromEntries(
newFilters.map(tf => [tf.id, tf.filter as TemplateFilter]),
)
: newFilters,
);
},
addTemplateGlobals(newGlobals) {
Object.assign(additionalTemplateGlobals, newGlobals);
Object.assign(
additionalTemplateGlobals,
Array.isArray(newGlobals)
? Object.fromEntries(
newGlobals.map(g => [
g.id,
('value' in g ? g.value : g.fn) as TemplateGlobal,
]),
)
: newGlobals,
);
},
});
@@ -81,6 +81,8 @@ import {
} from '@backstage/plugin-scaffolder-node';
import {
AutocompleteHandler,
CreatedTemplateFilter,
CreatedTemplateGlobal,
WorkspaceProvider,
} from '@backstage/plugin-scaffolder-node/alpha';
import { HumanDuration, JsonObject, JsonValue } from '@backstage/types';
@@ -173,8 +175,12 @@ export interface RouterOptions {
*/
concurrentTasksLimit?: number;
taskBroker?: TaskBroker;
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
additionalTemplateFilters?:
| Record<string, TemplateFilter>
| CreatedTemplateFilter[];
additionalTemplateGlobals?:
| Record<string, TemplateGlobal>
| CreatedTemplateGlobal[];
additionalWorkspaceProviders?: Record<string, WorkspaceProvider>;
permissions?: PermissionsService;
permissionRules?: Array<
@@ -363,6 +369,24 @@ export async function createRouter(
}
const actionRegistry = new TemplateActionRegistry();
const templateExtensions = {
additionalTemplateFilters: Array.isArray(additionalTemplateFilters)
? Object.fromEntries(
additionalTemplateFilters.map(f => [
f.id,
f.filter as TemplateFilter,
]),
)
: additionalTemplateFilters,
additionalTemplateGlobals: Array.isArray(additionalTemplateGlobals)
? Object.fromEntries(
additionalTemplateGlobals.map(g => [
g.id,
('value' in g ? g.value : g.fn) as TemplateGlobal,
]),
)
: additionalTemplateGlobals,
};
const workers: TaskWorker[] = [];
if (concurrentTasksLimit !== 0) {
@@ -378,11 +402,10 @@ export async function createRouter(
logger,
auditor,
workingDirectory,
additionalTemplateFilters,
additionalTemplateGlobals,
concurrentTasksLimit,
permissions,
gracefulShutdown,
...templateExtensions,
});
workers.push(worker);
}
@@ -395,9 +418,8 @@ export async function createRouter(
catalogClient,
reader,
config,
additionalTemplateFilters,
additionalTemplateGlobals,
auth,
...templateExtensions,
});
actionsToRegister.forEach(action => actionRegistry.register(action));
@@ -421,9 +443,8 @@ export async function createRouter(
logger,
auditor,
workingDirectory,
additionalTemplateFilters,
additionalTemplateGlobals,
permissions,
...templateExtensions,
});
const templateRules: TemplatePermissionRuleInput[] = Object.values(
+2 -2
View File
@@ -26,7 +26,7 @@
"license": "Apache-2.0",
"exports": {
".": "./src/index.ts",
"./alpha": "./src/alpha.ts",
"./alpha": "./src/alpha/index.ts",
"./package.json": "./package.json"
},
"main": "src/index.ts",
@@ -34,7 +34,7 @@
"typesVersions": {
"*": {
"alpha": [
"src/alpha.ts"
"src/alpha/index.ts"
],
"package.json": [
"package.json"
+140 -4
View File
@@ -6,10 +6,12 @@
/// <reference types="node" />
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { JsonValue } from '@backstage/types';
import { TaskBroker } from '@backstage/plugin-scaffolder-node';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { TemplateFilter } from '@backstage/plugin-scaffolder-node';
import { TemplateGlobal } from '@backstage/plugin-scaffolder-node';
import { TemplateFilter as TemplateFilter_2 } from '@backstage/plugin-scaffolder-node';
import { TemplateGlobal as TemplateGlobal_2 } from '@backstage/plugin-scaffolder-node';
import { z } from 'zod';
// @alpha
export type AutocompleteHandler = ({
@@ -27,6 +29,92 @@ export type AutocompleteHandler = ({
}[];
}>;
// @alpha (undocumented)
export type CreatedTemplateFilter<
TSchema extends
| TemplateFilterSchema<any, any>
| undefined
| unknown = unknown,
TFilterSchema extends TSchema extends TemplateFilterSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: TemplateFilter = TSchema extends TemplateFilterSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: TemplateFilter,
> = {
id: string;
description?: string;
examples?: TemplateFilterExample[];
schema?: TSchema;
filter: TFilterSchema;
};
// @alpha (undocumented)
export type CreatedTemplateGlobal =
| CreatedTemplateGlobalValue
| CreatedTemplateGlobalFunction<unknown, unknown>;
// @alpha (undocumented)
export type CreatedTemplateGlobalFunction<
TSchema extends
| TemplateGlobalFunctionSchema<any, any>
| undefined
| unknown = unknown,
TFilterSchema extends TSchema extends TemplateGlobalFunctionSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: Exclude<
TemplateGlobal,
JsonValue
> = TSchema extends TemplateGlobalFunctionSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: Exclude<TemplateGlobal, JsonValue>,
> = {
id: string;
description?: string;
examples?: TemplateGlobalFunctionExample[];
schema?: TSchema;
fn: TFilterSchema;
};
// @alpha (undocumented)
export type CreatedTemplateGlobalValue<T extends JsonValue = JsonValue> = {
id: string;
value: T;
description?: string;
};
// @alpha
export const createTemplateFilter: <
TSchema extends TemplateFilterSchema<any, any> | undefined,
TFunctionSchema extends TSchema extends TemplateFilterSchema<any, any>
? z.TypeOf<ReturnType<TSchema>>
: (arg: JsonValue, ...rest: JsonValue[]) => JsonValue | undefined,
>(
filter: CreatedTemplateFilter<TSchema, TFunctionSchema>,
) => CreatedTemplateFilter<unknown, unknown>;
// @alpha
export const createTemplateGlobalFunction: <
TSchema extends TemplateGlobalFunctionSchema<any, any> | undefined,
TFilterSchema extends TSchema extends TemplateGlobalFunctionSchema<any, any>
? z.TypeOf<ReturnType<TSchema>>
: (...args: JsonValue[]) => JsonValue | undefined,
>(
fn: CreatedTemplateGlobalFunction<TSchema, TFilterSchema>,
) => CreatedTemplateGlobalFunction<any, any>;
// @alpha
export const createTemplateGlobalValue: (
v: CreatedTemplateGlobalValue,
) => CreatedTemplateGlobalValue;
// @alpha
export const restoreWorkspace: (opts: {
path: string;
@@ -69,9 +157,13 @@ export const scaffolderTaskBrokerExtensionPoint: ExtensionPoint<ScaffolderTaskBr
// @alpha
export interface ScaffolderTemplatingExtensionPoint {
// (undocumented)
addTemplateFilters(filters: Record<string, TemplateFilter>): void;
addTemplateFilters(
filters: Record<string, TemplateFilter_2> | CreatedTemplateFilter[],
): void;
// (undocumented)
addTemplateGlobals(filters: Record<string, TemplateGlobal>): void;
addTemplateGlobals(
globals: Record<string, TemplateGlobal_2> | CreatedTemplateGlobal[],
): void;
}
// @alpha
@@ -91,6 +183,50 @@ export const serializeWorkspace: (opts: { path: string }) => Promise<{
contents: Buffer;
}>;
// @public (undocumented)
export type TemplateFilter = (
arg: JsonValue,
...rest: JsonValue[]
) => JsonValue | undefined;
// @alpha (undocumented)
export type TemplateFilterExample = {
description?: string;
example: string;
notes?: string;
};
// @alpha (undocumented)
export type TemplateFilterSchema<
Args extends z.ZodTuple<
| [z.ZodType<JsonValue>]
| [z.ZodType<JsonValue>, ...(z.ZodType<JsonValue> | z.ZodUnknown)[]],
z.ZodType<JsonValue> | z.ZodUnknown | null
>,
Result extends z.ZodType<JsonValue> | z.ZodUndefined,
> = (zod: typeof z) => z.ZodFunction<Args, Result>;
// @public (undocumented)
export type TemplateGlobal =
| ((...args: JsonValue[]) => JsonValue | undefined)
| JsonValue;
// @alpha (undocumented)
export type TemplateGlobalFunctionExample = {
description?: string;
example: string;
notes?: string;
};
// @alpha (undocumented)
export type TemplateGlobalFunctionSchema<
Args extends z.ZodTuple<
[] | [z.ZodType<JsonValue>, ...(z.ZodType<JsonValue> | z.ZodUnknown)[]],
z.ZodType<JsonValue> | z.ZodUnknown | null
>,
Result extends z.ZodType<JsonValue> | z.ZodUndefined,
> = (zod: typeof z) => z.ZodFunction<Args, Result>;
// @alpha
export interface WorkspaceProvider {
// (undocumented)
+4 -1
View File
@@ -482,7 +482,10 @@ export type TemplateExample = {
};
// @public (undocumented)
export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined;
export type TemplateFilter = (
arg: JsonValue,
...rest: JsonValue[]
) => JsonValue | undefined;
// @public (undocumented)
export type TemplateGlobal =
@@ -0,0 +1,32 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonValue } from '@backstage/types';
import { CreatedTemplateFilter, TemplateFilterSchema } from './types';
import { z } from 'zod';
/**
* This function is used to create new template filters in type-safe manner.
* @alpha
*/
export const createTemplateFilter = <
TSchema extends TemplateFilterSchema<any, any> | undefined,
TFunctionSchema extends TSchema extends TemplateFilterSchema<any, any>
? z.infer<ReturnType<TSchema>>
: (arg: JsonValue, ...rest: JsonValue[]) => JsonValue | undefined,
>(
filter: CreatedTemplateFilter<TSchema, TFunctionSchema>,
): CreatedTemplateFilter<unknown, unknown> => filter;
@@ -0,0 +1,17 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './types';
export * from './createTemplateFilter';
@@ -0,0 +1,60 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { z } from 'zod';
import { TemplateFilter } from '../../types';
import { JsonValue } from '@backstage/types';
export type { TemplateFilter } from '../../types';
/** @alpha */
export type TemplateFilterSchema<
Args extends z.ZodTuple<
| [z.ZodType<JsonValue>]
| [z.ZodType<JsonValue>, ...(z.ZodType<JsonValue> | z.ZodUnknown)[]],
z.ZodType<JsonValue> | z.ZodUnknown | null
>,
Result extends z.ZodType<JsonValue> | z.ZodUndefined,
> = (zod: typeof z) => z.ZodFunction<Args, Result>;
/** @alpha */
export type TemplateFilterExample = {
description?: string;
example: string;
notes?: string;
};
/** @alpha */
export type CreatedTemplateFilter<
TSchema extends
| TemplateFilterSchema<any, any>
| undefined
| unknown = unknown,
TFilterSchema extends TSchema extends TemplateFilterSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: TemplateFilter = TSchema extends TemplateFilterSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: TemplateFilter,
> = {
id: string;
description?: string;
examples?: TemplateFilterExample[];
schema?: TSchema;
filter: TFilterSchema;
};
@@ -0,0 +1,48 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { z } from 'zod';
import {
CreatedTemplateGlobalFunction,
CreatedTemplateGlobalValue,
TemplateGlobalFunctionSchema,
} from './types';
import { JsonValue } from '@backstage/types';
/**
* This function is used to create new template global values in type-safe manner.
* @param t - CreatedTemplateGlobalValue | CreatedTemplateGlobalFunction
* @returns t
* @alpha
*/
export const createTemplateGlobalValue = (
v: CreatedTemplateGlobalValue,
): CreatedTemplateGlobalValue => v;
/**
* This function is used to create new template global functions in type-safe manner.
* @param fn - CreatedTemplateGlobalFunction
* @returns fn
* @alpha
*/
export const createTemplateGlobalFunction = <
TSchema extends TemplateGlobalFunctionSchema<any, any> | undefined,
TFilterSchema extends TSchema extends TemplateGlobalFunctionSchema<any, any>
? z.infer<ReturnType<TSchema>>
: (...args: JsonValue[]) => JsonValue | undefined,
>(
fn: CreatedTemplateGlobalFunction<TSchema, TFilterSchema>,
): CreatedTemplateGlobalFunction<any, any> => fn;
@@ -0,0 +1,17 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './types';
export * from './createTemplateGlobal';
@@ -0,0 +1,74 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonValue } from '@backstage/types';
import { z } from 'zod';
import { TemplateGlobal } from '../../types';
export type { TemplateGlobal } from '../../types';
/** @alpha */
export type CreatedTemplateGlobalValue<T extends JsonValue = JsonValue> = {
id: string;
value: T;
description?: string;
};
/** @alpha */
export type TemplateGlobalFunctionSchema<
Args extends z.ZodTuple<
[] | [z.ZodType<JsonValue>, ...(z.ZodType<JsonValue> | z.ZodUnknown)[]],
z.ZodType<JsonValue> | z.ZodUnknown | null
>,
Result extends z.ZodType<JsonValue> | z.ZodUndefined,
> = (zod: typeof z) => z.ZodFunction<Args, Result>;
/** @alpha */
export type TemplateGlobalFunctionExample = {
description?: string;
example: string;
notes?: string;
};
/** @alpha */
export type CreatedTemplateGlobalFunction<
TSchema extends
| TemplateGlobalFunctionSchema<any, any>
| undefined
| unknown = unknown,
TFilterSchema extends TSchema extends TemplateGlobalFunctionSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: Exclude<
TemplateGlobal,
JsonValue
> = TSchema extends TemplateGlobalFunctionSchema<any, any>
? z.infer<ReturnType<TSchema>>
: TSchema extends unknown
? unknown
: Exclude<TemplateGlobal, JsonValue>,
> = {
id: string;
description?: string;
examples?: TemplateGlobalFunctionExample[];
schema?: TSchema;
fn: TFilterSchema;
};
/** @alpha */
export type CreatedTemplateGlobal =
| CreatedTemplateGlobalValue
| CreatedTemplateGlobalFunction<unknown, unknown>;
@@ -21,8 +21,12 @@ import {
TemplateFilter,
TemplateGlobal,
} from '@backstage/plugin-scaffolder-node';
import { CreatedTemplateFilter } from './filters';
import { CreatedTemplateGlobal } from './globals';
export * from './tasks/alpha';
export * from '../tasks/alpha';
export * from './filters';
export * from './globals';
/**
* Extension point for managing scaffolder actions.
@@ -68,9 +72,13 @@ export const scaffolderTaskBrokerExtensionPoint =
* @alpha
*/
export interface ScaffolderTemplatingExtensionPoint {
addTemplateFilters(filters: Record<string, TemplateFilter>): void;
addTemplateFilters(
filters: Record<string, TemplateFilter> | CreatedTemplateFilter[],
): void;
addTemplateGlobals(filters: Record<string, TemplateGlobal>): void;
addTemplateGlobals(
globals: Record<string, TemplateGlobal> | CreatedTemplateGlobal[],
): void;
}
/**
+1 -1
View File
@@ -23,4 +23,4 @@
export * from './actions';
export * from './tasks';
export * from './files';
export type { TemplateFilter, TemplateGlobal } from './types';
export * from './types';