From e7c6c32f264b68e4af9711a711fe3072b4658a73 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Sat, 14 Mar 2026 13:48:07 +0100 Subject: [PATCH 01/55] Refactor Slack Notifications Backend Module to use MetricsApi Signed-off-by: Gabriel Dugny --- .changeset/solid-bats-flash.md | 5 ++++ .../package.json | 1 - .../lib/SlackNotificationProcessor.test.ts | 29 +++++++++++++++++++ .../src/lib/SlackNotificationProcessor.ts | 17 +++++++---- .../src/module.ts | 5 +++- yarn.lock | 1 - 6 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 .changeset/solid-bats-flash.md diff --git a/.changeset/solid-bats-flash.md b/.changeset/solid-bats-flash.md new file mode 100644 index 00000000000000..c14603ec503b03 --- /dev/null +++ b/.changeset/solid-bats-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-slack': patch +--- + +The Slack notification processor now uses the `MetricsService` to create metrics, providing plugin-scoped attribution. diff --git a/plugins/notifications-backend-module-slack/package.json b/plugins/notifications-backend-module-slack/package.json index 7cc7b280caec20..55759c0428bd18 100644 --- a/plugins/notifications-backend-module-slack/package.json +++ b/plugins/notifications-backend-module-slack/package.json @@ -42,7 +42,6 @@ "@backstage/plugin-notifications-common": "workspace:^", "@backstage/plugin-notifications-node": "workspace:^", "@backstage/types": "workspace:^", - "@opentelemetry/api": "^1.9.0", "@slack/bolt": "^3.21.4", "@slack/types": "^2.14.0", "@slack/web-api": "^7.5.0", diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts index ce7898566be917..0393070b3f7ea5 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts @@ -15,6 +15,7 @@ */ import { mockServices } from '@backstage/backend-test-utils'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; import { SlackNotificationProcessor } from './SlackNotificationProcessor'; import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; import { KnownBlock, WebClient } from '@slack/web-api'; @@ -125,6 +126,7 @@ const DEFAULT_ENTITIES_RESPONSE = { describe('SlackNotificationProcessor', () => { const logger = mockServices.logger.mock(); const auth = mockServices.auth(); + const metrics = metricsServiceMock.mock(); const config = mockServices.rootConfig({ data: { app: { @@ -157,6 +159,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -224,6 +227,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, blockKitRenderer: () => customBlocks, })[0]; @@ -256,6 +260,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -331,6 +336,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -365,6 +371,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -410,6 +417,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -465,6 +473,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -529,6 +538,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -584,6 +594,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -639,6 +650,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -694,6 +706,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -750,6 +763,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -809,6 +823,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -863,6 +878,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -921,6 +937,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -959,6 +976,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -982,6 +1000,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: [DEFAULT_ENTITIES_RESPONSE.items[2]], }), + metrics, slack, })[0]; @@ -1021,6 +1040,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1066,6 +1086,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1125,6 +1146,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1204,6 +1226,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1298,6 +1321,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, }, )[0]; @@ -1375,6 +1399,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1441,6 +1466,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1481,6 +1507,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1520,6 +1547,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, })[0]; @@ -1568,6 +1596,7 @@ describe('SlackNotificationProcessor', () => { catalog: catalogServiceMock({ entities: DEFAULT_ENTITIES_RESPONSE.items, }), + metrics, slack, }, )[0]; diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index de084df4648d0a..d20587f2a19811 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -15,6 +15,10 @@ */ import { AuthService, LoggerService } from '@backstage/backend-plugin-api'; +import { + MetricsService, + MetricsServiceCounter, +} from '@backstage/backend-plugin-api/alpha'; import { Entity, isUserEntity, @@ -30,7 +34,6 @@ import { NotificationSendOptions, } from '@backstage/plugin-notifications-node'; import { durationToMilliseconds } from '@backstage/types'; -import { Counter, metrics } from '@opentelemetry/api'; import { ChatPostMessageArguments, WebClient } from '@slack/web-api'; import DataLoader from 'dataloader'; import pThrottle from 'p-throttle'; @@ -48,8 +51,8 @@ export class SlackNotificationProcessor implements NotificationProcessor { private readonly sendNotifications: ( opts: ChatPostMessageArguments[], ) => Promise; - private readonly messagesSent: Counter; - private readonly messagesFailed: Counter; + private readonly messagesSent: MetricsServiceCounter; + private readonly messagesFailed: MetricsServiceCounter; private readonly broadcastChannels?: string[]; private readonly broadcastRoutes?: BroadcastRoute[]; private readonly entityLoader: DataLoader; @@ -64,6 +67,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { auth: AuthService; logger: LoggerService; catalog: CatalogService; + metrics: MetricsService; slack?: WebClient; broadcastChannels?: string[]; blockKitRenderer?: SlackBlockKitRenderer; @@ -103,6 +107,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { auth: AuthService; logger: LoggerService; catalog: CatalogService; + metrics: MetricsService; broadcastChannels?: string[]; broadcastRoutes?: BroadcastRoute[]; username?: string; @@ -114,6 +119,7 @@ export class SlackNotificationProcessor implements NotificationProcessor { auth, catalog, logger, + metrics, slack, broadcastChannels, broadcastRoutes, @@ -159,14 +165,13 @@ export class SlackNotificationProcessor implements NotificationProcessor { }, ); - const meter = metrics.getMeter('default'); - this.messagesSent = meter.createCounter( + this.messagesSent = metrics.createCounter( 'notifications.processors.slack.sent.count', { description: 'Number of messages sent to Slack successfully', }, ); - this.messagesFailed = meter.createCounter( + this.messagesFailed = metrics.createCounter( 'notifications.processors.slack.error.count', { description: 'Number of messages that failed to send to Slack', diff --git a/plugins/notifications-backend-module-slack/src/module.ts b/plugins/notifications-backend-module-slack/src/module.ts index e3d6aebee522f9..88f0272eb3ba1d 100644 --- a/plugins/notifications-backend-module-slack/src/module.ts +++ b/plugins/notifications-backend-module-slack/src/module.ts @@ -17,6 +17,7 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; +import { metricsServiceRef } from '@backstage/backend-plugin-api/alpha'; import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node'; import { SlackNotificationProcessor } from './lib/SlackNotificationProcessor'; import { catalogServiceRef } from '@backstage/plugin-catalog-node'; @@ -52,13 +53,15 @@ export const notificationsModuleSlack = createBackendModule({ logger: coreServices.logger, catalog: catalogServiceRef, notifications: notificationsProcessingExtensionPoint, + metrics: metricsServiceRef, }, - async init({ auth, config, logger, catalog, notifications }) { + async init({ auth, config, logger, catalog, notifications, metrics }) { notifications.addProcessor( SlackNotificationProcessor.fromConfig(config, { auth, logger, catalog, + metrics, blockKitRenderer, }), ); diff --git a/yarn.lock b/yarn.lock index 32b09cc5e37f66..03d6e900de52a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5910,7 +5910,6 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@faker-js/faker": "npm:^10.0.0" - "@opentelemetry/api": "npm:^1.9.0" "@slack/bolt": "npm:^3.21.4" "@slack/types": "npm:^2.14.0" "@slack/web-api": "npm:^7.5.0" From 25e672c581f8369e78fc58d7e60f56784c50e9b5 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Sat, 14 Mar 2026 14:19:11 +0100 Subject: [PATCH 02/55] Add metric unit Signed-off-by: Gabriel Dugny --- .changeset/solid-bats-flash.md | 2 +- .../src/lib/SlackNotificationProcessor.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/solid-bats-flash.md b/.changeset/solid-bats-flash.md index c14603ec503b03..0a4918fcc2d3a6 100644 --- a/.changeset/solid-bats-flash.md +++ b/.changeset/solid-bats-flash.md @@ -2,4 +2,4 @@ '@backstage/plugin-notifications-backend-module-slack': patch --- -The Slack notification processor now uses the `MetricsService` to create metrics, providing plugin-scoped attribution. +The Slack notification processor now uses the `MetricsService` to create metrics, providing plugin-scoped attribution. `{message}` unit has also been added. diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index d20587f2a19811..28846fcbb552cb 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -169,12 +169,14 @@ export class SlackNotificationProcessor implements NotificationProcessor { 'notifications.processors.slack.sent.count', { description: 'Number of messages sent to Slack successfully', + unit: '{message}', }, ); this.messagesFailed = metrics.createCounter( 'notifications.processors.slack.error.count', { description: 'Number of messages that failed to send to Slack', + unit: '{message}', }, ); From b35de8d2aaf47ca8c17b5c5491ec86f923f5da1a Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 28 Feb 2026 11:32:46 -0600 Subject: [PATCH 03/55] [Docs] Scaffolder Clean Up Signed-off-by: Andre Wanlin --- .../software-templates/builtin-actions.md | 54 ---- docs/features/software-templates/index.md | 11 - .../migrating-from-v1beta2-to-v1beta3.md | 241 ------------------ .../migrating-to-rjsf-v5.md | 138 ---------- .../writing-custom-actions.md | 9 - microsite/sidebars.ts | 2 - mkdocs.yml | 1 - 7 files changed, 456 deletions(-) delete mode 100644 docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md delete mode 100644 docs/features/software-templates/migrating-to-rjsf-v5.md diff --git a/docs/features/software-templates/builtin-actions.md b/docs/features/software-templates/builtin-actions.md index 378c15e18ff3ae..d74123704e54d6 100644 --- a/docs/features/software-templates/builtin-actions.md +++ b/docs/features/software-templates/builtin-actions.md @@ -59,57 +59,3 @@ backend.start(); A list of all registered actions can be found under `/create/actions`. For local development you should be able to reach them at `http://localhost:3000/create/actions`. - -## Migrating from `fetch:cookiecutter` to `fetch:template` - -The `fetch:template` action is a new action with a similar API to -`fetch:cookiecutter` but no dependency on `cookiecutter`. There are two options -for migrating templates that use `fetch:cookiecutter` to use `fetch:template`: - -### Using `cookiecutterCompat` mode - -The new `fetch:template` action has a `cookiecutterCompat` flag which should -allow most templates built for `fetch:cookiecutter` to work without any changes. - -1. Update action name in `template.yaml`. The name should be changed from - `fetch:cookiecutter` to `fetch:template`. -2. Set `cookiecutterCompat` to `true` in the `fetch:template` step input in - `template.yaml`. - -```yaml title="template.yaml" -steps: - - id: fetchBase - name: Fetch Base - # highlight-remove-next-line - action: fetch:cookiecutter - # highlight-add-next-line - action: fetch:template - input: - url: ./skeleton - # highlight-add-next-line - cookiecutterCompat: true - values: -``` - -### Manual migration - -If you prefer, you can manually migrate your templates to avoid the need for -enabling cookiecutter compatibility mode, which will result in slightly less -verbose template variables expressions. - -1. Update action name in `template.yaml`. The name should be changed from - `fetch:cookiecutter` to `fetch:template`. -2. Update variable syntax in file names and content. `fetch:cookiecutter` - expects variables to be enclosed in `{{` `}}` and prefixed with - `cookiecutter.`, while `fetch:template` expects variables to be enclosed in - `${{` `}}` and prefixed with `values.`. For example, a reference to variable - `myInputVariable` would need to be migrated from - `{{ cookiecutter.myInputVariable }}` to `${{ values.myInputVariable }}`. -3. Replace uses of `jsonify` with `dump`. The - [`jsonify` filter](https://cookiecutter.readthedocs.io/en/latest/advanced/template_extensions.html#jsonify-extension) - is built in to `cookiecutter`, and is not available by default when using - `fetch:template`. The - [`dump` filter](https://mozilla.github.io/nunjucks/templating.html#dump) is - the equivalent filter in nunjucks, so an expression like - `{{ cookiecutter.myAwesomeList | jsonify }}` should be migrated to - `${{ values.myAwesomeList | dump }}`. diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index 58da85c6eb0275..511f2385627db9 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -22,17 +22,6 @@ See the [Writing Custom Actions guide](./writing-custom-actions.md#naming-conven ## Prerequisites -:::note Note - -If you're running Backstage with Node 20 or later, you'll need to pass the flag `--no-node-snapshot` to Node in order to -use the templates feature. -One way to do this is to specify the `NODE_OPTIONS` environment variable before starting Backstage: -`export NODE_OPTIONS="${NODE_OPTIONS:-} --no-node-snapshot"` - -It's important to append to the existing `NODE_OPTIONS` value, if it's already set, rather than overwriting it, since some NodeJS Debugging tools may rely on this environment variable to work properly. - -::: - These docs assume you have already gone over the [Backstage Getting Started](../../getting-started) section and you are able to run Backstage locally or it has been deployed somewhere. ## Getting Started diff --git a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md deleted file mode 100644 index 91fc34f97840f5..00000000000000 --- a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -id: migrating-from-v1beta2-to-v1beta3 -title: Migrating to v1beta3 templates -description: How to migrate your existing templates to beta3 syntax ---- - -# What's new? - -Well then, here we are! 🚀 - -Backstage has had many forms of templating languages throughout different -plugins and different systems. We've had `cookiecutter` syntax in templates, and -we also had `handlebars` templating in the `kind: Template`. Then we wanted to -remove the additional dependency on `cookiecutter` for Software Templates out of -the box, so we introduced `nunjucks` as an alternative in `fetch:template` -action which is based on the `jinja2` syntax so they're pretty similar. In an -effort to reduce confusion and unify on to one templating language, we're -officially deprecating support for `handlebars` templating in the -`kind: Template` entities with `apiVersion` `scaffolder.backstage.io/v1beta3` -and moving to using `nunjucks` instead. - -This provides us a lot of built in `filters` (`handlebars` helpers), that as -Template authors will give you much more flexibility out of the box, and also -open up sharing of filters in the Entity and the actual `skeleton` too, and -removing the slight differences between the two languages. - -We've also removed a lot of the built in helpers that we shipped with -`handlebars`, as they're now supported as first class citizens by either -`nunjucks` or the new `scaffolder` when using `scaffolder.backstage.io/v1beta3` -`apiVersion` - -The migration path is pretty simple, and we've removed some of the pain points -from writing the `handlebars` templates too. Let's go through what's new and how -to upgrade. - -## Add the Processor to the `plugin-catalog-backend` - -An important change is to add the required processor to your `packages/backend/src/plugins/catalog.ts` - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /* highlight-add-next-line */ - builder.addProcessor(new ScaffolderEntitiesProcessor()); - const { processingEngine, router } = await builder.build(); - - // .. -} -``` - -## `backstage.io/v1beta2` -> `scaffolder.backstage.io/v1beta3` - -The most important change is that you'll need to switch over the `apiVersion` in -your templates to the new one. - -```yaml - kind: Template - # highlight-remove-next-line - apiVersion: backstage.io/v1beta2 - # highlight-add-next-line - apiVersion: scaffolder.backstage.io/v1beta3 -``` - -## `${{ }}` instead of `"{{ }}"` - -One really big readability issue and cause for confusion was the fact that with -`handlebars` and `yaml` you always had to wrap your templating strings in quotes -in `yaml` so that it didn't try to parse it as a `json` object and fail. This -was pretty annoying, as it also meant that all things look like strings. Now -that's no longer the case, you can now remove the `""` and take advantage of -writing nice `yaml` files that just work. - -```yaml -spec: - steps: - input: - # highlight-remove-next-line - description: 'This is {{ parameters.name }}' - # highlight-add-next-line - description: This is ${{ parameters.name }} - # highlight-remove-next-line - repoUrl: '{{ parameters.repoUrl }}' - # highlight-add-next-line - repoUrl: ${{ parameters.repoUrl }} -``` - -## No more `eq` or `not` helpers - -These helpers are no longer needed with the more expressive `api` that -`nunjucks` provides. You can simply use the built-in `nunjucks` and `jinja2` -style operators. - -```yaml -spec: - steps: - input: - # highlight-remove-next-line - if: '{{ eq parameters.value "backstage" }}' - # highlight-add-next-line - if: ${{ parameters.value === "backstage" }} -``` - -And then for the `not` - -```yaml -spec: - steps: - input: - # highlight-remove-next-line - if: '{{ not parameters.value "backstage" }}' - # highlight-add-next-line - if: ${{ parameters.value !== "backstage" }} -``` - -Much better right? ✨ - -## No more `json` helper - -This helper is no longer needed, as we've added support for complex values and -supporting the additional primitive values now rather than everything being a -`string`. This means that now that you can pass around `parameters` and it -should all work as expected and keep the type that has been declared in the -input schema. - -```yaml -spec: - parameters: - test: - type: number - name: Test Number - address: - type: object - required: - - line1 - properties: - line1: - type: string - name: Line 1 - line2: - type: string - name: Line 2 - - steps: - - id: test step - action: run:something - input: - # highlight-remove-next-line - address: '{{ json parameters.address }}' - # highlight-add-next-line - address: ${{ parameters.address }} - # highlight-remove-next-line - test: '{{ parameters.test }}' - # highlight-add-next-line - test: ${{ parameters.test }} # this will now make sure that the type of test is a number 🙏 -``` - -## `parseRepoUrl` is now a `filter` - -All calls to `parseRepoUrl` are now a `jinja2` `filter`, which means you'll need -to update the syntax. - -```yaml -spec: - steps: - input: - # highlight-remove-next-line - repoUrl: '{{ parseRepoUrl parameters.repoUrl }}' - # highlight-add-next-line - repoUrl: ${{ parameters.repoUrl | parseRepoUrl }} -``` - -Now we have complex value support here too, expect that this `filter` will go -away in future versions and the `RepoUrlPicker` will return an object so -`parameters.repoUrl` will already be a -`{ host: string; owner: string; repo: string }` 🚀 - -## Links should be used instead of named outputs - -Previously, it was possible to provide links to the frontend using the named output `entityRef` and `remoteUrl`. -These should be moved to `links` under the `output` object instead. - -```yaml -output: - # highlight-remove-start - remoteUrl: {{ steps['publish'].output.remoteUrl }} - entityRef: {{ steps['register'].output.entityRef }} - # highlight-remove-end - # highlight-add-start - links: - - title: Repository - url: ${{ steps['publish'].output.remoteUrl }} - - title: Open in catalog - icon: catalog - entityRef: ${{ steps['register'].output.entityRef }} - # highlight-add-end -``` - -## Watch out for `dash-case` - -The nunjucks compiler can run into issues if the `id` fields in your template steps use dash characters, since these IDs translate directly to JavaScript object properties when accessed as output. One possible migration path is to use `camelCase` for your action IDs. - -```yaml - steps: - # highlight-remove-start - id: my-custom-action - ... - - id: publish-pull-request - input: - repoUrl: {{ steps.my-custom-action.output.repoUrl }} # Will not recognize 'my-custom-action' as a JS property since it contains dashes! - # highlight-remove-end - - steps: - # highlight-add-start - id: myCustomAction - ... - - id: publishPullRequest - input: - repoUrl: ${{ steps.myCustomAction.output.repoUrl }} - # highlight-add-end -``` - -Alternatively, it's possible to keep the `dash-case` syntax and use brackets for property access as you would in JavaScript: - -```yaml -input: - repoUrl: ${{ steps['my-custom-action'].output.repoUrl }} -``` - -### Summary - -Of course, we're always available on [discord](https://discord.gg/backstage-687207715902193673) if -you're stuck or something's not working as expected. You can also -[raise an issue](https://github.com/backstage/backstage/issues/new/choose) with -feedback or bugs! diff --git a/docs/features/software-templates/migrating-to-rjsf-v5.md b/docs/features/software-templates/migrating-to-rjsf-v5.md deleted file mode 100644 index d35eb166f0a8fd..00000000000000 --- a/docs/features/software-templates/migrating-to-rjsf-v5.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -id: migrating-to-rjsf-v5 -title: 'Migrating to react-jsonschema-form@v5' -description: Docs on migrating to `react-jsonschema-form`@v5 and the new designs ---- - -:::note Note - -If you were previously using the `/alpha` imports to test out the `scaffolder/next` work, those imports have been promoted to the default exports from the respective packages. You should just have to remove the `/alpha` from the import path, and remove the `Next` from the import name. `NextScaffolderPage` -> `ScaffolderPage`, `createNextScaffolderFieldExtension` -> `createScaffolderFieldExtension` etc. - -::: - -## What's `react-jsonschema-form`? - -This library is core to the frontend part of the scaffolder plugin, and is responsible for rendering the form in which developers and end users fill out to meet the `jsonschema` requirement for the parameters section. - -Since the initial release of the `scaffolder` plugin, we we're on a pretty old version of `react-jsonschema-form` (v3), which has been pretty outdated as of late. The problem with us just bumping this library was that there are several breaking changes with the new v5 version, which we've tried pretty aggressively not to pass on to our end users for their templates and [Custom Field Extensions](https://backstage.io/docs/features/software-templates/writing-custom-field-extensions/). - -We're hoping that by duplicating the types from version 3 of `react-jsonschema-form` and making these the types that we will support even though the underlying library is v5, it should get us through all of the breaking changes without passing that down. - -## What's new? - -With that in mind, this release has `v5` of `react-jsonschema-form`, and with that comes all the new features and bugfixes in `v4` that we were waiting for - one of the main ones being the ability to use `if / then / else` syntax in the `template.yaml` definitions! 🎉 - -We've also rebuilt how validation works in the `scaffolder` components, which now means that we've opened the ability to have `async` validation functions in your `Field Extensions`. - -Some of the pages have gotten a little bit of an overhaul in terms of UI based on some research and feedback from the community and internally. - -- The `TemplateList` page has gotten some new `Card` components which show a little more information than the previous version with a little `material-ui` standards. -- The `WizardPage` has received some new updates with the stepper now running horizontally, and the `Review` step being a dedicated step in the stepper. -- The `OngoingTask` page now does not show the logs by default, and instead has a much cleaner interface for tracking the ongoing steps and the pipeline of actions that are currently showing. - - You can also now provide your own `OutputsComponent` which can be used to render the outputs from an ongoing / completed task in a way that suits your templates the best. For instance, if your template produces `Pull Requests`, it could be useful to render these in an interactive way where you can see the statuses of each of these `Pull Requests` in the `Ongoing Task` page. - -There's also a lot of bug fixes, and other things, but these are the main ones that we wanted to highlight. - -## How do I upgrade - -With the release of [`v1.20.0`](https://github.com/backstage/backstage/releases/tag/v1.20.0) these changes should have been made for you. We're hoping that it should be pretty transparent, and things just work as expected. Please reach out to us on [discord](https://discord.com/invite/MUpMjP2) or in a [issue](https://github.com/backstage/backstage/issues/new?assignees=&labels=bug&projects=&template=bug.yaml&title=%F0%9F%90%9B+Bug+Report%3A+%3Ctitle%3E) if you're having issues. - -It's possible that if you have a hard dependency on any of the `@rjsf/*` libraries in your app, you'll need to bump these manually to the version that we currently support: `5.13.6` at the time of writing. There could be breaking changes that you will have to fix here however, which we think should be pretty simple, but they're things like changing imports from `@rjsf/core` to `@rjsf/utils`. - -```ts -/* highlight-remove-next-line */ -import { FieldValidation } from '@rjsf/core'; -/* highlight-add-next-line */ -import { FieldValidation } from '@rjsf/utils'; -``` - -## Escape hatch - -If for some reason the upgrade to [`v1.20.0`](https://github.com/backstage/backstage/releases/tag/v1.20.0) didn't go as planned, there's an escape hatch for use until the next mainline release in which we will try to get any issues fixed before removing the legacy code. - -We've moved some of the older exports to an `/alpha` export so you should be able switch to using the old library just in case. - -```tsx -/* highlight-remove-next-line */ -import { ScaffolderPage } from '@backstage/plugin-scaffolder'; -/* highlight-add-next-line */ -import { LegacyScaffolderPage } from '@backstage/plugin-scaffolder/alpha'; -``` - -And this API should be the exact same as the previous Router, so you should be able to make a change like the following further down in this file: - -```tsx - - entity?.metadata?.tags?.includes('recommended') ?? false, - }, - ]} - /> - } -> - - - {/* ... other extensions */} - - - - {/* ... other layouts */} - - -``` - -And you can also update any of your `CustomFieldExtensions` to use the old helper like so: - -```ts -/* highlight-remove-next-line */ -import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder'; -/* highlight-add-next-line */ -import { createLegacyScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react/alpha'; - -export const EntityNamePickerFieldExtension = scaffolderPlugin.provide( - /* highlight-remove-next-line */ - createScaffolderFieldExtension({ - /* highlight-add-next-line */ - createLegacyScaffolderFieldExtension({ - component: EntityNamePicker, - name: 'EntityNamePicker', - validation: entityNamePickerValidation, - }), -); -``` - -And in the component themselves, you might have to do the following: - -```tsx -/* highlight-remove-next-line */ -import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react'; -/* highlight-add-next-line */ -import { LegacyFieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react/alpha'; - -export const EntityNamePicker = ( - /* highlight-remove-next-line */ - props: FieldExtensionComponentProps, - /* highlight-add-next-line */ - props: LegacyFieldExtensionComponentProps, -) => { - const { - onChange, - required, - schema: { title = 'Name', description = 'Unique name of the component' }, - rawErrors, - formData, - idSchema, - placeholder, - } = props; - // .. -}; -``` diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index f5ed9e48547086..c4a12898a06dee 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -8,15 +8,6 @@ If you want to extend the functionality of the Scaffolder, you can do so by writing custom actions which can be used alongside our [built-in actions](./builtin-actions.md). -:::note Note - -When adding custom actions, the actions array will **replace the -built-in actions too**. Meaning, you will no longer be able to use them. -If you want to continue using the builtin actions, include them in the `actions` -array when registering your custom actions, as seen below. - -::: - ## Streamlining Custom Action Creation with Backstage CLI The creation of custom actions in Backstage has never been easier thanks to the Backstage CLI. This tool streamlines the diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 9e56faac211105..31161d527800ea 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -302,8 +302,6 @@ export default { 'features/software-templates/writing-custom-field-extensions', 'features/software-templates/writing-custom-step-layouts', 'features/software-templates/authorizing-scaffolder-template-details', - 'features/software-templates/migrating-to-rjsf-v5', - 'features/software-templates/migrating-from-v1beta2-to-v1beta3', 'features/software-templates/dry-run-testing', 'features/software-templates/experimental', 'features/software-templates/templating-extensions', diff --git a/mkdocs.yml b/mkdocs.yml index 618f77a59f8a03..cbf8b91477ea05 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,7 +80,6 @@ nav: - Writing Custom Actions: 'features/software-templates/writing-custom-actions.md' - Writing Custom Step Layouts: 'features/software-templates/writing-custom-step-layouts.md' - Templating Extensions: 'features/software-templates/templating-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: - Overview: 'features/search/README.md' From ce8198bc156c82a1806e571c4559ac49dad05871 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 1 Mar 2026 09:32:55 -0600 Subject: [PATCH 04/55] Fixed link Signed-off-by: Andre Wanlin --- docs/features/software-templates/index.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index 511f2385627db9..1b36fcb2dd9064 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -14,9 +14,7 @@ locations like GitHub or GitLab. When creating custom scaffolder actions, **use camelCase for action IDs** instead of kebab-case. Action IDs with dashes (like `fetch-component-id`) will cause template expressions like `${{ steps.fetch-component-id.output.componentId }}` to return `NaN` because the dashes are evaluated as subtraction operators in JavaScript expressions. -:::note - -See the [Writing Custom Actions guide](./writing-custom-actions.md#naming-conventions) and [Template Migration guide](./migrating-from-v1beta2-to-v1beta3.md#watch-out-for-dash-case) for more details. +See the [Writing Custom Actions guide](./writing-custom-actions.md#naming-conventions) for more details. ::: From 765725c8c21a528d7c91a1a7a0e55b99aeae246f Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Mon, 2 Mar 2026 09:44:32 -0600 Subject: [PATCH 05/55] Fixed CI and addressed feedback Signed-off-by: Andre Wanlin --- .../software-templates/configuration.md | 23 ------------------- microsite/docusaurus.config.ts | 6 ++++- mkdocs.yml | 1 + 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md index bbf35557cec3ea..c266fa1217118a 100644 --- a/docs/features/software-templates/configuration.md +++ b/docs/features/software-templates/configuration.md @@ -107,29 +107,6 @@ Default secrets are resolved from environment variables and accessible via `${{ **Security Note:** Secrets are automatically masked in logs and are only available to backend actions, never exposed to the frontend. -## Disabling Docker in Docker situation (Optional) - -Software templates use the `fetch:template` action by default, which requires no -external dependencies and offers a -[Cookiecutter-compatible mode](https://backstage.io/docs/features/software-templates/builtin-actions#using-cookiecuttercompat-mode). -There is also a `fetch:cookiecutter` action, which uses -[Cookiecutter](https://github.com/cookiecutter/cookiecutter) directly for -templating. By default, the `fetch:cookiecutter` action will use the -[scaffolder-backend/Cookiecutter](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile) -docker image. - -If you are running Backstage from a Docker container and you want to avoid -calling a container inside a container, you can set up Cookiecutter in your own -image, this will use the local installation instead. - -You can do so by including the following lines in the last step of your -`Dockerfile`: - -```Dockerfile -RUN apt-get update && apt-get install -y python3 python3-pip -RUN pip3 install cookiecutter -``` - ## Customizing the ScaffolderPage with Grouping and Filtering Once you have more than a few software templates you may want to customize your diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index 718a337b014b19..5b69e262374f51 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -237,7 +237,7 @@ const config: Config = { }, { from: '/docs/features/software-templates/testing-scaffolder-alpha', - to: '/docs/features/software-templates/migrating-to-rjsf-v5', + to: '/docs/features/software-templates/', }, { from: '/docs/auth/glossary', @@ -291,6 +291,10 @@ const config: Config = { from: '/docs/plugins/existing-plugins', to: '/docs/plugins/', }, + { + from: '/docs/features/software-templates/migrating-to-rjsf-v5', + to: '/docs/features/software-templates/', + }, ], }), [ diff --git a/mkdocs.yml b/mkdocs.yml index cbf8b91477ea05..827e7ac7631eb2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,7 @@ plugins: - redirects: redirect_maps: 'index.md': 'overview/what-is-backstage.md' + 'features/software-templates/migrating-from-v1beta2-to-v1beta3.md': 'features/software-templates/index.md' # For sidebar navigation on https://backstage.io/, see `microsite/sidebars.js` nav: From 88fef53c08a002af1948bb393b846f59b1733291 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 14 Mar 2026 09:27:49 -0500 Subject: [PATCH 06/55] Removed redirect based on feedback Signed-off-by: Andre Wanlin --- microsite/docusaurus.config.ts | 4 ---- mkdocs.yml | 1 - 2 files changed, 5 deletions(-) diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index 5b69e262374f51..1cff303d548fef 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -291,10 +291,6 @@ const config: Config = { from: '/docs/plugins/existing-plugins', to: '/docs/plugins/', }, - { - from: '/docs/features/software-templates/migrating-to-rjsf-v5', - to: '/docs/features/software-templates/', - }, ], }), [ diff --git a/mkdocs.yml b/mkdocs.yml index 827e7ac7631eb2..cbf8b91477ea05 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,7 +8,6 @@ plugins: - redirects: redirect_maps: 'index.md': 'overview/what-is-backstage.md' - 'features/software-templates/migrating-from-v1beta2-to-v1beta3.md': 'features/software-templates/index.md' # For sidebar navigation on https://backstage.io/, see `microsite/sidebars.js` nav: From 35e130c60161e1495e81d59f65508dfdf53293b7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 10:49:54 +0100 Subject: [PATCH 07/55] docs: sunset the docs/plugins section as legacy documentation Mark all pages in docs/plugins/ with legacy admonition banners pointing to the appropriate new frontend system and backend system documentation. Rename the sidebar section to "Plugins (Legacy)" and move it lower in the navigation hierarchy. Add cross-references from the new frontend system building-plugins guide to relevant cross-cutting topics (i18n, analytics, feature flags, search) that still live in the legacy section. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../building-plugins/01-index.md | 9 ++ docs/plugins/add-to-directory.md | 6 ++ docs/plugins/analytics.md | 6 ++ docs/plugins/backend-plugin.md | 6 ++ docs/plugins/call-existing-api.md | 6 ++ docs/plugins/composability.md | 6 ++ docs/plugins/create-a-plugin.md | 6 ++ docs/plugins/feature-flags.md | 6 ++ docs/plugins/index.md | 10 +- ...ntegrating-plugin-into-software-catalog.md | 6 ++ .../integrating-search-into-plugins.md | 6 ++ docs/plugins/internationalization.md | 6 ++ docs/plugins/new-backend-system.md | 6 ++ docs/plugins/observability.md | 6 ++ docs/plugins/plugin-development.md | 6 ++ docs/plugins/plugin-directory-audit.md | 6 ++ docs/plugins/proxying.md | 6 ++ docs/plugins/publish-private.md | 6 ++ docs/plugins/structure-of-a-plugin.md | 6 ++ docs/plugins/testing.md | 6 ++ microsite/sidebars.ts | 98 ++++++++++--------- mkdocs.yml | 11 ++- 22 files changed, 181 insertions(+), 55 deletions(-) diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index 861f454280d91c..795249c73f63cf 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -238,3 +238,12 @@ export const examplePlugin = createFrontendPlugin({ The `ExampleEntityContent` itself is again a regular React component where you can implement any functionality you want. To access the entity that the content is being rendered for, you can use the `useEntity` hook from `@backstage/plugin-catalog-react`. You can see a full list of APIs provided by the catalog React library in [the API reference](https://backstage.io/api/stable/modules/_backstage_plugin-catalog-react.index.html). For a more complete list of the different kinds of extensions that you can create for your plugin, see the [extension blueprints](./03-common-extension-blueprints.md) section. + +## Related topics + +The following guides cover cross-cutting concerns that apply to both the old and new frontend systems: + +- [Internationalization (i18n)](../../plugins/internationalization.md) — Adding translations to your plugin using `createTranslationRef` and `useTranslationRef`. +- [Plugin Analytics](../../plugins/analytics.md) — Instrumenting user interactions with the Analytics API, including the `AnalyticsImplementationBlueprint` for the new frontend system. +- [Feature Flags](../../plugins/feature-flags.md) — Defining and using feature flags. In the new frontend system, flags are declared in the `featureFlags` option of `createFrontendPlugin`. +- [Integrating Search into Plugins](../../plugins/integrating-search-into-plugins.md) — Building search experiences and collators. For the new frontend system, use `SearchResultListItemBlueprint` from `@backstage/plugin-search-react/alpha`. diff --git a/docs/plugins/add-to-directory.md b/docs/plugins/add-to-directory.md index 48affef489f9e1..a87062e207bb2e 100644 --- a/docs/plugins/add-to-directory.md +++ b/docs/plugins/add-to-directory.md @@ -4,6 +4,12 @@ title: Add to Directory description: Documentation on Adding Plugin to Plugin Directory --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The process for adding plugins to the directory described here is still current. + +::: + ## Adding a Plugin to the Directory To add a new plugin to the [plugin directory](https://backstage.io/plugins) create a file with the following pattern `.yaml` where `` is the name of your plugin. This file will go in [`microsite/data/plugins`](https://github.com/backstage/backstage/tree/master/microsite/data/plugins) with your plugin's information. Example: diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index 3e8d5b4111409d..692849c1eb98a7 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -4,6 +4,12 @@ title: Plugin Analytics description: Measuring usage of your Backstage instance. --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The concepts and events described here apply to both the old and new frontend systems. However, the "Writing Integrations" section shows examples for both the old frontend system (`createApiFactory`) and the new frontend system (`AnalyticsImplementationBlueprint`). For new development, prefer the new frontend system approach. See also [Utility APIs](../frontend-system/utility-apis/01-index.md). + +::: + Setting up, maintaining, and iterating on an instance of Backstage can be a large investment. To help measure return on this investment, Backstage comes with an event-based Analytics API that grants app integrators the flexibility to diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 2872fdcda9a7eb..91a3e048398914 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -4,6 +4,12 @@ title: Backend plugins description: Creating and Developing Backend plugins --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. While this page already describes the new backend system patterns, the canonical documentation for building backend plugins has moved to [Building Backend Plugins and Modules](../backend-system/building-plugins-and-modules/01-index.md). + +::: + This page describes the process of creating and managing backend plugins in your Backstage repository. diff --git a/docs/plugins/call-existing-api.md b/docs/plugins/call-existing-api.md index 35a6d8d599908c..5b5a91b708a7d8 100644 --- a/docs/plugins/call-existing-api.md +++ b/docs/plugins/call-existing-api.md @@ -4,6 +4,12 @@ title: Call Existing API description: Describes the various options that Backstage frontend plugins have, in communicating with service APIs that already exist --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The frontend code examples on this page use the old frontend system APIs (`discoveryApiRef`, `fetchApiRef` from `@backstage/core-plugin-api`). The same APIs are available in the new frontend system via `@backstage/frontend-plugin-api`. The general guidance on when to use direct requests vs. the proxy vs. a backend plugin remains valid for both systems. + +::: + This article describes the various options that Backstage frontend plugins have, in communicating with service APIs that already exist. Each section below describes a possible choice, and the circumstances under which it fits. diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index d6c9abc98f29f8..35da80a5b2866e 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -4,6 +4,12 @@ title: Composability System description: Documentation for the Backstage plugin composability APIs. --- +:::caution Legacy Documentation + +This page describes the composability system for the **old frontend system**, including `createRoutableExtension`, `createComponentExtension`, `RouteRef`, `ExternalRouteRef`, and component data. For the new frontend system, see [Extensions](../frontend-system/architecture/20-extensions.md), [Extension Blueprints](../frontend-system/architecture/23-extension-blueprints.md), and [Routes](../frontend-system/architecture/36-routes.md). + +::: + ## Summary This page describes the composability system that helps bring together content diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index ea0b088af7508b..8c97a3d30dd984 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -4,6 +4,12 @@ title: Create a Backstage Plugin description: Documentation on How to Create a Backstage Plugin --- +:::caution Legacy Documentation + +This page describes creating plugins for the **old frontend system**. For creating plugins using the new frontend system, see [Building Frontend Plugins](../frontend-system/building-plugins/01-index.md). For creating backend plugins, see [Building Backend Plugins and Modules](../backend-system/building-plugins-and-modules/01-index.md). + +::: + A Backstage Plugin adds functionality to Backstage. ## Create a Plugin diff --git a/docs/plugins/feature-flags.md b/docs/plugins/feature-flags.md index 2d0e34ba4b215d..5d35e3c057c829 100644 --- a/docs/plugins/feature-flags.md +++ b/docs/plugins/feature-flags.md @@ -4,6 +4,12 @@ title: Feature Flags description: Details the process of defining setting and reading a feature flag. --- +:::caution Legacy Documentation + +This page describes feature flags using the **old frontend system** APIs (`createPlugin` from `@backstage/core-plugin-api` and `createApp` from `@backstage/app-defaults`). For the new frontend system, feature flags are declared via the `featureFlags` option in `createFrontendPlugin` — see [Building Frontend Plugins](../frontend-system/building-plugins/01-index.md). The `FeatureFlagged` component and `featureFlagsApiRef` work the same way in both systems. + +::: + Backstage offers the ability to define feature flags inside a plugin or during application creation. This allows you to restrict parts of your plugin to those individual users who have toggled the feature flag to on. This page describes the process of defining, setting and reading a feature flag. If you are looking for using feature flags specifically with software templates please see [Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates#remove-sections-or-fields-based-on-feature-flags). diff --git a/docs/plugins/index.md b/docs/plugins/index.md index ef45152f55e59d..388c3b1177e480 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -1,9 +1,15 @@ --- id: index -title: Introduction to Plugins -description: Learn about integrating various infrastructure and software development tools into Backstage through plugins. +title: Introduction to Plugins (Legacy) +description: Legacy documentation for integrating various infrastructure and software development tools into Backstage through plugins using the old frontend system. --- +:::caution Legacy Documentation + +This section covers plugin development using the **old frontend system**. For new development, please refer to the [new frontend system](../frontend-system/index.md) and [new backend system](../backend-system/index.md) documentation. The content here is kept for reference and for maintaining existing plugins that have not yet been migrated. + +::: + Backstage orchestrates a cohesive single-page application by seamlessly integrating various plugins. Our vision for the plugin ecosystem champions flexibility, empowering you to incorporate a broad spectrum of infrastructure and software development tools into Backstage as plugins. Adherence to stringent [design guidelines](../dls/design.md) guarantees a consistent and intuitive user experience across the entire plugin landscape. diff --git a/docs/plugins/integrating-plugin-into-software-catalog.md b/docs/plugins/integrating-plugin-into-software-catalog.md index 417b9324516553..1e3ebb2d2b716e 100644 --- a/docs/plugins/integrating-plugin-into-software-catalog.md +++ b/docs/plugins/integrating-plugin-into-software-catalog.md @@ -4,6 +4,12 @@ title: Integrate into the Software Catalog description: How to integrate a plugin into software catalog --- +:::caution Legacy Documentation + +This page describes integrating plugins into the Software Catalog using the **old frontend system** patterns (`EntitySwitch`, `EntityLayout`, `EntityLayout.Route`). For the new frontend system, entity page integrations are done using `EntityCardBlueprint` and `EntityContentBlueprint` — see [Common Extension Blueprints](../frontend-system/building-plugins/03-common-extension-blueprints.md). + +::: + > This is an advanced use case and currently is an experimental feature. Expect > API to change over time diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md index 7a7201d9e6d495..c27192e05c5e91 100644 --- a/docs/plugins/integrating-search-into-plugins.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -4,6 +4,12 @@ title: Integrating Search into a plugin description: How to integrate Search into a Backstage plugin --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The backend search collator patterns described here use the new backend system and are still current. The frontend search experience examples use the old frontend system APIs. For the new frontend system, search result list items are created using `SearchResultListItemBlueprint` — see [Common Extension Blueprints](../frontend-system/building-plugins/03-common-extension-blueprints.md). + +::: + The Backstage Search Platform was designed to give plugin developers the APIs and interfaces needed to offer search experiences within their plugins, while abstracting away (and instead empowering application integrators to choose) the diff --git a/docs/plugins/internationalization.md b/docs/plugins/internationalization.md index 2770f767990208..7466d17efa53f8 100644 --- a/docs/plugins/internationalization.md +++ b/docs/plugins/internationalization.md @@ -4,6 +4,12 @@ title: Internationalization description: Documentation on adding internationalization to plugins and apps --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The i18n APIs (`createTranslationRef`, `useTranslationRef`) work the same way in both the old and new frontend systems. The "For an application developer" section already shows the new frontend system approach using `createTranslationResource` from `@backstage/frontend-plugin-api`. + +::: + ## Overview The Backstage core function provides internationalization for plugins and apps. The underlying library is [`i18next`](https://www.i18next.com/) with some additional Backstage typescript magic for type safety with keys. diff --git a/docs/plugins/new-backend-system.md b/docs/plugins/new-backend-system.md index cf983615b5d2a5..e7cef270abfd97 100644 --- a/docs/plugins/new-backend-system.md +++ b/docs/plugins/new-backend-system.md @@ -4,6 +4,12 @@ title: New Backend System description: Details of the new backend system --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The canonical documentation for the backend system has moved to the [Backend System](../backend-system/index.md) section, which includes more detailed and up-to-date guides for [building plugins and modules](../backend-system/building-plugins-and-modules/01-index.md), [architecture](../backend-system/architecture/01-index.md), and [core services](../backend-system/core-services/01-index.md). + +::: + ## Status The new backend system is released and ready for production use, and many plugins and modules have already been migrated. We recommend all plugins and deployments to migrate to the new system. diff --git a/docs/plugins/observability.md b/docs/plugins/observability.md index 7803d90bd5e9df..eab2ed200cc1df 100644 --- a/docs/plugins/observability.md +++ b/docs/plugins/observability.md @@ -4,6 +4,12 @@ title: Observability description: Adding Observability to Your Plugin --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. For new backend system logging, see the [Logger](../backend-system/core-services/logger.md) and [Root Logger](../backend-system/core-services/root-logger.md) core service documentation. For health checks, see [Root Health](../backend-system/core-services/root-health.md). + +::: + This article briefly describes the observability options that are available to a Backstage integrator. diff --git a/docs/plugins/plugin-development.md b/docs/plugins/plugin-development.md index 5ba1de4a7d3502..89318e7c5f8134 100644 --- a/docs/plugins/plugin-development.md +++ b/docs/plugins/plugin-development.md @@ -4,6 +4,12 @@ title: Plugin Development description: Documentation on Plugin Development --- +:::caution Legacy Documentation + +This page covers plugin development patterns for the **old frontend system**, including `createPlugin`, `createRoutableExtension`, and `RouteRef` from `@backstage/core-plugin-api`. For the new frontend system equivalents, see [Building Frontend Plugins](../frontend-system/building-plugins/01-index.md) and [Routes](../frontend-system/architecture/36-routes.md). + +::: + Backstage plugins provide features to a Backstage App. Each plugin is treated as a self-contained web app and can include almost any diff --git a/docs/plugins/plugin-directory-audit.md b/docs/plugins/plugin-directory-audit.md index 9e82cb6de52ff8..b080463c829c6b 100644 --- a/docs/plugins/plugin-directory-audit.md +++ b/docs/plugins/plugin-directory-audit.md @@ -4,6 +4,12 @@ title: Plugin Directory Audit description: Details about the process for auditing plugins in the directory --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The audit process described here is still current. + +::: + ## Audit Process We have a simple process in place to audit the plugins in the Plugin Directory: diff --git a/docs/plugins/proxying.md b/docs/plugins/proxying.md index bd89fc6c36b46d..a764ea64d2ef72 100644 --- a/docs/plugins/proxying.md +++ b/docs/plugins/proxying.md @@ -4,6 +4,12 @@ title: Proxying description: Documentation on Proxying --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The proxy configuration and usage described here applies to both the old and new backend systems. For creating backend plugins and modules, see [Building Backend Plugins and Modules](../backend-system/building-plugins-and-modules/01-index.md). + +::: + This page describes how to configure and use the built-in HTTP proxy functionality in your Backstage backend. ## Overview diff --git a/docs/plugins/publish-private.md b/docs/plugins/publish-private.md index d04449d46569b4..b5bb1b937a7998 100644 --- a/docs/plugins/publish-private.md +++ b/docs/plugins/publish-private.md @@ -4,4 +4,10 @@ title: Publish private description: Documentation on How to Publish private --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. + +::: + ## TODO diff --git a/docs/plugins/structure-of-a-plugin.md b/docs/plugins/structure-of-a-plugin.md index 7cfaca45619491..3843359857c8f7 100644 --- a/docs/plugins/structure-of-a-plugin.md +++ b/docs/plugins/structure-of-a-plugin.md @@ -4,6 +4,12 @@ title: Structure of a Plugin description: Details about structure of a plugin --- +:::caution Legacy Documentation + +This page describes the structure of a plugin for the **old frontend system**. For the new frontend system, see [Building Frontend Plugins](../frontend-system/building-plugins/01-index.md). The general folder structure is similar, but the plugin wiring in `plugin.ts` differs significantly. + +::: + Nice, you have a new plugin! We'll soon see how we can develop it into doing great things. But first off, let's look at what we get out of the box. diff --git a/docs/plugins/testing.md b/docs/plugins/testing.md index 1297d516f311cb..94015700fe4800 100644 --- a/docs/plugins/testing.md +++ b/docs/plugins/testing.md @@ -4,6 +4,12 @@ title: Testing with Jest description: Documentation on How to do unit testing with Jest --- +:::caution Legacy Documentation + +This section is part of the legacy plugins documentation. The general testing principles described here still apply, but for system-specific testing guides, see [Testing Frontend Plugins](../frontend-system/building-plugins/02-testing.md) and [Testing Backend Plugins and Modules](../backend-system/building-plugins-and-modules/02-testing.md). + +::: + :::note Note You may want to consider migrating to Jest 30, to do this, you can follow this guide: [Migrating to Jest 30](../tutorials/jest30-migration.md) diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index ec4585b4077887..1be013af7d7d9f 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -412,56 +412,14 @@ export default { ), sidebarElementWithIndex( { - label: 'Plugins', - description: 'Extend Backstage with custom functionality.', + label: 'OpenAPI', + description: + 'Work with OpenAPI specifications and generate clients.', }, [ - 'plugins/index', - 'plugins/create-a-plugin', - 'plugins/plugin-development', - 'plugins/structure-of-a-plugin', - 'plugins/integrating-plugin-into-software-catalog', - 'plugins/integrating-search-into-plugins', - 'plugins/composability', - 'plugins/internationalization', - 'plugins/analytics', - 'plugins/feature-flags', - sidebarElementWithIndex( - { - label: 'OpenAPI', - description: - 'Work with OpenAPI specifications and generate clients.', - }, - [ - 'openapi/01-getting-started', - 'openapi/generate-client', - 'openapi/test-case-validation', - ], - ), - sidebarElementWithIndex( - { - label: 'Backends and APIs', - description: 'Build and manage backend services and APIs.', - }, - [ - 'plugins/proxying', - 'plugins/backend-plugin', - 'plugins/call-existing-api', - ], - ), - sidebarElementWithIndex( - { label: 'Testing', description: 'Testing plugins and modules.' }, - ['plugins/testing'], - ), - sidebarElementWithIndex( - { label: 'Publishing', description: 'Publishing your plugins.' }, - [ - 'plugins/publish-private', - 'plugins/add-to-directory', - 'plugins/plugin-directory-audit', - ], - ), - 'plugins/observability', + 'openapi/01-getting-started', + 'openapi/generate-client', + 'openapi/test-case-validation', ], ), sidebarElementWithIndex( @@ -719,6 +677,50 @@ export default { ), ], ), + sidebarElementWithIndex( + { + label: 'Plugins (Legacy)', + description: + 'Legacy plugin development documentation for the old frontend system. For new development, see the Frontend System and Backend System sections under Framework.', + }, + [ + 'plugins/index', + 'plugins/create-a-plugin', + 'plugins/plugin-development', + 'plugins/structure-of-a-plugin', + 'plugins/integrating-plugin-into-software-catalog', + 'plugins/integrating-search-into-plugins', + 'plugins/composability', + 'plugins/internationalization', + 'plugins/analytics', + 'plugins/feature-flags', + sidebarElementWithIndex( + { + label: 'Backends and APIs', + description: 'Build and manage backend services and APIs.', + }, + [ + 'plugins/proxying', + 'plugins/backend-plugin', + 'plugins/call-existing-api', + ], + ), + sidebarElementWithIndex( + { label: 'Testing', description: 'Testing plugins and modules.' }, + ['plugins/testing'], + ), + sidebarElementWithIndex( + { label: 'Publishing', description: 'Publishing your plugins.' }, + [ + 'plugins/publish-private', + 'plugins/add-to-directory', + 'plugins/plugin-directory-audit', + ], + ), + 'plugins/observability', + 'plugins/new-backend-system', + ], + ), sidebarElementWithIndex( { label: 'FAQ', description: 'Frequently asked questions and answers.' }, ['faq/index', 'faq/product', 'faq/technical'], diff --git a/mkdocs.yml b/mkdocs.yml index 618f77a59f8a03..2030d87c450628 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -141,7 +141,11 @@ nav: - Locations: 'integrations/google-cloud-storage/locations.md' - LDAP: - Org Data: 'integrations/ldap/org.md' - - Plugins: + - OpenAPI: + - Schema-first plugins with OpenAPI (Experimental): 'openapi/01-getting-started.md' + - Generate a client from your OpenAPI spec: 'openapi/generate-client.md' + - Validate your OpenAPI spec against test data: 'openapi/test-case-validation.md' + - Plugins (Legacy): - Intro to plugins: 'plugins/index.md' - Create a Backstage Plugin: 'plugins/create-a-plugin.md' - Plugin Development: 'plugins/plugin-development.md' @@ -151,10 +155,6 @@ nav: - Plugin Analytics: 'plugins/analytics.md' - Feature Flags: 'plugins/feature-flags.md' - Internationalization (i18n): 'plugins/internationalization.md' - - OpenAPI: - - Schema-first plugins with OpenAPI (Experimental): 'openapi/01-getting-started.md' - - Generate a client from your OpenAPI spec: 'openapi/generate-client.md' - - Validate your OpenAPI spec against test data: 'openapi/test-case-validation.md' - Backends and APIs: - Proxying: 'plugins/proxying.md' - Backend plugin: 'plugins/backend-plugin.md' @@ -165,6 +165,7 @@ nav: - Publish private: 'plugins/publish-private.md' - Add to Directory: 'plugins/add-to-directory.md' - Observability: 'plugins/observability.md' + - New Backend System: 'plugins/new-backend-system.md' - Configuration: - Static Configuration in Backstage: 'conf/index.md' - Reading Backstage Configuration: 'conf/reading.md' From aac778ba8c58c66ae02541c4a2fe0d88383872ea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 11:05:09 +0100 Subject: [PATCH 08/55] docs: adapt cross-cutting plugin docs into the frontend system section Instead of just referencing the legacy docs/plugins documentation, adapt and add them as new pages under frontend-system/building-plugins/ with updated imports and examples for the new frontend system. Also moves the OpenAPI sidebar entry under Framework instead of top-level. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../building-plugins/01-index.md | 10 +- .../07-internationalization.md | 410 +++++++++++++ .../building-plugins/08-analytics.md | 317 ++++++++++ .../building-plugins/09-feature-flags.md | 90 +++ .../10-integrating-search-into-plugins.md | 550 ++++++++++++++++++ docs/plugins/analytics.md | 2 +- docs/plugins/feature-flags.md | 2 +- .../integrating-search-into-plugins.md | 2 +- docs/plugins/internationalization.md | 2 +- microsite/sidebars.ts | 28 +- mkdocs.yml | 8 +- 11 files changed, 1396 insertions(+), 25 deletions(-) create mode 100644 docs/frontend-system/building-plugins/07-internationalization.md create mode 100644 docs/frontend-system/building-plugins/08-analytics.md create mode 100644 docs/frontend-system/building-plugins/09-feature-flags.md create mode 100644 docs/frontend-system/building-plugins/10-integrating-search-into-plugins.md diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index 795249c73f63cf..24a815ef7b9a61 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -241,9 +241,9 @@ For a more complete list of the different kinds of extensions that you can creat ## Related topics -The following guides cover cross-cutting concerns that apply to both the old and new frontend systems: +The following guides cover cross-cutting concerns for building frontend plugins: -- [Internationalization (i18n)](../../plugins/internationalization.md) — Adding translations to your plugin using `createTranslationRef` and `useTranslationRef`. -- [Plugin Analytics](../../plugins/analytics.md) — Instrumenting user interactions with the Analytics API, including the `AnalyticsImplementationBlueprint` for the new frontend system. -- [Feature Flags](../../plugins/feature-flags.md) — Defining and using feature flags. In the new frontend system, flags are declared in the `featureFlags` option of `createFrontendPlugin`. -- [Integrating Search into Plugins](../../plugins/integrating-search-into-plugins.md) — Building search experiences and collators. For the new frontend system, use `SearchResultListItemBlueprint` from `@backstage/plugin-search-react/alpha`. +- [Internationalization (i18n)](./07-internationalization.md) — Adding translations to your plugin using `createTranslationRef` and `useTranslationRef`. +- [Plugin Analytics](./08-analytics.md) — Instrumenting user interactions with the Analytics API using `AnalyticsImplementationBlueprint`. +- [Feature Flags](./09-feature-flags.md) — Defining and using feature flags via the `featureFlags` option of `createFrontendPlugin`. +- [Integrating Search into Plugins](./10-integrating-search-into-plugins.md) — Building search experiences, collators, and custom result list items using `SearchResultListItemBlueprint`. diff --git a/docs/frontend-system/building-plugins/07-internationalization.md b/docs/frontend-system/building-plugins/07-internationalization.md new file mode 100644 index 00000000000000..270f1f66259214 --- /dev/null +++ b/docs/frontend-system/building-plugins/07-internationalization.md @@ -0,0 +1,410 @@ +--- +id: internationalization +title: Internationalization +sidebar_label: Internationalization +description: Adding internationalization to plugins and apps +--- + +## Overview + +The Backstage core function provides internationalization for plugins and apps. The underlying library is [`i18next`](https://www.i18next.com/) with some additional Backstage typescript magic for type safety with keys. + +## For a plugin developer + +When you are creating your plugin, you have the possibility to use `createTranslationRef` to define all messages for your plugin. For example: + +```ts +import { createTranslationRef } from '@backstage/frontend-plugin-api'; + +/** @alpha */ +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + indexPage: { + title: 'All your components', + createButtonTitle: 'Create new component', + }, + entityPage: { + notFound: 'Entity not found', + }, + }, +}); +``` + +And then use these messages in your components like: + +```tsx +import { useTranslationRef } from '@backstage/frontend-plugin-api'; + +const { t } = useTranslationRef(myPluginTranslationRef); + +return ( + + + +); +``` + +You will see how the initial dictionary structure and nesting get converted into dot notation, so we encourage `camelCase` in key names and lean on the nesting structure to separate keys. + +### Guidelines for `i18n` messages and keys + +The API for `i18n` messages and keys can be pretty tricky to get right, as it's a pretty flexible API. We've put together some guidelines to help you get started that encourage good practices when thinking about translating plugins: + +#### Key names + +When defining messages it is recommended to use a nested structure that represents the semantic hierarchy in your translations. This allows for better organization and understanding of the structure. For example: + +```ts +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + dashboardPage: { + title: 'All your components', + subtitle: 'Create new component', + widgets: { + weather: { + title: 'Weather', + description: 'Shows the weather', + }, + calendar: { + title: 'Calendar', + description: 'Shows the calendar', + }, + }, + }, + entityPage: { + notFound: 'Entity not found', + }, + }, +}); +``` + +Think about the semantic placement of content rather than the text content itself. Group related translations under a common prefix, and use nesting to represent relationships between different parts of your application. It's good to start grouping under extensions, page sections, or visual scopes and experiences. + +Translations should avoid using their own text content as key where possible, as this can lead to confusion if the translation changes. Instead prefer to use keys that describe the location or usage of the text. + +#### Common Key names + +This list is intended to grow over time, but below are some examples of common key names and patterns that we encourage you to use where possible: + +- `${page}.title` +- `${page}.subtitle` +- `${page}.description` + +- `${page}.header.title` + +#### Key reuse + +Reusing the same key in multiple places is discouraged. This helps prevent ambiguity, and instead keeps the usage of each key as clear as possible. Consider creating duplicate keys that are grouped under a semantic section instead. + +#### Flat keys + +Avoid a flat key structure at the root level, as it can lead to naming conflicts and make the translation file harder to manage and change evolve over time. Instead, group translations under a common prefix. + +```ts +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + // this is BAD + title: 'My page', + subtitle: 'My subtitle', + // this is GOOD + dashboardPage: { + header: { + title: 'All your components', + subtitle: 'Create new component', + }, + }, + }, +}); +``` + +#### Plurals + +The `i18next` library, which is used as the underlying implementation, has built-in support for pluralization. You can use this feature as is described in [the documentation](https://www.i18next.com/translation-function/plurals). + +We encourage you to use this feature and avoid creating different key prefixes for pluralized content. For example: + +```ts +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + dashboardPage: { + title: 'All your components', + subtitle: 'Create new component', + cards: { + title_one: 'You have one card', + title_two: 'You have two cards', + title_other: 'You have many cards ({{count}})', + }, + }, + entityPage: { + notFound: 'Entity not found', + }, + }, +}); +``` + +#### JSX Elements + +The translation API supports interpolation of JSX elements by passing them directly as values to the translation function. If any of the provided interpolation values are JSX elements, the translation function will return a JSX element instead of a string. + +For example, you might define the following messages: + +```ts title="define the message" +export const myPluginTranslationRef = createTranslationRef({ + id: 'plugin.my-plugin', + messages: { + entityPage: { + redirect: { + message: 'The entity you are looking for has been moved to {{link}}.', + link: 'new location', + }, + }, + }, +}); +``` + +Which can be used within a component like this: + +```tsx title="use within a component" +const { t } = useTranslationRef(myPluginTranslationRef); + +return ( +
+ {t('entityPage.redirect.message', { + link: {t('entityPage.redirect.link')}, + })} +
+); +``` + +The return type of the outer `t` function will be a `JSX.Element`, with the underlying value being a React fragment of the different parts of the message. + +## For an application developer + +As an app developer you can both override the default English messages of any plugin, and provide translations for additional languages. + +### Overriding messages + +To customize specific messages without adding new languages, create a translation resource that overrides the default English messages: + +```ts +// packages/app/src/translations/catalog.ts + +import { createTranslationResource } from '@backstage/frontend-plugin-api'; +import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha'; + +export const catalogTranslations = createTranslationResource({ + ref: catalogTranslationRef, + translations: { + en: () => + Promise.resolve({ + default: { + 'indexPage.title': 'Service directory', + 'indexPage.createButtonTitle': 'Register new service', + }, + }), + }, +}); +``` + +Then register it in your app: + +```diff ++ import { catalogTranslations } from './translations/catalog'; + + const app = createApp({ ++ __experimentalTranslations: { ++ resources: [catalogTranslations], ++ }, + }) +``` + +You only need to include the keys you want to override — any missing keys fall back to the plugin's defaults. + +### Adding language translations + +To add support for additional languages, create translation resources with lazy-loaded message files for each language: + +```ts +// packages/app/src/translations/userSettings.ts + +import { createTranslationResource } from '@backstage/frontend-plugin-api'; +import { userSettingsTranslationRef } from '@backstage/plugin-user-settings/alpha'; + +export const userSettingsTranslations = createTranslationResource({ + ref: userSettingsTranslationRef, + translations: { + zh: () => import('./userSettings-zh'), + }, +}); +``` + +The translation messages can be defined using `createTranslationMessages` for type safety: + +```ts +// packages/app/src/translations/userSettings-zh.ts + +import { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { userSettingsTranslationRef } from '@backstage/plugin-user-settings/alpha'; + +const zh = createTranslationMessages({ + ref: userSettingsTranslationRef, + full: false, // False means that this is a partial translation + messages: { + 'languageToggle.title': '语言', + 'languageToggle.select': '选择{{language}}', + }, +}); + +export default zh; +``` + +Or as a plain object export: + +```ts +// packages/app/src/translations/userSettings-zh.ts +export default { + 'languageToggle.title': '语言', + 'languageToggle.select': '选择{{language}}', + 'languageToggle.description': '切换语言', + 'themeToggle.title': '主题', + 'themeToggle.description': '切换主题', + 'themeToggle.select': '选择{{theme}}', + 'themeToggle.selectAuto': '选择自动主题', + 'themeToggle.names.auto': '自动', + 'themeToggle.names.dark': '暗黑', + 'themeToggle.names.light': '明亮', +}; +``` + +Register it with the available languages declared: + +```diff ++ import { userSettingsTranslations } from './translations/userSettings'; + + const app = createApp({ ++ __experimentalTranslations: { ++ availableLanguages: ['en', 'zh'], ++ resources: [userSettingsTranslations], ++ }, + }) +``` + +Go to the Settings page — you should see language switching buttons. Switch languages to verify your translations are loaded correctly. + +### Using the CLI for full translation workflows + +When translating your app to other languages at scale — especially when working with external translation systems — the Backstage CLI provides `translations export` and `translations import` commands that automate the extraction and wiring of translation messages across all your plugin dependencies. + +#### Exporting default messages + +From your app package directory (e.g. `packages/app`), run: + +```bash +yarn backstage-cli translations export +``` + +This scans all frontend plugin dependencies (including transitive ones) for `TranslationRef` definitions and writes their default English messages as JSON files: + +```text +translations/ + manifest.json + messages/ + catalog.en.json + org.en.json + scaffolder.en.json + ... +``` + +Each `.en.json` file contains the flattened message keys and their default values: + +```json +{ + "indexPage.title": "All your components", + "indexPage.createButtonTitle": "Create new component", + "entityPage.notFound": "Entity not found" +} +``` + +#### Creating translations + +Copy the exported files and translate them for your target languages: + +```bash +cp translations/messages/catalog.en.json translations/messages/catalog.zh.json +``` + +Then edit `catalog.zh.json` with the translated strings. You only need to include the keys you want to translate — missing keys fall back to the English defaults at runtime. + +#### Generating wiring code + +Once you have translated files in place, run: + +```bash +yarn backstage-cli translations import +``` + +This generates a TypeScript module at `src/translations/resources.ts` that wires everything together: + +```ts +// This file is auto-generated by backstage-cli translations import +// Do not edit manually. + +import { createTranslationResource } from '@backstage/frontend-plugin-api'; +import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha'; + +export default [ + createTranslationResource({ + ref: catalogTranslationRef, + translations: { + zh: () => import('../../translations/messages/catalog.zh.json'), + }, + }), +]; +``` + +Import the generated resources in your app: + +```ts +import translationResources from './translations/resources'; + +const app = createApp({ + __experimentalTranslations: { + availableLanguages: ['en', 'zh'], + resources: translationResources, + }, +}); +``` + +#### Custom file patterns + +By default, message files use the pattern `messages/{id}.{lang}.json` (e.g. `messages/catalog.en.json`). You can change this with the `--pattern` option: + +```bash +yarn backstage-cli translations export --pattern '{lang}/{id}.json' +``` + +This produces a directory structure grouped by language instead: + +```text +translations/en/catalog.json +translations/zh/catalog.json +``` + +The pattern is stored in the manifest, so the `import` command automatically uses the same layout. + +#### Integration with external translation systems + +The exported JSON files are standard key-value pairs compatible with most external translation systems. A typical workflow looks like: + +1. Run `translations export` to generate the source English files +2. Upload the `.en.json` files to your translation system +3. Download the translated files back into the translations directory +4. Run `translations import` to regenerate the wiring code + +For full command reference, see the [CLI commands documentation](../../tooling/cli/03-commands.md#translations-export). diff --git a/docs/frontend-system/building-plugins/08-analytics.md b/docs/frontend-system/building-plugins/08-analytics.md new file mode 100644 index 00000000000000..08babdb61263b6 --- /dev/null +++ b/docs/frontend-system/building-plugins/08-analytics.md @@ -0,0 +1,317 @@ +--- +id: analytics +title: Plugin Analytics +sidebar_label: Analytics +description: Measuring usage of your Backstage instance +--- + +Setting up, maintaining, and iterating on an instance of Backstage can be a +large investment. To help measure return on this investment, Backstage comes +with an event-based Analytics API that grants app integrators the flexibility to +collect and analyze Backstage usage in the analytics tool of their choice, while +providing plugin developers a standard interface for instrumenting key user +interactions. + +## Concepts + +- **Events** consist of, at a minimum, an `action` (like `click`) and a + `subject` (like `thing that was clicked on`). +- **Attributes** represent additional dimensional data (in the form of key/value + pairs) that may be provided on an event-by-event basis. To continue the above + example, the URL a user clicked to might look like `{ "to": "/a/page" }`. +- **Context** represents the broader context in which an event took place. By + default, information like `pluginId`, `extension`, and `routeRef` are + provided. + +This composition of events aims to allow analysis at different levels of detail, +enabling very granular questions (like "what is the most clicked on thing on a +particular route") as well as very high-level questions (like "what is the most +used plugin in my Backstage instance") to be answered. + +## Supported Analytics Tools + +While all that's needed to consume and forward these events to an analytics tool +is a concrete implementation of [AnalyticsApi][analytics-api-type], common +integrations are packaged and provided as plugins. Find your analytics tool of +choice below. + +| Analytics Tool | Support Status | +| ------------------------------------- | -------------- | +| [Google Analytics][ga] | Yes ✅ | +| [Google Analytics 4][ga4] | Yes ✅ | +| [New Relic Browser][newrelic-browser] | Community ✅ | +| [Matomo][matomo] | Community ✅ | +| [Quantum Metric][qm] | Community ✅ | +| [Generic HTTP][generic-http] | Community ✅ | + +To suggest an integration, please [open an issue][add-tool] for the analytics +tool your organization uses. Or jump to [Writing Integrations][int-howto] to +learn how to contribute the integration yourself! + +[ga]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-ga/README.md +[ga4]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-ga4/README.md +[newrelic-browser]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-newrelic-browser/README.md +[qm]: https://github.com/quantummetric/analytics-module-qm/blob/main/README.md +[matomo]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-matomo/README.md +[add-tool]: https://github.com/backstage/backstage/issues/new?assignees=&labels=plugin&template=plugin_template.md&title=%5BAnalytics+Module%5D+THE+ANALYTICS+TOOL+TO+INTEGRATE +[int-howto]: #writing-integrations +[analytics-api-type]: https://backstage.io/api/stable/types/_backstage_core-plugin-api.index.AnalyticsApi.html +[generic-http]: https://github.com/pfeifferj/backstage-plugin-analytics-generic/blob/main/README.md + +## Key Events + +The following table summarizes events that, depending on the plugins you have +installed, may be captured. + +| Action | Subject | Other Notes | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `navigate` | The URL of the page that was navigated to. | Fired immediately when route location changes (unless associated plugin/route data is ambiguous, in which case the event is fired after plugin/route data becomes known, immediately before the next event or document unload). The parameters of the current route will be included as attributes. | +| `click` | The text of the link that was clicked on. | The `to` attribute represents the URL clicked to. | +| `create` | The `name` of the software being created; if no `name` property is requested by the given Software Template, then the string `new {templateName}` is used instead. | The context holds an `entityRef`, set to the template's ref (e.g. `template:default/template-name`). The `value` represents the number of minutes saved by running the template (based on the template's `backstage.io/time-saved` annotation, if available). | +| `search` | The search term entered in any search bar component. | The context holds `searchTypes`, representing `types` constraining the search. The `value` represents the total number of search results for the query. This may not be visible if the permission framework is being used. | +| `discover` | The title of the search result that was clicked on | The `value` is the result rank. A `to` attribute is also provided. | +| `not-found` | The path of the resource that resulted in a not found page | Fired by at least TechDocs. | + +If there is an event you'd like to see captured, please [open an issue](https://github.com/backstage/backstage/issues/new?assignees=&labels=enhancement&template=feature_template.md&title=[Analytics%20Event]:%20THE+EVENT+TO+CAPTURE) describing the event you want to see and the questions it +would help you answer. Or jump to [Capturing Events](#capturing-events) to learn how +to contribute the instrumentation yourself! + +_OSS plugin maintainers: feel free to document your events in the table above._ + +## Writing Integrations + +Analytics event forwarding is implemented as a Backstage [Utility API](../utility-apis/01-index.md). The +provided API need only provide a single method `captureEvent`, which takes +an `AnalyticsEvent` object. + +A simple implementation using `AnalyticsImplementationBlueprint`: + +```ts +import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api'; + +export const acmeAnalyticsImplementation = + AnalyticsImplementationBlueprint.make({ + name: 'acme', + params: define => + define({ + deps: {}, + factory() { + return { + captureEvent: event => { + window._AcmeAnalyticsQ.push(event); + }, + }; + }, + }), + }); +``` + +In reality, you would likely want to encapsulate instantiation logic and pull +some details from configuration. A more complete example might look like: + +```ts +import { + AnalyticsApi, + AnalyticsEvent, + configApiRef, +} from '@backstage/frontend-plugin-api'; +import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api'; +import { AcmeAnalytics } from 'acme-analytics'; + +class AcmeAnalyticsImpl implements AnalyticsApi { + private constructor(accountId: number) { + AcmeAnalytics.init(accountId); + } + + static fromConfig(config) { + const accountId = config.getString('app.analytics.acme.id'); + return new AcmeAnalyticsImpl(accountId); + } + + captureEvent(event: AnalyticsEvent) { + const { action, ...rest } = event; + AcmeAnalytics.send(action, rest); + } +} + +export const acmeAnalyticsImplementation = + AnalyticsImplementationBlueprint.make({ + name: 'acme', + params: define => + define({ + deps: { configApi: configApiRef }, + factory: ({ configApi }) => AcmeAnalyticsImpl.fromConfig(configApi), + }), + }); +``` + +If you are integrating with an analytics service (as opposed to an internal +tool), consider contributing your API implementation as a plugin! + +By convention, such packages should be named +`@backstage/analytics-module-[name]`, and any configuration should be keyed +under `app.analytics.[name]`. + +### Handling User Identity + +If the analytics platform you are integrating with has a first-class concept of +user identity, you can (optionally) choose to support this by the following this +convention: + +- Allow your implementation to be instantiated with the `identityApi` as one of + its dependencies. +- Use the `userEntityRef` resolved by `identityApi`'s `getBackstageIdentity()` + method as the basis for the user ID you send to your analytics platform. + +## Capturing Events + +To instrument an event in a component, start by retrieving an analytics tracker +using the `useAnalytics()` hook provided by `@backstage/frontend-plugin-api`. The +tracker includes a `captureEvent` method which takes an `action` and a `subject` +as arguments. + +```ts +import { useAnalytics } from '@backstage/frontend-plugin-api'; + +const analytics = useAnalytics(); +analytics.captureEvent('deploy', serviceName); +``` + +### Providing Extra Attributes + +Additional dimensional `attributes` as well as a numeric `value` can be provided +on a third `options` argument if/when relevant for the event: + +```ts +analytics.captureEvent('merge', pullRequestName, { + value: pullRequestAgeInMinutes, + attributes: { + org, + repo, + }, +}); +``` + +In the above example, an event resembling the following object would be +captured: + +```json +{ + "action": "merge", + "subject": "Name of Pull Request", + "value": 60, + "attributes": { + "org": "some-org", + "repo": "some-repo" + } +} +``` + +### Providing Context for Events + +The `attributes` option is good for capturing details available to you within +the component that you're instrumenting. For capturing metadata only available +further up the react tree, or to help app integrators aggregate distinct events +by some common value, use an ``. + +```tsx +import { AnalyticsContext, useAnalytics } from '@backstage/frontend-plugin-api'; + +const MyComponent = ({ value }) => { + const analytics = useAnalytics(); + const handleClick = () => analytics.captureEvent('check', value); + return ; +}; + +const MyWrapper = () => { + return ( + + + + ); +}; +``` + +In the above example, clicking on `` would result in an analytics +event resembling: + +```json +{ + "action": "check", + "subject": "Some Value", + "context": { + "segment": "xyz" + } +} +``` + +Note that, for brevity in the example above, the context keys provided by +Backstage core (`pluginId`, `extension`, and `routeRef`) have been omitted. In +reality, those details would be included alongside any additional context +provided by you. + +Analytics contexts can be nested; their values are merged down the react tree, +allowing keys to be overwritten. + +### Event Naming Considerations + +An event is split into its constituent parts to enable analysis at various +levels of granularity. In order to maintain this flexibility at analysis-time, +it's important to keep each of these levels of detail disaggregated. + +- Avoid providing an overly specific `action`. For example, instead of + `filterEntityTable`, consider just using `filter` as the action, and allowing + `EntityTable` to be specified as part of the event's `context` (most likely + automatically as part of the `extension` in which the `filter` event was + captured). + +- On the flip side, when adding `attributes` to or `context` around an event, + look at existing events and see if the data you are capturing matches the + intention, type, or even the content of _their_ `attributes` or `context`. + For instance, it's common for events that involve the Catalog to include an + `entityRef` contextual key. Using the same keys and values in your event will + ensure that events instrumented across plugins can easily be aggregated. + +### Unit Testing Event Capture + +The `@backstage/frontend-test-utils` package includes a `MockAnalyticsApi` implementation +that you can use in your unit tests to spy on and make assertions about any +analytics events captured. + +Use it like this: + +```tsx +import { render, fireEvent, waitFor } from '@testing-library/react'; +import { analyticsApiRef } from '@backstage/frontend-plugin-api'; +import { + MockAnalyticsApi, + TestApiProvider, + wrapInTestApp, +} from '@backstage/frontend-test-utils'; + +describe('SomeComponent', () => { + it('should capture event on click', () => { + const apiSpy = new MockAnalyticsApi(); + + const { getByText } = render( + wrapInTestApp( + + + , + ), + ); + + fireEvent.click(getByText('some component text')); + + await waitFor(() => { + expect(apiSpy.getEvents()[0]).toMatchObject({ + action: 'expected action', + subject: 'expected subject', + attributes: { + foo: 'bar', + }, + }); + }); + }); +}); +``` diff --git a/docs/frontend-system/building-plugins/09-feature-flags.md b/docs/frontend-system/building-plugins/09-feature-flags.md new file mode 100644 index 00000000000000..0d3f60da106875 --- /dev/null +++ b/docs/frontend-system/building-plugins/09-feature-flags.md @@ -0,0 +1,90 @@ +--- +id: feature-flags +title: Feature Flags +sidebar_label: Feature Flags +description: Defining and using feature flags in plugins and apps +--- + +Backstage offers the ability to define feature flags inside a plugin or during application creation. This allows you to restrict parts of your plugin to those individual users who have toggled the feature flag to on. + +This page describes the process of defining, setting and reading a feature flag. If you are looking for using feature flags specifically with software templates please see [Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates#remove-sections-or-fields-based-on-feature-flags). + +## Defining a Feature Flag + +### In a plugin + +Feature flags are declared via the `featureFlags` option in `createFrontendPlugin`: + +```ts title="src/plugin.ts" +import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; + +export const examplePlugin = createFrontendPlugin({ + pluginId: 'example', + featureFlags: [ + { + name: 'show-example-feature', + description: 'Enables the new beta dashboard view', + }, + ], + extensions: [ + // ... + ], +}); +``` + +Note that the `description` property is optional. If not provided, the default "Registered in {pluginId} plugin" message is shown. + +### In the application + +Defining a feature flag in the application is done by adding feature flags in the `featureFlags` array in the +`createApp()` function call: + +```ts title="packages/app/src/App.tsx" +import { createApp } from '@backstage/frontend-defaults'; + +const app = createApp({ + // ... + featureFlags: [ + { + name: 'tech-radar', + description: 'Enables the tech radar plugin', + }, + ], + // ... +}); +``` + +## Enabling Feature Flags + +Feature flags are defaulted to off and can be updated by individual users in the backstage interface. These are set by navigating to the page under `Settings` > `Feature Flags`. + +The user's selection is saved in the user's browser local storage. Once a feature flag is toggled it may be required for a user to refresh the page to see the change. + +## FeatureFlagged Component + +The easiest way to control content based on the state of a feature flag is to use the [FeatureFlagged](https://backstage.io/api/stable/functions/_backstage_core-app-api.FeatureFlagged.html) component. + +```ts +import { FeatureFlagged } from '@backstage/core-app-api'; + +... + + + + + + + + +``` + +## Evaluating Feature Flag State + +It is also possible to query a feature flag using the [FeatureFlags Api](https://backstage.io/api/stable/interfaces/_backstage_core-plugin-api.index.FeatureFlagsApi.html). + +```ts +import { useApi, featureFlagsApiRef } from '@backstage/frontend-plugin-api'; + +const featureFlagsApi = useApi(featureFlagsApiRef); +const isOn = featureFlagsApi.isActive('show-example-feature'); +``` diff --git a/docs/frontend-system/building-plugins/10-integrating-search-into-plugins.md b/docs/frontend-system/building-plugins/10-integrating-search-into-plugins.md new file mode 100644 index 00000000000000..892d3b80a0ef9e --- /dev/null +++ b/docs/frontend-system/building-plugins/10-integrating-search-into-plugins.md @@ -0,0 +1,550 @@ +--- +id: integrating-search-into-plugins +title: Integrating Search into a Plugin +sidebar_label: Integrating Search +description: How to integrate Search into a Backstage plugin +--- + +The Backstage Search Platform was designed to give plugin developers the APIs +and interfaces needed to offer search experiences within their plugins, while +abstracting away (and instead empowering application integrators to choose) the +specific underlying search technologies. + +On this page, you'll find concepts and tutorials for leveraging the Backstage +Search Platform in your plugin. + +## Providing data to the search platform + +### Create a collator + +> Knowing what a [collator](../../features/search/concepts.md#collators) is will help you as you build it out. + +Imagine you have a plugin that is responsible for storing FAQ snippets in a database. You want other engineers to be able to easily find your questions and answers. So that means you want them to be indexed by the search platform. Lets say the FAQ snippets can be viewed at a URL like `backstage.example.biz/faq-snippets`. + +The search platform provides an interface (`DocumentCollatorFactory` from package `@backstage/plugin-search-common`) that allows you to do exactly that. It works by registering each of your entries as a "document" that later represents one search result each. + +> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices. + +#### 1. Create a collator module package + +In order to add an FAQ collator to the Backstage index registry we have to create a [plugin module](https://backstage.io/docs/backend-system/building-plugins-and-modules/index#modules), and the best way to do this is to create it in a separate package, e.g., `plugins/search-backend-module-faq-snippets-collator`, using the `yarn new` command: + +1. Access your Backstage project root directory and run `yarn new`; +2. When asked about what do you want to create, please select `backend-module` and hit enter; +3. Input `search` as the plugin ID and `faq-snippets-collator` as the module ID; +4. A `search-backend-module-faq-snippets-collator` folder should have been created in your project's "plugins" directory. + +#### 2. Install the collator dependencies + +We will use some libraries in the module, so let's add them to your plugin module dependencies: + +```sh +# Create a new branch using Git command-line +git checkout -b tutorials/new-faq-snippets-collator + +# Install the package containing the interface +yarn workspace @internal/backstage-plugin-search-backend-module-faq-snippets-collator add @backstage/plugin-search-common @backstage/plugin-search-backend-node +``` + +#### 3. Use Backstage App configuration + +Your new collator could benefit from using configuration directly from the Backstage `app-config.yaml` file which is located on the project's root folder: + +```yaml +faq: + baseUrl: https://backstage.example.biz/faq-snippets +``` + +It is optional to define a schedule for the collator to run, or else it defaults to the value in the collator factory code (See [5. Implement the collator factory](#5-implement-the-collator-factory)): + +```yaml +faq: + baseUrl: https://backstage.example.biz/faq-snippets + /* highlight-add-start */ + schedule: + # supports cron, ISO duration, "human duration" as used in code + frequency: { minutes: 30 } + # supports ISO duration, "human duration" as used in code + timeout: { minutes: 3 } + /* highlight-add-end */ +``` + +#### 4. Define the collator document type + +Before we can start generating documents from our FAQ entries, we first have to define a document type containing all necessary information we need to later display our entry as search result. The package `@backstage/plugin-search-common` we installed earlier contains a type `IndexableDocument` that we can extend. + +Create a new file `plugins/search-backend-module-faq-snippets-collator/src/types.ts` and paste the following below: + +```ts +import { IndexableDocument } from '@backstage/plugin-search-common'; + +export interface FaqSnippetDocument extends IndexableDocument { + answered_by: string; +} +``` + +#### 5. Implement the collator factory + +Imagine your FAQs can be retrieved at the URL `https://backstage.example.biz/faq-snippets` with following JSON response format: + +```json +{ + "items": [ + { + "id": 42, + "question": "What is The Answer to the Ultimate Question of Life, the Universe, and Everything?", + "answer": "Forty-two", + "user": "Deep Thought" + } + ] +} +``` + +Below we provide an example implementation of how the FAQ collator factory could look like using our new document type, placed in the `plugins/search-backend-module-faq-snippets-collator/src/factory.ts` file: + +```ts +import { Readable } from 'stream'; +import { + LoggerService, + RootConfigService, +} from '@backstage/backend-plugin-api'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { FaqSnippetDocument } from './types'; + +const DEFAULT_BASE_URL = 'https://backstage.example.biz/faq-snippets'; + +export class FaqSnippetsCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'faq-snippets'; + private readonly baseUrl: string; + private readonly logger: LoggerService; + + private constructor(options: { logger: LoggerService; baseUrl: string }) { + this.baseUrl = options.baseUrl; + this.logger = options.logger; + } + + static fromConfig( + config: RootConfigService, + options: { + logger: LoggerService; + }, + ) { + const baseUrl = config.getOptionalString('faq.baseUrl') ?? DEFAULT_BASE_URL; + return new FaqSnippetsCollatorFactory({ ...options, baseUrl }); + } + + async getCollator() { + return Readable.from(this.execute()); + } + + async *execute(): AsyncGenerator { + this.logger.info(`Fetching faq snippets from ${this.baseUrl}`); + const response = await fetch(this.baseUrl); + const data = await response.json(); + for (const faq of data.items) { + yield { + title: faq.question, + location: `/faq-snippets/${faq.id}`, + text: faq.answer, + answered_by: faq.user, + }; + } + } +} +``` + +#### 6. Implement the collator plugin module + +Now we have to connect the search backend plugin with our FAQ Snippets collator factory, so replace the `module.ts` file with the content below: + +```ts title='plugins/search-backend-module-faq-snippets-collator/src/module.ts' +import { + coreServices, + createBackendModule, + readSchedulerServiceTaskScheduleDefinitionFromConfig, +} from '@backstage/backend-plugin-api'; +import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; +import { FaqSnippetsCollatorFactory } from './factory'; + +export const searchFaqSnippetsCollatorModule = createBackendModule({ + pluginId: 'search', + moduleId: 'faq-snippets-collator', + register(env) { + env.registerInit({ + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + scheduler: coreServices.scheduler, + indexRegistry: searchIndexRegistryExtensionPoint, + }, + async init({ config, logger, scheduler, indexRegistry }) { + const defaultSchedule = { + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, + }; + + const schedule = config.has('faq.schedule') + ? readSchedulerServiceTaskScheduleDefinitionFromConfig( + config.getConfig('faq.schedule'), + ) + : defaultSchedule; + + indexRegistry.addCollator({ + schedule: scheduler.createScheduledTaskRunner(schedule), + factory: FaqSnippetsCollatorFactory.fromConfig(config, { logger }), + }); + }, + }); + }, +}); +``` + +In the fragment above, the module is registered, and when the Backstage backend initializes it, it adds the FAQ Snippets collator to the search index registry. Now let's export the module as default from the `index.ts` file: + +```ts title='plugins/search-backend-module-faq-snippets-collator/src/index.ts' +export { searchFaqSnippetsCollatorModule as default } from './module'; +``` + +#### 7. Install the collator module + +The newly created module should be added to the backend package dependencies as follows: + +```sh +yarn --cwd packages/backend add @internal/backstage-plugin-search-backend-module-faq-snippets-collator +``` + +After that, install the module on your Backstage backend instance: + +```ts title='packages/backend/src/index.ts' +import { createBackend } from '@backstage/backend-defaults'; +//... +const backend = createBackend(); +// Installing the search backend plugin +backend.add(import('@backstage/plugin-search-backend')); +// Installing the newly created faq snippets collator module +backend.add( + import( + '@internal/backstage-plugin-search-backend-module-faq-snippets-collator' + ), +); +//... +backend.start(); +``` + +#### 8. Testing the collator code + +To verify your implementation works as expected make sure to add tests for it. For your convenience, there is the [`TestPipeline`](https://backstage.io/api/stable/classes/_backstage_plugin-search-backend-node.index.TestPipeline.html) utility that emulates a pipeline into which you can integrate your custom collator. + +Look at [DefaultTechDocsCollatorFactory test](https://github.com/backstage/backstage/blob/de294ce5c410c9eb56da6870a1fab795268f60e3/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts), for an example. + +You can also check out the documentation on [how to test Backstage plugin modules](../../backend-system/building-plugins-and-modules/02-testing.md). + +#### 9. Running the collator locally + +Run `yarn start` in the root folder of your Backstage project and look for logs like these: + +```sh +[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Registered scheduled task: search_index_faq_snippets, {"version":2,"cadence":"PT10M","initialDelayDuration":"PT3S","timeoutAfterDuration":"PT15M"} task=search_index_faq_snippets +[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Collating documents for faq-snippets via FaqSnippetsCollatorFactory documentType=faq-snippets +[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Fetching faq snippets from https://backstage.example.biz/faq-snippets +[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Collating documents for faq-snippets succeeded documentType=faq-snippets +``` + +It means that the collator task was started and completed successfully. Visit http://localhost:3000, log in, select the 'All' tab, and type in one of your snippets title in the search box. + +Results should appear for snippets. + +#### 10. Make your plugins collator discoverable for others + +If you want to make your collator discoverable for other adopters, add it to the list of [plugins integrated to search](https://backstage.io/docs/features/search/#plugins-integrated-with-backstage-search). + +## Building a search experience into your plugin + +While the core Search plugin offers components and extensions that empower app +integrators to compose a global search experience, you may find that you want a +narrower search experience just within your plugin. This could be as literal as +an autocomplete-style search bar focused on documents provided by your plugin +(for example, the [TechDocsSearch](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/search/components/TechDocsSearch.tsx) +component), or as abstract as a widget that presents a list of links that +are contextually related to something else on the page. + +### Search Result List Items + +In the new frontend system, search result list items are created using `SearchResultListItemBlueprint` from `@backstage/plugin-search-react/alpha`. This blueprint lets you register custom result renderers that the search plugin will use when displaying results of your document type. + +```tsx +import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha'; + +const mySearchResultListItem = SearchResultListItemBlueprint.make({ + name: 'my-plugin', + params: { + lineClamp: 3, + predicate: result => result.type === 'my-plugin', + component: async () => { + const { MySearchResultListItem } = await import( + './components/MySearchResultListItem' + ); + return MySearchResultListItem; + }, + }, +}); +``` + +### Search Experience Concepts + +Knowing these high-level concepts will help you as you craft your in-plugin +search experience. + +- All search experiences must be wrapped in a ``, which + is provided by `@backstage/plugin-search-react`. This context keeps track + of state necessary to perform search queries and display any results. As + inputs to the query are updated (e.g. a `term` or `filter` values), the + updated query is executed and `results` are refreshed. Check out the + [SearchContextValue](https://backstage.io/api/stable/types/_backstage_plugin-search-react.index.SearchContextValue.html) + for details. +- The aforementioned state can be modified and/or consumed via the + `useSearch()` hook, also exported by `@backstage/plugin-search-react`. +- For more literal search experiences, reusable components are available + to import and compose into a cohesive experience in your plugin (e.g. + `` or ``). You can see all such + components in [Backstage's storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchbar--default). + +### Search Experience Tutorials + +The following tutorials make use of packages and plugins that you may not yet +have as dependencies for your plugin; be sure to add them before you use them! + +- [`@backstage/plugin-search-react`](https://www.npmjs.com/package/@backstage/plugin-search-react) - A + package containing components, hooks, and types that are shared across all + frontend plugins, including plugins like yours! +- [`@backstage/plugin-search`](https://www.npmjs.com/package/@backstage/plugin-search) - The + main search plugin, used by app integrators to compose global search + experiences. +- [`@backstage/core-components`](https://www.npmjs.com/package/@backstage/core-components) - A + package containing generic components useful for a variety of experiences + built in Backstage. + +#### Improved "404" page experience + +Imagine you have a plugin that allows users to manage _widgets_. Perhaps they +can be viewed at a URL like `backstage.example.biz/widgets/{widgetName}`. +At some point, a widget is renamed, and links to that widget's page from +chat systems, wikis, or browser bookmarks become stale, resulting in errors or +404s. + +What if instead of showing a broken page or the generic "looks like someone +dropped the mic" 404 page, you showed a list of possibly related widgets? + +```javascript +import { Link } from '@backstage/core-components'; +import { SearchResult } from '@backstage/plugin-search'; +import { SearchContextProvider } from '@backstage/plugin-search-react'; + +export const Widget404Page = ({ widgetName }) => { + // Supplying this to runs a pre-filtered search with + // the given widgetName as the search term, focused on search result of type + // "widget" with no other filters. + const preFiltered = { + term: widgetName, + types: ['widget'], + filters: {}, + }; + + return ( + + {/* The component allows us to iterate through results and + display them in whatever way fits best! */} + + {({ results }) => ( + {results.map(({ document }) => ( + + {document.title} + + ))} + )} + + + ); +); +``` + +Not all search experiences require user input! As you can see, it's possible to +leverage the Backstage Search Platform's frontend framework without necessarily +giving users input controls. + +#### Simple search page + +Of course, it's also possible to provide a more fully featured search +experience in your plugin. The simplest way is to leverage reusable components +provided by the `@backstage/plugin-search` package, like this: + +```javascript +import { useProfile } from '@internal/api'; +import { + Content, + ContentHeader, + PageWithHeader, +} from '@backstage/core-components'; +import { SearchBar, SearchResult } from '@backstage/plugin-search'; +import { SearchContextProvider } from '@backstage/plugin-search-react'; + +export const ManageMyWidgets = () => { + const { primaryTeam } = useProfile(); + // In this example, note how we are pre-filtering results down to a specific + // owner field value (the currently logged-in user's team), but allowing the + // search term to be controlled by the user via the component. + const preFiltered = { + types: ['widget'], + term: '', + filters: { + owner: primaryTeam, + }, + }; + + return ( + + + + + + + {/* Render results here, just like above */} + + + + + ); +}; +``` + +#### Custom search control surfaces + +If the reusable search components provided by `@backstage/plugin-search` aren't +adequate, no problem! There's an API in place that you can use to author your +own components to control the various parts of the search context. + +```javascript +import { useSearch } from '@backstage/plugin-search-react'; +import ChipInput from 'material-ui-chip-input'; + +export const CustomChipFilter = ({ name }) => { + const { filters, setFilters } = useSearch(); + const chipValues = filters[name] || []; + + // When a chip value is changed, update the filters value by calling the + // setFilters function from the search context. + const handleChipChange = (chip, index) => { + // There may be filters set for other fields. Be sure to maintain them. + setFilters(prevState => { + const { [name]: filter = [], ...others } = prevState; + + if (index === undefined) { + filter.push(chip); + } else { + filter.splice(index, 1); + } + + return { ...others, [name]: filter }; + }); + }; + + return ( + + ); +}; +``` + +Check out the [SearchContextValue type](https://github.com/backstage/backstage/blob/master/plugins/search-react/src/context/SearchContext.tsx) +for more details on what methods and values are available for manipulating and +reading the search context. + +If you produce something generic and reusable, consider contributing your +component upstream so that all users of the Backstage Search Platform can +benefit. Issues and pull requests welcome. + +#### Custom search results + +Search results throughout Backstage are rendered as lists so that list items can easily be customized; although a [default result list item](https://backstage.io/storybook/?path=/story/plugins-search-defaultresultlistitem--default) is available, plugins are in the best position to provide custom result list items that surface relevant information only known to the plugin. + +The example below imagines `YourCustomSearchResult` as a type of search result that contains associated `tags` which could be rendered as chips below the title/text. + +```tsx +import { Link } from '@backstage/core-components'; +import { useAnalytics } from '@backstage/frontend-plugin-api'; +import { ResultHighlight } from '@backstage/plugin-search-common'; +import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; + +type CustomSearchResultListItemProps = { + result: YourCustomSearchResult; + rank?: number; + highlight?: ResultHighlight; +}; + +export const CustomSearchResultListItem = ( + props: CustomSearchResultListItemProps, +) => { + const { title, text, location, tags } = props.result; + + const analytics = useAnalytics(); + const handleClick = () => { + analytics.captureEvent('discover', title, { + attributes: { to: location }, + value: props.rank, + }); + }; + + return ( + + + + + ) : ( + title + ) + } + secondary={ + highlight?.fields?.text ? ( + + ) : ( + text + ) + } + /> + {tags && + tags.map((tag: string) => ( + + ))} + + + + + ); +}; +``` + +The optional use of the `` component makes it possible to highlight relevant parts of the result based on the user's search query. + +**Note on Analytics**: In order for app integrators to track and improve search experiences across Backstage, it's important for them to understand when and what users search for, as well as what they click on after searching. When providing a custom result component, it's your responsibility as a plugin developer to instrument it according to search analytics conventions. In particular: + +- You must use the `analytics.captureEvent` method, from the `useAnalytics()` hook (detailed [plugin analytics docs are here](./08-analytics.md)). +- You must ensure that the action of the event, representing a click on a search result item, is `discover`, and the subject is the `title` of the clicked result. In addition, the `to` attribute should be set to the result's `location`, and the `value` of the event must be set to the `rank` (passed in as a prop). +- You must ensure that the aforementioned `captureEvent` method is called when a user clicks the link; you should further ensure that the `noTrack` prop is added to the link (which disables default link click tracking, in favor of this custom instrumentation). + +For other examples and inspiration on custom result list items, check out the [``](https://github.com/backstage/backstage/blob/c981e83/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx) or [``](https://github.com/backstage/backstage/blob/c981e83/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx) components. diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index 692849c1eb98a7..a758d63ebe98e3 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -6,7 +6,7 @@ description: Measuring usage of your Backstage instance. :::caution Legacy Documentation -This section is part of the legacy plugins documentation. The concepts and events described here apply to both the old and new frontend systems. However, the "Writing Integrations" section shows examples for both the old frontend system (`createApiFactory`) and the new frontend system (`AnalyticsImplementationBlueprint`). For new development, prefer the new frontend system approach. See also [Utility APIs](../frontend-system/utility-apis/01-index.md). +This section is part of the legacy plugins documentation. For the new frontend system version, see [Plugin Analytics](../frontend-system/building-plugins/08-analytics.md). The concepts and events described here apply to both the old and new frontend systems. ::: diff --git a/docs/plugins/feature-flags.md b/docs/plugins/feature-flags.md index 5d35e3c057c829..25251e3db51cc4 100644 --- a/docs/plugins/feature-flags.md +++ b/docs/plugins/feature-flags.md @@ -6,7 +6,7 @@ description: Details the process of defining setting and reading a feature flag. :::caution Legacy Documentation -This page describes feature flags using the **old frontend system** APIs (`createPlugin` from `@backstage/core-plugin-api` and `createApp` from `@backstage/app-defaults`). For the new frontend system, feature flags are declared via the `featureFlags` option in `createFrontendPlugin` — see [Building Frontend Plugins](../frontend-system/building-plugins/01-index.md). The `FeatureFlagged` component and `featureFlagsApiRef` work the same way in both systems. +This page describes feature flags using the **old frontend system** APIs (`createPlugin` from `@backstage/core-plugin-api` and `createApp` from `@backstage/app-defaults`). For the new frontend system version, see [Feature Flags](../frontend-system/building-plugins/09-feature-flags.md). The `FeatureFlagged` component and `featureFlagsApiRef` work the same way in both systems. ::: diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md index c27192e05c5e91..39dd490799afca 100644 --- a/docs/plugins/integrating-search-into-plugins.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -6,7 +6,7 @@ description: How to integrate Search into a Backstage plugin :::caution Legacy Documentation -This section is part of the legacy plugins documentation. The backend search collator patterns described here use the new backend system and are still current. The frontend search experience examples use the old frontend system APIs. For the new frontend system, search result list items are created using `SearchResultListItemBlueprint` — see [Common Extension Blueprints](../frontend-system/building-plugins/03-common-extension-blueprints.md). +This section is part of the legacy plugins documentation. For the new frontend system version, see [Integrating Search into a Plugin](../frontend-system/building-plugins/10-integrating-search-into-plugins.md). The backend search collator patterns described here use the new backend system and are still current. The frontend search experience examples use the old frontend system APIs. ::: diff --git a/docs/plugins/internationalization.md b/docs/plugins/internationalization.md index 7466d17efa53f8..67a6b962a0fd7c 100644 --- a/docs/plugins/internationalization.md +++ b/docs/plugins/internationalization.md @@ -6,7 +6,7 @@ description: Documentation on adding internationalization to plugins and apps :::caution Legacy Documentation -This section is part of the legacy plugins documentation. The i18n APIs (`createTranslationRef`, `useTranslationRef`) work the same way in both the old and new frontend systems. The "For an application developer" section already shows the new frontend system approach using `createTranslationResource` from `@backstage/frontend-plugin-api`. +This section is part of the legacy plugins documentation. For the new frontend system version, see [Internationalization](../frontend-system/building-plugins/07-internationalization.md). The i18n APIs (`createTranslationRef`, `useTranslationRef`) work the same way in both the old and new frontend systems. ::: diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 1be013af7d7d9f..2b04fbe1a5720b 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -410,18 +410,6 @@ export default { sidebarElementWithIndex({ label: 'Okta' }, ['integrations/okta/org']), ], ), - sidebarElementWithIndex( - { - label: 'OpenAPI', - description: - 'Work with OpenAPI specifications and generate clients.', - }, - [ - 'openapi/01-getting-started', - 'openapi/generate-client', - 'openapi/test-case-validation', - ], - ), sidebarElementWithIndex( { label: 'Configuration', @@ -555,6 +543,10 @@ export default { 'frontend-system/building-plugins/common-extension-blueprints', 'frontend-system/building-plugins/built-in-data-refs', 'frontend-system/building-plugins/migrating', + 'frontend-system/building-plugins/internationalization', + 'frontend-system/building-plugins/analytics', + 'frontend-system/building-plugins/feature-flags', + 'frontend-system/building-plugins/integrating-search-into-plugins', ], ), sidebarElementWithIndex( @@ -624,6 +616,18 @@ export default { 'conf/user-interface/sidebar', ], ), + sidebarElementWithIndex( + { + label: 'OpenAPI', + description: + 'Work with OpenAPI specifications and generate clients.', + }, + [ + 'openapi/01-getting-started', + 'openapi/generate-client', + 'openapi/test-case-validation', + ], + ), ], ), sidebarElementWithIndex( diff --git a/mkdocs.yml b/mkdocs.yml index 2030d87c450628..ecc7b5890d93d6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -141,10 +141,6 @@ nav: - Locations: 'integrations/google-cloud-storage/locations.md' - LDAP: - Org Data: 'integrations/ldap/org.md' - - OpenAPI: - - Schema-first plugins with OpenAPI (Experimental): 'openapi/01-getting-started.md' - - Generate a client from your OpenAPI spec: 'openapi/generate-client.md' - - Validate your OpenAPI spec against test data: 'openapi/test-case-validation.md' - Plugins (Legacy): - Intro to plugins: 'plugins/index.md' - Create a Backstage Plugin: 'plugins/create-a-plugin.md' @@ -155,6 +151,10 @@ nav: - Plugin Analytics: 'plugins/analytics.md' - Feature Flags: 'plugins/feature-flags.md' - Internationalization (i18n): 'plugins/internationalization.md' + - OpenAPI: + - Schema-first plugins with OpenAPI (Experimental): 'openapi/01-getting-started.md' + - Generate a client from your OpenAPI spec: 'openapi/generate-client.md' + - Validate your OpenAPI spec against test data: 'openapi/test-case-validation.md' - Backends and APIs: - Proxying: 'plugins/proxying.md' - Backend plugin: 'plugins/backend-plugin.md' From f8161a67fa0c491040ab1559d7c430ed89998787 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 11:26:12 +0100 Subject: [PATCH 09/55] docs: address review feedback on migrated plugin docs - Update internationalization page to use new frontend system patterns (TranslationBlueprint from @backstage/plugin-app-react instead of createApp __experimentalTranslations) - Fix AnalyticsImplementationBlueprint import to use @backstage/plugin-app-react - Remove search integration page from frontend-system section as it belongs in the core feature search docs Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../building-plugins/01-index.md | 1 - .../07-internationalization.md | 82 ++- .../building-plugins/08-analytics.md | 4 +- .../10-integrating-search-into-plugins.md | 550 ------------------ microsite/sidebars.ts | 1 - 5 files changed, 41 insertions(+), 597 deletions(-) delete mode 100644 docs/frontend-system/building-plugins/10-integrating-search-into-plugins.md diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index 24a815ef7b9a61..d89eae87b08825 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -246,4 +246,3 @@ The following guides cover cross-cutting concerns for building frontend plugins: - [Internationalization (i18n)](./07-internationalization.md) — Adding translations to your plugin using `createTranslationRef` and `useTranslationRef`. - [Plugin Analytics](./08-analytics.md) — Instrumenting user interactions with the Analytics API using `AnalyticsImplementationBlueprint`. - [Feature Flags](./09-feature-flags.md) — Defining and using feature flags via the `featureFlags` option of `createFrontendPlugin`. -- [Integrating Search into Plugins](./10-integrating-search-into-plugins.md) — Building search experiences, collators, and custom result list items using `SearchResultListItemBlueprint`. diff --git a/docs/frontend-system/building-plugins/07-internationalization.md b/docs/frontend-system/building-plugins/07-internationalization.md index 270f1f66259214..3e882cb8e20703 100644 --- a/docs/frontend-system/building-plugins/07-internationalization.md +++ b/docs/frontend-system/building-plugins/07-internationalization.md @@ -190,56 +190,57 @@ As an app developer you can both override the default English messages of any pl ### Overriding messages -To customize specific messages without adding new languages, create a translation resource that overrides the default English messages: +To customize specific messages without adding new languages, create a translation extension using `TranslationBlueprint` from `@backstage/plugin-app-react` together with `createTranslationMessages` from `@backstage/frontend-plugin-api`: ```ts -// packages/app/src/translations/catalog.ts - -import { createTranslationResource } from '@backstage/frontend-plugin-api'; +import { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { TranslationBlueprint } from '@backstage/plugin-app-react'; import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha'; -export const catalogTranslations = createTranslationResource({ - ref: catalogTranslationRef, - translations: { - en: () => - Promise.resolve({ - default: { - 'indexPage.title': 'Service directory', - 'indexPage.createButtonTitle': 'Register new service', - }, - }), +const catalogTranslations = TranslationBlueprint.make({ + name: 'catalog-overrides', + params: { + resource: createTranslationMessages({ + ref: catalogTranslationRef, + messages: { + 'indexPage.title': 'Service directory', + 'indexPage.createButtonTitle': 'Register new service', + }, + }), }, }); ``` -Then register it in your app: +Then install it as a feature in your app: -```diff -+ import { catalogTranslations } from './translations/catalog'; +```ts +import { createApp } from '@backstage/frontend-defaults'; - const app = createApp({ -+ __experimentalTranslations: { -+ resources: [catalogTranslations], -+ }, - }) +const app = createApp({ + features: [catalogTranslations], +}); ``` You only need to include the keys you want to override — any missing keys fall back to the plugin's defaults. ### Adding language translations -To add support for additional languages, create translation resources with lazy-loaded message files for each language: +To add support for additional languages, create a translation resource with lazy-loaded message files for each language, and install it using `TranslationBlueprint`: ```ts -// packages/app/src/translations/userSettings.ts - import { createTranslationResource } from '@backstage/frontend-plugin-api'; +import { TranslationBlueprint } from '@backstage/plugin-app-react'; import { userSettingsTranslationRef } from '@backstage/plugin-user-settings/alpha'; -export const userSettingsTranslations = createTranslationResource({ - ref: userSettingsTranslationRef, - translations: { - zh: () => import('./userSettings-zh'), +const userSettingsTranslations = TranslationBlueprint.make({ + name: 'user-settings-zh', + params: { + resource: createTranslationResource({ + ref: userSettingsTranslationRef, + translations: { + zh: () => import('./userSettings-zh'), + }, + }), }, }); ``` @@ -282,17 +283,14 @@ export default { }; ``` -Register it with the available languages declared: +Install the translation extension in your app: -```diff -+ import { userSettingsTranslations } from './translations/userSettings'; +```ts +import { createApp } from '@backstage/frontend-defaults'; - const app = createApp({ -+ __experimentalTranslations: { -+ availableLanguages: ['en', 'zh'], -+ resources: [userSettingsTranslations], -+ }, - }) +const app = createApp({ + features: [userSettingsTranslations], +}); ``` Go to the Settings page — you should see language switching buttons. Switch languages to verify your translations are loaded correctly. @@ -368,16 +366,14 @@ export default [ ]; ``` -Import the generated resources in your app: +Install the generated resources as features in your app: ```ts +import { createApp } from '@backstage/frontend-defaults'; import translationResources from './translations/resources'; const app = createApp({ - __experimentalTranslations: { - availableLanguages: ['en', 'zh'], - resources: translationResources, - }, + features: translationResources, }); ``` diff --git a/docs/frontend-system/building-plugins/08-analytics.md b/docs/frontend-system/building-plugins/08-analytics.md index 08babdb61263b6..75a0aee5cde14e 100644 --- a/docs/frontend-system/building-plugins/08-analytics.md +++ b/docs/frontend-system/building-plugins/08-analytics.md @@ -87,7 +87,7 @@ an `AnalyticsEvent` object. A simple implementation using `AnalyticsImplementationBlueprint`: ```ts -import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api'; +import { AnalyticsImplementationBlueprint } from '@backstage/plugin-app-react'; export const acmeAnalyticsImplementation = AnalyticsImplementationBlueprint.make({ @@ -115,7 +115,7 @@ import { AnalyticsEvent, configApiRef, } from '@backstage/frontend-plugin-api'; -import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api'; +import { AnalyticsImplementationBlueprint } from '@backstage/plugin-app-react'; import { AcmeAnalytics } from 'acme-analytics'; class AcmeAnalyticsImpl implements AnalyticsApi { diff --git a/docs/frontend-system/building-plugins/10-integrating-search-into-plugins.md b/docs/frontend-system/building-plugins/10-integrating-search-into-plugins.md deleted file mode 100644 index 892d3b80a0ef9e..00000000000000 --- a/docs/frontend-system/building-plugins/10-integrating-search-into-plugins.md +++ /dev/null @@ -1,550 +0,0 @@ ---- -id: integrating-search-into-plugins -title: Integrating Search into a Plugin -sidebar_label: Integrating Search -description: How to integrate Search into a Backstage plugin ---- - -The Backstage Search Platform was designed to give plugin developers the APIs -and interfaces needed to offer search experiences within their plugins, while -abstracting away (and instead empowering application integrators to choose) the -specific underlying search technologies. - -On this page, you'll find concepts and tutorials for leveraging the Backstage -Search Platform in your plugin. - -## Providing data to the search platform - -### Create a collator - -> Knowing what a [collator](../../features/search/concepts.md#collators) is will help you as you build it out. - -Imagine you have a plugin that is responsible for storing FAQ snippets in a database. You want other engineers to be able to easily find your questions and answers. So that means you want them to be indexed by the search platform. Lets say the FAQ snippets can be viewed at a URL like `backstage.example.biz/faq-snippets`. - -The search platform provides an interface (`DocumentCollatorFactory` from package `@backstage/plugin-search-common`) that allows you to do exactly that. It works by registering each of your entries as a "document" that later represents one search result each. - -> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices. - -#### 1. Create a collator module package - -In order to add an FAQ collator to the Backstage index registry we have to create a [plugin module](https://backstage.io/docs/backend-system/building-plugins-and-modules/index#modules), and the best way to do this is to create it in a separate package, e.g., `plugins/search-backend-module-faq-snippets-collator`, using the `yarn new` command: - -1. Access your Backstage project root directory and run `yarn new`; -2. When asked about what do you want to create, please select `backend-module` and hit enter; -3. Input `search` as the plugin ID and `faq-snippets-collator` as the module ID; -4. A `search-backend-module-faq-snippets-collator` folder should have been created in your project's "plugins" directory. - -#### 2. Install the collator dependencies - -We will use some libraries in the module, so let's add them to your plugin module dependencies: - -```sh -# Create a new branch using Git command-line -git checkout -b tutorials/new-faq-snippets-collator - -# Install the package containing the interface -yarn workspace @internal/backstage-plugin-search-backend-module-faq-snippets-collator add @backstage/plugin-search-common @backstage/plugin-search-backend-node -``` - -#### 3. Use Backstage App configuration - -Your new collator could benefit from using configuration directly from the Backstage `app-config.yaml` file which is located on the project's root folder: - -```yaml -faq: - baseUrl: https://backstage.example.biz/faq-snippets -``` - -It is optional to define a schedule for the collator to run, or else it defaults to the value in the collator factory code (See [5. Implement the collator factory](#5-implement-the-collator-factory)): - -```yaml -faq: - baseUrl: https://backstage.example.biz/faq-snippets - /* highlight-add-start */ - schedule: - # supports cron, ISO duration, "human duration" as used in code - frequency: { minutes: 30 } - # supports ISO duration, "human duration" as used in code - timeout: { minutes: 3 } - /* highlight-add-end */ -``` - -#### 4. Define the collator document type - -Before we can start generating documents from our FAQ entries, we first have to define a document type containing all necessary information we need to later display our entry as search result. The package `@backstage/plugin-search-common` we installed earlier contains a type `IndexableDocument` that we can extend. - -Create a new file `plugins/search-backend-module-faq-snippets-collator/src/types.ts` and paste the following below: - -```ts -import { IndexableDocument } from '@backstage/plugin-search-common'; - -export interface FaqSnippetDocument extends IndexableDocument { - answered_by: string; -} -``` - -#### 5. Implement the collator factory - -Imagine your FAQs can be retrieved at the URL `https://backstage.example.biz/faq-snippets` with following JSON response format: - -```json -{ - "items": [ - { - "id": 42, - "question": "What is The Answer to the Ultimate Question of Life, the Universe, and Everything?", - "answer": "Forty-two", - "user": "Deep Thought" - } - ] -} -``` - -Below we provide an example implementation of how the FAQ collator factory could look like using our new document type, placed in the `plugins/search-backend-module-faq-snippets-collator/src/factory.ts` file: - -```ts -import { Readable } from 'stream'; -import { - LoggerService, - RootConfigService, -} from '@backstage/backend-plugin-api'; -import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; -import { FaqSnippetDocument } from './types'; - -const DEFAULT_BASE_URL = 'https://backstage.example.biz/faq-snippets'; - -export class FaqSnippetsCollatorFactory implements DocumentCollatorFactory { - public readonly type: string = 'faq-snippets'; - private readonly baseUrl: string; - private readonly logger: LoggerService; - - private constructor(options: { logger: LoggerService; baseUrl: string }) { - this.baseUrl = options.baseUrl; - this.logger = options.logger; - } - - static fromConfig( - config: RootConfigService, - options: { - logger: LoggerService; - }, - ) { - const baseUrl = config.getOptionalString('faq.baseUrl') ?? DEFAULT_BASE_URL; - return new FaqSnippetsCollatorFactory({ ...options, baseUrl }); - } - - async getCollator() { - return Readable.from(this.execute()); - } - - async *execute(): AsyncGenerator { - this.logger.info(`Fetching faq snippets from ${this.baseUrl}`); - const response = await fetch(this.baseUrl); - const data = await response.json(); - for (const faq of data.items) { - yield { - title: faq.question, - location: `/faq-snippets/${faq.id}`, - text: faq.answer, - answered_by: faq.user, - }; - } - } -} -``` - -#### 6. Implement the collator plugin module - -Now we have to connect the search backend plugin with our FAQ Snippets collator factory, so replace the `module.ts` file with the content below: - -```ts title='plugins/search-backend-module-faq-snippets-collator/src/module.ts' -import { - coreServices, - createBackendModule, - readSchedulerServiceTaskScheduleDefinitionFromConfig, -} from '@backstage/backend-plugin-api'; -import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; -import { FaqSnippetsCollatorFactory } from './factory'; - -export const searchFaqSnippetsCollatorModule = createBackendModule({ - pluginId: 'search', - moduleId: 'faq-snippets-collator', - register(env) { - env.registerInit({ - deps: { - config: coreServices.rootConfig, - logger: coreServices.logger, - scheduler: coreServices.scheduler, - indexRegistry: searchIndexRegistryExtensionPoint, - }, - async init({ config, logger, scheduler, indexRegistry }) { - const defaultSchedule = { - frequency: { minutes: 10 }, - timeout: { minutes: 15 }, - initialDelay: { seconds: 3 }, - }; - - const schedule = config.has('faq.schedule') - ? readSchedulerServiceTaskScheduleDefinitionFromConfig( - config.getConfig('faq.schedule'), - ) - : defaultSchedule; - - indexRegistry.addCollator({ - schedule: scheduler.createScheduledTaskRunner(schedule), - factory: FaqSnippetsCollatorFactory.fromConfig(config, { logger }), - }); - }, - }); - }, -}); -``` - -In the fragment above, the module is registered, and when the Backstage backend initializes it, it adds the FAQ Snippets collator to the search index registry. Now let's export the module as default from the `index.ts` file: - -```ts title='plugins/search-backend-module-faq-snippets-collator/src/index.ts' -export { searchFaqSnippetsCollatorModule as default } from './module'; -``` - -#### 7. Install the collator module - -The newly created module should be added to the backend package dependencies as follows: - -```sh -yarn --cwd packages/backend add @internal/backstage-plugin-search-backend-module-faq-snippets-collator -``` - -After that, install the module on your Backstage backend instance: - -```ts title='packages/backend/src/index.ts' -import { createBackend } from '@backstage/backend-defaults'; -//... -const backend = createBackend(); -// Installing the search backend plugin -backend.add(import('@backstage/plugin-search-backend')); -// Installing the newly created faq snippets collator module -backend.add( - import( - '@internal/backstage-plugin-search-backend-module-faq-snippets-collator' - ), -); -//... -backend.start(); -``` - -#### 8. Testing the collator code - -To verify your implementation works as expected make sure to add tests for it. For your convenience, there is the [`TestPipeline`](https://backstage.io/api/stable/classes/_backstage_plugin-search-backend-node.index.TestPipeline.html) utility that emulates a pipeline into which you can integrate your custom collator. - -Look at [DefaultTechDocsCollatorFactory test](https://github.com/backstage/backstage/blob/de294ce5c410c9eb56da6870a1fab795268f60e3/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts), for an example. - -You can also check out the documentation on [how to test Backstage plugin modules](../../backend-system/building-plugins-and-modules/02-testing.md). - -#### 9. Running the collator locally - -Run `yarn start` in the root folder of your Backstage project and look for logs like these: - -```sh -[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Registered scheduled task: search_index_faq_snippets, {"version":2,"cadence":"PT10M","initialDelayDuration":"PT3S","timeoutAfterDuration":"PT15M"} task=search_index_faq_snippets -[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Collating documents for faq-snippets via FaqSnippetsCollatorFactory documentType=faq-snippets -[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Fetching faq snippets from https://backstage.example.biz/faq-snippets -[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Collating documents for faq-snippets succeeded documentType=faq-snippets -``` - -It means that the collator task was started and completed successfully. Visit http://localhost:3000, log in, select the 'All' tab, and type in one of your snippets title in the search box. - -Results should appear for snippets. - -#### 10. Make your plugins collator discoverable for others - -If you want to make your collator discoverable for other adopters, add it to the list of [plugins integrated to search](https://backstage.io/docs/features/search/#plugins-integrated-with-backstage-search). - -## Building a search experience into your plugin - -While the core Search plugin offers components and extensions that empower app -integrators to compose a global search experience, you may find that you want a -narrower search experience just within your plugin. This could be as literal as -an autocomplete-style search bar focused on documents provided by your plugin -(for example, the [TechDocsSearch](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/search/components/TechDocsSearch.tsx) -component), or as abstract as a widget that presents a list of links that -are contextually related to something else on the page. - -### Search Result List Items - -In the new frontend system, search result list items are created using `SearchResultListItemBlueprint` from `@backstage/plugin-search-react/alpha`. This blueprint lets you register custom result renderers that the search plugin will use when displaying results of your document type. - -```tsx -import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha'; - -const mySearchResultListItem = SearchResultListItemBlueprint.make({ - name: 'my-plugin', - params: { - lineClamp: 3, - predicate: result => result.type === 'my-plugin', - component: async () => { - const { MySearchResultListItem } = await import( - './components/MySearchResultListItem' - ); - return MySearchResultListItem; - }, - }, -}); -``` - -### Search Experience Concepts - -Knowing these high-level concepts will help you as you craft your in-plugin -search experience. - -- All search experiences must be wrapped in a ``, which - is provided by `@backstage/plugin-search-react`. This context keeps track - of state necessary to perform search queries and display any results. As - inputs to the query are updated (e.g. a `term` or `filter` values), the - updated query is executed and `results` are refreshed. Check out the - [SearchContextValue](https://backstage.io/api/stable/types/_backstage_plugin-search-react.index.SearchContextValue.html) - for details. -- The aforementioned state can be modified and/or consumed via the - `useSearch()` hook, also exported by `@backstage/plugin-search-react`. -- For more literal search experiences, reusable components are available - to import and compose into a cohesive experience in your plugin (e.g. - `` or ``). You can see all such - components in [Backstage's storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchbar--default). - -### Search Experience Tutorials - -The following tutorials make use of packages and plugins that you may not yet -have as dependencies for your plugin; be sure to add them before you use them! - -- [`@backstage/plugin-search-react`](https://www.npmjs.com/package/@backstage/plugin-search-react) - A - package containing components, hooks, and types that are shared across all - frontend plugins, including plugins like yours! -- [`@backstage/plugin-search`](https://www.npmjs.com/package/@backstage/plugin-search) - The - main search plugin, used by app integrators to compose global search - experiences. -- [`@backstage/core-components`](https://www.npmjs.com/package/@backstage/core-components) - A - package containing generic components useful for a variety of experiences - built in Backstage. - -#### Improved "404" page experience - -Imagine you have a plugin that allows users to manage _widgets_. Perhaps they -can be viewed at a URL like `backstage.example.biz/widgets/{widgetName}`. -At some point, a widget is renamed, and links to that widget's page from -chat systems, wikis, or browser bookmarks become stale, resulting in errors or -404s. - -What if instead of showing a broken page or the generic "looks like someone -dropped the mic" 404 page, you showed a list of possibly related widgets? - -```javascript -import { Link } from '@backstage/core-components'; -import { SearchResult } from '@backstage/plugin-search'; -import { SearchContextProvider } from '@backstage/plugin-search-react'; - -export const Widget404Page = ({ widgetName }) => { - // Supplying this to runs a pre-filtered search with - // the given widgetName as the search term, focused on search result of type - // "widget" with no other filters. - const preFiltered = { - term: widgetName, - types: ['widget'], - filters: {}, - }; - - return ( - - {/* The component allows us to iterate through results and - display them in whatever way fits best! */} - - {({ results }) => ( - {results.map(({ document }) => ( - - {document.title} - - ))} - )} - - - ); -); -``` - -Not all search experiences require user input! As you can see, it's possible to -leverage the Backstage Search Platform's frontend framework without necessarily -giving users input controls. - -#### Simple search page - -Of course, it's also possible to provide a more fully featured search -experience in your plugin. The simplest way is to leverage reusable components -provided by the `@backstage/plugin-search` package, like this: - -```javascript -import { useProfile } from '@internal/api'; -import { - Content, - ContentHeader, - PageWithHeader, -} from '@backstage/core-components'; -import { SearchBar, SearchResult } from '@backstage/plugin-search'; -import { SearchContextProvider } from '@backstage/plugin-search-react'; - -export const ManageMyWidgets = () => { - const { primaryTeam } = useProfile(); - // In this example, note how we are pre-filtering results down to a specific - // owner field value (the currently logged-in user's team), but allowing the - // search term to be controlled by the user via the component. - const preFiltered = { - types: ['widget'], - term: '', - filters: { - owner: primaryTeam, - }, - }; - - return ( - - - - - - - {/* Render results here, just like above */} - - - - - ); -}; -``` - -#### Custom search control surfaces - -If the reusable search components provided by `@backstage/plugin-search` aren't -adequate, no problem! There's an API in place that you can use to author your -own components to control the various parts of the search context. - -```javascript -import { useSearch } from '@backstage/plugin-search-react'; -import ChipInput from 'material-ui-chip-input'; - -export const CustomChipFilter = ({ name }) => { - const { filters, setFilters } = useSearch(); - const chipValues = filters[name] || []; - - // When a chip value is changed, update the filters value by calling the - // setFilters function from the search context. - const handleChipChange = (chip, index) => { - // There may be filters set for other fields. Be sure to maintain them. - setFilters(prevState => { - const { [name]: filter = [], ...others } = prevState; - - if (index === undefined) { - filter.push(chip); - } else { - filter.splice(index, 1); - } - - return { ...others, [name]: filter }; - }); - }; - - return ( - - ); -}; -``` - -Check out the [SearchContextValue type](https://github.com/backstage/backstage/blob/master/plugins/search-react/src/context/SearchContext.tsx) -for more details on what methods and values are available for manipulating and -reading the search context. - -If you produce something generic and reusable, consider contributing your -component upstream so that all users of the Backstage Search Platform can -benefit. Issues and pull requests welcome. - -#### Custom search results - -Search results throughout Backstage are rendered as lists so that list items can easily be customized; although a [default result list item](https://backstage.io/storybook/?path=/story/plugins-search-defaultresultlistitem--default) is available, plugins are in the best position to provide custom result list items that surface relevant information only known to the plugin. - -The example below imagines `YourCustomSearchResult` as a type of search result that contains associated `tags` which could be rendered as chips below the title/text. - -```tsx -import { Link } from '@backstage/core-components'; -import { useAnalytics } from '@backstage/frontend-plugin-api'; -import { ResultHighlight } from '@backstage/plugin-search-common'; -import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; - -type CustomSearchResultListItemProps = { - result: YourCustomSearchResult; - rank?: number; - highlight?: ResultHighlight; -}; - -export const CustomSearchResultListItem = ( - props: CustomSearchResultListItemProps, -) => { - const { title, text, location, tags } = props.result; - - const analytics = useAnalytics(); - const handleClick = () => { - analytics.captureEvent('discover', title, { - attributes: { to: location }, - value: props.rank, - }); - }; - - return ( - - - - - ) : ( - title - ) - } - secondary={ - highlight?.fields?.text ? ( - - ) : ( - text - ) - } - /> - {tags && - tags.map((tag: string) => ( - - ))} - - - - - ); -}; -``` - -The optional use of the `` component makes it possible to highlight relevant parts of the result based on the user's search query. - -**Note on Analytics**: In order for app integrators to track and improve search experiences across Backstage, it's important for them to understand when and what users search for, as well as what they click on after searching. When providing a custom result component, it's your responsibility as a plugin developer to instrument it according to search analytics conventions. In particular: - -- You must use the `analytics.captureEvent` method, from the `useAnalytics()` hook (detailed [plugin analytics docs are here](./08-analytics.md)). -- You must ensure that the action of the event, representing a click on a search result item, is `discover`, and the subject is the `title` of the clicked result. In addition, the `to` attribute should be set to the result's `location`, and the `value` of the event must be set to the `rank` (passed in as a prop). -- You must ensure that the aforementioned `captureEvent` method is called when a user clicks the link; you should further ensure that the `noTrack` prop is added to the link (which disables default link click tracking, in favor of this custom instrumentation). - -For other examples and inspiration on custom result list items, check out the [``](https://github.com/backstage/backstage/blob/c981e83/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx) or [``](https://github.com/backstage/backstage/blob/c981e83/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx) components. diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 2b04fbe1a5720b..ba8276c70d206f 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -546,7 +546,6 @@ export default { 'frontend-system/building-plugins/internationalization', 'frontend-system/building-plugins/analytics', 'frontend-system/building-plugins/feature-flags', - 'frontend-system/building-plugins/integrating-search-into-plugins', ], ), sidebarElementWithIndex( From 0f30e2b8868c7d71282cac7fc400cdc32da7f1ba Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 11:36:49 +0100 Subject: [PATCH 10/55] docs: remove broken link to non-existent search integration doc The caution block referenced a new frontend system version of the search integration doc that was never created. Remove the dead link while keeping the rest of the legacy notice intact. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- docs/plugins/integrating-search-into-plugins.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md index 39dd490799afca..cbd9c89e14dedc 100644 --- a/docs/plugins/integrating-search-into-plugins.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -6,7 +6,7 @@ description: How to integrate Search into a Backstage plugin :::caution Legacy Documentation -This section is part of the legacy plugins documentation. For the new frontend system version, see [Integrating Search into a Plugin](../frontend-system/building-plugins/10-integrating-search-into-plugins.md). The backend search collator patterns described here use the new backend system and are still current. The frontend search experience examples use the old frontend system APIs. +This section is part of the legacy plugins documentation. The backend search collator patterns described here use the new backend system and are still current. The frontend search experience examples use the old frontend system APIs. ::: From d9655bcb16bf23527cda319b695781d570418734 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 14:55:18 +0100 Subject: [PATCH 11/55] Remove empty publish-private doc page The page only contained a TODO placeholder and was never filled in. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- docs/plugins/publish-private.md | 13 ------------- microsite/sidebars.ts | 1 - mkdocs.yml | 1 - 3 files changed, 15 deletions(-) delete mode 100644 docs/plugins/publish-private.md diff --git a/docs/plugins/publish-private.md b/docs/plugins/publish-private.md deleted file mode 100644 index b5bb1b937a7998..00000000000000 --- a/docs/plugins/publish-private.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -id: publish-private -title: Publish private -description: Documentation on How to Publish private ---- - -:::caution Legacy Documentation - -This section is part of the legacy plugins documentation. - -::: - -## TODO diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index ba8276c70d206f..93579baf422a11 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -715,7 +715,6 @@ export default { sidebarElementWithIndex( { label: 'Publishing', description: 'Publishing your plugins.' }, [ - 'plugins/publish-private', 'plugins/add-to-directory', 'plugins/plugin-directory-audit', ], diff --git a/mkdocs.yml b/mkdocs.yml index ecc7b5890d93d6..19c20984960884 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -162,7 +162,6 @@ nav: - Testing: - Testing with Jest: 'plugins/testing.md' - Publishing: - - Publish private: 'plugins/publish-private.md' - Add to Directory: 'plugins/add-to-directory.md' - Observability: 'plugins/observability.md' - New Backend System: 'plugins/new-backend-system.md' From c5cbc84e7f9d8773c15692afc8e21071fa7c2e1a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 15:25:33 +0100 Subject: [PATCH 12/55] Fix prettier formatting in microsite sidebars Signed-off-by: Patrik Oldsberg Made-with: Cursor --- microsite/sidebars.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 93579baf422a11..1f4eb7e0ef769b 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -714,10 +714,7 @@ export default { ), sidebarElementWithIndex( { label: 'Publishing', description: 'Publishing your plugins.' }, - [ - 'plugins/add-to-directory', - 'plugins/plugin-directory-audit', - ], + ['plugins/add-to-directory', 'plugins/plugin-directory-audit'], ), 'plugins/observability', 'plugins/new-backend-system', From e8ba654360b60c85927f2c43f0041a450e174599 Mon Sep 17 00:00:00 2001 From: David Festal Date: Tue, 17 Mar 2026 15:14:50 +0100 Subject: [PATCH 13/55] chore(dyamic-frontend-feature-loader): update README with the new `bundle` command Signed-off-by: David Festal --- .changeset/tangy-toys-carry.md | 5 +++++ packages/frontend-dynamic-feature-loader/README.md | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 .changeset/tangy-toys-carry.md diff --git a/.changeset/tangy-toys-carry.md b/.changeset/tangy-toys-carry.md new file mode 100644 index 00000000000000..77d8243c7c8e1f --- /dev/null +++ b/.changeset/tangy-toys-carry.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-dynamic-feature-loader': patch +--- + +Update the README of the `frontend-dynamic-feature-loader` package to mention the new `backstage-cli package bundle` command. diff --git a/packages/frontend-dynamic-feature-loader/README.md b/packages/frontend-dynamic-feature-loader/README.md index a1ab46d7b04227..a3802c39017a21 100644 --- a/packages/frontend-dynamic-feature-loader/README.md +++ b/packages/frontend-dynamic-feature-loader/README.md @@ -22,11 +22,12 @@ The frontend feature loader provided in this package works hand-in-hand with the Adding a frontend plugin (with new frontend system support, possibly in alpha support), is straightforward and consists in: -- building the frontend plugin with the `frontend-dynamic-container` role, which enables the module federation support, and packages the plugin as a module remote -- copying the frontend package folder, with the `dist` folder generated during the build, to the dynamic plugins root folder of the Backstage installation (defined by the `dynamicPlugins.rootDirectory` configuration value, which is usually set as `dynamic-plugins-root`). +- bundling the frontend plugin with the [`backstage-cli package bundle`](../../docs/tooling/cli/03-commands.md#package-bundle) command, thus producing a self-contained bundle based on Module Federation. +- copying the bundle folder into the Backstage installation dynamic plugins root folder for dynamic loading. -So from a frontend plugin package folder, you would use the following command: +So from a `my-backstage-plugin` frontend plugin package folder, you would use the following command: ```bash -yarn build --role frontend-dynamic-container && cp -R $(pwd) /dynamic-plugins-root/ +yarn backstage-cli package bundle --output-destination /path/to/dynamic-plugins-root +# Creates a self-contained bundle in the /path/to/dynamic-plugins-root/my-backstage-plugin/ sub-folder ``` From 64b7132d40efbf4c0324045f2b6be4619d9ead6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 17 Mar 2026 15:37:24 +0100 Subject: [PATCH 14/55] fix(cli): exclude __fixtures__ from coverage in cli-module-build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the transform tests were moved from @backstage/cli to @backstage/cli-module-build, the coveragePathIgnorePatterns config was not carried over. This caused test failures in CI when running with --coverage, as Jest tried to instrument the fixture files containing fake node_modules packages. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- packages/cli-module-build/package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index d13fa4bac06c5c..9b32eca0fee0f6 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -34,6 +34,11 @@ "postpack": "backstage-cli package postpack", "test": "backstage-cli package test" }, + "jest": { + "coveragePathIgnorePatterns": [ + "/__fixtures__/" + ] + }, "dependencies": { "@backstage/cli-common": "workspace:^", "@backstage/cli-node": "workspace:^", From b02ed6ee796985568f0f2e93513888a89fc8cca3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 15:39:42 +0100 Subject: [PATCH 15/55] docs: replace core-plugin-api and core-app-api references with frontend-plugin-api Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../building-plugins/08-analytics.md | 2 +- .../building-plugins/09-feature-flags.md | 32 ++++++------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/docs/frontend-system/building-plugins/08-analytics.md b/docs/frontend-system/building-plugins/08-analytics.md index 75a0aee5cde14e..0b51b5ae1ab1da 100644 --- a/docs/frontend-system/building-plugins/08-analytics.md +++ b/docs/frontend-system/building-plugins/08-analytics.md @@ -55,7 +55,7 @@ learn how to contribute the integration yourself! [matomo]: https://github.com/backstage/community-plugins/blob/main/workspaces/analytics/plugins/analytics-module-matomo/README.md [add-tool]: https://github.com/backstage/backstage/issues/new?assignees=&labels=plugin&template=plugin_template.md&title=%5BAnalytics+Module%5D+THE+ANALYTICS+TOOL+TO+INTEGRATE [int-howto]: #writing-integrations -[analytics-api-type]: https://backstage.io/api/stable/types/_backstage_core-plugin-api.index.AnalyticsApi.html +[analytics-api-type]: https://backstage.io/api/stable/types/_backstage_frontend-plugin-api.index.AnalyticsApi.html [generic-http]: https://github.com/pfeifferj/backstage-plugin-analytics-generic/blob/main/README.md ## Key Events diff --git a/docs/frontend-system/building-plugins/09-feature-flags.md b/docs/frontend-system/building-plugins/09-feature-flags.md index 0d3f60da106875..06ee56be24e0d3 100644 --- a/docs/frontend-system/building-plugins/09-feature-flags.md +++ b/docs/frontend-system/building-plugins/09-feature-flags.md @@ -60,31 +60,19 @@ Feature flags are defaulted to off and can be updated by individual users in the The user's selection is saved in the user's browser local storage. Once a feature flag is toggled it may be required for a user to refresh the page to see the change. -## FeatureFlagged Component - -The easiest way to control content based on the state of a feature flag is to use the [FeatureFlagged](https://backstage.io/api/stable/functions/_backstage_core-app-api.FeatureFlagged.html) component. - -```ts -import { FeatureFlagged } from '@backstage/core-app-api'; - -... - - - - - - - - -``` - ## Evaluating Feature Flag State -It is also possible to query a feature flag using the [FeatureFlags Api](https://backstage.io/api/stable/interfaces/_backstage_core-plugin-api.index.FeatureFlagsApi.html). +You can query a feature flag using the [FeatureFlagsApi](https://backstage.io/api/stable/interfaces/_backstage_frontend-plugin-api.index.FeatureFlagsApi.html): -```ts +```tsx import { useApi, featureFlagsApiRef } from '@backstage/frontend-plugin-api'; -const featureFlagsApi = useApi(featureFlagsApiRef); -const isOn = featureFlagsApi.isActive('show-example-feature'); +function MyComponent() { + const featureFlagsApi = useApi(featureFlagsApiRef); + + if (featureFlagsApi.isActive('show-example-feature')) { + return ; + } + return ; +} ``` From 85346e51b252846d2af373d09b3237bc7bc7dd85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 17 Mar 2026 15:40:41 +0100 Subject: [PATCH 16/55] chore(cli): remove unused coveragePathIgnorePatterns from @backstage/cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The __fixtures__ directory that required this exclusion was moved to @backstage/cli-module-build. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- packages/cli/package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 4ba287a9efec9f..d34a1860cab5d7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,11 +41,6 @@ "ext": "ts", "watch": "./src" }, - "jest": { - "coveragePathIgnorePatterns": [ - "/__fixtures__/" - ] - }, "dependencies": { "@backstage/cli-common": "workspace:^", "@backstage/cli-defaults": "workspace:^", From a6735c33b13bd20dfe9560d0f16112dfc688cdbe Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Feb 2026 22:38:49 +0100 Subject: [PATCH 17/55] create-app: Make new frontend system the default The new frontend system is now the default template when creating a new Backstage app. The previous `--next` flag has been replaced with a `--legacy` flag that can be used to create an app using the old frontend system instead. Signed-off-by: Patrik Oldsberg --- .changeset/new-frontend-system-default.md | 5 +++++ docs/frontend-system/building-apps/01-index.md | 4 ++-- docs/frontend-system/building-apps/08-migrating.md | 2 +- packages/create-app/cli-report.md | 2 +- packages/create-app/src/createApp.test.ts | 10 +++++----- packages/create-app/src/createApp.ts | 8 ++++---- packages/create-app/src/index.ts | 2 +- 7 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 .changeset/new-frontend-system-default.md diff --git a/.changeset/new-frontend-system-default.md b/.changeset/new-frontend-system-default.md new file mode 100644 index 00000000000000..eeef22ce35df94 --- /dev/null +++ b/.changeset/new-frontend-system-default.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': minor +--- + +**BREAKING**: The new frontend system is now the default template when creating a new Backstage app. The previous `--next` flag has been replaced with a `--legacy` flag that can be used to create an app using the old frontend system instead. diff --git a/docs/frontend-system/building-apps/01-index.md b/docs/frontend-system/building-apps/01-index.md index cfef547afc90c4..bedf42033f2610 100644 --- a/docs/frontend-system/building-apps/01-index.md +++ b/docs/frontend-system/building-apps/01-index.md @@ -20,10 +20,10 @@ The create-app CLI requires Node.js Active LTS Release, see the [prerequisites d ```sh # The command bellow creates a Backstage App inside the current folder. # The name of the app-folder is the name that was provided when prompted. -npx @backstage/create-app@latest --next +npx @backstage/create-app@latest ``` -Using the `--next` flag will result in a Backstage app using the New Frontend System which will be further explained in the sections below. +This will create a Backstage app using the new frontend system which will be further explained in the sections below. ## The app instance diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index 9f18067a55b692..d0e05d98ae7fa3 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -899,7 +899,7 @@ It's encouraged that once you switch over to using the new frontend system, that This practice is also pretty important early on, as it's going to help you get familiar with the practices of the new frontend system. -When creating a new Backstage app with `create-app` and using the `--next` flag you'll automatically get these choices in the `yarn new` command, but if you want to bring these templates to an older app, you can add the following to your root `package.json`: +When creating a new Backstage app with `create-app` you'll automatically get these choices in the `yarn new` command, but if you want to bring these templates to an older app, you can add the following to your root `package.json`: ```json { diff --git a/packages/create-app/cli-report.md b/packages/create-app/cli-report.md index 88e19d05c6560a..f9286a628cbfa8 100644 --- a/packages/create-app/cli-report.md +++ b/packages/create-app/cli-report.md @@ -8,7 +8,7 @@ Usage: backstage-create-app [options] Options: - --next + --legacy --path [directory] --skip-install --template-path [directory] diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index c38186b1d814d7..a032f9bb3b8bc6 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -69,7 +69,7 @@ describe('command entrypoint', () => { expect(tryInitGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(templatingMock.mock.lastCall?.[0]).toEqual( - findOwnPaths(__dirname).resolve('templates/default-app'), + findOwnPaths(__dirname).resolve('templates/next-app'), ); expect(templatingMock.mock.lastCall?.[1]).toContain( path.join(tmpdir(), 'MyApp'), @@ -85,20 +85,20 @@ describe('command entrypoint', () => { expect(tryInitGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(templatingMock.mock.lastCall?.[0]).toEqual( - findOwnPaths(__dirname).resolve('templates/default-app'), + findOwnPaths(__dirname).resolve('templates/next-app'), ); expect(templatingMock.mock.lastCall?.[1]).toEqual('myDirectory'); expect(buildAppMock).toHaveBeenCalled(); }); - it('should call expected tasks when `--next` is supplied', async () => { - const cmd = { next: true } as unknown as Command; + it('should call expected tasks when `--legacy` is supplied', async () => { + const cmd = { legacy: true } as unknown as Command; await createApp(cmd); expect(checkAppExistsMock).toHaveBeenCalled(); expect(tryInitGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(templatingMock.mock.lastCall?.[0]).toEqual( - findOwnPaths(__dirname).resolve('templates/next-app'), + findOwnPaths(__dirname).resolve('templates/default-app'), ); expect(templatingMock.mock.lastCall?.[1]).toContain( path.join(tmpdir(), 'MyApp'), diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 64db2e52abbe45..68c27d3a1a1b43 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -63,12 +63,12 @@ export default async (opts: OptionValues): Promise => { }, ]); - // Pick the built-in template based on the --next flag + // Pick the built-in template based on the --legacy flag /* eslint-disable-next-line no-restricted-syntax */ const ownPaths = findOwnPaths(__dirname); - const builtInTemplate = opts.next - ? ownPaths.resolve('templates/next-app') - : ownPaths.resolve('templates/default-app'); + const builtInTemplate = opts.legacy + ? ownPaths.resolve('templates/default-app') + : ownPaths.resolve('templates/next-app'); // Use `--template-path` argument as template when specified. Otherwise, use the default template. const templateDir = opts.templatePath diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index cfa448aa1a4a41..ff3ef29b186ee5 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -31,7 +31,7 @@ const main = (argv: string[]) => { .name('backstage-create-app') .version(version) .description('Creates a new app in a new directory or specified path') - .option('--next', 'Use the next generation of the app template') + .option('--legacy', 'Use the legacy version of the app template') .option( '--path [directory]', 'Location to store the app defaulting to a new folder with the app name', From 08d977071566ad91974c83277b85481c7b3eae94 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Feb 2026 13:55:06 +0100 Subject: [PATCH 18/55] cli: Rename frontend plugin templates and add auto-detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed the CLI templates for frontend plugins: - new-frontend-plugin → frontend-plugin - new-frontend-plugin-module → frontend-plugin-module - frontend-plugin (legacy) → legacy-frontend-plugin Added auto-detection logic that checks packages/app/package.json to determine which frontend system the app uses. When using default templates, only the appropriate frontend plugin template is shown: - Apps with @backstage/frontend-defaults see the new system templates - Apps with @backstage/app-defaults see the legacy template Both templates display as "frontend-plugin" to users, so existing workflows are preserved while automatically using the correct template. Signed-off-by: Patrik Oldsberg --- .../cli-new-frontend-templates-default.md | 16 ++++ .../building-apps/08-migrating.md | 34 +------- docs/tooling/cli/04-templates.md | 3 + .../src/lib/defaultTemplates.ts | 2 + .../preparation/loadPortableTemplateConfig.ts | 79 ++++++++++++++++++- .../.eslintrc.js.hbs | 0 .../README.md.hbs | 0 .../package.json.hbs | 0 .../portable-template.yaml | 0 .../src/index.ts.hbs | 0 .../src/module.tsx.hbs | 0 .../src/setupTests.ts | 0 .../templates/frontend-plugin/README.md.hbs | 9 ++- .../dev/index.tsx | 0 .../frontend-plugin/package.json.hbs | 15 ++-- .../frontend-plugin/portable-template.yaml | 1 - .../ExampleComponent.test.tsx.hbs | 2 +- .../ExampleFetchComponent.test.tsx.hbs | 2 +- .../frontend-plugin/src/index.ts.hbs | 2 +- .../src/plugin.tsx.hbs | 0 .../src/routes.ts | 0 .../.eslintrc.js.hbs | 0 .../legacy-frontend-plugin/README.md.hbs | 13 +++ .../dev/index.tsx.hbs | 0 .../package.json.hbs | 15 ++-- .../portable-template.yaml | 6 ++ .../ExampleComponent.test.tsx.hbs | 2 +- .../ExampleComponent/ExampleComponent.tsx.hbs | 0 .../src/components/ExampleComponent/index.ts | 0 .../ExampleFetchComponent.test.tsx.hbs | 2 +- .../ExampleFetchComponent.tsx.hbs | 0 .../components/ExampleFetchComponent/index.ts | 0 .../legacy-frontend-plugin/src/index.ts.hbs | 1 + .../src/plugin.test.ts.hbs | 0 .../src/plugin.ts.hbs | 0 .../src/routes.ts.hbs | 0 .../src/setupTests.ts | 0 .../new-frontend-plugin/README.md.hbs | 20 ----- .../portable-template.yaml | 5 -- .../new-frontend-plugin/src/index.ts.hbs | 1 - .../templates/default-app/package.json.hbs | 22 ++++++ .../templates/next-app/package.json.hbs | 15 +--- 42 files changed, 168 insertions(+), 99 deletions(-) create mode 100644 .changeset/cli-new-frontend-templates-default.md rename packages/cli-module-new/templates/{new-frontend-plugin-module => frontend-plugin-module}/.eslintrc.js.hbs (100%) rename packages/cli-module-new/templates/{new-frontend-plugin-module => frontend-plugin-module}/README.md.hbs (100%) rename packages/cli-module-new/templates/{new-frontend-plugin-module => frontend-plugin-module}/package.json.hbs (100%) rename packages/cli-module-new/templates/{new-frontend-plugin-module => frontend-plugin-module}/portable-template.yaml (100%) rename packages/cli-module-new/templates/{new-frontend-plugin-module => frontend-plugin-module}/src/index.ts.hbs (100%) rename packages/cli-module-new/templates/{new-frontend-plugin-module => frontend-plugin-module}/src/module.tsx.hbs (100%) rename packages/cli-module-new/templates/{new-frontend-plugin-module => frontend-plugin-module}/src/setupTests.ts (100%) rename packages/cli-module-new/templates/{new-frontend-plugin => frontend-plugin}/dev/index.tsx (100%) rename packages/cli-module-new/templates/{new-frontend-plugin => frontend-plugin}/src/plugin.tsx.hbs (100%) rename packages/cli-module-new/templates/{new-frontend-plugin => frontend-plugin}/src/routes.ts (100%) rename packages/cli-module-new/templates/{new-frontend-plugin => legacy-frontend-plugin}/.eslintrc.js.hbs (100%) create mode 100644 packages/cli-module-new/templates/legacy-frontend-plugin/README.md.hbs rename packages/cli-module-new/templates/{frontend-plugin => legacy-frontend-plugin}/dev/index.tsx.hbs (100%) rename packages/cli-module-new/templates/{new-frontend-plugin => legacy-frontend-plugin}/package.json.hbs (72%) create mode 100644 packages/cli-module-new/templates/legacy-frontend-plugin/portable-template.yaml rename packages/cli-module-new/templates/{new-frontend-plugin => legacy-frontend-plugin}/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs (94%) rename packages/cli-module-new/templates/{new-frontend-plugin => legacy-frontend-plugin}/src/components/ExampleComponent/ExampleComponent.tsx.hbs (100%) rename packages/cli-module-new/templates/{new-frontend-plugin => legacy-frontend-plugin}/src/components/ExampleComponent/index.ts (100%) rename packages/cli-module-new/templates/{new-frontend-plugin => legacy-frontend-plugin}/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs (91%) rename packages/cli-module-new/templates/{new-frontend-plugin => legacy-frontend-plugin}/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs (100%) rename packages/cli-module-new/templates/{new-frontend-plugin => legacy-frontend-plugin}/src/components/ExampleFetchComponent/index.ts (100%) create mode 100644 packages/cli-module-new/templates/legacy-frontend-plugin/src/index.ts.hbs rename packages/cli-module-new/templates/{new-frontend-plugin => legacy-frontend-plugin}/src/plugin.test.ts.hbs (100%) rename packages/cli-module-new/templates/{frontend-plugin => legacy-frontend-plugin}/src/plugin.ts.hbs (100%) rename packages/cli-module-new/templates/{frontend-plugin => legacy-frontend-plugin}/src/routes.ts.hbs (100%) rename packages/cli-module-new/templates/{new-frontend-plugin => legacy-frontend-plugin}/src/setupTests.ts (100%) delete mode 100644 packages/cli-module-new/templates/new-frontend-plugin/README.md.hbs delete mode 100644 packages/cli-module-new/templates/new-frontend-plugin/portable-template.yaml delete mode 100644 packages/cli-module-new/templates/new-frontend-plugin/src/index.ts.hbs diff --git a/.changeset/cli-new-frontend-templates-default.md b/.changeset/cli-new-frontend-templates-default.md new file mode 100644 index 00000000000000..0513b367803147 --- /dev/null +++ b/.changeset/cli-new-frontend-templates-default.md @@ -0,0 +1,16 @@ +--- +'@backstage/cli': minor +--- + +**BREAKING**: The CLI templates for frontend plugins have been renamed: + +- `new-frontend-plugin` → `frontend-plugin` +- `new-frontend-plugin-module` → `frontend-plugin-module` +- `frontend-plugin` (legacy) → `legacy-frontend-plugin` + +To smooth out this breaking change, the CLI now auto-detects which frontend system your app uses based on the dependencies in `packages/app/package.json`. When using the default templates (no explicit `templates` configuration): + +- Apps using `@backstage/frontend-defaults` will see the new frontend system templates (`frontend-plugin`, `frontend-plugin-module`) +- Apps using `@backstage/app-defaults` will see the legacy template (displayed as `frontend-plugin`) + +This means existing projects that haven't migrated to the new frontend system will continue to create legacy plugins by default, while new projects will get the new frontend system templates. If you have explicit template configuration in your `package.json`, it will be used as-is without any auto-detection. diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index d0e05d98ae7fa3..ceba8fd8681818 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -899,39 +899,7 @@ It's encouraged that once you switch over to using the new frontend system, that This practice is also pretty important early on, as it's going to help you get familiar with the practices of the new frontend system. -When creating a new Backstage app with `create-app` you'll automatically get these choices in the `yarn new` command, but if you want to bring these templates to an older app, you can add the following to your root `package.json`: - -```json -{ - ... - "scripts": { - ... - "new": "backstage-cli new" - }, - "backstage": { - "cli": { - "new": { - "globals": { - "license": "UNLICENSED" - }, - "templates": [ - "@backstage/cli-module-new/templates/new-frontend-plugin", - "@backstage/cli-module-new/templates/new-frontend-plugin-module", - "@backstage/cli-module-new/templates/backend-plugin", - "@backstage/cli-module-new/templates/backend-plugin-module", - "@backstage/cli-module-new/templates/plugin-web-library", - "@backstage/cli-module-new/templates/plugin-node-library", - "@backstage/cli-module-new/templates/plugin-common-library", - "@backstage/cli-module-new/templates/web-library", - "@backstage/cli-module-new/templates/node-library", - "@backstage/cli-module-new/templates/catalog-provider-module", - "@backstage/cli-module-new/templates/scaffolder-backend-module" - ] - } - } - } -} -``` +The `yarn new` command now defaults to the new frontend system templates for frontend plugins. If you have an older app that was created before this change, you can simply update the `@backstage/cli-module-new` package to get access to the new templates. ## Troubleshooting diff --git a/docs/tooling/cli/04-templates.md b/docs/tooling/cli/04-templates.md index 552076ed665ce6..69b89a51c7542f 100644 --- a/docs/tooling/cli/04-templates.md +++ b/docs/tooling/cli/04-templates.md @@ -83,6 +83,8 @@ When defining the `templates` array it will override the default set of template "new": { "templates": [ "@backstage/cli-module-new/templates/frontend-plugin", + "@backstage/cli-module-new/templates/frontend-plugin-module", + "@backstage/cli-module-new/templates/legacy-frontend-plugin", "@backstage/cli-module-new/templates/backend-plugin", "@backstage/cli-module-new/templates/backend-plugin-module", "@backstage/cli-module-new/templates/plugin-web-library", @@ -90,6 +92,7 @@ When defining the `templates` array it will override the default set of template "@backstage/cli-module-new/templates/plugin-common-library", "@backstage/cli-module-new/templates/web-library", "@backstage/cli-module-new/templates/node-library", + "@backstage/cli-module-new/templates/cli-module", "@backstage/cli-module-new/templates/catalog-provider-module", "@backstage/cli-module-new/templates/scaffolder-backend-module" ] diff --git a/packages/cli-module-new/src/lib/defaultTemplates.ts b/packages/cli-module-new/src/lib/defaultTemplates.ts index e8b8e7eb70c4a9..9bfd7f74edde74 100644 --- a/packages/cli-module-new/src/lib/defaultTemplates.ts +++ b/packages/cli-module-new/src/lib/defaultTemplates.ts @@ -16,6 +16,8 @@ export const defaultTemplates = [ '@backstage/cli-module-new/templates/frontend-plugin', + '@backstage/cli-module-new/templates/frontend-plugin-module', + '@backstage/cli-module-new/templates/legacy-frontend-plugin', '@backstage/cli-module-new/templates/backend-plugin', '@backstage/cli-module-new/templates/backend-plugin-module', '@backstage/cli-module-new/templates/plugin-web-library', diff --git a/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts index 50ffda751649e7..3541d5a4cf522e 100644 --- a/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts +++ b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts @@ -15,9 +15,13 @@ */ import fs from 'fs-extra'; -import { resolve as resolvePath, dirname, isAbsolute } from 'node:path'; +import { + resolve as resolvePath, + dirname, + isAbsolute, + join, +} from 'node:path'; import { targetPaths } from '@backstage/cli-common'; - import { defaultTemplates } from '../defaultTemplates'; import { PortableTemplateConfig, @@ -29,6 +33,60 @@ import { z } from 'zod'; import { fromZodError } from 'zod-validation-error/v3'; import { ForwardedError } from '@backstage/errors'; +type FrontendSystem = 'new' | 'legacy' | 'unknown'; + +async function detectFrontendSystem(basePath: string): Promise { + const appPkgPath = join(basePath, 'packages', 'app', 'package.json'); + + try { + const appPkgJson = await fs.readJson(appPkgPath); + const deps = { + ...appPkgJson.dependencies, + ...appPkgJson.devDependencies, + }; + + if ( + deps['@backstage/frontend-defaults'] || + deps['@backstage/frontend-app-api'] + ) { + return 'new'; + } + if (deps['@backstage/app-defaults'] || deps['@backstage/core-app-api']) { + return 'legacy'; + } + } catch { + // App package doesn't exist or can't be read + } + + return 'unknown'; +} + +// Templates to exclude based on frontend system detection (by path, not name) +const newFrontendTemplates = [ + '@backstage/cli-module-new/templates/frontend-plugin', + '@backstage/cli-module-new/templates/frontend-plugin-module', +]; +const legacyFrontendTemplates = [ + '@backstage/cli-module-new/templates/legacy-frontend-plugin', +]; + +function filterTemplateEntriesForFrontendSystem( + entries: Array<{ pointer: PortableTemplatePointer; rawPointer: string }>, + frontendSystem: FrontendSystem, +): Array<{ pointer: PortableTemplatePointer; rawPointer: string }> { + if (frontendSystem === 'unknown') { + return entries; + } + + if (frontendSystem === 'new') { + // Filter out legacy frontend templates + return entries.filter(e => !legacyFrontendTemplates.includes(e.rawPointer)); + } + + // Legacy system - filter out new frontend templates + return entries.filter(e => !newFrontendTemplates.includes(e.rawPointer)); +} + const defaults = { license: 'Apache-2.0', version: '0.1.0', @@ -105,7 +163,9 @@ export async function loadPortableTemplateConfig( const config = parsed.data.backstage?.cli?.new; const basePath = dirname(pkgPath); - const templatePointerEntries = await Promise.all( + const isUsingDefaultTemplates = !config?.templates; + + let templatePointerEntries = await Promise.all( (config?.templates ?? defaultTemplates).map(async rawPointer => { try { const templatePath = resolveLocalTemplatePath(rawPointer, basePath); @@ -121,6 +181,17 @@ export async function loadPortableTemplateConfig( }), ); + // Auto-filter frontend templates based on detected frontend system. + // This must happen before the conflict check since both the new and legacy + // frontend plugin templates have the same name, but only one will be shown. + if (isUsingDefaultTemplates) { + const frontendSystem = await detectFrontendSystem(basePath); + templatePointerEntries = filterTemplateEntriesForFrontendSystem( + templatePointerEntries, + frontendSystem, + ); + } + const templateNameConflicts = new Map(); for (const { pointer, rawPointer } of templatePointerEntries) { const conflict = templateNameConflicts.get(pointer.name); @@ -143,7 +214,7 @@ export async function loadPortableTemplateConfig( ); return { - isUsingDefaultTemplates: !config?.templates, + isUsingDefaultTemplates, templatePointers: templatePointerEntries.map(({ pointer }) => pointer), license: overrides.license ?? config?.globals?.license ?? defaults.license, version: overrides.version ?? config?.globals?.version ?? defaults.version, diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/.eslintrc.js.hbs b/packages/cli-module-new/templates/frontend-plugin-module/.eslintrc.js.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/.eslintrc.js.hbs rename to packages/cli-module-new/templates/frontend-plugin-module/.eslintrc.js.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/README.md.hbs b/packages/cli-module-new/templates/frontend-plugin-module/README.md.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/README.md.hbs rename to packages/cli-module-new/templates/frontend-plugin-module/README.md.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/package.json.hbs b/packages/cli-module-new/templates/frontend-plugin-module/package.json.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/package.json.hbs rename to packages/cli-module-new/templates/frontend-plugin-module/package.json.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/portable-template.yaml b/packages/cli-module-new/templates/frontend-plugin-module/portable-template.yaml similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/portable-template.yaml rename to packages/cli-module-new/templates/frontend-plugin-module/portable-template.yaml diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/src/index.ts.hbs b/packages/cli-module-new/templates/frontend-plugin-module/src/index.ts.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/src/index.ts.hbs rename to packages/cli-module-new/templates/frontend-plugin-module/src/index.ts.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/src/module.tsx.hbs b/packages/cli-module-new/templates/frontend-plugin-module/src/module.tsx.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/src/module.tsx.hbs rename to packages/cli-module-new/templates/frontend-plugin-module/src/module.tsx.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin-module/src/setupTests.ts b/packages/cli-module-new/templates/frontend-plugin-module/src/setupTests.ts similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin-module/src/setupTests.ts rename to packages/cli-module-new/templates/frontend-plugin-module/src/setupTests.ts diff --git a/packages/cli-module-new/templates/frontend-plugin/README.md.hbs b/packages/cli-module-new/templates/frontend-plugin/README.md.hbs index 5eae32b7dc26ec..3f1ff3e4f7c1d9 100644 --- a/packages/cli-module-new/templates/frontend-plugin/README.md.hbs +++ b/packages/cli-module-new/templates/frontend-plugin/README.md.hbs @@ -6,7 +6,14 @@ _This plugin was created through the Backstage CLI_ ## Getting started -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/{{pluginId}}](http://localhost:3000/{{pluginId}}). +Your plugin has been added to the app in this repository, meaning you'll be able +to access it by running `yarn start` in the root directory, and then navigating +to [/{{pluginId}}](http://localhost:3000/{{pluginId}}). + +This plugin is built with Backstage's [new frontend +system](https://backstage.io/docs/frontend-system/architecture/index), and you +can find more information about building plugins in the [plugin builder +documentation](https://backstage.io/docs/frontend-system/building-plugins/index). You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. diff --git a/packages/cli-module-new/templates/new-frontend-plugin/dev/index.tsx b/packages/cli-module-new/templates/frontend-plugin/dev/index.tsx similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/dev/index.tsx rename to packages/cli-module-new/templates/frontend-plugin/dev/index.tsx diff --git a/packages/cli-module-new/templates/frontend-plugin/package.json.hbs b/packages/cli-module-new/templates/frontend-plugin/package.json.hbs index 9499d11b182d25..4a37a4fa633a27 100644 --- a/packages/cli-module-new/templates/frontend-plugin/package.json.hbs +++ b/packages/cli-module-new/templates/frontend-plugin/package.json.hbs @@ -23,7 +23,7 @@ }, "dependencies": { "@backstage/core-components": "{{versionQuery '@backstage/core-components'}}", - "@backstage/core-plugin-api": "{{versionQuery '@backstage/core-plugin-api'}}", + "@backstage/frontend-plugin-api": "{{versionQuery '@backstage/frontend-plugin-api'}}", "@backstage/theme": "{{versionQuery '@backstage/theme'}}", "@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}", "@material-ui/icons": "{{versionQuery '@material-ui/icons' '4.9.1'}}", @@ -31,22 +31,17 @@ "react-use": "{{versionQuery 'react-use' '17.2.4'}}" }, "peerDependencies": { - "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", - "react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", - "react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}" + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}" }, "devDependencies": { "@backstage/cli": "{{versionQuery '@backstage/cli'}}", - "@backstage/core-app-api": "{{versionQuery '@backstage/core-app-api'}}", - "@backstage/dev-utils": "{{versionQuery '@backstage/dev-utils'}}", - "@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}", + "@backstage/frontend-dev-utils": "{{versionQuery '@backstage/frontend-dev-utils'}}", + "@backstage/frontend-test-utils": "{{versionQuery '@backstage/frontend-test-utils'}}", "@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}", "@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}", "@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}", "msw": "{{versionQuery 'msw' '1.0.0'}}", - "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", - "react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", - "react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}" + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}" }, "files": [ "dist" diff --git a/packages/cli-module-new/templates/frontend-plugin/portable-template.yaml b/packages/cli-module-new/templates/frontend-plugin/portable-template.yaml index 3afb60eb223fd5..a70ce316f1503a 100644 --- a/packages/cli-module-new/templates/frontend-plugin/portable-template.yaml +++ b/packages/cli-module-new/templates/frontend-plugin/portable-template.yaml @@ -3,4 +3,3 @@ role: frontend-plugin description: A new frontend plugin values: pluginVar: '{{ camelCase pluginId }}Plugin' - extensionName: '{{ upperFirst ( camelCase pluginId ) }}Page' diff --git a/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index c7c20863c2fa45..d1a8a93d0b95d9 100644 --- a/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -5,7 +5,7 @@ import { screen } from '@testing-library/react'; import { registerMswTestHooks, renderInTestApp, -} from '@backstage/test-utils'; +} from '@backstage/frontend-test-utils'; describe('ExampleComponent', () => { const server = setupServer(); diff --git a/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index 820e54fa377363..c9b553287f1ce3 100644 --- a/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli-module-new/templates/frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -1,4 +1,4 @@ -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp } from '@backstage/frontend-test-utils'; import { ExampleFetchComponent } from './ExampleFetchComponent'; describe('ExampleFetchComponent', () => { diff --git a/packages/cli-module-new/templates/frontend-plugin/src/index.ts.hbs b/packages/cli-module-new/templates/frontend-plugin/src/index.ts.hbs index be4881efafc5db..0d6810ad6b94e7 100644 --- a/packages/cli-module-new/templates/frontend-plugin/src/index.ts.hbs +++ b/packages/cli-module-new/templates/frontend-plugin/src/index.ts.hbs @@ -1 +1 @@ -export { {{ pluginVar }}, {{ extensionName }} } from './plugin'; +export { {{ pluginVar }} as default } from './plugin'; diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/plugin.tsx.hbs b/packages/cli-module-new/templates/frontend-plugin/src/plugin.tsx.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/plugin.tsx.hbs rename to packages/cli-module-new/templates/frontend-plugin/src/plugin.tsx.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/routes.ts b/packages/cli-module-new/templates/frontend-plugin/src/routes.ts similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/routes.ts rename to packages/cli-module-new/templates/frontend-plugin/src/routes.ts diff --git a/packages/cli-module-new/templates/new-frontend-plugin/.eslintrc.js.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/.eslintrc.js.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/.eslintrc.js.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/.eslintrc.js.hbs diff --git a/packages/cli-module-new/templates/legacy-frontend-plugin/README.md.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/README.md.hbs new file mode 100644 index 00000000000000..5eae32b7dc26ec --- /dev/null +++ b/packages/cli-module-new/templates/legacy-frontend-plugin/README.md.hbs @@ -0,0 +1,13 @@ +# {{pluginId}} + +Welcome to the {{pluginId}} plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/{{pluginId}}](http://localhost:3000/{{pluginId}}). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/packages/cli-module-new/templates/frontend-plugin/dev/index.tsx.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/dev/index.tsx.hbs similarity index 100% rename from packages/cli-module-new/templates/frontend-plugin/dev/index.tsx.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/dev/index.tsx.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin/package.json.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/package.json.hbs similarity index 72% rename from packages/cli-module-new/templates/new-frontend-plugin/package.json.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/package.json.hbs index 4a37a4fa633a27..9499d11b182d25 100644 --- a/packages/cli-module-new/templates/new-frontend-plugin/package.json.hbs +++ b/packages/cli-module-new/templates/legacy-frontend-plugin/package.json.hbs @@ -23,7 +23,7 @@ }, "dependencies": { "@backstage/core-components": "{{versionQuery '@backstage/core-components'}}", - "@backstage/frontend-plugin-api": "{{versionQuery '@backstage/frontend-plugin-api'}}", + "@backstage/core-plugin-api": "{{versionQuery '@backstage/core-plugin-api'}}", "@backstage/theme": "{{versionQuery '@backstage/theme'}}", "@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}", "@material-ui/icons": "{{versionQuery '@material-ui/icons' '4.9.1'}}", @@ -31,17 +31,22 @@ "react-use": "{{versionQuery 'react-use' '17.2.4'}}" }, "peerDependencies": { - "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}" + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", + "react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", + "react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}" }, "devDependencies": { "@backstage/cli": "{{versionQuery '@backstage/cli'}}", - "@backstage/frontend-dev-utils": "{{versionQuery '@backstage/frontend-dev-utils'}}", - "@backstage/frontend-test-utils": "{{versionQuery '@backstage/frontend-test-utils'}}", + "@backstage/core-app-api": "{{versionQuery '@backstage/core-app-api'}}", + "@backstage/dev-utils": "{{versionQuery '@backstage/dev-utils'}}", + "@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}", "@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}", "@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}", "@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}", "msw": "{{versionQuery 'msw' '1.0.0'}}", - "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}" + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", + "react-dom": "{{versionQuery 'react-dom' '^16.13.1 || ^17.0.0 || ^18.0.0'}}", + "react-router-dom": "{{versionQuery 'react-router-dom' '^6.0.0'}}" }, "files": [ "dist" diff --git a/packages/cli-module-new/templates/legacy-frontend-plugin/portable-template.yaml b/packages/cli-module-new/templates/legacy-frontend-plugin/portable-template.yaml new file mode 100644 index 00000000000000..d69a1f35df8e53 --- /dev/null +++ b/packages/cli-module-new/templates/legacy-frontend-plugin/portable-template.yaml @@ -0,0 +1,6 @@ +name: frontend-plugin +role: frontend-plugin +description: A new frontend plugin (legacy system) +values: + pluginVar: '{{ camelCase pluginId }}Plugin' + extensionName: '{{ upperFirst ( camelCase pluginId ) }}Page' diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs similarity index 94% rename from packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index d1a8a93d0b95d9..c7c20863c2fa45 100644 --- a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -5,7 +5,7 @@ import { screen } from '@testing-library/react'; import { registerMswTestHooks, renderInTestApp, -} from '@backstage/frontend-test-utils'; +} from '@backstage/test-utils'; describe('ExampleComponent', () => { const server = setupServer(); diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/index.ts b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/index.ts similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleComponent/index.ts rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleComponent/index.ts diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs similarity index 91% rename from packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index c9b553287f1ce3..820e54fa377363 100644 --- a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -1,4 +1,4 @@ -import { renderInTestApp } from '@backstage/frontend-test-utils'; +import { renderInTestApp } from '@backstage/test-utils'; import { ExampleFetchComponent } from './ExampleFetchComponent'; describe('ExampleFetchComponent', () => { diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/index.ts b/packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/index.ts similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/components/ExampleFetchComponent/index.ts rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/components/ExampleFetchComponent/index.ts diff --git a/packages/cli-module-new/templates/legacy-frontend-plugin/src/index.ts.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/index.ts.hbs new file mode 100644 index 00000000000000..be4881efafc5db --- /dev/null +++ b/packages/cli-module-new/templates/legacy-frontend-plugin/src/index.ts.hbs @@ -0,0 +1 @@ +export { {{ pluginVar }}, {{ extensionName }} } from './plugin'; diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/plugin.test.ts.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/plugin.test.ts.hbs similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/plugin.test.ts.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/plugin.test.ts.hbs diff --git a/packages/cli-module-new/templates/frontend-plugin/src/plugin.ts.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/plugin.ts.hbs similarity index 100% rename from packages/cli-module-new/templates/frontend-plugin/src/plugin.ts.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/plugin.ts.hbs diff --git a/packages/cli-module-new/templates/frontend-plugin/src/routes.ts.hbs b/packages/cli-module-new/templates/legacy-frontend-plugin/src/routes.ts.hbs similarity index 100% rename from packages/cli-module-new/templates/frontend-plugin/src/routes.ts.hbs rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/routes.ts.hbs diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/setupTests.ts b/packages/cli-module-new/templates/legacy-frontend-plugin/src/setupTests.ts similarity index 100% rename from packages/cli-module-new/templates/new-frontend-plugin/src/setupTests.ts rename to packages/cli-module-new/templates/legacy-frontend-plugin/src/setupTests.ts diff --git a/packages/cli-module-new/templates/new-frontend-plugin/README.md.hbs b/packages/cli-module-new/templates/new-frontend-plugin/README.md.hbs deleted file mode 100644 index 3f1ff3e4f7c1d9..00000000000000 --- a/packages/cli-module-new/templates/new-frontend-plugin/README.md.hbs +++ /dev/null @@ -1,20 +0,0 @@ -# {{pluginId}} - -Welcome to the {{pluginId}} plugin! - -_This plugin was created through the Backstage CLI_ - -## Getting started - -Your plugin has been added to the app in this repository, meaning you'll be able -to access it by running `yarn start` in the root directory, and then navigating -to [/{{pluginId}}](http://localhost:3000/{{pluginId}}). - -This plugin is built with Backstage's [new frontend -system](https://backstage.io/docs/frontend-system/architecture/index), and you -can find more information about building plugins in the [plugin builder -documentation](https://backstage.io/docs/frontend-system/building-plugins/index). - -You can also serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/packages/cli-module-new/templates/new-frontend-plugin/portable-template.yaml b/packages/cli-module-new/templates/new-frontend-plugin/portable-template.yaml deleted file mode 100644 index a70ce316f1503a..00000000000000 --- a/packages/cli-module-new/templates/new-frontend-plugin/portable-template.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: frontend-plugin -role: frontend-plugin -description: A new frontend plugin -values: - pluginVar: '{{ camelCase pluginId }}Plugin' diff --git a/packages/cli-module-new/templates/new-frontend-plugin/src/index.ts.hbs b/packages/cli-module-new/templates/new-frontend-plugin/src/index.ts.hbs deleted file mode 100644 index 0d6810ad6b94e7..00000000000000 --- a/packages/cli-module-new/templates/new-frontend-plugin/src/index.ts.hbs +++ /dev/null @@ -1 +0,0 @@ -export { {{ pluginVar }} as default } from './plugin'; diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 4262e0fc828d35..836ecccac5941d 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -22,6 +22,28 @@ "prettier:check": "prettier --check .", "new": "backstage-cli new" }, + "backstage": { + "cli": { + "new": { + "globals": { + "license": "UNLICENSED" + }, + "templates": [ + "@backstage/cli-module-new/templates/legacy-frontend-plugin", + "@backstage/cli-module-new/templates/backend-plugin", + "@backstage/cli-module-new/templates/backend-plugin-module", + "@backstage/cli-module-new/templates/plugin-web-library", + "@backstage/cli-module-new/templates/plugin-node-library", + "@backstage/cli-module-new/templates/plugin-common-library", + "@backstage/cli-module-new/templates/web-library", + "@backstage/cli-module-new/templates/node-library", + "@backstage/cli-module-new/templates/cli-module", + "@backstage/cli-module-new/templates/catalog-provider-module", + "@backstage/cli-module-new/templates/scaffolder-backend-module" + ] + } + } + }, "workspaces": [ "packages/*", "plugins/*" diff --git a/packages/create-app/templates/next-app/package.json.hbs b/packages/create-app/templates/next-app/package.json.hbs index 2ace0f549b58ac..b2c7f5cbb0ceaa 100644 --- a/packages/create-app/templates/next-app/package.json.hbs +++ b/packages/create-app/templates/next-app/package.json.hbs @@ -27,20 +27,7 @@ "new": { "globals": { "license": "UNLICENSED" - }, - "templates": [ - "@backstage/cli-module-new/templates/new-frontend-plugin", - "@backstage/cli-module-new/templates/new-frontend-plugin-module", - "@backstage/cli-module-new/templates/backend-plugin", - "@backstage/cli-module-new/templates/backend-plugin-module", - "@backstage/cli-module-new/templates/plugin-web-library", - "@backstage/cli-module-new/templates/plugin-node-library", - "@backstage/cli-module-new/templates/plugin-common-library", - "@backstage/cli-module-new/templates/web-library", - "@backstage/cli-module-new/templates/node-library", - "@backstage/cli-module-new/templates/catalog-provider-module", - "@backstage/cli-module-new/templates/scaffolder-backend-module" - ] + } } } }, From 35686dc9f79c46f1647badf5c6048bc447c6ff9b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 01:18:13 +0100 Subject: [PATCH 19/55] cli: add frontend template filtering coverage Clarify the legacy frontend plugin template label and lock in template selection behavior for legacy and new frontend system apps. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../loadPortableTemplateConfig.test.ts | 135 ++++++++++++++++++ .../preparation/loadPortableTemplateConfig.ts | 7 +- 2 files changed, 136 insertions(+), 6 deletions(-) diff --git a/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.test.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.test.ts index a89d0c3905bbbc..454ece648acab4 100644 --- a/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.test.ts +++ b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.test.ts @@ -360,6 +360,141 @@ describe('loadPortableTemplateConfig', () => { ); }); + it('should filter out legacy frontend template for new frontend system apps', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({}), + packages: { + app: { + 'package.json': JSON.stringify({ + dependencies: { + '@backstage/frontend-defaults': '^0.1.0', + }, + }), + }, + }, + node_modules: Object.fromEntries( + defaultTemplates.map(t => { + // Match the real behavior: both frontend-plugin and legacy-frontend-plugin + // have the same template name "frontend-plugin" + const name = t.endsWith('/legacy-frontend-plugin') + ? 'frontend-plugin' + : basename(t); + return [ + t, + { [TEMPLATE_FILE_NAME]: `name: ${name}\nrole: web-library\n` }, + ]; + }), + ), + }); + + const config = await loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }); + + expect(config.isUsingDefaultTemplates).toBe(true); + + const templateNames = config.templatePointers.map(t => t.name); + expect(templateNames).toContain('frontend-plugin'); + expect(templateNames).toContain('frontend-plugin-module'); + expect(templateNames).toContain('backend-plugin'); + // Legacy template should be filtered out + expect(templateNames).not.toContain('legacy-frontend-plugin'); + + // The frontend-plugin in the list should be from the new template, not legacy + const frontendPlugin = config.templatePointers.find( + t => t.name === 'frontend-plugin', + ); + expect(frontendPlugin?.target).toContain('/frontend-plugin/'); + expect(frontendPlugin?.target).not.toContain('/legacy-frontend-plugin/'); + }); + + it('should filter out new frontend templates for legacy frontend system apps', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({}), + packages: { + app: { + 'package.json': JSON.stringify({ + dependencies: { + '@backstage/app-defaults': '^0.1.0', + '@backstage/core-app-api': '^0.1.0', + }, + }), + }, + }, + node_modules: Object.fromEntries( + defaultTemplates.map(t => { + const name = t.endsWith('/legacy-frontend-plugin') + ? 'frontend-plugin' + : basename(t); + return [ + t, + { [TEMPLATE_FILE_NAME]: `name: ${name}\nrole: web-library\n` }, + ]; + }), + ), + }); + + const config = await loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }); + + expect(config.isUsingDefaultTemplates).toBe(true); + + const templateNames = config.templatePointers.map(t => t.name); + // Legacy template should be present (shown as "frontend-plugin") + expect(templateNames).toContain('frontend-plugin'); + expect(templateNames).toContain('backend-plugin'); + // New frontend templates should be filtered out + expect(templateNames).not.toContain('frontend-plugin-module'); + + // The frontend-plugin in the list should be from the legacy template + const frontendPlugin = config.templatePointers.find( + t => t.name === 'frontend-plugin', + ); + expect(frontendPlugin?.target).toContain('/legacy-frontend-plugin/'); + }); + + it('should not filter templates when using explicit configuration', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + backstage: { + cli: { + new: { + templates: ['./my-frontend-plugin', './my-backend-plugin'], + }, + }, + }, + }), + // Even with a new frontend system app, explicit templates aren't filtered + packages: { + app: { + 'package.json': JSON.stringify({ + dependencies: { + '@backstage/frontend-defaults': '^0.1.0', + }, + }), + }, + }, + 'my-frontend-plugin': { + [TEMPLATE_FILE_NAME]: 'name: frontend-plugin\nrole: frontend-plugin\n', + }, + 'my-backend-plugin': { + [TEMPLATE_FILE_NAME]: 'name: backend-plugin\nrole: backend-plugin\n', + }, + }); + + const config = await loadPortableTemplateConfig({ + packagePath: mockDir.resolve('package.json'), + }); + + expect(config.isUsingDefaultTemplates).toBe(false); + expect(config.templatePointers).toHaveLength(2); + expect(config.templatePointers.map(t => t.name)).toEqual([ + 'frontend-plugin', + 'backend-plugin', + ]); + }); + it('should handle missing backstage.new configuration', async () => { mockDir.setContent({ 'package.json': JSON.stringify({}), diff --git a/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts index 3541d5a4cf522e..d2909ebe9866fb 100644 --- a/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts +++ b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts @@ -15,12 +15,7 @@ */ import fs from 'fs-extra'; -import { - resolve as resolvePath, - dirname, - isAbsolute, - join, -} from 'node:path'; +import { resolve as resolvePath, dirname, isAbsolute, join } from 'node:path'; import { targetPaths } from '@backstage/cli-common'; import { defaultTemplates } from '../defaultTemplates'; import { From 7cd15e7c90141a7990e8f0bd5f354d2a7d80be80 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 10:32:27 +0100 Subject: [PATCH 20/55] create-app: fix next-app template formatting Align the next-app template output with the scaffolded app Prettier config so the default app E2E checks pass. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../create-app/templates/next-app/app-config.yaml.hbs | 1 - .../next-app/examples/template/template.yaml | 2 +- .../next-app/packages/app/src/modules/nav/Sidebar.tsx | 11 +++++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/create-app/templates/next-app/app-config.yaml.hbs b/packages/create-app/templates/next-app/app-config.yaml.hbs index 67917fd193dd18..c8814a9e96cf4d 100644 --- a/packages/create-app/templates/next-app/app-config.yaml.hbs +++ b/packages/create-app/templates/next-app/app-config.yaml.hbs @@ -17,7 +17,6 @@ app: config: path: / - organization: name: My Company diff --git a/packages/create-app/templates/next-app/examples/template/template.yaml b/packages/create-app/templates/next-app/examples/template/template.yaml index 2a20bd45ed398f..efb33be86601d7 100644 --- a/packages/create-app/templates/next-app/examples/template/template.yaml +++ b/packages/create-app/templates/next-app/examples/template/template.yaml @@ -65,7 +65,7 @@ spec: input: repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }} catalogInfoPath: '/catalog-info.yaml' - + # Let's notify the user that the template has completed using the Notification action - id: notify name: Notify diff --git a/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx b/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx index d436252edf248c..8614f6fa0c0a0d 100644 --- a/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx +++ b/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx @@ -12,18 +12,17 @@ import { SidebarLogo } from './SidebarLogo'; import MenuIcon from '@material-ui/icons/Menu'; import SearchIcon from '@material-ui/icons/Search'; import { SidebarSearchModal } from '@backstage/plugin-search'; -import { UserSettingsSignInAvatar, Settings as SidebarSettings } from '@backstage/plugin-user-settings'; +import { + UserSettingsSignInAvatar, + Settings as SidebarSettings, +} from '@backstage/plugin-user-settings'; import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; export const SidebarContent = NavContentBlueprint.make({ params: { component: ({ navItems }) => { const nav = navItems.withComponent(item => ( - item.icon} - to={item.href} - text={item.title} - /> + item.icon} to={item.href} text={item.title} /> )); return compatWrapper( From 9603f9c6c0df66405dc73cb6bc490da54c376908 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 10:47:24 +0100 Subject: [PATCH 21/55] cli: fix frontend plugin template dev dependency Add the missing frontend dev utils package so newly scaffolded frontend plugins compile in the create-app E2E flow. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../cli-module-new/templates/frontend-plugin/package.json.hbs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli-module-new/templates/frontend-plugin/package.json.hbs b/packages/cli-module-new/templates/frontend-plugin/package.json.hbs index 4a37a4fa633a27..955c6077b84514 100644 --- a/packages/cli-module-new/templates/frontend-plugin/package.json.hbs +++ b/packages/cli-module-new/templates/frontend-plugin/package.json.hbs @@ -36,6 +36,7 @@ "devDependencies": { "@backstage/cli": "{{versionQuery '@backstage/cli'}}", "@backstage/frontend-dev-utils": "{{versionQuery '@backstage/frontend-dev-utils'}}", + "@backstage/frontend-defaults": "{{versionQuery '@backstage/frontend-defaults'}}", "@backstage/frontend-test-utils": "{{versionQuery '@backstage/frontend-test-utils'}}", "@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}", "@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}", From 2dce1f23ebf2069d3eae4de3b2c92dfc9290fdd7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 11:05:03 +0100 Subject: [PATCH 22/55] e2e-test: fix React 17 app bootstrap for new frontend apps Use the generated app root API when switching the scaffolded app to React 17 so the new frontend app still type-checks in E2E. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/e2e-test/src/commands/runCommand.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-test/src/commands/runCommand.ts b/packages/e2e-test/src/commands/runCommand.ts index 896ac5ed72e2f3..14cc2f8320896b 100644 --- a/packages/e2e-test/src/commands/runCommand.ts +++ b/packages/e2e-test/src/commands/runCommand.ts @@ -437,7 +437,7 @@ async function switchToReact17(appDir: string) { import ReactDOM from 'react-dom'; import App from './App'; -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render(App.createRoot(), document.getElementById('root')); `, 'utf8', ); From d0a2b0b7dca32cdeca8a8d9bbd3ce249fe6c6d69 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 11:30:34 +0100 Subject: [PATCH 23/55] e2e-test: preserve next-app styles in React 17 bootstrap Keep the generated app stylesheet import when rewriting the new frontend app entrypoint for React 17 so the E2E app boots with the same UI setup. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/e2e-test/src/commands/runCommand.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/e2e-test/src/commands/runCommand.ts b/packages/e2e-test/src/commands/runCommand.ts index 14cc2f8320896b..40aa8a75bd1c37 100644 --- a/packages/e2e-test/src/commands/runCommand.ts +++ b/packages/e2e-test/src/commands/runCommand.ts @@ -436,6 +436,7 @@ async function switchToReact17(appDir: string) { `import '@backstage/cli/asset-types'; import ReactDOM from 'react-dom'; import App from './App'; +import '@backstage/ui/css/styles.css'; ReactDOM.render(App.createRoot(), document.getElementById('root')); `, From 4568618cf1a2a12424d398d78782991d1a268a80 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 15:43:55 +0100 Subject: [PATCH 24/55] create-app: fix next-app e2e sign-in assertion Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/fix-next-app-e2e-sidebar.md | 5 +++++ .../templates/next-app/packages/app/e2e-tests/app.test.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-next-app-e2e-sidebar.md diff --git a/.changeset/fix-next-app-e2e-sidebar.md b/.changeset/fix-next-app-e2e-sidebar.md new file mode 100644 index 00000000000000..d5875b32e415cd --- /dev/null +++ b/.changeset/fix-next-app-e2e-sidebar.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Fixed the generated next app E2E test to verify the post-sign-in navigation, matching the current default app behavior. diff --git a/packages/create-app/templates/next-app/packages/app/e2e-tests/app.test.ts b/packages/create-app/templates/next-app/packages/app/e2e-tests/app.test.ts index 839ff883de1c5c..3347034856aa14 100644 --- a/packages/create-app/templates/next-app/packages/app/e2e-tests/app.test.ts +++ b/packages/create-app/templates/next-app/packages/app/e2e-tests/app.test.ts @@ -23,5 +23,6 @@ test('App should render the welcome page', async ({ page }) => { await expect(enterButton).toBeVisible(); await enterButton.click(); - await expect(page.getByText('My Company Catalog')).toBeVisible(); + await expect(page.getByRole('link', { name: 'Catalog' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'APIs' })).toBeVisible(); }); From 9a82ed52699bed2150485c74fbcec705e4a946d8 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 28 Feb 2026 10:38:26 -0600 Subject: [PATCH 25/55] [Docs] NFS - Authentication Signed-off-by: Andre Wanlin --- docs/auth/index--new.md | 540 ++++++++++++++++++ docs/auth/index.md | 4 + docs/features/techdocs/addons--new.md | 2 +- .../config/authentication--new.md | 239 ++++++++ docs/getting-started/config/authentication.md | 4 + 5 files changed, 788 insertions(+), 1 deletion(-) create mode 100644 docs/auth/index--new.md create mode 100644 docs/getting-started/config/authentication--new.md diff --git a/docs/auth/index--new.md b/docs/auth/index--new.md new file mode 100644 index 00000000000000..0a2e980658c6e6 --- /dev/null +++ b/docs/auth/index--new.md @@ -0,0 +1,540 @@ +--- +id: index +title: Authentication in Backstage +description: Introduction to authentication in Backstage +--- + +:::info +This documentation is written for [the new frontend system](../../frontend-system/index.md). If you are on the old frontend system you may want to read [its own article](./index.md) instead. +::: + +The authentication system in Backstage serves two distinct purposes: sign-in and identification of users, as well as delegating access to third-party resources. It is possible to configure Backstage to have any number of authentication providers, but only one of these will typically be used for sign-in, with the rest being used to provide access to external resources. + +:::note Note + +Identity management and the Sign-In page in Backstage will only block external access when using the new backend system, without setting `backend.auth.dangerouslyDisableDefaultAuthPolicy` in configuration. Even so, the frontend bundle is not protected from external access, protecting it requires the use of the [experimental public entry point](https://backstage.io/docs/tutorials/enable-public-entry/). You can learn more about this in the [Threat Model](../overview/threat-model.md#operator-responsibilities). + +::: + +## Built-in Authentication Providers + +Backstage comes with many common authentication providers in the core library: + +- [Auth0](auth0/provider.md) +- [Atlassian](atlassian/provider.md) +- [Azure](microsoft/provider.md) +- [Azure Easy Auth](microsoft/azure-easyauth.md) +- [Bitbucket](bitbucket/provider.md) +- [Bitbucket Server](bitbucketServer/provider.md) +- [Cloudflare Access](cloudflare/provider.md) +- [GitHub](github/provider.md) +- [GitLab](gitlab/provider.md) +- [Google](google/provider.md) +- [Google IAP](google/gcp-iap-auth.md) +- [Okta](okta/provider.md) +- [OAuth 2 Custom Proxy](oauth2-proxy/provider.md) +- [OneLogin](onelogin/provider.md) +- [OpenShift](openshift/provider.md) +- [VMware Cloud](vmware-cloud/provider.md) + +These built-in providers handle the authentication flow for a particular service, including required scopes, callbacks, etc. These providers are each added to a Backstage app in a similar way. + +## Configuring Authentication Providers + +Each built-in provider has a configuration block under the `auth` section of `app-config.yaml`. For example, the GitHub provider: + +```yaml +auth: + environment: development + providers: + github: + development: + clientId: ${AUTH_GITHUB_CLIENT_ID} + clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} +``` + +See the documentation for a particular provider to see what configuration is needed. + +The `providers` key may have several authentication providers if multiple authentication methods are supported. Each provider may also have configuration for different authentication environments (development, production, etc). This allows a single auth backend to serve multiple environments, such as running a local frontend against a deployed backend. The provider configuration matching the local `auth.environment` setting will be selected. + +## Sign-In Configuration + +Using an authentication provider for sign-in is something you need to configure both in the frontend app as well as the `auth` backend plugin. For information on how to configure the backend app, see [Sign-in Identities and Resolvers](./identity-resolver.md). The rest of this section will focus on how to configure sign-in for the frontend app. + +Sign-in is configured by providing a custom `SignInPage` app component. It will be rendered before any other routes in the app and is responsible for providing the identity of the current user. The `SignInPage` can render any number of pages and components, or just blank space with logic running in the background. In the end, however, it must provide a valid Backstage user identity through the `onSignInSuccess` callback prop, at which point the rest of the app is rendered. + +If you want to, you can use the `SignInPage` component that is provided by `@backstage/core-components`, which takes either a `provider` or `providers` (array) prop of `SignInProviderConfig` definitions. + +The following example for GitHub shows the additions needed to `packages/app/src/App.tsx`, and can be adapted to any of the built-in providers: + +```tsx title="packages/app/src/App.tsx" +import { createApp } from '@backstage/frontend-defaults'; +import catalogPlugin from '@backstage/plugin-catalog/alpha'; +import { navModule } from './modules/nav'; + +/* highlight-add-start */ +import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { SignInPage } from '@backstage/core-components'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; + +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => + ( + + ), + }, +}); +/* highlight-add-end */ + +export default createApp({ + features: [ + catalogPlugin, + navModule, + /* highlight-add-start */ + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + /* highlight-add-end */ + ], +}); +``` + +:::note Note + +You can configure sign-in to use a redirect flow with no pop-up by adding `enableExperimentalRedirectFlow: true` to the root of your `app-config.yaml` + +::: + +### Using Multiple Providers + +You can also use the `providers` prop to enable multiple sign-in methods, for example to allow guest access: + +```tsx title="packages/app/src/App.tsx" +import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { SignInPage } from '@backstage/core-components'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; + +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => + ( + + ), + }, +}); + +export default createApp({ + features: [ + catalogPlugin, + navModule, + /* highlight-add-start */ + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + /* highlight-add-end */ + ], +}); +``` + +### Conditionally Render Sign In Provider + +In the above example, you have both Guest and GitHub sign-in options; this is helpful for non-production, but in Production you will most likely not want to offer Guest access. You can easily use information from your config to help conditionally render the provider: + +```tsx title="packages/app/src/App.tsx" +import { + configApiRef, + githubAuthApiRef, + useApi, +} from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { SignInPage } from '@backstage/core-components'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; + +const signInPage = SignInPageBlueprint.make({ + params: { + /* highlight-add-start */ + loader: async () => props => { + const configApi = useApi(configApiRef); + if (configApi.getString('auth.environment') === 'development') { + return ( + + ); + } + + return ( + + ); + }, + /* highlight-add-end */ + }, +}); + +export default createApp({ + features: [ + catalogPlugin, + navModule, + /* highlight-add-start */ + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + /* highlight-add-end */ + ], +}); +``` + +## Sign-In with Proxy Providers + +Some auth providers are so-called "proxy" providers, meaning they're meant to be used behind an authentication proxy. Examples of these are [Amazon Application Load Balancer](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md), [Azure EasyAuth](./microsoft/azure-easyauth.md), [Cloudflare Access](./cloudflare/provider.md), [Google Identity-Aware Proxy](./google/gcp-iap-auth.md) and [OAuth2 Proxy](./oauth2-proxy/provider.md). + +When using a proxy provider, you'll end up wanting to use a different sign-in page, as there is no need for further user interaction once you've signed in towards the proxy. All the sign-in page needs to do is call the `/refresh` endpoint of the auth providers to get the existing session, which is exactly what the `ProxiedSignInPage` does. The only thing you need to do to configure the `ProxiedSignInPage` is to pass the ID of the provider like this: + +```tsx title="packages/app/src/App.tsx" +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; +import { ProxiedSignInPage } from '@backstage/core-components'; + +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => + , + }, +}); + +export default createApp({ + features: [ + catalogPlugin, + navModule, + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + ], +}); +``` + +If the provider in auth backend expects additional headers such as `x-provider-token`, there is now a way to configure that in `ProxiedSignInPage` using the optional `headers` prop. + +Example: + +```tsx + +``` + +Headers can also be returned in an async manner: + +```tsx + { + const someValue = await someFn(); + return { 'x-some-key': someValue }; + }} + /* highlight-end */ +/> +``` + +A downside of this method is that it can be cumbersome to set up for local development. As a workaround for this, it's possible to dynamically select the sign-in page based on what environment the app is running in and then use a different sign-in method for local development, if one is needed at all. Depending on the exact setup, one might choose to select the sign-in method based on the `process.env.NODE_ENV` environment variable, by checking the `hostname` of the current location, or by accessing the configuration API to read a configuration value. For example: + +```tsx title="packages/app/src/App.tsx" +import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { ProxiedSignInPage, SignInPage } from '@backstage/core-components'; +import { + createFrontendModule, + googleAuthApiRef, +} from '@backstage/frontend-plugin-api'; + +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => { + const configApi = useApi(configApiRef); + if (configApi.getString('auth.environment') === 'development') { + return ( + + ); + } + + return ; + }, + }, +}); + +export default createApp({ + features: [ + catalogPlugin, + navModule, + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + ], +}); +``` + +When using multiple auth providers like this, it's important that you configure the different sign-in resolvers so that they resolve to the same identity regardless of the method used. + +## For Plugin Developers + +The Backstage frontend core APIs provide a set of Utility APIs for plugin developers to use, both to access the user identity as well as third-party resources. + +### Identity for Plugin Developers + +For plugin developers, there is one main touchpoint for accessing the user identity: the `IdentityApi` exported by `@backstage/core-plugin-api` via the `identityApiRef`. + +The `IdentityApi` gives access to the signed-in user's identity in the frontend. It provides access to the user's entity reference, lightweight profile information, and a Backstage token that identifies the user when making authenticated calls within Backstage. + +When making calls to backend plugins, we recommend that the `FetchApi` is used, which is exported via the `fetchApiRef` from `@backstage/core-plugin-api`. The `FetchApi` will automatically include a Backstage token in the request, meaning there is no need to interact directly with the `IdentityApi`. + +### Accessing Third Party Resources + +A common pattern for talking to third-party services in Backstage is user-to-server requests, where short-lived OAuth Access Tokens are requested by plugins to authenticate calls to external services. These calls can be made either directly to the services or through a backend plugin or service. + +By relying on user-to-server calls, we keep the coupling between the frontend and backend low and provide a much lower barrier for plugins to make use of third party services. This is in comparison to, for example, a session-based system where access tokens are stored server-side. Such a solution would require a much deeper coupling between the auth backend plugin, its session storage, and other backend plugins or separate services. A goal of Backstage is to make it as easy as possible to create new plugins, and an auth solution based on user-to-server OAuth helps in that regard. + +The method with which frontend plugins request access to third-party services is through [Utility APIs](../api/utility-apis.md) for each service provider. These are all suffixed with `*AuthApiRef`, for example `githubAuthApiRef`. For a full list of providers, see the [@backstage/core-plugin-api](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.index.html#alertapiref) reference. + +## Custom Authentication Provider + +There are generic authentication providers for OAuth2 and SAML. These can reduce the amount of code needed to implement a custom authentication provider that adheres to these standards. + +Backstage uses [Passport](http://www.passportjs.org/) under the hood, which has a wide library of authentication strategies for different providers. See[Add authentication provider](add-auth-provider.md) for details on adding a new Passport-supported authentication method. + +## Custom ScmAuthApi Implementation + +The default `ScmAuthApi` provides integrations for `github`, `gitlab`, `azure` (Azure DevOps), `bitbucketServer` and `bitbucketCloud` and is created and registered automatically for you by the New Frontend System. + +If you require only a subset of these integrations, then you will need a custom implementation of the [`ScmAuthApi`](https://backstage.io/api/stable/interfaces/_backstage_integration-react.ScmAuthApi.html). It is an API used to authenticate different SCM systems generically, based on what resource is being accessed, and is used for example, by the Scaffolder (Software Templates) and Catalog Import plugins. + +The first step is to remove the code that creates the default providers. + +```ts title="packages/app/src/apis.ts" +import { + ScmIntegrationsApi, + scmIntegrationsApiRef, + /* highlight-add-next-line */ + ScmAuth, +} from '@backstage/integration-react'; + +export const apis: AnyApiFactory[] = [ + /* highlight-add-next-line */ + ScmAuth.createDefaultApiFactory(), + // ... +]; +``` + +Then replace it with something like this, which will create an `ApiFactory` with only a GitHub provider. + +```ts title="packages/app/src/apis.ts" +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: scmAuthApiRef, + deps: { + githubAuthApi: githubAuthApiRef, + }, + factory: ({ githubAuthApi }) => + ScmAuth.merge( + ScmAuth.forGithub(githubAuthApi), + ), + }); +``` + +If you use any custom authentication integrations, a new provider can be added to the `ApiFactory`. + +The first step is to create a new authentication ref, which follows the naming convention of `xxxAuthApiRef`. The example below is for a new GitHub enterprise integration which can be defined either inside the app itself if it's only used for this purpose or inside a common internal package for APIs, such as `@internal/apis`: + +```ts +const gheAuthApiRef: ApiRef = + createApiRef({ + id: 'internal.auth.ghe', + }); +``` + +This new API ref will only work if you define an API factory for it. For example: + +```ts +createApiFactory({ + api: gheAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GithubAuth.create({ + configApi, + discoveryApi, + oauthRequestApi, + provider: { id: 'ghe', title: 'GitHub Enterprise', icon: () => null }, + defaultScopes: ['read:user'], + environment: configApi.getOptionalString('auth.environment'), + }), +}); +``` + +The new API ref is then used to add a new provider to the ApiFactory: + +```ts +createApiFactory({ + api: scmAuthApiRef, + deps: { + gheAuthApi: gheAuthApiRef, + githubAuthApi: githubAuthApiRef, + }, + factory: ({ githubAuthApi, gheAuthApi }) => + ScmAuth.merge( + ScmAuth.forGithub(githubAuthApi), + ScmAuth.forGithub(gheAuthApi, { + host: 'ghe.example.com', + }), + ), +}); +``` + +Finally, you also need to add and configure another provider to the `auth-backend` using the provider ID, which in this example is `ghe`: + +```ts +import { providers } from '@backstage/plugin-auth-backend'; + +// Add the following options to `createRouter` in packages/backend/src/plugins/auth.ts +providerFactories: { + ghe: providers.github.create(), +}, +``` + +In the new backend system you can leverage the `authProvidersExtensionPoint` for this: + +```ts +// your-auth-plugin-module.ts +export const gheAuth = createBackendModule({ + // This ID must be exactly "auth" because that's the plugin it targets + pluginId: 'auth', + // This ID must be unique, but can be anything + moduleId: 'ghe-auth-provider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + logger: coreServices.logger, + }, + async init({ providers, logger }) { + providers.registerProvider({ + // This ID must match the actual provider config, e.g. addressing + // auth.providers.ghe means that this must be "ghe". + providerId: 'ghe', + factory: createOAuthProviderFactory({ + authenticator: githubAuthenticator, + signInResolverFactories: { + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); + +// backend index.ts +backend.add(gheAuth); +``` + +## Configuring token issuers + +By default, the Backstage authentication backend generates and manages its own signing keys automatically for any issued Backstage tokens. However, these keys have a short lifetime and do not persist after instance restarts. + +Alternatively, users can provide their own public and private key files to sign issued tokens. This is beneficial in scenarios where the token verification implementation aggressively caches the list of keys, and doesn't attempt to fetch new ones even if they encounter an unknown key id. To enable this feature add the following configuration to your config file: + +```yaml +auth: + keyStore: + provider: 'static' + static: + keys: + # Must be declared at least once and the first one will be used for signing + - keyId: 'primary' + publicKeyFile: /path/to/public.key + privateKeyFile: /path/to/private.key + algorithm: # Optional, algorithm used to generate the keys, defaults to ES256 + # More keys can be added so with future key rotations caches already know about it + - keyId: ... +``` + +The private key should be stored in the PKCS#8 format. The public key should be stored in the SPKI format. You can generate the public/private key pair, using openssl and the ES256 algorithm by performing the following steps: + +Generate a private key using the ES256 algorithm + +```sh +openssl ecparam -name prime256v1 -genkey -out private.ec.key +``` + +Convert it to PKCS#8 format + +```sh +openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private.ec.key -out private.key +``` + +Extract the public key + +```sh +openssl ec -inform PEM -outform PEM -pubout -in private.key -out public.key +``` diff --git a/docs/auth/index.md b/docs/auth/index.md index 389f824be3d39a..892f7a408f09da 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -4,6 +4,10 @@ title: Authentication in Backstage description: Introduction to authentication in Backstage --- +:::info +This documentation is written for the old frontend system. If you are on the [new frontend system](../../frontend-system/index.md) you may want to read [its own article](./index--new.md) instead. +::: + The authentication system in Backstage serves two distinct purposes: sign-in and identification of users, as well as delegating access to third-party resources. It is possible to configure Backstage to have any number of authentication providers, but only diff --git a/docs/features/techdocs/addons--new.md b/docs/features/techdocs/addons--new.md index 170bd80f701fb3..04fa68b4235f33 100644 --- a/docs/features/techdocs/addons--new.md +++ b/docs/features/techdocs/addons--new.md @@ -5,7 +5,7 @@ description: How to find, use, or create TechDocs Addons. --- :::info -This documentation is written for [the new frontend system](../../frontend-system/index.md) which is still in alpha and is only supported by a small number of plugins. If you are on the [old frontend system](./getting-started.md#adding-techdocs-frontend-plugin) you may want to read [its own article](./addons.md) instead. +This documentation is written for [the new frontend system](../../frontend-system/index.md). If you are on the [old frontend system](./getting-started.md#adding-techdocs-frontend-plugin) you may want to read [its own article](./addons.md) instead. ::: ## Concepts diff --git a/docs/getting-started/config/authentication--new.md b/docs/getting-started/config/authentication--new.md new file mode 100644 index 00000000000000..04b1e2b46b578e --- /dev/null +++ b/docs/getting-started/config/authentication--new.md @@ -0,0 +1,239 @@ +--- +id: authentication +title: Authentication +description: How to setup authentication for your Backstage app +--- + +:::info +This documentation is written for [the new frontend system](../../frontend-system/index.md). If you are on the old frontend system you may want to read [its own article](./authentication.md) instead. +::: + +Audience: Admins or Developers + +## Summary + +We'll be walking you through how to setup authentication for your Backstage app using GitHub. After finishing this guide, you'll have both working authentication and users in your Backstage app to match to the users logging in! + +There are multiple authentication providers available for you to use with Backstage, feel free to follow [their instructions for adding authentication](../../auth/index.md). + +:::note Note + +The default Backstage app comes with a guest Sign In Resolver. This resolver makes all users share a single "guest" identity and is only intended as a minimum requirement to quickly get up and running. You can read more about how [Sign In Resolvers](../../auth/identity-resolver.md#sign-in-resolvers) play a role in creating a [Backstage User Identity](../../auth/identity-resolver.md#backstage-user-identity) for logged in users. + +::: + +## Setting up authentication + +For this tutorial we choose to use GitHub, a free service most of you might be familiar with, and we'll be using an OAuth app. For detailed options, see +[the GitHub auth provider documentation](../../auth/github/provider.md#create-an-oauth-app-on-github). + +Go to [https://github.com/settings/applications/new](https://github.com/settings/applications/new) to create your OAuth App. The "Homepage URL" should point to Backstage's frontend, in our tutorial it would be `http://localhost:3000`. The "Authorization callback URL" will point to the auth backend, which will most likely be `http://localhost:7007/api/auth/github/handler/frame`. + +![Screenshot of the GitHub OAuth creation page](../../assets/getting-started/gh-oauth.png) + +Take note of the `Client ID` and the `Client Secret` (clicking the "Generate a new client secret" button will get this value for you). Open `app-config.yaml`, and add them as `clientId` and `clientSecret` in this file. It should end up looking like this: + +```yaml title="app-config.yaml" +auth: + # see https://backstage.io/docs/auth/ to learn about auth providers + /* highlight-add-start */ + environment: development + /* highlight-add-end */ + providers: + # See https://backstage.io/docs/auth/guest/provider + guest: {} + /* highlight-add-start */ + github: + development: + clientId: YOUR CLIENT ID + clientSecret: YOUR CLIENT SECRET + /* highlight-add-end */ +``` + +## Add sign-in option to the frontend + +The next step is to change the sign-in page. For this, you'll actually need to write some code. + +First let's add the packages we need, do this from the root: + +```shell +yarn --cwd packages/app add @backstage/core-plugin-api @backstage/plugin-app-react +``` + +Then open `packages/app/src/App.tsx` and below the last `import` line, add: + +```typescript title="packages/app/src/App.tsx" +import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { SignInPage } from '@backstage/core-components'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; +``` + +Now below this we are going to use the `SignInPageBlueprint` to create an extension, add this code block to do that: + +```tsx +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => + ( + + ), + }, +}); +``` + +Search for `const app = createApp({` in this file, and replace: + +```tsx title="packages/app/src/App.tsx" +export default createApp({ + features: [catalogPlugin, navModule], +}); +``` + +with + +```tsx title="packages/app/src/App.tsx" +export default createApp({ + features: [ + catalogPlugin, + navModule, + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + ], +}); +``` + +## Add sign-in resolver(s) + +Next we need to add the sign-in resolver to our configuration. Here's how: + +```yaml title="app-config.yaml" +auth: + # see https://backstage.io/docs/auth/ to learn about auth providers + environment: development + providers: + # See https://backstage.io/docs/auth/guest/provider + guest: {} + github: + development: + clientId: YOUR CLIENT ID + clientSecret: YOUR CLIENT SECRET + /* highlight-add-start */ + signIn: + resolvers: + # Matches the GitHub username with the Backstage user entity name. + # See https://backstage.io/docs/auth/github/provider#resolvers for more resolvers. + - resolver: usernameMatchingUserEntityName + /* highlight-add-end */ +``` + +What this will do is take the user details provided by the auth provider and match that against a User in the Catalog. In this case - `usernameMatchingUserEntityName` - will match the GitHub user name with the `metadata.name` value of a User in the Catalog, if none is found you will get an "Failed to sign-in, unable to resolve user identity" message. We'll cover this in the next few sections. + +Learn more about this topic in the [Sign-in Resolvers](../../auth/identity-resolver.md#sign-in-resolvers) documentation. + +## Add the auth provider to the backend + +To add the auth provider to the backend, we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-github-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-github-provider')); +/* highlight-add-end */ +``` + +Restart Backstage from the terminal, by stopping it with `Ctrl+C`, and starting it with `yarn start`. You should be welcomed by a login prompt! If you try to login at this point you will get a "Failed to sign-in, unable to resolve user identity" message, read on as we'll fix that next. + +:::note Note + +Sometimes the frontend starts before the backend resulting in errors on the sign in page. Wait for the backend to start and then reload Backstage to proceed. + +::: + +## Adding a User + +The recommended approach for adding Users, and Groups, into your Catalog is to use one of the existing Org Entity Providers - [like this one for GitHub](https://backstage.io/docs/integrations/github/org) - or if those don't work you may need to [create one](https://backstage.io/docs/features/software-catalog/external-integrations#custom-entity-providers) that fits your Organization's needs. + +For the sake of this guide we'll simply step you though adding a User to the `org.yaml` file that is included when you create a new Backstage instance. Let's do that: + +1. First open the `/examples/org.yaml` file in your text editor of choice +2. At the bottom we'll add the following YAML: + + ```yaml + --- + apiVersion: backstage.io/v1alpha1 + kind: User + metadata: + name: YOUR GITHUB USERNAME + spec: + memberOf: [guests] + ``` + +3. Now make sure to replace the text "YOUR GITHUB USERNAME" with your actual GitHub User name. + +Let's restart Backstage from the terminal once more, by stopping it with `Ctrl+C`, and starting it with `yarn start`. You should now be able to log into Backstage and see items in your Catalog. + +To learn more about Authentication in Backstage, here are some docs you +could read: + +- [Authentication in Backstage](../../auth/index.md) +- [Using organizational data from GitHub](../../integrations/github/org.md) + +## Setting up a GitHub Integration + +The GitHub integration supports loading catalog entities from GitHub or GitHub Enterprise. Entities can be added to static catalog configuration, registered with the catalog-import plugin, or discovered from a GitHub organization. Users and Groups can also be loaded from an organization. While using [GitHub Apps](../../integrations/github/github-apps.md) might be the best way to set up integrations, for this tutorial you'll use a Personal Access Token. + +Create your Personal Access Token by opening [the GitHub token creation page](https://github.com/settings/tokens/new). Use a name to identify this token and put it in the notes field. Choose a number of days for expiration. If you have a hard time picking a number, we suggest to go for 7 days, it's a lucky number. + +![Screenshot of the GitHub Personal Access Token creation page](../../assets/getting-started/gh-pat.png) + +Set the scope to your likings. For this tutorial, selecting `repo` and `workflow` is required as the scaffolding job in this guide configures a GitHub actions workflow for the newly created project. + +For this tutorial, we will be writing the token to `app-config.local.yaml`. This file might not exist for you, so if it doesn't go ahead and create it alongside the `app-config.yaml` at the root of the project. This file should also be excluded in `.gitignore`, to avoid accidental committing of this file. More details on this file can be found in the [Static Configuration documentation](../../conf/index.md). + +In your `app-config.local.yaml` go ahead and add the following: + +```yaml title="app-config.local.yaml" +integrations: + github: + - host: github.com + token: ghp_urtokendeinfewinfiwebfweb # this should be the token from GitHub +``` + +That's settled. This information will be leveraged by other plugins. + +If you're looking for a more production way to manage this secret, then you can do the following with the token being stored in an environment variable called `GITHUB_TOKEN`. + +```yaml title="app-config.local.yaml" +integrations: + github: + - host: github.com + token: ${GITHUB_TOKEN} # this will use the environment variable GITHUB_TOKEN +``` + +:::note Note + +If you've updated the configuration for your integration, it's likely that the backend will need a restart to apply these changes. To do this, stop the running instance in your terminal with `Control-C`, then start it again with `yarn start`. Once the backend has restarted, retry the operation. + +::: + +Some helpful links, for if you want to learn more about: + +- [Other available integrations](../../integrations/index.md) +- [Using GitHub Apps instead of a Personal Access Token](../../integrations/github/github-apps.md#docsNav) diff --git a/docs/getting-started/config/authentication.md b/docs/getting-started/config/authentication.md index e25f357ff830ef..3585ae066d8f03 100644 --- a/docs/getting-started/config/authentication.md +++ b/docs/getting-started/config/authentication.md @@ -4,6 +4,10 @@ title: Authentication description: How to setup authentication for your Backstage app --- +:::info +This documentation is written for the old frontend system. If you are on the [new frontend system](../../frontend-system/index.md) you may want to read [its own article](./authentication--new.md) instead. +::: + Audience: Admins or Developers ## Summary From b7f37a60ad74cd0f262836441bb699c2f946bf70 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 1 Mar 2026 09:20:19 -0600 Subject: [PATCH 26/55] Fixed CI and addressed comments Signed-off-by: Andre Wanlin --- docs/auth/index--new.md | 6 +++--- docs/auth/index.md | 2 +- docs/getting-started/config/authentication--new.md | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/auth/index--new.md b/docs/auth/index--new.md index 0a2e980658c6e6..b1948f3a7ef8a8 100644 --- a/docs/auth/index--new.md +++ b/docs/auth/index--new.md @@ -1,11 +1,11 @@ --- -id: index +id: index--new title: Authentication in Backstage description: Introduction to authentication in Backstage --- :::info -This documentation is written for [the new frontend system](../../frontend-system/index.md). If you are on the old frontend system you may want to read [its own article](./index.md) instead. +This documentation is written for [the new frontend system](../frontend-system/index.md). If you are on the old frontend system you may want to read [its own article](./index.md) instead. ::: The authentication system in Backstage serves two distinct purposes: sign-in and identification of users, as well as delegating access to third-party resources. It is possible to configure Backstage to have any number of authentication providers, but only one of these will typically be used for sign-in, with the rest being used to provide access to external resources. @@ -358,7 +358,7 @@ The method with which frontend plugins request access to third-party services is There are generic authentication providers for OAuth2 and SAML. These can reduce the amount of code needed to implement a custom authentication provider that adheres to these standards. -Backstage uses [Passport](http://www.passportjs.org/) under the hood, which has a wide library of authentication strategies for different providers. See[Add authentication provider](add-auth-provider.md) for details on adding a new Passport-supported authentication method. +Backstage uses [Passport](http://www.passportjs.org/) under the hood, which has a wide library of authentication strategies for different providers. See [Add authentication provider](add-auth-provider.md) for details on adding a new Passport-supported authentication method. ## Custom ScmAuthApi Implementation diff --git a/docs/auth/index.md b/docs/auth/index.md index 892f7a408f09da..6b6b57d5033bf2 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -5,7 +5,7 @@ description: Introduction to authentication in Backstage --- :::info -This documentation is written for the old frontend system. If you are on the [new frontend system](../../frontend-system/index.md) you may want to read [its own article](./index--new.md) instead. +This documentation is written for the old frontend system. If you are on the [new frontend system](../frontend-system/index.md) you may want to read [its own article](./index--new.md) instead. ::: The authentication system in Backstage serves two distinct purposes: sign-in and diff --git a/docs/getting-started/config/authentication--new.md b/docs/getting-started/config/authentication--new.md index 04b1e2b46b578e..ee71bf791351f2 100644 --- a/docs/getting-started/config/authentication--new.md +++ b/docs/getting-started/config/authentication--new.md @@ -1,5 +1,5 @@ --- -id: authentication +id: authentication--new title: Authentication description: How to setup authentication for your Backstage app --- @@ -14,7 +14,7 @@ Audience: Admins or Developers We'll be walking you through how to setup authentication for your Backstage app using GitHub. After finishing this guide, you'll have both working authentication and users in your Backstage app to match to the users logging in! -There are multiple authentication providers available for you to use with Backstage, feel free to follow [their instructions for adding authentication](../../auth/index.md). +There are multiple authentication providers available for you to use with Backstage, feel free to follow [their instructions for adding authentication](../../auth/index--new.md). :::note Note @@ -90,7 +90,7 @@ const signInPage = SignInPageBlueprint.make({ }); ``` -Search for `const app = createApp({` in this file, and replace: +Search for the `createApp()` function call in this file, and replace: ```tsx title="packages/app/src/App.tsx" export default createApp({ @@ -137,7 +137,7 @@ auth: /* highlight-add-end */ ``` -What this will do is take the user details provided by the auth provider and match that against a User in the Catalog. In this case - `usernameMatchingUserEntityName` - will match the GitHub user name with the `metadata.name` value of a User in the Catalog, if none is found you will get an "Failed to sign-in, unable to resolve user identity" message. We'll cover this in the next few sections. +What this will do is take the user details provided by the auth provider and match that against a User in the Catalog. In this case - `usernameMatchingUserEntityName` - will match the GitHub user name with the `metadata.name` value of a User in the Catalog, if none is found you will get a "Failed to sign-in, unable to resolve user identity" message. We'll cover this in the next few sections. Learn more about this topic in the [Sign-in Resolvers](../../auth/identity-resolver.md#sign-in-resolvers) documentation. @@ -170,7 +170,7 @@ Sometimes the frontend starts before the backend resulting in errors on the sign The recommended approach for adding Users, and Groups, into your Catalog is to use one of the existing Org Entity Providers - [like this one for GitHub](https://backstage.io/docs/integrations/github/org) - or if those don't work you may need to [create one](https://backstage.io/docs/features/software-catalog/external-integrations#custom-entity-providers) that fits your Organization's needs. -For the sake of this guide we'll simply step you though adding a User to the `org.yaml` file that is included when you create a new Backstage instance. Let's do that: +For the sake of this guide we'll simply step you through adding a User to the `org.yaml` file that is included when you create a new Backstage instance. Let's do that: 1. First open the `/examples/org.yaml` file in your text editor of choice 2. At the bottom we'll add the following YAML: @@ -192,7 +192,7 @@ Let's restart Backstage from the terminal once more, by stopping it with `Ctrl+C To learn more about Authentication in Backstage, here are some docs you could read: -- [Authentication in Backstage](../../auth/index.md) +- [Authentication in Backstage](../../auth/index--new.md) - [Using organizational data from GitHub](../../integrations/github/org.md) ## Setting up a GitHub Integration From 219490cd92410634d326995c4cda81d0cd885e29 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 16:45:11 +0100 Subject: [PATCH 27/55] create-app: Fix E2E test for new default template Add @backstage/cli-module-new to the version map so that the next-app template can resolve it during templating, and update the E2E test to build the dist workspace from the next-app template since it is now the default. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/create-app/src/lib/versions.ts | 2 ++ packages/e2e-test/src/commands/runCommand.ts | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 006dd6d7b8c343..9b10660d19e3be 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -39,6 +39,7 @@ import { version as catalogClient } from '../../../catalog-client/package.json'; import { version as catalogModel } from '../../../catalog-model/package.json'; import { version as cli } from '../../../cli/package.json'; import { version as cliDefaults } from '../../../cli-defaults/package.json'; +import { version as cliModuleNew } from '../../../cli-module-new/package.json'; import { version as config } from '../../../config/package.json'; import { version as coreAppApi } from '../../../core-app-api/package.json'; import { version as coreCompatApi } from '../../../core-compat-api/package.json'; @@ -109,6 +110,7 @@ export const packageVersions = { '@backstage/catalog-model': catalogModel, '@backstage/cli': cli, '@backstage/cli-defaults': cliDefaults, + '@backstage/cli-module-new': cliModuleNew, '@backstage/config': config, '@backstage/core-app-api': coreAppApi, '@backstage/core-compat-api': coreCompatApi, diff --git a/packages/e2e-test/src/commands/runCommand.ts b/packages/e2e-test/src/commands/runCommand.ts index 40aa8a75bd1c37..48fd1c862aa0a7 100644 --- a/packages/e2e-test/src/commands/runCommand.ts +++ b/packages/e2e-test/src/commands/runCommand.ts @@ -35,9 +35,9 @@ const ownPaths = findOwnPaths(__dirname); const templatePackagePaths = [ 'packages/cli-module-new/templates/frontend-plugin/package.json.hbs', - 'packages/create-app/templates/default-app/package.json.hbs', - 'packages/create-app/templates/default-app/packages/app/package.json.hbs', - 'packages/create-app/templates/default-app/packages/backend/package.json.hbs', + 'packages/create-app/templates/next-app/package.json.hbs', + 'packages/create-app/templates/next-app/packages/app/package.json.hbs', + 'packages/create-app/templates/next-app/packages/backend/package.json.hbs', ]; export async function runCommand(opts: OptionValues) { From 49171c9de48b1e89f9d30a15e18efb979970a698 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Sun, 11 Jan 2026 21:55:47 +0100 Subject: [PATCH 28/55] chore: Update all imports to zod/v3 Signed-off-by: Gabriel Dugny --- docs/features/software-templates/writing-custom-actions.md | 2 +- .../software-templates/writing-custom-field-extensions.md | 2 +- docs/permissions/custom-rules.md | 2 +- .../plugin-authors/03-adding-a-resource-permission-check.md | 2 +- .../actionsRegistry/DefaultActionsRegistryService.ts | 2 +- packages/backend-defaults/src/entrypoints/auditor/types.ts | 2 +- packages/backend-defaults/src/entrypoints/auditor/utils.ts | 2 +- .../backend-defaults/src/entrypoints/scheduler/lib/types.ts | 2 +- packages/backend-plugin-api/report-alpha.api.md | 4 ++-- .../backend-plugin-api/src/alpha/ActionsRegistryService.ts | 2 +- .../src/services/definitions/ActionsRegistryService.ts | 2 +- packages/backend-test-utils/report-alpha.api.md | 2 +- .../src/alpha/services/MockActionsRegistry.ts | 2 +- .../catalog-model/examples/apis/hello-world-trpc-api.yaml | 2 +- packages/cli-module-auth/src/lib/auth.ts | 2 +- packages/cli-module-auth/src/lib/storage.ts | 2 +- .../src/lib/preparation/loadPortableTemplate.ts | 2 +- .../src/lib/preparation/loadPortableTemplateConfig.ts | 2 +- .../cli-module-new/templates/backend-plugin/src/router.ts | 2 +- packages/cli-node/src/roles/PackageRoles.ts | 2 +- packages/cli-node/src/yarn/yarnPlugin.ts | 2 +- .../core-app-api/src/apis/implementations/auth/saml/types.ts | 2 +- .../src/lib/AuthSessionManager/AuthSessionStore.test.ts | 2 +- .../src/lib/AuthSessionManager/AuthSessionStore.ts | 2 +- .../src/layout/ProxiedSignInPage/types.test.ts | 2 +- .../core-components/src/layout/ProxiedSignInPage/types.ts | 2 +- packages/filter-predicates/src/predicates/schema.test.ts | 2 +- packages/frontend-plugin-api/report.api.md | 2 +- .../frontend-plugin-api/src/schema/createSchemaFromZod.ts | 2 +- packages/frontend-plugin-api/src/wiring/createExtension.ts | 2 +- .../src/wiring/createExtensionBlueprint.ts | 2 +- packages/repo-tools/src/commands/package-docs/Cache.ts | 2 +- .../scaffolder-internal/src/wiring/InternalFormDecorator.ts | 2 +- packages/scaffolder-internal/src/wiring/InternalFormField.ts | 2 +- plugins/api-docs/dev/trpc-example-api.yaml | 2 +- .../auth-backend-module-atlassian-provider/src/resolvers.ts | 2 +- plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts | 2 +- .../src/resolvers.ts | 2 +- .../auth-backend-module-bitbucket-provider/src/resolvers.ts | 2 +- .../src/resolvers.ts | 2 +- .../src/resolvers.ts | 2 +- plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts | 2 +- plugins/auth-backend-module-github-provider/src/resolvers.ts | 2 +- plugins/auth-backend-module-gitlab-provider/src/resolvers.ts | 2 +- plugins/auth-backend-module-google-provider/src/resolvers.ts | 2 +- .../auth-backend-module-microsoft-provider/src/resolvers.ts | 2 +- plugins/auth-backend-module-oauth2-provider/src/resolvers.ts | 2 +- .../src/resolvers.ts | 2 +- plugins/auth-backend-module-okta-provider/src/resolvers.ts | 2 +- .../auth-backend-module-onelogin-provider/src/resolvers.ts | 2 +- .../src/authenticator.ts | 2 +- .../auth-backend-module-openshift-provider/src/resolvers.ts | 2 +- plugins/auth-backend/src/service/OidcRouter.ts | 2 +- plugins/auth-node/report.api.md | 4 ++-- plugins/auth-node/src/sign-in/commonSignInResolvers.ts | 2 +- plugins/auth-node/src/sign-in/createSignInResolverFactory.ts | 2 +- plugins/catalog-backend/src/ingestion/CatalogRules.ts | 2 +- .../src/permissions/rules/createPropertyRule.ts | 2 +- .../catalog-backend/src/permissions/rules/hasAnnotation.ts | 2 +- plugins/catalog-backend/src/permissions/rules/hasLabel.ts | 2 +- plugins/catalog-backend/src/permissions/rules/isEntityKind.ts | 2 +- .../catalog-backend/src/permissions/rules/isEntityOwner.ts | 2 +- plugins/catalog-backend/src/service/createRouter.test.ts | 2 +- plugins/catalog-backend/src/service/createRouter.ts | 2 +- .../src/service/request/entitiesBatchRequest.ts | 1 - plugins/catalog-backend/src/service/util.ts | 2 +- plugins/home/src/components/CustomHomepage/types.ts | 2 +- .../src/service/PermissionIntegrationClient.test.ts | 2 +- .../src/service/PermissionIntegrationClient.ts | 2 +- plugins/permission-backend/src/service/router.ts | 2 +- plugins/permission-common/src/PermissionClient.ts | 2 +- plugins/permission-node/report.api.md | 2 +- .../src/integration/createConditionExports.test.ts | 2 +- .../src/integration/createConditionFactory.test.ts | 2 +- .../src/integration/createConditionTransformer.test.ts | 2 +- .../src/integration/createPermissionIntegrationRouter.test.ts | 2 +- .../src/integration/createPermissionIntegrationRouter.ts | 2 +- .../permission-node/src/integration/createPermissionRule.ts | 2 +- plugins/permission-node/src/types.ts | 2 +- .../src/actions/inputProperties.ts | 2 +- plugins/scaffolder-backend-module-cookiecutter/report.api.md | 4 ++-- .../src/actions/inputProperties.ts | 2 +- .../src/actions/outputProperties.ts | 2 +- .../src/commonGitlabConfig.ts | 2 +- plugins/scaffolder-backend-module-gitlab/src/util.ts | 2 +- .../src/scaffolder/actions/builtin/filesystem/read.ts | 2 +- plugins/scaffolder-backend/src/service/router.ts | 2 +- plugins/scaffolder-backend/src/service/rules.ts | 2 +- plugins/scaffolder-backend/src/util/templating.ts | 2 +- plugins/scaffolder-node/report-alpha.api.md | 2 +- plugins/scaffolder-node/report.api.md | 2 +- plugins/scaffolder-node/src/actions/createTemplateAction.ts | 2 +- plugins/scaffolder-node/src/actions/util.ts | 2 +- .../scaffolder-node/src/alpha/filters/createTemplateFilter.ts | 2 +- plugins/scaffolder-node/src/alpha/filters/types.ts | 2 +- .../scaffolder-node/src/alpha/globals/createTemplateGlobal.ts | 2 +- plugins/scaffolder-node/src/alpha/globals/types.ts | 2 +- plugins/scaffolder-node/src/alpha/types.ts | 2 +- plugins/scaffolder-react/report-alpha.api.md | 2 +- plugins/scaffolder-react/report.api.md | 2 +- .../src/next/blueprints/FormFieldBlueprint.tsx | 2 +- plugins/scaffolder-react/src/next/blueprints/types.ts | 2 +- .../src/next/extensions/createScaffolderFormDecorator.ts | 2 +- plugins/scaffolder-react/src/utils.ts | 2 +- plugins/scaffolder/report.api.md | 2 +- .../scaffolder/src/components/fields/EntityPicker/schema.ts | 2 +- .../src/components/fields/MultiEntityPicker/schema.ts | 2 +- plugins/scaffolder/src/components/fields/utils.ts | 2 +- plugins/search-backend/src/service/router.ts | 2 +- plugins/user-settings-backend/src/service/router.ts | 2 +- scripts/verify-plugin-directory.js | 2 +- 111 files changed, 113 insertions(+), 114 deletions(-) diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index f5ed9e48547086..744c15ed75a87b 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -56,7 +56,7 @@ its generated unit test. We will replace the existing placeholder code with our import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import fs from 'fs-extra'; -import { type z } from 'zod'; +import { type z } from 'zod/v3'; export const createNewFileAction = () => { return createTemplateAction({ diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index 3fe4d82a1fa517..f2e8486a07fb6c 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -290,7 +290,7 @@ and type for your field props to preventing having to duplicate the definitions: ```tsx //packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx ... -import { z } from 'zod'; +import { z } from 'zod/v3'; import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder'; const MyCustomExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod( diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 4b188ce7f91c87..039805bcb42c61 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -23,7 +23,7 @@ import { createConditionFactory, createPermissionRule, } from '@backstage/plugin-permission-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; export const isInSystemRule = createPermissionRule({ name: 'IS_IN_SYSTEM', diff --git a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md index 1e1a73607ba205..afdb3b8c62c3db 100644 --- a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md +++ b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md @@ -126,7 +126,7 @@ import { createPermissionRule, } from '@backstage/plugin-permission-node'; import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Todo, TodoFilter } from './todos'; export const todoListPermissionResourceRef = createPermissionResourceRef< diff --git a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts index 73b5126ab5cb91..01255db10c3639 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts @@ -25,7 +25,7 @@ import { } from '@backstage/backend-plugin-api'; import PromiseRouter from 'express-promise-router'; import { Router, json } from 'express'; -import { z, AnyZodObject } from 'zod'; +import { z, AnyZodObject } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; import { ActionsRegistryActionOptions, diff --git a/packages/backend-defaults/src/entrypoints/auditor/types.ts b/packages/backend-defaults/src/entrypoints/auditor/types.ts index 7827a31d1f8721..49a6de75eaa0eb 100644 --- a/packages/backend-defaults/src/entrypoints/auditor/types.ts +++ b/packages/backend-defaults/src/entrypoints/auditor/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; /** @internal */ export const severityLogLevelMappingsSchema = z.record( diff --git a/packages/backend-defaults/src/entrypoints/auditor/utils.ts b/packages/backend-defaults/src/entrypoints/auditor/utils.ts index 719a227bf97d05..bf1e1f47560028 100644 --- a/packages/backend-defaults/src/entrypoints/auditor/utils.ts +++ b/packages/backend-defaults/src/entrypoints/auditor/utils.ts @@ -16,7 +16,7 @@ import type { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { CONFIG_ROOT_KEY, severityLogLevelMappingsSchema } from './types'; /** diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts index f691ef78a072c2..c7f77e7a850f4d 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts @@ -17,7 +17,7 @@ import { JsonObject } from '@backstage/types'; import { CronTime } from 'cron'; import { Duration } from 'luxon'; -import { z } from 'zod'; +import { z } from 'zod/v3'; function isValidOptionalDurationString(d: string | undefined): boolean { try { diff --git a/packages/backend-plugin-api/report-alpha.api.md b/packages/backend-plugin-api/report-alpha.api.md index 0f2c220609505d..72d9a2561e3f93 100644 --- a/packages/backend-plugin-api/report-alpha.api.md +++ b/packages/backend-plugin-api/report-alpha.api.md @@ -3,7 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { AnyZodObject } from 'zod'; +import { AnyZodObject } from 'zod/v3'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { BasicPermission } from '@backstage/plugin-permission-common'; import { JsonObject } from '@backstage/types'; @@ -11,7 +11,7 @@ import { JSONSchema7 } from 'json-schema'; import { JsonValue } from '@backstage/types'; import { LoggerService } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; -import { z } from 'zod'; +import { z } from 'zod/v3'; // @alpha (undocumented) export type ActionsRegistryActionContext = { diff --git a/packages/backend-plugin-api/src/alpha/ActionsRegistryService.ts b/packages/backend-plugin-api/src/alpha/ActionsRegistryService.ts index a707e350e95b66..20b4d769ccd44d 100644 --- a/packages/backend-plugin-api/src/alpha/ActionsRegistryService.ts +++ b/packages/backend-plugin-api/src/alpha/ActionsRegistryService.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z, AnyZodObject } from 'zod'; +import { z, AnyZodObject } from 'zod/v3'; import { BasicPermission } from '@backstage/plugin-permission-common'; import { LoggerService, diff --git a/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts b/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts index a17c82c6f29776..05f6879f0711b2 100644 --- a/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts +++ b/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z, AnyZodObject } from 'zod'; +import { z, AnyZodObject } from 'zod/v3'; import { BasicPermission } from '@backstage/plugin-permission-common'; import { LoggerService } from './LoggerService'; import { BackstageCredentials } from './AuthService'; diff --git a/packages/backend-test-utils/report-alpha.api.md b/packages/backend-test-utils/report-alpha.api.md index b6bf9e883d79bf..4813f49c35a0ba 100644 --- a/packages/backend-test-utils/report-alpha.api.md +++ b/packages/backend-test-utils/report-alpha.api.md @@ -7,7 +7,7 @@ import { ActionsRegistryActionOptions } from '@backstage/backend-plugin-api/alph import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; import { ActionsService } from '@backstage/backend-plugin-api/alpha'; import { ActionsServiceAction } from '@backstage/backend-plugin-api/alpha'; -import { AnyZodObject } from 'zod'; +import { AnyZodObject } from 'zod/v3'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; diff --git a/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts b/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts index 94d844746f88d7..03edb42fad3909 100644 --- a/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts +++ b/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts @@ -19,7 +19,7 @@ import { } from '@backstage/backend-plugin-api'; import { InputError, NotFoundError } from '@backstage/errors'; import { JsonObject, JsonValue } from '@backstage/types'; -import { z, AnyZodObject } from 'zod'; +import { z, AnyZodObject } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; import { mockCredentials } from '../../services'; import { diff --git a/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml b/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml index f5807537b3e6b4..f1847154ced74c 100644 --- a/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml +++ b/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml @@ -8,7 +8,7 @@ spec: lifecycle: experimental owner: team-c definition: | - import { z } from 'zod'; + import { z } from 'zod/v3'; import { publicProcedure, router } from '../trpc'; export const apiRouter = router({ diff --git a/packages/cli-module-auth/src/lib/auth.ts b/packages/cli-module-auth/src/lib/auth.ts index 0344d4636e95ca..36bdf3dfbce9fb 100644 --- a/packages/cli-module-auth/src/lib/auth.ts +++ b/packages/cli-module-auth/src/lib/auth.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { StoredInstance, upsertInstance, diff --git a/packages/cli-module-auth/src/lib/storage.ts b/packages/cli-module-auth/src/lib/storage.ts index 9d2551a4f9743d..0e7668a0eafd0d 100644 --- a/packages/cli-module-auth/src/lib/storage.ts +++ b/packages/cli-module-auth/src/lib/storage.ts @@ -20,7 +20,7 @@ import os from 'node:os'; import path from 'node:path'; import lockfile from 'proper-lockfile'; import YAML from 'yaml'; -import { z } from 'zod'; +import { z } from 'zod/v3'; const METADATA_FILE = 'auth-instances.yaml'; diff --git a/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts index 6a683fb31c8d64..13bae23f3c7ce3 100644 --- a/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts +++ b/packages/cli-module-new/src/lib/preparation/loadPortableTemplate.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import fs from 'fs-extra'; import recursiveReaddir from 'recursive-readdir'; import { resolve as resolvePath, relative as relativePath } from 'node:path'; diff --git a/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts index 50ffda751649e7..06625c79f87ffe 100644 --- a/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts +++ b/packages/cli-module-new/src/lib/preparation/loadPortableTemplateConfig.ts @@ -25,7 +25,7 @@ import { TEMPLATE_FILE_NAME, } from '../types'; import { parse as parseYaml } from 'yaml'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { fromZodError } from 'zod-validation-error/v3'; import { ForwardedError } from '@backstage/errors'; diff --git a/packages/cli-module-new/templates/backend-plugin/src/router.ts b/packages/cli-module-new/templates/backend-plugin/src/router.ts index fba8a24cb29348..0fbbc54cc36512 100644 --- a/packages/cli-module-new/templates/backend-plugin/src/router.ts +++ b/packages/cli-module-new/templates/backend-plugin/src/router.ts @@ -1,6 +1,6 @@ import { HttpAuthService } from '@backstage/backend-plugin-api'; import { InputError } from '@backstage/errors'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import express from 'express'; import Router from 'express-promise-router'; import { todoListServiceRef } from './services/TodoListService'; diff --git a/packages/cli-node/src/roles/PackageRoles.ts b/packages/cli-node/src/roles/PackageRoles.ts index 7761b0d4cc85c5..2987a7cbda4fb1 100644 --- a/packages/cli-node/src/roles/PackageRoles.ts +++ b/packages/cli-node/src/roles/PackageRoles.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { PackageRole, PackageRoleInfo } from './types'; const packageRoleInfos: PackageRoleInfo[] = [ diff --git a/packages/cli-node/src/yarn/yarnPlugin.ts b/packages/cli-node/src/yarn/yarnPlugin.ts index 65383edd0c825a..78f6c724be3625 100644 --- a/packages/cli-node/src/yarn/yarnPlugin.ts +++ b/packages/cli-node/src/yarn/yarnPlugin.ts @@ -17,7 +17,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import yaml from 'yaml'; -import z from 'zod'; +import z from 'zod/v3'; import { targetPaths } from '@backstage/cli-common'; const yarnRcSchema = z.object({ diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts index e5e32e34575219..bc3e0c20fb3eaf 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts @@ -18,7 +18,7 @@ import { BackstageIdentityResponse, ProfileInfo, } from '@backstage/core-plugin-api'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** @internal */ export type SamlSession = { diff --git a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts index 3f8622890bd0ea..b1bba120274b04 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts @@ -15,7 +15,7 @@ */ import { withLogCollector } from '@backstage/test-utils'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { AuthSessionStore } from './AuthSessionStore'; import { SessionManager } from './types'; diff --git a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts index e1fb80d41f8ab6..d3be6748bfd25a 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ZodSchema } from 'zod'; +import { ZodSchema } from 'zod/v3'; import { MutableSessionManager, SessionScopesFunc, diff --git a/packages/core-components/src/layout/ProxiedSignInPage/types.test.ts b/packages/core-components/src/layout/ProxiedSignInPage/types.test.ts index 178714ecf90d91..d3088e5f1970db 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/types.test.ts +++ b/packages/core-components/src/layout/ProxiedSignInPage/types.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TypeOf } from 'zod'; +import { TypeOf } from 'zod/v3'; import { ProxiedSession, proxiedSessionSchema } from './types'; describe('types', () => { diff --git a/packages/core-components/src/layout/ProxiedSignInPage/types.ts b/packages/core-components/src/layout/ProxiedSignInPage/types.ts index dd4230cd8b932a..bf237aabc7b66f 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/types.ts +++ b/packages/core-components/src/layout/ProxiedSignInPage/types.ts @@ -18,7 +18,7 @@ import { BackstageIdentityResponse, ProfileInfo, } from '@backstage/core-plugin-api'; -import { z } from 'zod'; +import { z } from 'zod/v3'; export const proxiedSessionSchema = z.object({ providerInfo: z.object({}).catchall(z.unknown()).optional(), diff --git a/packages/filter-predicates/src/predicates/schema.test.ts b/packages/filter-predicates/src/predicates/schema.test.ts index 08c92c20e882dc..ff38dabfce3b3a 100644 --- a/packages/filter-predicates/src/predicates/schema.test.ts +++ b/packages/filter-predicates/src/predicates/schema.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { createZodV3FilterPredicateSchema, parseFilterPredicate, diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index b9c5bf269aa2c3..c3936ba0da65f4 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -23,7 +23,7 @@ import { Observable } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { ReactNode } from 'react'; import { SwappableComponentRef as SwappableComponentRef_2 } from '@backstage/frontend-plugin-api'; -import type { z } from 'zod'; +import type { z } from 'zod/v3'; // @public @deprecated export type AlertApi = { diff --git a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts index b15ff980ec0971..6dbac61c62d255 100644 --- a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts +++ b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts @@ -15,7 +15,7 @@ */ import { JsonObject } from '@backstage/types'; -import { z, type ZodSchema, type ZodTypeDef } from 'zod'; +import { z, type ZodSchema, type ZodTypeDef } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; import { PortableSchema } from './types'; diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 8632b3caebdfd8..1b5869460a8e9e 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -26,7 +26,7 @@ import { } from '@internal/frontend'; import { ExtensionDataRef, ExtensionDataValue } from './createExtensionDataRef'; import { ExtensionInput } from './createExtensionInput'; -import type { z } from 'zod'; +import type { z } from 'zod/v3'; import { createSchemaFromZod } from '../schema/createSchemaFromZod'; import { OpaqueExtensionDefinition } from '@internal/frontend'; import { ExtensionDataContainer } from './types'; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 2d2288b1204a09..cd593f3c93b4c4 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -26,7 +26,7 @@ import { ctxParamsSymbol, VerifyExtensionAttachTo, } from './createExtension'; -import type { z } from 'zod'; +import type { z } from 'zod/v3'; import { ExtensionInput } from './createExtensionInput'; import { ExtensionDataRef, ExtensionDataValue } from './createExtensionDataRef'; import { createExtensionDataContainer } from '@internal/frontend'; diff --git a/packages/repo-tools/src/commands/package-docs/Cache.ts b/packages/repo-tools/src/commands/package-docs/Cache.ts index be005ac2807947..0ca38c1c39188b 100644 --- a/packages/repo-tools/src/commands/package-docs/Cache.ts +++ b/packages/repo-tools/src/commands/package-docs/Cache.ts @@ -19,7 +19,7 @@ import { dirname, join as joinPath, relative } from 'node:path'; import crypto from 'node:crypto'; import { Lockfile } from '@backstage/cli-node'; import { exists, rm, mkdirp } from 'fs-extra'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { CACHE_DIR, CACHE_FILE } from './constants'; const version = '1'; diff --git a/packages/scaffolder-internal/src/wiring/InternalFormDecorator.ts b/packages/scaffolder-internal/src/wiring/InternalFormDecorator.ts index 7942544acb5b11..39de69a5bf1e8b 100644 --- a/packages/scaffolder-internal/src/wiring/InternalFormDecorator.ts +++ b/packages/scaffolder-internal/src/wiring/InternalFormDecorator.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { OpaqueType } from '@internal/opaque'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { ScaffolderFormDecorator, diff --git a/packages/scaffolder-internal/src/wiring/InternalFormField.ts b/packages/scaffolder-internal/src/wiring/InternalFormField.ts index b711341ea5ef04..24e279f47f920e 100644 --- a/packages/scaffolder-internal/src/wiring/InternalFormField.ts +++ b/packages/scaffolder-internal/src/wiring/InternalFormField.ts @@ -15,7 +15,7 @@ */ import { OpaqueType } from '@internal/opaque'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { FormFieldExtensionData, diff --git a/plugins/api-docs/dev/trpc-example-api.yaml b/plugins/api-docs/dev/trpc-example-api.yaml index f5807537b3e6b4..f1847154ced74c 100644 --- a/plugins/api-docs/dev/trpc-example-api.yaml +++ b/plugins/api-docs/dev/trpc-example-api.yaml @@ -8,7 +8,7 @@ spec: lifecycle: experimental owner: team-c definition: | - import { z } from 'zod'; + import { z } from 'zod/v3'; import { publicProcedure, router } from '../trpc'; export const apiRouter = router({ diff --git a/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts b/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts index 949f25db9c5d68..21d07b772c13af 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Atlassian auth provider. diff --git a/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts b/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts index 38b4b60f270b12..f3a65c91083db5 100644 --- a/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { AwsAlbResult } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the AWS ALB auth provider. diff --git a/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts b/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts index 94afa90c5fee2f..b40b137a11bc0f 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-azure-easyauth-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { AzureEasyAuthResult } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** @public */ export namespace azureEasyAuthSignInResolvers { diff --git a/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts b/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts index 6691806e639ff3..bef3d2b34d8687 100644 --- a/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-bitbucket-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Bitbucket auth provider. diff --git a/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts b/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts index 2e92d8c6add334..96821669bd4571 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-bitbucket-server-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Bitbucket Server auth provider. diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts index 21cb124be4a5b1..aadd80b2a21ec3 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { CloudflareAccessResult } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Cloudflare Access auth provider. diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts index 77dc7b106227ff..9d57dc198e31f3 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { GcpIapResult } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Google auth provider. diff --git a/plugins/auth-backend-module-github-provider/src/resolvers.ts b/plugins/auth-backend-module-github-provider/src/resolvers.ts index e0654328269525..b66478d7a62384 100644 --- a/plugins/auth-backend-module-github-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-github-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { OAuthAuthenticatorResult, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { GithubProfile } from './authenticator'; diff --git a/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts b/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts index 0b6abd0f2860a0..42e30709af5289 100644 --- a/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { OAuthAuthenticatorResult, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { GitlabProfile } from './authenticator'; diff --git a/plugins/auth-backend-module-google-provider/src/resolvers.ts b/plugins/auth-backend-module-google-provider/src/resolvers.ts index 297ac0da6e9701..ed9b080b25625a 100644 --- a/plugins/auth-backend-module-google-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-google-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Google auth provider. diff --git a/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts b/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts index 0ce276522efffa..e27c40684457f9 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Microsoft auth provider. diff --git a/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts b/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts index bad3f015c83df1..f27f3c70a4445c 100644 --- a/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the oauth2 auth provider. diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts b/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts index 8ac639b3c06586..6453b2ab6e6129 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts @@ -19,7 +19,7 @@ import { SignInInfo, } from '@backstage/plugin-auth-node'; import { OAuth2ProxyResult } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * @public diff --git a/plugins/auth-backend-module-okta-provider/src/resolvers.ts b/plugins/auth-backend-module-okta-provider/src/resolvers.ts index cdb37dbaae2dac..35839b45813c41 100644 --- a/plugins/auth-backend-module-okta-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-okta-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the Okta auth provider. diff --git a/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts b/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts index 56710472beb37b..288e070530b81d 100644 --- a/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts @@ -20,7 +20,7 @@ import { PassportProfile, SignInInfo, } from '@backstage/plugin-auth-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Available sign-in resolvers for the OneLogin auth provider. diff --git a/plugins/auth-backend-module-openshift-provider/src/authenticator.ts b/plugins/auth-backend-module-openshift-provider/src/authenticator.ts index 0c9acb6287d5ee..9972c62adfdad5 100644 --- a/plugins/auth-backend-module-openshift-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-openshift-provider/src/authenticator.ts @@ -22,7 +22,7 @@ import { } from '@backstage/plugin-auth-node'; import { createHash } from 'node:crypto'; import OAuth2Strategy from 'passport-oauth2'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** @public */ export interface OpenShiftAuthenticatorContext { diff --git a/plugins/auth-backend-module-openshift-provider/src/resolvers.ts b/plugins/auth-backend-module-openshift-provider/src/resolvers.ts index dee55ec4caac93..f342b69133ae43 100644 --- a/plugins/auth-backend-module-openshift-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-openshift-provider/src/resolvers.ts @@ -25,7 +25,7 @@ import { DEFAULT_NAMESPACE, stringifyEntityRef, } from '@backstage/catalog-model'; -import { z } from 'zod'; +import { z } from 'zod/v3'; export namespace openshiftSignInResolvers { export const displayNameMatchingUserEntityName = createSignInResolverFactory({ diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index dc921a9489b81a..304bfa5ed9f7e0 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -27,7 +27,7 @@ import { UserInfoDatabase } from '../database/UserInfoDatabase'; import { OidcDatabase } from '../database/OidcDatabase'; import { OfflineAccessService } from './OfflineAccessService'; import { json } from 'express'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { fromZodError } from 'zod-validation-error'; import { OidcError } from './OidcError'; diff --git a/plugins/auth-node/report.api.md b/plugins/auth-node/report.api.md index a5939e6e24cf71..7cc68b44b3fcad 100644 --- a/plugins/auth-node/report.api.md +++ b/plugins/auth-node/report.api.md @@ -16,8 +16,8 @@ import { Profile } from 'passport'; import { Request as Request_2 } from 'express'; import { Response as Response_2 } from 'express'; import { Strategy } from 'passport'; -import { ZodSchema } from 'zod'; -import { ZodTypeDef } from 'zod'; +import { ZodSchema } from 'zod/v3'; +import { ZodTypeDef } from 'zod/v3'; // @public (undocumented) export interface AuthOwnershipResolutionExtensionPoint { diff --git a/plugins/auth-node/src/sign-in/commonSignInResolvers.ts b/plugins/auth-node/src/sign-in/commonSignInResolvers.ts index 2b7442f7469361..2d593ec44a4ae3 100644 --- a/plugins/auth-node/src/sign-in/commonSignInResolvers.ts +++ b/plugins/auth-node/src/sign-in/commonSignInResolvers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { createSignInResolverFactory } from './createSignInResolverFactory'; import { NotAllowedError } from '@backstage/errors'; diff --git a/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts b/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts index 2bcdbefbb26f63..e74be6dbb8a8ae 100644 --- a/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts +++ b/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ZodSchema, ZodTypeDef } from 'zod'; +import { ZodSchema, ZodTypeDef } from 'zod/v3'; import { SignInResolver } from '../types'; import zodToJsonSchema from 'zod-to-json-schema'; import { JsonObject } from '@backstage/types'; diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index cfac644e34c347..3a5fec589195b6 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -19,7 +19,7 @@ import { Entity } from '@backstage/catalog-model'; import path from 'node:path'; import { LocationSpec } from '@backstage/plugin-catalog-common'; import { minimatch } from 'minimatch'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Rules to apply to catalog entities. diff --git a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts index d0a37802e06ce9..a7f348763b66fc 100644 --- a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts +++ b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts @@ -17,7 +17,7 @@ import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; import { createPermissionRule } from '@backstage/plugin-permission-node'; import { get } from 'lodash'; -import { z } from 'zod'; +import { z } from 'zod/v3'; export const createPropertyRule = (propertyType: 'metadata' | 'spec') => createPermissionRule({ diff --git a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts index 32e0019715be9a..825eb75cb83fea 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts @@ -16,7 +16,7 @@ import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; import { createPermissionRule } from '@backstage/plugin-permission-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which diff --git a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts index 40a96dbf49b164..f4bc8f3feed323 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts @@ -16,7 +16,7 @@ import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; import { createPermissionRule } from '@backstage/plugin-permission-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts index 62916925560c79..c0d045bea942e4 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts @@ -16,7 +16,7 @@ import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; import { createPermissionRule } from '@backstage/plugin-permission-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts index de336ba9664144..74b22842e63cd9 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts @@ -16,7 +16,7 @@ import { RELATION_OWNED_BY } from '@backstage/catalog-model'; import { createPermissionRule } from '@backstage/plugin-permission-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; /** diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 88cd5abde31025..9d5944f664d176 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -41,7 +41,7 @@ import { import express from 'express'; import { Server } from 'node:http'; import request from 'supertest'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Cursor, EntitiesCatalog } from '../catalog/types'; import { applyDatabaseMigrations } from '../database/migrations'; import { DbLocationsRow } from '../database/tables'; diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 89377c9e12ca14..c2711a00ed5076 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -33,7 +33,7 @@ import { InputError, serializeError } from '@backstage/errors'; import { LocationAnalyzer } from '@backstage/plugin-catalog-node'; import express from 'express'; import yn from 'yn'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Cursor, EntitiesCatalog } from '../catalog/types'; import { CatalogProcessingOrchestrator } from '../processing/types'; import { validateEntityEnvelope } from '../processing/util'; diff --git a/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts b/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts index 02ad1d5566adae..09e40f45bc725d 100644 --- a/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts +++ b/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts @@ -22,7 +22,6 @@ import { import { Request } from 'express'; import { z } from 'zod/v3'; import { fromZodError } from 'zod-validation-error/v3'; - const filterPredicateSchema = createZodV3FilterPredicateSchema(z); const schema = z.object({ diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts index 708eec2de3a3aa..a83782a31b4641 100644 --- a/plugins/catalog-backend/src/service/util.ts +++ b/plugins/catalog-backend/src/service/util.ts @@ -18,7 +18,7 @@ import { InputError, NotAllowedError } from '@backstage/errors'; import { createZodV3FilterPredicateSchema } from '@backstage/filter-predicates'; import { Request } from 'express'; import lodash from 'lodash'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Cursor, QueryEntitiesCursorRequest, diff --git a/plugins/home/src/components/CustomHomepage/types.ts b/plugins/home/src/components/CustomHomepage/types.ts index 8d73d6d66d9936..9ca12334d59ffc 100644 --- a/plugins/home/src/components/CustomHomepage/types.ts +++ b/plugins/home/src/components/CustomHomepage/types.ts @@ -16,7 +16,7 @@ import { CSSProperties, ReactElement, ReactNode } from 'react'; import { Layout } from 'react-grid-layout'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { RJSFSchema, UiSchema } from '@rjsf/utils'; const RSJFTypeSchema: z.ZodType = z.any(); diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts index 7d2a8a34c2baad..7f0caa9670cc41 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts @@ -30,7 +30,7 @@ import { createPermissionRule, } from '@backstage/plugin-permission-node'; import { PermissionIntegrationClient } from './PermissionIntegrationClient'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { DiscoveryService } from '@backstage/backend-plugin-api'; describe('PermissionIntegrationClient', () => { diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts index 610eb8b34bb861..83dc7780f3c500 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { AuthorizeResult, ConditionalPolicyDecision, diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index b6f570562b0648..92a2e4025928f6 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import express, { Request, Response } from 'express'; import Router from 'express-promise-router'; import { InputError } from '@backstage/errors'; diff --git a/plugins/permission-common/src/PermissionClient.ts b/plugins/permission-common/src/PermissionClient.ts index f801ba2a4bf252..d178dcf2afde1a 100644 --- a/plugins/permission-common/src/PermissionClient.ts +++ b/plugins/permission-common/src/PermissionClient.ts @@ -18,7 +18,7 @@ import { Config } from '@backstage/config'; import { ResponseError } from '@backstage/errors'; import fetch from 'cross-fetch'; import * as uuid from 'uuid'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { AuthorizeResult, PermissionMessageBatch, diff --git a/plugins/permission-node/report.api.md b/plugins/permission-node/report.api.md index 6345861c939171..09452b969b632a 100644 --- a/plugins/permission-node/report.api.md +++ b/plugins/permission-node/report.api.md @@ -30,7 +30,7 @@ import { PermissionsServiceRequestOptions } from '@backstage/backend-plugin-api' import { PolicyDecision } from '@backstage/plugin-permission-common'; import { QueryPermissionRequest } from '@backstage/plugin-permission-common'; import { ResourcePermission } from '@backstage/plugin-permission-common'; -import { z } from 'zod'; +import { z } from 'zod/v3'; // @public export type ApplyConditionsRequest = { diff --git a/plugins/permission-node/src/integration/createConditionExports.test.ts b/plugins/permission-node/src/integration/createConditionExports.test.ts index 926e607baadda2..341b4e73e1503b 100644 --- a/plugins/permission-node/src/integration/createConditionExports.test.ts +++ b/plugins/permission-node/src/integration/createConditionExports.test.ts @@ -18,7 +18,7 @@ import { AuthorizeResult, createPermission, } from '@backstage/plugin-permission-common'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { createConditionExports } from './createConditionExports'; import { createPermissionRule } from './createPermissionRule'; import { createPermissionResourceRef } from './createPermissionResourceRef'; diff --git a/plugins/permission-node/src/integration/createConditionFactory.test.ts b/plugins/permission-node/src/integration/createConditionFactory.test.ts index 520fcc01f95719..02d01e7d14d215 100644 --- a/plugins/permission-node/src/integration/createConditionFactory.test.ts +++ b/plugins/permission-node/src/integration/createConditionFactory.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { createConditionFactory } from './createConditionFactory'; import { createPermissionRule } from './createPermissionRule'; diff --git a/plugins/permission-node/src/integration/createConditionTransformer.test.ts b/plugins/permission-node/src/integration/createConditionTransformer.test.ts index a5507f0b44920a..ce725679028e3a 100644 --- a/plugins/permission-node/src/integration/createConditionTransformer.test.ts +++ b/plugins/permission-node/src/integration/createConditionTransformer.test.ts @@ -18,7 +18,7 @@ import { PermissionCondition, PermissionCriteria, } from '@backstage/plugin-permission-common'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { createConditionTransformer } from './createConditionTransformer'; import { createPermissionRule } from './createPermissionRule'; diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts index 0d28ef710f6729..52352dc454bd72 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/plugin-permission-common'; import express from 'express'; import request, { Response } from 'supertest'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { createPermissionIntegrationRouter, CreatePermissionIntegrationRouterResourceOptions, diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 522e60bac5dd41..6d96e45659f8a2 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -16,7 +16,7 @@ import express, { Response } from 'express'; import Router from 'express-promise-router'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; import { InputError } from '@backstage/errors'; import { diff --git a/plugins/permission-node/src/integration/createPermissionRule.ts b/plugins/permission-node/src/integration/createPermissionRule.ts index 36f8205cf8573a..f1970a025421dd 100644 --- a/plugins/permission-node/src/integration/createPermissionRule.ts +++ b/plugins/permission-node/src/integration/createPermissionRule.ts @@ -19,7 +19,7 @@ import { PermissionRuleParams, } from '@backstage/plugin-permission-common'; import { NoInfer, PermissionRule } from '../types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { PermissionResourceRef } from './createPermissionResourceRef'; /** diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts index 578117a4f1844b..a8a337ca88b164 100644 --- a/plugins/permission-node/src/types.ts +++ b/plugins/permission-node/src/types.ts @@ -18,7 +18,7 @@ import type { PermissionCriteria, PermissionRuleParams, } from '@backstage/plugin-permission-common'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * Prevent use of type parameter from contributing to type inference. diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/inputProperties.ts index fadc8bcefba422..ed35bfeab81b53 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/inputProperties.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z as zod } from 'zod'; +import { z as zod } from 'zod/v3'; const repoUrl = (z: typeof zod) => z.string({ diff --git a/plugins/scaffolder-backend-module-cookiecutter/report.api.md b/plugins/scaffolder-backend-module-cookiecutter/report.api.md index 49888ad784d6a6..43303508e7eb36 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/report.api.md +++ b/plugins/scaffolder-backend-module-cookiecutter/report.api.md @@ -4,12 +4,12 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; -import { objectOutputType } from 'zod'; +import { objectOutputType } from 'zod/v3'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { Writable } from 'node:stream'; -import { ZodTypeAny } from 'zod'; +import { ZodTypeAny } from 'zod/v3'; // @public export interface ContainerRunner { diff --git a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts index a474272ef7cb6a..a2e0ae0bf3ff0a 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z as zod } from 'zod'; +import { z as zod } from 'zod/v3'; const repoUrl = (z: typeof zod) => z.string({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/outputProperties.ts b/plugins/scaffolder-backend-module-github/src/actions/outputProperties.ts index b35047083d2358..50a727be8815fd 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/outputProperties.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/outputProperties.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z as zod } from 'zod'; +import { z as zod } from 'zod/v3'; const remoteUrl = (z: typeof zod) => z.string({ diff --git a/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts b/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts index 5e8d92338ec535..4208cdb805be40 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts @@ -16,7 +16,7 @@ /* We want to maintain the same information as an enum, so we disable the redeclaration warning */ /* eslint-disable @typescript-eslint/no-redeclare */ -import { z } from 'zod'; +import { z } from 'zod/v3'; const commonGitlabConfig = z.object({ repoUrl: z.string({ description: 'Repository Location' }), diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.ts b/plugins/scaffolder-backend-module-gitlab/src/util.ts index e5e27231e522e5..328f9d237edcad 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.ts @@ -21,7 +21,7 @@ import { ScmIntegrationRegistry, } from '@backstage/integration'; import { Gitlab, GroupSchema, RepositoryTreeSchema } from '@gitbeaker/rest'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import commonGitlabConfig from './commonGitlabConfig'; import { SerializedFile } from '@backstage/plugin-scaffolder-node'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/read.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/read.ts index 0e82092ab3d8d3..84fa7a33e54019 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/read.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/read.ts @@ -17,7 +17,7 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { z as zod } from 'zod'; +import { z as zod } from 'zod/v3'; import { examples } from './read.examples'; const contentSchema = (z: typeof zod) => diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 0c9daf53c27224..9e84b38e39c9d5 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -85,7 +85,7 @@ import express from 'express'; import { Duration } from 'luxon'; import { pathToFileURL } from 'node:url'; import { v4 as uuid } from 'uuid'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { DatabaseTaskStore, DefaultTemplateActionRegistry, diff --git a/plugins/scaffolder-backend/src/service/rules.ts b/plugins/scaffolder-backend/src/service/rules.ts index 5753668089d1ff..7dea3e1f95bd9e 100644 --- a/plugins/scaffolder-backend/src/service/rules.ts +++ b/plugins/scaffolder-backend/src/service/rules.ts @@ -29,7 +29,7 @@ import { import { SerializedTask, TaskFilter } from '@backstage/plugin-scaffolder-node'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { JsonObject, JsonPrimitive } from '@backstage/types'; import { get } from 'lodash'; diff --git a/plugins/scaffolder-backend/src/util/templating.ts b/plugins/scaffolder-backend/src/util/templating.ts index 3318b64fdfac90..a088c955255ee7 100644 --- a/plugins/scaffolder-backend/src/util/templating.ts +++ b/plugins/scaffolder-backend/src/util/templating.ts @@ -26,7 +26,7 @@ import { } from '@backstage/plugin-scaffolder-node/alpha'; import { JsonValue } from '@backstage/types'; import { Schema } from 'jsonschema'; -import { ZodType, z } from 'zod'; +import { ZodType, z } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; /** diff --git a/plugins/scaffolder-node/report-alpha.api.md b/plugins/scaffolder-node/report-alpha.api.md index 3a01b7f78828a3..62240f210af3e9 100644 --- a/plugins/scaffolder-node/report-alpha.api.md +++ b/plugins/scaffolder-node/report-alpha.api.md @@ -8,7 +8,7 @@ import { JsonValue } from '@backstage/types'; import { TaskBroker } 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'; +import { z } from 'zod/v3'; // @alpha export type AutocompleteHandler = (input: { diff --git a/plugins/scaffolder-node/report.api.md b/plugins/scaffolder-node/report.api.md index 24b51ed98976eb..538f19cd1e2cb5 100644 --- a/plugins/scaffolder-node/report.api.md +++ b/plugins/scaffolder-node/report.api.md @@ -33,7 +33,7 @@ import { UpdateTaskCheckpointOptions } from '@backstage/plugin-scaffolder-node/a import { UrlReaderService } from '@backstage/backend-plugin-api'; import { UserEntity } from '@backstage/catalog-model'; import { Writable } from 'node:stream'; -import { z } from 'zod'; +import { z } from 'zod/v3'; // @public export type ActionContext< diff --git a/plugins/scaffolder-node/src/actions/createTemplateAction.ts b/plugins/scaffolder-node/src/actions/createTemplateAction.ts index edf3e203b96fe3..a3dfe3f4693d43 100644 --- a/plugins/scaffolder-node/src/actions/createTemplateAction.ts +++ b/plugins/scaffolder-node/src/actions/createTemplateAction.ts @@ -15,7 +15,7 @@ */ import { ActionContext, TemplateAction } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Expand, JsonObject } from '@backstage/types'; import { parseSchemas } from './util'; diff --git a/plugins/scaffolder-node/src/actions/util.ts b/plugins/scaffolder-node/src/actions/util.ts index e698306a450c8d..bddf3ba0f3fdec 100644 --- a/plugins/scaffolder-node/src/actions/util.ts +++ b/plugins/scaffolder-node/src/actions/util.ts @@ -20,7 +20,7 @@ import { join as joinPath, normalize as normalizePath } from 'node:path'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TemplateActionOptions } from './createTemplateAction'; import zodToJsonSchema from 'zod-to-json-schema'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { Schema } from 'jsonschema'; import { trim } from 'lodash'; diff --git a/plugins/scaffolder-node/src/alpha/filters/createTemplateFilter.ts b/plugins/scaffolder-node/src/alpha/filters/createTemplateFilter.ts index f6486585889efd..ce8bd517eee4c0 100644 --- a/plugins/scaffolder-node/src/alpha/filters/createTemplateFilter.ts +++ b/plugins/scaffolder-node/src/alpha/filters/createTemplateFilter.ts @@ -16,7 +16,7 @@ import { ZodFunctionSchema } from '../types'; import { CreatedTemplateFilter, TemplateFilterExample } from './types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * This function is used to create new template filters in type-safe manner. diff --git a/plugins/scaffolder-node/src/alpha/filters/types.ts b/plugins/scaffolder-node/src/alpha/filters/types.ts index 1906ba1c9c4426..e721618f7aa1f0 100644 --- a/plugins/scaffolder-node/src/alpha/filters/types.ts +++ b/plugins/scaffolder-node/src/alpha/filters/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { ZodFunctionSchema } from '../types'; export type { TemplateFilter } from '../../types'; diff --git a/plugins/scaffolder-node/src/alpha/globals/createTemplateGlobal.ts b/plugins/scaffolder-node/src/alpha/globals/createTemplateGlobal.ts index 2687b83bb48832..0d5a01650cbc87 100644 --- a/plugins/scaffolder-node/src/alpha/globals/createTemplateGlobal.ts +++ b/plugins/scaffolder-node/src/alpha/globals/createTemplateGlobal.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { CreatedTemplateGlobalFunction, CreatedTemplateGlobalValue, diff --git a/plugins/scaffolder-node/src/alpha/globals/types.ts b/plugins/scaffolder-node/src/alpha/globals/types.ts index 27a3a700e57472..977f733673edea 100644 --- a/plugins/scaffolder-node/src/alpha/globals/types.ts +++ b/plugins/scaffolder-node/src/alpha/globals/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { JsonValue } from '@backstage/types'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { ZodFunctionSchema } from '../types'; export type { TemplateGlobal } from '../../types'; diff --git a/plugins/scaffolder-node/src/alpha/types.ts b/plugins/scaffolder-node/src/alpha/types.ts index ca529737754329..171c8e85f9ff94 100644 --- a/plugins/scaffolder-node/src/alpha/types.ts +++ b/plugins/scaffolder-node/src/alpha/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; /** * @alpha diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index 9e01257c5c949b..84c88bd319b09e 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -40,7 +40,7 @@ import { TemplatePresentationV1beta3 } from '@backstage/plugin-scaffolder-common import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UiSchema } from '@rjsf/utils'; import { WidgetProps } from '@rjsf/utils'; -import { z } from 'zod'; +import { z } from 'zod/v3'; // @alpha (undocumented) export type BackstageOverrides = Overrides & { diff --git a/plugins/scaffolder-react/report.api.md b/plugins/scaffolder-react/report.api.md index d40b0278bf8b54..5c0ef0d2be871a 100644 --- a/plugins/scaffolder-react/report.api.md +++ b/plugins/scaffolder-react/report.api.md @@ -59,7 +59,7 @@ import { TemplatesType } from '@rjsf/utils'; import { UIOptionsType } from '@rjsf/utils'; import { UiSchema } from '@rjsf/utils'; import { ValidatorType } from '@rjsf/utils'; -import { z } from 'zod'; +import { z } from 'zod/v3'; // @public @deprecated export type Action = Action_2; diff --git a/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx b/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx index 8b5a88f7dfe12d..35eeb28c17f7e2 100644 --- a/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx +++ b/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsx @@ -17,7 +17,7 @@ import { createExtensionBlueprint, createExtensionDataRef, } from '@backstage/frontend-plugin-api'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { OpaqueFormField } from '@internal/scaffolder'; import { FormFieldExtensionData } from './types'; diff --git a/plugins/scaffolder-react/src/next/blueprints/types.ts b/plugins/scaffolder-react/src/next/blueprints/types.ts index eccf7e4606fae2..4f3de47e5ea735 100644 --- a/plugins/scaffolder-react/src/next/blueprints/types.ts +++ b/plugins/scaffolder-react/src/next/blueprints/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z } from 'zod'; +import { z } from 'zod/v3'; import { CustomFieldValidator, FieldExtensionComponentProps, diff --git a/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.ts b/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.ts index 0064ae039729b6..51963072b4288d 100644 --- a/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.ts +++ b/plugins/scaffolder-react/src/next/extensions/createScaffolderFormDecorator.ts @@ -16,7 +16,7 @@ import { AnyApiRef } from '@backstage/core-plugin-api'; import { JsonObject, JsonValue } from '@backstage/types'; import { OpaqueFormDecorator } from '@internal/scaffolder'; -import { z } from 'zod'; +import { z } from 'zod/v3'; /** @alpha */ export type ScaffolderFormDecoratorContext< diff --git a/plugins/scaffolder-react/src/utils.ts b/plugins/scaffolder-react/src/utils.ts index dc24df759d57f6..c3ca89855671f4 100644 --- a/plugins/scaffolder-react/src/utils.ts +++ b/plugins/scaffolder-react/src/utils.ts @@ -16,7 +16,7 @@ import zodToJsonSchema from 'zod-to-json-schema'; import { JSONSchema7 } from 'json-schema'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { CustomFieldExtensionSchema, FieldExtensionComponentProps, diff --git a/plugins/scaffolder/report.api.md b/plugins/scaffolder/report.api.md index 17fd25e646a7a0..7a34ac1cc2a86e 100644 --- a/plugins/scaffolder/report.api.md +++ b/plugins/scaffolder/report.api.md @@ -49,7 +49,7 @@ import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react'; import { TemplateListPageProps } from '@backstage/plugin-scaffolder/alpha'; import { TemplateParameterSchema as TemplateParameterSchema_2 } from '@backstage/plugin-scaffolder-common'; import { TemplateWizardPageProps } from '@backstage/plugin-scaffolder/alpha'; -import { z } from 'zod'; +import { z } from 'zod/v3'; // @public @deprecated (undocumented) export const createScaffolderFieldExtension: typeof createScaffolderFieldExtension_2; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts index 9a77d6b26e2cfc..e69fed9df381cb 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z as zod } from 'zod'; +import { z as zod } from 'zod/v3'; import { makeFieldSchema } from '@backstage/plugin-scaffolder-react'; export const createEntityQueryFilterExpressionSchema = (z: typeof zod) => diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts index 667b4638b856be..d07e2d031c73c0 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/schema.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { z as zod } from 'zod'; +import { z as zod } from 'zod/v3'; import { makeFieldSchema } from '@backstage/plugin-scaffolder-react'; export const entityQueryFilterExpressionSchema = zod.record( diff --git a/plugins/scaffolder/src/components/fields/utils.ts b/plugins/scaffolder/src/components/fields/utils.ts index 2fa6aab1b34690..a2b01d91fae5f9 100644 --- a/plugins/scaffolder/src/components/fields/utils.ts +++ b/plugins/scaffolder/src/components/fields/utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { JSONSchema7 } from 'json-schema'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import zodToJsonSchema from 'zod-to-json-schema'; import { FieldSchema as FieldSchemaType } from '@backstage/plugin-scaffolder-react'; diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index eedd84105a72ef..2be6612c8b11c9 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -15,7 +15,7 @@ */ import express from 'express'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; import { JsonObject, JsonValue } from '@backstage/types'; diff --git a/plugins/user-settings-backend/src/service/router.ts b/plugins/user-settings-backend/src/service/router.ts index c9971bbb4db66a..6d1518eb8e4678 100644 --- a/plugins/user-settings-backend/src/service/router.ts +++ b/plugins/user-settings-backend/src/service/router.ts @@ -17,7 +17,7 @@ import { InputError } from '@backstage/errors'; import express, { Request } from 'express'; import Router from 'express-promise-router'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { UserSettingsStore } from '../database/UserSettingsStore'; import { SignalsService } from '@backstage/plugin-signals-node'; import { diff --git a/scripts/verify-plugin-directory.js b/scripts/verify-plugin-directory.js index 24c068b96ed4f0..70d3fbe9e900b7 100644 --- a/scripts/verify-plugin-directory.js +++ b/scripts/verify-plugin-directory.js @@ -18,7 +18,7 @@ const fs = require('fs-extra'); const { resolve, join } = require('node:path'); const yaml = require('js-yaml'); -const z = require('zod'); +const z = require('zod/v3'); const configSchema = z.object({ title: z.string(), From 0257363c519afc3eef773b8bab049f84dd6611d9 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Sun, 11 Jan 2026 22:31:48 +0100 Subject: [PATCH 29/55] Allow zod v3 or v4 dependency (keep using v3) Signed-off-by: Gabriel Dugny --- packages/backend-defaults/package.json | 2 +- packages/backend-test-utils/package.json | 2 +- packages/frontend-plugin-api/package.json | 2 +- plugins/auth-node/package.json | 2 +- plugins/permission-common/package.json | 2 +- plugins/permission-node/package.json | 2 +- plugins/scaffolder-backend/package.json | 2 +- plugins/scaffolder-node/package.json | 2 +- plugins/scaffolder-react/package.json | 2 +- plugins/scaffolder/package.json | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index fb4e29a3cea26f..7cb4e56a8f8f80 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -196,7 +196,7 @@ "winston-transport": "^4.5.0", "yauzl": "^3.0.0", "yn": "^4.0.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index c2d2268878447c..373f74fcbaad7d 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -79,7 +79,7 @@ "text-extensions": "^2.4.0", "uuid": "^11.0.0", "yn": "^4.0.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 66973d44068d01..04e11771449d17 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -48,7 +48,7 @@ "@backstage/filter-predicates": "workspace:^", "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index c89095dd9534fd..a1d06111b87339 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -50,7 +50,7 @@ "jose": "^5.0.0", "lodash": "^4.17.21", "passport": "^0.7.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1", "zod-validation-error": "^4.0.2" }, diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 85607676cf39a3..e065226c856e85 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -53,7 +53,7 @@ "@backstage/types": "workspace:^", "cross-fetch": "^4.0.0", "uuid": "^11.0.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index bfcc5d5ad0aaeb..b8258a42294593 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -64,7 +64,7 @@ "@types/express": "^4.17.6", "express": "^4.22.0", "express-promise-router": "^4.1.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index fc5396377e7127..b8f7b649db9b1c 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -99,7 +99,7 @@ "winston-transport": "^4.7.0", "yaml": "^2.0.0", "zen-observable": "^0.10.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index a79c47d3675b5e..c9859ffa46da93 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -76,7 +76,7 @@ "tar": "^7.5.6", "winston": "^3.2.1", "winston-transport": "^4.7.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index f48b242f7bdd66..e9bf8020356536 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -96,7 +96,7 @@ "react-use": "^17.2.4", "use-immer": "^0.11.0", "zen-observable": "^0.10.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index b2ee8a47d4b543..400e3f89faff22 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -101,7 +101,7 @@ "react-use": "^17.2.4", "react-window": "^1.8.10", "yaml": "^2.0.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-to-json-schema": "^3.25.1" }, "devDependencies": { From a49a40d314e40d89e958f3dc5e3777a75ca5c8c6 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Sun, 11 Jan 2026 23:04:50 +0100 Subject: [PATCH 30/55] Proper /v3 usage everywhere Signed-off-by: Gabriel Dugny --- .changeset/silver-snails-pull.md | 63 ++ package.json | 2 +- packages/backend-plugin-api/package.json | 2 +- .../templates/backend-plugin/package.json.hbs | 2 +- packages/cli-node/package.json | 2 +- packages/core-app-api/package.json | 2 +- packages/core-compat-api/package.json | 4 +- packages/core-components/package.json | 2 +- packages/core-plugin-api/package.json | 2 +- packages/filter-predicates/package.json | 2 +- packages/frontend-app-api/package.json | 2 +- packages/frontend-test-utils/package.json | 2 +- packages/repo-tools/package.json | 2 +- packages/scaffolder-internal/package.json | 2 +- plugins/app/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- plugins/catalog-backend/package.json | 2 +- plugins/catalog-react/package.json | 2 +- plugins/home/package.json | 2 +- plugins/mcp-actions-backend/package.json | 2 +- plugins/permission-backend/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- plugins/search-backend/package.json | 2 +- plugins/user-settings-backend/package.json | 2 +- yarn.lock | 889 +++++++----------- 43 files changed, 445 insertions(+), 591 deletions(-) create mode 100644 .changeset/silver-snails-pull.md diff --git a/.changeset/silver-snails-pull.md b/.changeset/silver-snails-pull.md new file mode 100644 index 00000000000000..e9b79ccdc89483 --- /dev/null +++ b/.changeset/silver-snails-pull.md @@ -0,0 +1,63 @@ +--- +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch +'@backstage/plugin-auth-backend-module-bitbucket-server-provider': patch +'@backstage/plugin-auth-backend-module-azure-easyauth-provider': patch +'@backstage/plugin-auth-backend-module-oauth2-proxy-provider': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +'@backstage/plugin-auth-backend-module-atlassian-provider': patch +'@backstage/plugin-auth-backend-module-bitbucket-provider': patch +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +'@backstage/plugin-auth-backend-module-openshift-provider': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-auth-backend-module-onelogin-provider': patch +'@backstage/plugin-auth-backend-module-aws-alb-provider': patch +'@backstage/plugin-auth-backend-module-gcp-iap-provider': patch +'@backstage/plugin-auth-backend-module-github-provider': patch +'@backstage/plugin-auth-backend-module-gitlab-provider': patch +'@backstage/plugin-auth-backend-module-google-provider': patch +'@backstage/plugin-auth-backend-module-oauth2-provider': patch +'@backstage/plugin-auth-backend-module-oidc-provider': patch +'@backstage/plugin-auth-backend-module-okta-provider': patch +'@backstage/plugin-scaffolder-backend-module-github': patch +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +'@backstage/frontend-plugin-api': patch +'@backstage/frontend-test-utils': patch +'@internal/scaffolder': patch +'@backstage/backend-plugin-api': patch +'@backstage/backend-test-utils': patch +'@backstage/plugin-mcp-actions-backend': patch +'@backstage/plugin-permission-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/backend-defaults': patch +'@backstage/frontend-app-api': patch +'@backstage/plugin-permission-common': patch +'@backstage/core-compat-api': patch +'@backstage/core-components': patch +'@backstage/core-plugin-api': patch +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-permission-node': patch +'@backstage/plugin-scaffolder-node': patch +'@backstage/catalog-model': patch +'@backstage/plugin-search-backend': patch +'@backstage/core-app-api': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-techdocs-node': patch +'@backstage/repo-tools': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-mui-to-bui': patch +'@backstage/plugin-scaffolder': patch +'example-app-next': patch +'@backstage/cli-node': patch +'@backstage/plugin-app-react': patch +'@backstage/plugin-auth-node': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-app-node': patch +'@backstage/cli': patch +'@backstage/plugin-home': patch +'@backstage/ui': patch +'@backstage/plugin-app': patch +'@backstage/plugin-org': patch +--- + +Updated dependency `zod` to `3.25.76 || ^4.0.0` diff --git a/package.json b/package.json index c89dcefce6c1cc..0c969724d23e2f 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,7 @@ "typedoc": "^0.28.0", "typescript": "~5.7.0", "vite": "^7.1.5", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "packageManager": "yarn@4.8.1", "engines": { diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 8f674cda44a977..f12b75d57f259d 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -66,7 +66,7 @@ "json-schema": "^0.4.0", "knex": "^3.0.0", "luxon": "^3.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/packages/cli-module-new/templates/backend-plugin/package.json.hbs b/packages/cli-module-new/templates/backend-plugin/package.json.hbs index 4e23376c566733..c1555a058f9d2b 100644 --- a/packages/cli-module-new/templates/backend-plugin/package.json.hbs +++ b/packages/cli-module-new/templates/backend-plugin/package.json.hbs @@ -29,7 +29,7 @@ "@backstage/plugin-catalog-node": "{{versionQuery '@backstage/plugin-catalog-node'}}", "express": "{{versionQuery 'express' '4.17.1'}}", "express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}", - "zod": "{{versionQuery 'zod' '3.25.76'}}" + "zod": "{{versionQuery 'zod' '3.25.76 || ^4.0.0'}}" }, "devDependencies": { "@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}", diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index 483b0eccff3bac..fbefa34fcc00c3 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -44,7 +44,7 @@ "pirates": "^4.0.6", "semver": "^7.5.3", "yaml": "^2.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index c37d4172a4e3b9..71c192e9577914 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -58,7 +58,7 @@ "prop-types": "^15.7.2", "react-use": "^17.2.4", "zen-observable": "^0.10.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 6ee93be87581cd..e82140d5e201b3 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -40,7 +40,7 @@ "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", "lodash": "^4.17.21", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", @@ -56,7 +56,7 @@ "react": "^18.0.2", "react-dom": "^18.0.2", "react-router-dom": "^6.30.2", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", diff --git a/packages/core-components/package.json b/packages/core-components/package.json index ecc27a6cc53502..b94cc13f2dddde 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -95,7 +95,7 @@ "rehype-sanitize": "^5.0.0", "remark-gfm": "^3.0.1", "zen-observable": "^0.10.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/app-defaults": "workspace:^", diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 8af605aa7298e3..f97e7b803cf949 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -55,7 +55,7 @@ "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", "history": "^5.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/filter-predicates/package.json b/packages/filter-predicates/package.json index 7c2a6bbf80b0de..678707c833427e 100644 --- a/packages/filter-predicates/package.json +++ b/packages/filter-predicates/package.json @@ -39,7 +39,7 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-validation-error": "^4.0.2" }, "devDependencies": { diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index f5dea017a942ca..e807188f6c170a 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -42,7 +42,7 @@ "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", "lodash": "^4.17.21", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 59c5545a811bc3..63f54171534102 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -46,7 +46,7 @@ "@backstage/version-bridge": "workspace:^", "i18next": "^22.4.15", "zen-observable": "^0.10.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index c2156beb3e6af2..f27e77862b5d59 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -84,7 +84,7 @@ "tar": "^7.5.6", "ts-morph": "^24.0.0", "yaml-diff-patch": "^2.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/packages/scaffolder-internal/package.json b/packages/scaffolder-internal/package.json index 1bbd38b5786d7c..0ee520153ee6c6 100644 --- a/packages/scaffolder-internal/package.json +++ b/packages/scaffolder-internal/package.json @@ -25,7 +25,7 @@ "dependencies": { "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-scaffolder-react": "workspace:^", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/plugins/app/package.json b/plugins/app/package.json index 974a39853b1c7c..25ec33883f234b 100644 --- a/plugins/app/package.json +++ b/plugins/app/package.json @@ -73,7 +73,7 @@ "motion": "^12.0.0", "react-use": "^17.2.4", "zen-observable": "^0.10.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index e96be47580e27e..604f8b1cb294dd 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -39,7 +39,7 @@ "express": "^4.22.0", "passport": "^0.7.0", "passport-atlassian-oauth2": "^2.1.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index 7145eea19f0638..a206da0824eb27 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -43,7 +43,7 @@ "@backstage/plugin-auth-node": "workspace:^", "jose": "^5.0.0", "node-cache": "^5.1.2", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-azure-easyauth-provider/package.json b/plugins/auth-backend-module-azure-easyauth-provider/package.json index fcb2c85d97a8d2..e2d6b3f6f2aa33 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/package.json +++ b/plugins/auth-backend-module-azure-easyauth-provider/package.json @@ -41,7 +41,7 @@ "express": "^4.22.0", "jose": "^5.0.0", "passport": "^0.7.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json index 4835828f1a247b..694fadcbb19c89 100644 --- a/plugins/auth-backend-module-bitbucket-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -39,7 +39,7 @@ "express": "^4.22.0", "passport": "^0.7.0", "passport-bitbucket-oauth2": "^0.1.2", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-bitbucket-server-provider/package.json b/plugins/auth-backend-module-bitbucket-server-provider/package.json index 4ae6ff37eb93bd..220691062ff06a 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-server-provider/package.json @@ -38,7 +38,7 @@ "@backstage/plugin-auth-node": "workspace:^", "passport": "^0.7.0", "passport-oauth2": "^1.6.1", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/package.json b/plugins/auth-backend-module-cloudflare-access-provider/package.json index 60f6cbc413bacf..70f7e27bdef71b 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/package.json +++ b/plugins/auth-backend-module-cloudflare-access-provider/package.json @@ -40,7 +40,7 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.22.0", "jose": "^5.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index 2c29c29e3cffe3..1533f796f8cc96 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -43,7 +43,7 @@ "@backstage/plugin-auth-node": "workspace:^", "@backstage/types": "workspace:^", "google-auth-library": "^9.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index bcebf762fd0f57..ca3262f3b6d370 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -37,7 +37,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "passport-github2": "^0.1.12", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index 8a12e491d77a2b..f836a87396dc06 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -39,7 +39,7 @@ "express": "^4.22.0", "passport": "^0.7.0", "passport-gitlab2": "^5.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index fa0ae5e42dcaec..409f96ac454b84 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -42,7 +42,7 @@ "@backstage/plugin-auth-node": "workspace:^", "google-auth-library": "^9.0.0", "passport-google-oauth20": "^2.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index a869d75af20796..fc978ba0232f83 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -39,7 +39,7 @@ "express": "^4.22.0", "jose": "^5.0.0", "passport-microsoft": "^1.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index 6eb517fbc6094c..9f705004ee0ce6 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -38,7 +38,7 @@ "@backstage/plugin-auth-node": "workspace:^", "passport": "^0.7.0", "passport-oauth2": "^1.6.1", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/package.json b/plugins/auth-backend-module-oauth2-proxy-provider/package.json index caece3cfb2438f..380bf340bc54b4 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/package.json +++ b/plugins/auth-backend-module-oauth2-proxy-provider/package.json @@ -37,7 +37,7 @@ "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "jose": "^5.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index 85f9b81cdd0e67..94803d49d7aae1 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -42,7 +42,7 @@ "express": "^4.22.0", "openid-client": "^5.5.0", "passport": "^0.7.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index 16b05d8f60bcbf..ee43e869ed4a24 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -39,7 +39,7 @@ "@davidzemon/passport-okta-oauth": "^0.0.7", "express": "^4.22.0", "passport": "^0.7.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json index 4d9258a5df2109..1bef49600fe97c 100644 --- a/plugins/auth-backend-module-onelogin-provider/package.json +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -39,7 +39,7 @@ "express": "^4.22.0", "passport": "^0.7.0", "passport-onelogin-oauth": "^0.0.1", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-openshift-provider/package.json b/plugins/auth-backend-module-openshift-provider/package.json index 4191cf7bc77038..24d2e59c165dd4 100644 --- a/plugins/auth-backend-module-openshift-provider/package.json +++ b/plugins/auth-backend-module-openshift-provider/package.json @@ -39,7 +39,7 @@ "@backstage/plugin-auth-node": "workspace:^", "@backstage/types": "workspace:^", "passport-oauth2": "^1.8.0", - "zod": "^3.24.2" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 5f034ed3e5097f..ce2a7a30cf26e7 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -95,7 +95,7 @@ "uuid": "^11.0.0", "yaml": "^2.0.0", "yn": "^4.0.0", - "zod": "^3.25.76", + "zod": "^3.25.76 || ^4.0.0", "zod-validation-error": "^4.0.2" }, "devDependencies": { diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 35a0abd20b1b21..9cda23e6b023b0 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -105,7 +105,7 @@ "react-dom": "^18.0.2", "react-router-dom": "^6.30.2", "react-test-renderer": "^16.13.1", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "peerDependencies": { "@backstage/frontend-test-utils": "workspace:^", diff --git a/plugins/home/package.json b/plugins/home/package.json index d35003d92153c3..23156430367389 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -79,7 +79,7 @@ "react-grid-layout": "1.3.4", "react-resizable": "^3.0.4", "react-use": "^17.2.4", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/mcp-actions-backend/package.json b/plugins/mcp-actions-backend/package.json index 770e52474c52a6..58b8d8fc5ad402 100644 --- a/plugins/mcp-actions-backend/package.json +++ b/plugins/mcp-actions-backend/package.json @@ -47,7 +47,7 @@ "express": "^4.22.0", "express-promise-router": "^4.1.0", "minimatch": "^10.2.1", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index ca97759c1928b9..40ef10dcf66da9 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -62,7 +62,7 @@ "express-promise-router": "^4.1.0", "lodash": "^4.17.21", "yn": "^4.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index 10ff9bc6796a3b..46be5ea69d9605 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -51,7 +51,7 @@ "bitbucket": "^2.12.0", "fs-extra": "^11.2.0", "yaml": "^2.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 27f4241a8df449..cad8221cd26980 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -55,7 +55,7 @@ "octokit": "^3.0.0", "octokit-plugin-create-pull-request": "^5.0.0", "yaml": "^2.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 28517d25f60f27..f803e90518c3a3 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -55,7 +55,7 @@ "@gitbeaker/rest": "^43.8.0", "luxon": "^3.0.0", "yaml": "^2.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index c734c4e5493c40..3777f83c9d6a2d 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -70,7 +70,7 @@ "lodash": "^4.17.21", "qs": "^6.10.1", "yn": "^4.0.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index e512de98c43767..90791ae5951a4a 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -59,7 +59,7 @@ "express-promise-router": "^4.1.0", "knex": "^3.0.0", "p-limit": "^3.1.0", - "zod": "^3.25.76" + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 32925b244624c3..dc6c8b6727cb83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2524,7 +2524,6 @@ __metadata: "@backstage/integration-aws-node": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-events-node": "workspace:^" - "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" "@backstage/types": "workspace:^" "@google-cloud/cloud-sql-connector": "npm:^1.4.0" @@ -2594,7 +2593,7 @@ __metadata: winston-transport: "npm:^4.5.0" yauzl: "npm:^3.0.0" yn: "npm:^4.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" peerDependencies: "@google-cloud/cloud-sql-connector": ^1.4.0 @@ -2703,7 +2702,7 @@ __metadata: json-schema: "npm:^0.4.0" knex: "npm:^3.0.0" luxon: "npm:^3.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -2746,7 +2745,7 @@ __metadata: text-extensions: "npm:^2.4.0" uuid: "npm:^11.0.0" yn: "npm:^4.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" peerDependencies: "@types/jest": "*" @@ -2802,80 +2801,60 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-defaults@workspace:*, @backstage/cli-defaults@workspace:^, @backstage/cli-defaults@workspace:packages/cli-defaults": - version: 0.0.0-use.local - resolution: "@backstage/cli-defaults@workspace:packages/cli-defaults" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/cli-module-actions": "workspace:^" - "@backstage/cli-module-auth": "workspace:^" - "@backstage/cli-module-build": "workspace:^" - "@backstage/cli-module-config": "workspace:^" - "@backstage/cli-module-github": "workspace:^" - "@backstage/cli-module-info": "workspace:^" - "@backstage/cli-module-lint": "workspace:^" - "@backstage/cli-module-maintenance": "workspace:^" - "@backstage/cli-module-migrate": "workspace:^" - "@backstage/cli-module-new": "workspace:^" - "@backstage/cli-module-test-jest": "workspace:^" - "@backstage/cli-module-translations": "workspace:^" - languageName: unknown - linkType: soft - -"@backstage/cli-module-actions@workspace:^, @backstage/cli-module-actions@workspace:packages/cli-module-actions": - version: 0.0.0-use.local - resolution: "@backstage/cli-module-actions@workspace:packages/cli-module-actions" - dependencies: - "@backstage/backend-test-utils": "workspace:^" - "@backstage/cli": "workspace:^" - "@backstage/cli-module-auth": "workspace:^" - "@backstage/cli-node": "workspace:^" - cleye: "npm:^2.3.0" - bin: - cli-module-actions: bin/backstage-cli-module-actions - languageName: unknown - linkType: soft - -"@backstage/cli-module-auth@workspace:^, @backstage/cli-module-auth@workspace:packages/cli-module-auth": +"@backstage/cli-node@workspace:^, @backstage/cli-node@workspace:packages/cli-node": version: 0.0.0-use.local - resolution: "@backstage/cli-module-auth@workspace:packages/cli-module-auth" + resolution: "@backstage/cli-node@workspace:packages/cli-node" dependencies: "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" - "@backstage/cli-node": "workspace:^" + "@backstage/cli-common": "workspace:^" "@backstage/errors": "workspace:^" - "@types/fs-extra": "npm:^11.0.0" - "@types/proper-lockfile": "npm:^4" - cleye: "npm:^2.3.0" + "@backstage/test-utils": "workspace:^" + "@backstage/types": "workspace:^" + "@manypkg/get-packages": "npm:^1.1.3" + "@types/yarnpkg__lockfile": "npm:^1.1.4" + "@yarnpkg/lockfile": "npm:^1.1.0" + "@yarnpkg/parsers": "npm:^3.0.0" fs-extra: "npm:^11.2.0" - glob: "npm:^7.1.7" - inquirer: "npm:^8.2.0" - keytar: "npm:^7.9.0" - proper-lockfile: "npm:^4.1.2" + semver: "npm:^7.5.3" yaml: "npm:^2.0.0" - zod: "npm:^3.25.76" - dependenciesMeta: - keytar: - optional: true - bin: - cli-module-auth: bin/backstage-cli-module-auth + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft -"@backstage/cli-module-build@workspace:^, @backstage/cli-module-build@workspace:packages/cli-module-build": +"@backstage/cli@workspace:*, @backstage/cli@workspace:^, @backstage/cli@workspace:packages/cli": version: 0.0.0-use.local - resolution: "@backstage/cli-module-build@workspace:packages/cli-module-build" + resolution: "@backstage/cli@workspace:packages/cli" dependencies: + "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" - "@backstage/cli": "workspace:^" + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" "@backstage/cli-common": "workspace:^" "@backstage/cli-node": "workspace:^" "@backstage/config": "workspace:^" "@backstage/config-loader": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/eslint-plugin": "workspace:^" + "@backstage/integration": "workspace:^" "@backstage/module-federation-common": "workspace:^" + "@backstage/plugin-auth-backend": "workspace:^" + "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" + "@backstage/release-manifests": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@backstage/types": "workspace:^" + "@jest/environment-jsdom-abstract": "npm:^30.0.0" "@manypkg/get-packages": "npm:^1.1.3" "@module-federation/enhanced": "npm:^0.21.6" + "@octokit/request": "npm:^8.0.0" "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.6.0" "@rollup/plugin-commonjs": "npm:^26.0.0" "@rollup/plugin-json": "npm:^6.0.0" @@ -2884,366 +2863,157 @@ __metadata: "@rspack/core": "npm:^1.4.11" "@rspack/dev-server": "npm:^1.1.4" "@rspack/plugin-react-refresh": "npm:^1.4.3" + "@spotify/eslint-config-base": "npm:^15.0.0" + "@spotify/eslint-config-react": "npm:^15.0.0" + "@spotify/eslint-config-typescript": "npm:^15.0.0" "@swc/core": "npm:^1.15.6" + "@swc/helpers": "npm:^0.5.17" + "@swc/jest": "npm:^0.2.39" + "@types/cross-spawn": "npm:^6.0.2" + "@types/ejs": "npm:^3.1.3" + "@types/express": "npm:^4.17.6" "@types/fs-extra": "npm:^11.0.0" - "@types/lodash": "npm:^4.14.151" + "@types/http-proxy": "npm:^1.17.4" + "@types/inquirer": "npm:^8.1.3" + "@types/jest": "npm:^30.0.0" + "@types/node": "npm:^22.13.14" "@types/npm-packlist": "npm:^3.0.0" + "@types/proper-lockfile": "npm:^4" + "@types/recursive-readdir": "npm:^2.2.0" + "@types/rollup-plugin-peer-deps-external": "npm:^2.2.0" + "@types/rollup-plugin-postcss": "npm:^3.1.4" "@types/shell-quote": "npm:^1.7.5" + "@types/svgo": "npm:^2.6.2" + "@types/terser-webpack-plugin": "npm:^5.0.4" + "@types/webpack-env": "npm:^1.15.2" + "@types/webpack-sources": "npm:^3.2.3" + "@typescript-eslint/eslint-plugin": "npm:^8.17.0" + "@typescript-eslint/parser": "npm:^8.16.0" bfj: "npm:^9.0.2" buffer: "npm:^6.0.3" chalk: "npm:^4.0.0" chokidar: "npm:^3.3.1" cleye: "npm:^2.3.0" + commander: "npm:^14.0.3" + cross-fetch: "npm:^4.0.0" cross-spawn: "npm:^7.0.3" css-loader: "npm:^6.5.1" ctrlc-windows: "npm:^2.1.0" + del: "npm:^8.0.0" + esbuild: "npm:^0.27.0" esbuild-loader: "npm:^4.0.0" + eslint: "npm:^8.6.0" + eslint-config-prettier: "npm:^9.0.0" + eslint-formatter-friendly: "npm:^7.0.0" + eslint-plugin-deprecation: "npm:^3.0.0" + eslint-plugin-import: "npm:^2.31.0" + eslint-plugin-jest: "npm:^28.9.0" + eslint-plugin-jsx-a11y: "npm:^6.10.2" + eslint-plugin-react: "npm:^7.37.2" + eslint-plugin-react-hooks: "npm:^5.0.0" + eslint-plugin-unused-imports: "npm:^4.1.4" eslint-rspack-plugin: "npm:^4.2.1" eslint-webpack-plugin: "npm:^4.2.0" + express: "npm:^4.22.0" fork-ts-checker-webpack-plugin: "npm:^9.0.0" fs-extra: "npm:^11.2.0" + git-url-parse: "npm:^15.0.0" glob: "npm:^7.1.7" + global-agent: "npm:^3.0.0" + globby: "npm:^11.1.0" + handlebars: "npm:^4.7.3" html-webpack-plugin: "npm:^5.6.3" + inquirer: "npm:^8.2.0" + jest: "npm:^30.2.0" + jest-css-modules: "npm:^2.1.0" + jsdom: "npm:^27.1.0" + json-schema: "npm:^0.4.0" + keytar: "npm:^7.9.0" lodash: "npm:^4.17.21" mini-css-extract-plugin: "npm:^2.4.2" + minimatch: "npm:^10.2.1" + msw: "npm:^1.0.0" node-stdlib-browser: "npm:^1.3.1" + nodemon: "npm:^3.0.1" npm-packlist: "npm:^5.0.0" + ora: "npm:^5.3.0" p-queue: "npm:^6.6.2" + pirates: "npm:^4.0.6" postcss: "npm:^8.1.0" postcss-import: "npm:^16.1.0" process: "npm:^0.11.10" + proper-lockfile: "npm:^4.1.2" raw-loader: "npm:^4.0.2" react-dev-utils: "npm:^12.0.0-next.60" react-refresh: "npm:^0.18.0" + recursive-readdir: "npm:^2.2.2" + replace-in-file: "npm:^7.1.0" rollup: "npm:^4.27.3" rollup-plugin-dts: "npm:^6.1.0" rollup-plugin-esbuild: "npm:^6.1.1" rollup-plugin-postcss: "npm:^4.0.0" rollup-pluginutils: "npm:^2.8.2" + semver: "npm:^7.5.3" shell-quote: "npm:^1.8.1" style-loader: "npm:^3.3.1" + sucrase: "npm:^3.20.2" swc-loader: "npm:^0.2.3" tar: "npm:^7.5.6" + terser-webpack-plugin: "npm:^5.1.3" ts-checker-rspack-plugin: "npm:^1.1.5" ts-morph: "npm:^24.0.0" + undici: "npm:^7.2.3" util: "npm:^0.12.3" webpack: "npm:~5.105.0" webpack-dev-server: "npm:^5.0.0" + yaml: "npm:^2.0.0" + yargs: "npm:^16.2.0" yml-loader: "npm:^2.1.0" yn: "npm:^4.0.0" - bin: - cli-module-build: bin/backstage-cli-module-build - languageName: unknown - linkType: soft - -"@backstage/cli-module-config@workspace:^, @backstage/cli-module-config@workspace:packages/cli-module-config": - version: 0.0.0-use.local - resolution: "@backstage/cli-module-config@workspace:packages/cli-module-config" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/cli-node": "workspace:^" - "@backstage/config": "workspace:^" - "@backstage/config-loader": "workspace:^" - "@backstage/types": "workspace:^" - "@manypkg/get-packages": "npm:^1.1.3" - "@types/json-schema": "npm:^7.0.6" - chalk: "npm:^4.0.0" - cleye: "npm:^2.3.0" - json-schema: "npm:^0.4.0" - react-dev-utils: "npm:^12.0.0-next.60" - yaml: "npm:^2.0.0" - bin: - cli-module-config: bin/backstage-cli-module-config - languageName: unknown - linkType: soft - -"@backstage/cli-module-github@workspace:^, @backstage/cli-module-github@workspace:packages/cli-module-github": - version: 0.0.0-use.local - resolution: "@backstage/cli-module-github@workspace:packages/cli-module-github" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/cli-node": "workspace:^" - "@octokit/request": "npm:^8.0.0" - "@types/express": "npm:^4.17.6" - "@types/fs-extra": "npm:^11.0.0" - chalk: "npm:^4.0.0" - cleye: "npm:^2.3.0" - express: "npm:^4.22.0" - fs-extra: "npm:^11.2.0" - inquirer: "npm:^8.2.0" - react-dev-utils: "npm:^12.0.0-next.60" - yaml: "npm:^2.0.0" - bin: - cli-module-github: bin/backstage-cli-module-github - languageName: unknown - linkType: soft - -"@backstage/cli-module-info@workspace:^, @backstage/cli-module-info@workspace:packages/cli-module-info": - version: 0.0.0-use.local - resolution: "@backstage/cli-module-info@workspace:packages/cli-module-info" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/cli-node": "workspace:^" - "@types/fs-extra": "npm:^11.0.0" - cleye: "npm:^2.3.0" - fs-extra: "npm:^11.2.0" - minimatch: "npm:^10.2.1" - bin: - cli-module-info: bin/backstage-cli-module-info - languageName: unknown - linkType: soft - -"@backstage/cli-module-lint@workspace:^, @backstage/cli-module-lint@workspace:packages/cli-module-lint": - version: 0.0.0-use.local - resolution: "@backstage/cli-module-lint@workspace:packages/cli-module-lint" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/cli-node": "workspace:^" - "@types/fs-extra": "npm:^11.0.0" - "@types/shell-quote": "npm:^1.7.5" - chalk: "npm:^4.0.0" - cleye: "npm:^2.3.0" - eslint: "npm:^8.6.0" - eslint-formatter-friendly: "npm:^7.0.0" - fs-extra: "npm:^11.2.0" - globby: "npm:^11.1.0" - shell-quote: "npm:^1.8.1" - bin: - cli-module-lint: bin/backstage-cli-module-lint - languageName: unknown - linkType: soft - -"@backstage/cli-module-maintenance@workspace:^, @backstage/cli-module-maintenance@workspace:packages/cli-module-maintenance": - version: 0.0.0-use.local - resolution: "@backstage/cli-module-maintenance@workspace:packages/cli-module-maintenance" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/cli-node": "workspace:^" - "@types/fs-extra": "npm:^11.0.0" - chalk: "npm:^4.0.0" - cleye: "npm:^2.3.0" - eslint: "npm:^8.6.0" - fs-extra: "npm:^11.2.0" - bin: - cli-module-maintenance: bin/backstage-cli-module-maintenance - languageName: unknown - linkType: soft - -"@backstage/cli-module-migrate@workspace:^, @backstage/cli-module-migrate@workspace:packages/cli-module-migrate": - version: 0.0.0-use.local - resolution: "@backstage/cli-module-migrate@workspace:packages/cli-module-migrate" - dependencies: - "@backstage/backend-test-utils": "workspace:^" - "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/cli-node": "workspace:^" - "@backstage/errors": "workspace:^" - "@backstage/release-manifests": "workspace:^" - "@backstage/test-utils": "workspace:^" - "@manypkg/get-packages": "npm:^1.1.3" - "@types/fs-extra": "npm:^11.0.0" - "@types/semver": "npm:^7" - chalk: "npm:^4.0.0" - cleye: "npm:^2.3.0" - fs-extra: "npm:^11.2.0" - minimatch: "npm:^10.2.1" - msw: "npm:^1.0.0" - ora: "npm:^5.3.0" - replace-in-file: "npm:^7.1.0" - semver: "npm:^7.5.3" - bin: - cli-module-migrate: bin/backstage-cli-module-migrate - languageName: unknown - linkType: soft - -"@backstage/cli-module-new@workspace:^, @backstage/cli-module-new@workspace:packages/cli-module-new": - version: 0.0.0-use.local - resolution: "@backstage/cli-module-new@workspace:packages/cli-module-new" - dependencies: - "@backstage/backend-test-utils": "workspace:^" - "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/cli-node": "workspace:^" - "@backstage/core-plugin-api": "workspace:^" - "@backstage/errors": "workspace:^" - "@backstage/test-utils": "workspace:^" - "@types/fs-extra": "npm:^11.0.0" - "@types/inquirer": "npm:^8.1.3" - "@types/lodash": "npm:^4.14.151" - "@types/recursive-readdir": "npm:^2.2.0" - chalk: "npm:^4.0.0" - cleye: "npm:^2.3.0" - fs-extra: "npm:^11.2.0" - handlebars: "npm:^4.7.3" - inquirer: "npm:^8.2.0" - lodash: "npm:^4.17.21" - ora: "npm:^5.3.0" - recursive-readdir: "npm:^2.2.2" - semver: "npm:^7.5.3" - yaml: "npm:^2.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-validation-error: "npm:^4.0.2" - bin: - cli-module-new: bin/backstage-cli-module-new - languageName: unknown - linkType: soft - -"@backstage/cli-module-test-jest@workspace:^, @backstage/cli-module-test-jest@workspace:packages/cli-module-test-jest": - version: 0.0.0-use.local - resolution: "@backstage/cli-module-test-jest@workspace:packages/cli-module-test-jest" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/cli-node": "workspace:^" - "@swc/core": "npm:^1.15.6" - "@swc/jest": "npm:^0.2.39" - cleye: "npm:^2.3.0" - cross-fetch: "npm:^4.0.0" - fs-extra: "npm:^11.2.0" - glob: "npm:^7.1.7" - jest-css-modules: "npm:^2.1.0" - sucrase: "npm:^3.20.2" - yargs: "npm:^16.2.0" peerDependencies: "@jest/environment-jsdom-abstract": ^30.0.0 + "@module-federation/enhanced": ^0.21.6 + "@pmmmwh/react-refresh-webpack-plugin": ^0.6.0 + esbuild-loader: ^4.0.0 + eslint-webpack-plugin: ^4.2.0 + fork-ts-checker-webpack-plugin: ^9.0.0 jest: ^29.0.0 || ^30.0.0 jest-environment-jsdom: "*" jsdom: ^27.1.0 + mini-css-extract-plugin: ^2.4.2 + terser-webpack-plugin: ^5.1.3 + webpack: ~5.105.0 + webpack-dev-server: ^5.0.0 + dependenciesMeta: + keytar: + optional: true peerDependenciesMeta: "@jest/environment-jsdom-abstract": optional: true - jest-environment-jsdom: + "@module-federation/enhanced": optional: true - jsdom: + "@pmmmwh/react-refresh-webpack-plugin": optional: true - bin: - cli-module-test-jest: bin/backstage-cli-module-test-jest - languageName: unknown - linkType: soft - -"@backstage/cli-module-translations@workspace:^, @backstage/cli-module-translations@workspace:packages/cli-module-translations": - version: 0.0.0-use.local - resolution: "@backstage/cli-module-translations@workspace:packages/cli-module-translations" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/cli-node": "workspace:^" - "@types/fs-extra": "npm:^11.0.0" - cleye: "npm:^2.3.0" - fs-extra: "npm:^11.2.0" - ts-morph: "npm:^24.0.0" - bin: - cli-module-translations: bin/backstage-cli-module-translations - languageName: unknown - linkType: soft - -"@backstage/cli-node@workspace:^, @backstage/cli-node@workspace:packages/cli-node": - version: 0.0.0-use.local - resolution: "@backstage/cli-node@workspace:packages/cli-node" - dependencies: - "@backstage/backend-test-utils": "workspace:^" - "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/errors": "workspace:^" - "@backstage/test-utils": "workspace:^" - "@backstage/types": "workspace:^" - "@manypkg/get-packages": "npm:^1.1.3" - "@types/yarnpkg__lockfile": "npm:^1.1.4" - "@yarnpkg/lockfile": "npm:^1.1.0" - "@yarnpkg/parsers": "npm:^3.0.0" - chalk: "npm:^4.0.0" - commander: "npm:^12.0.0" - fs-extra: "npm:^11.2.0" - pirates: "npm:^4.0.6" - semver: "npm:^7.5.3" - yaml: "npm:^2.0.0" - zod: "npm:^3.25.76" - peerDependencies: - "@swc/core": ^1.15.6 - peerDependenciesMeta: - "@swc/core": + esbuild-loader: optional: true - languageName: unknown - linkType: soft - -"@backstage/cli@workspace:*, @backstage/cli@workspace:^, @backstage/cli@workspace:packages/cli": - version: 0.0.0-use.local - resolution: "@backstage/cli@workspace:packages/cli" - dependencies: - "@backstage/backend-plugin-api": "workspace:^" - "@backstage/backend-test-utils": "workspace:^" - "@backstage/catalog-client": "workspace:^" - "@backstage/cli-common": "workspace:^" - "@backstage/cli-defaults": "workspace:^" - "@backstage/cli-module-build": "workspace:^" - "@backstage/cli-module-test-jest": "workspace:^" - "@backstage/cli-node": "workspace:^" - "@backstage/config": "workspace:^" - "@backstage/core-app-api": "workspace:^" - "@backstage/core-components": "workspace:^" - "@backstage/core-plugin-api": "workspace:^" - "@backstage/dev-utils": "workspace:^" - "@backstage/errors": "workspace:^" - "@backstage/eslint-plugin": "workspace:^" - "@backstage/plugin-auth-backend": "workspace:^" - "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" - "@backstage/plugin-catalog-node": "workspace:^" - "@backstage/plugin-scaffolder-node": "workspace:^" - "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" - "@backstage/test-utils": "workspace:^" - "@backstage/theme": "workspace:^" - "@jest/environment-jsdom-abstract": "npm:^30.0.0" - "@manypkg/get-packages": "npm:^1.1.3" - "@spotify/eslint-config-base": "npm:^15.0.0" - "@spotify/eslint-config-react": "npm:^15.0.0" - "@spotify/eslint-config-typescript": "npm:^15.0.0" - "@swc/core": "npm:^1.15.6" - "@swc/jest": "npm:^0.2.39" - "@types/fs-extra": "npm:^11.0.0" - "@types/jest": "npm:^30.0.0" - "@types/node": "npm:^22.13.14" - "@types/webpack-env": "npm:^1.15.2" - "@typescript-eslint/eslint-plugin": "npm:^8.17.0" - "@typescript-eslint/parser": "npm:^8.16.0" - chalk: "npm:^4.0.0" - commander: "npm:^14.0.3" - cross-fetch: "npm:^4.0.0" - eslint: "npm:^8.6.0" - eslint-config-prettier: "npm:^9.0.0" - eslint-plugin-deprecation: "npm:^3.0.0" - eslint-plugin-import: "npm:^2.31.0" - eslint-plugin-jest: "npm:^28.9.0" - eslint-plugin-jsx-a11y: "npm:^6.10.2" - eslint-plugin-react: "npm:^7.37.2" - eslint-plugin-react-hooks: "npm:^5.0.0" - eslint-plugin-unused-imports: "npm:^4.1.4" - fs-extra: "npm:^11.2.0" - glob: "npm:^7.1.7" - jest: "npm:^30.2.0" - jest-css-modules: "npm:^2.1.0" - jsdom: "npm:^27.1.0" - nodemon: "npm:^3.0.1" - pirates: "npm:^4.0.6" - postcss: "npm:^8.1.0" - sucrase: "npm:^3.20.2" - yaml: "npm:^2.0.0" - peerDependencies: - "@jest/environment-jsdom-abstract": ^30.0.0 - jest: ^29.0.0 || ^30.0.0 - jest-environment-jsdom: "*" - jsdom: ^27.1.0 - peerDependenciesMeta: - "@jest/environment-jsdom-abstract": + eslint-webpack-plugin: + optional: true + fork-ts-checker-webpack-plugin: optional: true jest-environment-jsdom: optional: true jsdom: optional: true + mini-css-extract-plugin: + optional: true + terser-webpack-plugin: + optional: true + webpack: + optional: true + webpack-dev-server: + optional: true bin: backstage-cli: bin/backstage-cli languageName: unknown @@ -3339,7 +3109,7 @@ __metadata: react-router-stable: "npm:react-router@^6.3.0" react-use: "npm:^17.2.4" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -3359,7 +3129,6 @@ __metadata: "@backstage/core-app-api": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" - "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" @@ -3376,7 +3145,7 @@ __metadata: react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -3459,7 +3228,7 @@ __metadata: rehype-sanitize: "npm:^5.0.0" remark-gfm: "npm:^3.0.1" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -3492,7 +3261,7 @@ __metadata: react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -3615,7 +3384,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/types": "workspace:^" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-validation-error: "npm:^4.0.2" languageName: unknown linkType: soft @@ -3629,12 +3398,10 @@ __metadata: "@backstage/core-app-api": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" - "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-defaults": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" "@backstage/plugin-app": "workspace:^" - "@backstage/plugin-permission-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@backstage/version-bridge": "workspace:^" @@ -3645,7 +3412,7 @@ __metadata: react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -3670,8 +3437,6 @@ __metadata: "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-app": "workspace:^" "@backstage/plugin-app-react": "workspace:^" - "@backstage/plugin-permission-common": "workspace:^" - "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@react-hookz/web": "npm:^24.0.0" "@testing-library/jest-dom": "npm:^6.0.0" @@ -3691,33 +3456,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/frontend-dev-utils@workspace:^, @backstage/frontend-dev-utils@workspace:packages/frontend-dev-utils": - version: 0.0.0-use.local - resolution: "@backstage/frontend-dev-utils@workspace:packages/frontend-dev-utils" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/frontend-defaults": "workspace:^" - "@backstage/frontend-plugin-api": "workspace:^" - "@backstage/plugin-app": "workspace:^" - "@backstage/test-utils": "workspace:^" - "@backstage/ui": "workspace:^" - "@testing-library/jest-dom": "npm:^6.0.0" - "@testing-library/react": "npm:^16.0.0" - "@types/react": "npm:^18.0.0" - react: "npm:^18.0.2" - react-dom: "npm:^18.0.2" - react-router-dom: "npm:^6.30.2" - peerDependencies: - "@types/react": ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 - react-router-dom: ^6.30.2 - peerDependenciesMeta: - "@types/react": - optional: true - languageName: unknown - linkType: soft - "@backstage/frontend-dynamic-feature-loader@workspace:packages/frontend-dynamic-feature-loader": version: 0.0.0-use.local resolution: "@backstage/frontend-dynamic-feature-loader@workspace:packages/frontend-dynamic-feature-loader" @@ -3757,7 +3495,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" - "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" "@backstage/test-utils": "workspace:^" @@ -3770,7 +3507,7 @@ __metadata: react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 @@ -3791,7 +3528,6 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/core-app-api": "workspace:^" "@backstage/core-plugin-api": "workspace:^" - "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-app": "workspace:^" @@ -3811,7 +3547,7 @@ __metadata: react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@testing-library/react": ^16.0.0 "@types/jest": "*" @@ -3968,12 +3704,10 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" - "@backstage/ui": "workspace:^" "@graphiql/react": "npm:0.29.0" "@material-ui/core": "npm:^4.12.2" "@material-ui/icons": "npm:^4.9.1" "@material-ui/lab": "npm:4.0.0-alpha.61" - "@remixicon/react": "npm:^4.6.0" "@testing-library/dom": "npm:^10.0.0" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" @@ -4077,7 +3811,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" - "@backstage/frontend-dev-utils": "workspace:^" + "@backstage/frontend-defaults": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/ui": "workspace:^" "@remixicon/react": "npm:^4.6.0" @@ -4105,7 +3839,6 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" - "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-defaults": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" @@ -4120,23 +3853,17 @@ __metadata: "@material-ui/core": "npm:^4.9.13" "@material-ui/icons": "npm:^4.9.1" "@material-ui/lab": "npm:^4.0.0-alpha.61" - "@react-aria/button": "npm:^3.14.3" - "@react-aria/toast": "npm:^3.0.9" "@react-hookz/web": "npm:^24.0.0" - "@react-stately/toast": "npm:^3.1.2" - "@remixicon/react": "npm:^4.6.0" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.0.0" "@types/react": "npm:^18.0.0" - motion: "npm:^12.0.0" msw: "npm:^1.0.0" react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" react-use: "npm:^17.2.4" - zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -4163,7 +3890,7 @@ __metadata: passport: "npm:^0.7.0" passport-atlassian-oauth2: "npm:^2.1.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4206,7 +3933,7 @@ __metadata: jose: "npm:^5.0.0" msw: "npm:^2.0.8" node-cache: "npm:^5.1.2" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4225,7 +3952,7 @@ __metadata: express: "npm:^4.22.0" jose: "npm:^5.0.0" passport: "npm:^0.7.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4244,7 +3971,7 @@ __metadata: passport: "npm:^0.7.0" passport-bitbucket-oauth2: "npm:^0.1.2" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4263,7 +3990,7 @@ __metadata: passport: "npm:^0.7.0" passport-oauth2: "npm:^1.6.1" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4285,7 +4012,7 @@ __metadata: msw: "npm:^2.0.0" node-mocks-http: "npm:^1.0.0" uuid: "npm:^11.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4301,7 +4028,7 @@ __metadata: "@backstage/types": "workspace:^" express: "npm:^4.22.0" google-auth-library: "npm:^9.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4319,7 +4046,7 @@ __metadata: "@types/passport-github2": "npm:^1.2.4" passport-github2: "npm:^0.1.12" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4338,7 +4065,7 @@ __metadata: passport: "npm:^0.7.0" passport-gitlab2: "npm:^5.0.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4356,7 +4083,7 @@ __metadata: google-auth-library: "npm:^9.0.0" passport-google-oauth20: "npm:^2.0.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4394,7 +4121,7 @@ __metadata: msw: "npm:^1.0.0" passport-microsoft: "npm:^1.0.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4412,7 +4139,7 @@ __metadata: passport: "npm:^0.7.0" passport-oauth2: "npm:^1.6.1" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4426,7 +4153,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" jose: "npm:^5.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4451,7 +4178,7 @@ __metadata: openid-client: "npm:^5.5.0" passport: "npm:^0.7.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4470,7 +4197,7 @@ __metadata: express: "npm:^4.22.0" passport: "npm:^0.7.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4489,7 +4216,7 @@ __metadata: passport: "npm:^0.7.0" passport-onelogin-oauth: "npm:^0.0.1" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4510,7 +4237,7 @@ __metadata: msw: "npm:^2.7.3" passport-oauth2: "npm:^1.8.0" supertest: "npm:^7.1.0" - zod: "npm:^3.24.2" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -4626,7 +4353,7 @@ __metadata: passport: "npm:^0.7.0" supertest: "npm:^7.0.0" uuid: "npm:^11.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" zod-validation-error: "npm:^4.0.2" languageName: unknown @@ -4667,13 +4394,16 @@ __metadata: resolution: "@backstage/plugin-auth@workspace:plugins/auth" dependencies: "@backstage/cli": "workspace:^" + "@backstage/core-components": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/frontend-defaults": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/test-utils": "workspace:^" - "@backstage/ui": "workspace:^" - "@remixicon/react": "npm:^4.6.0" + "@backstage/theme": "workspace:^" + "@material-ui/core": "npm:^4.12.2" + "@material-ui/icons": "npm:^4.9.1" + "@material-ui/lab": "npm:4.0.0-alpha.61" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.0.0" @@ -5155,7 +4885,7 @@ __metadata: winston: "npm:^3.13.0" yaml: "npm:^2.0.0" yn: "npm:^4.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-validation-error: "npm:^4.0.2" languageName: unknown linkType: soft @@ -5339,7 +5069,7 @@ __metadata: react-use: "npm:^17.2.4" yaml: "npm:^2.0.0" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@backstage/frontend-test-utils": "workspace:^" "@types/react": ^17.0.0 || ^18.0.0 @@ -5878,7 +5608,7 @@ __metadata: react-resizable: "npm:^3.0.4" react-router-dom: "npm:^6.30.2" react-use: "npm:^17.2.4" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -6108,7 +5838,7 @@ __metadata: express-promise-router: "npm:^4.1.0" minimatch: "npm:^10.2.1" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -6412,7 +6142,7 @@ __metadata: msw: "npm:^1.0.0" supertest: "npm:^7.0.0" yn: "npm:^4.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -6427,7 +6157,7 @@ __metadata: cross-fetch: "npm:^4.0.0" msw: "npm:^1.0.0" uuid: "npm:^11.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" languageName: unknown linkType: soft @@ -6450,7 +6180,7 @@ __metadata: express-promise-router: "npm:^4.1.0" msw: "npm:^1.0.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" languageName: unknown linkType: soft @@ -6467,7 +6197,6 @@ __metadata: "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@types/react": "npm:^18.0.0" - dataloader: "npm:^2.0.0" react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" @@ -6551,7 +6280,7 @@ __metadata: fs-extra: "npm:^11.2.0" msw: "npm:^1.0.0" yaml: "npm:^2.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -6690,7 +6419,7 @@ __metadata: octokit: "npm:^3.0.0" octokit-plugin-create-pull-request: "npm:^5.0.0" yaml: "npm:^2.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -6711,7 +6440,7 @@ __metadata: "@gitbeaker/rest": "npm:^43.8.0" luxon: "npm:^3.0.0" yaml: "npm:^2.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -6836,7 +6565,7 @@ __metadata: winston-transport: "npm:^4.7.0" yaml: "npm:^2.0.0" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" languageName: unknown linkType: soft @@ -6915,7 +6644,7 @@ __metadata: tar: "npm:^7.5.6" winston: "npm:^3.2.1" winston-transport: "npm:^4.7.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" peerDependencies: "@backstage/backend-test-utils": "workspace:^" @@ -6980,7 +6709,7 @@ __metadata: swr: "npm:^2.0.0" use-immer: "npm:^0.11.0" zen-observable: "npm:^0.10.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" peerDependencies: "@backstage/frontend-test-utils": "workspace:^" @@ -7065,7 +6794,7 @@ __metadata: react-window: "npm:^1.8.10" swr: "npm:^2.0.0" yaml: "npm:^2.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" zod-to-json-schema: "npm:^3.25.1" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 @@ -7234,7 +6963,7 @@ __metadata: qs: "npm:^6.10.1" supertest: "npm:^7.0.0" yn: "npm:^4.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -7700,7 +7429,7 @@ __metadata: knex: "npm:^3.0.0" p-limit: "npm:^3.1.0" supertest: "npm:^7.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -7822,7 +7551,7 @@ __metadata: ts-morph: "npm:^24.0.0" typedoc: "npm:^0.28.0" yaml-diff-patch: "npm:^2.0.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@microsoft/api-extractor-model": "*" "@microsoft/tsdoc": "*" @@ -9944,21 +9673,11 @@ __metadata: languageName: node linkType: hard -"@internal/cli@workspace:packages/cli-internal": - version: 0.0.0-use.local - resolution: "@internal/cli@workspace:packages/cli-internal" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/cli-node": "workspace:^" - languageName: unknown - linkType: soft - "@internal/frontend@workspace:packages/frontend-internal": version: 0.0.0-use.local resolution: "@internal/frontend@workspace:packages/frontend-internal" dependencies: "@backstage/cli": "workspace:^" - "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" @@ -10042,7 +9761,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-scaffolder-react": "workspace:^" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -15710,7 +15429,7 @@ __metadata: languageName: node linkType: hard -"@react-aria/button@npm:^3.14.3, @react-aria/button@npm:^3.14.5": +"@react-aria/button@npm:^3.14.5": version: 3.14.5 resolution: "@react-aria/button@npm:3.14.5" dependencies: @@ -16459,7 +16178,7 @@ __metadata: languageName: node linkType: hard -"@react-aria/toast@npm:^3.0.11, @react-aria/toast@npm:^3.0.9": +"@react-aria/toast@npm:^3.0.11": version: 3.0.11 resolution: "@react-aria/toast@npm:3.0.11" dependencies: @@ -16984,7 +16703,7 @@ __metadata: languageName: node linkType: hard -"@react-stately/toast@npm:^3.1.2, @react-stately/toast@npm:^3.1.3": +"@react-stately/toast@npm:^3.1.3": version: 3.1.3 resolution: "@react-stately/toast@npm:3.1.3" dependencies: @@ -18320,6 +18039,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/merge-streams@npm:^2.1.0": + version: 2.3.0 + resolution: "@sindresorhus/merge-streams@npm:2.3.0" + checksum: 10/798bcb53cd1ace9df84fcdd1ba86afdc9e0cd84f5758d26ae9b1eefd8e8887e5fc30051132b9e74daf01bb41fa5a2faf1369361f83d76a3b3d7ee938058fd71c + languageName: node + linkType: hard + "@sinonjs/commons@npm:^2.0.0": version: 2.0.0 resolution: "@sinonjs/commons@npm:2.0.0" @@ -20616,7 +20342,7 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:^0.5.0, @swc/helpers@npm:^0.5.8": +"@swc/helpers@npm:^0.5.0, @swc/helpers@npm:^0.5.17, @swc/helpers@npm:^0.5.8": version: 0.5.19 resolution: "@swc/helpers@npm:0.5.19" dependencies: @@ -21420,6 +21146,13 @@ __metadata: languageName: node linkType: hard +"@types/ejs@npm:^3.1.3": + version: 3.1.5 + resolution: "@types/ejs@npm:3.1.5" + checksum: 10/918898fd279108087722c1713e2ddb0c152ab839397946d164db8a18b5bbd732af9746373882a9bcf4843d35c6b191a8f569a7a4e51e90726d24501b39f40367 + languageName: node + linkType: hard + "@types/emscripten@npm:^1.39.6": version: 1.39.10 resolution: "@types/emscripten@npm:1.39.10" @@ -22492,6 +22225,24 @@ __metadata: languageName: node linkType: hard +"@types/rollup-plugin-peer-deps-external@npm:^2.2.0": + version: 2.2.6 + resolution: "@types/rollup-plugin-peer-deps-external@npm:2.2.6" + peerDependencies: + rollup: "*" + checksum: 10/9fcee30d60d9d8de0f4bbf7694c254a18798b0ff415563a60fb2ed8c6c46423c64e1a977e0344b8393c26221dbd6e74d9e872c2694b518fcf149ba85d94e7eac + languageName: node + linkType: hard + +"@types/rollup-plugin-postcss@npm:^3.1.4": + version: 3.1.4 + resolution: "@types/rollup-plugin-postcss@npm:3.1.4" + dependencies: + rollup-plugin-postcss: "npm:*" + checksum: 10/a94119c43db77ad10a96f34a4190b678e87fe63bdedfb2b989b64a9c0e15680c034d728daa4ade9cfd31d2fdc48827c79ff69ad2599d0b323c2c321896866cf5 + languageName: node + linkType: hard + "@types/sarif@npm:^2.1.4": version: 2.1.5 resolution: "@types/sarif@npm:2.1.5" @@ -22506,10 +22257,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7, @types/semver@npm:^7.1.0": - version: 7.7.1 - resolution: "@types/semver@npm:7.7.1" - checksum: 10/8f09e7e6ca3ded67d78ba7a8f7535c8d9cf8ced83c52e7f3ac3c281fe8c689c3fe475d199d94390dc04fc681d51f2358b430bb7b2e21c62de24f2bee2c719068 +"@types/semver@npm:^7.1.0": + version: 7.7.0 + resolution: "@types/semver@npm:7.7.0" + checksum: 10/ee4514c6c852b1c38f951239db02f9edeea39f5310fad9396a00b51efa2a2d96b3dfca1ae84c88181ea5b7157c57d32d7ef94edacee36fbf975546396b85ba5b languageName: node linkType: hard @@ -22593,6 +22344,13 @@ __metadata: languageName: node linkType: hard +"@types/source-list-map@npm:*": + version: 0.1.6 + resolution: "@types/source-list-map@npm:0.1.6" + checksum: 10/9cd294c121f1562062de5d241fe4d10780b1131b01c57434845fe50968e9dcf67ede444591c2b1ad6d3f9b6bc646ac02cc8f51a3577c795f9c64cf4573dcc6b1 + languageName: node + linkType: hard + "@types/ssh2-streams@npm:*": version: 0.1.8 resolution: "@types/ssh2-streams@npm:0.1.8" @@ -22663,6 +22421,15 @@ __metadata: languageName: node linkType: hard +"@types/svgo@npm:^2.6.2": + version: 2.6.4 + resolution: "@types/svgo@npm:2.6.4" + dependencies: + "@types/node": "npm:*" + checksum: 10/9632b350949677fa68d6f13b4d45495a4af3108bb5f020a7257ae5a883e20b9efc0fada3bc3e012f215be312fabe5a28485fffaff5afd6da4daa8cb4fe5b04a2 + languageName: node + linkType: hard + "@types/swagger-ui-react@npm:^5.0.0": version: 5.18.0 resolution: "@types/swagger-ui-react@npm:5.18.0" @@ -22690,6 +22457,15 @@ __metadata: languageName: node linkType: hard +"@types/terser-webpack-plugin@npm:^5.0.4": + version: 5.2.0 + resolution: "@types/terser-webpack-plugin@npm:5.2.0" + dependencies: + terser-webpack-plugin: "npm:*" + checksum: 10/475b0f160c9f83641255f6516b69d7908b9e3e8e8ab653f3a257690249f9614f9386c0897eba6e4e35182ec0d83f57454d62fb94b38ae7c171891641350072ee + languageName: node + linkType: hard + "@types/through@npm:*": version: 0.0.30 resolution: "@types/through@npm:0.0.30" @@ -22772,6 +22548,17 @@ __metadata: languageName: node linkType: hard +"@types/webpack-sources@npm:^3.2.3": + version: 3.2.3 + resolution: "@types/webpack-sources@npm:3.2.3" + dependencies: + "@types/node": "npm:*" + "@types/source-list-map": "npm:*" + source-map: "npm:^0.7.3" + checksum: 10/7b557f242efaa10e4e3e18cc4171a0c98e22898570caefdd4f7b076fe8534b5abfac92c953c6604658dcb7218507f970230352511840fe9fdea31a9af3b9a906 + languageName: node + linkType: hard + "@types/webpack@npm:^5.28.0": version: 5.28.5 resolution: "@types/webpack@npm:5.28.5" @@ -27478,7 +27265,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^12.0.0, commander@npm:^12.1.0": +"commander@npm:^12.1.0": version: 12.1.0 resolution: "commander@npm:12.1.0" checksum: 10/cdaeb672d979816853a4eed7f1310a9319e8b976172485c2a6b437ed0db0a389a44cfb222bfbde772781efa9f215bdd1b936f80d6b249485b465c6cb906e1f93 @@ -28959,6 +28746,21 @@ __metadata: languageName: node linkType: hard +"del@npm:^8.0.0": + version: 8.0.1 + resolution: "del@npm:8.0.1" + dependencies: + globby: "npm:^14.0.2" + is-glob: "npm:^4.0.3" + is-path-cwd: "npm:^3.0.0" + is-path-inside: "npm:^4.0.0" + p-map: "npm:^7.0.2" + presentable-error: "npm:^0.0.1" + slash: "npm:^5.1.0" + checksum: 10/53ed4a379a68c90e7d6d3bcce09c49229e77de9a946d0a5fc25f45b16c950cb8665986b7d0d0423416c03bfd43e0f31e528c5a19c558fe47449be9d6fae7f846 + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -31204,13 +31006,13 @@ __metadata: linkType: hard "express-rate-limit@npm:^8.2.1, express-rate-limit@npm:^8.2.2": - version: 8.3.1 - resolution: "express-rate-limit@npm:8.3.1" + version: 8.3.0 + resolution: "express-rate-limit@npm:8.3.0" dependencies: ip-address: "npm:10.1.0" peerDependencies: express: ">= 4.11" - checksum: 10/dd97bfc48c01a6d4c5433203232b5e7a1e55e21322bde49033e5f8c4339584fe671a94096144a0810f4ea21dcec8aaaf15823109627e609f8ed1bc5912a345cf + checksum: 10/e896a66fecc10639e65873186fdfb71f19d6af650220eb7ea5450725215c3eed8dc6ddcfa1e68a9db8c9facc3326fbc281512ad3ccd8f107f42a2466ce12c18c languageName: node linkType: hard @@ -32117,28 +31919,6 @@ __metadata: languageName: node linkType: hard -"framer-motion@npm:^12.38.0": - version: 12.38.0 - resolution: "framer-motion@npm:12.38.0" - dependencies: - motion-dom: "npm:^12.38.0" - motion-utils: "npm:^12.36.0" - tslib: "npm:^2.4.0" - peerDependencies: - "@emotion/is-prop-valid": "*" - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@emotion/is-prop-valid": - optional: true - react: - optional: true - react-dom: - optional: true - checksum: 10/4d529d1648a8e31ec9859e7ff1296b7e4ef0028eb09cbc7d626068766ab53e486038b431fac33b1438a1cc076a244e6843c5a8c0f38442885832308452b4b25e - languageName: node - linkType: hard - "framer-motion@npm:^6.5.1": version: 6.5.1 resolution: "framer-motion@npm:6.5.1" @@ -32929,6 +32709,20 @@ __metadata: languageName: node linkType: hard +"globby@npm:^14.0.2": + version: 14.0.2 + resolution: "globby@npm:14.0.2" + dependencies: + "@sindresorhus/merge-streams": "npm:^2.1.0" + fast-glob: "npm:^3.3.2" + ignore: "npm:^5.2.4" + path-type: "npm:^5.0.0" + slash: "npm:^5.1.0" + unicorn-magic: "npm:^0.1.0" + checksum: 10/67660da70fc1223f7170c1a62ba6c373385e9e39765d952b6518606dec15ed8c7958e9dae6ba5752a31dbc1e9126f146938b830ad680fe794141734ffc3fbb75 + languageName: node + linkType: hard + "globrex@npm:^0.1.2": version: 0.1.2 resolution: "globrex@npm:0.1.2" @@ -34190,7 +33984,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.1.4, ignore@npm:^5.2.0": +"ignore@npm:^5.1.4, ignore@npm:^5.2.0, ignore@npm:^5.2.4": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 @@ -34999,6 +34793,13 @@ __metadata: languageName: node linkType: hard +"is-path-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "is-path-cwd@npm:3.0.0" + checksum: 10/bc34d13b6a03dfca4a3ab6a8a5ba78ae4b24f4f1db4b2b031d2760c60d0913bd16a4b980dcb4e590adfc906649d5f5132684079a3972bd219da49deebb9adea8 + languageName: node + linkType: hard + "is-path-inside@npm:^3.0.2, is-path-inside@npm:^3.0.3": version: 3.0.3 resolution: "is-path-inside@npm:3.0.3" @@ -35006,6 +34807,13 @@ __metadata: languageName: node linkType: hard +"is-path-inside@npm:^4.0.0": + version: 4.0.0 + resolution: "is-path-inside@npm:4.0.0" + checksum: 10/8810fa11c58e6360b82c3e0d6cd7d9c7d0392d3ac9eb10f980b81f9839f40ac6d1d6d6f05d069db0d227759801228f0b072e1b6c343e4469b065ab5fe0b68fe5 + languageName: node + linkType: hard + "is-plain-obj@npm:^3.0.0": version: 3.0.0 resolution: "is-plain-obj@npm:3.0.0" @@ -39012,14 +38820,14 @@ __metadata: linkType: hard "mini-css-extract-plugin@npm:^2.4.2": - version: 2.10.1 - resolution: "mini-css-extract-plugin@npm:2.10.1" + version: 2.10.0 + resolution: "mini-css-extract-plugin@npm:2.10.0" dependencies: schema-utils: "npm:^4.0.0" tapable: "npm:^2.2.1" peerDependencies: webpack: ^5.0.0 - checksum: 10/2d0cecc3bea85cd7f9b1ce0974f1672976d610a9267e2988ff19f5d03b017bff12b32151a412de0f519a70be7d3b050b499b20101445fb21728cc2d35dd4041a + checksum: 10/bae5350ab82171c6c9a22a4397df14aa69280f5ff0e1ff4d2429ea841bc096927b1e27ba7b75a9c3dd77bd44bab449d6197bd748381f1326cbc8befcb10d1a9e languageName: node linkType: hard @@ -39477,43 +39285,6 @@ __metadata: languageName: node linkType: hard -"motion-dom@npm:^12.38.0": - version: 12.38.0 - resolution: "motion-dom@npm:12.38.0" - dependencies: - motion-utils: "npm:^12.36.0" - checksum: 10/78c040b46d93273932cf80c70e39845be5a442dcaf18d4345b45a9193de9dfa87c885b609943cb652115e4eac5d46ef40b452185073dd43fc328b134f9975e90 - languageName: node - linkType: hard - -"motion-utils@npm:^12.36.0": - version: 12.36.0 - resolution: "motion-utils@npm:12.36.0" - checksum: 10/c4a2a7ffac48ca44082d6d31b115f245025060a7e69d70dac062646d8f96c39e5662a7c8a51f255566fdf8e719ef1269a8e9aa3a04fc263bb65b5a7b61331901 - languageName: node - linkType: hard - -"motion@npm:^12.0.0": - version: 12.38.0 - resolution: "motion@npm:12.38.0" - dependencies: - framer-motion: "npm:^12.38.0" - tslib: "npm:^2.4.0" - peerDependencies: - "@emotion/is-prop-valid": "*" - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@emotion/is-prop-valid": - optional: true - react: - optional: true - react-dom: - optional: true - checksum: 10/d7ae2ba3cc112c4467822956b92065239640b9c62204d3bee1780da9fc0147185373534138d39975e82bf73b5f1b28d3fb3581031e4e7e0cfb230472767bd10d - languageName: node - linkType: hard - "mri@npm:1.1.4": version: 1.1.4 resolution: "mri@npm:1.1.4" @@ -41795,6 +41566,13 @@ __metadata: languageName: node linkType: hard +"path-type@npm:^5.0.0": + version: 5.0.0 + resolution: "path-type@npm:5.0.0" + checksum: 10/15ec24050e8932c2c98d085b72cfa0d6b4eeb4cbde151a0a05726d8afae85784fc5544f733d8dfc68536587d5143d29c0bd793623fad03d7e61cc00067291cd5 + languageName: node + linkType: hard + "pathe@npm:^2.0.3": version: 2.0.3 resolution: "pathe@npm:2.0.3" @@ -42820,6 +42598,13 @@ __metadata: languageName: node linkType: hard +"presentable-error@npm:^0.0.1": + version: 0.0.1 + resolution: "presentable-error@npm:0.0.1" + checksum: 10/013809ee7a47ced847a8d860e9b89a56cdd8c4f1ad04ad8da1e58fd60843f77f497d204146bb15aaa9793d3b94ad8626eed01256fc9eb5839a545af2000a5fa4 + languageName: node + linkType: hard + "prettier@npm:^2.2.1, prettier@npm:^2.7.1": version: 2.8.8 resolution: "prettier@npm:2.8.8" @@ -45344,7 +45129,7 @@ __metadata: languageName: node linkType: hard -"rollup-plugin-postcss@npm:^4.0.0": +"rollup-plugin-postcss@npm:*, rollup-plugin-postcss@npm:^4.0.0": version: 4.0.2 resolution: "rollup-plugin-postcss@npm:4.0.2" dependencies: @@ -45489,7 +45274,6 @@ __metadata: resolution: "root@workspace:." dependencies: "@backstage/cli": "workspace:*" - "@backstage/cli-defaults": "workspace:*" "@backstage/codemods": "workspace:*" "@backstage/create-app": "workspace:*" "@backstage/e2e-test-utils": "workspace:*" @@ -45547,7 +45331,7 @@ __metadata: typescript: "npm:~5.7.0" vite: "npm:^7.1.5" yaml: "npm:^2.7.0" - zod: "npm:^3.25.76" + zod: "npm:^3.25.76 || ^4.0.0" languageName: unknown linkType: soft @@ -46445,6 +46229,13 @@ __metadata: languageName: node linkType: hard +"slash@npm:^5.1.0": + version: 5.1.0 + resolution: "slash@npm:5.1.0" + checksum: 10/2c41ec6fb1414cd9bba0fa6b1dd00e8be739e3fe85d079c69d4b09ca5f2f86eafd18d9ce611c0c0f686428638a36c272a6ac14799146a8295f259c10cc45cde4 + languageName: node + linkType: hard + "slice-ansi@npm:^3.0.0": version: 3.0.0 resolution: "slice-ansi@npm:3.0.0" @@ -48043,7 +47834,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:^5.3.17": +"terser-webpack-plugin@npm:*, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.17": version: 5.3.17 resolution: "terser-webpack-plugin@npm:5.3.17" dependencies: @@ -49294,9 +49085,9 @@ __metadata: linkType: hard "undici@npm:^7.2.3, undici@npm:^7.22.0": - version: 7.24.4 - resolution: "undici@npm:7.24.4" - checksum: 10/747e76e0fd685ae1bb6fc1a2ebce0caca4ee8bd5599a77da36a3f94eac146987a9547bdbec7a74d18c0776df8ad348dccb4209901ca83fc4076f560de0d5dc7a + version: 7.22.0 + resolution: "undici@npm:7.22.0" + checksum: 10/a7a1813ba4b74c0d46cc8dd160386202c05699ffc487c5d882cf40e6d2435c8d6faff3b8f8675d09bd1ef0386e370675c26b59b9a8c8b3f17b9f82a42236a927 languageName: node linkType: hard @@ -49309,6 +49100,13 @@ __metadata: languageName: node linkType: hard +"unicorn-magic@npm:^0.1.0": + version: 0.1.0 + resolution: "unicorn-magic@npm:0.1.0" + checksum: 10/9b4d0e9809807823dc91d0920a4a4c0cff2de3ebc54ee87ac1ee9bc75eafd609b09d1f14495e0173aef26e01118706196b6ab06a75fe0841028b3983a8af313f + languageName: node + linkType: hard + "unified@npm:^10.0.0": version: 10.1.0 resolution: "unified@npm:10.1.0" @@ -51065,12 +50863,12 @@ __metadata: linkType: soft "yauzl@npm:^3.0.0": - version: 3.2.1 - resolution: "yauzl@npm:3.2.1" + version: 3.2.0 + resolution: "yauzl@npm:3.2.0" dependencies: buffer-crc32: "npm:~0.2.3" pend: "npm:~1.2.0" - checksum: 10/15dfae75fbfe59c6a1b7a2cb27a995cda0ee70549d32d6b19937e84897436170f169f6bbefc34b9e9beb9c9114a1b8a8a40e7687a907909a19681ebcbf35a1f3 + checksum: 10/a3cd2bfcf7590673bb35750f2a4e5107e3cc939d32d98a072c0673fe42329e390f471b4a53dbbd72512229099b18aa3b79e6ddb87a73b3a17446080c903a2c4b languageName: node linkType: hard @@ -51227,14 +51025,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.24.2, zod@npm:^3.25.76": - version: 3.25.76 - resolution: "zod@npm:3.25.76" - checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995 - languageName: node - linkType: hard - -"zod@npm:^3.25 || ^4.0, zod@npm:^4.1.11, zod@npm:^4.3.5": +"zod@npm:^3.25 || ^4.0, zod@npm:^3.25.76 || ^4.0.0, zod@npm:^4.1.11, zod@npm:^4.3.5": version: 4.3.6 resolution: "zod@npm:4.3.6" checksum: 10/25fc0f62e01b557b4644bf0b393bbaf47542ab30877c37837ea8caf314a8713d220c7d7fe51f68ffa72f0e1018ddfa34d96f1973d23033f5a2a5a9b6b9d9da01 From ffaded0b3060b1b95d76b16a159af5703b235656 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Wed, 11 Mar 2026 08:42:02 +0100 Subject: [PATCH 31/55] chore: lint & changeset Signed-off-by: Gabriel Dugny --- .changeset/silver-snails-pull.md | 13 ++++--------- .../templates/backend-plugin/package.json.hbs | 2 +- plugins/auth-backend/src/service/OidcRouter.ts | 2 +- .../src/service/request/entitiesBatchRequest.ts | 1 + 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.changeset/silver-snails-pull.md b/.changeset/silver-snails-pull.md index e9b79ccdc89483..b6bbacf39defc2 100644 --- a/.changeset/silver-snails-pull.md +++ b/.changeset/silver-snails-pull.md @@ -20,12 +20,14 @@ '@backstage/plugin-auth-backend-module-okta-provider': patch '@backstage/plugin-scaffolder-backend-module-github': patch '@backstage/plugin-scaffolder-backend-module-gitlab': patch +'@backstage/plugin-user-settings-backend': patch '@backstage/frontend-plugin-api': patch '@backstage/frontend-test-utils': patch '@internal/scaffolder': patch '@backstage/backend-plugin-api': patch '@backstage/backend-test-utils': patch '@backstage/plugin-mcp-actions-backend': patch +'@backstage/filter-predicates': patch '@backstage/plugin-permission-backend': patch '@backstage/plugin-scaffolder-backend': patch '@backstage/backend-defaults': patch @@ -42,22 +44,15 @@ '@backstage/plugin-search-backend': patch '@backstage/core-app-api': patch '@backstage/plugin-catalog-react': patch -'@backstage/plugin-techdocs-node': patch +'@backstage/plugin-auth-backend': patch '@backstage/repo-tools': patch -'@backstage/plugin-app-backend': patch -'@backstage/plugin-mui-to-bui': patch '@backstage/plugin-scaffolder': patch -'example-app-next': patch '@backstage/cli-node': patch -'@backstage/plugin-app-react': patch '@backstage/plugin-auth-node': patch '@backstage/plugin-api-docs': patch -'@backstage/plugin-app-node': patch '@backstage/cli': patch '@backstage/plugin-home': patch -'@backstage/ui': patch '@backstage/plugin-app': patch -'@backstage/plugin-org': patch --- -Updated dependency `zod` to `3.25.76 || ^4.0.0` +Updated dependency `zod` to `^3.25.76 || ^4.0.0` & migrated to `/v3` or `/v4` imports. diff --git a/packages/cli-module-new/templates/backend-plugin/package.json.hbs b/packages/cli-module-new/templates/backend-plugin/package.json.hbs index c1555a058f9d2b..eb08e2a54b0c20 100644 --- a/packages/cli-module-new/templates/backend-plugin/package.json.hbs +++ b/packages/cli-module-new/templates/backend-plugin/package.json.hbs @@ -29,7 +29,7 @@ "@backstage/plugin-catalog-node": "{{versionQuery '@backstage/plugin-catalog-node'}}", "express": "{{versionQuery 'express' '4.17.1'}}", "express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}", - "zod": "{{versionQuery 'zod' '3.25.76 || ^4.0.0'}}" + "zod": "{{versionQuery 'zod' '^3.25.76 || ^4.0.0'}}" }, "devDependencies": { "@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}", diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 304bfa5ed9f7e0..e7ed82b6bd262a 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -28,7 +28,7 @@ import { OidcDatabase } from '../database/OidcDatabase'; import { OfflineAccessService } from './OfflineAccessService'; import { json } from 'express'; import { z } from 'zod/v4'; -import { fromZodError } from 'zod-validation-error'; +import { fromZodError } from 'zod-validation-error/v4'; import { OidcError } from './OidcError'; function ensureTrailingSlash(url: string): string { diff --git a/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts b/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts index 09e40f45bc725d..02ad1d5566adae 100644 --- a/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts +++ b/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts @@ -22,6 +22,7 @@ import { import { Request } from 'express'; import { z } from 'zod/v3'; import { fromZodError } from 'zod-validation-error/v3'; + const filterPredicateSchema = createZodV3FilterPredicateSchema(z); const schema = z.object({ From e6c41459f56b3eb9a81fbc2b80734c79a50d3ba0 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Sat, 14 Mar 2026 11:38:12 +0100 Subject: [PATCH 32/55] chore: Switch some zod imports to type only Signed-off-by: Gabriel Dugny --- .../src/lib/AuthSessionManager/AuthSessionStore.ts | 2 +- .../src/layout/ProxiedSignInPage/types.test.ts | 2 +- plugins/auth-node/report.api.md | 4 ++-- plugins/auth-node/src/sign-in/createSignInResolverFactory.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts index d3be6748bfd25a..fc3030a7a6c40e 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ZodSchema } from 'zod/v3'; +import type { ZodSchema } from 'zod/v3'; import { MutableSessionManager, SessionScopesFunc, diff --git a/packages/core-components/src/layout/ProxiedSignInPage/types.test.ts b/packages/core-components/src/layout/ProxiedSignInPage/types.test.ts index d3088e5f1970db..abc9cabac6946e 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/types.test.ts +++ b/packages/core-components/src/layout/ProxiedSignInPage/types.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TypeOf } from 'zod/v3'; +import type { TypeOf } from 'zod/v3'; import { ProxiedSession, proxiedSessionSchema } from './types'; describe('types', () => { diff --git a/plugins/auth-node/report.api.md b/plugins/auth-node/report.api.md index 7cc68b44b3fcad..23565164a4f7d4 100644 --- a/plugins/auth-node/report.api.md +++ b/plugins/auth-node/report.api.md @@ -16,8 +16,8 @@ import { Profile } from 'passport'; import { Request as Request_2 } from 'express'; import { Response as Response_2 } from 'express'; import { Strategy } from 'passport'; -import { ZodSchema } from 'zod/v3'; -import { ZodTypeDef } from 'zod/v3'; +import type { ZodSchema } from 'zod/v3'; +import type { ZodTypeDef } from 'zod/v3'; // @public (undocumented) export interface AuthOwnershipResolutionExtensionPoint { diff --git a/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts b/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts index e74be6dbb8a8ae..a74a1d4bfdf427 100644 --- a/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts +++ b/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ZodSchema, ZodTypeDef } from 'zod/v3'; +import type { ZodSchema, ZodTypeDef } from 'zod/v3'; import { SignInResolver } from '../types'; import zodToJsonSchema from 'zod-to-json-schema'; import { JsonObject } from '@backstage/types'; From 44a8c0937af70f7d3230aa48ebdc901320809375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 17 Mar 2026 15:55:36 +0100 Subject: [PATCH 33/55] chore: regenerate yarn.lock after rebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- yarn.lock | 692 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 417 insertions(+), 275 deletions(-) diff --git a/yarn.lock b/yarn.lock index dc6c8b6727cb83..b773a8bfe368de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2524,6 +2524,7 @@ __metadata: "@backstage/integration-aws-node": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-events-node": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" "@backstage/types": "workspace:^" "@google-cloud/cloud-sql-connector": "npm:^1.4.0" @@ -2801,60 +2802,80 @@ __metadata: languageName: unknown linkType: soft -"@backstage/cli-node@workspace:^, @backstage/cli-node@workspace:packages/cli-node": +"@backstage/cli-defaults@workspace:*, @backstage/cli-defaults@workspace:^, @backstage/cli-defaults@workspace:packages/cli-defaults": version: 0.0.0-use.local - resolution: "@backstage/cli-node@workspace:packages/cli-node" + resolution: "@backstage/cli-defaults@workspace:packages/cli-defaults" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-module-actions": "workspace:^" + "@backstage/cli-module-auth": "workspace:^" + "@backstage/cli-module-build": "workspace:^" + "@backstage/cli-module-config": "workspace:^" + "@backstage/cli-module-github": "workspace:^" + "@backstage/cli-module-info": "workspace:^" + "@backstage/cli-module-lint": "workspace:^" + "@backstage/cli-module-maintenance": "workspace:^" + "@backstage/cli-module-migrate": "workspace:^" + "@backstage/cli-module-new": "workspace:^" + "@backstage/cli-module-test-jest": "workspace:^" + "@backstage/cli-module-translations": "workspace:^" + languageName: unknown + linkType: soft + +"@backstage/cli-module-actions@workspace:^, @backstage/cli-module-actions@workspace:packages/cli-module-actions": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-actions@workspace:packages/cli-module-actions" dependencies: "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" - "@backstage/cli-common": "workspace:^" + "@backstage/cli-module-auth": "workspace:^" + "@backstage/cli-node": "workspace:^" + cleye: "npm:^2.3.0" + bin: + cli-module-actions: bin/backstage-cli-module-actions + languageName: unknown + linkType: soft + +"@backstage/cli-module-auth@workspace:^, @backstage/cli-module-auth@workspace:packages/cli-module-auth": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-auth@workspace:packages/cli-module-auth" + dependencies: + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-node": "workspace:^" "@backstage/errors": "workspace:^" - "@backstage/test-utils": "workspace:^" - "@backstage/types": "workspace:^" - "@manypkg/get-packages": "npm:^1.1.3" - "@types/yarnpkg__lockfile": "npm:^1.1.4" - "@yarnpkg/lockfile": "npm:^1.1.0" - "@yarnpkg/parsers": "npm:^3.0.0" + "@types/fs-extra": "npm:^11.0.0" + "@types/proper-lockfile": "npm:^4" + cleye: "npm:^2.3.0" fs-extra: "npm:^11.2.0" - semver: "npm:^7.5.3" + glob: "npm:^7.1.7" + inquirer: "npm:^8.2.0" + keytar: "npm:^7.9.0" + proper-lockfile: "npm:^4.1.2" yaml: "npm:^2.0.0" - zod: "npm:^3.25.76 || ^4.0.0" + zod: "npm:^3.25.76" + dependenciesMeta: + keytar: + optional: true + bin: + cli-module-auth: bin/backstage-cli-module-auth languageName: unknown linkType: soft -"@backstage/cli@workspace:*, @backstage/cli@workspace:^, @backstage/cli@workspace:packages/cli": +"@backstage/cli-module-build@workspace:^, @backstage/cli-module-build@workspace:packages/cli-module-build": version: 0.0.0-use.local - resolution: "@backstage/cli@workspace:packages/cli" + resolution: "@backstage/cli-module-build@workspace:packages/cli-module-build" dependencies: - "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" - "@backstage/catalog-client": "workspace:^" - "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" "@backstage/cli-node": "workspace:^" "@backstage/config": "workspace:^" "@backstage/config-loader": "workspace:^" - "@backstage/core-app-api": "workspace:^" - "@backstage/core-components": "workspace:^" - "@backstage/core-plugin-api": "workspace:^" - "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" - "@backstage/eslint-plugin": "workspace:^" - "@backstage/integration": "workspace:^" "@backstage/module-federation-common": "workspace:^" - "@backstage/plugin-auth-backend": "workspace:^" - "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" - "@backstage/plugin-catalog-node": "workspace:^" - "@backstage/plugin-scaffolder-node": "workspace:^" - "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" - "@backstage/release-manifests": "workspace:^" - "@backstage/test-utils": "workspace:^" - "@backstage/theme": "workspace:^" - "@backstage/types": "workspace:^" - "@jest/environment-jsdom-abstract": "npm:^30.0.0" "@manypkg/get-packages": "npm:^1.1.3" "@module-federation/enhanced": "npm:^0.21.6" - "@octokit/request": "npm:^8.0.0" "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.6.0" "@rollup/plugin-commonjs": "npm:^26.0.0" "@rollup/plugin-json": "npm:^6.0.0" @@ -2863,156 +2884,365 @@ __metadata: "@rspack/core": "npm:^1.4.11" "@rspack/dev-server": "npm:^1.1.4" "@rspack/plugin-react-refresh": "npm:^1.4.3" - "@spotify/eslint-config-base": "npm:^15.0.0" - "@spotify/eslint-config-react": "npm:^15.0.0" - "@spotify/eslint-config-typescript": "npm:^15.0.0" "@swc/core": "npm:^1.15.6" - "@swc/helpers": "npm:^0.5.17" - "@swc/jest": "npm:^0.2.39" - "@types/cross-spawn": "npm:^6.0.2" - "@types/ejs": "npm:^3.1.3" - "@types/express": "npm:^4.17.6" "@types/fs-extra": "npm:^11.0.0" - "@types/http-proxy": "npm:^1.17.4" - "@types/inquirer": "npm:^8.1.3" - "@types/jest": "npm:^30.0.0" - "@types/node": "npm:^22.13.14" + "@types/lodash": "npm:^4.14.151" "@types/npm-packlist": "npm:^3.0.0" - "@types/proper-lockfile": "npm:^4" - "@types/recursive-readdir": "npm:^2.2.0" - "@types/rollup-plugin-peer-deps-external": "npm:^2.2.0" - "@types/rollup-plugin-postcss": "npm:^3.1.4" "@types/shell-quote": "npm:^1.7.5" - "@types/svgo": "npm:^2.6.2" - "@types/terser-webpack-plugin": "npm:^5.0.4" - "@types/webpack-env": "npm:^1.15.2" - "@types/webpack-sources": "npm:^3.2.3" - "@typescript-eslint/eslint-plugin": "npm:^8.17.0" - "@typescript-eslint/parser": "npm:^8.16.0" bfj: "npm:^9.0.2" buffer: "npm:^6.0.3" chalk: "npm:^4.0.0" chokidar: "npm:^3.3.1" cleye: "npm:^2.3.0" - commander: "npm:^14.0.3" - cross-fetch: "npm:^4.0.0" cross-spawn: "npm:^7.0.3" css-loader: "npm:^6.5.1" ctrlc-windows: "npm:^2.1.0" - del: "npm:^8.0.0" - esbuild: "npm:^0.27.0" esbuild-loader: "npm:^4.0.0" - eslint: "npm:^8.6.0" - eslint-config-prettier: "npm:^9.0.0" - eslint-formatter-friendly: "npm:^7.0.0" - eslint-plugin-deprecation: "npm:^3.0.0" - eslint-plugin-import: "npm:^2.31.0" - eslint-plugin-jest: "npm:^28.9.0" - eslint-plugin-jsx-a11y: "npm:^6.10.2" - eslint-plugin-react: "npm:^7.37.2" - eslint-plugin-react-hooks: "npm:^5.0.0" - eslint-plugin-unused-imports: "npm:^4.1.4" eslint-rspack-plugin: "npm:^4.2.1" eslint-webpack-plugin: "npm:^4.2.0" - express: "npm:^4.22.0" fork-ts-checker-webpack-plugin: "npm:^9.0.0" fs-extra: "npm:^11.2.0" - git-url-parse: "npm:^15.0.0" glob: "npm:^7.1.7" - global-agent: "npm:^3.0.0" - globby: "npm:^11.1.0" - handlebars: "npm:^4.7.3" html-webpack-plugin: "npm:^5.6.3" - inquirer: "npm:^8.2.0" - jest: "npm:^30.2.0" - jest-css-modules: "npm:^2.1.0" - jsdom: "npm:^27.1.0" - json-schema: "npm:^0.4.0" - keytar: "npm:^7.9.0" lodash: "npm:^4.17.21" mini-css-extract-plugin: "npm:^2.4.2" - minimatch: "npm:^10.2.1" - msw: "npm:^1.0.0" node-stdlib-browser: "npm:^1.3.1" - nodemon: "npm:^3.0.1" npm-packlist: "npm:^5.0.0" - ora: "npm:^5.3.0" p-queue: "npm:^6.6.2" - pirates: "npm:^4.0.6" postcss: "npm:^8.1.0" postcss-import: "npm:^16.1.0" process: "npm:^0.11.10" - proper-lockfile: "npm:^4.1.2" raw-loader: "npm:^4.0.2" react-dev-utils: "npm:^12.0.0-next.60" react-refresh: "npm:^0.18.0" - recursive-readdir: "npm:^2.2.2" - replace-in-file: "npm:^7.1.0" rollup: "npm:^4.27.3" rollup-plugin-dts: "npm:^6.1.0" rollup-plugin-esbuild: "npm:^6.1.1" rollup-plugin-postcss: "npm:^4.0.0" rollup-pluginutils: "npm:^2.8.2" - semver: "npm:^7.5.3" shell-quote: "npm:^1.8.1" style-loader: "npm:^3.3.1" - sucrase: "npm:^3.20.2" swc-loader: "npm:^0.2.3" tar: "npm:^7.5.6" - terser-webpack-plugin: "npm:^5.1.3" ts-checker-rspack-plugin: "npm:^1.1.5" ts-morph: "npm:^24.0.0" - undici: "npm:^7.2.3" util: "npm:^0.12.3" webpack: "npm:~5.105.0" webpack-dev-server: "npm:^5.0.0" - yaml: "npm:^2.0.0" - yargs: "npm:^16.2.0" yml-loader: "npm:^2.1.0" yn: "npm:^4.0.0" - zod: "npm:^3.25.76 || ^4.0.0" + bin: + cli-module-build: bin/backstage-cli-module-build + languageName: unknown + linkType: soft + +"@backstage/cli-module-config@workspace:^, @backstage/cli-module-config@workspace:packages/cli-module-config": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-config@workspace:packages/cli-module-config" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/config-loader": "workspace:^" + "@backstage/types": "workspace:^" + "@manypkg/get-packages": "npm:^1.1.3" + "@types/json-schema": "npm:^7.0.6" + chalk: "npm:^4.0.0" + cleye: "npm:^2.3.0" + json-schema: "npm:^0.4.0" + react-dev-utils: "npm:^12.0.0-next.60" + yaml: "npm:^2.0.0" + bin: + cli-module-config: bin/backstage-cli-module-config + languageName: unknown + linkType: soft + +"@backstage/cli-module-github@workspace:^, @backstage/cli-module-github@workspace:packages/cli-module-github": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-github@workspace:packages/cli-module-github" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@octokit/request": "npm:^8.0.0" + "@types/express": "npm:^4.17.6" + "@types/fs-extra": "npm:^11.0.0" + chalk: "npm:^4.0.0" + cleye: "npm:^2.3.0" + express: "npm:^4.22.0" + fs-extra: "npm:^11.2.0" + inquirer: "npm:^8.2.0" + react-dev-utils: "npm:^12.0.0-next.60" + yaml: "npm:^2.0.0" + bin: + cli-module-github: bin/backstage-cli-module-github + languageName: unknown + linkType: soft + +"@backstage/cli-module-info@workspace:^, @backstage/cli-module-info@workspace:packages/cli-module-info": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-info@workspace:packages/cli-module-info" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@types/fs-extra": "npm:^11.0.0" + cleye: "npm:^2.3.0" + fs-extra: "npm:^11.2.0" + minimatch: "npm:^10.2.1" + bin: + cli-module-info: bin/backstage-cli-module-info + languageName: unknown + linkType: soft + +"@backstage/cli-module-lint@workspace:^, @backstage/cli-module-lint@workspace:packages/cli-module-lint": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-lint@workspace:packages/cli-module-lint" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@types/fs-extra": "npm:^11.0.0" + "@types/shell-quote": "npm:^1.7.5" + chalk: "npm:^4.0.0" + cleye: "npm:^2.3.0" + eslint: "npm:^8.6.0" + eslint-formatter-friendly: "npm:^7.0.0" + fs-extra: "npm:^11.2.0" + globby: "npm:^11.1.0" + shell-quote: "npm:^1.8.1" + bin: + cli-module-lint: bin/backstage-cli-module-lint + languageName: unknown + linkType: soft + +"@backstage/cli-module-maintenance@workspace:^, @backstage/cli-module-maintenance@workspace:packages/cli-module-maintenance": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-maintenance@workspace:packages/cli-module-maintenance" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@types/fs-extra": "npm:^11.0.0" + chalk: "npm:^4.0.0" + cleye: "npm:^2.3.0" + eslint: "npm:^8.6.0" + fs-extra: "npm:^11.2.0" + bin: + cli-module-maintenance: bin/backstage-cli-module-maintenance + languageName: unknown + linkType: soft + +"@backstage/cli-module-migrate@workspace:^, @backstage/cli-module-migrate@workspace:packages/cli-module-migrate": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-migrate@workspace:packages/cli-module-migrate" + dependencies: + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/release-manifests": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@manypkg/get-packages": "npm:^1.1.3" + "@types/fs-extra": "npm:^11.0.0" + "@types/semver": "npm:^7" + chalk: "npm:^4.0.0" + cleye: "npm:^2.3.0" + fs-extra: "npm:^11.2.0" + minimatch: "npm:^10.2.1" + msw: "npm:^1.0.0" + ora: "npm:^5.3.0" + replace-in-file: "npm:^7.1.0" + semver: "npm:^7.5.3" + bin: + cli-module-migrate: bin/backstage-cli-module-migrate + languageName: unknown + linkType: soft + +"@backstage/cli-module-new@workspace:^, @backstage/cli-module-new@workspace:packages/cli-module-new": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-new@workspace:packages/cli-module-new" + dependencies: + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@types/fs-extra": "npm:^11.0.0" + "@types/inquirer": "npm:^8.1.3" + "@types/lodash": "npm:^4.14.151" + "@types/recursive-readdir": "npm:^2.2.0" + chalk: "npm:^4.0.0" + cleye: "npm:^2.3.0" + fs-extra: "npm:^11.2.0" + handlebars: "npm:^4.7.3" + inquirer: "npm:^8.2.0" + lodash: "npm:^4.17.21" + ora: "npm:^5.3.0" + recursive-readdir: "npm:^2.2.2" + semver: "npm:^7.5.3" + yaml: "npm:^2.0.0" + zod: "npm:^3.25.76" zod-validation-error: "npm:^4.0.2" + bin: + cli-module-new: bin/backstage-cli-module-new + languageName: unknown + linkType: soft + +"@backstage/cli-module-test-jest@workspace:^, @backstage/cli-module-test-jest@workspace:packages/cli-module-test-jest": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-test-jest@workspace:packages/cli-module-test-jest" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@swc/core": "npm:^1.15.6" + "@swc/jest": "npm:^0.2.39" + cleye: "npm:^2.3.0" + cross-fetch: "npm:^4.0.0" + fs-extra: "npm:^11.2.0" + glob: "npm:^7.1.7" + jest-css-modules: "npm:^2.1.0" + sucrase: "npm:^3.20.2" + yargs: "npm:^16.2.0" peerDependencies: "@jest/environment-jsdom-abstract": ^30.0.0 - "@module-federation/enhanced": ^0.21.6 - "@pmmmwh/react-refresh-webpack-plugin": ^0.6.0 - esbuild-loader: ^4.0.0 - eslint-webpack-plugin: ^4.2.0 - fork-ts-checker-webpack-plugin: ^9.0.0 jest: ^29.0.0 || ^30.0.0 jest-environment-jsdom: "*" jsdom: ^27.1.0 - mini-css-extract-plugin: ^2.4.2 - terser-webpack-plugin: ^5.1.3 - webpack: ~5.105.0 - webpack-dev-server: ^5.0.0 - dependenciesMeta: - keytar: - optional: true peerDependenciesMeta: "@jest/environment-jsdom-abstract": optional: true - "@module-federation/enhanced": - optional: true - "@pmmmwh/react-refresh-webpack-plugin": - optional: true - esbuild-loader: - optional: true - eslint-webpack-plugin: - optional: true - fork-ts-checker-webpack-plugin: - optional: true jest-environment-jsdom: optional: true jsdom: optional: true - mini-css-extract-plugin: + bin: + cli-module-test-jest: bin/backstage-cli-module-test-jest + languageName: unknown + linkType: soft + +"@backstage/cli-module-translations@workspace:^, @backstage/cli-module-translations@workspace:packages/cli-module-translations": + version: 0.0.0-use.local + resolution: "@backstage/cli-module-translations@workspace:packages/cli-module-translations" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@types/fs-extra": "npm:^11.0.0" + cleye: "npm:^2.3.0" + fs-extra: "npm:^11.2.0" + ts-morph: "npm:^24.0.0" + bin: + cli-module-translations: bin/backstage-cli-module-translations + languageName: unknown + linkType: soft + +"@backstage/cli-node@workspace:^, @backstage/cli-node@workspace:packages/cli-node": + version: 0.0.0-use.local + resolution: "@backstage/cli-node@workspace:packages/cli-node" + dependencies: + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/types": "workspace:^" + "@manypkg/get-packages": "npm:^1.1.3" + "@types/yarnpkg__lockfile": "npm:^1.1.4" + "@yarnpkg/lockfile": "npm:^1.1.0" + "@yarnpkg/parsers": "npm:^3.0.0" + chalk: "npm:^4.0.0" + commander: "npm:^12.0.0" + fs-extra: "npm:^11.2.0" + pirates: "npm:^4.0.6" + semver: "npm:^7.5.3" + yaml: "npm:^2.0.0" + zod: "npm:^3.25.76 || ^4.0.0" + peerDependencies: + "@swc/core": ^1.15.6 + peerDependenciesMeta: + "@swc/core": optional: true - terser-webpack-plugin: + languageName: unknown + linkType: soft + +"@backstage/cli@workspace:*, @backstage/cli@workspace:^, @backstage/cli@workspace:packages/cli": + version: 0.0.0-use.local + resolution: "@backstage/cli@workspace:packages/cli" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-client": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/cli-defaults": "workspace:^" + "@backstage/cli-module-build": "workspace:^" + "@backstage/cli-module-test-jest": "workspace:^" + "@backstage/cli-node": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/eslint-plugin": "workspace:^" + "@backstage/plugin-auth-backend": "workspace:^" + "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@jest/environment-jsdom-abstract": "npm:^30.0.0" + "@manypkg/get-packages": "npm:^1.1.3" + "@spotify/eslint-config-base": "npm:^15.0.0" + "@spotify/eslint-config-react": "npm:^15.0.0" + "@spotify/eslint-config-typescript": "npm:^15.0.0" + "@swc/core": "npm:^1.15.6" + "@swc/jest": "npm:^0.2.39" + "@types/fs-extra": "npm:^11.0.0" + "@types/jest": "npm:^30.0.0" + "@types/node": "npm:^22.13.14" + "@types/webpack-env": "npm:^1.15.2" + "@typescript-eslint/eslint-plugin": "npm:^8.17.0" + "@typescript-eslint/parser": "npm:^8.16.0" + chalk: "npm:^4.0.0" + commander: "npm:^14.0.3" + cross-fetch: "npm:^4.0.0" + eslint: "npm:^8.6.0" + eslint-config-prettier: "npm:^9.0.0" + eslint-plugin-deprecation: "npm:^3.0.0" + eslint-plugin-import: "npm:^2.31.0" + eslint-plugin-jest: "npm:^28.9.0" + eslint-plugin-jsx-a11y: "npm:^6.10.2" + eslint-plugin-react: "npm:^7.37.2" + eslint-plugin-react-hooks: "npm:^5.0.0" + eslint-plugin-unused-imports: "npm:^4.1.4" + fs-extra: "npm:^11.2.0" + glob: "npm:^7.1.7" + jest: "npm:^30.2.0" + jest-css-modules: "npm:^2.1.0" + jsdom: "npm:^27.1.0" + nodemon: "npm:^3.0.1" + pirates: "npm:^4.0.6" + postcss: "npm:^8.1.0" + sucrase: "npm:^3.20.2" + yaml: "npm:^2.0.0" + peerDependencies: + "@jest/environment-jsdom-abstract": ^30.0.0 + jest: ^29.0.0 || ^30.0.0 + jest-environment-jsdom: "*" + jsdom: ^27.1.0 + peerDependenciesMeta: + "@jest/environment-jsdom-abstract": optional: true - webpack: + jest-environment-jsdom: optional: true - webpack-dev-server: + jsdom: optional: true bin: backstage-cli: bin/backstage-cli @@ -3129,6 +3359,7 @@ __metadata: "@backstage/core-app-api": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" @@ -3398,10 +3629,12 @@ __metadata: "@backstage/core-app-api": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-defaults": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" "@backstage/plugin-app": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@backstage/version-bridge": "workspace:^" @@ -3437,6 +3670,8 @@ __metadata: "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-app": "workspace:^" "@backstage/plugin-app-react": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@react-hookz/web": "npm:^24.0.0" "@testing-library/jest-dom": "npm:^6.0.0" @@ -3456,6 +3691,33 @@ __metadata: languageName: unknown linkType: soft +"@backstage/frontend-dev-utils@workspace:^, @backstage/frontend-dev-utils@workspace:packages/frontend-dev-utils": + version: 0.0.0-use.local + resolution: "@backstage/frontend-dev-utils@workspace:packages/frontend-dev-utils" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/frontend-defaults": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/plugin-app": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/ui": "workspace:^" + "@testing-library/jest-dom": "npm:^6.0.0" + "@testing-library/react": "npm:^16.0.0" + "@types/react": "npm:^18.0.0" + react: "npm:^18.0.2" + react-dom: "npm:^18.0.2" + react-router-dom: "npm:^6.30.2" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + react-router-dom: ^6.30.2 + peerDependenciesMeta: + "@types/react": + optional: true + languageName: unknown + linkType: soft + "@backstage/frontend-dynamic-feature-loader@workspace:packages/frontend-dynamic-feature-loader": version: 0.0.0-use.local resolution: "@backstage/frontend-dynamic-feature-loader@workspace:packages/frontend-dynamic-feature-loader" @@ -3495,6 +3757,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" "@backstage/test-utils": "workspace:^" @@ -3528,6 +3791,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/core-app-api": "workspace:^" "@backstage/core-plugin-api": "workspace:^" + "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-app": "workspace:^" @@ -3811,7 +4075,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" - "@backstage/frontend-defaults": "workspace:^" + "@backstage/frontend-dev-utils": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/ui": "workspace:^" "@remixicon/react": "npm:^4.6.0" @@ -3839,6 +4103,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-defaults": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" @@ -4394,16 +4659,13 @@ __metadata: resolution: "@backstage/plugin-auth@workspace:plugins/auth" dependencies: "@backstage/cli": "workspace:^" - "@backstage/core-components": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/frontend-defaults": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/test-utils": "workspace:^" - "@backstage/theme": "workspace:^" - "@material-ui/core": "npm:^4.12.2" - "@material-ui/icons": "npm:^4.9.1" - "@material-ui/lab": "npm:4.0.0-alpha.61" + "@backstage/ui": "workspace:^" + "@remixicon/react": "npm:^4.6.0" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.0.0" @@ -6197,6 +6459,7 @@ __metadata: "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@types/react": "npm:^18.0.0" + dataloader: "npm:^2.0.0" react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" @@ -9673,11 +9936,21 @@ __metadata: languageName: node linkType: hard +"@internal/cli@workspace:packages/cli-internal": + version: 0.0.0-use.local + resolution: "@internal/cli@workspace:packages/cli-internal" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/cli-node": "workspace:^" + languageName: unknown + linkType: soft + "@internal/frontend@workspace:packages/frontend-internal": version: 0.0.0-use.local resolution: "@internal/frontend@workspace:packages/frontend-internal" dependencies: "@backstage/cli": "workspace:^" + "@backstage/filter-predicates": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" @@ -18039,13 +18312,6 @@ __metadata: languageName: node linkType: hard -"@sindresorhus/merge-streams@npm:^2.1.0": - version: 2.3.0 - resolution: "@sindresorhus/merge-streams@npm:2.3.0" - checksum: 10/798bcb53cd1ace9df84fcdd1ba86afdc9e0cd84f5758d26ae9b1eefd8e8887e5fc30051132b9e74daf01bb41fa5a2faf1369361f83d76a3b3d7ee938058fd71c - languageName: node - linkType: hard - "@sinonjs/commons@npm:^2.0.0": version: 2.0.0 resolution: "@sinonjs/commons@npm:2.0.0" @@ -20342,7 +20608,7 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:^0.5.0, @swc/helpers@npm:^0.5.17, @swc/helpers@npm:^0.5.8": +"@swc/helpers@npm:^0.5.0, @swc/helpers@npm:^0.5.8": version: 0.5.19 resolution: "@swc/helpers@npm:0.5.19" dependencies: @@ -21146,13 +21412,6 @@ __metadata: languageName: node linkType: hard -"@types/ejs@npm:^3.1.3": - version: 3.1.5 - resolution: "@types/ejs@npm:3.1.5" - checksum: 10/918898fd279108087722c1713e2ddb0c152ab839397946d164db8a18b5bbd732af9746373882a9bcf4843d35c6b191a8f569a7a4e51e90726d24501b39f40367 - languageName: node - linkType: hard - "@types/emscripten@npm:^1.39.6": version: 1.39.10 resolution: "@types/emscripten@npm:1.39.10" @@ -22225,24 +22484,6 @@ __metadata: languageName: node linkType: hard -"@types/rollup-plugin-peer-deps-external@npm:^2.2.0": - version: 2.2.6 - resolution: "@types/rollup-plugin-peer-deps-external@npm:2.2.6" - peerDependencies: - rollup: "*" - checksum: 10/9fcee30d60d9d8de0f4bbf7694c254a18798b0ff415563a60fb2ed8c6c46423c64e1a977e0344b8393c26221dbd6e74d9e872c2694b518fcf149ba85d94e7eac - languageName: node - linkType: hard - -"@types/rollup-plugin-postcss@npm:^3.1.4": - version: 3.1.4 - resolution: "@types/rollup-plugin-postcss@npm:3.1.4" - dependencies: - rollup-plugin-postcss: "npm:*" - checksum: 10/a94119c43db77ad10a96f34a4190b678e87fe63bdedfb2b989b64a9c0e15680c034d728daa4ade9cfd31d2fdc48827c79ff69ad2599d0b323c2c321896866cf5 - languageName: node - linkType: hard - "@types/sarif@npm:^2.1.4": version: 2.1.5 resolution: "@types/sarif@npm:2.1.5" @@ -22257,10 +22498,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.1.0": - version: 7.7.0 - resolution: "@types/semver@npm:7.7.0" - checksum: 10/ee4514c6c852b1c38f951239db02f9edeea39f5310fad9396a00b51efa2a2d96b3dfca1ae84c88181ea5b7157c57d32d7ef94edacee36fbf975546396b85ba5b +"@types/semver@npm:^7, @types/semver@npm:^7.1.0": + version: 7.7.1 + resolution: "@types/semver@npm:7.7.1" + checksum: 10/8f09e7e6ca3ded67d78ba7a8f7535c8d9cf8ced83c52e7f3ac3c281fe8c689c3fe475d199d94390dc04fc681d51f2358b430bb7b2e21c62de24f2bee2c719068 languageName: node linkType: hard @@ -22344,13 +22585,6 @@ __metadata: languageName: node linkType: hard -"@types/source-list-map@npm:*": - version: 0.1.6 - resolution: "@types/source-list-map@npm:0.1.6" - checksum: 10/9cd294c121f1562062de5d241fe4d10780b1131b01c57434845fe50968e9dcf67ede444591c2b1ad6d3f9b6bc646ac02cc8f51a3577c795f9c64cf4573dcc6b1 - languageName: node - linkType: hard - "@types/ssh2-streams@npm:*": version: 0.1.8 resolution: "@types/ssh2-streams@npm:0.1.8" @@ -22421,15 +22655,6 @@ __metadata: languageName: node linkType: hard -"@types/svgo@npm:^2.6.2": - version: 2.6.4 - resolution: "@types/svgo@npm:2.6.4" - dependencies: - "@types/node": "npm:*" - checksum: 10/9632b350949677fa68d6f13b4d45495a4af3108bb5f020a7257ae5a883e20b9efc0fada3bc3e012f215be312fabe5a28485fffaff5afd6da4daa8cb4fe5b04a2 - languageName: node - linkType: hard - "@types/swagger-ui-react@npm:^5.0.0": version: 5.18.0 resolution: "@types/swagger-ui-react@npm:5.18.0" @@ -22457,15 +22682,6 @@ __metadata: languageName: node linkType: hard -"@types/terser-webpack-plugin@npm:^5.0.4": - version: 5.2.0 - resolution: "@types/terser-webpack-plugin@npm:5.2.0" - dependencies: - terser-webpack-plugin: "npm:*" - checksum: 10/475b0f160c9f83641255f6516b69d7908b9e3e8e8ab653f3a257690249f9614f9386c0897eba6e4e35182ec0d83f57454d62fb94b38ae7c171891641350072ee - languageName: node - linkType: hard - "@types/through@npm:*": version: 0.0.30 resolution: "@types/through@npm:0.0.30" @@ -22548,17 +22764,6 @@ __metadata: languageName: node linkType: hard -"@types/webpack-sources@npm:^3.2.3": - version: 3.2.3 - resolution: "@types/webpack-sources@npm:3.2.3" - dependencies: - "@types/node": "npm:*" - "@types/source-list-map": "npm:*" - source-map: "npm:^0.7.3" - checksum: 10/7b557f242efaa10e4e3e18cc4171a0c98e22898570caefdd4f7b076fe8534b5abfac92c953c6604658dcb7218507f970230352511840fe9fdea31a9af3b9a906 - languageName: node - linkType: hard - "@types/webpack@npm:^5.28.0": version: 5.28.5 resolution: "@types/webpack@npm:5.28.5" @@ -27265,7 +27470,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^12.1.0": +"commander@npm:^12.0.0, commander@npm:^12.1.0": version: 12.1.0 resolution: "commander@npm:12.1.0" checksum: 10/cdaeb672d979816853a4eed7f1310a9319e8b976172485c2a6b437ed0db0a389a44cfb222bfbde772781efa9f215bdd1b936f80d6b249485b465c6cb906e1f93 @@ -28746,21 +28951,6 @@ __metadata: languageName: node linkType: hard -"del@npm:^8.0.0": - version: 8.0.1 - resolution: "del@npm:8.0.1" - dependencies: - globby: "npm:^14.0.2" - is-glob: "npm:^4.0.3" - is-path-cwd: "npm:^3.0.0" - is-path-inside: "npm:^4.0.0" - p-map: "npm:^7.0.2" - presentable-error: "npm:^0.0.1" - slash: "npm:^5.1.0" - checksum: 10/53ed4a379a68c90e7d6d3bcce09c49229e77de9a946d0a5fc25f45b16c950cb8665986b7d0d0423416c03bfd43e0f31e528c5a19c558fe47449be9d6fae7f846 - languageName: node - linkType: hard - "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -32709,20 +32899,6 @@ __metadata: languageName: node linkType: hard -"globby@npm:^14.0.2": - version: 14.0.2 - resolution: "globby@npm:14.0.2" - dependencies: - "@sindresorhus/merge-streams": "npm:^2.1.0" - fast-glob: "npm:^3.3.2" - ignore: "npm:^5.2.4" - path-type: "npm:^5.0.0" - slash: "npm:^5.1.0" - unicorn-magic: "npm:^0.1.0" - checksum: 10/67660da70fc1223f7170c1a62ba6c373385e9e39765d952b6518606dec15ed8c7958e9dae6ba5752a31dbc1e9126f146938b830ad680fe794141734ffc3fbb75 - languageName: node - linkType: hard - "globrex@npm:^0.1.2": version: 0.1.2 resolution: "globrex@npm:0.1.2" @@ -33984,7 +34160,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.1.4, ignore@npm:^5.2.0, ignore@npm:^5.2.4": +"ignore@npm:^5.1.4, ignore@npm:^5.2.0": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 @@ -34793,13 +34969,6 @@ __metadata: languageName: node linkType: hard -"is-path-cwd@npm:^3.0.0": - version: 3.0.0 - resolution: "is-path-cwd@npm:3.0.0" - checksum: 10/bc34d13b6a03dfca4a3ab6a8a5ba78ae4b24f4f1db4b2b031d2760c60d0913bd16a4b980dcb4e590adfc906649d5f5132684079a3972bd219da49deebb9adea8 - languageName: node - linkType: hard - "is-path-inside@npm:^3.0.2, is-path-inside@npm:^3.0.3": version: 3.0.3 resolution: "is-path-inside@npm:3.0.3" @@ -34807,13 +34976,6 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^4.0.0": - version: 4.0.0 - resolution: "is-path-inside@npm:4.0.0" - checksum: 10/8810fa11c58e6360b82c3e0d6cd7d9c7d0392d3ac9eb10f980b81f9839f40ac6d1d6d6f05d069db0d227759801228f0b072e1b6c343e4469b065ab5fe0b68fe5 - languageName: node - linkType: hard - "is-plain-obj@npm:^3.0.0": version: 3.0.0 resolution: "is-plain-obj@npm:3.0.0" @@ -41566,13 +41728,6 @@ __metadata: languageName: node linkType: hard -"path-type@npm:^5.0.0": - version: 5.0.0 - resolution: "path-type@npm:5.0.0" - checksum: 10/15ec24050e8932c2c98d085b72cfa0d6b4eeb4cbde151a0a05726d8afae85784fc5544f733d8dfc68536587d5143d29c0bd793623fad03d7e61cc00067291cd5 - languageName: node - linkType: hard - "pathe@npm:^2.0.3": version: 2.0.3 resolution: "pathe@npm:2.0.3" @@ -42598,13 +42753,6 @@ __metadata: languageName: node linkType: hard -"presentable-error@npm:^0.0.1": - version: 0.0.1 - resolution: "presentable-error@npm:0.0.1" - checksum: 10/013809ee7a47ced847a8d860e9b89a56cdd8c4f1ad04ad8da1e58fd60843f77f497d204146bb15aaa9793d3b94ad8626eed01256fc9eb5839a545af2000a5fa4 - languageName: node - linkType: hard - "prettier@npm:^2.2.1, prettier@npm:^2.7.1": version: 2.8.8 resolution: "prettier@npm:2.8.8" @@ -45129,7 +45277,7 @@ __metadata: languageName: node linkType: hard -"rollup-plugin-postcss@npm:*, rollup-plugin-postcss@npm:^4.0.0": +"rollup-plugin-postcss@npm:^4.0.0": version: 4.0.2 resolution: "rollup-plugin-postcss@npm:4.0.2" dependencies: @@ -45274,6 +45422,7 @@ __metadata: resolution: "root@workspace:." dependencies: "@backstage/cli": "workspace:*" + "@backstage/cli-defaults": "workspace:*" "@backstage/codemods": "workspace:*" "@backstage/create-app": "workspace:*" "@backstage/e2e-test-utils": "workspace:*" @@ -46229,13 +46378,6 @@ __metadata: languageName: node linkType: hard -"slash@npm:^5.1.0": - version: 5.1.0 - resolution: "slash@npm:5.1.0" - checksum: 10/2c41ec6fb1414cd9bba0fa6b1dd00e8be739e3fe85d079c69d4b09ca5f2f86eafd18d9ce611c0c0f686428638a36c272a6ac14799146a8295f259c10cc45cde4 - languageName: node - linkType: hard - "slice-ansi@npm:^3.0.0": version: 3.0.0 resolution: "slice-ansi@npm:3.0.0" @@ -47834,7 +47976,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:*, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.17": +"terser-webpack-plugin@npm:^5.3.17": version: 5.3.17 resolution: "terser-webpack-plugin@npm:5.3.17" dependencies: @@ -49100,13 +49242,6 @@ __metadata: languageName: node linkType: hard -"unicorn-magic@npm:^0.1.0": - version: 0.1.0 - resolution: "unicorn-magic@npm:0.1.0" - checksum: 10/9b4d0e9809807823dc91d0920a4a4c0cff2de3ebc54ee87ac1ee9bc75eafd609b09d1f14495e0173aef26e01118706196b6ab06a75fe0841028b3983a8af313f - languageName: node - linkType: hard - "unified@npm:^10.0.0": version: 10.1.0 resolution: "unified@npm:10.1.0" @@ -51032,6 +51167,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^3.25.76": + version: 3.25.76 + resolution: "zod@npm:3.25.76" + checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995 + languageName: node + linkType: hard + "zstd-codec@npm:^0.1.4, zstd-codec@npm:^0.1.5": version: 0.1.5 resolution: "zstd-codec@npm:0.1.5" From 4190ec7f0df33a0db0ec053685c96df7ec9382ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 17 Mar 2026 16:00:45 +0100 Subject: [PATCH 34/55] chore: refresh frontend-plugin-api alpha API report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- .../frontend-plugin-api/report-alpha.api.md | 2 +- yarn.lock | 73 ++++++++++++++++++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/packages/frontend-plugin-api/report-alpha.api.md b/packages/frontend-plugin-api/report-alpha.api.md index db4570ee2f7e80..c27ba1b814d241 100644 --- a/packages/frontend-plugin-api/report-alpha.api.md +++ b/packages/frontend-plugin-api/report-alpha.api.md @@ -14,7 +14,7 @@ import { FilterPredicate } from '@backstage/filter-predicates'; import { JsonObject } from '@backstage/types'; import { JSX as JSX_2 } from 'react'; import { ReactNode } from 'react'; -import type { z } from 'zod'; +import type { z } from 'zod/v3'; // @public export type AnyRouteRefParams = diff --git a/yarn.lock b/yarn.lock index b773a8bfe368de..ae36fcc9b60871 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3968,10 +3968,12 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" + "@backstage/ui": "workspace:^" "@graphiql/react": "npm:0.29.0" "@material-ui/core": "npm:^4.12.2" "@material-ui/icons": "npm:^4.9.1" "@material-ui/lab": "npm:4.0.0-alpha.61" + "@remixicon/react": "npm:^4.6.0" "@testing-library/dom": "npm:^10.0.0" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" @@ -4118,16 +4120,22 @@ __metadata: "@material-ui/core": "npm:^4.9.13" "@material-ui/icons": "npm:^4.9.1" "@material-ui/lab": "npm:^4.0.0-alpha.61" + "@react-aria/button": "npm:^3.14.3" + "@react-aria/toast": "npm:^3.0.9" "@react-hookz/web": "npm:^24.0.0" + "@react-stately/toast": "npm:^3.1.2" + "@remixicon/react": "npm:^4.6.0" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.0.0" "@types/react": "npm:^18.0.0" + motion: "npm:^12.0.0" msw: "npm:^1.0.0" react: "npm:^18.0.2" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.30.2" react-use: "npm:^17.2.4" + zen-observable: "npm:^0.10.0" zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 @@ -15702,7 +15710,7 @@ __metadata: languageName: node linkType: hard -"@react-aria/button@npm:^3.14.5": +"@react-aria/button@npm:^3.14.3, @react-aria/button@npm:^3.14.5": version: 3.14.5 resolution: "@react-aria/button@npm:3.14.5" dependencies: @@ -16451,7 +16459,7 @@ __metadata: languageName: node linkType: hard -"@react-aria/toast@npm:^3.0.11": +"@react-aria/toast@npm:^3.0.11, @react-aria/toast@npm:^3.0.9": version: 3.0.11 resolution: "@react-aria/toast@npm:3.0.11" dependencies: @@ -16976,7 +16984,7 @@ __metadata: languageName: node linkType: hard -"@react-stately/toast@npm:^3.1.3": +"@react-stately/toast@npm:^3.1.2, @react-stately/toast@npm:^3.1.3": version: 3.1.3 resolution: "@react-stately/toast@npm:3.1.3" dependencies: @@ -32109,6 +32117,28 @@ __metadata: languageName: node linkType: hard +"framer-motion@npm:^12.38.0": + version: 12.38.0 + resolution: "framer-motion@npm:12.38.0" + dependencies: + motion-dom: "npm:^12.38.0" + motion-utils: "npm:^12.36.0" + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 10/4d529d1648a8e31ec9859e7ff1296b7e4ef0028eb09cbc7d626068766ab53e486038b431fac33b1438a1cc076a244e6843c5a8c0f38442885832308452b4b25e + languageName: node + linkType: hard + "framer-motion@npm:^6.5.1": version: 6.5.1 resolution: "framer-motion@npm:6.5.1" @@ -39447,6 +39477,43 @@ __metadata: languageName: node linkType: hard +"motion-dom@npm:^12.38.0": + version: 12.38.0 + resolution: "motion-dom@npm:12.38.0" + dependencies: + motion-utils: "npm:^12.36.0" + checksum: 10/78c040b46d93273932cf80c70e39845be5a442dcaf18d4345b45a9193de9dfa87c885b609943cb652115e4eac5d46ef40b452185073dd43fc328b134f9975e90 + languageName: node + linkType: hard + +"motion-utils@npm:^12.36.0": + version: 12.36.0 + resolution: "motion-utils@npm:12.36.0" + checksum: 10/c4a2a7ffac48ca44082d6d31b115f245025060a7e69d70dac062646d8f96c39e5662a7c8a51f255566fdf8e719ef1269a8e9aa3a04fc263bb65b5a7b61331901 + languageName: node + linkType: hard + +"motion@npm:^12.0.0": + version: 12.38.0 + resolution: "motion@npm:12.38.0" + dependencies: + framer-motion: "npm:^12.38.0" + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 10/d7ae2ba3cc112c4467822956b92065239640b9c62204d3bee1780da9fc0147185373534138d39975e82bf73b5f1b28d3fb3581031e4e7e0cfb230472767bd10d + languageName: node + linkType: hard + "mri@npm:1.1.4": version: 1.1.4 resolution: "mri@npm:1.1.4" From 833f6b5ea4af7a02c271db12db46b5586300c45e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 17 Mar 2026 16:38:51 +0100 Subject: [PATCH 35/55] chore: fix changeset packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- .changeset/silver-snails-pull.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.changeset/silver-snails-pull.md b/.changeset/silver-snails-pull.md index b6bbacf39defc2..99d900db65dbe0 100644 --- a/.changeset/silver-snails-pull.md +++ b/.changeset/silver-snails-pull.md @@ -8,7 +8,8 @@ '@backstage/plugin-auth-backend-module-bitbucket-provider': patch '@backstage/plugin-auth-backend-module-microsoft-provider': patch '@backstage/plugin-auth-backend-module-openshift-provider': patch -'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/cli-module-auth': patch +'@backstage/cli-module-new': patch '@backstage/plugin-auth-backend-module-onelogin-provider': patch '@backstage/plugin-auth-backend-module-aws-alb-provider': patch '@backstage/plugin-auth-backend-module-gcp-iap-provider': patch @@ -23,7 +24,6 @@ '@backstage/plugin-user-settings-backend': patch '@backstage/frontend-plugin-api': patch '@backstage/frontend-test-utils': patch -'@internal/scaffolder': patch '@backstage/backend-plugin-api': patch '@backstage/backend-test-utils': patch '@backstage/plugin-mcp-actions-backend': patch @@ -49,8 +49,6 @@ '@backstage/plugin-scaffolder': patch '@backstage/cli-node': patch '@backstage/plugin-auth-node': patch -'@backstage/plugin-api-docs': patch -'@backstage/cli': patch '@backstage/plugin-home': patch '@backstage/plugin-app': patch --- From 68aca28f0e64429a82e27daa6c916ce6f8eb208e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 17 Mar 2026 16:45:17 +0100 Subject: [PATCH 36/55] update the import anyway MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/cli-node/src/yarn/yarnPlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli-node/src/yarn/yarnPlugin.ts b/packages/cli-node/src/yarn/yarnPlugin.ts index 78f6c724be3625..fb592e6898e3c2 100644 --- a/packages/cli-node/src/yarn/yarnPlugin.ts +++ b/packages/cli-node/src/yarn/yarnPlugin.ts @@ -17,7 +17,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import yaml from 'yaml'; -import z from 'zod/v3'; +import { z } from 'zod/v3'; import { targetPaths } from '@backstage/cli-common'; const yarnRcSchema = z.object({ From 4919273bea620643ed4d14ff66f5aae2fe7bae90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 17 Mar 2026 16:52:00 +0100 Subject: [PATCH 37/55] chore: align auth-backend zod version range with rest of repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- plugins/auth-backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index c4fdee3338ba48..6f7cce3511168f 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -66,7 +66,7 @@ "minimatch": "^10.2.1", "passport": "^0.7.0", "uuid": "^11.0.0", - "zod": "^4.3.5", + "zod": "^3.25.76 || ^4.0.0", "zod-validation-error": "^5.0.0" }, "devDependencies": { From 31aa3ce7b7a493f9a2cca7e242a9b8a82609fcb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 17 Mar 2026 17:00:28 +0100 Subject: [PATCH 38/55] dedupe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index ae36fcc9b60871..4063ff72e86d5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4597,7 +4597,7 @@ __metadata: passport: "npm:^0.7.0" supertest: "npm:^7.0.0" uuid: "npm:^11.0.0" - zod: "npm:^4.3.5" + zod: "npm:^3.25.76 || ^4.0.0" zod-validation-error: "npm:^5.0.0" languageName: unknown linkType: soft @@ -51227,7 +51227,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.25 || ^4.0, zod@npm:^3.25.76 || ^4.0.0, zod@npm:^4.1.11, zod@npm:^4.3.5": +"zod@npm:^3.25 || ^4.0, zod@npm:^3.25.76 || ^4.0.0, zod@npm:^4.1.11": version: 4.3.6 resolution: "zod@npm:4.3.6" checksum: 10/25fc0f62e01b557b4644bf0b393bbaf47542ab30877c37837ea8caf314a8713d220c7d7fe51f68ffa72f0e1018ddfa34d96f1973d23033f5a2a5a9b6b9d9da01 From 11e380b83a1ff395cccdefd9499342191b6cf8b9 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Tue, 17 Mar 2026 11:15:41 -0500 Subject: [PATCH 39/55] Invert based on feedback Signed-off-by: Andre Wanlin --- docs/auth/{index--new.md => index--old.md} | 345 +++++++++--------- docs/auth/index.md | 343 +++++++++-------- .../{addons--new.md => addons--old.md} | 113 ++++-- docs/features/techdocs/addons.md | 111 ++---- ...ication--new.md => authentication--old.md} | 76 ++-- docs/getting-started/config/authentication.md | 74 ++-- 6 files changed, 531 insertions(+), 531 deletions(-) rename docs/auth/{index--new.md => index--old.md} (52%) rename docs/features/techdocs/{addons--new.md => addons--old.md} (56%) rename docs/getting-started/config/{authentication--new.md => authentication--old.md} (82%) diff --git a/docs/auth/index--new.md b/docs/auth/index--old.md similarity index 52% rename from docs/auth/index--new.md rename to docs/auth/index--old.md index b1948f3a7ef8a8..6ba50f7eeabbe0 100644 --- a/docs/auth/index--new.md +++ b/docs/auth/index--old.md @@ -1,18 +1,22 @@ --- -id: index--new +id: index--old title: Authentication in Backstage description: Introduction to authentication in Backstage --- :::info -This documentation is written for [the new frontend system](../frontend-system/index.md). If you are on the old frontend system you may want to read [its own article](./index.md) instead. +This documentation is written for the old frontend system. If you are on the [new frontend system](../frontend-system/index.md) you may want to read [its own article](./index.md) instead. ::: -The authentication system in Backstage serves two distinct purposes: sign-in and identification of users, as well as delegating access to third-party resources. It is possible to configure Backstage to have any number of authentication providers, but only one of these will typically be used for sign-in, with the rest being used to provide access to external resources. +The authentication system in Backstage serves two distinct purposes: sign-in and +identification of users, as well as delegating access to third-party resources. It is possible to +configure Backstage to have any number of authentication providers, but only +one of these will typically be used for sign-in, with the rest being used to provide +access to external resources. :::note Note -Identity management and the Sign-In page in Backstage will only block external access when using the new backend system, without setting `backend.auth.dangerouslyDisableDefaultAuthPolicy` in configuration. Even so, the frontend bundle is not protected from external access, protecting it requires the use of the [experimental public entry point](https://backstage.io/docs/tutorials/enable-public-entry/). You can learn more about this in the [Threat Model](../overview/threat-model.md#operator-responsibilities). +Identity management and the Sign-In page in Backstage will block external access by default, without setting `backend.auth.dangerouslyDisableDefaultAuthPolicy` in configuration. Even so, the frontend bundle is not protected from external access, protecting it requires the use of the [experimental public entry point](https://backstage.io/docs/tutorials/enable-public-entry/). You can learn more about this in the [Threat Model](../overview/threat-model.md#operator-responsibilities). ::: @@ -37,11 +41,13 @@ Backstage comes with many common authentication providers in the core library: - [OpenShift](openshift/provider.md) - [VMware Cloud](vmware-cloud/provider.md) -These built-in providers handle the authentication flow for a particular service, including required scopes, callbacks, etc. These providers are each added to a Backstage app in a similar way. +These built-in providers handle the authentication flow for a particular service, including required scopes, callbacks, etc. These providers are each added to a +Backstage app in a similar way. ## Configuring Authentication Providers -Each built-in provider has a configuration block under the `auth` section of `app-config.yaml`. For example, the GitHub provider: +Each built-in provider has a configuration block under the `auth` section of +`app-config.yaml`. For example, the GitHub provider: ```yaml auth: @@ -53,66 +59,66 @@ auth: clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} ``` -See the documentation for a particular provider to see what configuration is needed. +See the documentation for a particular provider to see what configuration is +needed. -The `providers` key may have several authentication providers if multiple authentication methods are supported. Each provider may also have configuration for different authentication environments (development, production, etc). This allows a single auth backend to serve multiple environments, such as running a local frontend against a deployed backend. The provider configuration matching the local `auth.environment` setting will be selected. +The `providers` key may have several authentication providers if multiple +authentication methods are supported. Each provider may also have configuration +for different authentication environments (development, production, etc). This +allows a single auth backend to serve multiple environments, such as running a +local frontend against a deployed backend. The provider configuration matching +the local `auth.environment` setting will be selected. ## Sign-In Configuration -Using an authentication provider for sign-in is something you need to configure both in the frontend app as well as the `auth` backend plugin. For information on how to configure the backend app, see [Sign-in Identities and Resolvers](./identity-resolver.md). The rest of this section will focus on how to configure sign-in for the frontend app. +Using an authentication provider for sign-in is something you need to configure +both in the frontend app as well as the `auth` backend plugin. For information +on how to configure the backend app, see [Sign-in Identities and Resolvers](./identity-resolver.md). +The rest of this section will focus on how to configure sign-in for the frontend app. -Sign-in is configured by providing a custom `SignInPage` app component. It will be rendered before any other routes in the app and is responsible for providing the identity of the current user. The `SignInPage` can render any number of pages and components, or just blank space with logic running in the background. In the end, however, it must provide a valid Backstage user identity through the `onSignInSuccess` callback prop, at which point the rest of the app is rendered. +Sign-in is configured by providing a custom `SignInPage` app component. It will be +rendered before any other routes in the app and is responsible for providing the +identity of the current user. The `SignInPage` can render any number of pages and +components, or just blank space with logic running in the background. In the end, however, it must provide a valid Backstage user identity through the `onSignInSuccess` +callback prop, at which point the rest of the app is rendered. -If you want to, you can use the `SignInPage` component that is provided by `@backstage/core-components`, which takes either a `provider` or `providers` (array) prop of `SignInProviderConfig` definitions. +If you want to, you can use the `SignInPage` component that is provided by `@backstage/core-components`, +which takes either a `provider` or `providers` (array) prop of `SignInProviderConfig` definitions. -The following example for GitHub shows the additions needed to `packages/app/src/App.tsx`, and can be adapted to any of the built-in providers: +The following example for GitHub shows the additions needed to `packages/app/src/App.tsx`, +and can be adapted to any of the built-in providers: ```tsx title="packages/app/src/App.tsx" -import { createApp } from '@backstage/frontend-defaults'; -import catalogPlugin from '@backstage/plugin-catalog/alpha'; -import { navModule } from './modules/nav'; - /* highlight-add-start */ import { githubAuthApiRef } from '@backstage/core-plugin-api'; -import { SignInPageBlueprint } from '@backstage/plugin-app-react'; import { SignInPage } from '@backstage/core-components'; -import { createFrontendModule } from '@backstage/frontend-plugin-api'; - -const signInPage = SignInPageBlueprint.make({ - params: { - loader: async () => props => - ( - - ), - }, -}); /* highlight-add-end */ -export default createApp({ - features: [ - catalogPlugin, - navModule, - /* highlight-add-start */ - createFrontendModule({ - pluginId: 'app', - extensions: [signInPage], - }), - /* highlight-add-end */ - ], +const app = createApp({ + /* highlight-add-start */ + components: { + SignInPage: props => ( + + ), + }, + /* highlight-add-end */ + // .. }); ``` :::note Note -You can configure sign-in to use a redirect flow with no pop-up by adding `enableExperimentalRedirectFlow: true` to the root of your `app-config.yaml` +You can configure sign-in to use a redirect flow with no pop-up by adding +`enableExperimentalRedirectFlow: true` to the root of your `app-config.yaml` ::: @@ -121,44 +127,26 @@ You can configure sign-in to use a redirect flow with no pop-up by adding `enabl You can also use the `providers` prop to enable multiple sign-in methods, for example to allow guest access: ```tsx title="packages/app/src/App.tsx" -import { githubAuthApiRef } from '@backstage/core-plugin-api'; -import { SignInPageBlueprint } from '@backstage/plugin-app-react'; -import { SignInPage } from '@backstage/core-components'; -import { createFrontendModule } from '@backstage/frontend-plugin-api'; - -const signInPage = SignInPageBlueprint.make({ - params: { - loader: async () => props => - ( - - ), +const app = createApp({ + /* highlight-add-start */ + components: { + SignInPage: props => ( + + ), }, -}); - -export default createApp({ - features: [ - catalogPlugin, - navModule, - /* highlight-add-start */ - createFrontendModule({ - pluginId: 'app', - extensions: [signInPage], - }), - /* highlight-add-end */ - ], + /* highlight-add-end */ + // .. }); ``` @@ -172,14 +160,10 @@ import { githubAuthApiRef, useApi, } from '@backstage/core-plugin-api'; -import { SignInPageBlueprint } from '@backstage/plugin-app-react'; -import { SignInPage } from '@backstage/core-components'; -import { createFrontendModule } from '@backstage/frontend-plugin-api'; -const signInPage = SignInPageBlueprint.make({ - params: { - /* highlight-add-start */ - loader: async () => props => { +const app = createApp({ + components: { + SignInPage: props => { const configApi = useApi(configApiRef); if (configApi.getString('auth.environment') === 'development') { return ( @@ -197,64 +181,47 @@ const signInPage = SignInPageBlueprint.make({ /> ); } - return ( ); }, - /* highlight-add-end */ }, -}); - -export default createApp({ - features: [ - catalogPlugin, - navModule, - /* highlight-add-start */ - createFrontendModule({ - pluginId: 'app', - extensions: [signInPage], - }), - /* highlight-add-end */ - ], + // .. }); ``` ## Sign-In with Proxy Providers -Some auth providers are so-called "proxy" providers, meaning they're meant to be used behind an authentication proxy. Examples of these are [Amazon Application Load Balancer](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md), [Azure EasyAuth](./microsoft/azure-easyauth.md), [Cloudflare Access](./cloudflare/provider.md), [Google Identity-Aware Proxy](./google/gcp-iap-auth.md) and [OAuth2 Proxy](./oauth2-proxy/provider.md). +Some auth providers are so-called "proxy" providers, meaning they're meant to be used +behind an authentication proxy. Examples of these are +[Amazon Application Load Balancer](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md), +[Azure EasyAuth](./microsoft/azure-easyauth.md), +[Cloudflare Access](./cloudflare/provider.md), +[Google Identity-Aware Proxy](./google/gcp-iap-auth.md) +and [OAuth2 Proxy](./oauth2-proxy/provider.md). -When using a proxy provider, you'll end up wanting to use a different sign-in page, as there is no need for further user interaction once you've signed in towards the proxy. All the sign-in page needs to do is call the `/refresh` endpoint of the auth providers to get the existing session, which is exactly what the `ProxiedSignInPage` does. The only thing you need to do to configure the `ProxiedSignInPage` is to pass the ID of the provider like this: +When using a proxy provider, you'll end up wanting to use a different sign-in page, as +there is no need for further user interaction once you've signed in towards the proxy. +All the sign-in page needs to do is call the `/refresh` endpoint of the auth providers +to get the existing session, which is exactly what the `ProxiedSignInPage` does. The only +thing you need to do to configure the `ProxiedSignInPage` is to pass the ID of the provider like this: ```tsx title="packages/app/src/App.tsx" -import { SignInPageBlueprint } from '@backstage/plugin-app-react'; -import { createFrontendModule } from '@backstage/frontend-plugin-api'; import { ProxiedSignInPage } from '@backstage/core-components'; -const signInPage = SignInPageBlueprint.make({ - params: { - loader: async () => props => - , +const app = createApp({ + components: { + SignInPage: props => , }, -}); - -export default createApp({ - features: [ - catalogPlugin, - navModule, - createFrontendModule({ - pluginId: 'app', - extensions: [signInPage], - }), - ], + // .. }); ``` @@ -286,20 +253,18 @@ Headers can also be returned in an async manner: /> ``` -A downside of this method is that it can be cumbersome to set up for local development. As a workaround for this, it's possible to dynamically select the sign-in page based on what environment the app is running in and then use a different sign-in method for local development, if one is needed at all. Depending on the exact setup, one might choose to select the sign-in method based on the `process.env.NODE_ENV` environment variable, by checking the `hostname` of the current location, or by accessing the configuration API to read a configuration value. For example: +A downside of this method is that it can be cumbersome to set up for local development. +As a workaround for this, it's possible to dynamically select the sign-in page based on +what environment the app is running in and then use a different sign-in method for local +development, if one is needed at all. Depending on the exact setup, one might choose to +select the sign-in method based on the `process.env.NODE_ENV` environment variable, +by checking the `hostname` of the current location, or by accessing the configuration API +to read a configuration value. For example: ```tsx title="packages/app/src/App.tsx" -import { configApiRef, useApi } from '@backstage/core-plugin-api'; -import { SignInPageBlueprint } from '@backstage/plugin-app-react'; -import { ProxiedSignInPage, SignInPage } from '@backstage/core-components'; -import { - createFrontendModule, - googleAuthApiRef, -} from '@backstage/frontend-plugin-api'; - -const signInPage = SignInPageBlueprint.make({ - params: { - loader: async () => props => { +const app = createApp({ + components: { + SignInPage: props => { const configApi = useApi(configApiRef); if (configApi.getString('auth.environment') === 'development') { return ( @@ -314,55 +279,93 @@ const signInPage = SignInPageBlueprint.make({ /> ); } - return ; }, }, + // .. }); +``` -export default createApp({ - features: [ - catalogPlugin, - navModule, - createFrontendModule({ - pluginId: 'app', - extensions: [signInPage], - }), - ], +When using multiple auth providers like this, it's important that you configure the different +sign-in resolvers so that they resolve to the same identity regardless of the method used. + +## Scaffolder Configuration (Software Templates) + +If you want to use the authentication capabilities of the [Repository Picker](../features/software-templates/writing-templates.md#the-repository-picker) inside your software templates, you will need to configure the [`ScmAuthApi`](https://backstage.io/api/stable/interfaces/_backstage_integration-react.ScmAuthApi.html) alongside your authentication provider. It is an API used to authenticate towards different SCM systems in a generic way, based on what resource is being accessed. + +To set it up, you'll need to add an API factory entry to `packages/app/src/apis.ts`. The example below sets up the `ScmAuthApi` for an already configured GitLab authentication provider: + +```ts title="packages/app/src/apis.ts" +createApiFactory({ + api: scmAuthApiRef, + deps: { + gitlabAuthApi: gitlabAuthApiRef, + }, + factory: ({ gitlabAuthApi }) => ScmAuth.forGitlab(gitlabAuthApi), }); ``` -When using multiple auth providers like this, it's important that you configure the different sign-in resolvers so that they resolve to the same identity regardless of the method used. +In case you are using a custom authentication providers, you might need to add a [custom `ScmAuthApi` implementation](./index.md#custom-scmauthapi-implementation). ## For Plugin Developers -The Backstage frontend core APIs provide a set of Utility APIs for plugin developers to use, both to access the user identity as well as third-party resources. +The Backstage frontend core APIs provide a set of Utility APIs for plugin developers +to use, both to access the user identity as well as third-party resources. ### Identity for Plugin Developers -For plugin developers, there is one main touchpoint for accessing the user identity: the `IdentityApi` exported by `@backstage/core-plugin-api` via the `identityApiRef`. +For plugin developers, there is one main touchpoint for accessing the user identity: the +`IdentityApi` exported by `@backstage/core-plugin-api` via the `identityApiRef`. -The `IdentityApi` gives access to the signed-in user's identity in the frontend. It provides access to the user's entity reference, lightweight profile information, and a Backstage token that identifies the user when making authenticated calls within Backstage. +The `IdentityApi` gives access to the signed-in user's identity in the frontend. +It provides access to the user's entity reference, lightweight profile information, and +a Backstage token that identifies the user when making authenticated calls within Backstage. -When making calls to backend plugins, we recommend that the `FetchApi` is used, which is exported via the `fetchApiRef` from `@backstage/core-plugin-api`. The `FetchApi` will automatically include a Backstage token in the request, meaning there is no need to interact directly with the `IdentityApi`. +When making calls to backend plugins, we recommend that the `FetchApi` is used, which +is exported via the `fetchApiRef` from `@backstage/core-plugin-api`. The `FetchApi` will +automatically include a Backstage token in the request, meaning there is no need +to interact directly with the `IdentityApi`. ### Accessing Third Party Resources -A common pattern for talking to third-party services in Backstage is user-to-server requests, where short-lived OAuth Access Tokens are requested by plugins to authenticate calls to external services. These calls can be made either directly to the services or through a backend plugin or service. - -By relying on user-to-server calls, we keep the coupling between the frontend and backend low and provide a much lower barrier for plugins to make use of third party services. This is in comparison to, for example, a session-based system where access tokens are stored server-side. Such a solution would require a much deeper coupling between the auth backend plugin, its session storage, and other backend plugins or separate services. A goal of Backstage is to make it as easy as possible to create new plugins, and an auth solution based on user-to-server OAuth helps in that regard. - -The method with which frontend plugins request access to third-party services is through [Utility APIs](../api/utility-apis.md) for each service provider. These are all suffixed with `*AuthApiRef`, for example `githubAuthApiRef`. For a full list of providers, see the [@backstage/core-plugin-api](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.index.html#alertapiref) reference. +A common pattern for talking to third-party services in Backstage is +user-to-server requests, where short-lived OAuth Access Tokens are requested by +plugins to authenticate calls to external services. These calls can be made +either directly to the services or through a backend plugin or service. + +By relying on user-to-server calls, we keep the coupling between the frontend and +backend low and provide a much lower barrier for plugins to make use of third +party services. This is in comparison to, for example, a session-based system +where access tokens are stored server-side. Such a solution would require a much +deeper coupling between the auth backend plugin, its session storage, and other +backend plugins or separate services. A goal of Backstage is to make it as easy +as possible to create new plugins, and an auth solution based on user-to-server +OAuth helps in that regard. + +The method with which frontend plugins request access to third-party services is +through [Utility APIs](../api/utility-apis.md) for each service provider. These +are all suffixed with `*AuthApiRef`, for example `githubAuthApiRef`. For a +full list of providers, see the +[@backstage/core-plugin-api](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.index.html#alertapiref) reference. ## Custom Authentication Provider -There are generic authentication providers for OAuth2 and SAML. These can reduce the amount of code needed to implement a custom authentication provider that adheres to these standards. +There are generic authentication providers for OAuth2 and SAML. These can reduce +the amount of code needed to implement a custom authentication provider that +adheres to these standards. -Backstage uses [Passport](http://www.passportjs.org/) under the hood, which has a wide library of authentication strategies for different providers. See [Add authentication provider](add-auth-provider.md) for details on adding a new Passport-supported authentication method. +Backstage uses [Passport](http://www.passportjs.org/) under the hood, which has +a wide library of authentication strategies for different providers. See +[Add authentication provider](add-auth-provider.md) for details on adding a new +Passport-supported authentication method. ## Custom ScmAuthApi Implementation -The default `ScmAuthApi` provides integrations for `github`, `gitlab`, `azure` (Azure DevOps), `bitbucketServer` and `bitbucketCloud` and is created and registered automatically for you by the New Frontend System. +The default `ScmAuthApi` provides integrations for `github`, `gitlab`, `azure` and `bitbucket` and is created by the following code in `packages/app/src/apis.ts`: + +```ts +ScmAuth.createDefaultApiFactory(); +``` If you require only a subset of these integrations, then you will need a custom implementation of the [`ScmAuthApi`](https://backstage.io/api/stable/interfaces/_backstage_integration-react.ScmAuthApi.html). It is an API used to authenticate different SCM systems generically, based on what resource is being accessed, and is used for example, by the Scaffolder (Software Templates) and Catalog Import plugins. @@ -462,7 +465,7 @@ providerFactories: { }, ``` -In the new backend system you can leverage the `authProvidersExtensionPoint` for this: +You can leverage the `authProvidersExtensionPoint` for this: ```ts // your-auth-plugin-module.ts @@ -500,9 +503,13 @@ backend.add(gheAuth); ## Configuring token issuers -By default, the Backstage authentication backend generates and manages its own signing keys automatically for any issued Backstage tokens. However, these keys have a short lifetime and do not persist after instance restarts. +By default, the Backstage authentication backend generates and manages its own signing keys automatically for any issued +Backstage tokens. However, these keys have a short lifetime and do not persist after instance restarts. -Alternatively, users can provide their own public and private key files to sign issued tokens. This is beneficial in scenarios where the token verification implementation aggressively caches the list of keys, and doesn't attempt to fetch new ones even if they encounter an unknown key id. To enable this feature add the following configuration to your config file: +Alternatively, users can provide their own public and private key files to sign issued tokens. This is beneficial in +scenarios where the token verification implementation aggressively caches the list of keys, and doesn't attempt to fetch +new ones even if they encounter an unknown key id. To enable this feature add the following configuration to your config +file: ```yaml auth: @@ -519,7 +526,9 @@ auth: - keyId: ... ``` -The private key should be stored in the PKCS#8 format. The public key should be stored in the SPKI format. You can generate the public/private key pair, using openssl and the ES256 algorithm by performing the following steps: +The private key should be stored in the PKCS#8 format. The public key should be stored in the SPKI format. +You can generate the public/private key pair, using openssl and the ES256 algorithm by performing the following +steps: Generate a private key using the ES256 algorithm diff --git a/docs/auth/index.md b/docs/auth/index.md index 6b6b57d5033bf2..8a2ce1db90653f 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -5,18 +5,14 @@ description: Introduction to authentication in Backstage --- :::info -This documentation is written for the old frontend system. If you are on the [new frontend system](../frontend-system/index.md) you may want to read [its own article](./index--new.md) instead. +This documentation is written for [the new frontend system](../frontend-system/index.md). If you are on the old frontend system you may want to read [its own article](./index--old.md) instead. ::: -The authentication system in Backstage serves two distinct purposes: sign-in and -identification of users, as well as delegating access to third-party resources. It is possible to -configure Backstage to have any number of authentication providers, but only -one of these will typically be used for sign-in, with the rest being used to provide -access to external resources. +The authentication system in Backstage serves two distinct purposes: sign-in and identification of users, as well as delegating access to third-party resources. It is possible to configure Backstage to have any number of authentication providers, but only one of these will typically be used for sign-in, with the rest being used to provide access to external resources. :::note Note -Identity management and the Sign-In page in Backstage will block external access by default, without setting `backend.auth.dangerouslyDisableDefaultAuthPolicy` in configuration. Even so, the frontend bundle is not protected from external access, protecting it requires the use of the [experimental public entry point](https://backstage.io/docs/tutorials/enable-public-entry/). You can learn more about this in the [Threat Model](../overview/threat-model.md#operator-responsibilities). +Identity management and the Sign-In page in Backstage will only block external access when using the new backend system, without setting `backend.auth.dangerouslyDisableDefaultAuthPolicy` in configuration. Even so, the frontend bundle is not protected from external access, protecting it requires the use of the [experimental public entry point](https://backstage.io/docs/tutorials/enable-public-entry/). You can learn more about this in the [Threat Model](../overview/threat-model.md#operator-responsibilities). ::: @@ -41,13 +37,11 @@ Backstage comes with many common authentication providers in the core library: - [OpenShift](openshift/provider.md) - [VMware Cloud](vmware-cloud/provider.md) -These built-in providers handle the authentication flow for a particular service, including required scopes, callbacks, etc. These providers are each added to a -Backstage app in a similar way. +These built-in providers handle the authentication flow for a particular service, including required scopes, callbacks, etc. These providers are each added to a Backstage app in a similar way. ## Configuring Authentication Providers -Each built-in provider has a configuration block under the `auth` section of -`app-config.yaml`. For example, the GitHub provider: +Each built-in provider has a configuration block under the `auth` section of `app-config.yaml`. For example, the GitHub provider: ```yaml auth: @@ -59,66 +53,66 @@ auth: clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} ``` -See the documentation for a particular provider to see what configuration is -needed. +See the documentation for a particular provider to see what configuration is needed. -The `providers` key may have several authentication providers if multiple -authentication methods are supported. Each provider may also have configuration -for different authentication environments (development, production, etc). This -allows a single auth backend to serve multiple environments, such as running a -local frontend against a deployed backend. The provider configuration matching -the local `auth.environment` setting will be selected. +The `providers` key may have several authentication providers if multiple authentication methods are supported. Each provider may also have configuration for different authentication environments (development, production, etc). This allows a single auth backend to serve multiple environments, such as running a local frontend against a deployed backend. The provider configuration matching the local `auth.environment` setting will be selected. ## Sign-In Configuration -Using an authentication provider for sign-in is something you need to configure -both in the frontend app as well as the `auth` backend plugin. For information -on how to configure the backend app, see [Sign-in Identities and Resolvers](./identity-resolver.md). -The rest of this section will focus on how to configure sign-in for the frontend app. +Using an authentication provider for sign-in is something you need to configure both in the frontend app as well as the `auth` backend plugin. For information on how to configure the backend app, see [Sign-in Identities and Resolvers](./identity-resolver.md). The rest of this section will focus on how to configure sign-in for the frontend app. -Sign-in is configured by providing a custom `SignInPage` app component. It will be -rendered before any other routes in the app and is responsible for providing the -identity of the current user. The `SignInPage` can render any number of pages and -components, or just blank space with logic running in the background. In the end, however, it must provide a valid Backstage user identity through the `onSignInSuccess` -callback prop, at which point the rest of the app is rendered. +Sign-in is configured by providing a custom `SignInPage` app component. It will be rendered before any other routes in the app and is responsible for providing the identity of the current user. The `SignInPage` can render any number of pages and components, or just blank space with logic running in the background. In the end, however, it must provide a valid Backstage user identity through the `onSignInSuccess` callback prop, at which point the rest of the app is rendered. -If you want to, you can use the `SignInPage` component that is provided by `@backstage/core-components`, -which takes either a `provider` or `providers` (array) prop of `SignInProviderConfig` definitions. +If you want to, you can use the `SignInPage` component that is provided by `@backstage/core-components`, which takes either a `provider` or `providers` (array) prop of `SignInProviderConfig` definitions. -The following example for GitHub shows the additions needed to `packages/app/src/App.tsx`, -and can be adapted to any of the built-in providers: +The following example for GitHub shows the additions needed to `packages/app/src/App.tsx`, and can be adapted to any of the built-in providers: ```tsx title="packages/app/src/App.tsx" +import { createApp } from '@backstage/frontend-defaults'; +import catalogPlugin from '@backstage/plugin-catalog/alpha'; +import { navModule } from './modules/nav'; + /* highlight-add-start */ import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; import { SignInPage } from '@backstage/core-components'; -/* highlight-add-end */ +import { createFrontendModule } from '@backstage/frontend-plugin-api'; -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => ( - - ), +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => + ( + + ), }, - /* highlight-add-end */ - // .. +}); +/* highlight-add-end */ + +export default createApp({ + features: [ + catalogPlugin, + navModule, + /* highlight-add-start */ + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + /* highlight-add-end */ + ], }); ``` :::note Note -You can configure sign-in to use a redirect flow with no pop-up by adding -`enableExperimentalRedirectFlow: true` to the root of your `app-config.yaml` +You can configure sign-in to use a redirect flow with no pop-up by adding `enableExperimentalRedirectFlow: true` to the root of your `app-config.yaml` ::: @@ -127,26 +121,44 @@ You can configure sign-in to use a redirect flow with no pop-up by adding You can also use the `providers` prop to enable multiple sign-in methods, for example to allow guest access: ```tsx title="packages/app/src/App.tsx" -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => ( - - ), +import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { SignInPage } from '@backstage/core-components'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; + +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => + ( + + ), }, - /* highlight-add-end */ - // .. +}); + +export default createApp({ + features: [ + catalogPlugin, + navModule, + /* highlight-add-start */ + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + /* highlight-add-end */ + ], }); ``` @@ -160,10 +172,14 @@ import { githubAuthApiRef, useApi, } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { SignInPage } from '@backstage/core-components'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; -const app = createApp({ - components: { - SignInPage: props => { +const signInPage = SignInPageBlueprint.make({ + params: { + /* highlight-add-start */ + loader: async () => props => { const configApi = useApi(configApiRef); if (configApi.getString('auth.environment') === 'development') { return ( @@ -181,47 +197,64 @@ const app = createApp({ /> ); } + return ( ); }, + /* highlight-add-end */ }, - // .. +}); + +export default createApp({ + features: [ + catalogPlugin, + navModule, + /* highlight-add-start */ + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + /* highlight-add-end */ + ], }); ``` ## Sign-In with Proxy Providers -Some auth providers are so-called "proxy" providers, meaning they're meant to be used -behind an authentication proxy. Examples of these are -[Amazon Application Load Balancer](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md), -[Azure EasyAuth](./microsoft/azure-easyauth.md), -[Cloudflare Access](./cloudflare/provider.md), -[Google Identity-Aware Proxy](./google/gcp-iap-auth.md) -and [OAuth2 Proxy](./oauth2-proxy/provider.md). +Some auth providers are so-called "proxy" providers, meaning they're meant to be used behind an authentication proxy. Examples of these are [Amazon Application Load Balancer](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md), [Azure EasyAuth](./microsoft/azure-easyauth.md), [Cloudflare Access](./cloudflare/provider.md), [Google Identity-Aware Proxy](./google/gcp-iap-auth.md) and [OAuth2 Proxy](./oauth2-proxy/provider.md). -When using a proxy provider, you'll end up wanting to use a different sign-in page, as -there is no need for further user interaction once you've signed in towards the proxy. -All the sign-in page needs to do is call the `/refresh` endpoint of the auth providers -to get the existing session, which is exactly what the `ProxiedSignInPage` does. The only -thing you need to do to configure the `ProxiedSignInPage` is to pass the ID of the provider like this: +When using a proxy provider, you'll end up wanting to use a different sign-in page, as there is no need for further user interaction once you've signed in towards the proxy. All the sign-in page needs to do is call the `/refresh` endpoint of the auth providers to get the existing session, which is exactly what the `ProxiedSignInPage` does. The only thing you need to do to configure the `ProxiedSignInPage` is to pass the ID of the provider like this: ```tsx title="packages/app/src/App.tsx" +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; import { ProxiedSignInPage } from '@backstage/core-components'; -const app = createApp({ - components: { - SignInPage: props => , +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => + , }, - // .. +}); + +export default createApp({ + features: [ + catalogPlugin, + navModule, + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + ], }); ``` @@ -253,18 +286,20 @@ Headers can also be returned in an async manner: /> ``` -A downside of this method is that it can be cumbersome to set up for local development. -As a workaround for this, it's possible to dynamically select the sign-in page based on -what environment the app is running in and then use a different sign-in method for local -development, if one is needed at all. Depending on the exact setup, one might choose to -select the sign-in method based on the `process.env.NODE_ENV` environment variable, -by checking the `hostname` of the current location, or by accessing the configuration API -to read a configuration value. For example: +A downside of this method is that it can be cumbersome to set up for local development. As a workaround for this, it's possible to dynamically select the sign-in page based on what environment the app is running in and then use a different sign-in method for local development, if one is needed at all. Depending on the exact setup, one might choose to select the sign-in method based on the `process.env.NODE_ENV` environment variable, by checking the `hostname` of the current location, or by accessing the configuration API to read a configuration value. For example: ```tsx title="packages/app/src/App.tsx" -const app = createApp({ - components: { - SignInPage: props => { +import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { ProxiedSignInPage, SignInPage } from '@backstage/core-components'; +import { + createFrontendModule, + googleAuthApiRef, +} from '@backstage/frontend-plugin-api'; + +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => { const configApi = useApi(configApiRef); if (configApi.getString('auth.environment') === 'development') { return ( @@ -279,93 +314,55 @@ const app = createApp({ /> ); } + return ; }, }, - // .. }); -``` - -When using multiple auth providers like this, it's important that you configure the different -sign-in resolvers so that they resolve to the same identity regardless of the method used. - -## Scaffolder Configuration (Software Templates) - -If you want to use the authentication capabilities of the [Repository Picker](../features/software-templates/writing-templates.md#the-repository-picker) inside your software templates, you will need to configure the [`ScmAuthApi`](https://backstage.io/api/stable/interfaces/_backstage_integration-react.ScmAuthApi.html) alongside your authentication provider. It is an API used to authenticate towards different SCM systems in a generic way, based on what resource is being accessed. -To set it up, you'll need to add an API factory entry to `packages/app/src/apis.ts`. The example below sets up the `ScmAuthApi` for an already configured GitLab authentication provider: - -```ts title="packages/app/src/apis.ts" -createApiFactory({ - api: scmAuthApiRef, - deps: { - gitlabAuthApi: gitlabAuthApiRef, - }, - factory: ({ gitlabAuthApi }) => ScmAuth.forGitlab(gitlabAuthApi), +export default createApp({ + features: [ + catalogPlugin, + navModule, + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + ], }); ``` -In case you are using a custom authentication providers, you might need to add a [custom `ScmAuthApi` implementation](./index.md#custom-scmauthapi-implementation). +When using multiple auth providers like this, it's important that you configure the different sign-in resolvers so that they resolve to the same identity regardless of the method used. ## For Plugin Developers -The Backstage frontend core APIs provide a set of Utility APIs for plugin developers -to use, both to access the user identity as well as third-party resources. +The Backstage frontend core APIs provide a set of Utility APIs for plugin developers to use, both to access the user identity as well as third-party resources. ### Identity for Plugin Developers -For plugin developers, there is one main touchpoint for accessing the user identity: the -`IdentityApi` exported by `@backstage/core-plugin-api` via the `identityApiRef`. +For plugin developers, there is one main touchpoint for accessing the user identity: the `IdentityApi` exported by `@backstage/core-plugin-api` via the `identityApiRef`. -The `IdentityApi` gives access to the signed-in user's identity in the frontend. -It provides access to the user's entity reference, lightweight profile information, and -a Backstage token that identifies the user when making authenticated calls within Backstage. +The `IdentityApi` gives access to the signed-in user's identity in the frontend. It provides access to the user's entity reference, lightweight profile information, and a Backstage token that identifies the user when making authenticated calls within Backstage. -When making calls to backend plugins, we recommend that the `FetchApi` is used, which -is exported via the `fetchApiRef` from `@backstage/core-plugin-api`. The `FetchApi` will -automatically include a Backstage token in the request, meaning there is no need -to interact directly with the `IdentityApi`. +When making calls to backend plugins, we recommend that the `FetchApi` is used, which is exported via the `fetchApiRef` from `@backstage/core-plugin-api`. The `FetchApi` will automatically include a Backstage token in the request, meaning there is no need to interact directly with the `IdentityApi`. ### Accessing Third Party Resources -A common pattern for talking to third-party services in Backstage is -user-to-server requests, where short-lived OAuth Access Tokens are requested by -plugins to authenticate calls to external services. These calls can be made -either directly to the services or through a backend plugin or service. - -By relying on user-to-server calls, we keep the coupling between the frontend and -backend low and provide a much lower barrier for plugins to make use of third -party services. This is in comparison to, for example, a session-based system -where access tokens are stored server-side. Such a solution would require a much -deeper coupling between the auth backend plugin, its session storage, and other -backend plugins or separate services. A goal of Backstage is to make it as easy -as possible to create new plugins, and an auth solution based on user-to-server -OAuth helps in that regard. - -The method with which frontend plugins request access to third-party services is -through [Utility APIs](../api/utility-apis.md) for each service provider. These -are all suffixed with `*AuthApiRef`, for example `githubAuthApiRef`. For a -full list of providers, see the -[@backstage/core-plugin-api](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.index.html#alertapiref) reference. +A common pattern for talking to third-party services in Backstage is user-to-server requests, where short-lived OAuth Access Tokens are requested by plugins to authenticate calls to external services. These calls can be made either directly to the services or through a backend plugin or service. + +By relying on user-to-server calls, we keep the coupling between the frontend and backend low and provide a much lower barrier for plugins to make use of third party services. This is in comparison to, for example, a session-based system where access tokens are stored server-side. Such a solution would require a much deeper coupling between the auth backend plugin, its session storage, and other backend plugins or separate services. A goal of Backstage is to make it as easy as possible to create new plugins, and an auth solution based on user-to-server OAuth helps in that regard. + +The method with which frontend plugins request access to third-party services is through [Utility APIs](../api/utility-apis.md) for each service provider. These are all suffixed with `*AuthApiRef`, for example `githubAuthApiRef`. For a full list of providers, see the [@backstage/core-plugin-api](https://backstage.io/api/stable/modules/_backstage_core-plugin-api.index.html#alertapiref) reference. ## Custom Authentication Provider -There are generic authentication providers for OAuth2 and SAML. These can reduce -the amount of code needed to implement a custom authentication provider that -adheres to these standards. +There are generic authentication providers for OAuth2 and SAML. These can reduce the amount of code needed to implement a custom authentication provider that adheres to these standards. -Backstage uses [Passport](http://www.passportjs.org/) under the hood, which has -a wide library of authentication strategies for different providers. See -[Add authentication provider](add-auth-provider.md) for details on adding a new -Passport-supported authentication method. +Backstage uses [Passport](http://www.passportjs.org/) under the hood, which has a wide library of authentication strategies for different providers. See [Add authentication provider](add-auth-provider.md) for details on adding a new Passport-supported authentication method. ## Custom ScmAuthApi Implementation -The default `ScmAuthApi` provides integrations for `github`, `gitlab`, `azure` and `bitbucket` and is created by the following code in `packages/app/src/apis.ts`: - -```ts -ScmAuth.createDefaultApiFactory(); -``` +The default `ScmAuthApi` provides integrations for `github`, `gitlab`, `azure` (Azure DevOps), `bitbucketServer` and `bitbucketCloud` and is created and registered automatically for you by the New Frontend System. If you require only a subset of these integrations, then you will need a custom implementation of the [`ScmAuthApi`](https://backstage.io/api/stable/interfaces/_backstage_integration-react.ScmAuthApi.html). It is an API used to authenticate different SCM systems generically, based on what resource is being accessed, and is used for example, by the Scaffolder (Software Templates) and Catalog Import plugins. @@ -465,7 +462,7 @@ providerFactories: { }, ``` -You can leverage the `authProvidersExtensionPoint` for this: +In the new backend system you can leverage the `authProvidersExtensionPoint` for this: ```ts // your-auth-plugin-module.ts @@ -503,13 +500,9 @@ backend.add(gheAuth); ## Configuring token issuers -By default, the Backstage authentication backend generates and manages its own signing keys automatically for any issued -Backstage tokens. However, these keys have a short lifetime and do not persist after instance restarts. +By default, the Backstage authentication backend generates and manages its own signing keys automatically for any issued Backstage tokens. However, these keys have a short lifetime and do not persist after instance restarts. -Alternatively, users can provide their own public and private key files to sign issued tokens. This is beneficial in -scenarios where the token verification implementation aggressively caches the list of keys, and doesn't attempt to fetch -new ones even if they encounter an unknown key id. To enable this feature add the following configuration to your config -file: +Alternatively, users can provide their own public and private key files to sign issued tokens. This is beneficial in scenarios where the token verification implementation aggressively caches the list of keys, and doesn't attempt to fetch new ones even if they encounter an unknown key id. To enable this feature add the following configuration to your config file: ```yaml auth: @@ -526,9 +519,7 @@ auth: - keyId: ... ``` -The private key should be stored in the PKCS#8 format. The public key should be stored in the SPKI format. -You can generate the public/private key pair, using openssl and the ES256 algorithm by performing the following -steps: +The private key should be stored in the PKCS#8 format. The public key should be stored in the SPKI format. You can generate the public/private key pair, using openssl and the ES256 algorithm by performing the following steps: Generate a private key using the ES256 algorithm diff --git a/docs/features/techdocs/addons--new.md b/docs/features/techdocs/addons--old.md similarity index 56% rename from docs/features/techdocs/addons--new.md rename to docs/features/techdocs/addons--old.md index 04fa68b4235f33..1a22befc2eb074 100644 --- a/docs/features/techdocs/addons--new.md +++ b/docs/features/techdocs/addons--old.md @@ -1,11 +1,11 @@ --- -id: addons--new +id: addons--old title: TechDocs Addons description: How to find, use, or create TechDocs Addons. --- :::info -This documentation is written for [the new frontend system](../../frontend-system/index.md). If you are on the [old frontend system](./getting-started.md#adding-techdocs-frontend-plugin) you may want to read [its own article](./addons.md) instead. +This documentation is written for [the old frontend system](./getting-started.md#adding-techdocs-frontend-plugin). If you are on the [new frontend system](../../frontend-system/index.md) you may want to read [its own article](./addons.md) instead. ::: ## Concepts @@ -50,8 +50,9 @@ representative of physical spaces in the TechDocs UI: ### Addon Registry The installation and configuration of Addons happens within a Backstage app's -frontend. Addons are imported from plugins and registered as a plugin extension which -are configured for both the TechDocs Reader page as well as the Entity docs page. +frontend. Addons are imported from plugins and added underneath a registry +component called ``. This registry can be configured for both +the TechDocs Reader page as well as the Entity docs page. Addons are rendered in the order in which they are registered. @@ -59,26 +60,62 @@ Addons are rendered in the order in which they are registered. To start using Addons you need to add the `@backstage/plugin-techdocs-module-addons-contrib` package to your app. You can do that by running this command from the root of your project: `yarn --cwd packages/app add @backstage/plugin-techdocs-module-addons-contrib` -Addons can then be installed as a module in your `App.tsx`: +Addons can be installed and configured in much the same way as extensions for +other Backstage plugins: by adding them underneath an extension registry +component (``) under the route representing the TechDocs Reader +page in your `App.tsx`: ```tsx // packages/app/src/App.tsx -import { createApp } from '@backstage/frontend-defaults'; -import { createFrontendModule } from '@backstage/frontend-plugin-api'; -import { techDocsReportIssueAddonModule } from '@backstage/plugin-techdocs-module-addons-contrib/alpha'; +import { TechDocsReaderPage } from '@backstage/plugin-techdocs'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; +import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; // ... -const app = createApp({ - features: [ - // ... - techDocsReportIssueAddonModule, - // ...other techdocs addon modules - ], -}); +}> + + + {/* Other addons can be added here. */} + +; +``` + +If you are using a custom [TechDocs reader page](./how-to-guides.md#how-to-customize-the-techdocs-reader-page) your setup will be very similar, here's an example: + +```ts +}> + + + {/* Other addons can be added here. */} + + {techDocsPage} // This is your custom TechDocs reader page + +``` + +The process for configuring Addons on the documentation tab on the entity page +is very similar; instead of adding the `` registry under a +``, you'd add it as a child of ``: + +```tsx +// packages/app/src/components/catalog/EntityPage.tsx + +import { EntityLayout } from '@backstage/plugin-catalog'; +import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; +import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; + +// ... -export default app.createRoot(); + + + + + {/* Other addons can be added here. */} + + +; ``` Note that on the entity page, because the Catalog plugin is responsible for the @@ -89,12 +126,12 @@ page header, TechDocs Addons whose location is `Header` will not be rendered. Addons can, in principle, be provided by any plugin! To make it easier to discover available Addons, we've compiled a list of them here: -| Addon | Package/Plugin | Description | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`techDocsExpandableNavigationAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsExpandableNavigationAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | -| [`techDocsReportIssueAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsReportIssueAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | -| [`techDocsTextSizeAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsTextSizeAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | -| [`techDocsLightBoxAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsLightBoxAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. When clicking on the zoom icon it zooms the image to fit in the screen (similar to `background-size: contain`). Images inside links are ignored to avoid blocking navigation. | +| Addon | Package/Plugin | Description | +| -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.ExpandableNavigation.html) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | +| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.ReportIssue.html) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | +| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.TextSize.html) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | +| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.LightBox.html) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. When clicking on the zoom icon it zooms the image to fit in the screen (similar to `background-size: contain`). Images inside links are ignored to avoid blocking navigation. | Got an Addon to contribute? Feel free to add a row above! @@ -105,32 +142,29 @@ specific locations within a TechDocs site. To package such a react component as an Addon, follow these steps: 1. Write the component in your plugin like any other component -2. Create the addon extension using the `TechDocsAddonBlueprint` -3. Create and export the addon module from your plugin +2. Create, provide, and export the component from your plugin ```ts // plugins/your-plugin/src/plugin.ts -import { TechDocsAddonLocations } from '@backstage/plugin-techdocs-react'; -import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha'; -import { CatGifComponent } from './addons'; -import { createFrontendModule } from '@backstage/frontend-plugin-api'; +import { + createTechDocsAddonExtension, + TechDocsAddonLocations, +} from '@backstage/plugin-techdocs-react'; +import { CatGifComponent, CatGifComponentProps } from './addons'; // ... -const techDocsCatGifAddon = AddonBlueprint.make({ - name: 'cat-gif', - params: { +// You must "provide" your Addon, just like any extension, via your plugin. +export const CatGif = yourPlugin.provide( + // This function "creates" the Addon given a component and location. If your + // component can be configured via props, pass the prop type here too. + createTechDocsAddonExtension({ name: 'CatGif', location: TechDocsAddonLocations.Header, component: CatGifComponent, - }, -}); - -export const techDocsCatGifAddonModule = createFrontendModule({ - pluginId: 'techdocs', - extensions: [techDocsCatGifAddon], -}); + }), +); ``` ### Addons in the Content location @@ -147,8 +181,7 @@ provided by the Addon framework. ```tsx // plugins/your-plugin/src/addons/MakeAllImagesCatGifs.tsx - -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { useShadowRootElements } from '@backstage/plugin-techdocs-react'; // This is a normal react component; in order to make it an Addon, you would diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index 72e8aa39e8de17..9a72f508c353c1 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -5,7 +5,7 @@ description: How to find, use, or create TechDocs Addons. --- :::info -This documentation is written for [the old frontend system](./getting-started.md#adding-techdocs-frontend-plugin). If you are on the [new frontend system](../../frontend-system/index.md) you may want to read [its own article](./addons--new.md) instead. +This documentation is written for [the new frontend system](../../frontend-system/index.md). If you are on the [old frontend system](./getting-started.md#adding-techdocs-frontend-plugin) you may want to read [its own article](./addons--old.md) instead. ::: ## Concepts @@ -50,9 +50,8 @@ representative of physical spaces in the TechDocs UI: ### Addon Registry The installation and configuration of Addons happens within a Backstage app's -frontend. Addons are imported from plugins and added underneath a registry -component called ``. This registry can be configured for both -the TechDocs Reader page as well as the Entity docs page. +frontend. Addons are imported from plugins and registered as a plugin extension which +are configured for both the TechDocs Reader page as well as the Entity docs page. Addons are rendered in the order in which they are registered. @@ -60,62 +59,26 @@ Addons are rendered in the order in which they are registered. To start using Addons you need to add the `@backstage/plugin-techdocs-module-addons-contrib` package to your app. You can do that by running this command from the root of your project: `yarn --cwd packages/app add @backstage/plugin-techdocs-module-addons-contrib` -Addons can be installed and configured in much the same way as extensions for -other Backstage plugins: by adding them underneath an extension registry -component (``) under the route representing the TechDocs Reader -page in your `App.tsx`: +Addons can then be installed as a module in your `App.tsx`: ```tsx // packages/app/src/App.tsx -import { TechDocsReaderPage } from '@backstage/plugin-techdocs'; -import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; -import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; +import { createApp } from '@backstage/frontend-defaults'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; +import { techDocsReportIssueAddonModule } from '@backstage/plugin-techdocs-module-addons-contrib/alpha'; // ... -}> - - - {/* Other addons can be added here. */} - -; -``` - -If you are using a custom [TechDocs reader page](./how-to-guides.md#how-to-customize-the-techdocs-reader-page) your setup will be very similar, here's an example: - -```ts -}> - - - {/* Other addons can be added here. */} - - {techDocsPage} // This is your custom TechDocs reader page - -``` - -The process for configuring Addons on the documentation tab on the entity page -is very similar; instead of adding the `` registry under a -``, you'd add it as a child of ``: - -```tsx -// packages/app/src/components/catalog/EntityPage.tsx - -import { EntityLayout } from '@backstage/plugin-catalog'; -import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; -import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; -import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; - -// ... +const app = createApp({ + features: [ + // ... + techDocsReportIssueAddonModule, + // ...other techdocs addon modules + ], +}); - - - - - {/* Other addons can be added here. */} - - -; +export default app.createRoot(); ``` Note that on the entity page, because the Catalog plugin is responsible for the @@ -126,12 +89,12 @@ page header, TechDocs Addons whose location is `Header` will not be rendered. Addons can, in principle, be provided by any plugin! To make it easier to discover available Addons, we've compiled a list of them here: -| Addon | Package/Plugin | Description | -| -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.ExpandableNavigation.html) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | -| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.ReportIssue.html) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | -| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.TextSize.html) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | -| [``](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.index.LightBox.html) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. When clicking on the zoom icon it zooms the image to fit in the screen (similar to `background-size: contain`). Images inside links are ignored to avoid blocking navigation. | +| Addon | Package/Plugin | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`techDocsExpandableNavigationAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsExpandableNavigationAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | +| [`techDocsReportIssueAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsReportIssueAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | +| [`techDocsTextSizeAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsTextSizeAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | +| [`techDocsLightBoxAddonModule`](https://backstage.io/api/stable/variables/_backstage_plugin-techdocs-module-addons-contrib.plugins_techdocs-module-addons-contrib_src_alpha.techDocsLightBoxAddonModule.html) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. When clicking on the zoom icon it zooms the image to fit in the screen (similar to `background-size: contain`). Images inside links are ignored to avoid blocking navigation. | Got an Addon to contribute? Feel free to add a row above! @@ -142,29 +105,32 @@ specific locations within a TechDocs site. To package such a react component as an Addon, follow these steps: 1. Write the component in your plugin like any other component -2. Create, provide, and export the component from your plugin +2. Create the addon extension using the `TechDocsAddonBlueprint` +3. Create and export the addon module from your plugin ```ts // plugins/your-plugin/src/plugin.ts -import { - createTechDocsAddonExtension, - TechDocsAddonLocations, -} from '@backstage/plugin-techdocs-react'; -import { CatGifComponent, CatGifComponentProps } from './addons'; +import { TechDocsAddonLocations } from '@backstage/plugin-techdocs-react'; +import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha'; +import { CatGifComponent } from './addons'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; // ... -// You must "provide" your Addon, just like any extension, via your plugin. -export const CatGif = yourPlugin.provide( - // This function "creates" the Addon given a component and location. If your - // component can be configured via props, pass the prop type here too. - createTechDocsAddonExtension({ +const techDocsCatGifAddon = AddonBlueprint.make({ + name: 'cat-gif', + params: { name: 'CatGif', location: TechDocsAddonLocations.Header, component: CatGifComponent, - }), -); + }, +}); + +export const techDocsCatGifAddonModule = createFrontendModule({ + pluginId: 'techdocs', + extensions: [techDocsCatGifAddon], +}); ``` ### Addons in the Content location @@ -181,7 +147,8 @@ provided by the Addon framework. ```tsx // plugins/your-plugin/src/addons/MakeAllImagesCatGifs.tsx -import { useEffect } from 'react'; + +import React, { useEffect } from 'react'; import { useShadowRootElements } from '@backstage/plugin-techdocs-react'; // This is a normal react component; in order to make it an Addon, you would diff --git a/docs/getting-started/config/authentication--new.md b/docs/getting-started/config/authentication--old.md similarity index 82% rename from docs/getting-started/config/authentication--new.md rename to docs/getting-started/config/authentication--old.md index ee71bf791351f2..6a320cf0dd805f 100644 --- a/docs/getting-started/config/authentication--new.md +++ b/docs/getting-started/config/authentication--old.md @@ -1,11 +1,11 @@ --- -id: authentication--new +id: authentication--old title: Authentication description: How to setup authentication for your Backstage app --- :::info -This documentation is written for [the new frontend system](../../frontend-system/index.md). If you are on the old frontend system you may want to read [its own article](./authentication.md) instead. +This documentation is written for the old frontend system. If you are on the [new frontend system](../../frontend-system/index.md) you may want to read [its own article](./authentication.md) instead. ::: Audience: Admins or Developers @@ -14,7 +14,7 @@ Audience: Admins or Developers We'll be walking you through how to setup authentication for your Backstage app using GitHub. After finishing this guide, you'll have both working authentication and users in your Backstage app to match to the users logging in! -There are multiple authentication providers available for you to use with Backstage, feel free to follow [their instructions for adding authentication](../../auth/index--new.md). +There are multiple authentication providers available for you to use with Backstage, feel free to follow [their instructions for adding authentication](../../auth/index.md). :::note Note @@ -54,63 +54,37 @@ auth: The next step is to change the sign-in page. For this, you'll actually need to write some code. -First let's add the packages we need, do this from the root: - -```shell -yarn --cwd packages/app add @backstage/core-plugin-api @backstage/plugin-app-react -``` - -Then open `packages/app/src/App.tsx` and below the last `import` line, add: +Open `packages/app/src/App.tsx` and below the last `import` line, add: ```typescript title="packages/app/src/App.tsx" import { githubAuthApiRef } from '@backstage/core-plugin-api'; -import { SignInPageBlueprint } from '@backstage/plugin-app-react'; -import { SignInPage } from '@backstage/core-components'; -import { createFrontendModule } from '@backstage/frontend-plugin-api'; -``` - -Now below this we are going to use the `SignInPageBlueprint` to create an extension, add this code block to do that: - -```tsx -const signInPage = SignInPageBlueprint.make({ - params: { - loader: async () => props => - ( - - ), - }, -}); ``` -Search for the `createApp()` function call in this file, and replace: +Search for `const app = createApp({` in this file, and replace: ```tsx title="packages/app/src/App.tsx" -export default createApp({ - features: [catalogPlugin, navModule], -}); +components: { + SignInPage: props => , +}, ``` with ```tsx title="packages/app/src/App.tsx" -export default createApp({ - features: [ - catalogPlugin, - navModule, - createFrontendModule({ - pluginId: 'app', - extensions: [signInPage], - }), - ], -}); +components: { + SignInPage: props => ( + + ), +}, ``` ## Add sign-in resolver(s) @@ -137,7 +111,7 @@ auth: /* highlight-add-end */ ``` -What this will do is take the user details provided by the auth provider and match that against a User in the Catalog. In this case - `usernameMatchingUserEntityName` - will match the GitHub user name with the `metadata.name` value of a User in the Catalog, if none is found you will get a "Failed to sign-in, unable to resolve user identity" message. We'll cover this in the next few sections. +What this will do is take the user details provided by the auth provider and match that against a User in the Catalog. In this case - `usernameMatchingUserEntityName` - will match the GitHub user name with the `metadata.name` value of a User in the Catalog, if none is found you will get an "Failed to sign-in, unable to resolve user identity" message. We'll cover this in the next few sections. Learn more about this topic in the [Sign-in Resolvers](../../auth/identity-resolver.md#sign-in-resolvers) documentation. @@ -170,7 +144,7 @@ Sometimes the frontend starts before the backend resulting in errors on the sign The recommended approach for adding Users, and Groups, into your Catalog is to use one of the existing Org Entity Providers - [like this one for GitHub](https://backstage.io/docs/integrations/github/org) - or if those don't work you may need to [create one](https://backstage.io/docs/features/software-catalog/external-integrations#custom-entity-providers) that fits your Organization's needs. -For the sake of this guide we'll simply step you through adding a User to the `org.yaml` file that is included when you create a new Backstage instance. Let's do that: +For the sake of this guide we'll simply step you though adding a User to the `org.yaml` file that is included when you create a new Backstage instance. Let's do that: 1. First open the `/examples/org.yaml` file in your text editor of choice 2. At the bottom we'll add the following YAML: @@ -192,7 +166,7 @@ Let's restart Backstage from the terminal once more, by stopping it with `Ctrl+C To learn more about Authentication in Backstage, here are some docs you could read: -- [Authentication in Backstage](../../auth/index--new.md) +- [Authentication in Backstage](../../auth/index.md) - [Using organizational data from GitHub](../../integrations/github/org.md) ## Setting up a GitHub Integration diff --git a/docs/getting-started/config/authentication.md b/docs/getting-started/config/authentication.md index 3585ae066d8f03..eaf2a01881c812 100644 --- a/docs/getting-started/config/authentication.md +++ b/docs/getting-started/config/authentication.md @@ -5,7 +5,7 @@ description: How to setup authentication for your Backstage app --- :::info -This documentation is written for the old frontend system. If you are on the [new frontend system](../../frontend-system/index.md) you may want to read [its own article](./authentication--new.md) instead. +This documentation is written for [the new frontend system](../../frontend-system/index.md). If you are on the old frontend system you may want to read [its own article](./authentication--old.md) instead. ::: Audience: Admins or Developers @@ -14,7 +14,7 @@ Audience: Admins or Developers We'll be walking you through how to setup authentication for your Backstage app using GitHub. After finishing this guide, you'll have both working authentication and users in your Backstage app to match to the users logging in! -There are multiple authentication providers available for you to use with Backstage, feel free to follow [their instructions for adding authentication](../../auth/index.md). +There are multiple authentication providers available for you to use with Backstage, feel free to follow [their instructions for adding authentication](../../auth/index--new.md). :::note Note @@ -54,37 +54,63 @@ auth: The next step is to change the sign-in page. For this, you'll actually need to write some code. -Open `packages/app/src/App.tsx` and below the last `import` line, add: +First let's add the packages we need, do this from the root: + +```shell +yarn --cwd packages/app add @backstage/core-plugin-api @backstage/plugin-app-react +``` + +Then open `packages/app/src/App.tsx` and below the last `import` line, add: ```typescript title="packages/app/src/App.tsx" import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; +import { SignInPage } from '@backstage/core-components'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; +``` + +Now below this we are going to use the `SignInPageBlueprint` to create an extension, add this code block to do that: + +```tsx +const signInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => + ( + + ), + }, +}); ``` -Search for `const app = createApp({` in this file, and replace: +Search for the `createApp()` function call in this file, and replace: ```tsx title="packages/app/src/App.tsx" -components: { - SignInPage: props => , -}, +export default createApp({ + features: [catalogPlugin, navModule], +}); ``` with ```tsx title="packages/app/src/App.tsx" -components: { - SignInPage: props => ( - - ), -}, +export default createApp({ + features: [ + catalogPlugin, + navModule, + createFrontendModule({ + pluginId: 'app', + extensions: [signInPage], + }), + ], +}); ``` ## Add sign-in resolver(s) @@ -111,7 +137,7 @@ auth: /* highlight-add-end */ ``` -What this will do is take the user details provided by the auth provider and match that against a User in the Catalog. In this case - `usernameMatchingUserEntityName` - will match the GitHub user name with the `metadata.name` value of a User in the Catalog, if none is found you will get an "Failed to sign-in, unable to resolve user identity" message. We'll cover this in the next few sections. +What this will do is take the user details provided by the auth provider and match that against a User in the Catalog. In this case - `usernameMatchingUserEntityName` - will match the GitHub user name with the `metadata.name` value of a User in the Catalog, if none is found you will get a "Failed to sign-in, unable to resolve user identity" message. We'll cover this in the next few sections. Learn more about this topic in the [Sign-in Resolvers](../../auth/identity-resolver.md#sign-in-resolvers) documentation. @@ -144,7 +170,7 @@ Sometimes the frontend starts before the backend resulting in errors on the sign The recommended approach for adding Users, and Groups, into your Catalog is to use one of the existing Org Entity Providers - [like this one for GitHub](https://backstage.io/docs/integrations/github/org) - or if those don't work you may need to [create one](https://backstage.io/docs/features/software-catalog/external-integrations#custom-entity-providers) that fits your Organization's needs. -For the sake of this guide we'll simply step you though adding a User to the `org.yaml` file that is included when you create a new Backstage instance. Let's do that: +For the sake of this guide we'll simply step you through adding a User to the `org.yaml` file that is included when you create a new Backstage instance. Let's do that: 1. First open the `/examples/org.yaml` file in your text editor of choice 2. At the bottom we'll add the following YAML: @@ -166,7 +192,7 @@ Let's restart Backstage from the terminal once more, by stopping it with `Ctrl+C To learn more about Authentication in Backstage, here are some docs you could read: -- [Authentication in Backstage](../../auth/index.md) +- [Authentication in Backstage](../../auth/index--new.md) - [Using organizational data from GitHub](../../integrations/github/org.md) ## Setting up a GitHub Integration From e189e60cb26b058a5b8626735cb563b99f0f3da6 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Tue, 17 Mar 2026 11:36:00 -0500 Subject: [PATCH 40/55] Fixed links Signed-off-by: Andre Wanlin --- docs/getting-started/config/authentication--old.md | 2 +- docs/getting-started/config/authentication.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/getting-started/config/authentication--old.md b/docs/getting-started/config/authentication--old.md index 6a320cf0dd805f..cc02eb2b7f045e 100644 --- a/docs/getting-started/config/authentication--old.md +++ b/docs/getting-started/config/authentication--old.md @@ -14,7 +14,7 @@ Audience: Admins or Developers We'll be walking you through how to setup authentication for your Backstage app using GitHub. After finishing this guide, you'll have both working authentication and users in your Backstage app to match to the users logging in! -There are multiple authentication providers available for you to use with Backstage, feel free to follow [their instructions for adding authentication](../../auth/index.md). +There are multiple authentication providers available for you to use with Backstage, feel free to follow [their instructions for adding authentication](../../auth/index--old.md). :::note Note diff --git a/docs/getting-started/config/authentication.md b/docs/getting-started/config/authentication.md index eaf2a01881c812..9df27692b83c61 100644 --- a/docs/getting-started/config/authentication.md +++ b/docs/getting-started/config/authentication.md @@ -14,7 +14,7 @@ Audience: Admins or Developers We'll be walking you through how to setup authentication for your Backstage app using GitHub. After finishing this guide, you'll have both working authentication and users in your Backstage app to match to the users logging in! -There are multiple authentication providers available for you to use with Backstage, feel free to follow [their instructions for adding authentication](../../auth/index--new.md). +There are multiple authentication providers available for you to use with Backstage, feel free to follow [their instructions for adding authentication](../../auth/index.md). :::note Note @@ -192,7 +192,7 @@ Let's restart Backstage from the terminal once more, by stopping it with `Ctrl+C To learn more about Authentication in Backstage, here are some docs you could read: -- [Authentication in Backstage](../../auth/index--new.md) +- [Authentication in Backstage](../../auth/index.md) - [Using organizational data from GitHub](../../integrations/github/org.md) ## Setting up a GitHub Integration From 0be2541b5cc0072441038ea3bce161e6ef0595c0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 13:02:06 +0100 Subject: [PATCH 41/55] Promote translation refs from alpha to stable entry points Add each plugin and package translation ref to the corresponding stable entry point, changing the JSDoc tag from @alpha to @public. The alpha entry points now re-export with a @deprecated annotation so existing consumers continue to work. Affected packages: core-components, api-docs, catalog, catalog-graph, catalog-import, catalog-react, home, home-react, kubernetes, kubernetes-cluster, kubernetes-react, notifications, org, scaffolder, scaffolder-react, search, search-react, user-settings. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../promote-translation-refs-stable-minor.md | 10 + .../promote-translation-refs-stable-patch.md | 16 ++ packages/core-components/report-alpha.api.md | 2 +- packages/core-components/report.api.md | 64 ++++++ packages/core-components/src/alpha.ts | 8 +- packages/core-components/src/index.ts | 1 + packages/core-components/src/translation.ts | 2 +- plugins/api-docs/report-alpha.api.md | 2 +- plugins/api-docs/report.api.md | 37 ++++ plugins/api-docs/src/alpha.tsx | 8 +- plugins/api-docs/src/index.ts | 1 + plugins/api-docs/src/translation.ts | 2 +- plugins/catalog-graph/report-alpha.api.md | 2 +- plugins/catalog-graph/report.api.md | 29 +++ plugins/catalog-graph/src/alpha.tsx | 8 +- plugins/catalog-graph/src/index.ts | 1 + plugins/catalog-graph/src/translation.ts | 2 +- plugins/catalog-import/report-alpha.api.md | 2 +- plugins/catalog-import/report.api.md | 80 +++++++- plugins/catalog-import/src/alpha.tsx | 10 +- plugins/catalog-import/src/index.ts | 1 + plugins/catalog-import/src/translation.ts | 2 +- plugins/catalog-react/report-alpha.api.md | 2 +- plugins/catalog-react/report.api.md | 96 +++++++++ plugins/catalog-react/src/alpha/index.ts | 8 +- plugins/catalog-react/src/index.ts | 1 + plugins/catalog-react/src/translation.ts | 2 +- plugins/catalog/report-alpha.api.md | 2 +- plugins/catalog/report.api.md | 99 ++++++++++ plugins/catalog/src/alpha/index.ts | 8 +- plugins/catalog/src/alpha/translation.ts | 2 +- plugins/catalog/src/index.ts | 1 + plugins/home-react/report-alpha.api.md | 4 +- plugins/home-react/report.api.md | 11 ++ plugins/home-react/src/alpha.ts | 8 +- plugins/home-react/src/index.ts | 1 + plugins/home-react/src/translation.ts | 4 +- plugins/home/report-alpha.api.md | 2 +- plugins/home/report.api.md | 34 ++++ plugins/home/src/alpha.tsx | 8 +- plugins/home/src/index.ts | 1 + plugins/home/src/translation.ts | 2 +- .../kubernetes-cluster/report-alpha.api.md | 2 +- plugins/kubernetes-cluster/report.api.md | 10 + plugins/kubernetes-cluster/src/alpha.ts | 8 +- plugins/kubernetes-cluster/src/index.ts | 1 + plugins/kubernetes-cluster/src/translation.ts | 2 +- plugins/kubernetes-react/report-alpha.api.md | 2 +- plugins/kubernetes-react/report.api.md | 86 ++++++++ plugins/kubernetes-react/src/alpha.ts | 8 +- plugins/kubernetes-react/src/index.ts | 1 + plugins/kubernetes-react/src/translation.ts | 2 +- plugins/kubernetes/report-alpha.api.md | 2 +- plugins/kubernetes/report.api.md | 14 ++ plugins/kubernetes/src/alpha/index.ts | 8 +- plugins/kubernetes/src/alpha/translation.ts | 2 +- plugins/kubernetes/src/index.ts | 1 + plugins/notifications/report-alpha.api.md | 2 +- plugins/notifications/report.api.md | 66 +++++++ plugins/notifications/src/alpha.tsx | 8 +- plugins/notifications/src/index.ts | 1 + plugins/notifications/src/translation.ts | 2 +- plugins/org/report-alpha.api.md | 2 +- plugins/org/report.api.md | 30 +++ plugins/org/src/alpha.tsx | 8 +- plugins/org/src/index.ts | 1 + plugins/org/src/translation.ts | 2 +- plugins/scaffolder-react/report-alpha.api.md | 12 +- plugins/scaffolder-react/report.api.md | 26 +++ plugins/scaffolder-react/src/alpha.ts | 8 +- plugins/scaffolder-react/src/index.ts | 1 + plugins/scaffolder-react/src/translation.ts | 2 +- plugins/scaffolder/report-alpha.api.md | 2 +- plugins/scaffolder/report.api.md | 187 ++++++++++++++++++ plugins/scaffolder/src/alpha/index.ts | 8 +- plugins/scaffolder/src/index.ts | 1 + plugins/scaffolder/src/translation.ts | 2 +- plugins/search-react/report-alpha.api.md | 2 +- plugins/search-react/report.api.md | 19 ++ plugins/search-react/src/alpha/index.ts | 8 +- plugins/search-react/src/index.ts | 1 + plugins/search-react/src/translation.ts | 2 +- plugins/search/report-alpha.api.md | 2 +- plugins/search/report.api.md | 15 ++ plugins/search/src/alpha.tsx | 9 +- plugins/search/src/index.ts | 1 + plugins/search/src/translation.ts | 2 +- plugins/user-settings/report-alpha.api.md | 2 +- plugins/user-settings/report.api.md | 58 ++++++ plugins/user-settings/src/alpha.tsx | 8 +- plugins/user-settings/src/index.ts | 1 + plugins/user-settings/src/translation.ts | 2 +- 92 files changed, 1171 insertions(+), 67 deletions(-) create mode 100644 .changeset/promote-translation-refs-stable-minor.md create mode 100644 .changeset/promote-translation-refs-stable-patch.md diff --git a/.changeset/promote-translation-refs-stable-minor.md b/.changeset/promote-translation-refs-stable-minor.md new file mode 100644 index 00000000000000..6bf063bde7f06c --- /dev/null +++ b/.changeset/promote-translation-refs-stable-minor.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-catalog-react': minor +'@backstage/plugin-catalog': minor +'@backstage/plugin-scaffolder-react': minor +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-search-react': minor +'@backstage/plugin-search': minor +--- + +Promoted the plugin's translation ref to the stable package entry point. It was previously only available through the alpha entry point. diff --git a/.changeset/promote-translation-refs-stable-patch.md b/.changeset/promote-translation-refs-stable-patch.md new file mode 100644 index 00000000000000..6cd963a3683f69 --- /dev/null +++ b/.changeset/promote-translation-refs-stable-patch.md @@ -0,0 +1,16 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-home-react': patch +'@backstage/plugin-home': patch +'@backstage/plugin-kubernetes-cluster': patch +'@backstage/plugin-kubernetes-react': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-notifications': patch +'@backstage/plugin-org': patch +'@backstage/plugin-user-settings': patch +--- + +Promoted the plugin's translation ref to the stable package entry point. It was previously only available through the alpha entry point. diff --git a/packages/core-components/report-alpha.api.md b/packages/core-components/report-alpha.api.md index f373dd56ff0b2a..e7a36c7df99ad7 100644 --- a/packages/core-components/report-alpha.api.md +++ b/packages/core-components/report-alpha.api.md @@ -5,7 +5,7 @@ ```ts import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const coreComponentsTranslationRef: TranslationRef< 'core-components', { diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index d487cc00b0c5c2..ac6dc5ca15feb3 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -54,6 +54,7 @@ import { SVGProps } from 'react'; import { TabProps } from '@material-ui/core/Tab'; import { Theme } from '@material-ui/core/styles'; import { TooltipProps } from '@material-ui/core/Tooltip'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { WithStyles } from '@material-ui/core/styles'; // @public @@ -222,6 +223,69 @@ export interface CopyTextButtonProps { tooltipText?: string; } +// @public (undocumented) +export const coreComponentsTranslationRef: TranslationRef< + 'core-components', + { + readonly 'table.filter.title': 'Filters'; + readonly 'table.filter.placeholder': 'All results'; + readonly 'table.filter.clearAll': 'Clear all'; + readonly 'table.body.emptyDataSourceMessage': 'No records to display'; + readonly 'table.header.actions': 'Actions'; + readonly 'table.toolbar.search': 'Filter'; + readonly 'table.pagination.labelDisplayedRows': '{from}-{to} of {count}'; + readonly 'table.pagination.firstTooltip': 'First Page'; + readonly 'table.pagination.labelRowsSelect': 'rows'; + readonly 'table.pagination.lastTooltip': 'Last Page'; + readonly 'table.pagination.nextTooltip': 'Next Page'; + readonly 'table.pagination.previousTooltip': 'Previous Page'; + readonly 'emptyState.missingAnnotation.title': 'Missing Annotation'; + readonly 'emptyState.missingAnnotation.actionTitle': 'Add the annotation to your component YAML as shown in the highlighted example below:'; + readonly 'emptyState.missingAnnotation.readMore': 'Read more'; + readonly 'signIn.title': 'Sign In'; + readonly 'signIn.loginFailed': 'Login failed'; + readonly 'signIn.customProvider.title': 'Custom User'; + readonly 'signIn.customProvider.subtitle': 'Enter your own User ID and credentials.\n This selection will not be stored.'; + readonly 'signIn.customProvider.userId': 'User ID'; + readonly 'signIn.customProvider.tokenInvalid': 'Token is not a valid OpenID Connect JWT Token'; + readonly 'signIn.customProvider.continue': 'Continue'; + readonly 'signIn.customProvider.idToken': 'ID Token (optional)'; + readonly 'signIn.guestProvider.title': 'Guest'; + readonly 'signIn.guestProvider.enter': 'Enter'; + readonly 'signIn.guestProvider.subtitle': 'Enter as a Guest User.\n You will not have a verified identity, meaning some features might be unavailable.'; + readonly skipToContent: 'Skip to content'; + readonly 'copyTextButton.tooltipText': 'Text copied to clipboard'; + readonly 'simpleStepper.finish': 'Finish'; + readonly 'simpleStepper.reset': 'Reset'; + readonly 'simpleStepper.next': 'Next'; + readonly 'simpleStepper.skip': 'Skip'; + readonly 'simpleStepper.back': 'Back'; + readonly 'errorPage.title': 'Looks like someone dropped the mic!'; + readonly 'errorPage.subtitle': 'ERROR {{status}}: {{statusMessage}}'; + readonly 'errorPage.goBack': 'Go back'; + readonly 'errorPage.showMoreDetails': 'Show more details'; + readonly 'errorPage.showLessDetails': 'Show less details'; + readonly 'supportConfig.default.title': 'Support Not Configured'; + readonly 'supportConfig.default.linkTitle': 'Add `app.support` config key'; + readonly 'errorBoundary.title': 'Please contact {{slackChannel}} for help.'; + readonly 'oauthRequestDialog.message': 'Sign-in to allow {{appTitle}} access to {{provider}} APIs and identities.'; + readonly 'oauthRequestDialog.title': 'Login Required'; + readonly 'oauthRequestDialog.authRedirectTitle': 'This will trigger a http redirect to OAuth Login.'; + readonly 'oauthRequestDialog.login': 'Log in'; + readonly 'oauthRequestDialog.rejectAll': 'Reject All'; + readonly 'supportButton.title': 'Support'; + readonly 'supportButton.close': 'Close'; + readonly 'alertDisplay.message_one': '({{ count }} newer message)'; + readonly 'alertDisplay.message_other': '({{ count }} newer messages)'; + readonly 'autoLogout.stillTherePrompt.title': 'Logging out due to inactivity'; + readonly 'autoLogout.stillTherePrompt.buttonText': "Yes! Don't log me out"; + readonly 'dependencyGraph.fullscreenTooltip': 'Toggle fullscreen'; + readonly 'proxiedSignInPage.title': 'You do not appear to be signed in. Please try reloading the browser page.'; + readonly 'logViewer.searchField.placeholder': 'Search'; + readonly 'logViewer.downloadBtn.tooltip': 'Download logs'; + } +>; + // @public export function CreateButton(props: CreateButtonProps): JSX_2.Element | null; diff --git a/packages/core-components/src/alpha.ts b/packages/core-components/src/alpha.ts index e1f7678bae75d4..b85e10fe709073 100644 --- a/packages/core-components/src/alpha.ts +++ b/packages/core-components/src/alpha.ts @@ -13,4 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './translation'; +import { coreComponentsTranslationRef as _coreComponentsTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/core-components` instead. + */ +export const coreComponentsTranslationRef = _coreComponentsTranslationRef; diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts index 3c5e708360005f..20068585998388 100644 --- a/packages/core-components/src/index.ts +++ b/packages/core-components/src/index.ts @@ -25,3 +25,4 @@ export * from './hooks'; export * from './icons'; export * from './layout'; export * from './overridableComponents'; +export { coreComponentsTranslationRef } from './translation'; diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts index 1c063195c955f1..d6012273e9916b 100644 --- a/packages/core-components/src/translation.ts +++ b/packages/core-components/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const coreComponentsTranslationRef = createTranslationRef({ id: 'core-components', messages: { diff --git a/plugins/api-docs/report-alpha.api.md b/plugins/api-docs/report-alpha.api.md index 4afa55ce773fb3..8e64334b09af22 100644 --- a/plugins/api-docs/report-alpha.api.md +++ b/plugins/api-docs/report-alpha.api.md @@ -26,7 +26,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const apiDocsTranslationRef: TranslationRef< 'api-docs', { diff --git a/plugins/api-docs/report.api.md b/plugins/api-docs/report.api.md index 370cbb3e0e0964..5e7dd8f056033d 100644 --- a/plugins/api-docs/report.api.md +++ b/plugins/api-docs/report.api.md @@ -19,6 +19,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { TableColumn } from '@backstage/core-components'; import { TableOptions } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; // @public (undocumented) @@ -62,6 +63,42 @@ const apiDocsPlugin: BackstagePlugin< export { apiDocsPlugin }; export { apiDocsPlugin as plugin }; +// @public (undocumented) +export const apiDocsTranslationRef: TranslationRef< + 'api-docs', + { + readonly 'apiDefinitionCard.error.title': 'Could not fetch the API'; + readonly 'apiDefinitionCard.rawButtonTitle': 'Raw'; + readonly 'apiDefinitionDialog.closeButtonTitle': 'Close'; + readonly 'apiDefinitionDialog.tabsAriaLabel': 'API definition options'; + readonly 'apiDefinitionDialog.rawButtonTitle': 'Raw'; + readonly 'apiDefinitionDialog.toggleButtonAriaLabel': 'Toggle API Definition Dialog'; + readonly 'defaultApiExplorerPage.title': 'APIs'; + readonly 'defaultApiExplorerPage.subtitle': '{{orgName}} API Explorer'; + readonly 'defaultApiExplorerPage.pageTitleOverride': 'APIs'; + readonly 'defaultApiExplorerPage.createButtonTitle': 'Register Existing API'; + readonly 'defaultApiExplorerPage.supportButtonTitle': 'All your APIs'; + readonly 'consumedApisCard.error.title': 'Could not load APIs'; + readonly 'consumedApisCard.title': 'Consumed APIs'; + readonly 'consumedApisCard.emptyContent.title': 'This {{entity}} does not consume any APIs.'; + readonly 'hasApisCard.error.title': 'Could not load APIs'; + readonly 'hasApisCard.title': 'APIs'; + readonly 'hasApisCard.emptyContent.title': 'This {{entity}} does not contain any APIs.'; + readonly 'providedApisCard.error.title': 'Could not load APIs'; + readonly 'providedApisCard.title': 'Provided APIs'; + readonly 'providedApisCard.emptyContent.title': 'This {{entity}} does not provide any APIs.'; + readonly 'apiEntityColumns.typeTitle': 'Type'; + readonly 'apiEntityColumns.apiDefinitionTitle': 'API Definition'; + readonly 'consumingComponentsCard.error.title': 'Could not load components'; + readonly 'consumingComponentsCard.title': 'Consumers'; + readonly 'consumingComponentsCard.emptyContent.title': 'No component consumes this API.'; + readonly 'providingComponentsCard.error.title': 'Could not load components'; + readonly 'providingComponentsCard.title': 'Providers'; + readonly 'providingComponentsCard.emptyContent.title': 'No component provides this API.'; + readonly apisCardHelpLinkTitle: 'Learn how to change this.'; + } +>; + // @public export const ApiExplorerIndexPage: ( props: DefaultApiExplorerPageProps, diff --git a/plugins/api-docs/src/alpha.tsx b/plugins/api-docs/src/alpha.tsx index 2724619a685e46..967e1c11680445 100644 --- a/plugins/api-docs/src/alpha.tsx +++ b/plugins/api-docs/src/alpha.tsx @@ -234,4 +234,10 @@ export default createFrontendPlugin({ ], }); -export { apiDocsTranslationRef } from './translation'; +import { apiDocsTranslationRef as _apiDocsTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-api-docs` instead. + */ +export const apiDocsTranslationRef = _apiDocsTranslationRef; diff --git a/plugins/api-docs/src/index.ts b/plugins/api-docs/src/index.ts index 49b2805aadd754..ae344890b0ffdf 100644 --- a/plugins/api-docs/src/index.ts +++ b/plugins/api-docs/src/index.ts @@ -34,3 +34,4 @@ export { EntityProvidedApisCard, EntityProvidingComponentsCard, } from './plugin'; +export { apiDocsTranslationRef } from './translation'; diff --git a/plugins/api-docs/src/translation.ts b/plugins/api-docs/src/translation.ts index a6a43952bccdf6..289115fd3a200b 100644 --- a/plugins/api-docs/src/translation.ts +++ b/plugins/api-docs/src/translation.ts @@ -17,7 +17,7 @@ import { createTranslationRef } from '@backstage/frontend-plugin-api'; /** - * @alpha + * @public */ export const apiDocsTranslationRef = createTranslationRef({ id: 'api-docs', diff --git a/plugins/catalog-graph/report-alpha.api.md b/plugins/catalog-graph/report-alpha.api.md index f9d4542625dc3f..b9b92cf3e468ca 100644 --- a/plugins/catalog-graph/report-alpha.api.md +++ b/plugins/catalog-graph/report-alpha.api.md @@ -22,7 +22,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const catalogGraphTranslationRef: TranslationRef< 'catalog-graph', { diff --git a/plugins/catalog-graph/report.api.md b/plugins/catalog-graph/report.api.md index 74b9713cf2785b..4a948ce26228d4 100644 --- a/plugins/catalog-graph/report.api.md +++ b/plugins/catalog-graph/report.api.md @@ -15,6 +15,7 @@ import { MouseEvent as MouseEvent_2 } from 'react'; import { MouseEventHandler } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public export const ALL_RELATION_PAIRS: RelationPairs; @@ -69,6 +70,34 @@ export const catalogGraphPlugin: BackstagePlugin< // @public export const catalogGraphRouteRef: RouteRef; +// @public (undocumented) +export const catalogGraphTranslationRef: TranslationRef< + 'catalog-graph', + { + readonly 'catalogGraphCard.title': 'Relations'; + readonly 'catalogGraphCard.deepLinkTitle': 'View graph'; + readonly 'catalogGraphPage.title': 'Catalog Graph'; + readonly 'catalogGraphPage.filterToggleButtonTitle': 'Filters'; + readonly 'catalogGraphPage.supportButtonDescription': 'Start tracking your component in by adding it to the software catalog.'; + readonly 'catalogGraphPage.simplifiedSwitchLabel': 'Simplified'; + readonly 'catalogGraphPage.mergeRelationsSwitchLabel': 'Merge relations'; + readonly 'catalogGraphPage.zoomOutDescription': 'Use pinch & zoom to move around the diagram. Click to change active node, shift click to navigate to entity.'; + readonly 'catalogGraphPage.curveFilter.title': 'Curve'; + readonly 'catalogGraphPage.curveFilter.curveStepBefore': 'Step Before'; + readonly 'catalogGraphPage.curveFilter.curveMonotoneX': 'Monotone X'; + readonly 'catalogGraphPage.directionFilter.title': 'Direction'; + readonly 'catalogGraphPage.directionFilter.leftToRight': 'Left to right'; + readonly 'catalogGraphPage.directionFilter.rightToLeft': 'Right to left'; + readonly 'catalogGraphPage.directionFilter.topToBottom': 'Top to bottom'; + readonly 'catalogGraphPage.directionFilter.bottomToTop': 'Bottom to top'; + readonly 'catalogGraphPage.maxDepthFilter.title': 'Max depth'; + readonly 'catalogGraphPage.maxDepthFilter.inputPlaceholder': '∞ Infinite'; + readonly 'catalogGraphPage.maxDepthFilter.clearButtonAriaLabel': 'clear max depth'; + readonly 'catalogGraphPage.selectedKindsFilter.title': 'Kinds'; + readonly 'catalogGraphPage.selectedRelationsFilter.title': 'Relations'; + } +>; + // @public (undocumented) export type CustomLabelClassKey = 'text' | 'secondary'; diff --git a/plugins/catalog-graph/src/alpha.tsx b/plugins/catalog-graph/src/alpha.tsx index a2a8c7eb5e6dce..eda6d7b1e2d825 100644 --- a/plugins/catalog-graph/src/alpha.tsx +++ b/plugins/catalog-graph/src/alpha.tsx @@ -107,4 +107,10 @@ export default createFrontendPlugin({ extensions: [CatalogGraphPage, CatalogGraphEntityCard, CatalogGraphApi], }); -export { catalogGraphTranslationRef } from './translation'; +import { catalogGraphTranslationRef as _catalogGraphTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-catalog-graph` instead. + */ +export const catalogGraphTranslationRef = _catalogGraphTranslationRef; diff --git a/plugins/catalog-graph/src/index.ts b/plugins/catalog-graph/src/index.ts index 95ac5048cb5af4..119e4e495235ea 100644 --- a/plugins/catalog-graph/src/index.ts +++ b/plugins/catalog-graph/src/index.ts @@ -36,3 +36,4 @@ export type { } from './lib/types'; export { Direction } from './lib/types'; export type { TransformationContext } from './lib/graph-transformations'; +export { catalogGraphTranslationRef } from './translation'; diff --git a/plugins/catalog-graph/src/translation.ts b/plugins/catalog-graph/src/translation.ts index 84b76f965def4d..846188f5c081ad 100644 --- a/plugins/catalog-graph/src/translation.ts +++ b/plugins/catalog-graph/src/translation.ts @@ -15,7 +15,7 @@ */ import { createTranslationRef } from '@backstage/frontend-plugin-api'; -/** @alpha */ +/** @public */ export const catalogGraphTranslationRef = createTranslationRef({ id: 'catalog-graph', messages: { diff --git a/plugins/catalog-import/report-alpha.api.md b/plugins/catalog-import/report-alpha.api.md index b58b6410508cd8..bc60f6fab7be6f 100644 --- a/plugins/catalog-import/report-alpha.api.md +++ b/plugins/catalog-import/report-alpha.api.md @@ -18,7 +18,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const catalogImportTranslationRef: TranslationRef< 'catalog-import', { diff --git a/plugins/catalog-import/report.api.md b/plugins/catalog-import/report.api.md index 238231e498bf74..7eff3a7a9c48a6 100644 --- a/plugins/catalog-import/report.api.md +++ b/plugins/catalog-import/report.api.md @@ -6,7 +6,7 @@ import { ApiRef } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; -import { catalogImportTranslationRef } from '@backstage/plugin-catalog-import/alpha'; +import { catalogImportTranslationRef as catalogImportTranslationRef_2 } from '@backstage/plugin-catalog-import/alpha'; import { ComponentProps } from 'react'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { ConfigApi } from '@backstage/core-plugin-api'; @@ -26,6 +26,7 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { SubmitHandler } from 'react-hook-form'; import { TextFieldProps } from '@material-ui/core/TextField/TextField'; import { TranslationFunction } from '@backstage/core-plugin-api/alpha'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UseFormProps } from 'react-hook-form'; import { UseFormReturn } from 'react-hook-form'; @@ -141,13 +142,86 @@ const catalogImportPlugin: BackstagePlugin< export { catalogImportPlugin }; export { catalogImportPlugin as plugin }; +// @public (undocumented) +export const catalogImportTranslationRef: TranslationRef< + 'catalog-import', + { + readonly 'buttons.back': 'Back'; + readonly 'defaultImportPage.headerTitle': 'Register an existing component'; + readonly 'defaultImportPage.contentHeaderTitle': 'Start tracking your component in {{appTitle}}'; + readonly 'defaultImportPage.supportTitle': 'Start tracking your component in {{appTitle}} by adding it to the software catalog.'; + readonly 'importInfoCard.title': 'Register an existing component'; + readonly 'importInfoCard.deepLinkTitle': 'Learn more about the Software Catalog'; + readonly 'importInfoCard.linkDescription': 'Enter the URL to your source code repository to add it to {{appTitle}}.'; + readonly 'importInfoCard.fileLinkTitle': 'Link to an existing entity file'; + readonly 'importInfoCard.examplePrefix': 'Example: '; + readonly 'importInfoCard.fileLinkDescription': 'The wizard analyzes the file, previews the entities, and adds them to the {{appTitle}} catalog.'; + readonly 'importInfoCard.exampleDescription': 'The wizard discovers all {{catalogFilename}} files in the repository, previews the entities, and adds them to the {{appTitle}} catalog.'; + readonly 'importInfoCard.preparePullRequestDescription': 'If no entities are found, the wizard will prepare a Pull Request that adds an example {{catalogFilename}} and prepares the {{appTitle}} catalog to load all entities as soon as the Pull Request is merged.'; + readonly 'importInfoCard.githubIntegration.label': 'GitHub only'; + readonly 'importInfoCard.githubIntegration.title': 'Link to a repository'; + readonly 'importStepper.finish.title': 'Finish'; + readonly 'importStepper.singleLocation.title': 'Select Locations'; + readonly 'importStepper.singleLocation.description': 'Discovered Locations: 1'; + readonly 'importStepper.multipleLocations.title': 'Select Locations'; + readonly 'importStepper.multipleLocations.description': 'Discovered Locations: {{length, number}}'; + readonly 'importStepper.noLocation.title': 'Create Pull Request'; + readonly 'importStepper.noLocation.createPr.detailsTitle': 'Pull Request Details'; + readonly 'importStepper.noLocation.createPr.titleLabel': 'Pull Request Title'; + readonly 'importStepper.noLocation.createPr.titlePlaceholder': 'Add Backstage catalog entity descriptor files'; + readonly 'importStepper.noLocation.createPr.bodyLabel': 'Pull Request Body'; + readonly 'importStepper.noLocation.createPr.bodyPlaceholder': 'A describing text with Markdown support'; + readonly 'importStepper.noLocation.createPr.configurationTitle': 'Entity Configuration'; + readonly 'importStepper.noLocation.createPr.componentNameLabel': 'Name of the created component'; + readonly 'importStepper.noLocation.createPr.componentNamePlaceholder': 'my-component'; + readonly 'importStepper.noLocation.createPr.ownerLoadingText': 'Loading groups…'; + readonly 'importStepper.noLocation.createPr.ownerHelperText': 'Select an owner from the list or enter a reference to a Group or a User'; + readonly 'importStepper.noLocation.createPr.ownerErrorHelperText': 'required value'; + readonly 'importStepper.noLocation.createPr.ownerLabel': 'Entity Owner'; + readonly 'importStepper.noLocation.createPr.ownerPlaceholder': 'my-group'; + readonly 'importStepper.noLocation.createPr.codeownersHelperText': 'WARNING: This may fail if no CODEOWNERS file is found at the target location.'; + readonly 'importStepper.analyze.title': 'Select URL'; + readonly 'importStepper.prepare.title': 'Import Actions'; + readonly 'importStepper.prepare.description': 'Optional'; + readonly 'importStepper.review.title': 'Review'; + readonly 'stepFinishImportLocation.repository.title': 'The following Pull Request has been opened: '; + readonly 'stepFinishImportLocation.repository.description': 'Your entities will be imported as soon as the Pull Request is merged.'; + readonly 'stepFinishImportLocation.locations.new': 'The following entities have been added to the catalog:'; + readonly 'stepFinishImportLocation.locations.backButtonText': 'Register another'; + readonly 'stepFinishImportLocation.locations.existing': 'A refresh was triggered for the following locations:'; + readonly 'stepFinishImportLocation.locations.viewButtonText': 'View Component'; + readonly 'stepFinishImportLocation.backButtonText': 'Register another'; + readonly 'stepInitAnalyzeUrl.error.default': 'Received unknown analysis result of type {{type}}. Please contact the support team.'; + readonly 'stepInitAnalyzeUrl.error.url': 'Must start with http:// or https://.'; + readonly 'stepInitAnalyzeUrl.error.repository': "Couldn't generate entities for your repository"; + readonly 'stepInitAnalyzeUrl.error.locations': 'There are no entities at this location'; + readonly 'stepInitAnalyzeUrl.urlHelperText': 'Enter the full path to your entity file to start tracking your component'; + readonly 'stepInitAnalyzeUrl.nextButtonText': 'Analyze'; + readonly 'stepPrepareCreatePullRequest.description': 'You entered a link to a {{integrationType}} repository but a {{catalogFilename}} could not be found. Use this form to open a Pull Request that creates one.'; + readonly 'stepPrepareCreatePullRequest.nextButtonText': 'Create PR'; + readonly 'stepPrepareCreatePullRequest.previewPr.title': 'Preview Pull Request'; + readonly 'stepPrepareCreatePullRequest.previewPr.subheader': 'Create a new Pull Request'; + readonly 'stepPrepareCreatePullRequest.previewCatalogInfo.title': 'Preview Entities'; + readonly 'stepPrepareSelectLocations.locations.description': 'Select one or more locations that are present in your git repository:'; + readonly 'stepPrepareSelectLocations.locations.selectAll': 'Select All'; + readonly 'stepPrepareSelectLocations.nextButtonText': 'Review'; + readonly 'stepPrepareSelectLocations.existingLocations.description': 'These locations already exist in the catalog:'; + readonly 'stepReviewLocation.refresh': 'Refresh'; + readonly 'stepReviewLocation.import': 'Import'; + readonly 'stepReviewLocation.catalog.new': 'The following entities will be added to the catalog:'; + readonly 'stepReviewLocation.catalog.exists': 'The following locations already exist in the catalog:'; + readonly 'stepReviewLocation.prepareResult.title': 'The following Pull Request has been opened: '; + readonly 'stepReviewLocation.prepareResult.description': 'You can already import the location and {{appTitle}} will fetch the entities as soon as the Pull Request is merged.'; + } +>; + // Warning: (ae-forgotten-export) The symbol "StepperProvider" needs to be exported by the entry point index.d.ts // // @public export function defaultGenerateStepper( flow: ImportFlows, defaults: StepperProvider, - t: TranslationFunction, + t: TranslationFunction, ): StepperProvider; // @public @@ -216,7 +290,7 @@ export interface ImportStepperProps { generateStepper?: ( flow: ImportFlows, defaults: StepperProvider, - t: TranslationFunction, + t: TranslationFunction, ) => StepperProvider; // (undocumented) initialUrl?: string; diff --git a/plugins/catalog-import/src/alpha.tsx b/plugins/catalog-import/src/alpha.tsx index a00eb39988240c..0a1f3b9f4e6b96 100644 --- a/plugins/catalog-import/src/alpha.tsx +++ b/plugins/catalog-import/src/alpha.tsx @@ -34,7 +34,13 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { RequirePermission } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; -export * from './translation'; +import { catalogImportTranslationRef as _catalogImportTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-catalog-import` instead. + */ +export const catalogImportTranslationRef = _catalogImportTranslationRef; // TODO: It's currently possible to override the import page with a custom one. We need to decide // whether this type of override is typically done with an input or by overriding the entire extension. @@ -91,5 +97,3 @@ export default createFrontendPlugin({ importPage: rootRouteRef, }, }); - -export { catalogImportTranslationRef } from './translation'; diff --git a/plugins/catalog-import/src/index.ts b/plugins/catalog-import/src/index.ts index 149c3e55c6e270..1174d812f73f36 100644 --- a/plugins/catalog-import/src/index.ts +++ b/plugins/catalog-import/src/index.ts @@ -27,3 +27,4 @@ export { } from './plugin'; export * from './components'; export * from './api'; +export { catalogImportTranslationRef } from './translation'; diff --git a/plugins/catalog-import/src/translation.ts b/plugins/catalog-import/src/translation.ts index 6fcb33f11ccb22..ca4d2e18faca11 100644 --- a/plugins/catalog-import/src/translation.ts +++ b/plugins/catalog-import/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const catalogImportTranslationRef = createTranslationRef({ id: 'catalog-import', messages: { diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 3cf5bc652c2b16..a5bf8d6fd55899 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -36,7 +36,7 @@ export const CatalogFilterBlueprint: ExtensionBlueprint<{ dataRefs: never; }>; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const catalogReactTranslationRef: TranslationRef< 'catalog-react', { diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md index 3ae36ce941ae51..f7f195c22641d0 100644 --- a/plugins/catalog-react/report.api.md +++ b/plugins/catalog-react/report.api.md @@ -31,6 +31,7 @@ import { SystemEntity } from '@backstage/catalog-model'; import { TableColumn } from '@backstage/core-components'; import { TableOptions } from '@backstage/core-components'; import { TextFieldProps } from '@material-ui/core/TextField'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { TypographyProps } from '@material-ui/core/Typography'; // @public (undocumented) @@ -158,6 +159,101 @@ export type CatalogReactEntitySearchBarClassKey = 'searchToolbar' | 'input'; // @public (undocumented) export type CatalogReactEntityTagPickerClassKey = 'input'; +// @public (undocumented) +export const catalogReactTranslationRef: TranslationRef< + 'catalog-react', + { + readonly 'catalogFilter.title': 'Filters'; + readonly 'catalogFilter.buttonTitle': 'Filters'; + readonly 'entityKindPicker.title': 'Kind'; + readonly 'entityKindPicker.errorMessage': 'Failed to load entity kinds'; + readonly 'entityLifecyclePicker.title': 'Lifecycle'; + readonly 'entityNamespacePicker.title': 'Namespace'; + readonly 'entityOwnerPicker.title': 'Owner'; + readonly 'entityProcessingStatusPicker.title': 'Processing Status'; + readonly 'entityTagPicker.title': 'Tags'; + readonly 'entityPeekAheadPopover.title': 'Drill into the entity to see all of the tags.'; + readonly 'entityPeekAheadPopover.entityCardActionsAriaLabel': 'Show'; + readonly 'entityPeekAheadPopover.entityCardActionsTitle': 'Show details'; + readonly 'entityPeekAheadPopover.emailCardAction.title': 'Email {{email}}'; + readonly 'entityPeekAheadPopover.emailCardAction.ariaLabel': 'Email'; + readonly 'entityPeekAheadPopover.emailCardAction.subTitle': 'mailto {{email}}'; + readonly 'entitySearchBar.placeholder': 'Search'; + readonly 'entityTypePicker.title': 'Type'; + readonly 'entityTypePicker.errorMessage': 'Failed to load entity types'; + readonly 'entityTypePicker.optionAllTitle': 'all'; + readonly 'favoriteEntity.addToFavorites': 'Add to favorites'; + readonly 'favoriteEntity.removeFromFavorites': 'Remove from favorites'; + readonly 'inspectEntityDialog.title': 'Entity Inspector'; + readonly 'inspectEntityDialog.closeButtonTitle': 'Close'; + readonly 'inspectEntityDialog.tabsAriaLabel': 'Inspector options'; + readonly 'inspectEntityDialog.ancestryPage.title': 'Ancestry'; + readonly 'inspectEntityDialog.ancestryPage.description': 'This is the ancestry of entities above the current one - as in, the chain(s) of entities down to the current one, where {{processorsLink}} child entities that ultimately led to the current one existing. Note that this is a completely different mechanism from relations.'; + readonly 'inspectEntityDialog.ancestryPage.processorsLink': 'processors emitted'; + readonly 'inspectEntityDialog.colocatedPage.title': 'Colocated'; + readonly 'inspectEntityDialog.colocatedPage.description': 'These are the entities that are colocated with this entity - as in, they originated from the same data source (e.g. came from the same YAML file), or from the same origin (e.g. the originally registered URL).'; + readonly 'inspectEntityDialog.colocatedPage.alertNoLocation': 'Entity had no location information.'; + readonly 'inspectEntityDialog.colocatedPage.alertNoEntity': 'There were no other entities on this location.'; + readonly 'inspectEntityDialog.colocatedPage.locationHeader': 'At the same location'; + readonly 'inspectEntityDialog.colocatedPage.originHeader': 'At the same origin'; + readonly 'inspectEntityDialog.jsonPage.title': 'Entity as JSON'; + readonly 'inspectEntityDialog.jsonPage.description': 'This is the raw entity data as received from the catalog, on JSON form.'; + readonly 'inspectEntityDialog.overviewPage.title': 'Overview'; + readonly 'inspectEntityDialog.overviewPage.metadata.title': 'Metadata'; + readonly 'inspectEntityDialog.overviewPage.labels': 'Labels'; + readonly 'inspectEntityDialog.overviewPage.status.title': 'Status'; + readonly 'inspectEntityDialog.overviewPage.identity.title': 'Identity'; + readonly 'inspectEntityDialog.overviewPage.annotations': 'Annotations'; + readonly 'inspectEntityDialog.overviewPage.tags': 'Tags'; + readonly 'inspectEntityDialog.overviewPage.relation.title': 'Relations'; + readonly 'inspectEntityDialog.yamlPage.title': 'Entity as YAML'; + readonly 'inspectEntityDialog.yamlPage.description': 'This is the raw entity data as received from the catalog, on YAML form.'; + readonly 'inspectEntityDialog.tabNames.json': 'Raw JSON'; + readonly 'inspectEntityDialog.tabNames.yaml': 'Raw YAML'; + readonly 'inspectEntityDialog.tabNames.overview': 'Overview'; + readonly 'inspectEntityDialog.tabNames.ancestry': 'Ancestry'; + readonly 'inspectEntityDialog.tabNames.colocated': 'Colocated'; + readonly 'unregisterEntityDialog.title': 'Are you sure you want to unregister this entity?'; + readonly 'unregisterEntityDialog.cancelButtonTitle': 'Cancel'; + readonly 'unregisterEntityDialog.deleteButtonTitle': 'Delete Entity'; + readonly 'unregisterEntityDialog.deleteEntitySuccessMessage': 'Removed entity {{entityName}}'; + readonly 'unregisterEntityDialog.onlyDeleteStateTitle': 'This entity does not seem to originate from a registered location. You therefore only have the option to delete it outright from the catalog.'; + readonly 'unregisterEntityDialog.errorStateTitle': 'Internal error: Unknown state'; + readonly 'unregisterEntityDialog.bootstrapState.title': 'You cannot unregister this entity, since it originates from a protected Backstage configuration (location "{{location}}"). If you believe this is in error, please contact the {{appTitle}} integrator.'; + readonly 'unregisterEntityDialog.bootstrapState.advancedDescription': 'You have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.'; + readonly 'unregisterEntityDialog.bootstrapState.advancedOptions': 'Advanced Options'; + readonly 'unregisterEntityDialog.unregisterState.title': 'This action will unregister the following entities:'; + readonly 'unregisterEntityDialog.unregisterState.description': 'To undo, just re-register the entity in {{appTitle}}.'; + readonly 'unregisterEntityDialog.unregisterState.subTitle': 'Located at the following location:'; + readonly 'unregisterEntityDialog.unregisterState.advancedDescription': 'You also have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.'; + readonly 'unregisterEntityDialog.unregisterState.advancedOptions': 'Advanced Options'; + readonly 'unregisterEntityDialog.unregisterState.unregisterButtonTitle': 'Unregister Location'; + readonly 'userListPicker.defaultOrgName': 'Company'; + readonly 'userListPicker.orgFilterAllLabel': 'All'; + readonly 'userListPicker.personalFilter.title': 'Personal'; + readonly 'userListPicker.personalFilter.ownedLabel': 'Owned'; + readonly 'userListPicker.personalFilter.starredLabel': 'Starred'; + readonly 'entityTableColumnTitle.name': 'Name'; + readonly 'entityTableColumnTitle.type': 'Type'; + readonly 'entityTableColumnTitle.label': 'Label'; + readonly 'entityTableColumnTitle.title': 'Title'; + readonly 'entityTableColumnTitle.description': 'Description'; + readonly 'entityTableColumnTitle.system': 'System'; + readonly 'entityTableColumnTitle.namespace': 'Namespace'; + readonly 'entityTableColumnTitle.domain': 'Domain'; + readonly 'entityTableColumnTitle.tags': 'Tags'; + readonly 'entityTableColumnTitle.owner': 'Owner'; + readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle'; + readonly 'entityTableColumnTitle.targets': 'Targets'; + readonly 'entityRelationCard.emptyHelpLinkTitle': 'Learn how to change this.'; + readonly 'missingAnnotationEmptyState.title': 'Missing Annotation'; + readonly 'missingAnnotationEmptyState.readMore': 'Read more'; + readonly 'missingAnnotationEmptyState.annotationYaml': 'Add the annotation to your {{entityKind}} YAML as shown in the highlighted example below:'; + readonly 'missingAnnotationEmptyState.generateDescription_one': 'The annotation {{annotations}} is missing. You need to add the annotation to your {{entityKind}} if you want to enable this tool.'; + readonly 'missingAnnotationEmptyState.generateDescription_other': 'The annotations {{annotations}} are missing. You need to add the annotations to your {{entityKind}} if you want to enable this tool.'; + } +>; + // @public (undocumented) export type CatalogReactUserListPickerClassKey = | 'root' diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts index a4c8daa83b0c81..dfd151da8a4ad1 100644 --- a/plugins/catalog-react/src/alpha/index.ts +++ b/plugins/catalog-react/src/alpha/index.ts @@ -16,7 +16,13 @@ export * from './blueprints'; export * from './converters'; -export { catalogReactTranslationRef } from '../translation'; +import { catalogReactTranslationRef as _catalogReactTranslationRef } from '../translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-catalog-react` instead. + */ +export const catalogReactTranslationRef = _catalogReactTranslationRef; export { isOwnerOf } from '../utils/isOwnerOf'; export { useEntityPermission } from '../hooks/useEntityPermission'; export * from '../components/EntityTable/TitleColumn'; diff --git a/plugins/catalog-react/src/index.ts b/plugins/catalog-react/src/index.ts index 2b1664abe25eff..1f58e5f6f949b5 100644 --- a/plugins/catalog-react/src/index.ts +++ b/plugins/catalog-react/src/index.ts @@ -34,3 +34,4 @@ export * from './overridableComponents'; export { getEntityRelations, getEntitySourceLocation } from './utils'; export type { EntitySourceLocation } from './utils'; export * from './deprecated'; +export { catalogReactTranslationRef } from './translation'; diff --git a/plugins/catalog-react/src/translation.ts b/plugins/catalog-react/src/translation.ts index 77e0162e8b80b2..db0c7964244d55 100644 --- a/plugins/catalog-react/src/translation.ts +++ b/plugins/catalog-react/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const catalogReactTranslationRef = createTranslationRef({ id: 'catalog-react', messages: { diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index a26143a1f62461..76539e776f44da 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -32,7 +32,7 @@ import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-rea import { SearchResultListItemBlueprintParams } from '@backstage/plugin-search-react/alpha'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const catalogTranslationRef: TranslationRef< 'catalog', { diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index cebf1fd5032c6a..84d9a8bd910f13 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -39,6 +39,7 @@ import { TableColumn } from '@backstage/core-components'; import { TableOptions } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; import { TabProps } from '@material-ui/core/Tab'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; // @public (undocumented) @@ -215,6 +216,104 @@ export interface CatalogTableRow { // @public (undocumented) export type CatalogTableToolbarClassKey = 'root' | 'text'; +// @public (undocumented) +export const catalogTranslationRef: TranslationRef< + 'catalog', + { + readonly 'deleteEntity.description': 'This entity is not referenced by any location and is therefore not receiving updates.'; + readonly 'deleteEntity.cancelButtonTitle': 'Cancel'; + readonly 'deleteEntity.deleteButtonTitle': 'Delete'; + readonly 'deleteEntity.dialogTitle': 'Are you sure you want to delete this entity?'; + readonly 'deleteEntity.actionButtonTitle': 'Delete entity'; + readonly 'indexPage.title': '{{orgName}} Catalog'; + readonly 'indexPage.createButtonTitle': 'Create'; + readonly 'indexPage.supportButtonContent': 'All your software catalog entities'; + readonly 'entityPage.notFoundMessage': 'There is no {{kind}} with the requested {{link}}.'; + readonly 'entityPage.notFoundLinkText': 'kind, namespace, and name'; + readonly 'aboutCard.title': 'About'; + readonly 'aboutCard.unknown': 'unknown'; + readonly 'aboutCard.refreshButtonTitle': 'Schedule entity refresh'; + readonly 'aboutCard.editButtonTitle': 'Edit Metadata'; + readonly 'aboutCard.editButtonAriaLabel': 'Edit'; + readonly 'aboutCard.createSimilarButtonTitle': 'Create something similar'; + readonly 'aboutCard.refreshScheduledMessage': 'Refresh scheduled'; + readonly 'aboutCard.refreshButtonAriaLabel': 'Refresh'; + readonly 'aboutCard.launchTemplate': 'Launch Template'; + readonly 'aboutCard.viewTechdocs': 'View TechDocs'; + readonly 'aboutCard.viewSource': 'View Source'; + readonly 'aboutCard.descriptionField.value': 'No description'; + readonly 'aboutCard.descriptionField.label': 'Description'; + readonly 'aboutCard.ownerField.value': 'No Owner'; + readonly 'aboutCard.ownerField.label': 'Owner'; + readonly 'aboutCard.domainField.value': 'No Domain'; + readonly 'aboutCard.domainField.label': 'Domain'; + readonly 'aboutCard.systemField.value': 'No System'; + readonly 'aboutCard.systemField.label': 'System'; + readonly 'aboutCard.parentComponentField.value': 'No Parent Component'; + readonly 'aboutCard.parentComponentField.label': 'Parent Component'; + readonly 'aboutCard.typeField.label': 'Type'; + readonly 'aboutCard.lifecycleField.label': 'Lifecycle'; + readonly 'aboutCard.tagsField.value': 'No Tags'; + readonly 'aboutCard.tagsField.label': 'Tags'; + readonly 'aboutCard.targetsField.label': 'Targets'; + readonly 'searchResultItem.type': 'Type'; + readonly 'searchResultItem.kind': 'Kind'; + readonly 'searchResultItem.owner': 'Owner'; + readonly 'searchResultItem.lifecycle': 'Lifecycle'; + readonly 'catalogTable.allFilters': 'All'; + readonly 'catalogTable.warningPanelTitle': 'Could not fetch catalog entities.'; + readonly 'catalogTable.viewActionTitle': 'View'; + readonly 'catalogTable.editActionTitle': 'Edit'; + readonly 'catalogTable.starActionTitle': 'Add to favorites'; + readonly 'catalogTable.unStarActionTitle': 'Remove from favorites'; + readonly 'dependencyOfComponentsCard.title': 'Dependency of components'; + readonly 'dependencyOfComponentsCard.emptyMessage': 'No component depends on this component.'; + readonly 'dependsOnComponentsCard.title': 'Depends on components'; + readonly 'dependsOnComponentsCard.emptyMessage': 'No component is a dependency of this component.'; + readonly 'dependsOnResourcesCard.title': 'Depends on resources'; + readonly 'dependsOnResourcesCard.emptyMessage': 'No resource is a dependency of this component.'; + readonly 'entityContextMenu.copiedMessage': 'Copied!'; + readonly 'entityContextMenu.moreButtonTitle': 'More'; + readonly 'entityContextMenu.inspectMenuTitle': 'Inspect entity'; + readonly 'entityContextMenu.copyURLMenuTitle': 'Copy entity URL'; + readonly 'entityContextMenu.unregisterMenuTitle': 'Unregister entity'; + readonly 'entityContextMenu.moreButtonAriaLabel': 'more'; + readonly 'entityLabelsCard.title': 'Labels'; + readonly 'entityLabelsCard.readMoreButtonTitle': 'Read more'; + readonly 'entityLabelsCard.columnKeyLabel': 'Label'; + readonly 'entityLabelsCard.columnValueLabel': 'Value'; + readonly 'entityLabelsCard.emptyDescription': 'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:'; + readonly 'entityLabels.ownerLabel': 'Owner'; + readonly 'entityLabels.warningPanelTitle': 'Entity not found'; + readonly 'entityLabels.lifecycleLabel': 'Lifecycle'; + readonly 'entityLinksCard.title': 'Links'; + readonly 'entityLinksCard.readMoreButtonTitle': 'Read more'; + readonly 'entityLinksCard.emptyDescription': 'No links defined for this entity. You can add links to your entity YAML as shown in the highlighted example below:'; + readonly 'entityNotFound.title': 'Entity was not found'; + readonly 'entityNotFound.description': 'Want to help us build this? Check out our Getting Started documentation.'; + readonly 'entityNotFound.docButtonTitle': 'DOCS'; + readonly 'entityTabs.tabsAriaLabel': 'Tabs'; + readonly entityProcessingErrorsDescription: 'The error below originates from'; + readonly entityRelationWarningDescription: "This entity has relations to other entities, which can't be found in the catalog.\n Entities not found are: "; + readonly 'hasComponentsCard.title': 'Has components'; + readonly 'hasComponentsCard.emptyMessage': 'No component is part of this system.'; + readonly 'hasResourcesCard.title': 'Has resources'; + readonly 'hasResourcesCard.emptyMessage': 'No resource is part of this system.'; + readonly 'hasSubcomponentsCard.title': 'Has subcomponents'; + readonly 'hasSubcomponentsCard.emptyMessage': 'No subcomponent is part of this component.'; + readonly 'hasSubdomainsCard.title': 'Has subdomains'; + readonly 'hasSubdomainsCard.emptyMessage': 'No subdomain is part of this domain.'; + readonly 'hasSystemsCard.title': 'Has systems'; + readonly 'hasSystemsCard.emptyMessage': 'No system is part of this domain.'; + readonly 'relatedEntitiesCard.emptyHelpLinkTitle': 'Learn how to change this.'; + readonly 'systemDiagramCard.title': 'System Diagram'; + readonly 'systemDiagramCard.description': 'Use pinch & zoom to move around the diagram.'; + readonly 'systemDiagramCard.edgeLabels.dependsOn': 'depends on'; + readonly 'systemDiagramCard.edgeLabels.partOf': 'part of'; + readonly 'systemDiagramCard.edgeLabels.provides': 'provides'; + } +>; + // @public (undocumented) export type ColumnBreakpoints = Record; diff --git a/plugins/catalog/src/alpha/index.ts b/plugins/catalog/src/alpha/index.ts index c9f4c8c4b7ee33..a563a0ee906a9a 100644 --- a/plugins/catalog/src/alpha/index.ts +++ b/plugins/catalog/src/alpha/index.ts @@ -16,4 +16,10 @@ export { default } from './plugin'; -export * from './translation'; +import { catalogTranslationRef as _catalogTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-catalog` instead. + */ +export const catalogTranslationRef = _catalogTranslationRef; diff --git a/plugins/catalog/src/alpha/translation.ts b/plugins/catalog/src/alpha/translation.ts index 240dea64071475..19738844e4e243 100644 --- a/plugins/catalog/src/alpha/translation.ts +++ b/plugins/catalog/src/alpha/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const catalogTranslationRef = createTranslationRef({ id: 'catalog', messages: { diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 303677b68ac87b..2c1e81f803c162 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -100,3 +100,4 @@ export type { } from './components/HasSystemsCard'; export type { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; export type { CatalogSearchResultListItemProps } from './components/CatalogSearchResultListItem'; +export { catalogTranslationRef } from './alpha/translation'; diff --git a/plugins/home-react/report-alpha.api.md b/plugins/home-react/report-alpha.api.md index 0de77172a05f4b..f8d2b7cb350b0b 100644 --- a/plugins/home-react/report-alpha.api.md +++ b/plugins/home-react/report-alpha.api.md @@ -125,7 +125,7 @@ export const homePageWidgetDataRef: ConfigurableExtensionDataRef< {} >; -// @alpha +// @public @deprecated (undocumented) export const homeReactTranslationRef: TranslationRef< 'home-react', { @@ -134,4 +134,6 @@ export const homeReactTranslationRef: TranslationRef< readonly 'cardExtension.settingsButtonTitle': 'Settings'; } >; + +// (No @packageDocumentation comment for this package) ``` diff --git a/plugins/home-react/report.api.md b/plugins/home-react/report.api.md index 26ef6267aa9790..0444010dea5bb0 100644 --- a/plugins/home-react/report.api.md +++ b/plugins/home-react/report.api.md @@ -9,6 +9,7 @@ import { JSX as JSX_3 } from 'react'; import { Overrides } from '@material-ui/core/styles/overrides'; import { RJSFSchema } from '@rjsf/utils'; import { StyleRules } from '@material-ui/core/styles/withStyles'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UiSchema } from '@rjsf/utils'; // @public (undocumented) @@ -81,6 +82,16 @@ export function createCardExtension(options: { settings?: CardSettings; }): Extension<(props: CardExtensionProps) => JSX_2.Element>; +// @public +export const homeReactTranslationRef: TranslationRef< + 'home-react', + { + readonly 'settingsModal.title': 'Settings'; + readonly 'settingsModal.closeButtonTitle': 'Close'; + readonly 'cardExtension.settingsButtonTitle': 'Settings'; + } +>; + // @public (undocumented) export type PluginHomeComponentsNameToClassKey = { PluginHomeContentModal: PluginHomeContentModalClassKey; diff --git a/plugins/home-react/src/alpha.ts b/plugins/home-react/src/alpha.ts index 00b7a7bed740b6..677e27f5e1c1a8 100644 --- a/plugins/home-react/src/alpha.ts +++ b/plugins/home-react/src/alpha.ts @@ -23,7 +23,13 @@ * * @packageDocumentation */ -export { homeReactTranslationRef } from './translation'; +import { homeReactTranslationRef as _homeReactTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-home-react` instead. + */ +export const homeReactTranslationRef = _homeReactTranslationRef; export { HomePageWidgetBlueprint, type HomePageWidgetBlueprintParams, diff --git a/plugins/home-react/src/index.ts b/plugins/home-react/src/index.ts index 54602dc8146a51..f24d85b2b813e0 100644 --- a/plugins/home-react/src/index.ts +++ b/plugins/home-react/src/index.ts @@ -31,3 +31,4 @@ export type { CardConfig, } from './extensions'; export * from './overridableComponents'; +export { homeReactTranslationRef } from './translation'; diff --git a/plugins/home-react/src/translation.ts b/plugins/home-react/src/translation.ts index 1a8fd9f1bf3e39..ab67d4b37e6a65 100644 --- a/plugins/home-react/src/translation.ts +++ b/plugins/home-react/src/translation.ts @@ -17,9 +17,9 @@ import { createTranslationRef } from '@backstage/frontend-plugin-api'; /** * Translation reference for the home-react plugin. - * Contains localized text strings for home page components and settings modals. + * Contains localized text strings for home page components and widgets. * - * @alpha + * @public */ export const homeReactTranslationRef = createTranslationRef({ id: 'home-react', diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index 0269f471fe0de4..6b1f89c779e8a4 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -206,7 +206,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @alpha +// @public @deprecated (undocumented) export const homeTranslationRef: TranslationRef< 'home', { diff --git a/plugins/home/report.api.md b/plugins/home/report.api.md index a90d5755acb993..2d5f8ddeccba0c 100644 --- a/plugins/home/report.api.md +++ b/plugins/home/report.api.md @@ -23,6 +23,7 @@ import { ReactNode } from 'react'; import { RendererProps as RendererProps_2 } from '@backstage/plugin-home-react'; import { RouteRef } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { Variant } from '@material-ui/core/styles/createTypography'; // @public @@ -176,6 +177,39 @@ export const homePlugin: BackstagePlugin< {} >; +// @public +export const homeTranslationRef: TranslationRef< + 'home', + { + readonly 'starredEntities.noStarredEntitiesMessage': 'Click the star beside an entity name to add it to this list!'; + readonly 'addWidgetDialog.title': 'Add new widget to dashboard'; + readonly 'customHomepageButtons.cancel': 'Cancel'; + readonly 'customHomepageButtons.clearAll': 'Clear all'; + readonly 'customHomepageButtons.edit': 'Edit'; + readonly 'customHomepageButtons.restoreDefaults': 'Restore defaults'; + readonly 'customHomepageButtons.addWidget': 'Add widget'; + readonly 'customHomepageButtons.save': 'Save'; + readonly 'customHomepage.noWidgets': "No widgets added. Start by clicking the 'Add widget' button."; + readonly 'widgetSettingsOverlay.cancelButtonTitle': 'Cancel'; + readonly 'widgetSettingsOverlay.editSettingsTooptip': 'Edit settings'; + readonly 'widgetSettingsOverlay.deleteWidgetTooltip': 'Delete widget'; + readonly 'widgetSettingsOverlay.submitButtonTitle': 'Submit'; + readonly 'starredEntityListItem.removeFavoriteEntityTitle': 'Remove entity from favorites'; + readonly 'visitList.empty.title': 'There are no visits to show yet.'; + readonly 'visitList.empty.description': 'Once you start using Backstage, your visits will appear here as a quick link to carry on where you left off.'; + readonly 'visitList.few.title': 'The more pages you visit, the more pages will appear here.'; + readonly 'quickStart.title': 'Onboarding'; + readonly 'quickStart.description': 'Get started with Backstage'; + readonly 'quickStart.learnMoreLinkTitle': 'Learn more'; + readonly 'visitedByType.action.viewMore': 'View more'; + readonly 'visitedByType.action.viewLess': 'View less'; + readonly 'featuredDocsCard.empty.title': 'No documents to show'; + readonly 'featuredDocsCard.empty.description': 'Create your own document. Check out our Getting Started Information'; + readonly 'featuredDocsCard.empty.learnMoreLinkTitle': 'DOCS'; + readonly 'featuredDocsCard.learnMoreTitle': 'LEARN MORE'; + } +>; + // @public export const isOperator: (s: string) => s is Operators; diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index 194f322317c4de..d4f350693d2e0c 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -225,4 +225,10 @@ export default createFrontendPlugin({ }, }); -export { homeTranslationRef } from './translation'; +import { homeTranslationRef as _homeTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-home` instead. + */ +export const homeTranslationRef = _homeTranslationRef; diff --git a/plugins/home/src/index.ts b/plugins/home/src/index.ts index 3a6f152d03efdc..35bd43460deac3 100644 --- a/plugins/home/src/index.ts +++ b/plugins/home/src/index.ts @@ -42,3 +42,4 @@ export * from './assets'; export * from './homePageComponents'; export * from './deprecated'; export * from './api'; +export { homeTranslationRef } from './translation'; diff --git a/plugins/home/src/translation.ts b/plugins/home/src/translation.ts index 480e2850b912ac..89cb0756d44e19 100644 --- a/plugins/home/src/translation.ts +++ b/plugins/home/src/translation.ts @@ -19,7 +19,7 @@ import { createTranslationRef } from '@backstage/frontend-plugin-api'; * Translation reference for the home plugin. * Contains localized text strings for home page components and widgets. * - * @alpha + * @public */ export const homeTranslationRef = createTranslationRef({ id: 'home', diff --git a/plugins/kubernetes-cluster/report-alpha.api.md b/plugins/kubernetes-cluster/report-alpha.api.md index 2ce9c33c47ecfc..2220ffb5fe7553 100644 --- a/plugins/kubernetes-cluster/report-alpha.api.md +++ b/plugins/kubernetes-cluster/report-alpha.api.md @@ -5,7 +5,7 @@ ```ts import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const kubernetesClusterTranslationRef: TranslationRef< 'kubernetes-cluster', { diff --git a/plugins/kubernetes-cluster/report.api.md b/plugins/kubernetes-cluster/report.api.md index 2c9a4c93ef972f..29669f19a828bb 100644 --- a/plugins/kubernetes-cluster/report.api.md +++ b/plugins/kubernetes-cluster/report.api.md @@ -5,6 +5,7 @@ ```ts import { Entity } from '@backstage/catalog-model'; import { JSX as JSX_2 } from 'react/jsx-runtime'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public export const EntityKubernetesClusterContent: ( @@ -17,6 +18,15 @@ export type EntityKubernetesClusterContentProps = {}; // @public (undocumented) export const isKubernetesClusterAvailable: (entity: Entity) => boolean; +// @public (undocumented) +export const kubernetesClusterTranslationRef: TranslationRef< + 'kubernetes-cluster', + { + readonly 'kubernetesClusterContentPage.permissionAlert.message': "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' permission."; + readonly 'kubernetesClusterContentPage.permissionAlert.title': 'Permission required'; + } +>; + // @public (undocumented) export const Router: () => JSX_2.Element; ``` diff --git a/plugins/kubernetes-cluster/src/alpha.ts b/plugins/kubernetes-cluster/src/alpha.ts index e49b7ac9c9c4f1..a2d816dc361835 100644 --- a/plugins/kubernetes-cluster/src/alpha.ts +++ b/plugins/kubernetes-cluster/src/alpha.ts @@ -14,4 +14,10 @@ * limitations under the License. */ -export { kubernetesClusterTranslationRef } from './translation'; +import { kubernetesClusterTranslationRef as _kubernetesClusterTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-kubernetes-cluster` instead. + */ +export const kubernetesClusterTranslationRef = _kubernetesClusterTranslationRef; diff --git a/plugins/kubernetes-cluster/src/index.ts b/plugins/kubernetes-cluster/src/index.ts index 6171b151e49a49..650101880c2ed8 100644 --- a/plugins/kubernetes-cluster/src/index.ts +++ b/plugins/kubernetes-cluster/src/index.ts @@ -25,3 +25,4 @@ export { type EntityKubernetesClusterContentProps, } from './plugin'; export { Router, isKubernetesClusterAvailable } from './Router'; +export { kubernetesClusterTranslationRef } from './translation'; diff --git a/plugins/kubernetes-cluster/src/translation.ts b/plugins/kubernetes-cluster/src/translation.ts index ba99c6744eff31..aac274a6176696 100644 --- a/plugins/kubernetes-cluster/src/translation.ts +++ b/plugins/kubernetes-cluster/src/translation.ts @@ -15,7 +15,7 @@ */ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const kubernetesClusterTranslationRef = createTranslationRef({ id: 'kubernetes-cluster', messages: { diff --git a/plugins/kubernetes-react/report-alpha.api.md b/plugins/kubernetes-react/report-alpha.api.md index f638bf67895252..b7b8c5fcb1a98c 100644 --- a/plugins/kubernetes-react/report-alpha.api.md +++ b/plugins/kubernetes-react/report-alpha.api.md @@ -5,7 +5,7 @@ ```ts import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const kubernetesReactTranslationRef: TranslationRef< 'kubernetes-react', { diff --git a/plugins/kubernetes-react/report.api.md b/plugins/kubernetes-react/report.api.md index bb8057b2e8e737..e9631fac38aa9b 100644 --- a/plugins/kubernetes-react/report.api.md +++ b/plugins/kubernetes-react/report.api.md @@ -34,6 +34,7 @@ import { Pod } from 'kubernetes-models/v1'; import { Pod as Pod_2 } from 'kubernetes-models/v1/Pod'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { ReactNode } from 'react'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import type { TypeMeta } from '@kubernetes-models/base'; import type { V1Job } from '@kubernetes/client-node'; import type { V1ObjectMeta } from '@kubernetes/client-node'; @@ -554,6 +555,91 @@ export class KubernetesProxyClient { }>; } +// @public (undocumented) +export const kubernetesReactTranslationRef: TranslationRef< + 'kubernetes-react', + { + readonly 'namespace.label': 'namespace:'; + readonly 'namespace.labelWithValue': 'namespace: {{namespace}}'; + readonly 'events.noEventsFound': 'No events found'; + readonly 'events.eventTooltip': '{{eventType}} event'; + readonly 'events.firstEvent': 'First event {{timeAgo}} (count: {{count}})'; + readonly 'cluster.label': 'Cluster'; + readonly 'cluster.pods': 'pods'; + readonly 'cluster.pods_one': '{{count}} pod'; + readonly 'cluster.pods_other': '{{count}} pods'; + readonly 'cluster.podsWithErrors': 'pods with errors'; + readonly 'cluster.podsWithErrors_one': '{{count}} pod with errors'; + readonly 'cluster.podsWithErrors_other': '{{count}} pods with errors'; + readonly 'cluster.noPodsWithErrors': 'No pods with errors'; + readonly 'pods.pods_one': '{{count}} pod'; + readonly 'pods.pods_other': '{{count}} pods'; + readonly 'podsTable.columns.name': 'name'; + readonly 'podsTable.columns.id': 'ID'; + readonly 'podsTable.columns.status': 'status'; + readonly 'podsTable.columns.phase': 'phase'; + readonly 'podsTable.columns.containersReady': 'containers ready'; + readonly 'podsTable.columns.totalRestarts': 'total restarts'; + readonly 'podsTable.columns.cpuUsage': 'CPU usage %'; + readonly 'podsTable.columns.memoryUsage': 'Memory usage %'; + readonly 'podsTable.unknown': 'unknown'; + readonly 'podsTable.status.running': 'Running'; + readonly 'podsTable.status.ok': 'OK'; + readonly 'errorPanel.message': 'There was a problem retrieving some Kubernetes resources for the entity: {{entityName}}. This could mean that the Error Reporting card is not completely accurate.'; + readonly 'errorPanel.title': 'There was a problem retrieving Kubernetes objects'; + readonly 'errorPanel.errorsLabel': 'Errors'; + readonly 'errorPanel.clusterLabel': 'Cluster'; + readonly 'errorPanel.clusterLabelValue': 'Cluster: {{cluster}}'; + readonly 'errorPanel.fetchError': 'Error communicating with Kubernetes: {{errorType}}, message: {{message}}'; + readonly 'errorPanel.resourceError': "Error fetching Kubernetes resource: '{{resourcePath}}', error: {{errorType}}, status code: {{statusCode}}"; + readonly 'fixDialog.title': '{{podName}} - {{errorType}}'; + readonly 'fixDialog.events': 'Events:'; + readonly 'fixDialog.helpButton': 'Help'; + readonly 'fixDialog.detectedError': 'Detected error:'; + readonly 'fixDialog.causeExplanation': 'Cause explanation:'; + readonly 'fixDialog.fix': 'Fix:'; + readonly 'fixDialog.crashLogs': 'Crash logs:'; + readonly 'fixDialog.openDocs': 'Open docs'; + readonly 'fixDialog.ariaLabels.close': 'close'; + readonly 'fixDialog.ariaLabels.fixIssue': 'fix issue'; + readonly 'podDrawer.buttons.delete': 'Delete Pod'; + readonly 'podDrawer.cpuRequests': 'CPU requests'; + readonly 'podDrawer.cpuLimits': 'CPU limits'; + readonly 'podDrawer.memoryRequests': 'Memory requests'; + readonly 'podDrawer.memoryLimits': 'Memory limits'; + readonly 'podDrawer.resourceUtilization': 'Resource utilization'; + readonly 'hpa.minReplicas': 'min replicas'; + readonly 'hpa.maxReplicas': 'max replicas'; + readonly 'hpa.replicasSummary': 'min replicas {{min}} / max replicas {{max}}'; + readonly 'hpa.currentCpuUsage': 'current CPU usage:'; + readonly 'hpa.currentCpuUsageLabel': 'current CPU usage: {{value}}%'; + readonly 'hpa.targetCpuUsage': 'target CPU usage:'; + readonly 'hpa.targetCpuUsageLabel': 'target CPU usage: {{value}}%'; + readonly 'errorReporting.columns.name': 'name'; + readonly 'errorReporting.columns.kind': 'kind'; + readonly 'errorReporting.columns.namespace': 'namespace'; + readonly 'errorReporting.columns.messages': 'messages'; + readonly 'errorReporting.columns.cluster': 'cluster'; + readonly 'errorReporting.title': 'Error Reporting'; + readonly 'podLogs.title': 'No logs emitted'; + readonly 'podLogs.description': 'No logs were emitted by the container'; + readonly 'podLogs.buttonText': 'Logs'; + readonly 'podLogs.titleTemplate': '{{podName}} - {{containerName}} logs on cluster {{clusterName}}'; + readonly 'podLogs.buttonAriaLabel': 'get logs'; + readonly 'podExecTerminal.buttonText': 'Terminal'; + readonly 'podExecTerminal.titleTemplate': '{{podName}} - {{containerName}} terminal shell on cluster {{clusterName}}'; + readonly 'podExecTerminal.buttonAriaLabel': 'open terminal'; + readonly 'kubernetesDrawer.yaml': 'YAML'; + readonly 'kubernetesDrawer.closeDrawer': 'Close the drawer'; + readonly 'kubernetesDrawer.managedFields': 'Managed Fields'; + readonly 'kubernetesDrawer.unknownName': 'unknown name'; + readonly 'linkErrorPanel.message': "Could not format the link to the dashboard of your cluster named '{{clusterName}}'. Its dashboardApp property has been set to '{{dashboardApp}}.'"; + readonly 'linkErrorPanel.title': 'There was a problem formatting the link to the Kubernetes dashboard'; + readonly 'linkErrorPanel.errorsLabel': 'Errors:'; + readonly 'kubernetesDialog.closeAriaLabel': 'close'; + } +>; + // @public (undocumented) export const KubernetesStructuredMetadataTableDrawer: < T extends KubernetesDrawerable, diff --git a/plugins/kubernetes-react/src/alpha.ts b/plugins/kubernetes-react/src/alpha.ts index 411bd788786431..212f3b3e96c307 100644 --- a/plugins/kubernetes-react/src/alpha.ts +++ b/plugins/kubernetes-react/src/alpha.ts @@ -14,6 +14,10 @@ * limitations under the License. */ -import { kubernetesReactTranslationRef } from './translation'; +import { kubernetesReactTranslationRef as _kubernetesReactTranslationRef } from './translation'; -export { kubernetesReactTranslationRef }; +/** + * @public + * @deprecated Import from `@backstage/plugin-kubernetes-react` instead. + */ +export const kubernetesReactTranslationRef = _kubernetesReactTranslationRef; diff --git a/plugins/kubernetes-react/src/index.ts b/plugins/kubernetes-react/src/index.ts index 326b363b052eb9..e52ac76d498d47 100644 --- a/plugins/kubernetes-react/src/index.ts +++ b/plugins/kubernetes-react/src/index.ts @@ -28,3 +28,4 @@ export * from './api'; export * from './kubernetes-auth-provider'; export * from './components'; export * from './types'; +export { kubernetesReactTranslationRef } from './translation'; diff --git a/plugins/kubernetes-react/src/translation.ts b/plugins/kubernetes-react/src/translation.ts index 0420cc04bf3d44..bc8f3d99dadc28 100644 --- a/plugins/kubernetes-react/src/translation.ts +++ b/plugins/kubernetes-react/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const kubernetesReactTranslationRef = createTranslationRef({ id: 'kubernetes-react', messages: { diff --git a/plugins/kubernetes/report-alpha.api.md b/plugins/kubernetes/report-alpha.api.md index 708697672479fa..d114524b8966fe 100644 --- a/plugins/kubernetes/report-alpha.api.md +++ b/plugins/kubernetes/report-alpha.api.md @@ -240,7 +240,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const kubernetesTranslationRef: TranslationRef< 'kubernetes', { diff --git a/plugins/kubernetes/report.api.md b/plugins/kubernetes/report.api.md index b9450e650d748b..8a6b02a7aa1342 100644 --- a/plugins/kubernetes/report.api.md +++ b/plugins/kubernetes/report.api.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import { RouteRef } from '@backstage/core-plugin-api'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // Warning: (ae-missing-release-tag) "EntityKubernetesContent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -37,6 +38,19 @@ const kubernetesPlugin: BackstagePlugin< export { kubernetesPlugin }; export { kubernetesPlugin as plugin }; +// @public (undocumented) +export const kubernetesTranslationRef: TranslationRef< + 'kubernetes', + { + readonly 'entityContent.title': 'Kubernetes'; + readonly 'kubernetesContentPage.title': 'Your Clusters'; + readonly 'kubernetesContentPage.emptyState.title': 'No Kubernetes resources'; + readonly 'kubernetesContentPage.emptyState.description': 'No resources on any known clusters for {{entityName}}'; + readonly 'kubernetesContentPage.permissionAlert.message': "To view Kubernetes objects, contact your portal administrator to give you the 'kubernetes.clusters.read' and 'kubernetes.resources.read' permission."; + readonly 'kubernetesContentPage.permissionAlert.title': 'Permission required'; + } +>; + // Warning: (ae-missing-release-tag) "Router" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/kubernetes/src/alpha/index.ts b/plugins/kubernetes/src/alpha/index.ts index 23888f968f9d33..02fad57851f783 100644 --- a/plugins/kubernetes/src/alpha/index.ts +++ b/plugins/kubernetes/src/alpha/index.ts @@ -13,5 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { kubernetesTranslationRef } from './translation'; +import { kubernetesTranslationRef as _kubernetesTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-kubernetes` instead. + */ +export const kubernetesTranslationRef = _kubernetesTranslationRef; export { default } from './plugin'; diff --git a/plugins/kubernetes/src/alpha/translation.ts b/plugins/kubernetes/src/alpha/translation.ts index 238bd3b0840704..17ebe90de07408 100644 --- a/plugins/kubernetes/src/alpha/translation.ts +++ b/plugins/kubernetes/src/alpha/translation.ts @@ -15,7 +15,7 @@ */ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const kubernetesTranslationRef = createTranslationRef({ id: 'kubernetes', messages: { diff --git a/plugins/kubernetes/src/index.ts b/plugins/kubernetes/src/index.ts index 3651a861fc2e2a..54abb07bc3f126 100644 --- a/plugins/kubernetes/src/index.ts +++ b/plugins/kubernetes/src/index.ts @@ -29,3 +29,4 @@ export type { EntityKubernetesContentProps } from './plugin'; export { Router, isKubernetesAvailable } from './Router'; // TODO remove this re-export as a breaking change after a couple of releases export * from '@backstage/plugin-kubernetes-react'; +export { kubernetesTranslationRef } from './alpha/translation'; diff --git a/plugins/notifications/report-alpha.api.md b/plugins/notifications/report-alpha.api.md index e1feed0cdef647..84731628ae0fea 100644 --- a/plugins/notifications/report-alpha.api.md +++ b/plugins/notifications/report-alpha.api.md @@ -120,7 +120,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const notificationsTranslationRef: TranslationRef< 'plugin.notifications', { diff --git a/plugins/notifications/report.api.md b/plugins/notifications/report.api.md index 2c056d4e91fe8f..21dc817d91fe73 100644 --- a/plugins/notifications/report.api.md +++ b/plugins/notifications/report.api.md @@ -15,6 +15,7 @@ import { NotificationSeverity } from '@backstage/plugin-notifications-common'; import { NotificationStatus } from '@backstage/plugin-notifications-common'; import { RouteRef } from '@backstage/core-plugin-api'; import { TableProps } from '@backstage/core-components'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) export type GetNotificationsCommonOptions = { @@ -187,6 +188,71 @@ export type NotificationsTableProps = Pick< pageSize: number; }; +// @public (undocumented) +export const notificationsTranslationRef: TranslationRef< + 'plugin.notifications', + { + readonly 'table.errors.markAllReadFailed': 'Failed to mark all notifications as read'; + readonly 'table.pagination.labelDisplayedRows': '{from}-{to} of {count}'; + readonly 'table.pagination.firstTooltip': 'First Page'; + readonly 'table.pagination.labelRowsSelect': 'rows'; + readonly 'table.pagination.lastTooltip': 'Last Page'; + readonly 'table.pagination.nextTooltip': 'Next Page'; + readonly 'table.pagination.previousTooltip': 'Previous Page'; + readonly 'table.emptyMessage': 'No records to display'; + readonly 'table.bulkActions.markAllRead': 'Mark all read'; + readonly 'table.bulkActions.markSelectedAsRead': 'Mark selected as read'; + readonly 'table.bulkActions.returnSelectedAmongUnread': 'Return selected among unread'; + readonly 'table.bulkActions.saveSelectedForLater': 'Save selected for later'; + readonly 'table.bulkActions.undoSaveForSelected': 'Undo save for selected'; + readonly 'table.confirmDialog.title': 'Are you sure?'; + readonly 'table.confirmDialog.markAllReadDescription': 'Mark all notifications as read.'; + readonly 'table.confirmDialog.markAllReadConfirmation': 'Mark All'; + readonly 'filters.view.all': 'All'; + readonly 'filters.view.label': 'View'; + readonly 'filters.view.read': 'Read notifications'; + readonly 'filters.view.saved': 'Saved'; + readonly 'filters.view.unread': 'Unread notifications'; + readonly 'filters.title': 'Filters'; + readonly 'filters.severity.normal': 'Normal'; + readonly 'filters.severity.high': 'High'; + readonly 'filters.severity.low': 'Low'; + readonly 'filters.severity.label': 'Min severity'; + readonly 'filters.severity.critical': 'Critical'; + readonly 'filters.topic.label': 'Topic'; + readonly 'filters.topic.anyTopic': 'Any topic'; + readonly 'filters.createdAfter.label': 'Sent out'; + readonly 'filters.createdAfter.placeholder': 'Notifications since'; + readonly 'filters.createdAfter.last24h': 'Last 24h'; + readonly 'filters.createdAfter.lastWeek': 'Last week'; + readonly 'filters.createdAfter.anyTime': 'Any time'; + readonly 'filters.sortBy.origin': 'Origin'; + readonly 'filters.sortBy.label': 'Sort by'; + readonly 'filters.sortBy.placeholder': 'Field to sort by'; + readonly 'filters.sortBy.newest': 'Newest on top'; + readonly 'filters.sortBy.oldest': 'Oldest on top'; + readonly 'filters.sortBy.topic': 'Topic'; + readonly 'settings.table.origin': 'Origin'; + readonly 'settings.table.topic': 'Topic'; + readonly 'settings.title': 'Notification settings'; + readonly 'settings.errors.useNotificationFormat': 'useNotificationFormat must be used within a NotificationFormatProvider'; + readonly 'settings.errorTitle': 'Failed to load settings'; + readonly 'settings.noSettingsAvailable': 'No notification settings available, check back later'; + readonly 'sidebar.title': 'Notifications'; + readonly 'sidebar.errors.markAsReadFailed': 'Failed to mark notification as read'; + readonly 'sidebar.errors.fetchNotificationFailed': 'Failed to fetch notification'; + readonly 'notificationsPage.title': 'Notifications'; + readonly 'notificationsPage.tableTitle.all_one': 'All notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.all_other': 'All notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.saved_one': 'Saved notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.saved_other': 'Saved notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.unread_one': 'Unread notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.unread_other': 'Unread notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.read_one': 'Read notifications ({{count}})'; + readonly 'notificationsPage.tableTitle.read_other': 'Read notifications ({{count}})'; + } +>; + // @public (undocumented) export type UpdateNotificationsOptions = { ids: string[]; diff --git a/plugins/notifications/src/alpha.tsx b/plugins/notifications/src/alpha.tsx index b930373a461024..9c2196e5254c54 100644 --- a/plugins/notifications/src/alpha.tsx +++ b/plugins/notifications/src/alpha.tsx @@ -56,4 +56,10 @@ export default createFrontendPlugin({ extensions: [page, api], }); -export { notificationsTranslationRef } from './translation'; +import { notificationsTranslationRef as _notificationsTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-notifications` instead. + */ +export const notificationsTranslationRef = _notificationsTranslationRef; diff --git a/plugins/notifications/src/index.ts b/plugins/notifications/src/index.ts index 25565f1ae123ad..b67ae9b108a27e 100644 --- a/plugins/notifications/src/index.ts +++ b/plugins/notifications/src/index.ts @@ -17,3 +17,4 @@ export { notificationsPlugin, NotificationsPage } from './plugin'; export * from './api'; export { useNotificationsApi } from './hooks'; export * from './components'; +export { notificationsTranslationRef } from './translation'; diff --git a/plugins/notifications/src/translation.ts b/plugins/notifications/src/translation.ts index d810ed9966697b..f8c2749c4205df 100644 --- a/plugins/notifications/src/translation.ts +++ b/plugins/notifications/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/frontend-plugin-api'; -/** @alpha */ +/** @public */ export const notificationsTranslationRef = createTranslationRef({ id: 'plugin.notifications', messages: { diff --git a/plugins/org/report-alpha.api.md b/plugins/org/report-alpha.api.md index 44fd429bfac644..0b8d974be53378 100644 --- a/plugins/org/report-alpha.api.md +++ b/plugins/org/report-alpha.api.md @@ -202,7 +202,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const orgTranslationRef: TranslationRef< 'org', { diff --git a/plugins/org/report.api.md b/plugins/org/report.api.md index d51a45e3f7e208..2fa5e7a2851dbe 100644 --- a/plugins/org/report.api.md +++ b/plugins/org/report.api.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react/jsx-runtime'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) export type ComponentsGridClassKey = @@ -85,6 +86,35 @@ const orgPlugin: BackstagePlugin< export { orgPlugin }; export { orgPlugin as plugin }; +// @public (undocumented) +export const orgTranslationRef: TranslationRef< + 'org', + { + readonly 'groupProfileCard.groupNotFound': 'Group not found'; + readonly 'groupProfileCard.editIconButtonTitle': 'Edit Metadata'; + readonly 'groupProfileCard.refreshIconButtonTitle': 'Schedule entity refresh'; + readonly 'groupProfileCard.refreshIconButtonAriaLabel': 'Refresh'; + readonly 'groupProfileCard.listItemTitle.email': 'Email'; + readonly 'groupProfileCard.listItemTitle.entityRef': 'Entity Ref'; + readonly 'groupProfileCard.listItemTitle.parentGroup': 'Parent Group'; + readonly 'groupProfileCard.listItemTitle.childGroups': 'Child Groups'; + readonly 'membersListCard.title': '{{groupName}} members'; + readonly 'membersListCard.cardLabel': 'User page for {{memberName}}'; + readonly 'membersListCard.noMembersDescription': 'This group has no members.'; + readonly 'membersListCard.noSearchResult': 'Found no members matching "{{searchTerm}}".'; + readonly 'membersListCard.aggregateMembersToggle.label': 'Include subgroups'; + readonly 'ownershipCard.title': 'Ownership'; + readonly 'ownershipCard.aggregateRelationsToggle.label': 'Include indirect ownership'; + readonly 'userProfileCard.editIconButtonTitle': 'Edit Metadata'; + readonly 'userProfileCard.listItemTitle.email': 'Email'; + readonly 'userProfileCard.listItemTitle.memberOf': 'Member of'; + readonly 'userProfileCard.userNotFound': 'User not found'; + readonly 'userProfileCard.moreGroupButtonTitle': '...More ({{number}})'; + readonly 'userProfileCard.allGroupDialog.title': "All {{name}}'s groups:"; + readonly 'userProfileCard.allGroupDialog.closeButtonTitle': 'Close'; + } +>; + // @public (undocumented) export const OwnershipCard: (props: { entityFilterKind?: string[]; diff --git a/plugins/org/src/alpha.tsx b/plugins/org/src/alpha.tsx index 4e1146378112aa..89acb8a6eea934 100644 --- a/plugins/org/src/alpha.tsx +++ b/plugins/org/src/alpha.tsx @@ -130,4 +130,10 @@ export default createFrontendPlugin({ }, }); -export { orgTranslationRef } from './translation'; +import { orgTranslationRef as _orgTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-org` instead. + */ +export const orgTranslationRef = _orgTranslationRef; diff --git a/plugins/org/src/index.ts b/plugins/org/src/index.ts index 33671697d6b6c2..90f475bb6033c0 100644 --- a/plugins/org/src/index.ts +++ b/plugins/org/src/index.ts @@ -29,3 +29,4 @@ export { EntityUserProfileCard, } from './plugin'; export * from './components'; +export { orgTranslationRef } from './translation'; diff --git a/plugins/org/src/translation.ts b/plugins/org/src/translation.ts index b5309d14e7a56f..aa1c248c17d536 100644 --- a/plugins/org/src/translation.ts +++ b/plugins/org/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/frontend-plugin-api'; /** - * @alpha + * @public */ export const orgTranslationRef = createTranslationRef({ id: 'org', diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index 84c88bd319b09e..a6c9fbb2325444 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -307,16 +307,11 @@ export type ScaffolderReactComponentsNameToClassKey = { // @alpha (undocumented) export type ScaffolderReactTemplateCategoryPickerClassKey = 'root' | 'label'; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const scaffolderReactTranslationRef: TranslationRef< 'scaffolder-react', { readonly 'workflow.noDescription': 'No description'; - readonly 'stepper.backButtonText': 'Back'; - readonly 'stepper.nextButtonText': 'Next'; - readonly 'stepper.createButtonText': 'Create'; - readonly 'stepper.reviewButtonText': 'Review'; - readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; readonly 'passwordWidget.content': 'This widget is insecure. Please use [`ui:field: Secret`](https://backstage.io/docs/features/software-templates/writing-templates/#using-secrets) instead of `ui:widget: password`'; readonly 'scaffolderPageContextMenu.createLabel': 'Create'; readonly 'scaffolderPageContextMenu.moreLabel': 'more'; @@ -324,6 +319,11 @@ export const scaffolderReactTranslationRef: TranslationRef< readonly 'scaffolderPageContextMenu.actionsLabel': 'Installed Actions'; readonly 'scaffolderPageContextMenu.tasksLabel': 'Task List'; readonly 'scaffolderPageContextMenu.templatingExtensionsLabel': 'Templating Extensions'; + readonly 'stepper.backButtonText': 'Back'; + readonly 'stepper.nextButtonText': 'Next'; + readonly 'stepper.createButtonText': 'Create'; + readonly 'stepper.reviewButtonText': 'Review'; + readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; readonly 'templateCategoryPicker.title': 'Categories'; readonly 'templateCard.noDescription': 'No description'; readonly 'templateCard.chooseButtonText': 'Choose'; diff --git a/plugins/scaffolder-react/report.api.md b/plugins/scaffolder-react/report.api.md index 5c0ef0d2be871a..9b9b00ad02cfba 100644 --- a/plugins/scaffolder-react/report.api.md +++ b/plugins/scaffolder-react/report.api.md @@ -56,6 +56,7 @@ import { TemplateGlobalFunction as TemplateGlobalFunction_2 } from '@backstage/p import { TemplateGlobalValue as TemplateGlobalValue_2 } from '@backstage/plugin-scaffolder-common'; import { TemplateParameterSchema as TemplateParameterSchema_2 } from '@backstage/plugin-scaffolder-common'; import { TemplatesType } from '@rjsf/utils'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UIOptionsType } from '@rjsf/utils'; import { UiSchema } from '@rjsf/utils'; import { ValidatorType } from '@rjsf/utils'; @@ -240,6 +241,31 @@ export type ScaffolderOutputLink = ScaffolderOutputLink_2; // @public @deprecated (undocumented) export type ScaffolderOutputText = ScaffolderOutputText_2; +// @public (undocumented) +export const scaffolderReactTranslationRef: TranslationRef< + 'scaffolder-react', + { + readonly 'workflow.noDescription': 'No description'; + readonly 'passwordWidget.content': 'This widget is insecure. Please use [`ui:field: Secret`](https://backstage.io/docs/features/software-templates/writing-templates/#using-secrets) instead of `ui:widget: password`'; + readonly 'scaffolderPageContextMenu.createLabel': 'Create'; + readonly 'scaffolderPageContextMenu.moreLabel': 'more'; + readonly 'scaffolderPageContextMenu.editorLabel': 'Manage Templates'; + readonly 'scaffolderPageContextMenu.actionsLabel': 'Installed Actions'; + readonly 'scaffolderPageContextMenu.tasksLabel': 'Task List'; + readonly 'scaffolderPageContextMenu.templatingExtensionsLabel': 'Templating Extensions'; + readonly 'stepper.backButtonText': 'Back'; + readonly 'stepper.nextButtonText': 'Next'; + readonly 'stepper.createButtonText': 'Create'; + readonly 'stepper.reviewButtonText': 'Review'; + readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; + readonly 'templateCategoryPicker.title': 'Categories'; + readonly 'templateCard.noDescription': 'No description'; + readonly 'templateCard.chooseButtonText': 'Choose'; + readonly 'cardHeader.detailBtnTitle': 'Show template entity details'; + readonly 'templateOutputs.title': 'Text Output'; + } +>; + // @public export type ScaffolderRJSFField< T = any, diff --git a/plugins/scaffolder-react/src/alpha.ts b/plugins/scaffolder-react/src/alpha.ts index e18c758b3d1d29..ab04bbf3f3da9c 100644 --- a/plugins/scaffolder-react/src/alpha.ts +++ b/plugins/scaffolder-react/src/alpha.ts @@ -16,4 +16,10 @@ export * from './next'; -export { scaffolderReactTranslationRef } from './translation'; +import { scaffolderReactTranslationRef as _scaffolderReactTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-scaffolder-react` instead. + */ +export const scaffolderReactTranslationRef = _scaffolderReactTranslationRef; diff --git a/plugins/scaffolder-react/src/index.ts b/plugins/scaffolder-react/src/index.ts index e73df50fce5505..bfffa342a16425 100644 --- a/plugins/scaffolder-react/src/index.ts +++ b/plugins/scaffolder-react/src/index.ts @@ -22,3 +22,4 @@ export * from './api'; export * from './hooks'; export * from './layouts'; export * from './utils'; +export { scaffolderReactTranslationRef } from './translation'; diff --git a/plugins/scaffolder-react/src/translation.ts b/plugins/scaffolder-react/src/translation.ts index 18da2f58e73cae..367710f214a9aa 100644 --- a/plugins/scaffolder-react/src/translation.ts +++ b/plugins/scaffolder-react/src/translation.ts @@ -15,7 +15,7 @@ */ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const scaffolderReactTranslationRef = createTranslationRef({ id: 'scaffolder-react', messages: { diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index b2cd1bbcbe1aa7..e864b310d8e15f 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -523,7 +523,7 @@ export type ScaffolderTemplateFormPreviewerClassKey = | 'textArea' | 'preview'; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const scaffolderTranslationRef: TranslationRef< 'scaffolder', { diff --git a/plugins/scaffolder/report.api.md b/plugins/scaffolder/report.api.md index 7a34ac1cc2a86e..a57f21f13cd257 100644 --- a/plugins/scaffolder/report.api.md +++ b/plugins/scaffolder/report.api.md @@ -49,6 +49,7 @@ import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react'; import { TemplateListPageProps } from '@backstage/plugin-scaffolder/alpha'; import { TemplateParameterSchema as TemplateParameterSchema_2 } from '@backstage/plugin-scaffolder-common'; import { TemplateWizardPageProps } from '@backstage/plugin-scaffolder/alpha'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { z } from 'zod/v3'; // @public @deprecated (undocumented) @@ -614,6 +615,192 @@ export type ScaffolderTaskOutput = ScaffolderTaskOutput_3; // @public @deprecated (undocumented) export type ScaffolderTaskStatus = ScaffolderTaskStatus_2; +// @public (undocumented) +export const scaffolderTranslationRef: TranslationRef< + 'scaffolder', + { + readonly 'fields.entityNamePicker.title': 'Name'; + readonly 'fields.entityNamePicker.description': 'Unique name of the component'; + readonly 'fields.entityPicker.title': 'Entity'; + readonly 'fields.entityPicker.description': 'An entity from the catalog'; + readonly 'fields.entityTagsPicker.title': 'Tags'; + readonly 'fields.entityTagsPicker.description': "Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters"; + readonly 'fields.multiEntityPicker.title': 'Entity'; + readonly 'fields.multiEntityPicker.description': 'An entity from the catalog'; + readonly 'fields.myGroupsPicker.title': 'Entity'; + readonly 'fields.myGroupsPicker.description': 'An entity from the catalog'; + readonly 'fields.ownedEntityPicker.title': 'Entity'; + readonly 'fields.ownedEntityPicker.description': 'An entity from the catalog'; + readonly 'fields.ownerPicker.title': 'Owner'; + readonly 'fields.ownerPicker.description': 'The owner of the component'; + readonly 'fields.azureRepoPicker.organization.title': 'Organization'; + readonly 'fields.azureRepoPicker.organization.description': 'The Organization that this repo will belong to'; + readonly 'fields.azureRepoPicker.project.title': 'Project'; + readonly 'fields.azureRepoPicker.project.description': 'The Project that this repo will belong to'; + readonly 'fields.bitbucketRepoPicker.project.title': 'Allowed Projects'; + readonly 'fields.bitbucketRepoPicker.project.description': 'The Project that this repo will belong to'; + readonly 'fields.bitbucketRepoPicker.project.inputTitle': 'Projects'; + readonly 'fields.bitbucketRepoPicker.workspaces.title': 'Allowed Workspaces'; + readonly 'fields.bitbucketRepoPicker.workspaces.description': 'The Workspace that this repo will belong to'; + readonly 'fields.bitbucketRepoPicker.workspaces.inputTitle': 'Workspaces'; + readonly 'fields.gerritRepoPicker.parent.title': 'Parent'; + readonly 'fields.gerritRepoPicker.parent.description': 'The project parent that the repo will belong to'; + readonly 'fields.gerritRepoPicker.owner.title': 'Owner'; + readonly 'fields.gerritRepoPicker.owner.description': 'The owner of the project (optional)'; + readonly 'fields.giteaRepoPicker.owner.title': 'Owner Available'; + readonly 'fields.giteaRepoPicker.owner.description': 'Gitea namespace where this repository will belong to. It can be the name of organization, group, subgroup, user, or the project.'; + readonly 'fields.giteaRepoPicker.owner.inputTitle': 'Owner'; + readonly 'fields.githubRepoPicker.owner.title': 'Owner Available'; + readonly 'fields.githubRepoPicker.owner.description': 'The organization, user or project that this repo will belong to'; + readonly 'fields.githubRepoPicker.owner.inputTitle': 'Owner'; + readonly 'fields.gitlabRepoPicker.owner.title': 'Owner Available'; + readonly 'fields.gitlabRepoPicker.owner.description': 'GitLab namespace where this repository will belong to. It can be the name of organization, group, subgroup, user, or the project.'; + readonly 'fields.gitlabRepoPicker.owner.inputTitle': 'Owner'; + readonly 'fields.repoUrlPicker.host.title': 'Host'; + readonly 'fields.repoUrlPicker.host.description': 'The host where the repository will be created'; + readonly 'fields.repoUrlPicker.repository.title': 'Repositories Available'; + readonly 'fields.repoUrlPicker.repository.description': 'The name of the repository'; + readonly 'fields.repoUrlPicker.repository.inputTitle': 'Repository'; + readonly 'fields.repoOwnerPicker.title': 'Owner'; + readonly 'fields.repoOwnerPicker.description': 'The owner of the repository'; + readonly 'aboutCard.launchTemplate': 'Launch Template'; + readonly 'actionsPage.content.emptyState.title': 'No information to display'; + readonly 'actionsPage.content.emptyState.description': 'There are no actions installed or there was an issue communicating with backend.'; + readonly 'actionsPage.content.searchFieldPlaceholder': 'Search for an action'; + readonly 'actionsPage.title': 'Installed actions'; + readonly 'actionsPage.action.input': 'Input'; + readonly 'actionsPage.action.output': 'Output'; + readonly 'actionsPage.action.examples': 'Examples'; + readonly 'actionsPage.subtitle': 'This is the collection of all installed actions'; + readonly 'actionsPage.pageTitle': 'Create a New Component'; + readonly 'listTaskPage.content.emptyState.title': 'No information to display'; + readonly 'listTaskPage.content.emptyState.description': 'There are no tasks or there was an issue communicating with backend.'; + readonly 'listTaskPage.content.tableCell.template': 'Template'; + readonly 'listTaskPage.content.tableCell.status': 'Status'; + readonly 'listTaskPage.content.tableCell.owner': 'Owner'; + readonly 'listTaskPage.content.tableCell.created': 'Created'; + readonly 'listTaskPage.content.tableCell.taskID': 'Task ID'; + readonly 'listTaskPage.content.tableTitle': 'Tasks'; + readonly 'listTaskPage.title': 'List template tasks'; + readonly 'listTaskPage.subtitle': 'All tasks that have been started'; + readonly 'listTaskPage.pageTitle': 'Templates Tasks'; + readonly 'ownerListPicker.title': 'Task Owner'; + readonly 'ownerListPicker.options.all': 'All'; + readonly 'ownerListPicker.options.owned': 'Owned'; + readonly 'ongoingTask.title': 'Run of'; + readonly 'ongoingTask.contextMenu.cancel': 'Cancel'; + readonly 'ongoingTask.contextMenu.retry': 'Retry'; + readonly 'ongoingTask.contextMenu.startOver': 'Start Over'; + readonly 'ongoingTask.contextMenu.hideLogs': 'Hide Logs'; + readonly 'ongoingTask.contextMenu.showLogs': 'Show Logs'; + readonly 'ongoingTask.contextMenu.hideButtonBar': 'Hide Button Bar'; + readonly 'ongoingTask.contextMenu.showButtonBar': 'Show Button Bar'; + readonly 'ongoingTask.subtitle': 'Task {{taskId}}'; + readonly 'ongoingTask.pageTitle.hasTemplateName': 'Run of {{templateName}}'; + readonly 'ongoingTask.pageTitle.noTemplateName': 'Scaffolder Run'; + readonly 'ongoingTask.cancelButtonTitle': 'Cancel'; + readonly 'ongoingTask.retryButtonTitle': 'Retry'; + readonly 'ongoingTask.startOverButtonTitle': 'Start Over'; + readonly 'ongoingTask.hideLogsButtonTitle': 'Hide Logs'; + readonly 'ongoingTask.showLogsButtonTitle': 'Show Logs'; + readonly 'templateEditorForm.stepper.emptyText': 'There are no spec parameters in the template to preview.'; + readonly 'renderSchema.undefined': 'No schema defined'; + readonly 'renderSchema.tableCell.name': 'Name'; + readonly 'renderSchema.tableCell.type': 'Type'; + readonly 'renderSchema.tableCell.title': 'Title'; + readonly 'renderSchema.tableCell.description': 'Description'; + readonly 'templatingExtensions.content.values.title': 'Values'; + readonly 'templatingExtensions.content.values.notAvailable': 'There are no global template values defined.'; + readonly 'templatingExtensions.content.emptyState.title': 'No information to display'; + readonly 'templatingExtensions.content.emptyState.description': 'There are no templating extensions available or there was an issue communicating with the backend.'; + readonly 'templatingExtensions.content.filters.title': 'Filters'; + readonly 'templatingExtensions.content.filters.schema.input': 'Input'; + readonly 'templatingExtensions.content.filters.schema.output': 'Output'; + readonly 'templatingExtensions.content.filters.schema.arguments': 'Arguments'; + readonly 'templatingExtensions.content.filters.examples': 'Examples'; + readonly 'templatingExtensions.content.filters.notAvailable': 'There are no template filters defined.'; + readonly 'templatingExtensions.content.filters.metadataAbsent': 'Filter metadata unavailable'; + readonly 'templatingExtensions.content.functions.title': 'Functions'; + readonly 'templatingExtensions.content.functions.schema.output': 'Output'; + readonly 'templatingExtensions.content.functions.schema.arguments': 'Arguments'; + readonly 'templatingExtensions.content.functions.examples': 'Examples'; + readonly 'templatingExtensions.content.functions.notAvailable': 'There are no global template functions defined.'; + readonly 'templatingExtensions.content.functions.metadataAbsent': 'Function metadata unavailable'; + readonly 'templatingExtensions.content.searchFieldPlaceholder': 'Search for an extension'; + readonly 'templatingExtensions.title': 'Templating Extensions'; + readonly 'templatingExtensions.subtitle': 'This is the collection of available templating extensions'; + readonly 'templatingExtensions.pageTitle': 'Templating Extensions'; + readonly 'templateTypePicker.title': 'Categories'; + readonly 'templateIntroPage.title': 'Manage Templates'; + readonly 'templateIntroPage.subtitle': 'Edit, preview, and try out templates, forms, and custom fields'; + readonly 'templateFormPage.title': 'Template Editor'; + readonly 'templateFormPage.subtitle': 'Edit, preview, and try out templates forms'; + readonly 'templateCustomFieldPage.title': 'Custom Field Explorer'; + readonly 'templateCustomFieldPage.subtitle': 'Edit, preview, and try out custom fields'; + readonly 'templateEditorPage.title': 'Template Editor'; + readonly 'templateEditorPage.subtitle': 'Edit, preview, and try out templates and template forms'; + readonly 'templateEditorPage.dryRunResults.title': 'Dry-run results'; + readonly 'templateEditorPage.dryRunResultsList.title': 'Result {{resultId}}'; + readonly 'templateEditorPage.dryRunResultsList.deleteButtonTitle': 'Delete result'; + readonly 'templateEditorPage.dryRunResultsList.downloadButtonTitle': 'Download as .zip'; + readonly 'templateEditorPage.dryRunResultsView.tab.output': 'Output'; + readonly 'templateEditorPage.dryRunResultsView.tab.log': 'Log'; + readonly 'templateEditorPage.dryRunResultsView.tab.files': 'Files'; + readonly 'templateEditorPage.taskStatusStepper.skippedStepTitle': 'Skipped'; + readonly 'templateEditorPage.customFieldExplorer.preview.title': 'Template Spec'; + readonly 'templateEditorPage.customFieldExplorer.fieldForm.title': 'Field Options'; + readonly 'templateEditorPage.customFieldExplorer.fieldForm.applyButtonTitle': 'Apply'; + readonly 'templateEditorPage.customFieldExplorer.selectFieldLabel': 'Choose Custom Field Extension'; + readonly 'templateEditorPage.customFieldExplorer.fieldPreview.title': 'Field Preview'; + readonly 'templateEditorPage.templateEditorBrowser.closeConfirmMessage': 'Are you sure? Unsaved changes will be lost'; + readonly 'templateEditorPage.templateEditorBrowser.saveIconTooltip': 'Save all files'; + readonly 'templateEditorPage.templateEditorBrowser.reloadIconTooltip': 'Reload directory'; + readonly 'templateEditorPage.templateEditorBrowser.closeIconTooltip': 'Close directory'; + readonly 'templateEditorPage.templateEditorIntro.title': 'Get started by choosing one of the options below'; + readonly 'templateEditorPage.templateEditorIntro.loadLocal.title': 'Load Template Directory'; + readonly 'templateEditorPage.templateEditorIntro.loadLocal.description': 'Load a local template directory, allowing you to both edit and try executing your own template.'; + readonly 'templateEditorPage.templateEditorIntro.loadLocal.unsupportedTooltip': 'Only supported in some Chromium-based browsers with the page loaded over HTTPS'; + readonly 'templateEditorPage.templateEditorIntro.createLocal.title': 'Create New Template'; + readonly 'templateEditorPage.templateEditorIntro.createLocal.description': 'Create a local template directory, allowing you to both edit and try executing your own template.'; + readonly 'templateEditorPage.templateEditorIntro.createLocal.unsupportedTooltip': 'Only supported in some Chromium-based browsers with the page loaded over HTTPS'; + readonly 'templateEditorPage.templateEditorIntro.formEditor.title': 'Template Form Playground'; + readonly 'templateEditorPage.templateEditorIntro.formEditor.description': 'Preview and edit a template form, either using a sample template or by loading a template from the catalog.'; + readonly 'templateEditorPage.templateEditorIntro.fieldExplorer.title': 'Custom Field Explorer'; + readonly 'templateEditorPage.templateEditorIntro.fieldExplorer.description': 'View and play around with available installed custom field extensions.'; + readonly 'templateEditorPage.templateEditorTextArea.saveIconTooltip': 'Save file'; + readonly 'templateEditorPage.templateEditorTextArea.refreshIconTooltip': 'Reload file'; + readonly 'templateEditorPage.templateEditorTextArea.emptyStateParagraph': 'Please select an action on the file menu.'; + readonly 'templateEditorPage.templateFormPreviewer.title': 'Load Existing Template'; + readonly 'templateListPage.title': 'Create a new component'; + readonly 'templateListPage.subtitle': 'Create new software components using standard templates in your organization'; + readonly 'templateListPage.pageTitle': 'Create a new component'; + readonly 'templateListPage.templateGroups.defaultTitle': 'Templates'; + readonly 'templateListPage.templateGroups.otherTitle': 'Other Templates'; + readonly 'templateListPage.contentHeader.supportButtonTitle': 'Create new software components using standard templates. Different templates create different kinds of components (services, websites, documentation, ...).'; + readonly 'templateListPage.contentHeader.registerExistingButtonTitle': 'Register Existing Component'; + readonly 'templateListPage.additionalLinksForEntity.viewTechDocsTitle': 'View TechDocs'; + readonly 'templateWizardPage.title': 'Create a new component'; + readonly 'templateWizardPage.subtitle': 'Create new software components using standard templates in your organization'; + readonly 'templateWizardPage.pageTitle': 'Create a new component'; + readonly 'templateWizardPage.templateWithTitle': 'Create new {{templateTitle}}'; + readonly 'templateWizardPage.pageContextMenu.editConfigurationTitle': 'Edit Configuration'; + readonly 'templateEditorToolbar.customFieldExplorerTooltip': 'Custom Fields Explorer'; + readonly 'templateEditorToolbar.installedActionsDocumentationTooltip': 'Installed Actions Documentation'; + readonly 'templateEditorToolbar.templatingExtensionsDocumentationTooltip': 'Templating Extensions Documentation'; + readonly 'templateEditorToolbar.addToCatalogButton': 'Publish'; + readonly 'templateEditorToolbar.addToCatalogDialogTitle': 'Publish changes'; + readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsIntroduction': 'Follow the instructions below to create or update a template:'; + readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsListItems': 'Save the template files in a local directory\nCreate a pull request to a new or existing git repository\nIf the template already exists, the changes will be reflected in the software catalog once the pull request gets merged\nBut if you are creating a new template, follow the documentation linked below to register the new template repository in software catalog'; + readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/'; + readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation'; + readonly 'templateEditorToolbarFileMenu.button': 'File'; + readonly 'templateEditorToolbarFileMenu.options.openDirectory': 'Open template directory'; + readonly 'templateEditorToolbarFileMenu.options.createDirectory': 'Create template directory'; + readonly 'templateEditorToolbarFileMenu.options.closeEditor': 'Close template editor'; + readonly 'templateEditorToolbarTemplatesMenu.button': 'Templates'; + } +>; + // @public @deprecated (undocumented) export type ScaffolderUseTemplateSecrets = ScaffolderUseTemplateSecrets_2; diff --git a/plugins/scaffolder/src/alpha/index.ts b/plugins/scaffolder/src/alpha/index.ts index a0b30fd3df5532..4d3366644c41ac 100644 --- a/plugins/scaffolder/src/alpha/index.ts +++ b/plugins/scaffolder/src/alpha/index.ts @@ -22,7 +22,13 @@ export { type ScaffolderTemplateFormPreviewerClassKey, } from './components'; -export { scaffolderTranslationRef } from '../translation'; +import { scaffolderTranslationRef as _scaffolderTranslationRef } from '../translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-scaffolder` instead. + */ +export const scaffolderTranslationRef = _scaffolderTranslationRef; export * from './api'; export { formFieldsApiRef, diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index cc89c656ebf75a..84c9d56ebec6d8 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -38,3 +38,4 @@ export { export * from './components'; export * from './deprecated'; +export { scaffolderTranslationRef } from './translation'; diff --git a/plugins/scaffolder/src/translation.ts b/plugins/scaffolder/src/translation.ts index 94a3fb590f4402..d0f332e2fa0e31 100644 --- a/plugins/scaffolder/src/translation.ts +++ b/plugins/scaffolder/src/translation.ts @@ -15,7 +15,7 @@ */ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const scaffolderTranslationRef = createTranslationRef({ id: 'scaffolder', messages: { diff --git a/plugins/search-react/report-alpha.api.md b/plugins/search-react/report-alpha.api.md index 2f7b32ab1650b1..335a499318306f 100644 --- a/plugins/search-react/report-alpha.api.md +++ b/plugins/search-react/report-alpha.api.md @@ -96,7 +96,7 @@ export interface SearchFilterResultTypeBlueprintParams { value: string; } -// @alpha (undocumented) +// @public @deprecated (undocumented) export const searchReactTranslationRef: TranslationRef< 'search-react', { diff --git a/plugins/search-react/report.api.md b/plugins/search-react/report.api.md index d4b7592682eca7..21fc5e2e946428 100644 --- a/plugins/search-react/report.api.md +++ b/plugins/search-react/report.api.md @@ -28,6 +28,7 @@ import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common' import { SearchResultSet } from '@backstage/plugin-search-common'; import { SetStateAction } from 'react'; import { TextFieldProps } from '@material-ui/core/TextField'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; import { TypographyProps } from '@material-ui/core/Typography'; // @public (undocumented) @@ -296,6 +297,24 @@ export type SearchPaginationProps = Omit< | 'hasNextPage' >; +// @public (undocumented) +export const searchReactTranslationRef: TranslationRef< + 'search-react', + { + readonly 'searchBar.title': 'Search'; + readonly 'searchBar.placeholder': 'Search in {{org}}'; + readonly 'searchBar.clearButtonTitle': 'Clear'; + readonly 'searchFilter.allOptionTitle': 'All'; + readonly 'searchPagination.limitLabel': 'Results per page:'; + readonly 'searchPagination.limitText': 'of {{num}}'; + readonly noResultsDescription: 'Sorry, no results were found'; + readonly 'searchResultGroup.linkTitle': 'See All'; + readonly 'searchResultGroup.addFilterButtonTitle': 'Add filter'; + readonly 'searchResultPager.next': 'Next'; + readonly 'searchResultPager.previous': 'Previous'; + } +>; + // @public export const SearchResult: (props: SearchResultProps) => JSX_2.Element; diff --git a/plugins/search-react/src/alpha/index.ts b/plugins/search-react/src/alpha/index.ts index 7c44f59d56e2bc..390a13cb4c0531 100644 --- a/plugins/search-react/src/alpha/index.ts +++ b/plugins/search-react/src/alpha/index.ts @@ -14,4 +14,10 @@ * limitations under the License. */ export * from './blueprints'; -export { searchReactTranslationRef } from '../translation'; +import { searchReactTranslationRef as _searchReactTranslationRef } from '../translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-search-react` instead. + */ +export const searchReactTranslationRef = _searchReactTranslationRef; diff --git a/plugins/search-react/src/index.ts b/plugins/search-react/src/index.ts index 8b234e3c05edb7..e376c67185b277 100644 --- a/plugins/search-react/src/index.ts +++ b/plugins/search-react/src/index.ts @@ -34,3 +34,4 @@ export type { SearchContextState, SearchContextValue, } from './context'; +export { searchReactTranslationRef } from './translation'; diff --git a/plugins/search-react/src/translation.ts b/plugins/search-react/src/translation.ts index e4c394d9528971..6105f746513817 100644 --- a/plugins/search-react/src/translation.ts +++ b/plugins/search-react/src/translation.ts @@ -17,7 +17,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; /** - * @alpha + * @public */ export const searchReactTranslationRef = createTranslationRef({ id: 'search-react', diff --git a/plugins/search/report-alpha.api.md b/plugins/search/report-alpha.api.md index e3f2d6108a18fc..bcfb720c3610dc 100644 --- a/plugins/search/report-alpha.api.md +++ b/plugins/search/report-alpha.api.md @@ -359,7 +359,7 @@ export const searchPage: OverridableExtensionDefinition<{ }; }>; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const searchTranslationRef: TranslationRef< 'search', { diff --git a/plugins/search/report.api.md b/plugins/search/report.api.md index c066d0ff4707d3..73d5a948c31f6d 100644 --- a/plugins/search/report.api.md +++ b/plugins/search/report.api.md @@ -10,6 +10,7 @@ import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SearchBarBaseProps } from '@backstage/plugin-search-react'; import { SearchResultSet } from '@backstage/plugin-search-common'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) export const HomePageSearchBar: ( @@ -78,6 +79,20 @@ const searchPlugin: BackstagePlugin< export { searchPlugin as plugin }; export { searchPlugin }; +// @public (undocumented) +export const searchTranslationRef: TranslationRef< + 'search', + { + readonly 'searchModal.viewFullResults': 'View Full Results'; + readonly 'searchType.tabs.allTitle': 'All'; + readonly 'searchType.allResults': 'All Results'; + readonly 'searchType.accordion.collapse': 'Collapse'; + readonly 'searchType.accordion.numberOfResults': '{{number}} results'; + readonly 'searchType.accordion.allTitle': 'All'; + readonly 'sidebarSearchModal.title': 'Search'; + } +>; + // @public (undocumented) export const SearchType: { (props: SearchTypeProps): JSX_2.Element; diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 9625f5e946e71a..c9ba79a52cb7ea 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -285,5 +285,10 @@ export default createFrontendPlugin({ }, }); -/** @alpha */ -export { searchTranslationRef } from './translation'; +import { searchTranslationRef as _searchTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-search` instead. + */ +export const searchTranslationRef = _searchTranslationRef; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 66b032c83cdee6..fc579c55cc6531 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -51,3 +51,4 @@ export { searchPlugin as plugin, searchPlugin, } from './plugin'; +export { searchTranslationRef } from './translation'; diff --git a/plugins/search/src/translation.ts b/plugins/search/src/translation.ts index 37f86b9218e9b2..58b5b72c776652 100644 --- a/plugins/search/src/translation.ts +++ b/plugins/search/src/translation.ts @@ -17,7 +17,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; /** - * @alpha + * @public */ export const searchTranslationRef = createTranslationRef({ id: 'search', diff --git a/plugins/user-settings/report-alpha.api.md b/plugins/user-settings/report-alpha.api.md index c99709d777265d..9d6439c1dfad7a 100644 --- a/plugins/user-settings/report-alpha.api.md +++ b/plugins/user-settings/report-alpha.api.md @@ -155,7 +155,7 @@ export const settingsNavItem: OverridableExtensionDefinition<{ }; }>; -// @alpha (undocumented) +// @public @deprecated (undocumented) export const userSettingsTranslationRef: TranslationRef< 'user-settings', { diff --git a/plugins/user-settings/report.api.md b/plugins/user-settings/report.api.md index 2994f2c13ccae4..0b2d683e7a7c09 100644 --- a/plugins/user-settings/report.api.md +++ b/plugins/user-settings/report.api.md @@ -26,6 +26,7 @@ import { SignalApi } from '@backstage/plugin-signals-react'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; import { TabProps } from '@material-ui/core/Tab'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) export const DefaultProviderSettings: (props: { @@ -163,6 +164,63 @@ export type UserSettingsTabProps = PropsWithChildren<{ // @public (undocumented) export const UserSettingsThemeToggle: () => JSX_2.Element; +// @public (undocumented) +export const userSettingsTranslationRef: TranslationRef< + 'user-settings', + { + readonly 'featureFlags.title': 'Feature Flags'; + readonly 'featureFlags.description': 'Please refresh the page when toggling feature flags'; + readonly 'featureFlags.filterTitle': 'Filter'; + readonly 'featureFlags.clearFilter': 'Clear filter'; + readonly 'featureFlags.emptyFlags.title': 'No Feature Flags'; + readonly 'featureFlags.emptyFlags.action.title': 'An example for how to add a feature flag is highlighted below:'; + readonly 'featureFlags.emptyFlags.action.readMoreButtonTitle': 'Read More'; + readonly 'featureFlags.emptyFlags.description': 'Feature Flags make it possible for plugins to register features in Backstage for users to opt into. You can use this to split out logic in your code for manual A/B testing, etc.'; + readonly 'featureFlags.flagItem.title.disable': 'Disable'; + readonly 'featureFlags.flagItem.title.enable': 'Enable'; + readonly 'featureFlags.flagItem.subtitle.registeredInApplication': 'Registered in the application'; + readonly 'featureFlags.flagItem.subtitle.registeredInPlugin': 'Registered in {{pluginId}} plugin'; + readonly 'languageToggle.select': 'Select language {{language}}'; + readonly 'languageToggle.title': 'Language'; + readonly 'languageToggle.description': 'Change the language'; + readonly 'themeToggle.select': 'Select {{theme}}'; + readonly 'themeToggle.title': 'Theme'; + readonly 'themeToggle.description': 'Change the theme mode'; + readonly 'themeToggle.names.auto': 'Auto'; + readonly 'themeToggle.names.dark': 'Dark'; + readonly 'themeToggle.names.light': 'Light'; + readonly 'themeToggle.selectAuto': 'Select Auto Theme'; + readonly 'signOutMenu.title': 'Sign Out'; + readonly 'signOutMenu.moreIconTitle': 'more'; + readonly 'pinToggle.title': 'Pin Sidebar'; + readonly 'pinToggle.description': 'Prevent the sidebar from collapsing'; + readonly 'pinToggle.ariaLabelTitle': 'Pin Sidebar Switch'; + readonly 'pinToggle.switchTitles.unpin': 'Unpin Sidebar'; + readonly 'pinToggle.switchTitles.pin': 'Pin Sidebar'; + readonly 'identityCard.title': 'Backstage Identity'; + readonly 'identityCard.noIdentityTitle': 'No Backstage Identity'; + readonly 'identityCard.userEntity': 'User Entity'; + readonly 'identityCard.ownershipEntities': 'Ownership Entities'; + readonly 'defaultProviderSettings.description': 'Provides authentication towards {{provider}} APIs and identities'; + readonly 'emptyProviders.title': 'No Authentication Providers'; + readonly 'emptyProviders.action.title': 'Open app-config.yaml and make the changes as highlighted below:'; + readonly 'emptyProviders.action.readMoreButtonTitle': 'Read More'; + readonly 'emptyProviders.description': 'You can add Authentication Providers to Backstage which allows you to use these providers to authenticate yourself.'; + readonly 'providerSettingsItem.title.signOut': 'Sign out from {{title}}'; + readonly 'providerSettingsItem.title.signIn': 'Sign in to {{title}}'; + readonly 'providerSettingsItem.buttonTitle.signOut': 'Sign out'; + readonly 'providerSettingsItem.buttonTitle.signIn': 'Sign in'; + readonly 'authProviders.title': 'Available Providers'; + readonly 'defaultSettingsPage.tabsTitle.featureFlags': 'Feature Flags'; + readonly 'defaultSettingsPage.tabsTitle.authProviders': 'Authentication Providers'; + readonly 'defaultSettingsPage.tabsTitle.general': 'General'; + readonly 'settingsLayout.title': 'Settings'; + readonly sidebarTitle: 'Settings'; + readonly 'profileCard.title': 'Profile'; + readonly 'appearanceCard.title': 'Appearance'; + } +>; + // @public (undocumented) export const useUserProfile: () => | { diff --git a/plugins/user-settings/src/alpha.tsx b/plugins/user-settings/src/alpha.tsx index 87b858fcec0c17..c2273285e64736 100644 --- a/plugins/user-settings/src/alpha.tsx +++ b/plugins/user-settings/src/alpha.tsx @@ -23,7 +23,13 @@ import { import SettingsIcon from '@material-ui/icons/Settings'; import { settingsRouteRef } from './plugin'; -export * from './translation'; +import { userSettingsTranslationRef as _userSettingsTranslationRef } from './translation'; + +/** + * @public + * @deprecated Import from `@backstage/plugin-user-settings` instead. + */ +export const userSettingsTranslationRef = _userSettingsTranslationRef; const userSettingsPage = PageBlueprint.makeWithOverrides({ inputs: { diff --git a/plugins/user-settings/src/index.ts b/plugins/user-settings/src/index.ts index e64d3c926c3490..84693ed60210a5 100644 --- a/plugins/user-settings/src/index.ts +++ b/plugins/user-settings/src/index.ts @@ -27,3 +27,4 @@ export { UserSettingsPage, } from './plugin'; export * from './components'; +export { userSettingsTranslationRef } from './translation'; diff --git a/plugins/user-settings/src/translation.ts b/plugins/user-settings/src/translation.ts index 7445a415f2d27c..c57789f1c52efe 100644 --- a/plugins/user-settings/src/translation.ts +++ b/plugins/user-settings/src/translation.ts @@ -16,7 +16,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; -/** @alpha */ +/** @public */ export const userSettingsTranslationRef = createTranslationRef({ id: 'user-settings', messages: { From a8bcba6706ff8925e1e91449c7fb8ab2cc7bbe5a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 15:44:26 +0100 Subject: [PATCH 42/55] Update extractTranslations test for promoted translation refs The test expected the org plugin's main entry point to have no TranslationRef exports, but the promotion changes now export orgTranslationRef from the stable entry point. Update the test to verify that only the TranslationRef is extracted while non-TranslationRef exports are still ignored. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/lib/extractTranslations.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/cli-module-translations/src/lib/extractTranslations.test.ts b/packages/cli-module-translations/src/lib/extractTranslations.test.ts index 13a94f3a0ac38d..03d9044753c3ec 100644 --- a/packages/cli-module-translations/src/lib/extractTranslations.test.ts +++ b/packages/cli-module-translations/src/lib/extractTranslations.test.ts @@ -62,7 +62,8 @@ describe('extractTranslations', () => { resolvePath(__dirname, '../../../../tsconfig.json'), ); - // The main entry of org plugin exports components but no translation ref + // The main entry of org plugin exports components and a translation ref; + // only the translation ref should be extracted. const sourceFile = project.addSourceFileAtPath( resolvePath(__dirname, '../../../..', 'plugins/org/src/index.ts'), ); @@ -73,7 +74,13 @@ describe('extractTranslations', () => { '.', ); - expect(refs).toHaveLength(0); + expect(refs).toHaveLength(1); + expect(refs[0]).toMatchObject({ + id: 'org', + packageName: '@backstage/plugin-org', + exportPath: '.', + exportName: 'orgTranslationRef', + }); }); it('extracts from the test fixtures translation ref', () => { From 717bbaf075233eba5d452639c1fafc28f4a68d04 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 16:42:33 +0100 Subject: [PATCH 43/55] Fix alpha entry point translation ref re-exports to use @alpha instead of @public Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/core-components/report-alpha.api.md | 2 +- packages/core-components/src/alpha.ts | 2 +- plugins/api-docs/report-alpha.api.md | 2 +- plugins/api-docs/src/alpha.tsx | 2 +- plugins/catalog-graph/report-alpha.api.md | 2 +- plugins/catalog-graph/src/alpha.tsx | 2 +- plugins/catalog-import/report-alpha.api.md | 2 +- plugins/catalog-import/src/alpha.tsx | 2 +- plugins/catalog-react/report-alpha.api.md | 2 +- plugins/catalog-react/src/alpha/index.ts | 2 +- plugins/catalog/report-alpha.api.md | 2 +- plugins/catalog/src/alpha/index.ts | 2 +- plugins/home-react/report-alpha.api.md | 2 +- plugins/home-react/src/alpha.ts | 2 +- plugins/home/report-alpha.api.md | 2 +- plugins/home/src/alpha.tsx | 2 +- plugins/kubernetes-cluster/report-alpha.api.md | 2 +- plugins/kubernetes-cluster/src/alpha.ts | 2 +- plugins/kubernetes-react/report-alpha.api.md | 2 +- plugins/kubernetes-react/src/alpha.ts | 2 +- plugins/kubernetes/report-alpha.api.md | 2 +- plugins/kubernetes/src/alpha/index.ts | 2 +- plugins/notifications/report-alpha.api.md | 2 +- plugins/notifications/src/alpha.tsx | 2 +- plugins/org/report-alpha.api.md | 2 +- plugins/org/src/alpha.tsx | 2 +- plugins/scaffolder-react/report-alpha.api.md | 2 +- plugins/scaffolder-react/src/alpha.ts | 2 +- plugins/scaffolder/report-alpha.api.md | 2 +- plugins/scaffolder/src/alpha/index.ts | 2 +- plugins/search-react/report-alpha.api.md | 2 +- plugins/search-react/src/alpha/index.ts | 2 +- plugins/search/report-alpha.api.md | 2 +- plugins/search/src/alpha.tsx | 2 +- plugins/user-settings/report-alpha.api.md | 2 +- plugins/user-settings/src/alpha.tsx | 2 +- 36 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/core-components/report-alpha.api.md b/packages/core-components/report-alpha.api.md index e7a36c7df99ad7..76adba09ecc717 100644 --- a/packages/core-components/report-alpha.api.md +++ b/packages/core-components/report-alpha.api.md @@ -5,7 +5,7 @@ ```ts import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const coreComponentsTranslationRef: TranslationRef< 'core-components', { diff --git a/packages/core-components/src/alpha.ts b/packages/core-components/src/alpha.ts index b85e10fe709073..6955ce55eb234e 100644 --- a/packages/core-components/src/alpha.ts +++ b/packages/core-components/src/alpha.ts @@ -16,7 +16,7 @@ import { coreComponentsTranslationRef as _coreComponentsTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/core-components` instead. */ export const coreComponentsTranslationRef = _coreComponentsTranslationRef; diff --git a/plugins/api-docs/report-alpha.api.md b/plugins/api-docs/report-alpha.api.md index 8e64334b09af22..3a96dd015f0774 100644 --- a/plugins/api-docs/report-alpha.api.md +++ b/plugins/api-docs/report-alpha.api.md @@ -26,7 +26,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const apiDocsTranslationRef: TranslationRef< 'api-docs', { diff --git a/plugins/api-docs/src/alpha.tsx b/plugins/api-docs/src/alpha.tsx index 967e1c11680445..94ae1ac27af09e 100644 --- a/plugins/api-docs/src/alpha.tsx +++ b/plugins/api-docs/src/alpha.tsx @@ -237,7 +237,7 @@ export default createFrontendPlugin({ import { apiDocsTranslationRef as _apiDocsTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-api-docs` instead. */ export const apiDocsTranslationRef = _apiDocsTranslationRef; diff --git a/plugins/catalog-graph/report-alpha.api.md b/plugins/catalog-graph/report-alpha.api.md index b9b92cf3e468ca..8bfbe17e9b6010 100644 --- a/plugins/catalog-graph/report-alpha.api.md +++ b/plugins/catalog-graph/report-alpha.api.md @@ -22,7 +22,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const catalogGraphTranslationRef: TranslationRef< 'catalog-graph', { diff --git a/plugins/catalog-graph/src/alpha.tsx b/plugins/catalog-graph/src/alpha.tsx index eda6d7b1e2d825..a0f16cda2a2137 100644 --- a/plugins/catalog-graph/src/alpha.tsx +++ b/plugins/catalog-graph/src/alpha.tsx @@ -110,7 +110,7 @@ export default createFrontendPlugin({ import { catalogGraphTranslationRef as _catalogGraphTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-catalog-graph` instead. */ export const catalogGraphTranslationRef = _catalogGraphTranslationRef; diff --git a/plugins/catalog-import/report-alpha.api.md b/plugins/catalog-import/report-alpha.api.md index bc60f6fab7be6f..d00d4634a2d8cf 100644 --- a/plugins/catalog-import/report-alpha.api.md +++ b/plugins/catalog-import/report-alpha.api.md @@ -18,7 +18,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const catalogImportTranslationRef: TranslationRef< 'catalog-import', { diff --git a/plugins/catalog-import/src/alpha.tsx b/plugins/catalog-import/src/alpha.tsx index 0a1f3b9f4e6b96..d0c671fe5a4bfa 100644 --- a/plugins/catalog-import/src/alpha.tsx +++ b/plugins/catalog-import/src/alpha.tsx @@ -37,7 +37,7 @@ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/ import { catalogImportTranslationRef as _catalogImportTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-catalog-import` instead. */ export const catalogImportTranslationRef = _catalogImportTranslationRef; diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index a5bf8d6fd55899..0135fe7d645152 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -36,7 +36,7 @@ export const CatalogFilterBlueprint: ExtensionBlueprint<{ dataRefs: never; }>; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const catalogReactTranslationRef: TranslationRef< 'catalog-react', { diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts index dfd151da8a4ad1..d473b0923fb162 100644 --- a/plugins/catalog-react/src/alpha/index.ts +++ b/plugins/catalog-react/src/alpha/index.ts @@ -19,7 +19,7 @@ export * from './converters'; import { catalogReactTranslationRef as _catalogReactTranslationRef } from '../translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-catalog-react` instead. */ export const catalogReactTranslationRef = _catalogReactTranslationRef; diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index 76539e776f44da..d41cff916f99f7 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -32,7 +32,7 @@ import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-rea import { SearchResultListItemBlueprintParams } from '@backstage/plugin-search-react/alpha'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const catalogTranslationRef: TranslationRef< 'catalog', { diff --git a/plugins/catalog/src/alpha/index.ts b/plugins/catalog/src/alpha/index.ts index a563a0ee906a9a..ae9f75fa099ffc 100644 --- a/plugins/catalog/src/alpha/index.ts +++ b/plugins/catalog/src/alpha/index.ts @@ -19,7 +19,7 @@ export { default } from './plugin'; import { catalogTranslationRef as _catalogTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-catalog` instead. */ export const catalogTranslationRef = _catalogTranslationRef; diff --git a/plugins/home-react/report-alpha.api.md b/plugins/home-react/report-alpha.api.md index f8d2b7cb350b0b..4cadb413bdca5e 100644 --- a/plugins/home-react/report-alpha.api.md +++ b/plugins/home-react/report-alpha.api.md @@ -125,7 +125,7 @@ export const homePageWidgetDataRef: ConfigurableExtensionDataRef< {} >; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const homeReactTranslationRef: TranslationRef< 'home-react', { diff --git a/plugins/home-react/src/alpha.ts b/plugins/home-react/src/alpha.ts index 677e27f5e1c1a8..c89ff12afda9c3 100644 --- a/plugins/home-react/src/alpha.ts +++ b/plugins/home-react/src/alpha.ts @@ -26,7 +26,7 @@ import { homeReactTranslationRef as _homeReactTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-home-react` instead. */ export const homeReactTranslationRef = _homeReactTranslationRef; diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index 6b1f89c779e8a4..8e8177980cc02d 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -206,7 +206,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const homeTranslationRef: TranslationRef< 'home', { diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index d4f350693d2e0c..bdce683cee873a 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -228,7 +228,7 @@ export default createFrontendPlugin({ import { homeTranslationRef as _homeTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-home` instead. */ export const homeTranslationRef = _homeTranslationRef; diff --git a/plugins/kubernetes-cluster/report-alpha.api.md b/plugins/kubernetes-cluster/report-alpha.api.md index 2220ffb5fe7553..08551d0050be0c 100644 --- a/plugins/kubernetes-cluster/report-alpha.api.md +++ b/plugins/kubernetes-cluster/report-alpha.api.md @@ -5,7 +5,7 @@ ```ts import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const kubernetesClusterTranslationRef: TranslationRef< 'kubernetes-cluster', { diff --git a/plugins/kubernetes-cluster/src/alpha.ts b/plugins/kubernetes-cluster/src/alpha.ts index a2d816dc361835..63ab8b692ce709 100644 --- a/plugins/kubernetes-cluster/src/alpha.ts +++ b/plugins/kubernetes-cluster/src/alpha.ts @@ -17,7 +17,7 @@ import { kubernetesClusterTranslationRef as _kubernetesClusterTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-kubernetes-cluster` instead. */ export const kubernetesClusterTranslationRef = _kubernetesClusterTranslationRef; diff --git a/plugins/kubernetes-react/report-alpha.api.md b/plugins/kubernetes-react/report-alpha.api.md index b7b8c5fcb1a98c..f9f6f67c1e64b5 100644 --- a/plugins/kubernetes-react/report-alpha.api.md +++ b/plugins/kubernetes-react/report-alpha.api.md @@ -5,7 +5,7 @@ ```ts import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const kubernetesReactTranslationRef: TranslationRef< 'kubernetes-react', { diff --git a/plugins/kubernetes-react/src/alpha.ts b/plugins/kubernetes-react/src/alpha.ts index 212f3b3e96c307..60f80039dcfda9 100644 --- a/plugins/kubernetes-react/src/alpha.ts +++ b/plugins/kubernetes-react/src/alpha.ts @@ -17,7 +17,7 @@ import { kubernetesReactTranslationRef as _kubernetesReactTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-kubernetes-react` instead. */ export const kubernetesReactTranslationRef = _kubernetesReactTranslationRef; diff --git a/plugins/kubernetes/report-alpha.api.md b/plugins/kubernetes/report-alpha.api.md index d114524b8966fe..136385e686eb40 100644 --- a/plugins/kubernetes/report-alpha.api.md +++ b/plugins/kubernetes/report-alpha.api.md @@ -240,7 +240,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const kubernetesTranslationRef: TranslationRef< 'kubernetes', { diff --git a/plugins/kubernetes/src/alpha/index.ts b/plugins/kubernetes/src/alpha/index.ts index 02fad57851f783..c3419cfa76dbfa 100644 --- a/plugins/kubernetes/src/alpha/index.ts +++ b/plugins/kubernetes/src/alpha/index.ts @@ -16,7 +16,7 @@ import { kubernetesTranslationRef as _kubernetesTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-kubernetes` instead. */ export const kubernetesTranslationRef = _kubernetesTranslationRef; diff --git a/plugins/notifications/report-alpha.api.md b/plugins/notifications/report-alpha.api.md index 84731628ae0fea..cfc88c3d7ac597 100644 --- a/plugins/notifications/report-alpha.api.md +++ b/plugins/notifications/report-alpha.api.md @@ -120,7 +120,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const notificationsTranslationRef: TranslationRef< 'plugin.notifications', { diff --git a/plugins/notifications/src/alpha.tsx b/plugins/notifications/src/alpha.tsx index 9c2196e5254c54..12ae164aa14fd4 100644 --- a/plugins/notifications/src/alpha.tsx +++ b/plugins/notifications/src/alpha.tsx @@ -59,7 +59,7 @@ export default createFrontendPlugin({ import { notificationsTranslationRef as _notificationsTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-notifications` instead. */ export const notificationsTranslationRef = _notificationsTranslationRef; diff --git a/plugins/org/report-alpha.api.md b/plugins/org/report-alpha.api.md index 0b8d974be53378..e3d61c263dc4dd 100644 --- a/plugins/org/report-alpha.api.md +++ b/plugins/org/report-alpha.api.md @@ -202,7 +202,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const orgTranslationRef: TranslationRef< 'org', { diff --git a/plugins/org/src/alpha.tsx b/plugins/org/src/alpha.tsx index 89acb8a6eea934..80155c8467e5ce 100644 --- a/plugins/org/src/alpha.tsx +++ b/plugins/org/src/alpha.tsx @@ -133,7 +133,7 @@ export default createFrontendPlugin({ import { orgTranslationRef as _orgTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-org` instead. */ export const orgTranslationRef = _orgTranslationRef; diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index a6c9fbb2325444..80b9d9bde3660d 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -307,7 +307,7 @@ export type ScaffolderReactComponentsNameToClassKey = { // @alpha (undocumented) export type ScaffolderReactTemplateCategoryPickerClassKey = 'root' | 'label'; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const scaffolderReactTranslationRef: TranslationRef< 'scaffolder-react', { diff --git a/plugins/scaffolder-react/src/alpha.ts b/plugins/scaffolder-react/src/alpha.ts index ab04bbf3f3da9c..dc51183658a27b 100644 --- a/plugins/scaffolder-react/src/alpha.ts +++ b/plugins/scaffolder-react/src/alpha.ts @@ -19,7 +19,7 @@ export * from './next'; import { scaffolderReactTranslationRef as _scaffolderReactTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-scaffolder-react` instead. */ export const scaffolderReactTranslationRef = _scaffolderReactTranslationRef; diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index e864b310d8e15f..60c455ece91a62 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -523,7 +523,7 @@ export type ScaffolderTemplateFormPreviewerClassKey = | 'textArea' | 'preview'; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const scaffolderTranslationRef: TranslationRef< 'scaffolder', { diff --git a/plugins/scaffolder/src/alpha/index.ts b/plugins/scaffolder/src/alpha/index.ts index 4d3366644c41ac..286101f9421e5a 100644 --- a/plugins/scaffolder/src/alpha/index.ts +++ b/plugins/scaffolder/src/alpha/index.ts @@ -25,7 +25,7 @@ export { import { scaffolderTranslationRef as _scaffolderTranslationRef } from '../translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-scaffolder` instead. */ export const scaffolderTranslationRef = _scaffolderTranslationRef; diff --git a/plugins/search-react/report-alpha.api.md b/plugins/search-react/report-alpha.api.md index 335a499318306f..c8a51f015544e7 100644 --- a/plugins/search-react/report-alpha.api.md +++ b/plugins/search-react/report-alpha.api.md @@ -96,7 +96,7 @@ export interface SearchFilterResultTypeBlueprintParams { value: string; } -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const searchReactTranslationRef: TranslationRef< 'search-react', { diff --git a/plugins/search-react/src/alpha/index.ts b/plugins/search-react/src/alpha/index.ts index 390a13cb4c0531..528b49d7dd1941 100644 --- a/plugins/search-react/src/alpha/index.ts +++ b/plugins/search-react/src/alpha/index.ts @@ -17,7 +17,7 @@ export * from './blueprints'; import { searchReactTranslationRef as _searchReactTranslationRef } from '../translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-search-react` instead. */ export const searchReactTranslationRef = _searchReactTranslationRef; diff --git a/plugins/search/report-alpha.api.md b/plugins/search/report-alpha.api.md index bcfb720c3610dc..9892469cc883b8 100644 --- a/plugins/search/report-alpha.api.md +++ b/plugins/search/report-alpha.api.md @@ -359,7 +359,7 @@ export const searchPage: OverridableExtensionDefinition<{ }; }>; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const searchTranslationRef: TranslationRef< 'search', { diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index c9ba79a52cb7ea..0c93fd71cca3c8 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -288,7 +288,7 @@ export default createFrontendPlugin({ import { searchTranslationRef as _searchTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-search` instead. */ export const searchTranslationRef = _searchTranslationRef; diff --git a/plugins/user-settings/report-alpha.api.md b/plugins/user-settings/report-alpha.api.md index 9d6439c1dfad7a..70a9ec869b74c3 100644 --- a/plugins/user-settings/report-alpha.api.md +++ b/plugins/user-settings/report-alpha.api.md @@ -155,7 +155,7 @@ export const settingsNavItem: OverridableExtensionDefinition<{ }; }>; -// @public @deprecated (undocumented) +// @alpha @deprecated (undocumented) export const userSettingsTranslationRef: TranslationRef< 'user-settings', { diff --git a/plugins/user-settings/src/alpha.tsx b/plugins/user-settings/src/alpha.tsx index c2273285e64736..0a267c14c62a2e 100644 --- a/plugins/user-settings/src/alpha.tsx +++ b/plugins/user-settings/src/alpha.tsx @@ -26,7 +26,7 @@ import { settingsRouteRef } from './plugin'; import { userSettingsTranslationRef as _userSettingsTranslationRef } from './translation'; /** - * @public + * @alpha * @deprecated Import from `@backstage/plugin-user-settings` instead. */ export const userSettingsTranslationRef = _userSettingsTranslationRef; From 956133bf457dacba8a16243c717690f8e67d2ffb Mon Sep 17 00:00:00 2001 From: Jon Koops Date: Tue, 17 Mar 2026 18:13:00 +0100 Subject: [PATCH 44/55] docs: recommend Node.js built-in proxy support for corporate proxies (#33006) Node.js 22.21.0+ natively supports HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables via NODE_USE_ENV_PROXY, eliminating the need for undici and global-agent workarounds. This also works with node-fetch and cross-fetch since they delegate to node:http/node:https without overriding the HTTP agent. Add a new corporate proxy tutorial under docs/ with the recommended approach and update the legacy guide in contrib/ to point to it. Update proxy references in the deployment, keeping-backstage-updated, and TechDocs CLI docs to mention NODE_USE_ENV_PROXY. Signed-off-by: Jon Koops --- .../help-im-behind-a-corporate-proxy.md | 5 ++- docs/deployment/index.md | 4 +-- docs/features/techdocs/cli.md | 7 ++-- .../keeping-backstage-updated.md | 13 +++++--- .../create-app/keeping-backstage-updated.md | 13 +++++--- docs/tutorials/corporate-proxy.md | 33 +++++++++++++++++++ 6 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 docs/tutorials/corporate-proxy.md diff --git a/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md b/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md index 4852443ecf8b57..4a7e0ad233f4bd 100644 --- a/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md +++ b/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md @@ -1,4 +1,7 @@ -# Running the backend behind a Corporate Proxy +# Legacy: Running the backend behind a Corporate Proxy + +> [!NOTE] +> On Node.js 22.21.0 or later, you can use Node.js's built-in proxy support instead of the workarounds described here. See the [recommended proxy setup guide](../../../docs/tutorials/corporate-proxy.md) for details. This article helps you get your backend installation up and running making calls through corporate proxies. diff --git a/docs/deployment/index.md b/docs/deployment/index.md index 76c8c129326c2f..a5f3c9ac7a631c 100644 --- a/docs/deployment/index.md +++ b/docs/deployment/index.md @@ -32,6 +32,4 @@ This method is covered in [Building a Docker image](docker.md) and There are many ways to deploy Backstage! You can find more examples in the community contributed guides found [here](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/). -If you need to run Backstage behind a corporate proxy, this -[contributed guide](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md) -may help. +If you need to run Backstage behind a corporate proxy, see the [corporate proxy guide](../tutorials/corporate-proxy.md). diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index f6be53cb55991d..fa3d9148e2e1a8 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -208,10 +208,13 @@ Options: #### Publishing from behind a proxy -For users attempting to publish TechDocs content behind a proxy, the TechDocs CLI leverages `global-agent` to navigate the proxy to successfully connect to that location. To enable `global-agent`, the following variables need to be set prior to running the techdocs-cli command: +On Node.js 22.21.0+, set `NODE_USE_ENV_PROXY=1` along with `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` to route TechDocs publishing through a proxy. See the [corporate proxy guide](../../tutorials/corporate-proxy.md) for details. + +On older Node.js versions, the TechDocs CLI leverages `global-agent` to navigate the proxy. To enable `global-agent`, the following variables need to be set prior to running the techdocs-cli command: ```bash -export GLOBAL_AGENT_HTTPS_PROXY=${HTTP_PROXY} +export GLOBAL_AGENT_HTTP_PROXY=${HTTP_PROXY} +export GLOBAL_AGENT_HTTPS_PROXY=${HTTPS_PROXY} export GLOBAL_AGENT_NO_PROXY=${NO_PROXY} ``` diff --git a/docs/getting-started/keeping-backstage-updated.md b/docs/getting-started/keeping-backstage-updated.md index c93c5200f6ba6b..a55f872f533329 100644 --- a/docs/getting-started/keeping-backstage-updated.md +++ b/docs/getting-started/keeping-backstage-updated.md @@ -151,12 +151,14 @@ down the number of duplicate packages. ## Proxy -The Backstage CLI uses [global-agent](https://www.npmjs.com/package/global-agent) and `undici` to configure HTTP/HTTPS proxy settings using environment variables. This allows you to route the CLI’s network traffic through a proxy server, which can be useful in environments with restricted internet access. +On Node.js 22.21.0+, the Backstage CLI respects the standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables when `NODE_USE_ENV_PROXY=1` is set. See the [corporate proxy guide](../tutorials/corporate-proxy.md) for full details. + +On older Node.js versions, the CLI falls back to [global-agent](https://www.npmjs.com/package/global-agent) and `undici` for proxy support, which require their own environment variables (prefixed with `GLOBAL_AGENT_`). This allows you to route the CLI’s network traffic through a proxy server, which can be useful in environments with restricted internet access. Additionally, yarn needs a proxy too (sometimes), when in environments with restricted internet access. It uses different settings than the other modules. If you decide to use the backstage yarn plugin [mentioned above](#plugin), you will need to set additional proxy values. If you will always need proxy settings in all environments and situations, you can add `httpProxy` and `httpsProxy` values to [the yarnrc.yml file](https://yarnpkg.com/configuration/yarnrc). If some environments need it (say a developer workstation) but other environments do not (perhaps a CI build server running on AWS), then you may not want to update the yarnrc.yml file but just set environment variables `YARN_HTTP_PROXY` and `YARN_HTTPS_PROXY` in the environments/situations where you need to proxy. -**If you plan to use the backstage yarn plugin, you will need these extra yarn proxy settings to both install the plugin and run the `versions:bump` command**. If you do not plan to use the backstage yarn plugin, it seems like the global agent proxy settings alone are sufficient. +**If you plan to use the backstage yarn plugin, you will need these extra yarn proxy settings to both install the plugin and run the `versions:bump` command**. If you do not plan to use the backstage yarn plugin, it seems like the proxy settings alone are sufficient. ### Example Configuration @@ -164,9 +166,10 @@ If you will always need proxy settings in all environments and situations, you c export HTTP_PROXY=http://proxy.company.com:8080 export HTTPS_PROXY=https://secure-proxy.company.com:8080 export NO_PROXY=localhost,internal.company.com -export GLOBAL_AGENT_HTTP_PROXY=${HTTP_PROXY} -export GLOBAL_AGENT_HTTPS_PROXY=${HTTPS_PROXY} -export GLOBAL_AGENT_NO_PROXY=${NO_PROXY} +export NODE_USE_ENV_PROXY=1 # Node.js 22.21.0+ +export GLOBAL_AGENT_HTTP_PROXY=${HTTP_PROXY} # Node.js < 22.21.0 +export GLOBAL_AGENT_HTTPS_PROXY=${HTTPS_PROXY} # Node.js < 22.21.0 +export GLOBAL_AGENT_NO_PROXY=${NO_PROXY} # Node.js < 22.21.0 export YARN_HTTP_PROXY=${HTTP_PROXY} # optional export YARN_HTTPS_PROXY=${HTTPS_PROXY} # optional ``` diff --git a/docs/golden-path/create-app/keeping-backstage-updated.md b/docs/golden-path/create-app/keeping-backstage-updated.md index de7d99b2b8692f..2692381737fd93 100644 --- a/docs/golden-path/create-app/keeping-backstage-updated.md +++ b/docs/golden-path/create-app/keeping-backstage-updated.md @@ -141,12 +141,14 @@ down the number of duplicate packages. ## Proxy -The Backstage CLI uses [global-agent](https://www.npmjs.com/package/global-agent) and `undici` to configure HTTP/HTTPS proxy settings using environment variables. This allows you to route the CLI’s network traffic through a proxy server, which can be useful in environments with restricted internet access. +On Node.js 22.21.0+, the Backstage CLI respects the standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables when `NODE_USE_ENV_PROXY=1` is set. See the [corporate proxy guide](../../tutorials/corporate-proxy.md) for full details. + +On older Node.js versions, the CLI falls back to [global-agent](https://www.npmjs.com/package/global-agent) and `undici` for proxy support, which require their own environment variables (prefixed with `GLOBAL_AGENT_`). This allows you to route the CLI’s network traffic through a proxy server, which can be useful in environments with restricted internet access. Additionally, `yarn` needs a proxy too (sometimes), when in environments with restricted internet access. It uses different settings than the other modules. If you decide to use the backstage yarn plugin [mentioned above](#plugin), you will need to set additional proxy values. If you will always need proxy settings in all environments and situations, you can add `httpProxy` and `httpsProxy` values to [the yarnrc.yml file](https://yarnpkg.com/configuration/yarnrc). If some environments need it (say a developer workstation) but other environments do not (perhaps a CI build server running on AWS), then you may not want to update the yarnrc.yml file but just set environment variables `YARN_HTTP_PROXY` and `YARN_HTTPS_PROXY` in the environments/situations where you need to proxy. -**If you plan to use the backstage yarn plugin, you will need these extra yarn proxy settings to both install the plugin and run the `versions:bump` command**. If you do not plan to use the backstage yarn plugin, it seems like the global agent proxy settings alone are sufficient. +**If you plan to use the backstage yarn plugin, you will need these extra yarn proxy settings to both install the plugin and run the `versions:bump` command**. If you do not plan to use the backstage yarn plugin, it seems like the proxy settings alone are sufficient. ### Example Configuration @@ -154,9 +156,10 @@ If you will always need proxy settings in all environments and situations, you c export HTTP_PROXY=http://proxy.company.com:8080 export HTTPS_PROXY=https://secure-proxy.company.com:8080 export NO_PROXY=localhost,internal.company.com -export GLOBAL_AGENT_HTTP_PROXY=${HTTP_PROXY} -export GLOBAL_AGENT_HTTPS_PROXY=${HTTPS_PROXY} -export GLOBAL_AGENT_NO_PROXY=${NO_PROXY} +export NODE_USE_ENV_PROXY=1 # Node.js 22.21.0+ +export GLOBAL_AGENT_HTTP_PROXY=${HTTP_PROXY} # Node.js < 22.21.0 +export GLOBAL_AGENT_HTTPS_PROXY=${HTTPS_PROXY} # Node.js < 22.21.0 +export GLOBAL_AGENT_NO_PROXY=${NO_PROXY} # Node.js < 22.21.0 export YARN_HTTP_PROXY=${HTTP_PROXY} # optional export YARN_HTTPS_PROXY=${HTTPS_PROXY} # optional ``` diff --git a/docs/tutorials/corporate-proxy.md b/docs/tutorials/corporate-proxy.md new file mode 100644 index 00000000000000..e4346e4f9479c7 --- /dev/null +++ b/docs/tutorials/corporate-proxy.md @@ -0,0 +1,33 @@ +--- +id: corporate-proxy +title: Running Backstage behind a Corporate Proxy +description: Guide on how to configure Backstage to work behind a corporate proxy using Node.js built-in proxy support. +--- + +# Running Backstage behind a Corporate Proxy + +Sometimes you have to run Backstage with no direct access to the public internet, except through a corporate proxy. The backend is most likely where you'll run into proxy issues, as it isn't helped by your browser or OS proxy settings. + +## Using Node.js built-in proxy support + +Starting with Node.js 22.21.0 and 24.5.0, Node.js has built-in support for proxy environment variables. When enabled, native `fetch()`, `node:http`, and `node:https` all respect the standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables without any additional packages. See the [Node.js enterprise network configuration docs](https://nodejs.org/en/learn/http/enterprise-network-configuration#proxy-configuration) for full details. + +To enable the built-in proxy support, set the `NODE_USE_ENV_PROXY` environment variable along with your proxy settings: + +```sh +export HTTP_PROXY=http://username:password@proxy.example.net:8888 +export HTTPS_PROXY=http://username:password@proxy.example.net:8888 +export NO_PROXY=localhost,127.0.0.1,.internal.company.com +export NODE_USE_ENV_PROXY=1 +yarn start +``` + +### Compatibility with third-party fetch libraries + +Per [ADR014](../architecture-decisions/adr014-use-fetch.md), Backstage backend code should use native `fetch()`, which works with Node.js's proxy out of the box. Some core packages and many [community plugins](https://github.com/backstage/community-plugins/) still use `node-fetch` (see [ADR013](../architecture-decisions/adr013-use-node-fetch.md)) or `cross-fetch` (for isomorphic packages). Both libraries delegate to `node:http`/`node:https` internally and do **not** set a custom HTTP agent by default, which means Node.js's proxy works for them as well. + +The exception is code that explicitly passes a custom `agent` to its fetch calls (e.g. the Kubernetes plugins, which use `new https.Agent(...)` for TLS client certificates). In those cases, the custom agent takes precedence and the built-in proxy is bypassed. This is generally the desired behavior, since those agents are configured for direct connections to specific endpoints like cluster APIs. + +## Legacy approach + +If you are on a Node.js version older than 22.21.0, you can use third-party packages to add proxy support. See the [legacy proxy setup guide](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md) for instructions using `undici`, `global-agent`, and `proxy-agent`. From 683e49ca327c1cf0d1fe0ba2ba2b2ec47a2866b0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 19:04:56 +0100 Subject: [PATCH 45/55] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Patrik Oldsberg --- docs/frontend-system/building-apps/01-index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/building-apps/01-index.md b/docs/frontend-system/building-apps/01-index.md index bedf42033f2610..d24e1622fd14a3 100644 --- a/docs/frontend-system/building-apps/01-index.md +++ b/docs/frontend-system/building-apps/01-index.md @@ -18,7 +18,7 @@ The create-app CLI requires Node.js Active LTS Release, see the [prerequisites d ::: ```sh -# The command bellow creates a Backstage App inside the current folder. +# The command below creates a Backstage App inside the current folder. # The name of the app-folder is the name that was provided when prompted. npx @backstage/create-app@latest ``` From 12fa965e67fd13cbaf7c71f58a3463e2bb18379b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 13:17:09 +0100 Subject: [PATCH 46/55] cli-node: add CliAuth class for shared CLI authentication Introduces a class-based authentication management API in @backstage/cli-node that reads the on-disk instance store, transparently refreshes expired tokens, and provides a convenient surface for other CLI modules to consume. The split keeps filesystem-based instance selection and writes owned by cli-module-auth, while reading and consuming the current instance is available through CliAuth in cli-node. Migrates cli-module-actions to use the new API and deprecates the ad-hoc function exports from cli-module-auth. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/cli-module-actions-use-cli-auth.md | 5 + .../cli-module-auth-deprecate-exports.md | 5 + .changeset/cli-node-auth-api.md | 5 + .../src/commands/sourcesAdd.ts | 15 +- .../src/commands/sourcesList.ts | 11 +- .../src/commands/sourcesRemove.ts | 15 +- .../src/lib/ActionsClient.test.ts | 14 +- .../src/lib/ActionsClient.ts | 2 +- .../src/lib/resolveAuth.test.ts | 115 ++++------ .../cli-module-actions/src/lib/resolveAuth.ts | 29 +-- packages/cli-module-auth/report.api.md | 45 ++-- .../src/commands/printToken.ts | 19 +- packages/cli-module-auth/src/commands/show.ts | 22 +- packages/cli-module-auth/src/index.ts | 8 +- packages/cli-module-auth/src/lib/auth.ts | 15 +- packages/cli-module-auth/src/lib/http.ts | 26 +-- .../cli-module-auth/src/lib/secretStore.ts | 8 +- packages/cli-module-auth/src/lib/storage.ts | 19 +- packages/cli-node/package.json | 3 + packages/cli-node/report.api.md | 47 ++++ packages/cli-node/src/auth/CliAuth.test.ts | 210 ++++++++++++++++++ packages/cli-node/src/auth/CliAuth.ts | 156 +++++++++++++ packages/cli-node/src/auth/httpJson.ts | 41 ++++ packages/cli-node/src/auth/index.ts | 20 ++ packages/cli-node/src/auth/secretStore.ts | 112 ++++++++++ packages/cli-node/src/auth/storage.ts | 144 ++++++++++++ packages/cli-node/src/index.ts | 1 + yarn.lock | 4 + 28 files changed, 869 insertions(+), 247 deletions(-) create mode 100644 .changeset/cli-module-actions-use-cli-auth.md create mode 100644 .changeset/cli-module-auth-deprecate-exports.md create mode 100644 .changeset/cli-node-auth-api.md create mode 100644 packages/cli-node/src/auth/CliAuth.test.ts create mode 100644 packages/cli-node/src/auth/CliAuth.ts create mode 100644 packages/cli-node/src/auth/httpJson.ts create mode 100644 packages/cli-node/src/auth/index.ts create mode 100644 packages/cli-node/src/auth/secretStore.ts create mode 100644 packages/cli-node/src/auth/storage.ts diff --git a/.changeset/cli-module-actions-use-cli-auth.md b/.changeset/cli-module-actions-use-cli-auth.md new file mode 100644 index 00000000000000..705ebac22056ee --- /dev/null +++ b/.changeset/cli-module-actions-use-cli-auth.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-module-actions': patch +--- + +Migrated to use `CliAuth` from `@backstage/cli-node` for authentication instead of importing individual functions from `@backstage/cli-module-auth`. diff --git a/.changeset/cli-module-auth-deprecate-exports.md b/.changeset/cli-module-auth-deprecate-exports.md new file mode 100644 index 00000000000000..5a8c79feef2e57 --- /dev/null +++ b/.changeset/cli-module-auth-deprecate-exports.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-module-auth': patch +--- + +Deprecated `getSelectedInstance`, `getInstanceConfig`, `accessTokenNeedsRefresh`, `refreshAccessToken`, `getSecretStore`, and `httpJson` exports in favor of the new `CliAuth` class and shared utilities from `@backstage/cli-node`. diff --git a/.changeset/cli-node-auth-api.md b/.changeset/cli-node-auth-api.md new file mode 100644 index 00000000000000..bed96811789666 --- /dev/null +++ b/.changeset/cli-node-auth-api.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': minor +--- + +Added `CliAuth` class for managing CLI authentication state. This provides a class-based API with a static `create` method that resolves the currently selected (or explicitly named) auth instance, transparently refreshes expired access tokens, and exposes helpers for other CLI modules to authenticate with a Backstage backend. Also added `httpJson`, `getSecretStore`, `SecretStore`, `StoredInstance`, and `HttpInit` exports. diff --git a/packages/cli-module-actions/src/commands/sourcesAdd.ts b/packages/cli-module-actions/src/commands/sourcesAdd.ts index 2a8135b63f8d5b..6d9ea2b524f804 100644 --- a/packages/cli-module-actions/src/commands/sourcesAdd.ts +++ b/packages/cli-module-actions/src/commands/sourcesAdd.ts @@ -15,12 +15,8 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '@backstage/cli-node'; -import { - getSelectedInstance, - getInstanceConfig, - updateInstanceConfig, -} from '@backstage/cli-module-auth'; +import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; +import { updateInstanceConfig } from '@backstage/cli-module-auth'; export default async ({ args, info }: CliCommandContext) => { const parsed = cli( @@ -34,9 +30,8 @@ export default async ({ args, info }: CliCommandContext) => { const pluginId = parsed._[0]; - const instance = await getSelectedInstance(); - const existing = - (await getInstanceConfig(instance.name, 'pluginSources')) ?? []; + const auth = await CliAuth.create(); + const existing = (await auth.getConfig('pluginSources')) ?? []; if (existing.includes(pluginId)) { process.stderr.write( @@ -45,7 +40,7 @@ export default async ({ args, info }: CliCommandContext) => { return; } - await updateInstanceConfig(instance.name, 'pluginSources', [ + await updateInstanceConfig(auth.instanceName, 'pluginSources', [ ...existing, pluginId, ]); diff --git a/packages/cli-module-actions/src/commands/sourcesList.ts b/packages/cli-module-actions/src/commands/sourcesList.ts index 68b368efd5db2f..23a6a6449ff8b1 100644 --- a/packages/cli-module-actions/src/commands/sourcesList.ts +++ b/packages/cli-module-actions/src/commands/sourcesList.ts @@ -15,18 +15,13 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '@backstage/cli-node'; -import { - getSelectedInstance, - getInstanceConfig, -} from '@backstage/cli-module-auth'; +import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { cli({ help: info }, undefined, args); - const instance = await getSelectedInstance(); - const sources = - (await getInstanceConfig(instance.name, 'pluginSources')) ?? []; + const auth = await CliAuth.create(); + const sources = (await auth.getConfig('pluginSources')) ?? []; if (!sources.length) { process.stderr.write('No plugin sources configured.\n'); diff --git a/packages/cli-module-actions/src/commands/sourcesRemove.ts b/packages/cli-module-actions/src/commands/sourcesRemove.ts index 731abe790dbabc..81a2d5abf300c8 100644 --- a/packages/cli-module-actions/src/commands/sourcesRemove.ts +++ b/packages/cli-module-actions/src/commands/sourcesRemove.ts @@ -15,12 +15,8 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '@backstage/cli-node'; -import { - getSelectedInstance, - getInstanceConfig, - updateInstanceConfig, -} from '@backstage/cli-module-auth'; +import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; +import { updateInstanceConfig } from '@backstage/cli-module-auth'; export default async ({ args, info }: CliCommandContext) => { const parsed = cli( @@ -34,9 +30,8 @@ export default async ({ args, info }: CliCommandContext) => { const pluginId = parsed._[0]; - const instance = await getSelectedInstance(); - const existing = - (await getInstanceConfig(instance.name, 'pluginSources')) ?? []; + const auth = await CliAuth.create(); + const existing = (await auth.getConfig('pluginSources')) ?? []; if (!existing.includes(pluginId)) { process.stderr.write(`Plugin source "${pluginId}" is not configured.\n`); @@ -44,7 +39,7 @@ export default async ({ args, info }: CliCommandContext) => { } await updateInstanceConfig( - instance.name, + auth.instanceName, 'pluginSources', existing.filter(s => s !== pluginId), ); diff --git a/packages/cli-module-actions/src/lib/ActionsClient.test.ts b/packages/cli-module-actions/src/lib/ActionsClient.test.ts index 61526873c988f6..ecf7861f26cd6f 100644 --- a/packages/cli-module-actions/src/lib/ActionsClient.test.ts +++ b/packages/cli-module-actions/src/lib/ActionsClient.test.ts @@ -15,11 +15,15 @@ */ import { ActionsClient } from './ActionsClient'; -import { httpJson } from '@backstage/cli-module-auth'; - -jest.mock('@backstage/cli-module-auth', () => ({ - httpJson: jest.fn(), -})); +import { httpJson } from '@backstage/cli-node'; + +jest.mock('@backstage/cli-node', () => { + const actual = jest.requireActual('@backstage/cli-node'); + return { + ...actual, + httpJson: jest.fn(), + }; +}); const mockHttpJson = httpJson as jest.MockedFunction; diff --git a/packages/cli-module-actions/src/lib/ActionsClient.ts b/packages/cli-module-actions/src/lib/ActionsClient.ts index aa04902b2faa98..f7d284ad1a0228 100644 --- a/packages/cli-module-actions/src/lib/ActionsClient.ts +++ b/packages/cli-module-actions/src/lib/ActionsClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { httpJson } from '@backstage/cli-module-auth'; +import { httpJson } from '@backstage/cli-node'; export type ActionDef = { id: string; diff --git a/packages/cli-module-actions/src/lib/resolveAuth.test.ts b/packages/cli-module-actions/src/lib/resolveAuth.test.ts index f2702cf757666b..21cf6ebc386298 100644 --- a/packages/cli-module-actions/src/lib/resolveAuth.test.ts +++ b/packages/cli-module-actions/src/lib/resolveAuth.test.ts @@ -15,39 +15,17 @@ */ import { resolveAuth } from './resolveAuth'; -import { - getSelectedInstance, - getInstanceConfig, - accessTokenNeedsRefresh, - refreshAccessToken, - getSecretStore, - type StoredInstance, -} from '@backstage/cli-module-auth'; - -jest.mock('@backstage/cli-module-auth', () => ({ - getSelectedInstance: jest.fn(), - getInstanceConfig: jest.fn(), - accessTokenNeedsRefresh: jest.fn(), - refreshAccessToken: jest.fn(), - getSecretStore: jest.fn(), -})); - -const mockGetSelectedInstance = getSelectedInstance as jest.MockedFunction< - typeof getSelectedInstance ->; -const mockGetInstanceConfig = getInstanceConfig as jest.MockedFunction< - typeof getInstanceConfig ->; -const mockAccessTokenNeedsRefresh = - accessTokenNeedsRefresh as jest.MockedFunction< - typeof accessTokenNeedsRefresh - >; -const mockRefreshAccessToken = refreshAccessToken as jest.MockedFunction< - typeof refreshAccessToken ->; -const mockGetSecretStore = getSecretStore as jest.MockedFunction< - typeof getSecretStore ->; +import { CliAuth, type StoredInstance } from '@backstage/cli-node'; + +jest.mock('@backstage/cli-node', () => { + const actual = jest.requireActual('@backstage/cli-node'); + return { + ...actual, + CliAuth: { create: jest.fn() }, + }; +}); + +const mockCreate = CliAuth.create as jest.MockedFunction; describe('resolveAuth', () => { const mockInstance: StoredInstance = { @@ -58,27 +36,22 @@ describe('resolveAuth', () => { accessTokenExpiresAt: Date.now() + 3600_000, }; - const mockSecretStore = { - get: jest.fn(), - set: jest.fn(), - delete: jest.fn(), - }; - beforeEach(() => { jest.clearAllMocks(); - mockGetSelectedInstance.mockResolvedValue(mockInstance); - mockAccessTokenNeedsRefresh.mockReturnValue(false); - mockGetSecretStore.mockResolvedValue(mockSecretStore); - mockSecretStore.get.mockResolvedValue('test-access-token'); - mockGetInstanceConfig.mockResolvedValue(['catalog', 'scaffolder']); }); it('resolves auth with the selected instance and stored token', async () => { + mockCreate.mockResolvedValue({ + instance: mockInstance, + instanceName: mockInstance.name, + baseUrl: mockInstance.baseUrl, + getAccessToken: jest.fn().mockResolvedValue('test-access-token'), + getConfig: jest.fn().mockResolvedValue(['catalog', 'scaffolder']), + } as unknown as CliAuth); + const result = await resolveAuth(); - expect(mockGetSelectedInstance).toHaveBeenCalledWith(undefined); - expect(mockAccessTokenNeedsRefresh).toHaveBeenCalledWith(mockInstance); - expect(mockRefreshAccessToken).not.toHaveBeenCalled(); + expect(mockCreate).toHaveBeenCalledWith({ instanceName: undefined }); expect(result).toEqual({ instance: mockInstance, accessToken: 'test-access-token', @@ -86,28 +59,32 @@ describe('resolveAuth', () => { }); }); - it('passes instance name flag to getSelectedInstance', async () => { - await resolveAuth('staging'); - - expect(mockGetSelectedInstance).toHaveBeenCalledWith('staging'); - }); - - it('refreshes the access token when it is about to expire', async () => { - const refreshedInstance = { - ...mockInstance, - accessTokenExpiresAt: Date.now() + 7200_000, - }; - mockAccessTokenNeedsRefresh.mockReturnValue(true); - mockRefreshAccessToken.mockResolvedValue(refreshedInstance); + it('passes instance name flag to CliAuth.create', async () => { + mockCreate.mockResolvedValue({ + instance: mockInstance, + instanceName: mockInstance.name, + baseUrl: mockInstance.baseUrl, + getAccessToken: jest.fn().mockResolvedValue('test-access-token'), + getConfig: jest.fn().mockResolvedValue([]), + } as unknown as CliAuth); - const result = await resolveAuth(); + await resolveAuth('staging'); - expect(mockRefreshAccessToken).toHaveBeenCalledWith('production'); - expect(result.instance).toBe(refreshedInstance); + expect(mockCreate).toHaveBeenCalledWith({ instanceName: 'staging' }); }); - it('throws when no access token is stored', async () => { - mockSecretStore.get.mockResolvedValue(undefined); + it('throws when getAccessToken fails', async () => { + mockCreate.mockResolvedValue({ + instance: mockInstance, + instanceName: mockInstance.name, + baseUrl: mockInstance.baseUrl, + getAccessToken: jest + .fn() + .mockRejectedValue( + new Error('No access token found. Run "auth login" to authenticate.'), + ), + getConfig: jest.fn().mockResolvedValue([]), + } as unknown as CliAuth); await expect(resolveAuth()).rejects.toThrow( 'No access token found. Run "auth login" to authenticate.', @@ -115,7 +92,13 @@ describe('resolveAuth', () => { }); it('returns empty plugin sources when none are configured', async () => { - mockGetInstanceConfig.mockResolvedValue(undefined); + mockCreate.mockResolvedValue({ + instance: mockInstance, + instanceName: mockInstance.name, + baseUrl: mockInstance.baseUrl, + getAccessToken: jest.fn().mockResolvedValue('test-access-token'), + getConfig: jest.fn().mockResolvedValue(undefined), + } as unknown as CliAuth); const result = await resolveAuth(); diff --git a/packages/cli-module-actions/src/lib/resolveAuth.ts b/packages/cli-module-actions/src/lib/resolveAuth.ts index 11fb6465451135..18664b1b552667 100644 --- a/packages/cli-module-actions/src/lib/resolveAuth.ts +++ b/packages/cli-module-actions/src/lib/resolveAuth.ts @@ -14,35 +14,16 @@ * limitations under the License. */ -import { - getSelectedInstance, - getInstanceConfig, - accessTokenNeedsRefresh, - refreshAccessToken, - getSecretStore, - type StoredInstance, -} from '@backstage/cli-module-auth'; +import { CliAuth, type StoredInstance } from '@backstage/cli-node'; export async function resolveAuth(instanceFlag?: string): Promise<{ instance: StoredInstance; accessToken: string; pluginSources: string[]; }> { - let instance = await getSelectedInstance(instanceFlag); + const auth = await CliAuth.create({ instanceName: instanceFlag }); + const accessToken = await auth.getAccessToken(); + const pluginSources = (await auth.getConfig('pluginSources')) ?? []; - if (accessTokenNeedsRefresh(instance)) { - instance = await refreshAccessToken(instance.name); - } - - const secretStore = await getSecretStore(); - const service = `backstage-cli:auth-instance:${instance.name}`; - const accessToken = await secretStore.get(service, 'accessToken'); - if (!accessToken) { - throw new Error('No access token found. Run "auth login" to authenticate.'); - } - - const pluginSources = - (await getInstanceConfig(instance.name, 'pluginSources')) ?? []; - - return { instance, accessToken, pluginSources }; + return { instance: auth.instance, accessToken, pluginSources }; } diff --git a/packages/cli-module-auth/report.api.md b/packages/cli-module-auth/report.api.md index a3b98e2f02d0f7..39011f0bac2c3c 100644 --- a/packages/cli-module-auth/report.api.md +++ b/packages/cli-module-auth/report.api.md @@ -4,61 +4,44 @@ ```ts import { CliModule } from '@backstage/cli-node'; +import { getSecretStore } from '@backstage/cli-node'; +import { HttpInit } from '@backstage/cli-node'; +import { httpJson } from '@backstage/cli-node'; +import { SecretStore } from '@backstage/cli-node'; +import { StoredInstance } from '@backstage/cli-node'; -// @public (undocumented) +// @public @deprecated (undocumented) export function accessTokenNeedsRefresh(instance: StoredInstance): boolean; // @public (undocumented) const _default: CliModule; export default _default; -// @public (undocumented) +// @public @deprecated (undocumented) export function getInstanceConfig( instanceName: string, key: string, ): Promise; -// @public (undocumented) -export function getSecretStore(): Promise; +export { getSecretStore }; -// @public (undocumented) +// @public @deprecated (undocumented) export function getSelectedInstance( instanceName?: string, ): Promise; -// @public (undocumented) -export type HttpInit = { - headers?: Record; - method?: string; - body?: any; - signal?: AbortSignal; -}; +export { HttpInit }; -// @public (undocumented) -export function httpJson(url: string, init?: HttpInit): Promise; +export { httpJson }; -// @public (undocumented) +// @public @deprecated (undocumented) export function refreshAccessToken( instanceName: string, ): Promise; -// @public (undocumented) -export type SecretStore = { - get(service: string, account: string): Promise; - set(service: string, account: string, secret: string): Promise; - delete(service: string, account: string): Promise; -}; +export { SecretStore }; -// @public (undocumented) -export type StoredInstance = { - name: string; - baseUrl: string; - clientId: string; - issuedAt: number; - accessTokenExpiresAt: number; - selected?: boolean; - config?: Record; -}; +export { StoredInstance }; // @public (undocumented) export function updateInstanceConfig( diff --git a/packages/cli-module-auth/src/commands/printToken.ts b/packages/cli-module-auth/src/commands/printToken.ts index 39a78c1832d5a4..45b4cfd9f6982a 100644 --- a/packages/cli-module-auth/src/commands/printToken.ts +++ b/packages/cli-module-auth/src/commands/printToken.ts @@ -15,10 +15,7 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '@backstage/cli-node'; -import { accessTokenNeedsRefresh, refreshAccessToken } from '../lib/auth'; -import { getSelectedInstance } from '../lib/storage'; -import { getSecretStore } from '../lib/secretStore'; +import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { const { @@ -37,18 +34,8 @@ export default async ({ args, info }: CliCommandContext) => { args, ); - let instance = await getSelectedInstance(instanceFlag); - - if (accessTokenNeedsRefresh(instance)) { - instance = await refreshAccessToken(instance.name); - } - - const secretStore = await getSecretStore(); - const service = `backstage-cli:auth-instance:${instance.name}`; - const accessToken = await secretStore.get(service, 'accessToken'); - if (!accessToken) { - throw new Error('No access token found. Run "auth login" to authenticate.'); - } + const auth = await CliAuth.create({ instanceName: instanceFlag }); + const accessToken = await auth.getAccessToken(); process.stdout.write(`${accessToken}\n`); }; diff --git a/packages/cli-module-auth/src/commands/show.ts b/packages/cli-module-auth/src/commands/show.ts index e1b7c62f72b4c3..f9a3f4eb90cbf3 100644 --- a/packages/cli-module-auth/src/commands/show.ts +++ b/packages/cli-module-auth/src/commands/show.ts @@ -15,11 +15,7 @@ */ import { cli } from 'cleye'; -import type { CliCommandContext } from '@backstage/cli-node'; -import { httpJson } from '../lib/http'; -import { getSelectedInstance } from '../lib/storage'; -import { accessTokenNeedsRefresh, refreshAccessToken } from '../lib/auth'; -import { getSecretStore } from '../lib/secretStore'; +import { CliAuth, httpJson, type CliCommandContext } from '@backstage/cli-node'; export default async ({ args, info }: CliCommandContext) => { const { @@ -38,23 +34,13 @@ export default async ({ args, info }: CliCommandContext) => { args, ); - let instance = await getSelectedInstance(instanceFlag); + const auth = await CliAuth.create({ instanceName: instanceFlag }); + const accessToken = await auth.getAccessToken(); - if (accessTokenNeedsRefresh(instance)) { - process.stdout.write('Refreshing access token...\n'); - instance = await refreshAccessToken(instance.name); - } - const authBase = new URL('/api/auth', instance.baseUrl) + const authBase = new URL('/api/auth', auth.baseUrl) .toString() .replace(/\/$/, ''); - const secretStore = await getSecretStore(); - const service = `backstage-cli:auth-instance:${instance.name}`; - const accessToken = await secretStore.get(service, 'accessToken'); - if (!accessToken) { - throw new Error('No access token found. Run "auth login" to authenticate.'); - } - const userinfo = await httpJson<{ claims: { sub: string; ent: string[] } }>( `${authBase}/v1/userinfo`, { diff --git a/packages/cli-module-auth/src/index.ts b/packages/cli-module-auth/src/index.ts index 38567e2174d89f..a8079932d4233e 100644 --- a/packages/cli-module-auth/src/index.ts +++ b/packages/cli-module-auth/src/index.ts @@ -53,16 +53,16 @@ export default createCliModule({ }, }); -/** @public */ +/** @public @deprecated Use {@link @backstage/cli-node#CliAuth} instead. */ export { getSelectedInstance, getInstanceConfig, updateInstanceConfig, type StoredInstance, } from './lib/storage'; -/** @public */ +/** @public @deprecated Use {@link @backstage/cli-node#CliAuth} instead. */ export { accessTokenNeedsRefresh, refreshAccessToken } from './lib/auth'; -/** @public */ +/** @public @deprecated Import from {@link @backstage/cli-node} instead. */ export { getSecretStore, type SecretStore } from './lib/secretStore'; -/** @public */ +/** @public @deprecated Import from {@link @backstage/cli-node} instead. */ export { httpJson, type HttpInit } from './lib/http'; diff --git a/packages/cli-module-auth/src/lib/auth.ts b/packages/cli-module-auth/src/lib/auth.ts index 36bdf3dfbce9fb..d53aad6cb2bf35 100644 --- a/packages/cli-module-auth/src/lib/auth.ts +++ b/packages/cli-module-auth/src/lib/auth.ts @@ -15,12 +15,8 @@ */ import { z } from 'zod/v3'; -import { - StoredInstance, - upsertInstance, - withMetadataLock, - getInstanceByName, -} from './storage'; +import type { StoredInstance } from '@backstage/cli-node'; +import { upsertInstance, withMetadataLock, getInstanceByName } from './storage'; import { getSecretStore } from './secretStore'; import { httpJson } from './http'; @@ -31,12 +27,13 @@ const TokenResponseSchema = z.object({ refresh_token: z.string().min(1).optional(), }); -/** @public */ +/** @public @deprecated Use {@link @backstage/cli-node#CliAuth} instead. */ export function accessTokenNeedsRefresh(instance: StoredInstance): boolean { - return instance.accessTokenExpiresAt <= Date.now() + 2 * 60_000; // 2 minutes before expiration + // 2 minutes before expiration + return instance.accessTokenExpiresAt <= Date.now() + 2 * 60_000; } -/** @public */ +/** @public @deprecated Use {@link @backstage/cli-node#CliAuth} instead. */ export async function refreshAccessToken( instanceName: string, ): Promise { diff --git a/packages/cli-module-auth/src/lib/http.ts b/packages/cli-module-auth/src/lib/http.ts index 4861a0772232df..df0704fcf4feca 100644 --- a/packages/cli-module-auth/src/lib/http.ts +++ b/packages/cli-module-auth/src/lib/http.ts @@ -14,28 +14,4 @@ * limitations under the License. */ -import { ResponseError } from '@backstage/errors'; - -/** @public */ -export type HttpInit = { - headers?: Record; - method?: string; - body?: any; - signal?: AbortSignal; -}; - -/** @public */ -export async function httpJson(url: string, init?: HttpInit): Promise { - const res = await fetch(url, { - ...init, - body: init?.body ? JSON.stringify(init.body) : undefined, - headers: { - ...(init?.body ? { 'Content-Type': 'application/json' } : {}), - ...init?.headers, - }, - }); - if (!res.ok) { - throw await ResponseError.fromResponse(res); - } - return (await res.json()) as T; -} +export { httpJson, type HttpInit } from '@backstage/cli-node'; diff --git a/packages/cli-module-auth/src/lib/secretStore.ts b/packages/cli-module-auth/src/lib/secretStore.ts index a0f3c81d51484c..f7aa5256ae3af8 100644 --- a/packages/cli-module-auth/src/lib/secretStore.ts +++ b/packages/cli-module-auth/src/lib/secretStore.ts @@ -18,12 +18,8 @@ import fs from 'fs-extra'; import os from 'node:os'; import path from 'node:path'; -/** @public */ -export type SecretStore = { - get(service: string, account: string): Promise; - set(service: string, account: string, secret: string): Promise; - delete(service: string, account: string): Promise; -}; +export type { SecretStore } from '@backstage/cli-node'; +import type { SecretStore } from '@backstage/cli-node'; async function loadKeytar(): Promise { try { diff --git a/packages/cli-module-auth/src/lib/storage.ts b/packages/cli-module-auth/src/lib/storage.ts index 0e7668a0eafd0d..b50b4721574241 100644 --- a/packages/cli-module-auth/src/lib/storage.ts +++ b/packages/cli-module-auth/src/lib/storage.ts @@ -22,6 +22,9 @@ import lockfile from 'proper-lockfile'; import YAML from 'yaml'; import { z } from 'zod/v3'; +export type { StoredInstance } from '@backstage/cli-node'; +import type { StoredInstance } from '@backstage/cli-node'; + const METADATA_FILE = 'auth-instances.yaml'; const INSTANCE_NAME_PATTERN = /^[a-zA-Z0-9._:@-]+$/; @@ -39,17 +42,6 @@ const storedInstanceSchema = z.object({ config: z.record(z.string(), z.unknown()).optional(), }); -/** @public */ -export type StoredInstance = { - name: string; - baseUrl: string; - clientId: string; - issuedAt: number; - accessTokenExpiresAt: number; - selected?: boolean; - config?: Record; -}; - const authYamlSchema = z.object({ instances: z.array(storedInstanceSchema).default([]), }); @@ -99,7 +91,6 @@ export async function getAllInstances(): Promise<{ const { instances } = await readAll(); const selected = instances.find(i => i.selected) ?? instances[0]; return { - // Normalize selection prop instances: instances.map(i => ({ ...i, selected: i.name === selected.name, @@ -108,7 +99,7 @@ export async function getAllInstances(): Promise<{ }; } -/** @public */ +/** @public @deprecated Use {@link @backstage/cli-node#CliAuth} instead. */ export async function getSelectedInstance( instanceName?: string, ): Promise { @@ -171,7 +162,7 @@ export async function setSelectedInstance(name: string): Promise { }); } -/** @public */ +/** @public @deprecated Use {@link @backstage/cli-node#CliAuth.getConfig} instead. */ export async function getInstanceConfig( instanceName: string, key: string, diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index fbefa34fcc00c3..4aeab07e7baa7e 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -52,6 +52,9 @@ "@backstage/test-utils": "workspace:^", "@types/yarnpkg__lockfile": "^1.1.4" }, + "optionalDependencies": { + "keytar": "^7.9.0" + }, "peerDependencies": { "@swc/core": "^1.15.6" }, diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 2fac82080be19b..5af6665789539f 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -86,6 +86,21 @@ export interface BackstagePackageJson { version: string; } +// @public +export class CliAuth { + get baseUrl(): string; + static create(options?: CliAuthCreateOptions): Promise; + getAccessToken(): Promise; + getConfig(key: string): Promise; + get instance(): StoredInstance; + get instanceName(): string; +} + +// @public +export interface CliAuthCreateOptions { + instanceName?: string; +} + // @public export interface CliCommand { deprecated?: boolean; @@ -133,6 +148,9 @@ export function createCliModule(options: { }) => Promise; }): CliModule; +// @public (undocumented) +export function getSecretStore(): Promise; + // @public export class GitUtils { static listChangedFiles(ref: string): Promise; @@ -142,6 +160,17 @@ export class GitUtils { // @public export function hasBackstageYarnPlugin(workspaceDir?: string): Promise; +// @public (undocumented) +export type HttpInit = { + headers?: Record; + method?: string; + body?: any; + signal?: AbortSignal; +}; + +// @public (undocumented) +export function httpJson(url: string, init?: HttpInit): Promise; + // @public export function isMonoRepo(): Promise; @@ -272,6 +301,24 @@ export function runWorkerQueueThreads( results: TResult[]; }>; +// @public (undocumented) +export type SecretStore = { + get(service: string, account: string): Promise; + set(service: string, account: string, secret: string): Promise; + delete(service: string, account: string): Promise; +}; + +// @public (undocumented) +export type StoredInstance = { + name: string; + baseUrl: string; + clientId: string; + issuedAt: number; + accessTokenExpiresAt: number; + selected?: boolean; + config?: Record; +}; + // @public export class SuccessCache { // (undocumented) diff --git a/packages/cli-node/src/auth/CliAuth.test.ts b/packages/cli-node/src/auth/CliAuth.test.ts new file mode 100644 index 00000000000000..16a9b10ddeea1f --- /dev/null +++ b/packages/cli-node/src/auth/CliAuth.test.ts @@ -0,0 +1,210 @@ +/* + * 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 { CliAuth } from './CliAuth'; +import * as storage from './storage'; +import * as secretStoreModule from './secretStore'; +import * as httpModule from './httpJson'; + +jest.mock('./storage'); +jest.mock('./secretStore'); +jest.mock('./httpJson'); + +const mockStorage = storage as jest.Mocked; +const mockSecretStoreModule = secretStoreModule as jest.Mocked< + typeof secretStoreModule +>; +const mockHttp = httpModule as jest.Mocked; + +describe('CliAuth', () => { + const now = Date.now(); + const mockInstance = { + name: 'production', + baseUrl: 'https://backstage.example.com', + clientId: 'prod-client', + issuedAt: now, + accessTokenExpiresAt: now + 3600_000, + }; + + const mockSecretStore = { + get: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockStorage.getSelectedInstance.mockResolvedValue(mockInstance); + mockSecretStoreModule.getSecretStore.mockResolvedValue(mockSecretStore); + mockStorage.accessTokenNeedsRefresh.mockReturnValue(false); + mockSecretStore.get.mockResolvedValue('test-access-token'); + }); + + describe('create', () => { + it('resolves the currently selected instance by default', async () => { + const auth = await CliAuth.create(); + + expect(mockStorage.getSelectedInstance).toHaveBeenCalledWith(undefined); + expect(auth.instance).toEqual(mockInstance); + expect(auth.instanceName).toBe('production'); + expect(auth.baseUrl).toBe('https://backstage.example.com'); + }); + + it('resolves a named instance when specified', async () => { + await CliAuth.create({ instanceName: 'staging' }); + + expect(mockStorage.getSelectedInstance).toHaveBeenCalledWith('staging'); + }); + + it('throws when no instance can be found', async () => { + mockStorage.getSelectedInstance.mockRejectedValue( + new Error( + 'No instances found. Run "auth login" to authenticate first.', + ), + ); + + await expect(CliAuth.create()).rejects.toThrow( + 'No instances found. Run "auth login" to authenticate first.', + ); + }); + }); + + describe('getAccessToken', () => { + it('returns a stored access token when it is still valid', async () => { + const auth = await CliAuth.create(); + const token = await auth.getAccessToken(); + + expect(token).toBe('test-access-token'); + expect(mockSecretStore.get).toHaveBeenCalledWith( + 'backstage-cli:auth-instance:production', + 'accessToken', + ); + expect(mockHttp.httpJson).not.toHaveBeenCalled(); + }); + + it('throws when no access token is stored', async () => { + mockSecretStore.get.mockResolvedValue(undefined); + + const auth = await CliAuth.create(); + + await expect(auth.getAccessToken()).rejects.toThrow( + 'No access token found. Run "auth login" to authenticate.', + ); + }); + + it('refreshes the token when it is about to expire', async () => { + mockStorage.accessTokenNeedsRefresh.mockReturnValue(true); + mockSecretStore.get.mockImplementation( + async (_service: string, account: string) => { + if (account === 'refreshToken') return 'old-refresh-token'; + if (account === 'accessToken') return 'new-access-token'; + return undefined; + }, + ); + + mockHttp.httpJson.mockResolvedValue({ + access_token: 'new-access-token', + token_type: 'Bearer', + expires_in: 3600, + refresh_token: 'new-refresh-token', + }); + + const auth = await CliAuth.create(); + const token = await auth.getAccessToken(); + + expect(token).toBe('new-access-token'); + expect(mockHttp.httpJson).toHaveBeenCalledWith( + 'https://backstage.example.com/api/auth/v1/token', + expect.objectContaining({ + method: 'POST', + body: { + grant_type: 'refresh_token', + refresh_token: 'old-refresh-token', + }, + }), + ); + expect(mockSecretStore.set).toHaveBeenCalledWith( + 'backstage-cli:auth-instance:production', + 'accessToken', + 'new-access-token', + ); + expect(mockSecretStore.set).toHaveBeenCalledWith( + 'backstage-cli:auth-instance:production', + 'refreshToken', + 'new-refresh-token', + ); + }); + + it('throws when refresh token is missing and access token has expired', async () => { + mockStorage.accessTokenNeedsRefresh.mockReturnValue(true); + mockSecretStore.get.mockResolvedValue(undefined); + + const auth = await CliAuth.create(); + + await expect(auth.getAccessToken()).rejects.toThrow( + 'Access token is expired and no refresh token is available', + ); + }); + + it('throws when the token response is malformed', async () => { + mockStorage.accessTokenNeedsRefresh.mockReturnValue(true); + mockSecretStore.get.mockImplementation( + async (_service: string, account: string) => { + if (account === 'refreshToken') return 'refresh-token'; + return undefined; + }, + ); + + mockHttp.httpJson.mockResolvedValue({ + token_type: 'Bearer', + expires_in: 3600, + }); + + const auth = await CliAuth.create(); + + await expect(auth.getAccessToken()).rejects.toThrow( + 'Invalid token response', + ); + }); + }); + + describe('getConfig', () => { + it('returns a config value from the instance', async () => { + mockStorage.getInstanceConfig.mockResolvedValue([ + 'catalog', + 'scaffolder', + ]); + + const auth = await CliAuth.create(); + const sources = await auth.getConfig('pluginSources'); + + expect(sources).toEqual(['catalog', 'scaffolder']); + expect(mockStorage.getInstanceConfig).toHaveBeenCalledWith( + 'production', + 'pluginSources', + ); + }); + + it('returns undefined for missing config keys', async () => { + mockStorage.getInstanceConfig.mockResolvedValue(undefined); + + const auth = await CliAuth.create(); + const value = await auth.getConfig('nonexistent'); + + expect(value).toBeUndefined(); + }); + }); +}); diff --git a/packages/cli-node/src/auth/CliAuth.ts b/packages/cli-node/src/auth/CliAuth.ts new file mode 100644 index 00000000000000..8934e9bd7f35af --- /dev/null +++ b/packages/cli-node/src/auth/CliAuth.ts @@ -0,0 +1,156 @@ +/* + * 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 { + type StoredInstance, + getSelectedInstance, + getInstanceConfig, + accessTokenNeedsRefresh, +} from './storage'; +import { getSecretStore, type SecretStore } from './secretStore'; +import { httpJson } from './httpJson'; +import { z } from 'zod'; + +const TokenResponseSchema = z.object({ + access_token: z.string().min(1), + token_type: z.string().min(1), + expires_in: z.number().positive().finite(), + refresh_token: z.string().min(1).optional(), +}); + +/** + * Options for creating a {@link CliAuth} instance. + * + * @public + */ +export interface CliAuthCreateOptions { + /** + * An explicit instance name to resolve. When omitted the currently + * selected instance is used. + */ + instanceName?: string; +} + +/** + * Manages authentication state for Backstage CLI commands. + * + * Reads the currently selected (or explicitly named) auth instance from + * the on-disk instance store, transparently refreshes expired access + * tokens, and exposes helpers that other CLI modules need to talk to a + * Backstage backend. + * + * @public + */ +export class CliAuth { + readonly #secretStore: SecretStore; + #instance: StoredInstance; + + /** + * Resolve the current auth instance and return a ready-to-use + * {@link CliAuth} object. Throws when no instance can be found. + */ + static async create(options?: CliAuthCreateOptions): Promise { + const instance = await getSelectedInstance(options?.instanceName); + const secretStore = await getSecretStore(); + return new CliAuth(instance, secretStore); + } + + private constructor(instance: StoredInstance, secretStore: SecretStore) { + this.#instance = instance; + this.#secretStore = secretStore; + } + + /** The resolved instance metadata. */ + get instance(): StoredInstance { + return this.#instance; + } + + /** Shorthand for `instance.name`. */ + get instanceName(): string { + return this.#instance.name; + } + + /** Shorthand for `instance.baseUrl`. */ + get baseUrl(): string { + return this.#instance.baseUrl; + } + + /** + * Returns a valid access token, refreshing it first if the current + * token is expired or about to expire. + */ + async getAccessToken(): Promise { + if (accessTokenNeedsRefresh(this.#instance)) { + await this.#refreshAccessToken(); + } + + const service = `backstage-cli:auth-instance:${this.#instance.name}`; + const token = await this.#secretStore.get(service, 'accessToken'); + if (!token) { + throw new Error( + 'No access token found. Run "auth login" to authenticate.', + ); + } + return token; + } + + /** + * Reads a per-instance configuration value previously stored by the + * auth module (e.g. `pluginSources`). + */ + async getConfig(key: string): Promise { + return getInstanceConfig(this.#instance.name, key); + } + + async #refreshAccessToken(): Promise { + const service = `backstage-cli:auth-instance:${this.#instance.name}`; + const refreshToken = + (await this.#secretStore.get(service, 'refreshToken')) ?? ''; + if (!refreshToken) { + throw new Error( + 'Access token is expired and no refresh token is available', + ); + } + + const response = await httpJson( + `${this.#instance.baseUrl}/api/auth/v1/token`, + { + method: 'POST', + body: { + grant_type: 'refresh_token', + refresh_token: refreshToken, + }, + signal: AbortSignal.timeout(30_000), + }, + ); + + const parsed = TokenResponseSchema.safeParse(response); + if (!parsed.success) { + throw new Error(`Invalid token response: ${parsed.error.message}`); + } + const token = parsed.data; + + await this.#secretStore.set(service, 'accessToken', token.access_token); + if (token.refresh_token) { + await this.#secretStore.set(service, 'refreshToken', token.refresh_token); + } + this.#instance = { + ...this.#instance, + issuedAt: Date.now(), + accessTokenExpiresAt: Date.now() + token.expires_in * 1000, + }; + } +} diff --git a/packages/cli-node/src/auth/httpJson.ts b/packages/cli-node/src/auth/httpJson.ts new file mode 100644 index 00000000000000..4861a0772232df --- /dev/null +++ b/packages/cli-node/src/auth/httpJson.ts @@ -0,0 +1,41 @@ +/* + * 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 { ResponseError } from '@backstage/errors'; + +/** @public */ +export type HttpInit = { + headers?: Record; + method?: string; + body?: any; + signal?: AbortSignal; +}; + +/** @public */ +export async function httpJson(url: string, init?: HttpInit): Promise { + const res = await fetch(url, { + ...init, + body: init?.body ? JSON.stringify(init.body) : undefined, + headers: { + ...(init?.body ? { 'Content-Type': 'application/json' } : {}), + ...init?.headers, + }, + }); + if (!res.ok) { + throw await ResponseError.fromResponse(res); + } + return (await res.json()) as T; +} diff --git a/packages/cli-node/src/auth/index.ts b/packages/cli-node/src/auth/index.ts new file mode 100644 index 00000000000000..8037f25714f6e1 --- /dev/null +++ b/packages/cli-node/src/auth/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { CliAuth, type CliAuthCreateOptions } from './CliAuth'; +export { type StoredInstance } from './storage'; +export { httpJson, type HttpInit } from './httpJson'; +export { getSecretStore, type SecretStore } from './secretStore'; diff --git a/packages/cli-node/src/auth/secretStore.ts b/packages/cli-node/src/auth/secretStore.ts new file mode 100644 index 00000000000000..a0f3c81d51484c --- /dev/null +++ b/packages/cli-node/src/auth/secretStore.ts @@ -0,0 +1,112 @@ +/* + * 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 fs from 'fs-extra'; +import os from 'node:os'; +import path from 'node:path'; + +/** @public */ +export type SecretStore = { + get(service: string, account: string): Promise; + set(service: string, account: string, secret: string): Promise; + delete(service: string, account: string): Promise; +}; + +async function loadKeytar(): Promise { + try { + // eslint-disable-next-line import/no-extraneous-dependencies, @backstage/no-undeclared-imports + const keytar = require('keytar') as typeof import('keytar'); + if (keytar && typeof keytar.getPassword === 'function') { + return keytar; + } + } catch { + // keytar not available + } + return undefined; +} + +class KeytarSecretStore implements SecretStore { + private readonly keytar: typeof import('keytar'); + constructor(keytar: typeof import('keytar')) { + this.keytar = keytar; + } + async get(service: string, account: string): Promise { + const result = await this.keytar.getPassword(service, account); + return result ?? undefined; + } + async set(service: string, account: string, secret: string): Promise { + await this.keytar.setPassword(service, account, secret); + } + async delete(service: string, account: string): Promise { + await this.keytar.deletePassword(service, account); + } +} + +class FileSecretStore implements SecretStore { + private readonly baseDir: string; + constructor() { + const root = + process.env.XDG_DATA_HOME || + (process.platform === 'win32' + ? process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming') + : path.join(os.homedir(), '.local', 'share')); + this.baseDir = path.join(root, 'backstage-cli', 'auth-secrets'); + } + private filePath(service: string, account: string): string { + return path.join( + this.baseDir, + encodeURIComponent(service), + `${encodeURIComponent(account)}.secret`, + ); + } + async get(service: string, account: string): Promise { + const file = this.filePath(service, account); + if (!(await fs.pathExists(file))) return undefined; + return await fs.readFile(file, 'utf8'); + } + async set(service: string, account: string, secret: string): Promise { + const file = this.filePath(service, account); + await fs.ensureDir(path.dirname(file)); + await fs.writeFile(file, secret, { encoding: 'utf8', mode: 0o600 }); + } + async delete(service: string, account: string): Promise { + const file = this.filePath(service, account); + await fs.remove(file); + } +} + +let singleton: SecretStore | undefined; + +/** @public */ +export async function getSecretStore(): Promise { + if (!singleton) { + const keytar = await loadKeytar(); + if (keytar) { + singleton = new KeytarSecretStore(keytar); + } else { + singleton = new FileSecretStore(); + } + } + return singleton; +} + +/** + * Reset the singleton instance (for testing purposes only) + * @internal + */ +export function resetSecretStore(): void { + singleton = undefined; +} diff --git a/packages/cli-node/src/auth/storage.ts b/packages/cli-node/src/auth/storage.ts new file mode 100644 index 00000000000000..e223cac45d89fe --- /dev/null +++ b/packages/cli-node/src/auth/storage.ts @@ -0,0 +1,144 @@ +/* + * 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 { NotFoundError } from '@backstage/errors'; +import fs from 'fs-extra'; +import os from 'node:os'; +import path from 'node:path'; +import YAML from 'yaml'; +import { z } from 'zod'; + +const METADATA_FILE = 'auth-instances.yaml'; + +const INSTANCE_NAME_PATTERN = /^[a-zA-Z0-9._:@-]+$/; + +const storedInstanceSchema = z.object({ + name: z + .string() + .min(1) + .regex(INSTANCE_NAME_PATTERN, 'Instance name contains invalid characters'), + baseUrl: z.string().url(), + clientId: z.string().min(1), + issuedAt: z.number().int().nonnegative(), + accessTokenExpiresAt: z.number().int().nonnegative(), + selected: z.boolean().optional(), + config: z.record(z.string(), z.unknown()).optional(), +}); + +/** @public */ +export type StoredInstance = { + name: string; + baseUrl: string; + clientId: string; + issuedAt: number; + accessTokenExpiresAt: number; + selected?: boolean; + config?: Record; +}; + +const authYamlSchema = z.object({ + instances: z.array(storedInstanceSchema).default([]), +}); + +/** @internal */ +export function getMetadataFilePath(): string { + const root = + process.env.XDG_CONFIG_HOME || + (process.platform === 'win32' + ? process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming') + : path.join(os.homedir(), '.config')); + + return path.join(root, 'backstage-cli', METADATA_FILE); +} + +/** @internal */ +export async function readAll(): Promise<{ instances: StoredInstance[] }> { + const file = getMetadataFilePath(); + if (!(await fs.pathExists(file))) { + return { instances: [] }; + } + const text = await fs.readFile(file, 'utf8'); + if (!text.trim()) { + return { instances: [] }; + } + try { + const doc = YAML.parse(text); + const parsed = authYamlSchema.safeParse(doc); + if (parsed.success) { + return parsed.data; + } + return { instances: [] }; + } catch { + return { instances: [] }; + } +} + +/** @internal */ +export async function getAllInstances(): Promise<{ + instances: StoredInstance[]; + selected: StoredInstance | undefined; +}> { + const { instances } = await readAll(); + const selected = instances.find(i => i.selected) ?? instances[0]; + return { + instances: instances.map(i => ({ + ...i, + selected: i.name === selected.name, + })), + selected, + }; +} + +/** @internal */ +export async function getSelectedInstance( + instanceName?: string, +): Promise { + if (instanceName) { + return await getInstanceByName(instanceName); + } + const { selected } = await getAllInstances(); + if (!selected) { + throw new Error( + 'No instances found. Run "auth login" to authenticate first.', + ); + } + return selected; +} + +/** @internal */ +export async function getInstanceByName(name: string): Promise { + const { instances } = await readAll(); + const instance = instances.find(i => i.name === name); + if (!instance) { + throw new NotFoundError(`Instance '${name}' not found`); + } + return instance; +} + +/** @internal */ +export async function getInstanceConfig( + instanceName: string, + key: string, +): Promise { + const instance = await getInstanceByName(instanceName); + return instance.config?.[key] as T | undefined; +} + +/** @internal */ +export function accessTokenNeedsRefresh(instance: StoredInstance): boolean { + // 2 minutes before expiration + return instance.accessTokenExpiresAt <= Date.now() + 2 * 60_000; +} diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index 8831753a820a03..a824673c1e0651 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ +export * from './auth'; export * from './cache'; export * from './cli-module'; export * from './concurrency'; diff --git a/yarn.lock b/yarn.lock index 4063ff72e86d5e..2a8c8e7f02d6f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3159,12 +3159,16 @@ __metadata: chalk: "npm:^4.0.0" commander: "npm:^12.0.0" fs-extra: "npm:^11.2.0" + keytar: "npm:^7.9.0" pirates: "npm:^4.0.6" semver: "npm:^7.5.3" yaml: "npm:^2.0.0" zod: "npm:^3.25.76 || ^4.0.0" peerDependencies: "@swc/core": ^1.15.6 + dependenciesMeta: + keytar: + optional: true peerDependenciesMeta: "@swc/core": optional: true From 3c6de38345b90201eca05aa4795323b0cdd2e685 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 13:32:46 +0100 Subject: [PATCH 47/55] Update API report for cli-module-auth Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-auth/report.api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli-module-auth/report.api.md b/packages/cli-module-auth/report.api.md index 39011f0bac2c3c..91191153c807f8 100644 --- a/packages/cli-module-auth/report.api.md +++ b/packages/cli-module-auth/report.api.md @@ -4,7 +4,6 @@ ```ts import { CliModule } from '@backstage/cli-node'; -import { getSecretStore } from '@backstage/cli-node'; import { HttpInit } from '@backstage/cli-node'; import { httpJson } from '@backstage/cli-node'; import { SecretStore } from '@backstage/cli-node'; @@ -23,7 +22,8 @@ export function getInstanceConfig( key: string, ): Promise; -export { getSecretStore }; +// @public (undocumented) +export function getSecretStore(): Promise; // @public @deprecated (undocumented) export function getSelectedInstance( From 8dda73d80641f8678e1579fd87ea36ea0d3729d8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 19:45:41 +0100 Subject: [PATCH 48/55] create-app: fix duplicate search item in next-app Signed-off-by: Patrik Oldsberg --- .../templates/next-app/packages/app/src/modules/nav/Sidebar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx b/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx index 8614f6fa0c0a0d..52d96151cb1b0d 100644 --- a/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx +++ b/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx @@ -24,6 +24,8 @@ export const SidebarContent = NavContentBlueprint.make({ const nav = navItems.withComponent(item => ( item.icon} to={item.href} text={item.title} /> )); + // Skipped items + nav.take('page:search'); return compatWrapper( From da8e6603a4319bb5229421dacbb10e2bae55ba4c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 16:26:49 +0100 Subject: [PATCH 49/55] Clean up unreleased API surface Since cli-module-auth and cli-module-actions are not yet released, remove deprecated exports instead of keeping them. Also make httpJson and getSecretStore internal to cli-node, duplicating the small httpJson wrapper locally in each consuming package. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/cli-module-actions-use-cli-auth.md | 5 --- .../cli-module-auth-deprecate-exports.md | 5 --- .changeset/cli-node-auth-api.md | 2 +- packages/cli-module-actions/package.json | 1 + .../src/lib/ActionsClient.test.ts | 14 +++---- .../src/lib/ActionsClient.ts | 2 +- .../cli-module-actions/src/lib/httpJson.ts | 39 +++++++++++++++++++ packages/cli-module-auth/report.api.md | 34 ---------------- packages/cli-module-auth/src/commands/show.ts | 3 +- packages/cli-module-auth/src/index.ts | 14 +------ packages/cli-module-auth/src/lib/auth.ts | 2 - packages/cli-module-auth/src/lib/http.ts | 24 +++++++++++- .../cli-module-auth/src/lib/secretStore.ts | 8 ++-- packages/cli-module-auth/src/lib/storage.ts | 2 - packages/cli-node/report.api.md | 21 ---------- packages/cli-node/src/auth/index.ts | 2 - plugins/app-react/report.api.md | 4 +- plugins/app/report.api.md | 2 +- yarn.lock | 1 + 19 files changed, 82 insertions(+), 103 deletions(-) delete mode 100644 .changeset/cli-module-actions-use-cli-auth.md delete mode 100644 .changeset/cli-module-auth-deprecate-exports.md create mode 100644 packages/cli-module-actions/src/lib/httpJson.ts diff --git a/.changeset/cli-module-actions-use-cli-auth.md b/.changeset/cli-module-actions-use-cli-auth.md deleted file mode 100644 index 705ebac22056ee..00000000000000 --- a/.changeset/cli-module-actions-use-cli-auth.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli-module-actions': patch ---- - -Migrated to use `CliAuth` from `@backstage/cli-node` for authentication instead of importing individual functions from `@backstage/cli-module-auth`. diff --git a/.changeset/cli-module-auth-deprecate-exports.md b/.changeset/cli-module-auth-deprecate-exports.md deleted file mode 100644 index 5a8c79feef2e57..00000000000000 --- a/.changeset/cli-module-auth-deprecate-exports.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli-module-auth': patch ---- - -Deprecated `getSelectedInstance`, `getInstanceConfig`, `accessTokenNeedsRefresh`, `refreshAccessToken`, `getSecretStore`, and `httpJson` exports in favor of the new `CliAuth` class and shared utilities from `@backstage/cli-node`. diff --git a/.changeset/cli-node-auth-api.md b/.changeset/cli-node-auth-api.md index bed96811789666..c28a2e29c88bb0 100644 --- a/.changeset/cli-node-auth-api.md +++ b/.changeset/cli-node-auth-api.md @@ -2,4 +2,4 @@ '@backstage/cli-node': minor --- -Added `CliAuth` class for managing CLI authentication state. This provides a class-based API with a static `create` method that resolves the currently selected (or explicitly named) auth instance, transparently refreshes expired access tokens, and exposes helpers for other CLI modules to authenticate with a Backstage backend. Also added `httpJson`, `getSecretStore`, `SecretStore`, `StoredInstance`, and `HttpInit` exports. +Added `CliAuth` class for managing CLI authentication state. This provides a class-based API with a static `create` method that resolves the currently selected (or explicitly named) auth instance, transparently refreshes expired access tokens, and exposes helpers for other CLI modules to authenticate with a Backstage backend. diff --git a/packages/cli-module-actions/package.json b/packages/cli-module-actions/package.json index fe19a8a90da972..c177e91fc4c2f2 100644 --- a/packages/cli-module-actions/package.json +++ b/packages/cli-module-actions/package.json @@ -35,6 +35,7 @@ "dependencies": { "@backstage/cli-module-auth": "workspace:^", "@backstage/cli-node": "workspace:^", + "@backstage/errors": "workspace:^", "cleye": "^2.3.0" }, "devDependencies": { diff --git a/packages/cli-module-actions/src/lib/ActionsClient.test.ts b/packages/cli-module-actions/src/lib/ActionsClient.test.ts index ecf7861f26cd6f..3aa2e06dc77653 100644 --- a/packages/cli-module-actions/src/lib/ActionsClient.test.ts +++ b/packages/cli-module-actions/src/lib/ActionsClient.test.ts @@ -15,15 +15,11 @@ */ import { ActionsClient } from './ActionsClient'; -import { httpJson } from '@backstage/cli-node'; - -jest.mock('@backstage/cli-node', () => { - const actual = jest.requireActual('@backstage/cli-node'); - return { - ...actual, - httpJson: jest.fn(), - }; -}); +import { httpJson } from './httpJson'; + +jest.mock('./httpJson', () => ({ + httpJson: jest.fn(), +})); const mockHttpJson = httpJson as jest.MockedFunction; diff --git a/packages/cli-module-actions/src/lib/ActionsClient.ts b/packages/cli-module-actions/src/lib/ActionsClient.ts index f7d284ad1a0228..736a3b7f3754d1 100644 --- a/packages/cli-module-actions/src/lib/ActionsClient.ts +++ b/packages/cli-module-actions/src/lib/ActionsClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { httpJson } from '@backstage/cli-node'; +import { httpJson } from './httpJson'; export type ActionDef = { id: string; diff --git a/packages/cli-module-actions/src/lib/httpJson.ts b/packages/cli-module-actions/src/lib/httpJson.ts new file mode 100644 index 00000000000000..0bea9dab94c786 --- /dev/null +++ b/packages/cli-module-actions/src/lib/httpJson.ts @@ -0,0 +1,39 @@ +/* + * 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 { ResponseError } from '@backstage/errors'; + +type HttpInit = { + headers?: Record; + method?: string; + body?: any; + signal?: AbortSignal; +}; + +export async function httpJson(url: string, init?: HttpInit): Promise { + const res = await fetch(url, { + ...init, + body: init?.body ? JSON.stringify(init.body) : undefined, + headers: { + ...(init?.body ? { 'Content-Type': 'application/json' } : {}), + ...init?.headers, + }, + }); + if (!res.ok) { + throw await ResponseError.fromResponse(res); + } + return (await res.json()) as T; +} diff --git a/packages/cli-module-auth/report.api.md b/packages/cli-module-auth/report.api.md index 91191153c807f8..67fb636baad9c8 100644 --- a/packages/cli-module-auth/report.api.md +++ b/packages/cli-module-auth/report.api.md @@ -4,45 +4,11 @@ ```ts import { CliModule } from '@backstage/cli-node'; -import { HttpInit } from '@backstage/cli-node'; -import { httpJson } from '@backstage/cli-node'; -import { SecretStore } from '@backstage/cli-node'; -import { StoredInstance } from '@backstage/cli-node'; - -// @public @deprecated (undocumented) -export function accessTokenNeedsRefresh(instance: StoredInstance): boolean; // @public (undocumented) const _default: CliModule; export default _default; -// @public @deprecated (undocumented) -export function getInstanceConfig( - instanceName: string, - key: string, -): Promise; - -// @public (undocumented) -export function getSecretStore(): Promise; - -// @public @deprecated (undocumented) -export function getSelectedInstance( - instanceName?: string, -): Promise; - -export { HttpInit }; - -export { httpJson }; - -// @public @deprecated (undocumented) -export function refreshAccessToken( - instanceName: string, -): Promise; - -export { SecretStore }; - -export { StoredInstance }; - // @public (undocumented) export function updateInstanceConfig( instanceName: string, diff --git a/packages/cli-module-auth/src/commands/show.ts b/packages/cli-module-auth/src/commands/show.ts index f9a3f4eb90cbf3..6a5db7f299b3a2 100644 --- a/packages/cli-module-auth/src/commands/show.ts +++ b/packages/cli-module-auth/src/commands/show.ts @@ -15,7 +15,8 @@ */ import { cli } from 'cleye'; -import { CliAuth, httpJson, type CliCommandContext } from '@backstage/cli-node'; +import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; +import { httpJson } from '../lib/http'; export default async ({ args, info }: CliCommandContext) => { const { diff --git a/packages/cli-module-auth/src/index.ts b/packages/cli-module-auth/src/index.ts index a8079932d4233e..6fef1aea2bbcea 100644 --- a/packages/cli-module-auth/src/index.ts +++ b/packages/cli-module-auth/src/index.ts @@ -53,16 +53,4 @@ export default createCliModule({ }, }); -/** @public @deprecated Use {@link @backstage/cli-node#CliAuth} instead. */ -export { - getSelectedInstance, - getInstanceConfig, - updateInstanceConfig, - type StoredInstance, -} from './lib/storage'; -/** @public @deprecated Use {@link @backstage/cli-node#CliAuth} instead. */ -export { accessTokenNeedsRefresh, refreshAccessToken } from './lib/auth'; -/** @public @deprecated Import from {@link @backstage/cli-node} instead. */ -export { getSecretStore, type SecretStore } from './lib/secretStore'; -/** @public @deprecated Import from {@link @backstage/cli-node} instead. */ -export { httpJson, type HttpInit } from './lib/http'; +export { updateInstanceConfig } from './lib/storage'; diff --git a/packages/cli-module-auth/src/lib/auth.ts b/packages/cli-module-auth/src/lib/auth.ts index d53aad6cb2bf35..4b330cf982019f 100644 --- a/packages/cli-module-auth/src/lib/auth.ts +++ b/packages/cli-module-auth/src/lib/auth.ts @@ -27,13 +27,11 @@ const TokenResponseSchema = z.object({ refresh_token: z.string().min(1).optional(), }); -/** @public @deprecated Use {@link @backstage/cli-node#CliAuth} instead. */ export function accessTokenNeedsRefresh(instance: StoredInstance): boolean { // 2 minutes before expiration return instance.accessTokenExpiresAt <= Date.now() + 2 * 60_000; } -/** @public @deprecated Use {@link @backstage/cli-node#CliAuth} instead. */ export async function refreshAccessToken( instanceName: string, ): Promise { diff --git a/packages/cli-module-auth/src/lib/http.ts b/packages/cli-module-auth/src/lib/http.ts index df0704fcf4feca..4daaf823fbbb57 100644 --- a/packages/cli-module-auth/src/lib/http.ts +++ b/packages/cli-module-auth/src/lib/http.ts @@ -14,4 +14,26 @@ * limitations under the License. */ -export { httpJson, type HttpInit } from '@backstage/cli-node'; +import { ResponseError } from '@backstage/errors'; + +export type HttpInit = { + headers?: Record; + method?: string; + body?: any; + signal?: AbortSignal; +}; + +export async function httpJson(url: string, init?: HttpInit): Promise { + const res = await fetch(url, { + ...init, + body: init?.body ? JSON.stringify(init.body) : undefined, + headers: { + ...(init?.body ? { 'Content-Type': 'application/json' } : {}), + ...init?.headers, + }, + }); + if (!res.ok) { + throw await ResponseError.fromResponse(res); + } + return (await res.json()) as T; +} diff --git a/packages/cli-module-auth/src/lib/secretStore.ts b/packages/cli-module-auth/src/lib/secretStore.ts index f7aa5256ae3af8..55fac878b7b99d 100644 --- a/packages/cli-module-auth/src/lib/secretStore.ts +++ b/packages/cli-module-auth/src/lib/secretStore.ts @@ -18,8 +18,11 @@ import fs from 'fs-extra'; import os from 'node:os'; import path from 'node:path'; -export type { SecretStore } from '@backstage/cli-node'; -import type { SecretStore } from '@backstage/cli-node'; +type SecretStore = { + get(service: string, account: string): Promise; + set(service: string, account: string, secret: string): Promise; + delete(service: string, account: string): Promise; +}; async function loadKeytar(): Promise { try { @@ -86,7 +89,6 @@ class FileSecretStore implements SecretStore { let singleton: SecretStore | undefined; -/** @public */ export async function getSecretStore(): Promise { if (!singleton) { const keytar = await loadKeytar(); diff --git a/packages/cli-module-auth/src/lib/storage.ts b/packages/cli-module-auth/src/lib/storage.ts index b50b4721574241..d66d4fb04afc4b 100644 --- a/packages/cli-module-auth/src/lib/storage.ts +++ b/packages/cli-module-auth/src/lib/storage.ts @@ -99,7 +99,6 @@ export async function getAllInstances(): Promise<{ }; } -/** @public @deprecated Use {@link @backstage/cli-node#CliAuth} instead. */ export async function getSelectedInstance( instanceName?: string, ): Promise { @@ -162,7 +161,6 @@ export async function setSelectedInstance(name: string): Promise { }); } -/** @public @deprecated Use {@link @backstage/cli-node#CliAuth.getConfig} instead. */ export async function getInstanceConfig( instanceName: string, key: string, diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index 5af6665789539f..eeeb92f5b3e7f9 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -148,9 +148,6 @@ export function createCliModule(options: { }) => Promise; }): CliModule; -// @public (undocumented) -export function getSecretStore(): Promise; - // @public export class GitUtils { static listChangedFiles(ref: string): Promise; @@ -160,17 +157,6 @@ export class GitUtils { // @public export function hasBackstageYarnPlugin(workspaceDir?: string): Promise; -// @public (undocumented) -export type HttpInit = { - headers?: Record; - method?: string; - body?: any; - signal?: AbortSignal; -}; - -// @public (undocumented) -export function httpJson(url: string, init?: HttpInit): Promise; - // @public export function isMonoRepo(): Promise; @@ -301,13 +287,6 @@ export function runWorkerQueueThreads( results: TResult[]; }>; -// @public (undocumented) -export type SecretStore = { - get(service: string, account: string): Promise; - set(service: string, account: string, secret: string): Promise; - delete(service: string, account: string): Promise; -}; - // @public (undocumented) export type StoredInstance = { name: string; diff --git a/packages/cli-node/src/auth/index.ts b/packages/cli-node/src/auth/index.ts index 8037f25714f6e1..c84c1742d7e3e5 100644 --- a/packages/cli-node/src/auth/index.ts +++ b/packages/cli-node/src/auth/index.ts @@ -16,5 +16,3 @@ export { CliAuth, type CliAuthCreateOptions } from './CliAuth'; export { type StoredInstance } from './storage'; -export { httpJson, type HttpInit } from './httpJson'; -export { getSecretStore, type SecretStore } from './secretStore'; diff --git a/plugins/app-react/report.api.md b/plugins/app-react/report.api.md index a4e63e254b888d..4e3fff6e42626c 100644 --- a/plugins/app-react/report.api.md +++ b/plugins/app-react/report.api.md @@ -86,7 +86,7 @@ export const IconBundleBlueprint: ExtensionBlueprint<{ }; output: ExtensionDataRef< { - [x: string]: IconComponent | IconElement; + [x: string]: IconElement | IconComponent; }, 'core.icons', {} @@ -97,7 +97,7 @@ export const IconBundleBlueprint: ExtensionBlueprint<{ dataRefs: { icons: ConfigurableExtensionDataRef< { - [x: string]: IconComponent | IconElement; + [x: string]: IconElement | IconComponent; }, 'core.icons', {} diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index 4c9b2a8ce4e400..baa3b714b05940 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -478,7 +478,7 @@ const appPlugin: OverridableFrontendPlugin< icons: ExtensionInput< ConfigurableExtensionDataRef< { - [x: string]: IconComponent | IconElement; + [x: string]: IconElement | IconComponent; }, 'core.icons', {} diff --git a/yarn.lock b/yarn.lock index 2a8c8e7f02d6f2..3996199a391c14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2830,6 +2830,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/cli-module-auth": "workspace:^" "@backstage/cli-node": "workspace:^" + "@backstage/errors": "workspace:^" cleye: "npm:^2.3.0" bin: cli-module-actions: bin/backstage-cli-module-actions From 2b9035873027c509e0b65854a3691db430ce4289 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 17:10:01 +0100 Subject: [PATCH 50/55] Address PR review feedback - Convert CliAuth getters to methods (getInstanceName, getBaseUrl) so options can be added in the future - Remove StoredInstance from cli-node public API, hiding instance details - Move secretStore to cli-internal for re-use, refactoring from fs-extra to node:fs - Add shared getAuthInstanceService helper in cli-internal for constructing secret-store service keys - Define StoredInstance locally in cli-module-auth instead of importing from cli-node - Update all consumers and tests for the new method-based API Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-internal/src/authIdentifiers.ts | 20 +++++++++++ packages/cli-internal/src/index.ts | 6 ++++ .../lib => cli-internal/src}/secretStore.ts | 27 ++++++++++++--- .../src/commands/execute.ts | 4 +-- .../cli-module-actions/src/commands/list.ts | 4 +-- .../src/commands/sourcesAdd.ts | 2 +- .../src/commands/sourcesRemove.ts | 2 +- .../src/lib/resolveAuth.test.ts | 33 +++++++------------ .../cli-module-actions/src/lib/resolveAuth.ts | 12 +++++-- packages/cli-module-auth/package.json | 4 +-- .../cli-module-auth/src/commands/login.ts | 4 +-- .../cli-module-auth/src/commands/logout.ts | 4 +-- packages/cli-module-auth/src/commands/show.ts | 2 +- packages/cli-module-auth/src/lib/auth.test.ts | 11 ++++--- packages/cli-module-auth/src/lib/auth.ts | 12 ++++--- .../src/lib/secretStore.test.ts | 2 +- packages/cli-module-auth/src/lib/storage.ts | 11 +++++-- packages/cli-node/report.api.md | 16 ++------- packages/cli-node/src/auth/CliAuth.test.ts | 5 ++- packages/cli-node/src/auth/CliAuth.ts | 18 ++++------ packages/cli-node/src/auth/authIdentifiers.ts | 20 +++++++++++ packages/cli-node/src/auth/index.ts | 1 - packages/cli-node/src/auth/secretStore.ts | 29 ++++++++++++---- 23 files changed, 160 insertions(+), 89 deletions(-) create mode 100644 packages/cli-internal/src/authIdentifiers.ts rename packages/{cli-module-auth/src/lib => cli-internal/src}/secretStore.ts (87%) create mode 100644 packages/cli-node/src/auth/authIdentifiers.ts diff --git a/packages/cli-internal/src/authIdentifiers.ts b/packages/cli-internal/src/authIdentifiers.ts new file mode 100644 index 00000000000000..6b72554a4fdaaa --- /dev/null +++ b/packages/cli-internal/src/authIdentifiers.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** Returns the secret-store service key for a given auth instance. */ +export function getAuthInstanceService(instanceName: string): string { + return `backstage-cli:auth-instance:${instanceName}`; +} diff --git a/packages/cli-internal/src/index.ts b/packages/cli-internal/src/index.ts index 0e69d450327c6f..281b998d92ddb7 100644 --- a/packages/cli-internal/src/index.ts +++ b/packages/cli-internal/src/index.ts @@ -25,3 +25,9 @@ export { OpaqueCommandLeafNode, isCommandNodeHidden, } from './InternalCommandNode'; +export { getAuthInstanceService } from './authIdentifiers'; +export { + getSecretStore, + resetSecretStore, + type SecretStore, +} from './secretStore'; diff --git a/packages/cli-module-auth/src/lib/secretStore.ts b/packages/cli-internal/src/secretStore.ts similarity index 87% rename from packages/cli-module-auth/src/lib/secretStore.ts rename to packages/cli-internal/src/secretStore.ts index 55fac878b7b99d..26c1e6ad2be033 100644 --- a/packages/cli-module-auth/src/lib/secretStore.ts +++ b/packages/cli-internal/src/secretStore.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import fs from 'fs-extra'; +import { promises as fs } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -type SecretStore = { +export type SecretStore = { get(service: string, account: string): Promise; set(service: string, account: string, secret: string): Promise; delete(service: string, account: string): Promise; @@ -54,6 +54,15 @@ class KeytarSecretStore implements SecretStore { } } +async function pathExists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} + class FileSecretStore implements SecretStore { private readonly baseDir: string; constructor() { @@ -73,17 +82,25 @@ class FileSecretStore implements SecretStore { } async get(service: string, account: string): Promise { const file = this.filePath(service, account); - if (!(await fs.pathExists(file))) return undefined; + if (!(await pathExists(file))) { + return undefined; + } return await fs.readFile(file, 'utf8'); } async set(service: string, account: string, secret: string): Promise { const file = this.filePath(service, account); - await fs.ensureDir(path.dirname(file)); + await fs.mkdir(path.dirname(file), { recursive: true }); await fs.writeFile(file, secret, { encoding: 'utf8', mode: 0o600 }); } async delete(service: string, account: string): Promise { const file = this.filePath(service, account); - await fs.remove(file); + try { + await fs.unlink(file); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { + throw err; + } + } } } diff --git a/packages/cli-module-actions/src/commands/execute.ts b/packages/cli-module-actions/src/commands/execute.ts index 4ff955096b1d83..f130c597ce5849 100644 --- a/packages/cli-module-actions/src/commands/execute.ts +++ b/packages/cli-module-actions/src/commands/execute.ts @@ -65,9 +65,9 @@ export default async ({ args, info }: CliCommandContext) => { process.exit(1); } - const { accessToken, instance } = await resolveAuth(instanceFlag); + const { accessToken, baseUrl } = await resolveAuth(instanceFlag); - const client = new ActionsClient(instance.baseUrl, accessToken); + const client = new ActionsClient(baseUrl, accessToken); const actions = await client.listForPlugin(actionId); const action = actions.find(a => a.id === actionId); diff --git a/packages/cli-module-actions/src/commands/list.ts b/packages/cli-module-actions/src/commands/list.ts index 601fa6d2c7e805..2049fc3e44f611 100644 --- a/packages/cli-module-actions/src/commands/list.ts +++ b/packages/cli-module-actions/src/commands/list.ts @@ -36,7 +36,7 @@ export default async ({ args, info }: CliCommandContext) => { args, ); - const { accessToken, pluginSources, instance } = await resolveAuth( + const { accessToken, pluginSources, baseUrl } = await resolveAuth( instanceFlag, ); @@ -47,7 +47,7 @@ export default async ({ args, info }: CliCommandContext) => { return; } - const client = new ActionsClient(instance.baseUrl, accessToken); + const client = new ActionsClient(baseUrl, accessToken); const actions = await client.list(pluginSources); if (!actions.length) { diff --git a/packages/cli-module-actions/src/commands/sourcesAdd.ts b/packages/cli-module-actions/src/commands/sourcesAdd.ts index 6d9ea2b524f804..0fc4addd5cb938 100644 --- a/packages/cli-module-actions/src/commands/sourcesAdd.ts +++ b/packages/cli-module-actions/src/commands/sourcesAdd.ts @@ -40,7 +40,7 @@ export default async ({ args, info }: CliCommandContext) => { return; } - await updateInstanceConfig(auth.instanceName, 'pluginSources', [ + await updateInstanceConfig(auth.getInstanceName(), 'pluginSources', [ ...existing, pluginId, ]); diff --git a/packages/cli-module-actions/src/commands/sourcesRemove.ts b/packages/cli-module-actions/src/commands/sourcesRemove.ts index 81a2d5abf300c8..b680c39684e457 100644 --- a/packages/cli-module-actions/src/commands/sourcesRemove.ts +++ b/packages/cli-module-actions/src/commands/sourcesRemove.ts @@ -39,7 +39,7 @@ export default async ({ args, info }: CliCommandContext) => { } await updateInstanceConfig( - auth.instanceName, + auth.getInstanceName(), 'pluginSources', existing.filter(s => s !== pluginId), ); diff --git a/packages/cli-module-actions/src/lib/resolveAuth.test.ts b/packages/cli-module-actions/src/lib/resolveAuth.test.ts index 21cf6ebc386298..21f494c54a9eeb 100644 --- a/packages/cli-module-actions/src/lib/resolveAuth.test.ts +++ b/packages/cli-module-actions/src/lib/resolveAuth.test.ts @@ -15,7 +15,7 @@ */ import { resolveAuth } from './resolveAuth'; -import { CliAuth, type StoredInstance } from '@backstage/cli-node'; +import { CliAuth } from '@backstage/cli-node'; jest.mock('@backstage/cli-node', () => { const actual = jest.requireActual('@backstage/cli-node'); @@ -28,23 +28,14 @@ jest.mock('@backstage/cli-node', () => { const mockCreate = CliAuth.create as jest.MockedFunction; describe('resolveAuth', () => { - const mockInstance: StoredInstance = { - name: 'production', - baseUrl: 'https://backstage.example.com', - clientId: 'my-client', - issuedAt: Date.now(), - accessTokenExpiresAt: Date.now() + 3600_000, - }; - beforeEach(() => { jest.clearAllMocks(); }); it('resolves auth with the selected instance and stored token', async () => { mockCreate.mockResolvedValue({ - instance: mockInstance, - instanceName: mockInstance.name, - baseUrl: mockInstance.baseUrl, + getInstanceName: jest.fn().mockReturnValue('production'), + getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'), getAccessToken: jest.fn().mockResolvedValue('test-access-token'), getConfig: jest.fn().mockResolvedValue(['catalog', 'scaffolder']), } as unknown as CliAuth); @@ -53,7 +44,8 @@ describe('resolveAuth', () => { expect(mockCreate).toHaveBeenCalledWith({ instanceName: undefined }); expect(result).toEqual({ - instance: mockInstance, + baseUrl: 'https://backstage.example.com', + instanceName: 'production', accessToken: 'test-access-token', pluginSources: ['catalog', 'scaffolder'], }); @@ -61,9 +53,8 @@ describe('resolveAuth', () => { it('passes instance name flag to CliAuth.create', async () => { mockCreate.mockResolvedValue({ - instance: mockInstance, - instanceName: mockInstance.name, - baseUrl: mockInstance.baseUrl, + getInstanceName: jest.fn().mockReturnValue('staging'), + getBaseUrl: jest.fn().mockReturnValue('https://staging.example.com'), getAccessToken: jest.fn().mockResolvedValue('test-access-token'), getConfig: jest.fn().mockResolvedValue([]), } as unknown as CliAuth); @@ -75,9 +66,8 @@ describe('resolveAuth', () => { it('throws when getAccessToken fails', async () => { mockCreate.mockResolvedValue({ - instance: mockInstance, - instanceName: mockInstance.name, - baseUrl: mockInstance.baseUrl, + getInstanceName: jest.fn().mockReturnValue('production'), + getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'), getAccessToken: jest .fn() .mockRejectedValue( @@ -93,9 +83,8 @@ describe('resolveAuth', () => { it('returns empty plugin sources when none are configured', async () => { mockCreate.mockResolvedValue({ - instance: mockInstance, - instanceName: mockInstance.name, - baseUrl: mockInstance.baseUrl, + getInstanceName: jest.fn().mockReturnValue('production'), + getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'), getAccessToken: jest.fn().mockResolvedValue('test-access-token'), getConfig: jest.fn().mockResolvedValue(undefined), } as unknown as CliAuth); diff --git a/packages/cli-module-actions/src/lib/resolveAuth.ts b/packages/cli-module-actions/src/lib/resolveAuth.ts index 18664b1b552667..4d2afe6695dd86 100644 --- a/packages/cli-module-actions/src/lib/resolveAuth.ts +++ b/packages/cli-module-actions/src/lib/resolveAuth.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import { CliAuth, type StoredInstance } from '@backstage/cli-node'; +import { CliAuth } from '@backstage/cli-node'; export async function resolveAuth(instanceFlag?: string): Promise<{ - instance: StoredInstance; + baseUrl: string; + instanceName: string; accessToken: string; pluginSources: string[]; }> { @@ -25,5 +26,10 @@ export async function resolveAuth(instanceFlag?: string): Promise<{ const accessToken = await auth.getAccessToken(); const pluginSources = (await auth.getConfig('pluginSources')) ?? []; - return { instance: auth.instance, accessToken, pluginSources }; + return { + baseUrl: auth.getBaseUrl(), + instanceName: auth.getInstanceName(), + accessToken, + pluginSources, + }; } diff --git a/packages/cli-module-auth/package.json b/packages/cli-module-auth/package.json index 94169e998eb81d..b0e2564661adc4 100644 --- a/packages/cli-module-auth/package.json +++ b/packages/cli-module-auth/package.json @@ -19,6 +19,7 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "bin": "bin/backstage-cli-module-auth", "files": [ "dist", "bin" @@ -50,6 +51,5 @@ }, "optionalDependencies": { "keytar": "^7.9.0" - }, - "bin": "bin/backstage-cli-module-auth" + } } diff --git a/packages/cli-module-auth/src/commands/login.ts b/packages/cli-module-auth/src/commands/login.ts index b16060f956b08c..6bb875114718f7 100644 --- a/packages/cli-module-auth/src/commands/login.ts +++ b/packages/cli-module-auth/src/commands/login.ts @@ -27,7 +27,7 @@ import { getInstanceByName, StoredInstance, } from '../lib/storage'; -import { getSecretStore } from '../lib/secretStore'; +import { getSecretStore, getAuthInstanceService } from '@internal/cli'; import crypto from 'node:crypto'; import fs from 'fs-extra'; import path from 'node:path'; @@ -321,7 +321,7 @@ async function persistInstance(options: { const { instanceName, backendBaseUrl, clientId, token } = options; const secretStore = await getSecretStore(); await withMetadataLock(async () => { - const service = `backstage-cli:auth-instance:${instanceName}`; + const service = getAuthInstanceService(instanceName); await secretStore.set(service, 'accessToken', token.access_token); if (token.refresh_token) { await secretStore.set(service, 'refreshToken', token.refresh_token); diff --git a/packages/cli-module-auth/src/commands/logout.ts b/packages/cli-module-auth/src/commands/logout.ts index f79ed2ef3559b5..a02fb3580d5686 100644 --- a/packages/cli-module-auth/src/commands/logout.ts +++ b/packages/cli-module-auth/src/commands/logout.ts @@ -16,7 +16,7 @@ import { cli } from 'cleye'; import type { CliCommandContext } from '@backstage/cli-node'; -import { getSecretStore } from '../lib/secretStore'; +import { getSecretStore, getAuthInstanceService } from '@internal/cli'; import { removeInstance, withMetadataLock, @@ -47,7 +47,7 @@ export default async ({ args, info }: CliCommandContext) => { await withMetadataLock(async () => { const instance = await getInstanceByName(instanceName); const secretStore = await getSecretStore(); - const service = `backstage-cli:auth-instance:${instanceName}`; + const service = getAuthInstanceService(instanceName); const refreshToken = (await secretStore.get(service, 'refreshToken')) ?? ''; if (refreshToken) { diff --git a/packages/cli-module-auth/src/commands/show.ts b/packages/cli-module-auth/src/commands/show.ts index 6a5db7f299b3a2..275b6ca66db884 100644 --- a/packages/cli-module-auth/src/commands/show.ts +++ b/packages/cli-module-auth/src/commands/show.ts @@ -38,7 +38,7 @@ export default async ({ args, info }: CliCommandContext) => { const auth = await CliAuth.create({ instanceName: instanceFlag }); const accessToken = await auth.getAccessToken(); - const authBase = new URL('/api/auth', auth.baseUrl) + const authBase = new URL('/api/auth', auth.getBaseUrl()) .toString() .replace(/\/$/, ''); diff --git a/packages/cli-module-auth/src/lib/auth.test.ts b/packages/cli-module-auth/src/lib/auth.test.ts index 5ba4ae0295924c..18e7658e9228e1 100644 --- a/packages/cli-module-auth/src/lib/auth.test.ts +++ b/packages/cli-module-auth/src/lib/auth.test.ts @@ -16,15 +16,15 @@ import { accessTokenNeedsRefresh, refreshAccessToken } from './auth'; import * as storage from './storage'; -import * as secretStore from './secretStore'; +import * as internalCli from '@internal/cli'; import * as http from './http'; jest.mock('./storage'); -jest.mock('./secretStore'); +jest.mock('@internal/cli'); jest.mock('./http'); const mockStorage = storage as jest.Mocked; -const mockSecretStore = secretStore as jest.Mocked; +const mockInternalCli = internalCli as jest.Mocked; const mockHttp = http as jest.Mocked; describe('auth', () => { @@ -95,7 +95,10 @@ describe('auth', () => { beforeEach(() => { jest.clearAllMocks(); - mockSecretStore.getSecretStore.mockResolvedValue(mockSecretStoreInstance); + mockInternalCli.getSecretStore.mockResolvedValue(mockSecretStoreInstance); + mockInternalCli.getAuthInstanceService.mockImplementation( + (name: string) => `backstage-cli:auth-instance:${name}`, + ); }); it('should successfully refresh access token', async () => { diff --git a/packages/cli-module-auth/src/lib/auth.ts b/packages/cli-module-auth/src/lib/auth.ts index 4b330cf982019f..1ad108cbf1a8db 100644 --- a/packages/cli-module-auth/src/lib/auth.ts +++ b/packages/cli-module-auth/src/lib/auth.ts @@ -15,9 +15,13 @@ */ import { z } from 'zod/v3'; -import type { StoredInstance } from '@backstage/cli-node'; -import { upsertInstance, withMetadataLock, getInstanceByName } from './storage'; -import { getSecretStore } from './secretStore'; +import { + type StoredInstance, + upsertInstance, + withMetadataLock, + getInstanceByName, +} from './storage'; +import { getSecretStore, getAuthInstanceService } from '@internal/cli'; import { httpJson } from './http'; const TokenResponseSchema = z.object({ @@ -40,7 +44,7 @@ export async function refreshAccessToken( return withMetadataLock(async () => { const instance = await getInstanceByName(instanceName); - const service = `backstage-cli:auth-instance:${instanceName}`; + const service = getAuthInstanceService(instanceName); const refreshToken = (await secretStore.get(service, 'refreshToken')) ?? ''; if (!refreshToken) { throw new Error( diff --git a/packages/cli-module-auth/src/lib/secretStore.test.ts b/packages/cli-module-auth/src/lib/secretStore.test.ts index 6729f7c6a5ac95..f27d364c7c6edb 100644 --- a/packages/cli-module-auth/src/lib/secretStore.test.ts +++ b/packages/cli-module-auth/src/lib/secretStore.test.ts @@ -21,7 +21,7 @@ jest.mock('keytar', () => { import fs from 'fs-extra'; import path from 'node:path'; import { createMockDirectory } from '@backstage/backend-test-utils'; -import { getSecretStore, resetSecretStore } from './secretStore'; +import { getSecretStore, resetSecretStore } from '@internal/cli'; const mockDir = createMockDirectory(); diff --git a/packages/cli-module-auth/src/lib/storage.ts b/packages/cli-module-auth/src/lib/storage.ts index d66d4fb04afc4b..32c61786962076 100644 --- a/packages/cli-module-auth/src/lib/storage.ts +++ b/packages/cli-module-auth/src/lib/storage.ts @@ -22,8 +22,15 @@ import lockfile from 'proper-lockfile'; import YAML from 'yaml'; import { z } from 'zod/v3'; -export type { StoredInstance } from '@backstage/cli-node'; -import type { StoredInstance } from '@backstage/cli-node'; +export type StoredInstance = { + name: string; + baseUrl: string; + clientId: string; + issuedAt: number; + accessTokenExpiresAt: number; + selected?: boolean; + config?: Record; +}; const METADATA_FILE = 'auth-instances.yaml'; diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index eeeb92f5b3e7f9..ca10eafff2b856 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -88,12 +88,11 @@ export interface BackstagePackageJson { // @public export class CliAuth { - get baseUrl(): string; static create(options?: CliAuthCreateOptions): Promise; getAccessToken(): Promise; + getBaseUrl(): string; getConfig(key: string): Promise; - get instance(): StoredInstance; - get instanceName(): string; + getInstanceName(): string; } // @public @@ -287,17 +286,6 @@ export function runWorkerQueueThreads( results: TResult[]; }>; -// @public (undocumented) -export type StoredInstance = { - name: string; - baseUrl: string; - clientId: string; - issuedAt: number; - accessTokenExpiresAt: number; - selected?: boolean; - config?: Record; -}; - // @public export class SuccessCache { // (undocumented) diff --git a/packages/cli-node/src/auth/CliAuth.test.ts b/packages/cli-node/src/auth/CliAuth.test.ts index 16a9b10ddeea1f..3d932e5e34b134 100644 --- a/packages/cli-node/src/auth/CliAuth.test.ts +++ b/packages/cli-node/src/auth/CliAuth.test.ts @@ -58,9 +58,8 @@ describe('CliAuth', () => { const auth = await CliAuth.create(); expect(mockStorage.getSelectedInstance).toHaveBeenCalledWith(undefined); - expect(auth.instance).toEqual(mockInstance); - expect(auth.instanceName).toBe('production'); - expect(auth.baseUrl).toBe('https://backstage.example.com'); + expect(auth.getInstanceName()).toBe('production'); + expect(auth.getBaseUrl()).toBe('https://backstage.example.com'); }); it('resolves a named instance when specified', async () => { diff --git a/packages/cli-node/src/auth/CliAuth.ts b/packages/cli-node/src/auth/CliAuth.ts index 8934e9bd7f35af..d7eaad6d402ecd 100644 --- a/packages/cli-node/src/auth/CliAuth.ts +++ b/packages/cli-node/src/auth/CliAuth.ts @@ -21,6 +21,7 @@ import { accessTokenNeedsRefresh, } from './storage'; import { getSecretStore, type SecretStore } from './secretStore'; +import { getAuthInstanceService } from './authIdentifiers'; import { httpJson } from './httpJson'; import { z } from 'zod'; @@ -73,18 +74,13 @@ export class CliAuth { this.#secretStore = secretStore; } - /** The resolved instance metadata. */ - get instance(): StoredInstance { - return this.#instance; - } - - /** Shorthand for `instance.name`. */ - get instanceName(): string { + /** Returns the name of the resolved auth instance. */ + getInstanceName(): string { return this.#instance.name; } - /** Shorthand for `instance.baseUrl`. */ - get baseUrl(): string { + /** Returns the base URL of the resolved auth instance. */ + getBaseUrl(): string { return this.#instance.baseUrl; } @@ -97,7 +93,7 @@ export class CliAuth { await this.#refreshAccessToken(); } - const service = `backstage-cli:auth-instance:${this.#instance.name}`; + const service = getAuthInstanceService(this.#instance.name); const token = await this.#secretStore.get(service, 'accessToken'); if (!token) { throw new Error( @@ -116,7 +112,7 @@ export class CliAuth { } async #refreshAccessToken(): Promise { - const service = `backstage-cli:auth-instance:${this.#instance.name}`; + const service = getAuthInstanceService(this.#instance.name); const refreshToken = (await this.#secretStore.get(service, 'refreshToken')) ?? ''; if (!refreshToken) { diff --git a/packages/cli-node/src/auth/authIdentifiers.ts b/packages/cli-node/src/auth/authIdentifiers.ts new file mode 100644 index 00000000000000..16ddb5b437fab4 --- /dev/null +++ b/packages/cli-node/src/auth/authIdentifiers.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** @internal */ +export function getAuthInstanceService(instanceName: string): string { + return `backstage-cli:auth-instance:${instanceName}`; +} diff --git a/packages/cli-node/src/auth/index.ts b/packages/cli-node/src/auth/index.ts index c84c1742d7e3e5..d6a1b08d657baa 100644 --- a/packages/cli-node/src/auth/index.ts +++ b/packages/cli-node/src/auth/index.ts @@ -15,4 +15,3 @@ */ export { CliAuth, type CliAuthCreateOptions } from './CliAuth'; -export { type StoredInstance } from './storage'; diff --git a/packages/cli-node/src/auth/secretStore.ts b/packages/cli-node/src/auth/secretStore.ts index a0f3c81d51484c..70ef5f2f725f47 100644 --- a/packages/cli-node/src/auth/secretStore.ts +++ b/packages/cli-node/src/auth/secretStore.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import fs from 'fs-extra'; +import { promises as fs } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -/** @public */ +/** @internal */ export type SecretStore = { get(service: string, account: string): Promise; set(service: string, account: string, secret: string): Promise; @@ -55,6 +55,15 @@ class KeytarSecretStore implements SecretStore { } } +async function pathExists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} + class FileSecretStore implements SecretStore { private readonly baseDir: string; constructor() { @@ -74,23 +83,31 @@ class FileSecretStore implements SecretStore { } async get(service: string, account: string): Promise { const file = this.filePath(service, account); - if (!(await fs.pathExists(file))) return undefined; + if (!(await pathExists(file))) { + return undefined; + } return await fs.readFile(file, 'utf8'); } async set(service: string, account: string, secret: string): Promise { const file = this.filePath(service, account); - await fs.ensureDir(path.dirname(file)); + await fs.mkdir(path.dirname(file), { recursive: true }); await fs.writeFile(file, secret, { encoding: 'utf8', mode: 0o600 }); } async delete(service: string, account: string): Promise { const file = this.filePath(service, account); - await fs.remove(file); + try { + await fs.unlink(file); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { + throw err; + } + } } } let singleton: SecretStore | undefined; -/** @public */ +/** @internal */ export async function getSecretStore(): Promise { if (!singleton) { const keytar = await loadKeytar(); From 4f6e7de1335cb53df6dc18441e3d02cf4e3b27ae Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 19:24:26 +0100 Subject: [PATCH 51/55] Address second round of PR review feedback - Replace getConfig with getMetadata/setMetadata on CliAuth, removing the unsafe type parameter in favor of returning unknown - Move updateInstanceConfig from cli-module-auth public API to CliAuth.setMetadata, removing the cross-package dependency - Rename 'config' to 'metadata' in StoredInstance and storage schemas - Add zod validation at consumer sites (cli-module-actions) for type-safe metadata access - Fix zod imports to use zod/v3 for compatibility with zod v4 - Add proper-lockfile to cli-node for metadata write locking - Refactor cli-node storage from fs-extra to node:fs - Remove @backstage/cli-module-auth dependency from cli-module-actions Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/auth-module-exports.md | 2 +- .changeset/cli-node-auth-api.md | 2 +- packages/cli-module-actions/package.json | 4 +- .../src/commands/sourcesAdd.ts | 13 ++-- .../src/commands/sourcesList.ts | 7 +- .../src/commands/sourcesRemove.ts | 11 ++- .../src/lib/resolveAuth.test.ts | 8 +- .../cli-module-actions/src/lib/resolveAuth.ts | 7 +- packages/cli-module-auth/report.api.md | 7 -- packages/cli-module-auth/src/index.ts | 2 - .../cli-module-auth/src/lib/storage.test.ts | 44 +++++------ packages/cli-module-auth/src/lib/storage.ts | 15 ++-- packages/cli-node/package.json | 2 + packages/cli-node/report.api.md | 3 +- packages/cli-node/src/auth/CliAuth.test.ts | 29 +++++-- packages/cli-node/src/auth/CliAuth.ts | 18 +++-- packages/cli-node/src/auth/storage.ts | 76 ++++++++++++++++--- yarn.lock | 4 +- 18 files changed, 168 insertions(+), 86 deletions(-) diff --git a/.changeset/auth-module-exports.md b/.changeset/auth-module-exports.md index 0b6dc46ac37a14..358f12cf61c40c 100644 --- a/.changeset/auth-module-exports.md +++ b/.changeset/auth-module-exports.md @@ -2,4 +2,4 @@ '@backstage/cli-module-auth': patch --- -Export auth helper utilities for use by other CLI modules. Added per-instance config storage with `getInstanceConfig` and `updateInstanceConfig`. +Export auth helper utilities for use by other CLI modules. diff --git a/.changeset/cli-node-auth-api.md b/.changeset/cli-node-auth-api.md index c28a2e29c88bb0..b4b943af5ae2de 100644 --- a/.changeset/cli-node-auth-api.md +++ b/.changeset/cli-node-auth-api.md @@ -1,5 +1,5 @@ --- -'@backstage/cli-node': minor +'@backstage/cli-node': patch --- Added `CliAuth` class for managing CLI authentication state. This provides a class-based API with a static `create` method that resolves the currently selected (or explicitly named) auth instance, transparently refreshes expired access tokens, and exposes helpers for other CLI modules to authenticate with a Backstage backend. diff --git a/packages/cli-module-actions/package.json b/packages/cli-module-actions/package.json index c177e91fc4c2f2..86c99973222a97 100644 --- a/packages/cli-module-actions/package.json +++ b/packages/cli-module-actions/package.json @@ -33,10 +33,10 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/cli-module-auth": "workspace:^", "@backstage/cli-node": "workspace:^", "@backstage/errors": "workspace:^", - "cleye": "^2.3.0" + "cleye": "^2.3.0", + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/packages/cli-module-actions/src/commands/sourcesAdd.ts b/packages/cli-module-actions/src/commands/sourcesAdd.ts index 0fc4addd5cb938..fee30076375408 100644 --- a/packages/cli-module-actions/src/commands/sourcesAdd.ts +++ b/packages/cli-module-actions/src/commands/sourcesAdd.ts @@ -16,7 +16,9 @@ import { cli } from 'cleye'; import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; -import { updateInstanceConfig } from '@backstage/cli-module-auth'; +import { z } from 'zod/v3'; + +const pluginSourcesSchema = z.array(z.string()).default([]); export default async ({ args, info }: CliCommandContext) => { const parsed = cli( @@ -31,7 +33,9 @@ export default async ({ args, info }: CliCommandContext) => { const pluginId = parsed._[0]; const auth = await CliAuth.create(); - const existing = (await auth.getConfig('pluginSources')) ?? []; + const existing = pluginSourcesSchema.parse( + await auth.getMetadata('pluginSources'), + ); if (existing.includes(pluginId)) { process.stderr.write( @@ -40,10 +44,7 @@ export default async ({ args, info }: CliCommandContext) => { return; } - await updateInstanceConfig(auth.getInstanceName(), 'pluginSources', [ - ...existing, - pluginId, - ]); + await auth.setMetadata('pluginSources', [...existing, pluginId]); process.stdout.write(`Added plugin source "${pluginId}".\n`); }; diff --git a/packages/cli-module-actions/src/commands/sourcesList.ts b/packages/cli-module-actions/src/commands/sourcesList.ts index 23a6a6449ff8b1..7557922c069723 100644 --- a/packages/cli-module-actions/src/commands/sourcesList.ts +++ b/packages/cli-module-actions/src/commands/sourcesList.ts @@ -16,12 +16,17 @@ import { cli } from 'cleye'; import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; +import { z } from 'zod/v3'; + +const pluginSourcesSchema = z.array(z.string()).default([]); export default async ({ args, info }: CliCommandContext) => { cli({ help: info }, undefined, args); const auth = await CliAuth.create(); - const sources = (await auth.getConfig('pluginSources')) ?? []; + const sources = pluginSourcesSchema.parse( + await auth.getMetadata('pluginSources'), + ); if (!sources.length) { process.stderr.write('No plugin sources configured.\n'); diff --git a/packages/cli-module-actions/src/commands/sourcesRemove.ts b/packages/cli-module-actions/src/commands/sourcesRemove.ts index b680c39684e457..922522850315b3 100644 --- a/packages/cli-module-actions/src/commands/sourcesRemove.ts +++ b/packages/cli-module-actions/src/commands/sourcesRemove.ts @@ -16,7 +16,9 @@ import { cli } from 'cleye'; import { CliAuth, type CliCommandContext } from '@backstage/cli-node'; -import { updateInstanceConfig } from '@backstage/cli-module-auth'; +import { z } from 'zod/v3'; + +const pluginSourcesSchema = z.array(z.string()).default([]); export default async ({ args, info }: CliCommandContext) => { const parsed = cli( @@ -31,15 +33,16 @@ export default async ({ args, info }: CliCommandContext) => { const pluginId = parsed._[0]; const auth = await CliAuth.create(); - const existing = (await auth.getConfig('pluginSources')) ?? []; + const existing = pluginSourcesSchema.parse( + await auth.getMetadata('pluginSources'), + ); if (!existing.includes(pluginId)) { process.stderr.write(`Plugin source "${pluginId}" is not configured.\n`); return; } - await updateInstanceConfig( - auth.getInstanceName(), + await auth.setMetadata( 'pluginSources', existing.filter(s => s !== pluginId), ); diff --git a/packages/cli-module-actions/src/lib/resolveAuth.test.ts b/packages/cli-module-actions/src/lib/resolveAuth.test.ts index 21f494c54a9eeb..2aabefe9eaf8a7 100644 --- a/packages/cli-module-actions/src/lib/resolveAuth.test.ts +++ b/packages/cli-module-actions/src/lib/resolveAuth.test.ts @@ -37,7 +37,7 @@ describe('resolveAuth', () => { getInstanceName: jest.fn().mockReturnValue('production'), getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'), getAccessToken: jest.fn().mockResolvedValue('test-access-token'), - getConfig: jest.fn().mockResolvedValue(['catalog', 'scaffolder']), + getMetadata: jest.fn().mockResolvedValue(['catalog', 'scaffolder']), } as unknown as CliAuth); const result = await resolveAuth(); @@ -56,7 +56,7 @@ describe('resolveAuth', () => { getInstanceName: jest.fn().mockReturnValue('staging'), getBaseUrl: jest.fn().mockReturnValue('https://staging.example.com'), getAccessToken: jest.fn().mockResolvedValue('test-access-token'), - getConfig: jest.fn().mockResolvedValue([]), + getMetadata: jest.fn().mockResolvedValue([]), } as unknown as CliAuth); await resolveAuth('staging'); @@ -73,7 +73,7 @@ describe('resolveAuth', () => { .mockRejectedValue( new Error('No access token found. Run "auth login" to authenticate.'), ), - getConfig: jest.fn().mockResolvedValue([]), + getMetadata: jest.fn().mockResolvedValue([]), } as unknown as CliAuth); await expect(resolveAuth()).rejects.toThrow( @@ -86,7 +86,7 @@ describe('resolveAuth', () => { getInstanceName: jest.fn().mockReturnValue('production'), getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'), getAccessToken: jest.fn().mockResolvedValue('test-access-token'), - getConfig: jest.fn().mockResolvedValue(undefined), + getMetadata: jest.fn().mockResolvedValue(undefined), } as unknown as CliAuth); const result = await resolveAuth(); diff --git a/packages/cli-module-actions/src/lib/resolveAuth.ts b/packages/cli-module-actions/src/lib/resolveAuth.ts index 4d2afe6695dd86..568bd4cdcdfd9e 100644 --- a/packages/cli-module-actions/src/lib/resolveAuth.ts +++ b/packages/cli-module-actions/src/lib/resolveAuth.ts @@ -15,6 +15,9 @@ */ import { CliAuth } from '@backstage/cli-node'; +import { z } from 'zod/v3'; + +const pluginSourcesSchema = z.array(z.string()).default([]); export async function resolveAuth(instanceFlag?: string): Promise<{ baseUrl: string; @@ -24,7 +27,9 @@ export async function resolveAuth(instanceFlag?: string): Promise<{ }> { const auth = await CliAuth.create({ instanceName: instanceFlag }); const accessToken = await auth.getAccessToken(); - const pluginSources = (await auth.getConfig('pluginSources')) ?? []; + const pluginSources = pluginSourcesSchema.parse( + await auth.getMetadata('pluginSources'), + ); return { baseUrl: auth.getBaseUrl(), diff --git a/packages/cli-module-auth/report.api.md b/packages/cli-module-auth/report.api.md index 67fb636baad9c8..510bcaabe72f3a 100644 --- a/packages/cli-module-auth/report.api.md +++ b/packages/cli-module-auth/report.api.md @@ -9,12 +9,5 @@ import { CliModule } from '@backstage/cli-node'; const _default: CliModule; export default _default; -// @public (undocumented) -export function updateInstanceConfig( - instanceName: string, - key: string, - value: unknown, -): Promise; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/cli-module-auth/src/index.ts b/packages/cli-module-auth/src/index.ts index 6fef1aea2bbcea..fef74ec8a59313 100644 --- a/packages/cli-module-auth/src/index.ts +++ b/packages/cli-module-auth/src/index.ts @@ -52,5 +52,3 @@ export default createCliModule({ }); }, }); - -export { updateInstanceConfig } from './lib/storage'; diff --git a/packages/cli-module-auth/src/lib/storage.test.ts b/packages/cli-module-auth/src/lib/storage.test.ts index 9264c6bb89a552..042b0eb72a22c0 100644 --- a/packages/cli-module-auth/src/lib/storage.test.ts +++ b/packages/cli-module-auth/src/lib/storage.test.ts @@ -22,8 +22,8 @@ import { getAllInstances, getSelectedInstance, getInstanceByName, - getInstanceConfig, - updateInstanceConfig, + getInstanceMetadata, + updateInstanceMetadata, upsertInstance, removeInstance, setSelectedInstance, @@ -359,65 +359,65 @@ describe('storage', () => { }); }); - describe('getInstanceConfig', () => { - it('should return undefined when no config set', async () => { + describe('getInstanceMetadata', () => { + it('should return undefined when no metadata set', async () => { await upsertInstance(mockInstance1); - const result = await getInstanceConfig('production', 'someKey'); + const result = await getInstanceMetadata('production', 'someKey'); expect(result).toBeUndefined(); }); - it('should return config value for a key', async () => { + it('should return metadata value for a key', async () => { await upsertInstance(mockInstance1); - await updateInstanceConfig('production', 'myKey', 'myValue'); + await updateInstanceMetadata('production', 'myKey', 'myValue'); - const result = await getInstanceConfig('production', 'myKey'); + const result = await getInstanceMetadata('production', 'myKey'); expect(result).toBe('myValue'); }); it('should throw NotFoundError for unknown instance', async () => { - await expect(getInstanceConfig('nonexistent', 'key')).rejects.toThrow( + await expect(getInstanceMetadata('nonexistent', 'key')).rejects.toThrow( NotFoundError, ); }); }); - describe('updateInstanceConfig', () => { - it('should set a config value', async () => { + describe('updateInstanceMetadata', () => { + it('should set a metadata value', async () => { await upsertInstance(mockInstance1); - await updateInstanceConfig('production', 'key1', 'value1'); + await updateInstanceMetadata('production', 'key1', 'value1'); - const result = await getInstanceConfig('production', 'key1'); + const result = await getInstanceMetadata('production', 'key1'); expect(result).toBe('value1'); }); - it('should preserve existing config keys', async () => { + it('should preserve existing metadata keys', async () => { await upsertInstance(mockInstance1); - await updateInstanceConfig('production', 'key1', 'value1'); - await updateInstanceConfig('production', 'key2', 'value2'); + await updateInstanceMetadata('production', 'key1', 'value1'); + await updateInstanceMetadata('production', 'key2', 'value2'); - const result1 = await getInstanceConfig('production', 'key1'); - const result2 = await getInstanceConfig('production', 'key2'); + const result1 = await getInstanceMetadata('production', 'key1'); + const result2 = await getInstanceMetadata('production', 'key2'); expect(result1).toBe('value1'); expect(result2).toBe('value2'); }); it('should throw NotFoundError for unknown instance', async () => { await expect( - updateInstanceConfig('nonexistent', 'key', 'value'), + updateInstanceMetadata('nonexistent', 'key', 'value'), ).rejects.toThrow(NotFoundError); }); - it('should remove instance along with its config', async () => { + it('should remove instance along with its metadata', async () => { await upsertInstance(mockInstance1); - await updateInstanceConfig('production', 'key1', 'value1'); + await updateInstanceMetadata('production', 'key1', 'value1'); await removeInstance('production'); const { instances } = await getAllInstances(); expect(instances.find(i => i.name === 'production')).toBeUndefined(); await upsertInstance(mockInstance1); - const result = await getInstanceConfig('production', 'key1'); + const result = await getInstanceMetadata('production', 'key1'); expect(result).toBeUndefined(); }); }); diff --git a/packages/cli-module-auth/src/lib/storage.ts b/packages/cli-module-auth/src/lib/storage.ts index 32c61786962076..07e48271a8c06f 100644 --- a/packages/cli-module-auth/src/lib/storage.ts +++ b/packages/cli-module-auth/src/lib/storage.ts @@ -29,7 +29,7 @@ export type StoredInstance = { issuedAt: number; accessTokenExpiresAt: number; selected?: boolean; - config?: Record; + metadata?: Record; }; const METADATA_FILE = 'auth-instances.yaml'; @@ -46,7 +46,7 @@ const storedInstanceSchema = z.object({ issuedAt: z.number().int().nonnegative(), accessTokenExpiresAt: z.number().int().nonnegative(), selected: z.boolean().optional(), - config: z.record(z.string(), z.unknown()).optional(), + metadata: z.record(z.string(), z.unknown()).optional(), }); const authYamlSchema = z.object({ @@ -168,16 +168,15 @@ export async function setSelectedInstance(name: string): Promise { }); } -export async function getInstanceConfig( +export async function getInstanceMetadata( instanceName: string, key: string, -): Promise { +): Promise { const instance = await getInstanceByName(instanceName); - return instance.config?.[key] as T | undefined; + return instance.metadata?.[key]; } -/** @public */ -export async function updateInstanceConfig( +export async function updateInstanceMetadata( instanceName: string, key: string, value: unknown, @@ -190,7 +189,7 @@ export async function updateInstanceConfig( } data.instances[idx] = { ...data.instances[idx], - config: { ...data.instances[idx].config, [key]: value }, + metadata: { ...data.instances[idx].metadata, [key]: value }, }; await writeAll(data); }); diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index 4aeab07e7baa7e..9c997c02aafea6 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -42,6 +42,7 @@ "commander": "^12.0.0", "fs-extra": "^11.2.0", "pirates": "^4.0.6", + "proper-lockfile": "^4.1.2", "semver": "^7.5.3", "yaml": "^2.0.0", "zod": "^3.25.76 || ^4.0.0" @@ -50,6 +51,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/test-utils": "workspace:^", + "@types/proper-lockfile": "^4", "@types/yarnpkg__lockfile": "^1.1.4" }, "optionalDependencies": { diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index ca10eafff2b856..9f04b80ddf8577 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -91,8 +91,9 @@ export class CliAuth { static create(options?: CliAuthCreateOptions): Promise; getAccessToken(): Promise; getBaseUrl(): string; - getConfig(key: string): Promise; getInstanceName(): string; + getMetadata(key: string): Promise; + setMetadata(key: string, value: unknown): Promise; } // @public diff --git a/packages/cli-node/src/auth/CliAuth.test.ts b/packages/cli-node/src/auth/CliAuth.test.ts index 3d932e5e34b134..65aa340d3385e9 100644 --- a/packages/cli-node/src/auth/CliAuth.test.ts +++ b/packages/cli-node/src/auth/CliAuth.test.ts @@ -180,30 +180,43 @@ describe('CliAuth', () => { }); }); - describe('getConfig', () => { - it('returns a config value from the instance', async () => { - mockStorage.getInstanceConfig.mockResolvedValue([ + describe('getMetadata / setMetadata', () => { + it('returns a metadata value from the instance', async () => { + mockStorage.getInstanceMetadata.mockResolvedValue([ 'catalog', 'scaffolder', ]); const auth = await CliAuth.create(); - const sources = await auth.getConfig('pluginSources'); + const sources = await auth.getMetadata('pluginSources'); expect(sources).toEqual(['catalog', 'scaffolder']); - expect(mockStorage.getInstanceConfig).toHaveBeenCalledWith( + expect(mockStorage.getInstanceMetadata).toHaveBeenCalledWith( 'production', 'pluginSources', ); }); - it('returns undefined for missing config keys', async () => { - mockStorage.getInstanceConfig.mockResolvedValue(undefined); + it('returns undefined for missing metadata keys', async () => { + mockStorage.getInstanceMetadata.mockResolvedValue(undefined); const auth = await CliAuth.create(); - const value = await auth.getConfig('nonexistent'); + const value = await auth.getMetadata('nonexistent'); expect(value).toBeUndefined(); }); + + it('writes a metadata value to the instance store', async () => { + mockStorage.updateInstanceMetadata.mockResolvedValue(undefined); + + const auth = await CliAuth.create(); + await auth.setMetadata('pluginSources', ['catalog']); + + expect(mockStorage.updateInstanceMetadata).toHaveBeenCalledWith( + 'production', + 'pluginSources', + ['catalog'], + ); + }); }); }); diff --git a/packages/cli-node/src/auth/CliAuth.ts b/packages/cli-node/src/auth/CliAuth.ts index d7eaad6d402ecd..d65fb9bbfaf945 100644 --- a/packages/cli-node/src/auth/CliAuth.ts +++ b/packages/cli-node/src/auth/CliAuth.ts @@ -17,13 +17,14 @@ import { type StoredInstance, getSelectedInstance, - getInstanceConfig, + getInstanceMetadata, + updateInstanceMetadata, accessTokenNeedsRefresh, } from './storage'; import { getSecretStore, type SecretStore } from './secretStore'; import { getAuthInstanceService } from './authIdentifiers'; import { httpJson } from './httpJson'; -import { z } from 'zod'; +import { z } from 'zod/v3'; const TokenResponseSchema = z.object({ access_token: z.string().min(1), @@ -104,11 +105,18 @@ export class CliAuth { } /** - * Reads a per-instance configuration value previously stored by the + * Reads a per-instance metadata value previously stored by the * auth module (e.g. `pluginSources`). */ - async getConfig(key: string): Promise { - return getInstanceConfig(this.#instance.name, key); + async getMetadata(key: string): Promise { + return getInstanceMetadata(this.#instance.name, key); + } + + /** + * Writes a per-instance metadata value to the on-disk instance store. + */ + async setMetadata(key: string, value: unknown): Promise { + return updateInstanceMetadata(this.#instance.name, key, value); } async #refreshAccessToken(): Promise { diff --git a/packages/cli-node/src/auth/storage.ts b/packages/cli-node/src/auth/storage.ts index e223cac45d89fe..13a866d058e08b 100644 --- a/packages/cli-node/src/auth/storage.ts +++ b/packages/cli-node/src/auth/storage.ts @@ -15,11 +15,12 @@ */ import { NotFoundError } from '@backstage/errors'; -import fs from 'fs-extra'; +import { promises as fs } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import lockfile from 'proper-lockfile'; import YAML from 'yaml'; -import { z } from 'zod'; +import { z } from 'zod/v3'; const METADATA_FILE = 'auth-instances.yaml'; @@ -35,10 +36,13 @@ const storedInstanceSchema = z.object({ issuedAt: z.number().int().nonnegative(), accessTokenExpiresAt: z.number().int().nonnegative(), selected: z.boolean().optional(), - config: z.record(z.string(), z.unknown()).optional(), + metadata: z.record(z.string(), z.unknown()).optional(), +}); + +const authYamlSchema = z.object({ + instances: z.array(storedInstanceSchema).default([]), }); -/** @public */ export type StoredInstance = { name: string; baseUrl: string; @@ -46,12 +50,17 @@ export type StoredInstance = { issuedAt: number; accessTokenExpiresAt: number; selected?: boolean; - config?: Record; + metadata?: Record; }; -const authYamlSchema = z.object({ - instances: z.array(storedInstanceSchema).default([]), -}); +async function pathExists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} /** @internal */ export function getMetadataFilePath(): string { @@ -67,7 +76,7 @@ export function getMetadataFilePath(): string { /** @internal */ export async function readAll(): Promise<{ instances: StoredInstance[] }> { const file = getMetadataFilePath(); - if (!(await fs.pathExists(file))) { + if (!(await pathExists(file))) { return { instances: [] }; } const text = await fs.readFile(file, 'utf8'); @@ -86,6 +95,29 @@ export async function readAll(): Promise<{ instances: StoredInstance[] }> { } } +async function writeAll(data: { instances: StoredInstance[] }): Promise { + const file = getMetadataFilePath(); + await fs.mkdir(path.dirname(file), { recursive: true }); + const yaml = YAML.stringify(authYamlSchema.parse(data), { indentSeq: false }); + await fs.writeFile(file, yaml, { encoding: 'utf8', mode: 0o600 }); +} + +async function withMetadataLock(fn: () => Promise): Promise { + const file = getMetadataFilePath(); + await fs.mkdir(path.dirname(file), { recursive: true }); + if (!(await pathExists(file))) { + await fs.writeFile(file, '', { encoding: 'utf8', mode: 0o600 }); + } + const release = await lockfile.lock(file, { + retries: { retries: 5, factor: 1.5, minTimeout: 100, maxTimeout: 1000 }, + }); + try { + return await fn(); + } finally { + await release(); + } +} + /** @internal */ export async function getAllInstances(): Promise<{ instances: StoredInstance[]; @@ -129,12 +161,32 @@ export async function getInstanceByName(name: string): Promise { } /** @internal */ -export async function getInstanceConfig( +export async function getInstanceMetadata( instanceName: string, key: string, -): Promise { +): Promise { const instance = await getInstanceByName(instanceName); - return instance.config?.[key] as T | undefined; + return instance.metadata?.[key]; +} + +/** @internal */ +export async function updateInstanceMetadata( + instanceName: string, + key: string, + value: unknown, +): Promise { + return withMetadataLock(async () => { + const data = await readAll(); + const idx = data.instances.findIndex(i => i.name === instanceName); + if (idx === -1) { + throw new NotFoundError(`Instance '${instanceName}' not found`); + } + data.instances[idx] = { + ...data.instances[idx], + metadata: { ...data.instances[idx].metadata, [key]: value }, + }; + await writeAll(data); + }); } /** @internal */ diff --git a/yarn.lock b/yarn.lock index 3996199a391c14..299ee9995b4a70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2828,10 +2828,10 @@ __metadata: dependencies: "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" - "@backstage/cli-module-auth": "workspace:^" "@backstage/cli-node": "workspace:^" "@backstage/errors": "workspace:^" cleye: "npm:^2.3.0" + zod: "npm:^3.25.76 || ^4.0.0" bin: cli-module-actions: bin/backstage-cli-module-actions languageName: unknown @@ -3154,6 +3154,7 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@manypkg/get-packages": "npm:^1.1.3" + "@types/proper-lockfile": "npm:^4" "@types/yarnpkg__lockfile": "npm:^1.1.4" "@yarnpkg/lockfile": "npm:^1.1.0" "@yarnpkg/parsers": "npm:^3.0.0" @@ -3162,6 +3163,7 @@ __metadata: fs-extra: "npm:^11.2.0" keytar: "npm:^7.9.0" pirates: "npm:^4.0.6" + proper-lockfile: "npm:^4.1.2" semver: "npm:^7.5.3" yaml: "npm:^2.0.0" zod: "npm:^3.25.76 || ^4.0.0" From 6cc77e2b5013cca3faf94caa9e79e2aa345764a3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 20:14:53 +0100 Subject: [PATCH 52/55] Drop unreleased cli-module-auth changeset The @backstage/cli-module-auth package is not yet released, so there is no need for a changeset tracking its exports. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/auth-module-exports.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/auth-module-exports.md diff --git a/.changeset/auth-module-exports.md b/.changeset/auth-module-exports.md deleted file mode 100644 index 358f12cf61c40c..00000000000000 --- a/.changeset/auth-module-exports.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli-module-auth': patch ---- - -Export auth helper utilities for use by other CLI modules. From 4d25b4b784bf3720a6a376237e6aaeb8e09ecced Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 20:35:27 +0100 Subject: [PATCH 53/55] Address remaining PR review comments - Fix getAllInstances to handle empty instance array without throwing - Persist updated token expiry timestamps to disk after refresh - Mark internal httpJson helpers as @internal instead of @public Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-auth/src/lib/storage.ts | 3 +++ packages/cli-node/src/auth/CliAuth.test.ts | 4 ++++ packages/cli-node/src/auth/CliAuth.ts | 13 ++++++++----- packages/cli-node/src/auth/httpJson.ts | 4 ++-- packages/cli-node/src/auth/storage.ts | 19 +++++++++++++++++++ 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/cli-module-auth/src/lib/storage.ts b/packages/cli-module-auth/src/lib/storage.ts index 07e48271a8c06f..c5d275149c318b 100644 --- a/packages/cli-module-auth/src/lib/storage.ts +++ b/packages/cli-module-auth/src/lib/storage.ts @@ -96,6 +96,9 @@ export async function getAllInstances(): Promise<{ selected: StoredInstance | undefined; }> { const { instances } = await readAll(); + if (instances.length === 0) { + return { instances: [], selected: undefined }; + } const selected = instances.find(i => i.selected) ?? instances[0]; return { instances: instances.map(i => ({ diff --git a/packages/cli-node/src/auth/CliAuth.test.ts b/packages/cli-node/src/auth/CliAuth.test.ts index 65aa340d3385e9..4b7cb61f5a89c8 100644 --- a/packages/cli-node/src/auth/CliAuth.test.ts +++ b/packages/cli-node/src/auth/CliAuth.test.ts @@ -145,6 +145,10 @@ describe('CliAuth', () => { 'refreshToken', 'new-refresh-token', ); + expect(mockStorage.updateInstance).toHaveBeenCalledWith('production', { + issuedAt: expect.any(Number), + accessTokenExpiresAt: expect.any(Number), + }); }); it('throws when refresh token is missing and access token has expired', async () => { diff --git a/packages/cli-node/src/auth/CliAuth.ts b/packages/cli-node/src/auth/CliAuth.ts index d65fb9bbfaf945..6f1fae221e66ea 100644 --- a/packages/cli-node/src/auth/CliAuth.ts +++ b/packages/cli-node/src/auth/CliAuth.ts @@ -19,6 +19,7 @@ import { getSelectedInstance, getInstanceMetadata, updateInstanceMetadata, + updateInstance, accessTokenNeedsRefresh, } from './storage'; import { getSecretStore, type SecretStore } from './secretStore'; @@ -151,10 +152,12 @@ export class CliAuth { if (token.refresh_token) { await this.#secretStore.set(service, 'refreshToken', token.refresh_token); } - this.#instance = { - ...this.#instance, - issuedAt: Date.now(), - accessTokenExpiresAt: Date.now() + token.expires_in * 1000, - }; + const issuedAt = Date.now(); + const accessTokenExpiresAt = Date.now() + token.expires_in * 1000; + this.#instance = { ...this.#instance, issuedAt, accessTokenExpiresAt }; + await updateInstance(this.#instance.name, { + issuedAt, + accessTokenExpiresAt, + }); } } diff --git a/packages/cli-node/src/auth/httpJson.ts b/packages/cli-node/src/auth/httpJson.ts index 4861a0772232df..832178befb7afb 100644 --- a/packages/cli-node/src/auth/httpJson.ts +++ b/packages/cli-node/src/auth/httpJson.ts @@ -16,7 +16,7 @@ import { ResponseError } from '@backstage/errors'; -/** @public */ +/** @internal */ export type HttpInit = { headers?: Record; method?: string; @@ -24,7 +24,7 @@ export type HttpInit = { signal?: AbortSignal; }; -/** @public */ +/** @internal */ export async function httpJson(url: string, init?: HttpInit): Promise { const res = await fetch(url, { ...init, diff --git a/packages/cli-node/src/auth/storage.ts b/packages/cli-node/src/auth/storage.ts index 13a866d058e08b..c58bd94505c77c 100644 --- a/packages/cli-node/src/auth/storage.ts +++ b/packages/cli-node/src/auth/storage.ts @@ -124,6 +124,9 @@ export async function getAllInstances(): Promise<{ selected: StoredInstance | undefined; }> { const { instances } = await readAll(); + if (instances.length === 0) { + return { instances: [], selected: undefined }; + } const selected = instances.find(i => i.selected) ?? instances[0]; return { instances: instances.map(i => ({ @@ -189,6 +192,22 @@ export async function updateInstanceMetadata( }); } +/** @internal */ +export async function updateInstance( + instanceName: string, + updates: Partial>, +): Promise { + return withMetadataLock(async () => { + const data = await readAll(); + const idx = data.instances.findIndex(i => i.name === instanceName); + if (idx === -1) { + throw new NotFoundError(`Instance '${instanceName}' not found`); + } + data.instances[idx] = { ...data.instances[idx], ...updates }; + await writeAll(data); + }); +} + /** @internal */ export function accessTokenNeedsRefresh(instance: StoredInstance): boolean { // 2 minutes before expiration From 65df9552442e9ff5e3c3f8534cbcd9626b50d37d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 20:53:51 +0100 Subject: [PATCH 54/55] e2e-test: remove React 17 compatibility checks The new frontend system does not support React 17, so the E2E test no longer switches the scaffolded app to React 17 for a second verification pass. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/fix-next-app-e2e-sidebar.md | 5 --- packages/e2e-test/src/commands/runCommand.ts | 46 -------------------- 2 files changed, 51 deletions(-) delete mode 100644 .changeset/fix-next-app-e2e-sidebar.md diff --git a/.changeset/fix-next-app-e2e-sidebar.md b/.changeset/fix-next-app-e2e-sidebar.md deleted file mode 100644 index d5875b32e415cd..00000000000000 --- a/.changeset/fix-next-app-e2e-sidebar.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Fixed the generated next app E2E test to verify the post-sign-in navigation, matching the current default app behavior. diff --git a/packages/e2e-test/src/commands/runCommand.ts b/packages/e2e-test/src/commands/runCommand.ts index 48fd1c862aa0a7..170d1252bfd8fc 100644 --- a/packages/e2e-test/src/commands/runCommand.ts +++ b/packages/e2e-test/src/commands/runCommand.ts @@ -66,20 +66,6 @@ export async function runCommand(opts: OptionValues) { env: { ...process.env, CI: undefined }, }); - await switchToReact17(appDir); - - print(`Running 'yarn install' to install React 17`); - await runOutput(['yarn', 'install'], { cwd: appDir }); - - print(`Running 'yarn tsc' with React 17`); - await runOutput(['yarn', 'tsc'], { cwd: appDir }); - - print(`Running 'yarn test:e2e' with React 17`); - await runOutput(['yarn', 'test:e2e'], { - cwd: appDir, - env: { ...process.env, CI: undefined }, - }); - if ( Boolean(process.env.POSTGRES_USER) || Boolean(process.env.MYSQL_CONNECTION) @@ -412,38 +398,6 @@ async function createPlugin(options: { } } -/** - * Switch the entire project to use React 17 - */ -async function switchToReact17(appDir: string) { - const rootPkg = await fs.readJson(resolvePath(appDir, 'package.json')); - rootPkg.resolutions = { - ...(rootPkg.resolutions || {}), - react: '^17.0.0', - 'react-dom': '^17.0.0', - '@types/react': '^17.0.0', - '@types/react-dom': '^17.0.0', - 'swagger-ui-react/react': '17.0.2', - 'swagger-ui-react/react-dom': '17.0.2', - 'swagger-ui-react/react-redux': '^8', - }; - await fs.writeJson(resolvePath(appDir, 'package.json'), rootPkg, { - spaces: 2, - }); - - await fs.writeFile( - resolvePath(appDir, 'packages/app/src/index.tsx'), - `import '@backstage/cli/asset-types'; -import ReactDOM from 'react-dom'; -import App from './App'; -import '@backstage/ui/css/styles.css'; - -ReactDOM.render(App.createRoot(), document.getElementById('root')); -`, - 'utf8', - ); -} - /** Drops PG databases */ async function dropDB(database: string, client: string) { try { From db4f942f0857d7ca8116000db988014758105690 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 20:54:43 +0100 Subject: [PATCH 55/55] plugins/{app,app-react}: revert API report changes Signed-off-by: Patrik Oldsberg --- plugins/app-react/report.api.md | 4 ++-- plugins/app/report.api.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/app-react/report.api.md b/plugins/app-react/report.api.md index 4e3fff6e42626c..a4e63e254b888d 100644 --- a/plugins/app-react/report.api.md +++ b/plugins/app-react/report.api.md @@ -86,7 +86,7 @@ export const IconBundleBlueprint: ExtensionBlueprint<{ }; output: ExtensionDataRef< { - [x: string]: IconElement | IconComponent; + [x: string]: IconComponent | IconElement; }, 'core.icons', {} @@ -97,7 +97,7 @@ export const IconBundleBlueprint: ExtensionBlueprint<{ dataRefs: { icons: ConfigurableExtensionDataRef< { - [x: string]: IconElement | IconComponent; + [x: string]: IconComponent | IconElement; }, 'core.icons', {} diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index baa3b714b05940..4c9b2a8ce4e400 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -478,7 +478,7 @@ const appPlugin: OverridableFrontendPlugin< icons: ExtensionInput< ConfigurableExtensionDataRef< { - [x: string]: IconElement | IconComponent; + [x: string]: IconComponent | IconElement; }, 'core.icons', {}